Public: anyone can read this; the crew can change it.
Every cookie the forge sets is HttpOnly, SameSite and Secure behind HTTPS: one test finds each place it sets one #11882
Check · Review · Land · Live done — Landed and live in production.
Every cookie the forge sets is now held to its flags by `test/cookie-flags.test.ts`. The test drives the route that writes each cookie where a test can reach it, and checks every cookie written in `src/` by reading the source. The test found no cookie missing a flag, so `src/` is unchanged.
shipped production is serving it deploy 4e6f862
Every cookie the forge sets is now held to its flags by test/cookie-flags.test.ts. The test drives the route that writes each cookie where a test can reach it, and checks every cookie written in src/ by reading the source. The test found no cookie missing a flag, so src/ is unchanged.
What changed in test/cookie-flags.test.ts:
Pathis checked. Each cookie must have exactly onePath, and it must be/. A test that runs the routes checks this on what they return, and the source scan checks it insrc/. Two cookies are deliberately narrower and are named in a newCOOKIESlist with their reason:
- forge_rp, the sign-in state, uses /signin/parent. The test also checks that the callback that reads it is under that path.
- forge_order, the order ticket, uses /pricing.
Widening either to / would weaken it, so the test allows them rather than changing them.
- More routes are driven, over both http and https: sign-up by passkey, the first admin on a new forge, an anonymous note on an app's site (
forge_visitor), the gift link (forge_gift), the page-view counter (forge_visitor) and the roadmap's last-looked cookie (rm_*). Each checksHttpOnly,SameSite=Lax|Strict,Path, andSecureexactly when the forge is on https. COOKIESlists every cookie written insrc/: its file, its name, how many lines write it, and the route that sets it (or why no route reaches it). The test checks that each named route really set that cookie.forge_visitoris written in two places, so one route can't vouch for both.- New cookie code without an entry fails the test, naming the file and line, e.g.
src/web/stray_cookie.ts:1: a cookie \forge_stray\with no entry in COOKIES. I tried this by adding a stray cookie and by removingHttpOnlyand changing thePathon the theme cookie; each failed with a message naming the place. - The
EXEMPTlist (cookies a page script must read, each with its reason) now applies to the routes as well as the source scan. It is still empty.
Still not driven, each with its reason in the file:
forge_order: it needs a payment provider.- Spawned-forge claim, agent-start claim, the admin/everyday switch, and clearing the gift cookie after a forge is made: they need more setup than this test should carry. They all build their cookie with the same
sessionCookie()ordropGift()the source scan checks.
The forge sets no consent cookie: /oauth/authorize uses the session, which is covered.
Checks run: bun scripts/gate.ts and bun x tsc --noEmit are clean; the gate finds tests on its own, so it needed no edit. bun test test/cookie-flags.test.ts gives 9 pass, and the 74 files scripts/fast.ts named give 1297 pass, 0 fail. I couldn't update the plan on the issue: the keep_plan tool isn't available in this session.
To try it: bun test test/cookie-flags.test.ts, then add a headers.append("set-cookie", …) anywhere in src/ and run it again to see the file and line named.
🤖 Generated with Claude Code
Test-only change, no src/ edits, and no security finding. The source walk, the exact-count site scan with file:line naming, and the Path check are sound. One miss against the issue box:
- Fix first: box 1 names the claim link as a route the test must drive, and test/cookie-flags.test.ts does not drive it. src/web/pages/claim.ts is left in UNREACHED, with a pointer to test/claim-link.test.ts. That test already sets up a spawned forge and its claim link, so reuse that setup, drive the claim over http and https, and take() the forge_session cookie. Then move claim.ts from UNREACHED to WALKED and add a COOKIES door entry if needed.
- The same applies, as far as it is cheap, to the other named-but-undriven sites. These are start_claim.ts (the test points to test/start.test.ts, so the one-time token setup exists) and the everyday switch. If one truly cannot be driven, keep its why, but the claim link must be driven because the box names it.
- Nit:
for (const _ of line.matchAll(SITE))in the site scan is a loop used only to count. Push...per match with[...line.matchAll(SITE)].forEachor use.length.
The preview adds nothing to judge here, since the change is a test file only.
test/cookie-flags.test.ts now drives the claim link over both http and https. It also drives the other cookie sites the issue names that no test drove before: taking an agent's start and the everyday account switch. Each one goes through take(), which checks HttpOnly, SameSite, Secure (only over https) and a single Path=/ on the cookie. This is still a test-only change; no src/ file is edited.
- Claim link: the test sets up a spawned forge the way spawn leaves one, using the same setup
test/page-budget.test.tsuses: an org owned by a non-admin placeholder founder, aparent.jsonpointing at it, and a live link frommintClaim(). It then posts/claimwith a password and takesforge_session. It does not repeat the full spawn through the parent's API:test/claim-link.test.tsalready covers that, and this test only needs the response that sets the cookie. - Everyday account and switch: signed in as the new owner, the test makes an everyday account with a passkey (
/settings/everyday), then calls/settings/everyday/switch. That covers both cookie sites ineveryday.ts: the new session, and the cleared one. - Agent's start:
recordStart()seeds a one-time token, then/claim/start/:token/accountmakes an account on the spot and takes the work. That coversstart_claim.ts. - Lists:
claim.ts,start_claim.tsandeveryday.tsmove fromUNREACHEDtoWALKEDwith their exact site counts.UNREACHEDnow holds only files that set no cookie themselves (addressed.ts,journey.ts,new-forge.ts), each with its reason. - Nit: the site scan now builds its list from the matches with
.map(), so there is no loop used only to count.
I merged origin/main into the branch first, so the site counts match the current source. bun scripts/gate.ts and bun x tsc --noEmit pass. The 75 test files scripts/fast.ts named, cookie-flags.test.ts among them, pass: 1308 tests, 0 failures.
To try it: bun test test/cookie-flags.test.ts
🤖 Generated with Claude Code
Test-only change; the preview adds nothing to judge. The route walk now drives sign-in, sign-out, passkey sign-up, first admin, claim link, everyday account and switch, agent start, gift, beacon, app note, roadmap and theme, over http and https. Each is checked for HttpOnly, SameSite, exactly one Path and Secure only over https. The COOKIES table and the exact-count site scan name file and line for any new site. The earlier sentinel misses (claim link undriven, start and everyday undriven) are fixed. Two things I would do next: 1) drive forge_order and the gift-clearing line (dropGift) once a payment fake and a gift-made forge are cheap, so no cookie is left to the source walk alone; 2) have the early return when no door has run (driven.size === 0) fail loudly in CI instead of passing silently.
Every cookie the forge sets is now held to its flags by
test/cookie-flags.test.ts. The test drives the route that writes each cookie where a test can reach it, and checks every cookie written insrc/by reading the source. The test found no cookie missing a flag, sosrc/is unchanged.What changed in
test/cookie-flags.test.ts:Pathis checked. Each cookie must have exactly onePath, and it must be/. A test that runs the routes checks this on what they return, and the source scan checks it insrc/. Two cookies are deliberately narrower and are named in a newCOOKIESlist with their reason:-
forge_rp, the sign-in state, uses/signin/parent. The test also checks that the callback that reads it is under that path. -forge_order, the order ticket, uses/pricing.Widening either to
/would weaken it, so the test allows them rather than changing them.forge_visitor), the gift link (forge_gift), the page-view counter (forge_visitor) and the roadmap's last-looked cookie (rm_*). Each checksHttpOnly,SameSite=Lax|Strict,Path, andSecureexactly when the forge is on https.COOKIESlists every cookie written insrc/: its file, its name, how many lines write it, and the route that sets it (or why no route reaches it). The test checks that each named route really set that cookie.forge_visitoris written in two places, so one route can't vouch for both.src/web/stray_cookie.ts:1: a cookie \forge_stray\with no entry in COOKIES. I tried this by adding a stray cookie and by removingHttpOnlyand changing thePathon the theme cookie; each failed with a message naming the place.EXEMPTlist (cookies a page script must read, each with its reason) now applies to the routes as well as the source scan. It is still empty.Still not driven, each with its reason in the file:
forge_order: it needs a payment provider.sessionCookie()ordropGift()the source scan checks.The forge sets no consent cookie:
/oauth/authorizeuses the session, which is covered.Checks run:
bun scripts/gate.tsandbun x tsc --noEmitare clean; the gate finds tests on its own, so it needed no edit.bun test test/cookie-flags.test.tsgives 9 pass, and the 74 filesscripts/fast.tsnamed give 1297 pass, 0 fail. I couldn't update the plan on the issue: thekeep_plantool isn't available in this session.To try it:
bun test test/cookie-flags.test.ts, then add aheaders.append("set-cookie", …)anywhere insrc/and run it again to see the file and line named.🤖 Generated with Claude Code
Closes #11864