Cache-Control/Vary hardening and shared-cache defaults#1746
Conversation
- Add Vary: Origin to CORS-enabled responses and preflight (#1518) - Stamp identity-dependent responses (authenticated or 401) with Cache-Control: private, no-cache + Vary: Authorization/Cookie, unless the app opted into shared caching with public/s-maxage (#1565) - New @table(cacheControl: "...") directive / resource static emits a default Cache-Control on anonymous reads; caching tables fall back to public, s-maxage=<expiration> (#1565 converse); persisted on the primary-key attribute like expiration - Fix initSystemSchemaPaths mutating process.env with CLI args (this clobbered real env vars, e.g. AUTHENTICATION_AUTHORIZELOCAL) - Parse boolean-ish env strings for AUTHENTICATION_AUTHORIZELOCAL so 'false' no longer enables the local auth bypass Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Reviewed; no blockers found. |
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oor, type addVaryHeader Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… s-maxage fallback Anonymous readability alone doesn't establish that content is uniformly public (an allowRead gated on IP or headers would leak across a URL-keyed shared cache), so shared-cache headers now require the explicit @table(cacheControl:) / static cacheControl declaration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Marking ready with "Integration Tests 2/6 (Windows)" red: that failure is the @embed-directive suite wholesale timeout, a pre-existing Windows flake on main — same suite/signature red on main runs 29015958195 and 28981465236 (and shard 3/6 on 29050105369). Everything else is green, including the cache-header integration suite this PR adds. — Claude (Opus 4.8) |
|
Companion docs PR: HarperFast/documentation#578 (covers the @table(cacheControl:) directive, the identity-floor/Vary behavior, and the static plugin options from #1748). — Claude (Opus 4.8) |
Ethan-Arrowood
left a comment
There was a problem hiding this comment.
LGTM — the 401-never-opts-in rule and the env-flag parsing fix are both great catches. The Windows red is the known @embed flake (same signature on main).
sent with Claude Fable 5
Fixes #1518 and #1565, plus the converse: declared caching headers so shared caches/CDNs (e.g. Akamai) can safely cache Harper responses.
Summary
#1518 —
Vary: Origin. CORS-enabled responses (default config reflects any Origin into ACAO) now emitVary: Origin— on reflected responses, on preflights, and on no-Origin responses (whose ACAO-less variant is also origin-dependent). Appended to the existingVary, not clobbering it.#1565 — identity floor. In
security/auth.ts → applyResponseHeaders(the choke point all authenticated surfaces pass through): any response where a principal was resolved, or credentials were rejected (401, including the 401→302 login redirect), getsCache-Control: private, no-cache+Vary: Authorization(+Cookiewith sessions). Per RFC 9111, an app-setpublic/s-maxageis the explicit opt-in to shared-caching an authenticated response and is trusted — but never on a 401 (a rejection's non-private Cache-Control is replaced outright). An appCache-Controlwithout a scope directive (e.g. baremax-age=60) gets, privateappended (matters for the cookie-auth path, which shared caches would otherwise store).Converse — declared shared-cache headers for anonymous reads. New
@table(cacheControl: "public, max-age=60")directive (also astatic cacheControlon JS resources), emitted on anonymous GET/HEAD 200/304 responses only. The declaration is required: anonymous readability alone never emits shared-cache headers, because "the anonymous request succeeded" doesn't establish the content is uniformly public — anallowReadgated on request attributes (IP, headers) would leak across a URL-keyed cache. (An earlier revision auto-derivedpublic, s-maxage=<expiration>for caching tables; dropped for this reason.) The value is persisted on the primary-key attribute (likeexpiration) so all threads and future boots see it, and surfaces indescribe_table. Authenticated reads never inherit the table declaration — the identity floor covers them; a resource that wants public-for-authenticated sets the header in code.Two root-cause fixes found along the way (both pre-existing bugs):
initSystemSchemaPaths(lmdbBridge) didObject.assign(process.env, minimist(process.argv))— permanently clobbering real env vars with CLI args (this is whyAUTHENTICATION_AUTHORIZELOCAL=falsein the environment could not beat a--AUTHENTICATION_AUTHORIZELOCAL=trueCLI arg). Now a non-mutating merge.AUTHENTICATION_AUTHORIZELOCAL/DEV_MODEenv strings were used raw, so the string'false'enabled the local auth bypass. Now parsed ('false'/'0'/''→ false, case/whitespace-insensitive).Where to look
security/auth.ts— the floor logic and the 401-never-opts-in rule; also thewasUnauthorizedcapture so the login-redirect 302 keeps the floor.resources/databases.ts—cacheControlpersistence semantics:null= schema explicitly has none (clears on reload),undefined= non-schema caller (add_attribute, cluster schema events — no clobber). This mirrors but slightly diverges from howdescription/hiddenare handled (those clobber; pre-existing).server/REST.ts— anonymous-only emission, gated to 200/304.Deliberate choices a reviewer may want to weigh
Response-like envelopes (e.g. from a source) exit throughfinalizeResponse()before the anonymous-emission block, so the table-level declaration doesn't apply there — the source's own headers win. Believed correct; flagging as reviewed-and-accepted.no-storewas considered and rejected for the floor:private, no-cachekeeps the browser cache + ETag revalidation (Harper already emits ETags) while blocking shared caches.staticplugin currently hardcodessend'spublic, max-age=0) are a separate follow-up PR: configurablemaxAge/immutable/cacheControlpassthrough.Cross-model review (Codex + Gemini + domain pass) ran; all blockers/significant findings were fixed in this diff (401 opt-in bypass, login-redirect escape, regex boundaries, env parsing, opt-out persistence). No open findings.
Tests: 14-case integration suite (
integrationTests/server/http-cache-headers.test.ts) covering all three tiers + unit tests foraddVaryHeader. Existing auth/server/config unit suites pass.Generated by Claude (Opus 4.8) with Kris.