Skip to content

Deferred hardening: /capture secret, ja/ko/es/fr UI, compiled dist automation - #6

Merged
howieyoung merged 3 commits into
mainfrom
feat/deferred-hardening
Aug 8, 2026
Merged

Deferred hardening: /capture secret, ja/ko/es/fr UI, compiled dist automation#6
howieyoung merged 3 commits into
mainfrom
feat/deferred-hardening

Conversation

@howieyoung

Copy link
Copy Markdown
Owner

Closes the three items we deferred from the archive/security work. Each is an independent commit.

POST /capture per-install secret (46ede54)

The extension's landing endpoint was gated only by the Host + Content-Type checks. Adds a per-install capability secret (Keychain browstack-capture, sha256 + timingSafeEqual, fail-closed) that the extension presents on every capture. Mirrors the archive-token design. scripts/build-ext.mjs mints/reads the secret and bakes it into the bundle at build time; a new security gate asserts the comparison stays constant-time.

Breaking for extension users: requires npm run build:ext + reloading the unpacked extension, or captures return 401.

② UI locales ja / ko / es / fr (8e7b5da)

Extends the i18n.ts UI string table with Japanese, Korean, Spanish, and French, using Intl.DateTimeFormat for locale-correct dates. Content is still generated in the reading language; image prompts stay English.

③ Compiled dist/ under a runtime-resolved node (ba4b1e4)

The LaunchAgents ran tsx off a node path baked in at install time — a node upgrade orphaned the path, and better-sqlite3's native ABI could drift and crash-loop the resident receiver.

  • tsconfig.build.json + npm run build emit runnable dist/.
  • scripts/run-with-node.sh: agents exec through a wrapper that resolves node at runtime.
  • install-weekly.mjs: builds + npm rebuild better-sqlite3 before install; two preflights refuse to install if the target node can't load better-sqlite3 or the jsdom-backed compiled pipeline. Serve plist runs dist/server.js.
  • weekly.mjs: runs compiled dist/ entries via the resolved node. jsdom's tree does require(ESM) (unflagged only on Node 20.19 / 22.0+); on 20.17–20.18 or 21.x it opts in with --experimental-require-module, gated on the resolved node's version.
  • engines.node>=20.17; AGENTS.md documents the run model (editing src/ needs npm run build to reach the automation).

Deploy step: re-run npm run schedule:weekly to rebuild + reinstall the agents on the compiled path.

Verification

  • npm run build, npm run typecheck, npm run security-gates green; 20/20 tests pass (node 20).
  • server.js loads flag-free under node 20.17; compiled cli.js and render/email.js (the jsdom path) load and run with the version-gated flag; the email render produced issue №6 (3 deep reads, 4 social echoes) without sending.

Not auto-merged

Merge and the two deploy steps above are left for you.

🤖 Generated with Claude Code

howieyoung and others added 3 commits August 8, 2026 22:38
Add full UIStrings sets for ja/ko/es/fr to the table (previously they fell back
to English). Dates use Intl.DateTimeFormat per locale; labels, signal/device
strings, figures, colophon, archive, and email subject are translated. en and
zh-TW unchanged. Resolves the "English content inside English chrome" gap for
non-CJK/non-English readers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Host + Content-Type gates block a cross-origin webpage drive-by, but that rests
entirely on browser CORS behaviour and does not stop a non-browser local process
(e.g. another OS user on a shared Mac) from POSTing fabricated captures. Add a
per-install shared secret: the extension holds it (baked in at `npm run build:ext`
via an esbuild define, read from the Keychain `browstack-capture`) and sends it as
`X-Browstack-Token`; the server validates it constant-time and fails closed.

- src/captureSecret.ts: Keychain-backed secret (get/ensure/cached/check), sha256 + timingSafeEqual.
- server.ts: /capture requires a valid token after the Host and content-type gates (401 otherwise);
  createBrowstackServer accepts an injectable getCaptureSecret for tests.
- extension/background.ts: sends X-Browstack-Token (a compile-time __BROWSTACK_CAPTURE_TOKEN__).
- scripts/build-ext.mjs: provisions the secret and injects it via `--define`; dist/ stays gitignored.
- tests + grep-gate updated.

BREAKING for extension users: after upgrading, run `npm run build:ext`, reload the unpacked
extension in chrome://extensions, and kickstart the receiver — otherwise the old bundle's
POSTs are rejected (401). Chrome-history ingest and non-extension flows are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The LaunchAgents ran tsx off a node path baked in at install time. Two
fragilities: a node upgrade orphaned the baked path, and better-sqlite3's
native ABI could drift from the running node and crash-loop the resident
receiver.

- tsconfig.build.json + `npm run build`: emit runnable dist/.
- scripts/run-with-node.sh: LaunchAgents exec through a wrapper that
  resolves node at runtime (launchd PATH lists the install-time node dir
  first), so a node upgrade no longer strands a stale path.
- install-weekly.mjs: build + `npm rebuild better-sqlite3` before install,
  and a second preflight that refuses to install if the target node can't
  load the jsdom-backed compiled pipeline (fail loud now, not silently
  every Saturday). Serve plist now runs dist/server.js, not tsx.
- weekly.mjs: run compiled dist/ entries via the resolved node, no npm/tsx
  indirection. jsdom's dep tree does require(ESM), unflagged only on Node
  20.19 / 22.0+; on 20.17-20.18 or 21.x it opts in with
  --experimental-require-module, gated on the resolved node's version so a
  future node that drops the flag isn't handed it.
- engines.node >= 20.17 (the floor where the flag exists); AGENTS.md gains
  the run-model note: editing src/ has no effect on the automation until
  `npm run build`.

Interactive `npm run <ingest|serve|...>` still use tsx; only the installed
automation runs compiled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@howieyoung
howieyoung merged commit c814fa0 into main Aug 8, 2026
1 check passed
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