Local archive showcase + open-source security hardening - #1
Merged
Conversation
Add a browsable archive (典藏) served by the resident local receiver: each weekly email carries a tokened button to http://127.0.0.1:8787/archive, which lays out every past issue — rebuilt live from the DB, so even email-only issues get a full web view — like a magazine rack. Security foundation (safe under full source disclosure — Kerckhoffs): - server: exact Host allowlist (anti-DNS-rebinding) on all routes incl /capture; /capture requires application/json; typed responders + headersSent guard; /health de-fingerprinted to {ok:true}; testable createBrowstackServer(). - capability token: 256-bit CSPRNG in Keychain (browstack-archive), fail-closed, sha256 + timingSafeEqual, short-TTL cached; k -> cookie(sha256) -> 302, SameSite=Lax. - strict CSP (default-src 'none'; img-src 'self' data:), CORP, nosniff, no-referrer. - numeric-only /issues/:n and /covers/:n (no filename param); findCover exactOnly. - idempotent 0700/0600 data-file hardening on every DB open; ignore .env variants + local db files. - CI (npm ci -> typecheck -> node:test -> grep-gates), CODEOWNERS, SECURITY.md. Archive feature: - render/archive.ts rebuilds past issues from the issue window + issue_items + persisted summaries; render/issueView.ts is shared by preview and archive. - weekly reading digest (render/digest.ts): an editor-LLM distils the week's actual reading into one grounded line naming its subjects — the understanding that precedes the cover prompt. Stored in meta:issue_digest:N; shown as the issue epigraph, the archive-card subtitle, and the email lead-in. Each card also shows deep-read/social counts. - email button injected at send time only (token never written to out/). - npm run archive:open / token:rotate; heartbeat probes /health; install guard rejects a node whose better-sqlite3 ABI won't load (prevents silent crash-loop). - archive section mirrored across all six READMEs; AGENTS.md token + synthetic-sample rules. 18 hermetic security-invariant tests; verified end-to-end on the resident server. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A browsable archive (典藏) served by the resident local receiver. Every weekly email
carries a tokened "Open your archive" button to
http://127.0.0.1:8787/archive, whichlays out every past issue like a magazine rack — rebuilt live from the DB, so even
email-only issues get a full web view. Each issue also gets a weekly reading digest: one
grounded editor-line naming what you actually read that week (the understanding that precedes
the cover), shown as the issue epigraph, the archive-card subtitle, and the email lead-in.
The UX principle throughout: the weekly email is the entry point — a button in the inbox
opens the local showcase; nothing leaves the machine.
Security foundation (open-source: safe under full source disclosure — Kerckhoffs)
Security rests only on a per-install random secret, never on code being hidden.
Hostallowlist on all routes (incl./capture)./capturehardened: requiresContent-Type: application/json(blocks the no-corsdrive-by that could poison the local DB);
/healthde-fingerprinted to{ok:true}.browstack-archive),fail-closed,
sha256 + timingSafeEqual, short-TTL cached;?k→sha256cookie →302,
SameSite=Lax. Never hardcoded/defaulted, never minted in a request handler.default-src 'none'; img-src 'self' data:), CORP, nosniff, no-referrer./issues/:n&/covers/:n; cover viafindCoverexactOnly (never a sibling issue's art).
0700/0600hardening on every DB open;.gitignoreextendedto
.envvariants and local*.db/*.sqlite.npm ci → typecheck → node:test → grep-gates),CODEOWNERSon thesecurity-critical files, and
SECURITY.mddocumenting the threat model + the invariantsabove (please don't "simplify" past them).
Archive feature
render/archive.tsrebuilds past issues from the issue window +issue_items+ persistedsummaries;
render/issueView.tsis shared by preview and archive (one source of truth).render/digest.ts: editor-LLM distils the week's real reading into one line; stored inmeta:issue_digest:N; surfaced in three places. Weekly pipeline now runsingest → enrich → cover → digest → send(digest tolerated).out/).npm run archive:open/npm run token:rotate; heartbeat probes/health; the installscript refuses to pin a node whose
better-sqlite3ABI won't load (prevents a silentcrash-loop of the resident receiver).
AGENTS.mdgains the token + synthetic-sample rules.
Testing
test/security.test.ts) — pass on Node 20 & 22.?k→ cookie → 302 → index/issue/coverserve; 403 without auth; 404 on missing/ traversal; gates active.
Operational note
The resident receiver is launched by launchd with a pinned node path. This branch's install
guard fails loudly (with
npm rebuild better-sqlite3) if that node'sbetter-sqlite3ABIwon't load, rather than crash-looping silently. To pick up new server code,
launchctl kickstart -k gui/$UID/com.browstack.serve(no re-pin) rather than reinstalling from adifferent node.
🤖 Generated with Claude Code