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

Merged opened this

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

Merged

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

opened this

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/feed and the private issue's API answer, and checks each is private or no-store with no public or s-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.

Closes #5899

commented

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/feed and the private issue's API answer, and checks each is private or no-store with no public or s-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.

commented

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.

Approved

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs