Per-Reader userscript credential #24
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Parent
#18
What to build
A Reader installs their userscript by clicking one button in the web UI and one Install in Violentmonkey. They never see a token, never type one, never copy one — the backend renders the script with their own credential already inside it.
Also carries the 14-day compatibility path that keeps already-installed scripts working across the cutover.
Acceptance criteria
Blocked by
#23
Landed on
feat/per-reader-userscript-credential(commit8f752ed), reviewed on both axes (standards + spec) before commit. All 9 acceptance criteria met.Design: derived credentials, not stored random. Each Reader's credential is HMAC-SHA256(
TOKEN_KEY, discord_id, token_epoch), hex-encoded; only its SHA-256 sits inreaders.token_sha256(newtoken_epochcolumn, migration 0006). The server must rebuild install URLs after restarts while the DB holds only hashes — a stored-random token with no plaintext copy would be unreconstructible, and in-memory plaintext would break install links on every restart. HMAC output is high-entropy and unbrute-forceable; the AC's intent (unguessable, DB-leak-proof) is met. Rotation = atomic epoch bump + hash rewrite.Cutover (grace window).
API_TOKENstill resolves to the owner untilAPI_TOKEN_GRACE_UNTIL(enforced in code, logged per use — verified in logs), on BOTH the bearer path and the script download path. A legacy-path update poll is served a copy carrying the Reader's derived credential, so installed devices self-migrate on their next auto-update instead of dying silently at the deadline.Web UI. 'Userscripts' panel (collapsible, under the chrome): one install link per library, served session-gated with the credential already inside — it never appears in page markup, the address bar, or a redirect. Rotation is confirm-gated (hx-confirm) and answers with a reinstall warning; old credential dies immediately (asserted 401/404 in tests).
Deploy steps (in DEPLOY.md / .env.example):
TOKEN_KEY(openssl rand -hex 32) — required; changing it later invalidates every credential.API_TOKEN+ setAPI_TOKEN_GRACE_UNTIL(e.g. 2026-08-22) for the 14-day window.Security note: the old global token literal was committed to this repo's history (present since
0ef5286). Removing it from HEAD does not scrub history — rotating via the web UI after deploy is what actually kills it.Tests: full Go suite green (real Postgres per test), userscript JS suite 45/45, plus a live smoke of the built binary (grace acceptance, derived auth, script substitution, restart resilience).