Skip to content

fix(api): restrict bootstrap shared caching#4499

Merged
koala73 merged 2 commits into
mainfrom
codex/issue-4495-bootstrap-cache
Jun 28, 2026
Merged

fix(api): restrict bootstrap shared caching#4499
koala73 merged 2 commits into
mainfrom
codex/issue-4495-bootstrap-cache

Conversation

@koala73

@koala73 koala73 commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Summary

  • Restricts successful /api/bootstrap shared caching to auth.kind === "public-weather" only.
  • Keeps anonymous GET /api/bootstrap?keys=weatherAlerts cacheable while making session/user/enterprise bootstrap successes no-store.
  • Adds a session-auth bootstrap regression test to the existing cache/auth matrix.

Intent

Fixes #4495.

The remaining cache boundary after the bootstrap auth hardening was that non-key success kinds could still inherit public/CDN cache headers. This PR makes the shared-cache exception explicit and narrow: anonymous weather bootstrap only.

Non-goals:

  • No changes to bootstrap key selection or payload shape.
  • No UI changes.
  • No changes to API-key validation semantics.

Validation Matrix

Check Reason Result
node --test api/bootstrap-auth.test.mjs api/bootstrap-cors.test.mjs api/_api-key.test.mjs Focused bootstrap/auth/session cache matrix Pass: 47 tests
TMPDIR=/tmp node --import tsx --test tests/embed-public-data-auth.test.mts Public/anonymous bootstrap exception guard Pass: 3 tests
npm run typecheck:api API typecheck and Convex call audit Pass
npm run test:sidecar Broader API/sidecar auth contract suite Pass: 163 tests
git diff --check Diff hygiene Pass
git push -u origin HEAD pre-push hook Repo pre-push gates including typecheck, API typecheck, boundary lint, safe HTML, rate-limit policy, premium-fetch parity, edge function bundle/tests, version sync Pass

Review Gates

  • Baseline reviewer: Pass. Found one fallback branch still using the old enterprise/user-only predicate; fixed before publish.
  • Code Review Expert: Pass. SOLID, security, code-quality, and removal checklists reviewed against the final two-file diff; no remaining findings.
  • Outcome reviewer: Pass. Final diff satisfies the issue acceptance criteria and validation rows are green.

Documentation

Not applicable. This is an internal API response-header policy fix with regression coverage.

Screenshots / UI Evidence

Not applicable. No user-visible UI changed.

Residual Findings

None.

@vercel

vercel Bot commented Jun 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment Jun 28, 2026 11:27am

Request Review

@greptile-apps

greptile-apps Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Restricts /api/bootstrap shared caching to auth.kind === 'public-weather' only by inverting the previous isKeyAuth predicate. The old check (authKind === 'enterprise' || authKind === 'user') left session and unknown auth kinds falling through to shared CDN-cacheable headers, meaning an HMAC-signed anonymous session request to any non-weather endpoint could have been shared-cached by a CDN — exposing potentially scoped response data to other callers.

  • bootstrap.js: Two sites updated — successCacheHeaders and the getCachedJsonBatch error fallback — both now use authKind !== 'public-weather' as the gate, so session, unknown, enterprise, and user all get no-store.
  • bootstrap-auth.test.mjs: Adds a session-cookie regression test (and snapshots/restores WM_SESSION_SECRET) to the existing auth/cache matrix, exercising the full cryptographic validation path against a test-secret.

Confidence Score: 5/5

Safe to merge — the change is a narrowly scoped inversion of a two-site predicate with no behavior change on the public-weather path and correct tightening everywhere else.

Both changed sites are logically equivalent: replacing an explicit allowlist (enterprise | user) with its complement (!== 'public-weather') correctly pulls session and unknown kinds into the no-store set. The validateBootstrapAuth function already assigns 'session' to HMAC-validated cookie requests and 'public-weather' only to keyless weather-URL requests, so the new gate exactly matches the intended boundary. The new test exercises the real crypto path via issueSessionToken against a properly snapshotted WM_SESSION_SECRET and asserts the same assertNonSharedCacheHeaders helper already used across the matrix.

No files require special attention.

Important Files Changed

