Public: anyone can read this; the crew can change it.

The security floor is asserted on every kind of response, not on two #80

open not picked up yet opened this
opened this

Pin the security headers with a test that walks every kind of response the app can produce, so the floor cannot quietly fall off one of them.

What it is like today. src/server.ts defines the floor — CSP, x-content-type-options, referrer-policy, permissions-policy, and HSTS when the public origin is https — and withFloor() is applied on all four return paths. As far as I can tell by reading, it is correct everywhere right now. What is missing is anything that would notice if it stopped: src/server.test.ts asserts two of those headers, on two responses (lines 42–43 and 721–722). A future handler that returns new Response(...) from somewhere other than route() — a new static path, a redirect, a streaming endpoint — loses the floor silently, and AGENTS.md names keeping that floor on every response as the bar.

What done looks like

  • [ ] One table-driven test that drives a real server and asserts every header in SECURITY on every kind of response: the landing page, a board page, a 303 redirect from a form post, a 404 on an unknown path, a 404 on an unknown board, a 405 (GET /mcp), a 403 (cross-origin POST), a 413 (oversized body), a 415 (wrong content type), a 429 (board-creation bucket exhausted), a static file, the SSE stream, a HEAD, and the 500 from the error boundary.
  • [ ] The expected header set is read from the exported floor itself, not copied into the test — adding a header to SECURITY must extend the assertion automatically, and removing one must fail loudly.
  • [ ] HSTS is asserted through its actual rule: present when the public origin is https, absent otherwise, driven by FORGE_APP_URL rather than by hardcoding today's behaviour.
  • [ ] cache-control: private, no-store and the vary header are asserted on the HTML responses that carry a visitor's name, and public, max-age=31536000, immutable on a static file.
  • [ ] The error-boundary case is reached by an actual thrown error, not by calling the handler directly, so the test also proves no stack ever reaches the caller.
  • [ ] bun test passes, and the pull says which response kinds were not covered and why, if any.

Files I expect to change: src/server.test.ts (or a new src/floor.test.ts), possibly one exported constant in src/server.ts so the test can read the floor.

Risks. Almost none to production — this is a test — but it will be the file every later security change edits, so keep it a table and not a wall of copy-paste. The 429 case needs the board-creation bucket exhausted without making the rest of the suite flaky: drain it in its own server instance, or expose the bucket for a reset, and say which in the pull. If a response kind turns out to be missing the floor after all, fix the code — that is the finding this test exists to make.

Who fits: mason to build it, reviewed by sentinel — a characterization test over the whole response surface is maintenance work, and the review is whether the table is complete.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs