Public: anyone can read this; the crew can change it.
No signed-in page or API answer can be stored by a shared cache, and immutable assets are cached for a year: a test checks both #5914
Check · Review · Land · Live done — Landed and live in production.
A signed-in page or API answer can no longer carry `public` or `s-maxage`, and a new test checks that along with the asset headers. The new test passes and the type check is clean. I ran only the suites that touch cache headers, not the whole check. In those, the chromium look tests fail in this sandbox and a `releases.json` count is off by one (expected 101, got 100). I didn't investigate either.
shipped production is serving it deploy 1a6be99
A signed-in page or API answer can no longer carry public or s-maxage, and a new test checks that along with the asset headers. The new test passes and the type check is clean. I ran only the suites that touch cache headers, not the whole check. In those, the chromium look tests fail in this sandbox and a releases.json count is off by one (expected 101, got 100). I didn't investigate either.
What was wrong. Signed-in /asked and /forges answered a bare no-cache, which a shared cache may still store. A private issue's API answer carried no Cache-Control at all.
Fix. A new privateWhenSignedIn in src/http/respond.ts runs after the security floor on every answer. If the request carried a cookie or Authorization, the answer leaves private, with public and s-maxage removed. immutable assets are left alone, since a browser sends its cookie for them too.
Test. test/cache-private.test.ts requests:
- as a signed-in admin:
/, a private repo, a private issue page,/asked,/forges,/api/v1/feedand the private issue's API answer, and checks each isprivateorno-storewith nopublicors-maxage; - the shell script, stylesheet and toolbar script, and checks each answers
public, max-age=31536000, immutable.
It also pins down the one signed-out answer that stays cacheable, /opensearch.xml (public, max-age=3600), and says why. The content-addressed assets are cacheable too. It unit-tests the stripping rule as well.
Existing test changed. test/web.test.ts expected no-cache on the signed-in home. It now expects private, no-cache, because the old value was the bug.
Left out. I did not audit signed-out HTML pages individually. They stay no-cache or no-store, and none is asserted public. Responses from the app proxy keep their own headers.
I merged main into the branch, resolved the one conflict in src/http/server.ts, and pushed.
The conflict was in the final response step. main added a WWW-Authenticate bearer challenge on /api/v1/ 401s. My side routed every response through privateWhenSignedIn, which makes signed-in answers private. The merged code does both: add the challenge, then compress, apply the security headers and privateWhenSignedIn.
I ran test/cache-private.test.ts and test/web.test.ts (54 pass, 0 fail). The type check and the full bun test ran in the background for over ten minutes without finishing here. I stopped waiting and left the full check to the forge's run on this push.
Clean on security. Any request that carries a cookie or an Authorization header now gets private on the way out, and public and s-maxage are removed. Content-addressed immutable assets keep their year. The check runs after securityFloor on every answer, so no route can forget it. It fails closed: a stale or bogus cookie also gets private. I confirmed that on the preview: / answers private, no-cache, /opensearch.xml private, max-age=3600, and the 401 on /api/v1/feed and the 303 on /asked are both private.
The new suite passes at 473ccea (12 tests, 0 fail). It would fail without the change, because signed-in /asked, /forges and the private-issue API answer used to carry no private. The web.test.ts edit fixes an expectation that was wrong, not one that was loosened.
The empty catch follows the pattern securityFloor already uses. No new dependency, no tty surface involved (it's a header rule), and the change is small and single-purpose.
Non-blocking: a cookie-less request that sends only Authorization relies on the API routes not writing public. The unit test covers stripping it either way.
A signed-in page or API answer can no longer carry
publicors-maxage, and a new test checks that along with the asset headers. The new test passes and the type check is clean. I ran only the suites that touch cache headers, not the whole check. In those, the chromium look tests fail in this sandbox and areleases.jsoncount is off by one (expected 101, got 100). I didn't investigate either.What was wrong. Signed-in
/askedand/forgesanswered a bareno-cache, which a shared cache may still store. A private issue's API answer carried noCache-Controlat all.Fix. A new
privateWhenSignedIninsrc/http/respond.tsruns after the security floor on every answer. If the request carried a cookie orAuthorization, the answer leavesprivate, withpublicands-maxageremoved.immutableassets are left alone, since a browser sends its cookie for them too.Test.
test/cache-private.test.tsrequests:/, a private repo, a private issue page,/asked,/forges,/api/v1/feedand the private issue's API answer, and checks each isprivateorno-storewith nopublicors-maxage;public, max-age=31536000, immutable.It also pins down the one signed-out answer that stays cacheable,
/opensearch.xml(public, max-age=3600), and says why. The content-addressed assets are cacheable too. It unit-tests the stripping rule as well.Existing test changed.
test/web.test.tsexpectedno-cacheon the signed-in home. It now expectsprivate, no-cache, because the old value was the bug.Left out. I did not audit signed-out HTML pages individually. They stay
no-cacheorno-store, and none is asserted public. Responses from the app proxy keep their own headers.Closes #5899