Files
webstudio/apps/builder/app/services/cache-control.server.test.ts
Oleg Isonen d259a6d970 fix: Prevent caching builder user data (#5761)
**Summary**

Migrates Builder bootstrap data loading from the legacy
`/rest/data/:projectId` endpoint to `build.loadData` tRPC, while keeping
the old REST route as a no-store compatibility shim.

Hardens authenticated Builder/dashboard responses by explicitly sending:

- `Cache-Control: private, no-store, max-age=0`
- `Pragma: no-cache`
- `Expires: 0`
- `Vary: Cookie`

**Details**

- Added `build.loadData` tRPC query for full Builder state.
- Switched Builder client bootstrap loading to tRPC.
- Added shared cache-control helpers for private no-store responses.
- Made tRPC responses private/no-store by default unless anonymous and
explicitly public-cacheable.
- Added no-store headers to
Builder/dashboard/auth/resource/asset-related responses.
- Updated redirect handling to preserve headers while enforcing
no-store.
- Prevented stale dashboard account data from flashing by applying
user-scoped loader data before paint and clearing stores on teardown.
- Added tests for cache headers, tRPC response metadata, and no-store
redirects.

**Validation**

- `pnpm --filter @webstudio-is/builder test --
app/services/cache-control.server.test.ts
app/services/trpc-response-meta.server.test.ts
app/services/no-store-redirect.test.ts app/routes/rest.assets.test.ts
app/shared/sync/project-queue.test.ts app/dashboard/dashboard.test.ts`
- `pnpm exec oxlint ...` on touched files
- `git diff --check`

`pnpm --filter @webstudio-is/builder typecheck` still fails on existing
unrelated Tailwind/css-engine errors.
2026-05-16 20:22:03 +02:00

50 lines
1.4 KiB
TypeScript

import { describe, expect, test } from "vitest";
import {
appendVaryHeader,
createPrivateNoStoreHeaders,
privateNoStoreResponseHeaders,
} from "./cache-control.server";
describe("private no-store cache headers", () => {
test("marks user-scoped responses as non-cacheable and cookie-varying", () => {
expect(privateNoStoreResponseHeaders).toMatchObject({
"Cache-Control": "private, no-store, max-age=0",
Pragma: "no-cache",
Expires: "0",
Vary: "Cookie",
});
});
test("preserves unrelated headers while enforcing cache policy", () => {
const headers = createPrivateNoStoreHeaders({
"Cache-Control": "public, max-age=600",
"Set-Cookie": "session=value",
});
expect(headers.get("Cache-Control")).toBe(
privateNoStoreResponseHeaders["Cache-Control"]
);
expect(headers.get("Vary")).toBe(privateNoStoreResponseHeaders.Vary);
expect(headers.get("Set-Cookie")).toBe("session=value");
});
test("preserves existing vary values while adding cookie", () => {
const headers = createPrivateNoStoreHeaders({
Vary: "Origin",
});
expect(headers.get("Vary")).toBe("Origin, Cookie");
});
test("does not duplicate vary values", () => {
const headers = new Headers({
Vary: "Origin, Cookie",
});
appendVaryHeader(headers, "Origin");
appendVaryHeader(headers, "Cookie");
expect(headers.get("Vary")).toBe("Origin, Cookie");
});
});