Skip to content

Local archive showcase + open-source security hardening - #1

Merged
howieyoung merged 1 commit into
mainfrom
feat/local-archive
Aug 8, 2026
Merged

Local archive showcase + open-source security hardening#1
howieyoung merged 1 commit into
mainfrom
feat/local-archive

Conversation

@howieyoung

Copy link
Copy Markdown
Owner

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, which
lays 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.

  • Anti-DNS-rebinding: exact Host allowlist on all routes (incl. /capture).
  • /capture hardened: requires Content-Type: application/json (blocks the no-cors
    drive-by that could poison the local DB); /health de-fingerprinted to {ok:true}.
  • Capability token: 256-bit CSPRNG in the macOS Keychain (browstack-archive),
    fail-closed, sha256 + timingSafeEqual, short-TTL cached; ?ksha256 cookie →
    302, SameSite=Lax. Never hardcoded/defaulted, never minted in a request handler.
  • Strict CSP (default-src 'none'; img-src 'self' data:), CORP, nosniff, no-referrer.
  • No arbitrary file reads: numeric-only /issues/:n & /covers/:n; cover via findCover
    exactOnly (never a sibling issue's art).
  • Data-at-rest: idempotent 0700/0600 hardening on every DB open; .gitignore extended
    to .env variants and local *.db/*.sqlite.
  • Guardrails: CI (npm ci → typecheck → node:test → grep-gates), CODEOWNERS on the
    security-critical files, and SECURITY.md documenting the threat model + the invariants
    above (please don't "simplify" past them).

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 (one source of truth).
  • render/digest.ts: editor-LLM distils the week's real reading into one line; stored in
    meta:issue_digest:N; surfaced in three places. Weekly pipeline now runs
    ingest → enrich → cover → digest → send (digest tolerated).
  • Email button injected at send time only (token never written to out/).
  • npm run archive:open / npm run token:rotate; heartbeat probes /health; the install
    script refuses to pin a node whose better-sqlite3 ABI won't load (prevents a silent
    crash-loop of the resident receiver).
  • Archive section mirrored across all six READMEs; AGENTS.md gains the token + synthetic-
    sample rules.

Testing

  • 18 hermetic security-invariant tests (test/security.test.ts) — pass on Node 20 & 22.
  • Verified end-to-end on the live resident server: ?k → cookie → 302 → index/issue/cover
    serve; 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's better-sqlite3 ABI
won'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 a
different node.

🤖 Generated with Claude Code

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>
@howieyoung
howieyoung merged commit 0823d3a into main Aug 8, 2026
1 check passed
@howieyoung
howieyoung deleted the feat/local-archive branch August 8, 2026 11:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant