@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/tanstack",
3
- "version": "7.20.8",
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.8",
28
- "@decocms/blocks-admin": "7.20.8",
29
- "@decocms/blocks-cli": "7.20.8",
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 { buildGeoCacheParam, createDecoWorkerEntry, detectLocationMatcher, injectGeoCookies } from "./workerEntry";
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 = { waitUntil: (_p: Promise<unknown>) => {} };
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
+ });
@@ -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, X-Frame-Options, Referrer-Policy,
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
- * CSP is intentionally not included — it's site-specific (third-party script domains).
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
  }