@decocms/tanstack 7.20.8 → 7.20.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/sdk/workerEntry.test.ts +74 -2
- package/src/sdk/workerEntry.ts +44 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/tanstack",
|
|
3
|
-
"version": "7.20.
|
|
3
|
+
"version": "7.20.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco framework binding for TanStack Start + Cloudflare Workers",
|
|
6
6
|
"repository": {
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"lint:unused": "knip"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@decocms/blocks": "7.20.
|
|
28
|
-
"@decocms/blocks-admin": "7.20.
|
|
29
|
-
"@decocms/blocks-cli": "7.20.
|
|
27
|
+
"@decocms/blocks": "7.20.10",
|
|
28
|
+
"@decocms/blocks-admin": "7.20.10",
|
|
29
|
+
"@decocms/blocks-cli": "7.20.10",
|
|
30
30
|
"@deco-cx/warp-node": "^0.3.16",
|
|
31
31
|
"fast-json-patch": "^3.1.0",
|
|
32
32
|
"ws": "^8.18.0"
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { setBlocks } from "@decocms/blocks/cms";
|
|
2
2
|
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
buildGeoCacheParam,
|
|
5
|
+
createDecoWorkerEntry,
|
|
6
|
+
DECO_ADMIN_FRAME_ANCESTORS,
|
|
7
|
+
DEFAULT_FRAME_ANCESTORS_CSP,
|
|
8
|
+
DEFAULT_SECURITY_HEADERS,
|
|
9
|
+
detectLocationMatcher,
|
|
10
|
+
injectGeoCookies,
|
|
11
|
+
} from "./workerEntry";
|
|
4
12
|
import { __resetKvHydrationStateForTests } from "./kvHydration";
|
|
5
13
|
|
|
6
14
|
const EMPTY_ENV = {};
|
|
7
|
-
const MOCK_CTX = {
|
|
15
|
+
const MOCK_CTX = {
|
|
16
|
+
waitUntil: (_p: Promise<unknown>) => {},
|
|
17
|
+
passThroughOnException: () => {},
|
|
18
|
+
};
|
|
8
19
|
const MOCK_SERVER_ENTRY = {
|
|
9
20
|
fetch: async (_req: Request) => new Response("page content", { status: 200 }),
|
|
10
21
|
};
|
|
@@ -313,3 +324,64 @@ describe("CMS redirects", () => {
|
|
|
313
324
|
expect(res2.headers.get("Location")).toBe("/v2");
|
|
314
325
|
});
|
|
315
326
|
});
|
|
327
|
+
|
|
328
|
+
describe("security headers — frame-ancestors default", () => {
|
|
329
|
+
afterEach(() => {
|
|
330
|
+
__resetKvHydrationStateForTests();
|
|
331
|
+
setBlocks({});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const HTML_SERVER_ENTRY = {
|
|
335
|
+
fetch: async (_req: Request) =>
|
|
336
|
+
new Response("<html>hi</html>", {
|
|
337
|
+
status: 200,
|
|
338
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
339
|
+
}),
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
it("does NOT ship X-Frame-Options (frame-ancestors supersedes it)", () => {
|
|
343
|
+
expect(DEFAULT_SECURITY_HEADERS["X-Frame-Options"]).toBeUndefined();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("allows the deco studio to embed the storefront by default", () => {
|
|
347
|
+
expect(DEFAULT_FRAME_ANCESTORS_CSP).toContain("frame-ancestors");
|
|
348
|
+
expect(DECO_ADMIN_FRAME_ANCESTORS).toContain("https://studio.decocms.com");
|
|
349
|
+
expect(DECO_ADMIN_FRAME_ANCESTORS).toContain("https://*.deco.studio");
|
|
350
|
+
expect(DECO_ADMIN_FRAME_ANCESTORS).toContain("'self'");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("applies the default frame-ancestors CSP on HTML responses and drops X-Frame-Options", async () => {
|
|
354
|
+
const worker = createDecoWorkerEntry(HTML_SERVER_ENTRY);
|
|
355
|
+
const res = await worker.fetch(new Request("https://example.com/"), EMPTY_ENV, MOCK_CTX);
|
|
356
|
+
expect(res.headers.get("Content-Security-Policy")).toBe(DEFAULT_FRAME_ANCESTORS_CSP);
|
|
357
|
+
expect(res.headers.get("X-Frame-Options")).toBeNull();
|
|
358
|
+
// Other defaults are still present.
|
|
359
|
+
expect(res.headers.get("X-Content-Type-Options")).toBe("nosniff");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("lets a site override the frame-ancestors CSP via securityHeaders", async () => {
|
|
363
|
+
const worker = createDecoWorkerEntry(HTML_SERVER_ENTRY, {
|
|
364
|
+
securityHeaders: { "Content-Security-Policy": "frame-ancestors 'none'" },
|
|
365
|
+
});
|
|
366
|
+
const res = await worker.fetch(new Request("https://example.com/"), EMPTY_ENV, MOCK_CTX);
|
|
367
|
+
expect(res.headers.get("Content-Security-Policy")).toBe("frame-ancestors 'none'");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("emits the full site CSP report-only alongside the enforced frame-ancestors", async () => {
|
|
371
|
+
const worker = createDecoWorkerEntry(HTML_SERVER_ENTRY, {
|
|
372
|
+
csp: ["default-src 'self'", "img-src 'self' https:"],
|
|
373
|
+
});
|
|
374
|
+
const res = await worker.fetch(new Request("https://example.com/"), EMPTY_ENV, MOCK_CTX);
|
|
375
|
+
expect(res.headers.get("Content-Security-Policy")).toBe(DEFAULT_FRAME_ANCESTORS_CSP);
|
|
376
|
+
expect(res.headers.get("Content-Security-Policy-Report-Only")).toBe(
|
|
377
|
+
"default-src 'self'; img-src 'self' https:",
|
|
378
|
+
);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("omits all security headers when securityHeaders is false", async () => {
|
|
382
|
+
const worker = createDecoWorkerEntry(HTML_SERVER_ENTRY, { securityHeaders: false });
|
|
383
|
+
const res = await worker.fetch(new Request("https://example.com/"), EMPTY_ENV, MOCK_CTX);
|
|
384
|
+
expect(res.headers.get("Content-Security-Policy")).toBeNull();
|
|
385
|
+
expect(res.headers.get("X-Content-Type-Options")).toBeNull();
|
|
386
|
+
});
|
|
387
|
+
});
|
package/src/sdk/workerEntry.ts
CHANGED
|
@@ -331,10 +331,13 @@ export interface DecoWorkerEntryOptions {
|
|
|
331
331
|
* Security headers appended to every SSR response (HTML pages).
|
|
332
332
|
* Pass `false` to disable entirely.
|
|
333
333
|
*
|
|
334
|
-
* Default headers: X-Content-Type-Options,
|
|
335
|
-
* Permissions-Policy, X-XSS-Protection, HSTS, Cross-Origin-Opener-Policy
|
|
334
|
+
* Default headers: X-Content-Type-Options, Referrer-Policy,
|
|
335
|
+
* Permissions-Policy, X-XSS-Protection, HSTS, Cross-Origin-Opener-Policy,
|
|
336
|
+
* and a `Content-Security-Policy: frame-ancestors` directive that lets the
|
|
337
|
+
* deco admin/studio embed the storefront (see DEFAULT_FRAME_ANCESTORS_CSP).
|
|
336
338
|
*
|
|
337
|
-
* Custom entries are merged with defaults (custom values take precedence)
|
|
339
|
+
* Custom entries are merged with defaults (custom values take precedence) —
|
|
340
|
+
* pass `Content-Security-Policy` here to override the default frame-ancestors.
|
|
338
341
|
*
|
|
339
342
|
* @default DEFAULT_SECURITY_HEADERS
|
|
340
343
|
*/
|
|
@@ -621,11 +624,19 @@ const ONE_YEAR = 31536000;
|
|
|
621
624
|
|
|
622
625
|
/**
|
|
623
626
|
* Sensible security headers for any production storefront.
|
|
624
|
-
*
|
|
627
|
+
*
|
|
628
|
+
* Framing protection is handled by a default `Content-Security-Policy:
|
|
629
|
+
* frame-ancestors` directive (see DEFAULT_FRAME_ANCESTORS_CSP), NOT
|
|
630
|
+
* `X-Frame-Options`. CSP level 3 `frame-ancestors` supersedes X-Frame-Options
|
|
631
|
+
* in every modern browser, and — unlike the all-or-nothing SAMEORIGIN value —
|
|
632
|
+
* it can allow the deco admin/studio to embed the storefront for live preview
|
|
633
|
+
* while still blocking arbitrary third-party framing (clickjacking).
|
|
634
|
+
*
|
|
635
|
+
* Full CSP (script-src etc.) is intentionally not included here — it's
|
|
636
|
+
* site-specific (third-party script domains) and passed via the `csp` option.
|
|
625
637
|
*/
|
|
626
638
|
export const DEFAULT_SECURITY_HEADERS: Record<string, string> = {
|
|
627
639
|
"X-Content-Type-Options": "nosniff",
|
|
628
|
-
"X-Frame-Options": "SAMEORIGIN",
|
|
629
640
|
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
630
641
|
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
|
|
631
642
|
"X-XSS-Protection": "1; mode=block",
|
|
@@ -633,6 +644,28 @@ export const DEFAULT_SECURITY_HEADERS: Record<string, string> = {
|
|
|
633
644
|
"Cross-Origin-Opener-Policy": "same-origin-allow-popups",
|
|
634
645
|
};
|
|
635
646
|
|
|
647
|
+
/**
|
|
648
|
+
* Origins allowed to embed the storefront in an iframe. These are the deco
|
|
649
|
+
* studio surfaces that render the live-preview iframe, plus `'self'` so the
|
|
650
|
+
* storefront can frame itself.
|
|
651
|
+
*
|
|
652
|
+
* A site can widen this by passing its own
|
|
653
|
+
* `securityHeaders["Content-Security-Policy"]`, or drop framing protection
|
|
654
|
+
* entirely with `securityHeaders: false`.
|
|
655
|
+
*/
|
|
656
|
+
export const DECO_ADMIN_FRAME_ANCESTORS: string[] = [
|
|
657
|
+
"'self'",
|
|
658
|
+
"https://studio.decocms.com",
|
|
659
|
+
"https://*.deco.studio",
|
|
660
|
+
];
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Default enforcement CSP shipped on every SSR HTML response. Only the
|
|
664
|
+
* `frame-ancestors` directive is enforced by default; the full site CSP
|
|
665
|
+
* (passed via the `csp` option) is emitted report-only.
|
|
666
|
+
*/
|
|
667
|
+
export const DEFAULT_FRAME_ANCESTORS_CSP = `frame-ancestors ${DECO_ADMIN_FRAME_ANCESTORS.join(" ")}`;
|
|
668
|
+
|
|
636
669
|
const DEFAULT_BYPASS_PATHS = ["/_build", "/deco/", "/live/", "/.decofile"];
|
|
637
670
|
|
|
638
671
|
/**
|
|
@@ -879,6 +912,12 @@ export function createDecoWorkerEntry(
|
|
|
879
912
|
const secHeaders: Record<string, string> | null = (() => {
|
|
880
913
|
if (securityHeadersOpt === false) return null;
|
|
881
914
|
const base = { ...DEFAULT_SECURITY_HEADERS };
|
|
915
|
+
// Ship an enforcement `frame-ancestors` CSP by default so the deco
|
|
916
|
+
// admin/studio can embed the storefront for live preview without every
|
|
917
|
+
// site copy-pasting the directive into its worker entry. A site can
|
|
918
|
+
// override the whole header via securityHeaders["Content-Security-Policy"]
|
|
919
|
+
// (the merge loop below wins) or disable all security headers with `false`.
|
|
920
|
+
base["Content-Security-Policy"] = DEFAULT_FRAME_ANCESTORS_CSP;
|
|
882
921
|
if (securityHeadersOpt) {
|
|
883
922
|
for (const [k, v] of Object.entries(securityHeadersOpt)) base[k] = v;
|
|
884
923
|
}
|