Per-Reader userscript credential #24

Closed
opened 2026-08-08 06:06:28 +07:00 by sulthan · 1 comment
Owner

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

  • Each Reader has a high-entropy random token, stored only as a SHA-256 hash. A fast hash is correct here: these are random tokens with nothing to brute-force, so a password hash would only add per-request cost.
  • Both userscripts are rendered at serve time with the requesting Reader's token substituted. No token literal is committed to the repository.
  • One token authenticates both the script download path and the API bearer header — anyone who can read the download URL can fetch the script and read the token, so two secrets would have identical blast radius at twice the code.
  • The web UI offers install links for both Libraries and the Reader never sees the token itself.
  • An installed script continues to auto-update from the same per-Reader path.
  • Rotation issues a new token, invalidates the old one immediately, and warns clearly that every device must reinstall — otherwise devices stop updating silently.
  • Token comparison is constant-time and performed against the stored hash.
  • For 14 days the retired global token resolves to the owner's Reader. The expiry is enforced in code, not a note in a runbook, and its use is logged so the window can be confirmed empty before removal.
  • A request bearing one Reader's token cannot read or write another Reader's Bookmarks.

Blocked by

#23

## 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 - [x] Each Reader has a high-entropy random token, stored only as a SHA-256 hash. A fast hash is correct here: these are random tokens with nothing to brute-force, so a password hash would only add per-request cost. - [x] Both userscripts are rendered at serve time with the requesting Reader's token substituted. No token literal is committed to the repository. - [x] One token authenticates both the script download path and the API bearer header — anyone who can read the download URL can fetch the script and read the token, so two secrets would have identical blast radius at twice the code. - [x] The web UI offers install links for both Libraries and the Reader never sees the token itself. - [x] An installed script continues to auto-update from the same per-Reader path. - [x] Rotation issues a new token, invalidates the old one immediately, and warns clearly that every device must reinstall — otherwise devices stop updating silently. - [x] Token comparison is constant-time and performed against the stored hash. - [x] For 14 days the retired global token resolves to the owner's Reader. The expiry is enforced in code, not a note in a runbook, and its use is logged so the window can be confirmed empty before removal. - [x] A request bearing one Reader's token cannot read or write another Reader's Bookmarks. ## Blocked by #23
sulthan added the ready-for-agent label 2026-08-08 06:06:28 +07:00
Author
Owner

Landed on feat/per-reader-userscript-credential (commit 8f752ed), 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 in readers.token_sha256 (new token_epoch column, 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_TOKEN still resolves to the owner until API_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):

  1. Add TOKEN_KEY (openssl rand -hex 32) — required; changing it later invalidates every credential.
  2. Keep API_TOKEN + set API_TOKEN_GRACE_UNTIL (e.g. 2026-08-22) for the 14-day window.
  3. After deploy, sign in → Userscripts → reinstall both scripts on every device (this also retires the old global credential for good).

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).

Landed on `feat/per-reader-userscript-credential` (commit 8f752ed), 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 in `readers.token_sha256` (new `token_epoch` column, 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_TOKEN` still resolves to the owner until `API_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):** 1. Add `TOKEN_KEY` (`openssl rand -hex 32`) — required; changing it later invalidates every credential. 2. Keep `API_TOKEN` + set `API_TOKEN_GRACE_UNTIL` (e.g. 2026-08-22) for the 14-day window. 3. After deploy, sign in → Userscripts → reinstall both scripts on every device (this also retires the old global credential for good). **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).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sulthan/mangaBookmark#24