feat(backend): per-Reader userscript credential with UI install and rotation (#24)

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
This commit is contained in:
2026-08-08 09:34:52 +07:00
parent bcc6b45515
commit 8f752ed86b
25 changed files with 1149 additions and 234 deletions
+3 -3
View File
@@ -4,8 +4,8 @@
// @version 1.0.0
// @description Track read progress on NovelFull & LightNovelWorld and sync to a self-hosted backend. Bromite-compatible (no GM_* APIs).
// @author you
// @downloadURL https://bookmark-api.violetcrown.my.id/u/40d79969b5442f90df4fe306a092c7c50e7b4a7a98099f98cc398f4fb374b1df/novel-bookmark.user.js
// @updateURL https://bookmark-api.violetcrown.my.id/u/40d79969b5442f90df4fe306a092c7c50e7b4a7a98099f98cc398f4fb374b1df/novel-bookmark.user.js
// @downloadURL https://bookmark-api.violetcrown.my.id/u/__API_TOKEN__/novel-bookmark.user.js
// @updateURL https://bookmark-api.violetcrown.my.id/u/__API_TOKEN__/novel-bookmark.user.js
// @match https://novelfull.com/*
// @match https://lightnovelworld.net/*
// @run-at document-idle
@@ -19,7 +19,7 @@
// CONFIG — fill these in before installing.
// ============================================================
const API_BASE = "https://bookmark-api.violetcrown.my.id"; // your backend origin, no trailing slash
const API_TOKEN = "40d79969b5442f90df4fe306a092c7c50e7b4a7a98099f98cc398f4fb374b1df"; // must equal backend API_TOKEN
const API_TOKEN = "__API_TOKEN__"; // substituted by the backend at serve time (issue #24)
const WEB_BASE = "https://bookmark.violetcrown.my.id"; // the browser UI, for the panel's nav chips
// This script owns the novel library; the manga script is a separate install