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:
@@ -243,6 +243,53 @@ button { cursor: pointer; }
|
||||
/* The label is 15px tall by design; the thumb gets 44 without moving it. */
|
||||
.ghost::after { content: ""; position: absolute; inset: -15px -12px; }
|
||||
|
||||
/* ---- userscript setup: collapsed by default, one hairline, no card ---- */
|
||||
.setup {
|
||||
margin: 0 20px;
|
||||
padding: 12px 0 0;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
color: var(--mute);
|
||||
}
|
||||
.setup summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 44px;
|
||||
padding: 0;
|
||||
font: 500 10px/1 var(--font-mono);
|
||||
letter-spacing: .2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--mute-2);
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
.setup summary::-webkit-details-marker { display: none; }
|
||||
.setup summary:hover { color: var(--paper); }
|
||||
.setup[open] { padding-bottom: 16px; }
|
||||
.setup-copy {
|
||||
margin: 0;
|
||||
padding: 4px 0 12px;
|
||||
font: 14px/1.55 var(--font-body);
|
||||
color: var(--mute);
|
||||
}
|
||||
.setup-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 20px;
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
.setup-links .ghost { font-size: 11px; }
|
||||
.setup-rotate { margin: 0; }
|
||||
/* Rotation confirmation: the one hot state the panel wears, and it is
|
||||
destruction, not new-chapter signal — danger, never ember. */
|
||||
.setup-warn {
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--danger);
|
||||
color: var(--danger);
|
||||
font: 500 12px/1.5 var(--font-mono);
|
||||
letter-spacing: .04em;
|
||||
}
|
||||
|
||||
.chrome { display: flex; flex-direction: column; }
|
||||
|
||||
.searchbar {
|
||||
|
||||
Reference in New Issue
Block a user