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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

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

opened this

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:

  • Path is checked. Each cookie must have exactly one Path, and it must be /. A test that runs the routes checks this on what they return, and the source scan checks it in src/. Two cookies are deliberately narrower and are named in a new COOKIES list 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 checks HttpOnly, SameSite=Lax|Strict, Path, and Secure exactly when the forge is on https.
  • COOKIES lists every cookie written in src/: 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_visitor is 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 removing HttpOnly and changing the Path on the theme cookie; each failed with a message naming the place.
  • The EXEMPT list (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() or dropGift() 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

Closes #11864

commented

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:

  • Path is checked. Each cookie must have exactly one Path, and it must be /. A test that runs the routes checks this on what they return, and the source scan checks it in src/. Two cookies are deliberately narrower and are named in a new COOKIES list 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 checks HttpOnly, SameSite=Lax|Strict, Path, and Secure exactly when the forge is on https.
  • COOKIES lists every cookie written in src/: 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_visitor is 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 removing HttpOnly and changing the Path on the theme cookie; each failed with a message naming the place.
  • The EXEMPT list (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() or dropGift() 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

Changes requested — before the latest push

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:

  1. 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.
  1. 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.
  1. 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)].forEach or use .length.

The preview adds nothing to judge here, since the change is a test file only.

commented

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.ts uses: an org owned by a non-admin placeholder founder, a parent.json pointing at it, and a live link from mintClaim(). It then posts /claim with a password and takes forge_session. It does not repeat the full spawn through the parent's API: test/claim-link.test.ts already 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 in everyday.ts: the new session, and the cleared one.
  • Agent's start: recordStart() seeds a one-time token, then /claim/start/:token/account makes an account on the spot and takes the work. That covers start_claim.ts.
  • Lists: claim.ts, start_claim.ts and everyday.ts move from UNREACHED to WALKED with their exact site counts. UNREACHED now 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

Approved

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs