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.
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.tsdefines the floor — CSP,x-content-type-options,referrer-policy,permissions-policy, and HSTS when the public origin is https — andwithFloor()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.tsasserts two of those headers, on two responses (lines 42–43 and 721–722). A future handler that returnsnew Response(...)from somewhere other thanroute()— a new static path, a redirect, a streaming endpoint — loses the floor silently, andAGENTS.mdnames keeping that floor on every response as the bar.What done looks like
SECURITYon 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, aHEAD, and the 500 from the error boundary.SECURITYmust extend the assertion automatically, and removing one must fail loudly.FORGE_APP_URLrather than by hardcoding today's behaviour.cache-control: private, no-storeand thevaryheader are asserted on the HTML responses that carry a visitor's name, andpublic, max-age=31536000, immutableon a static file.bun testpasses, 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 newsrc/floor.test.ts), possibly one exported constant insrc/server.tsso 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.