Each Reader's userscript credential is derived from TOKEN_KEY, their
Discord id and a token epoch (HMAC-SHA256, hex); only its SHA-256 sits in
readers.token_sha256, so install URLs survive restarts while a database
leak yields nothing but hashes. One credential authenticates the script
download path and the API bearer header.
- internal/token: derivation + hashing; migration 0006 adds token_epoch
- seed refreshes the owner's epoch-0 hash only before first rotation
- httpmw.Auth resolves the acting Reader from the credential hash and
stashes it in the request context; the retired API_TOKEN resolves to
the owner until API_TOKEN_GRACE_UNTIL, logged per use, on both the
bearer and script-download paths
- userscript handler renders the bindmounted file with the resolved
Reader's credential substituted for __API_TOKEN__; a legacy-path
request during grace serves the derived credential, so devices
self-migrate on their next update poll
- web UI: Userscripts panel with session-gated install endpoints that
render the script directly (credential never in markup, address bar
or a redirect) and confirm-gated rotation; atomic epoch bump + hash
rewrite in the store
- both userscripts carry __API_TOKEN__ placeholders; the committed
global-token literal is removed (rotating at deploy retires it for
real — it survives in git history)
- env: TOKEN_KEY required, API_TOKEN/API_TOKEN_GRACE_UNTIL retire the
legacy credential; docs and compose updated