Filename Overview
api/bootstrap.js Two-site predicate fix: successCacheHeaders and the Redis-error fallback both updated from the explicit enterprise/user allowlist to the narrower authKind !== 'public-weather' check, correctly adding session and unknown kinds to the no-store set.
api/bootstrap-auth.test.mjs Adds WM_SESSION_SECRET to env snapshot/restore and a session-authenticated regression test that exercises the real HMAC path via issueSessionToken, correctly asserting no-store on the response.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant Handler as bootstrap handler
    participant Auth as validateBootstrapAuth
    participant Redis as getCachedJsonBatch
    participant Headers as successCacheHeaders

    Client->>Handler: "GET /api/bootstrap?keys=..."
    Handler->>Auth: validateBootstrapAuth(req)

    alt "No API key header AND ?keys=weatherAlerts only"
        Auth-->>Handler: "{ ok:true, kind:'public-weather' }"
    else Session cookie (wms_ token)
        Auth->>Auth: validateApiKey → kind:'session'
        Auth-->>Handler: "{ ok:true, kind:'session' }"
    else Enterprise key
        Auth-->>Handler: "{ ok:true, kind:'enterprise' }"
    else wm_ user key
        Auth-->>Handler: "{ ok:true, kind:'user' }"
    end

    Handler->>Redis: getCachedJsonBatch(keys)

    alt Redis throws
        Note over Handler: auth.kind === 'public-weather'? no-cache : no-store
        Handler-->>Client: "200 { data:{}, missing } + no-cache OR no-store"
    else Redis succeeds
        Redis-->>Handler: cached data map
        Handler->>Headers: successCacheHeaders(tier, auth.kind, cors)
        alt "auth.kind === 'public-weather'"
            Headers-->>Handler: Cache-Control: public + CDN-Cache-Control
        else any other kind (session, enterprise, user, unknown)
            Headers-->>Handler: Cache-Control: no-store
        end
        Handler-->>Client: "200 { data, missing } + cache headers"
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant Handler as bootstrap handler
    participant Auth as validateBootstrapAuth
    participant Redis as getCachedJsonBatch
    participant Headers as successCacheHeaders

    Client->>Handler: "GET /api/bootstrap?keys=..."
    Handler->>Auth: validateBootstrapAuth(req)

    alt "No API key header AND ?keys=weatherAlerts only"
        Auth-->>Handler: "{ ok:true, kind:'public-weather' }"
    else Session cookie (wms_ token)
        Auth->>Auth: validateApiKey → kind:'session'
        Auth-->>Handler: "{ ok:true, kind:'session' }"
    else Enterprise key
        Auth-->>Handler: "{ ok:true, kind:'enterprise' }"
    else wm_ user key
        Auth-->>Handler: "{ ok:true, kind:'user' }"
    end

    Handler->>Redis: getCachedJsonBatch(keys)

    alt Redis throws
        Note over Handler: auth.kind === 'public-weather'? no-cache : no-store
        Handler-->>Client: "200 { data:{}, missing } + no-cache OR no-store"
    else Redis succeeds
        Redis-->>Handler: cached data map
        Handler->>Headers: successCacheHeaders(tier, auth.kind, cors)
        alt "auth.kind === 'public-weather'"
            Headers-->>Handler: Cache-Control: public + CDN-Cache-Control
        else any other kind (session, enterprise, user, unknown)
            Headers-->>Handler: Cache-Control: no-store
        end
        Handler-->>Client: "200 { data, missing } + cache headers"
    end
Loading

Reviews (1): Last reviewed commit: "fix(api): restrict bootstrap shared cach..." | Re-trigger Greptile

@koala73
koala73 merged commit 7a20a4d into main Jun 28, 2026
24 checks passed
koala73 added a commit that referenced this pull request Jul 12, 2026
* perf(bootstrap): restore CDN cache shield for tier reads (#5249)

PR #4499 set Cache-Control: no-store on every successful /api/bootstrap
response except the anonymous single-key weatherAlerts probe. Because the
browser runtime fetches tiers with default same-origin credentials and every
visitor carries an anonymous wm-session cookie, hasBootstrapCredentialCookie()
disqualified ~100% of dashboard boots from the public path. The CDN shield that
used to absorb bootstrap traffic was gone, so every boot/refresh pipelined the
full seed registry (~5-10 MB) straight out of Upstash — driving Redis read
egress ~30x (from ~17 GB/day to 300-520 GB/day).

The tier payload is the shared production seed bundle, identical for every
caller (per #4499's own non-goals: only static transforms like wildfire
compaction and enrichmentMeta strip apply, never per-user variance). So a
credential-less tier read is safe to serve from the shared CDN.

Server (api/bootstrap.js):
- Add isPublicTierBootstrapRequest(): a GET/HEAD ?tier=fast|slow with no other
  params. Scoped to the two fixed tier shapes (not arbitrary ?keys=) so the CDN
  key space stays tiny and hit rate high.
- Treat credential-less tier reads as a new 'public-tier' auth kind and emit the
  CDN-Cache-Control shield for them (fast s-maxage=600, slow s-maxage=7200).
- Credentialed / API-key / arbitrary-key reads and all failures keep no-store,
  preserving #4495/#4499 intent.

Client (src/services/bootstrap.ts):
- Send tier fetches with credentials: 'omit' so the anonymous wm-session cookie
  is dropped and the request qualifies for the public CDN-cacheable path.

Adds bootstrap-auth coverage for the public-tier path (cacheable anonymous
fast/slow reads; no-store retained for cookie/enterprise/extra-param/unknown-tier).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FncwEiWddJ9My2HhQmrbYL

* perf(bootstrap): serve public bootstrap paths with ACAO:* and drop HEAD (#5249 review)

Addresses code-review findings on the public-tier cache path:

- CDN cache fragmentation / ACAO pinning: the public (weather + tier) responses
  reused getCorsHeaders() (echoed ACAO + Vary: Origin + credentials), so the
  shared CDN stored one entry per Origin and a preview/embed origin could pin an
  echoed ACAO onto a cached response. Switch both public paths (success and the
  Redis-error fallback) to getPublicCorsHeaders() (ACAO:*, no Vary, no
  credentials) — one cache entry per URL, no pinning. Safe because
  isDisallowedOrigin() already rejected unauthorized origins at handler entry;
  the public paths are always credential-less.

- HEAD unshielded read: isPublicTierBootstrapRequest accepted HEAD, so a HEAD
  ?tier=fast would run the full registry Redis pipeline to build a body it must
  not return. Restrict the public-tier path to GET; HEAD falls through to
  no-store (it has no client).

- Document the load-bearing invariant: the CDN caches by URL only and does not
  vary on Cookie, so classifying tier reads as public is safe ONLY while the
  payload is byte-identical for every caller. Future per-user tier variance must
  stop classifying these as public.

Tests: assert ACAO:* / no Vary / no credentials on the public-tier response, and
that a HEAD tier read returns no-store without touching Redis.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FncwEiWddJ9My2HhQmrbYL

* fix(bootstrap): isolate public tier cache reads

---------

Co-authored-by: Claude <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(api): restrict bootstrap shared caching to anonymous weather bootstrap

1 participant