Files
Miles WardandClaude Opus 4.8 f33effe20a v0.1: self-hosted TOTP authenticator with live Discord delivery
Rust workspace serving a Google/Microsoft-Authenticator-style web UI, with
each account's rotating code mirrored into Discord as a message that
rewrites itself in place as the code rolls.

The live mechanism is webhook message editing: POST ?wait=true returns the
message id, PATCH .../messages/{id} rewrites it thereafter. Two approaches
were evaluated and rejected — an externally-hosted image URL is cached by
Discord's media proxy and, worse, must be anonymously reachable, which would
publish live codes on an unauthenticated endpoint; and an animated countdown
drifts out of phase because playback starts at decode time, not on a wall
clock. The countdown instead uses Discord's <t:unix:R> markup, which the
client re-renders every second for free.

Codes are also sent as text in a fenced block so they stay copyable, with
the grouped, readable form on a rendered WebP tile.

- core: RFC 6238 TOTP over HMAC, lenient otpauth:// parsing (issuer conflicts
  and non-canonical base32 both occur in the wild), XChaCha20-Poly1305 vault,
  Argon2id admin auth
- discord: webhook client with a serialising rate limiter (~2 req/s) so
  globally-aligned rollovers don't burst, and error classification that
  distinguishes a deleted message (repost) from a dead webhook (stop)
- render: SVG -> WebP tiles via resvg with embedded JetBrains Mono
- http-api: axum, session auth, QR decode with a rescale/contrast/rotation
  retry ladder
- webui: offline vanilla JS, Beautiful UI design language, light and dark

Ships as a 27 MB Alpine image (about 8 MB to pull); the binary is fully static musl with no
OpenSSL and no C dependencies, cross-compiled via cargo-zigbuild so building
linux/amd64 on Apple Silicon needs no QEMU.

101 tests, including the RFC 6238 vectors, QR round-trips, and the live push
cycle exercised against a mock Discord.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-28 16:06:08 -04:00

32 lines
1.3 KiB
Bash
Executable File

#!/bin/sh
# Fix bind-mount ownership, then drop privileges.
#
# Bind-mounted volumes arrive owned by whatever the host says — on Unraid
# that is typically nobody:users (99:100), and on a plain `docker run -v`
# it is usually root:root. The service runs as uid 10001 and could not
# otherwise write the vault, so when we start as root we chown the data
# directory first and only then hand off.
#
# When the container is already running as non-root (some platforms assign
# their own uid), we skip the chown and exec directly — there is nothing
# useful we could do about permissions from there anyway.
set -e
DA_UID=${DA_UID:-10001}
DA_GID=${DA_GID:-10001}
DATA_DIR=${DA_WORK_DIR:-/var/lib/discord-authenticator}
if [ "$(id -u)" = "0" ]; then
mkdir -p "$DATA_DIR"
chown -R "${DA_UID}:${DA_GID}" "$DATA_DIR" 2>/dev/null || true
# The vault key is the one file that must never be group- or
# world-readable; a permissive umask on the host can otherwise leave it
# readable to anyone who can browse the appdata share.
if [ -f "$DATA_DIR/vault.key" ]; then
chmod 600 "$DATA_DIR/vault.key" 2>/dev/null || true
fi
exec su-exec "${DA_UID}:${DA_GID}" /usr/local/bin/discord-authenticator "$@"
fi
exec /usr/local/bin/discord-authenticator "$@"