Every response in this app stands on the floor in src/http.ts, and the floor has no cache directive in it. GET / is rendered per person — it carries who.name, the sign-out link, the notice cookie's toast, and with #6 a guest session — and production answers it with no cache-control and no vary at all. Nothing in front of us caches HTML today; that is a default, not a rule, and the rule is the thing this app keeps.
Checked on production (df130d5): GET / returns content-security-policy, x-content-type-options, referrer-policy, permissions-policy and strict-transport-security, and no cache header of any kind.
Done looks like
[ ] The floor gains a cache directive: html(), json(), text() and empty() in src/http.ts carry one by default, so a door cannot forget it by existing.
[ ] The directive is chosen and the choice is written in the pull: private, no-store is the strongest and costs Chrome's back-forward cache; private, no-cache keeps bfcache and still stops a shared cache serving one person's page to the next. Either is acceptable; picking one without saying why is not.
[ ] A genuinely public, identity-free response can opt out through the same one argument the callers already pass headers through — and the only one that does today says so in a comment.
[ ] Identity-varying responses carry vary: cookie, x-forge-identity as well, so a cache that honours vary but not private still keys on the right thing.
[ ] Unchanged and asserted rather than assumed: staticFile/bytes keep public, max-age=31536000, immutable when linked by content (asset) and stay short-lived without ?v; eventStream keeps no-store.
[ ] A test in the shape of the existing floor test walks every route in routes.ts and fails if a response carries no cache directive — the streaming and image doors included, and HEAD answering as GET.
[ ] AGENTS.md's security floor gains the one clause, so the next twelve doors inherit it.
Filessrc/http.ts, src/server.test.ts, AGENTS.md.
Risksno-store on / disables bfcache, which for a game page that reopens a socket on every restore is a real cost — hence the choice above. Do not put a directive on /card.png and /icon-180.png that stops a chat client caching them; they are public by design and have no identity in them.
Fits Vesper's brief: it is the floor, and the floor is one file.
Every response in this app stands on the floor in
src/http.ts, and the floor has no cache directive in it.GET /is rendered per person — it carrieswho.name, the sign-out link, the notice cookie's toast, and with #6 a guest session — and production answers it with nocache-controland novaryat all. Nothing in front of us caches HTML today; that is a default, not a rule, and the rule is the thing this app keeps.Checked on production (df130d5):
GET /returnscontent-security-policy,x-content-type-options,referrer-policy,permissions-policyandstrict-transport-security, and no cache header of any kind.Done looks like
html(),json(),text()andempty()insrc/http.tscarry one by default, so a door cannot forget it by existing.private, no-storeis the strongest and costs Chrome's back-forward cache;private, no-cachekeeps bfcache and still stops a shared cache serving one person's page to the next. Either is acceptable; picking one without saying why is not.vary: cookie, x-forge-identityas well, so a cache that honoursvarybut notprivatestill keys on the right thing.staticFile/byteskeeppublic, max-age=31536000, immutablewhen linked by content (asset) and stay short-lived without?v;eventStreamkeepsno-store.routes.tsand fails if a response carries no cache directive — the streaming and image doors included, andHEADanswering asGET.AGENTS.md's security floor gains the one clause, so the next twelve doors inherit it.Files
src/http.ts,src/server.test.ts,AGENTS.md.Risks
no-storeon/disables bfcache, which for a game page that reopens a socket on every restore is a real cost — hence the choice above. Do not put a directive on/card.pngand/icon-180.pngthat stops a chat client caching them; they are public by design and have no identity in them.Fits Vesper's brief: it is the floor, and the floor is one file.