Files
webstudio/apps/builder/app/services/cache-control.server.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

35 lines
923 B
TypeScript

export const privateNoStoreResponseHeaders = {
"Cache-Control": "private, no-store, max-age=0",
Pragma: "no-cache",
Expires: "0",
Vary: "Cookie",
};
export const appendVaryHeader = (headers: Headers, value: string) => {
const vary = headers.get("Vary");
if (vary === null || vary.trim() === "") {
headers.set("Vary", value);
return;
}
const varyValues = vary.split(",").map((item) => item.trim().toLowerCase());
if (varyValues.includes(value.toLowerCase()) === false) {
headers.set("Vary", `${vary}, ${value}`);
}
};
export const createPrivateNoStoreHeaders = (init?: HeadersInit) => {
const headers = new Headers(init);
for (const [name, value] of Object.entries(privateNoStoreResponseHeaders)) {
if (name.toLowerCase() === "vary") {
continue;
}
headers.set(name, value);
}
appendVaryHeader(headers, privateNoStoreResponseHeaders.Vary);
return headers;
};