3 Commits
Author SHA1 Message Date
Miles WardandClaude Opus 4.8 7751ebe9b3 v0.1.2: working Edit sheet, centred search, brand assets
Fixes a bug that shipped in v0.1: the per-account Edit sheet never opened at
all. Its Cancel button used `onclick: dlg.close`, evaluated while building the
arguments to openModal() and therefore before `const dlg` was initialised — a
temporal-dead-zone ReferenceError that aborted the whole handler. Every other
button in that sheet was already a closure, which is why it went unnoticed:
browser testing had covered add, import and settings, but never opened this
sheet.

Renames now reach Discord almost immediately. The push loop skips any account
whose current code it has already sent, so a rename previously stayed invisible
until the code next rolled — up to a full period. update() now clears the
last-sent code when the naming actually changes, so the next tick treats the
tile as stale and rewrites it in about a second. Toggling push alone is not a
rename and still costs nothing.

- webui: pencil icon and "Edit account" title so the row action reads as edit
  rather than settings; both fields labelled, with the second one explained as
  the username/email and left untouched unless changed
- webui: topbar is a three-column grid so the search sits on the same centre
  line as the account tiles. Below 820px there isn't room for brand + 420px
  search + actions, so it falls back to a row rather than clipping the
  wordmark; the brand now reads "Discord Authenticator" in full
- webui: the add-account FAB pins to the viewport corner instead of tracking
  the content column edge
- brand: docs/brand/icon.svg plus PNGs rendered from it by da-render, so the
  README image, Gitea avatar and Unraid template icon all derive from one
  source rather than drifting. Unraid's <Icon> now points at a PNG, which it
  renders more reliably than SVG

126 tests (was 124).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-31 01:42:18 -04:00
Miles WardandClaude Opus 4.8 eecdacd890 v0.1.1: import Google Authenticator exports
Google's "Transfer accounts -> Export accounts" produces an
otpauth-migration:// link wrapping a base64 protobuf that holds many accounts
at once, and splits large exports across several QR codes. v0.1 rejected these
outright; now they import.

The protobuf is decoded by hand rather than pulling prost: the schema is four
messages that have not changed since 2020, and the wire format needed is two
types (varint, length-delimited). Unknown fields are skipped properly, so a
future Google addition degrades rather than corrupting the parse.

Two details in this format silently produce plausible-but-wrong codes if taken
backwards, so both are pinned by tests: the `secret` field is RAW BYTES, not
base32, and the payload carries NO period field -- every Google export is
30-second TOTP.

- core/migration.rs: protobuf decode, lenient base64 (all four alphabet and
  padding spellings), issuer reconciliation between the `name` prefix and the
  dedicated `issuer` field, and batch metadata surfaced as "Part 1 of 2"
- vault: add_many() with duplicate detection, persisting once per batch rather
  than once per account; duplicates match on secret AND naming, so keeping one
  secret under two labels stays possible
- http-api: preview returns a list flagged with what is already stored; import
  takes the link plus selected indices and re-parses server-side, so secrets
  never travel out to the browser and back. /api/qr/decode now returns every
  payload in an image, not just the first
- webui: multi-account import checklist with select all/none, per-entry
  algorithm and digit chips, already-present entries pre-unticked, and skipped
  entries named with their reason (HOTP, MD5) instead of silently dropped

Verified end to end against real QR images: upload -> decode -> preview ->
import, with the resulting codes cross-checked against an independent RFC 6238
implementation, including an 8-digit account.

124 tests (was 101).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-31 00:44:59 -04:00
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