@decocms/tanstack 7.35.0 → 7.37.0

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.35.0",
3
+ "version": "7.37.0",
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.35.0",
28
- "@decocms/blocks-admin": "7.35.0",
29
- "@decocms/blocks-cli": "7.35.0",
27
+ "@decocms/blocks": "7.37.0",
28
+ "@decocms/blocks-admin": "7.37.0",
29
+ "@decocms/blocks-cli": "7.37.0",
30
30
  "@deco-cx/warp-node": "^0.3.16",
31
31
  "fast-json-patch": "^3.1.0",
32
32
  "ws": "^8.18.0"
@@ -19,11 +19,15 @@ import { DRAFT_POINTER_BAG_KEY } from "./draftShared";
19
19
  const HOST = "preview.example";
20
20
  const DRAFT_BLOCKS = { "pages-home": { title: "draft" } };
21
21
 
22
- function req(opts: { url?: string; host?: string; cookie?: string } = {}): Request {
22
+ function req(
23
+ opts: { url?: string; host?: string; cookie?: string } = {},
24
+ ): Request {
23
25
  const headers = new Headers();
24
26
  headers.set("host", opts.host ?? HOST);
25
27
  if (opts.cookie) headers.set("cookie", opts.cookie);
26
- return new Request(opts.url ?? `https://${opts.host ?? HOST}/some/page`, { headers });
28
+ return new Request(opts.url ?? `https://${opts.host ?? HOST}/some/page`, {
29
+ headers,
30
+ });
27
31
  }
28
32
 
29
33
  /** A fetch stub serving the sandbox decofile; throws if `fail`. */
@@ -51,41 +55,54 @@ afterEach(() => {
51
55
  describe("requestCarriesDraft", () => {
52
56
  it("is inert when no allowlist is configured", () => {
53
57
  setDraftPreviewHosts([]);
54
- const url = new URL(`https://${HOST}/p?__draft=abc.preview-studio.decocms.com@v1`);
58
+ const url = new URL(
59
+ `https://${HOST}/p?__draft=abc.preview-studio.decocms.com@v1`,
60
+ );
55
61
  expect(requestCarriesDraft(req({ url: url.toString() }), url)).toBe(false);
56
62
  });
57
63
 
58
64
  it("detects the ?__draft= entry param", () => {
59
- const url = new URL(`https://${HOST}/p?__draft=abc.preview-studio.decocms.com@v1`);
65
+ const url = new URL(
66
+ `https://${HOST}/p?__draft=abc.preview-studio.decocms.com@v1`,
67
+ );
60
68
  expect(requestCarriesDraft(req({ url: url.toString() }), url)).toBe(true);
61
69
  });
62
70
 
63
71
  it("detects the navigation cookie (SPA nav after entry)", () => {
64
72
  const url = new URL(`https://${HOST}/p`);
65
73
  expect(
66
- requestCarriesDraft(req({ url: url.toString(), cookie: "__deco_draft=abc.x@v1" }), url),
74
+ requestCarriesDraft(
75
+ req({ url: url.toString(), cookie: "__deco_draft=abc.x@v1" }),
76
+ url,
77
+ ),
67
78
  ).toBe(true);
68
79
  });
69
80
 
70
81
  it("is false on ?__draft=off (leaving preview)", () => {
71
82
  const url = new URL(`https://${HOST}/p?__draft=off`);
72
83
  expect(
73
- requestCarriesDraft(req({ url: url.toString(), cookie: "__deco_draft=abc.x@v1" }), url),
84
+ requestCarriesDraft(
85
+ req({ url: url.toString(), cookie: "__deco_draft=abc.x@v1" }),
86
+ url,
87
+ ),
74
88
  ).toBe(false);
75
89
  });
76
90
 
77
91
  it("is structurally inert on a non-preview host, param or not", () => {
78
92
  const url = new URL("https://prod.example/p?__draft=abc.x@v1");
79
- expect(requestCarriesDraft(req({ url: url.toString(), host: "prod.example" }), url)).toBe(
80
- false,
81
- );
93
+ expect(
94
+ requestCarriesDraft(
95
+ req({ url: url.toString(), host: "prod.example" }),
96
+ url,
97
+ ),
98
+ ).toBe(false);
82
99
  });
83
100
  });
84
101
 
85
102
  describe("bindRequestDraft", () => {
86
103
  it("binds the resolved draft to the request bag on entry", async () => {
87
104
  registerDraftOverride();
88
- const pointer = "abc.preview-studio.decocms.com@v1";
105
+ const pointer = "studio.decocms.com/api/o/decofile/m/main?token=t@v1";
89
106
  const request = req({ url: `https://${HOST}/p?__draft=${pointer}` });
90
107
 
91
108
  const decision = await RequestContext.run(request, async () => {
@@ -95,20 +112,31 @@ describe("bindRequestDraft", () => {
95
112
  return d;
96
113
  });
97
114
 
98
- expect(decision).toEqual({ previewing: true, setCookie: pointer, clearCookie: false });
115
+ expect(decision).toEqual({
116
+ previewing: true,
117
+ setCookie: pointer,
118
+ clearCookie: false,
119
+ });
99
120
  });
100
121
 
101
122
  it("carries the draft across navigation via the cookie (no re-set)", async () => {
102
123
  registerDraftOverride();
103
124
  const pointer = "abc.preview-studio.decocms.com@v1";
104
- const request = req({ url: `https://${HOST}/other`, cookie: `__deco_draft=${pointer}` });
125
+ const request = req({
126
+ url: `https://${HOST}/other`,
127
+ cookie: `__deco_draft=${pointer}`,
128
+ });
105
129
 
106
130
  const decision = await RequestContext.run(request, () =>
107
131
  bindRequestDraft(request, sandboxFetch()),
108
132
  );
109
133
 
110
134
  // previewing, but setCookie is null — the cookie is already present.
111
- expect(decision).toEqual({ previewing: true, setCookie: null, clearCookie: false });
135
+ expect(decision).toEqual({
136
+ previewing: true,
137
+ setCookie: null,
138
+ clearCookie: false,
139
+ });
112
140
  });
113
141
 
114
142
  it("decodes a URL-encoded cookie so the badge pointer round-trips (no double-encode)", async () => {
@@ -116,7 +144,7 @@ describe("bindRequestDraft", () => {
116
144
  // pointer stashed for the badge must be the DECODED value, or the badge's
117
145
  // share link would re-encode an already-encoded string.
118
146
  registerDraftOverride();
119
- const decoded = "abc.localhost:60534@v7";
147
+ const decoded = "abc.localhost:60534/api/o/decofile/m/main?token=t@v7";
120
148
  const request = req({
121
149
  url: `https://${HOST}/other`,
122
150
  cookie: `__deco_draft=${encodeURIComponent(decoded)}`,
@@ -131,7 +159,9 @@ describe("bindRequestDraft", () => {
131
159
  });
132
160
 
133
161
  it("stays previewing (uncacheable) even when the draft fails to resolve", async () => {
134
- const request = req({ url: `https://${HOST}/p?__draft=abc.preview-studio.decocms.com@v1` });
162
+ const request = req({
163
+ url: `https://${HOST}/p?__draft=abc.preview-studio.decocms.com@v1`,
164
+ });
135
165
  const decision = await RequestContext.run(request, () =>
136
166
  bindRequestDraft(request, sandboxFetch(true)),
137
167
  );
@@ -140,9 +170,18 @@ describe("bindRequestDraft", () => {
140
170
  });
141
171
 
142
172
  it("clears the cookie on ?__draft=off", async () => {
143
- const request = req({ url: `https://${HOST}/p?__draft=off`, cookie: "__deco_draft=x@v1" });
144
- const decision = await RequestContext.run(request, () => bindRequestDraft(request));
145
- expect(decision).toEqual({ previewing: false, setCookie: null, clearCookie: true });
173
+ const request = req({
174
+ url: `https://${HOST}/p?__draft=off`,
175
+ cookie: "__deco_draft=x@v1",
176
+ });
177
+ const decision = await RequestContext.run(request, () =>
178
+ bindRequestDraft(request),
179
+ );
180
+ expect(decision).toEqual({
181
+ previewing: false,
182
+ setCookie: null,
183
+ clearCookie: true,
184
+ });
146
185
  });
147
186
 
148
187
  it("is inert on a non-preview host", async () => {
@@ -150,8 +189,14 @@ describe("bindRequestDraft", () => {
150
189
  url: "https://prod.example/p?__draft=abc.x@v1",
151
190
  host: "prod.example",
152
191
  });
153
- const decision = await RequestContext.run(request, () => bindRequestDraft(request));
154
- expect(decision).toEqual({ previewing: false, setCookie: null, clearCookie: false });
192
+ const decision = await RequestContext.run(request, () =>
193
+ bindRequestDraft(request),
194
+ );
195
+ expect(decision).toEqual({
196
+ previewing: false,
197
+ setCookie: null,
198
+ clearCookie: false,
199
+ });
155
200
  });
156
201
  });
157
202
 
@@ -180,7 +225,11 @@ describe("applyDraftCookieAndHeaders", () => {
180
225
 
181
226
  it("expires the cookie when leaving preview", () => {
182
227
  const res = new Response("ok");
183
- applyDraftCookieAndHeaders(res, { previewing: false, setCookie: null, clearCookie: true });
228
+ applyDraftCookieAndHeaders(res, {
229
+ previewing: false,
230
+ setCookie: null,
231
+ clearCookie: true,
232
+ });
184
233
  expect(res.headers.get("set-cookie") ?? "").toContain("Max-Age=0");
185
234
  // Not previewing → no cache/index headers.
186
235
  expect(res.headers.get("cache-control")).toBeNull();
@@ -188,19 +237,33 @@ describe("applyDraftCookieAndHeaders", () => {
188
237
 
189
238
  it("merges Cookie into an existing Vary instead of clobbering it", () => {
190
239
  const res = new Response("ok", { headers: { Vary: "Accept-Encoding" } });
191
- applyDraftCookieAndHeaders(res, { previewing: true, setCookie: null, clearCookie: false });
240
+ applyDraftCookieAndHeaders(res, {
241
+ previewing: true,
242
+ setCookie: null,
243
+ clearCookie: false,
244
+ });
192
245
  expect(res.headers.get("vary")).toBe("Accept-Encoding, Cookie");
193
246
  });
194
247
 
195
248
  it("leaves a Vary that already lists Cookie untouched (no token dropped)", () => {
196
- const res = new Response("ok", { headers: { Vary: "Accept-Encoding, Cookie" } });
197
- applyDraftCookieAndHeaders(res, { previewing: true, setCookie: null, clearCookie: false });
249
+ const res = new Response("ok", {
250
+ headers: { Vary: "Accept-Encoding, Cookie" },
251
+ });
252
+ applyDraftCookieAndHeaders(res, {
253
+ previewing: true,
254
+ setCookie: null,
255
+ clearCookie: false,
256
+ });
198
257
  expect(res.headers.get("vary")).toBe("Accept-Encoding, Cookie");
199
258
  });
200
259
 
201
260
  it("does nothing for an ordinary (non-draft) response", () => {
202
261
  const res = new Response("ok");
203
- applyDraftCookieAndHeaders(res, { previewing: false, setCookie: null, clearCookie: false });
262
+ applyDraftCookieAndHeaders(res, {
263
+ previewing: false,
264
+ setCookie: null,
265
+ clearCookie: false,
266
+ });
204
267
  expect(res.headers.get("set-cookie")).toBeNull();
205
268
  expect(res.headers.get("cache-control")).toBeNull();
206
269
  });
@@ -209,7 +272,9 @@ describe("applyDraftCookieAndHeaders", () => {
209
272
  describe("installPreviewHostsFromBlocks", () => {
210
273
  it("installs previewHosts from the site block (either casing)", async () => {
211
274
  setDraftPreviewHosts([]);
212
- installPreviewHostsFromBlocks({ Site: { previewHosts: ["fila.vtex.app"] } });
275
+ installPreviewHostsFromBlocks({
276
+ Site: { previewHosts: ["fila.vtex.app"] },
277
+ });
213
278
 
214
279
  // Verified through the wire: an allowed host now previews.
215
280
  const request = req({
@@ -224,8 +289,11 @@ describe("installPreviewHostsFromBlocks", () => {
224
289
  setDraftPreviewHosts([]);
225
290
  installPreviewHostsFromBlocks({ site: {} });
226
291
  const url = new URL("https://fila.vtex.app/p?__draft=x@v1");
227
- expect(requestCarriesDraft(req({ url: url.toString(), host: "fila.vtex.app" }), url)).toBe(
228
- false,
229
- );
292
+ expect(
293
+ requestCarriesDraft(
294
+ req({ url: url.toString(), host: "fila.vtex.app" }),
295
+ url,
296
+ ),
297
+ ).toBe(false);
230
298
  });
231
299
  });
@@ -59,7 +59,10 @@ describe("injectGeoCookies", () => {
59
59
  });
60
60
 
61
61
  it("strips cf-ipcity from the outgoing Request headers while preserving the value in __cf_geo_city cookie", () => {
62
- const req = makeRequest({ city: "Brasília", country: "BR" }, { "cf-ipcity": "Brasília" });
62
+ const req = makeRequest(
63
+ { city: "Brasília", country: "BR" },
64
+ { "cf-ipcity": "Brasília" },
65
+ );
63
66
 
64
67
  const out = injectGeoCookies(req);
65
68
 
@@ -88,7 +91,10 @@ describe("injectGeoCookies", () => {
88
91
  });
89
92
 
90
93
  it("preserves a pre-existing cookie header", () => {
91
- const req = makeRequest({ region: "São Paulo" }, { cookie: "vtex_segment=abc; another=xyz" });
94
+ const req = makeRequest(
95
+ { region: "São Paulo" },
96
+ { cookie: "vtex_segment=abc; another=xyz" },
97
+ );
92
98
 
93
99
  const out = injectGeoCookies(req);
94
100
 
@@ -143,7 +149,9 @@ describe("buildGeoCacheParam", () => {
143
149
 
144
150
  it("omits missing fields gracefully", () => {
145
151
  expect(buildGeoCacheParam({ country: "BR" }, "city")).toBe("BR");
146
- expect(buildGeoCacheParam({ country: "BR", region: "MG" }, "city")).toBe("BR|MG");
152
+ expect(buildGeoCacheParam({ country: "BR", region: "MG" }, "city")).toBe(
153
+ "BR|MG",
154
+ );
147
155
  });
148
156
 
149
157
  it("returns undefined when cf has none of country/region/city", () => {
@@ -214,8 +222,14 @@ describe("CMS redirects", () => {
214
222
  redirects: [{ from: "/old", to: "/new", type: "permanent" }],
215
223
  },
216
224
  });
217
- const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, { observability: false });
218
- const res = await worker.fetch(new Request("https://example.com/old"), EMPTY_ENV, MOCK_CTX);
225
+ const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, {
226
+ observability: false,
227
+ });
228
+ const res = await worker.fetch(
229
+ new Request("https://example.com/old"),
230
+ EMPTY_ENV,
231
+ MOCK_CTX,
232
+ );
219
233
  expect(res.status).toBe(301);
220
234
  expect(res.headers.get("Location")).toBe("/new");
221
235
  });
@@ -227,7 +241,9 @@ describe("CMS redirects", () => {
227
241
  redirects: [{ from: "/old", to: "/new", type: "permanent" }],
228
242
  },
229
243
  });
230
- const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, { observability: false });
244
+ const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, {
245
+ observability: false,
246
+ });
231
247
  const res = await worker.fetch(
232
248
  new Request("https://example.com/old?asJson"),
233
249
  EMPTY_ENV,
@@ -244,8 +260,14 @@ describe("CMS redirects", () => {
244
260
  redirects: [{ from: "/promo", to: "/promoção", type: "temporary" }],
245
261
  },
246
262
  });
247
- const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, { observability: false });
248
- const res = await worker.fetch(new Request("https://example.com/promo"), EMPTY_ENV, MOCK_CTX);
263
+ const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, {
264
+ observability: false,
265
+ });
266
+ const res = await worker.fetch(
267
+ new Request("https://example.com/promo"),
268
+ EMPTY_ENV,
269
+ MOCK_CTX,
270
+ );
249
271
  expect(res.status).toBe(302);
250
272
  expect(res.headers.get("Location")).toBe("/promo%C3%A7%C3%A3o");
251
273
  });
@@ -257,8 +279,14 @@ describe("CMS redirects", () => {
257
279
  redirects: [{ from: "/promo", to: "/sale", type: "temporary" }],
258
280
  },
259
281
  });
260
- const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, { observability: false });
261
- const res = await worker.fetch(new Request("https://example.com/promo"), EMPTY_ENV, MOCK_CTX);
282
+ const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, {
283
+ observability: false,
284
+ });
285
+ const res = await worker.fetch(
286
+ new Request("https://example.com/promo"),
287
+ EMPTY_ENV,
288
+ MOCK_CTX,
289
+ );
262
290
  expect(res.status).toBe(302);
263
291
  expect(res.headers.get("Location")).toBe("/sale");
264
292
  });
@@ -270,17 +298,29 @@ describe("CMS redirects", () => {
270
298
  redirects: [{ from: "/old", to: "/new", type: "permanent" }],
271
299
  },
272
300
  });
273
- const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, { observability: false });
274
- const res = await worker.fetch(new Request("https://example.com/other"), EMPTY_ENV, MOCK_CTX);
301
+ const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, {
302
+ observability: false,
303
+ });
304
+ const res = await worker.fetch(
305
+ new Request("https://example.com/other"),
306
+ EMPTY_ENV,
307
+ MOCK_CTX,
308
+ );
275
309
  expect(res.status).toBe(200);
276
310
  });
277
311
 
278
312
  it("picks up new redirects after setBlocks hot-reload", async () => {
279
313
  setBlocks({});
280
- const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, { observability: false });
314
+ const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, {
315
+ observability: false,
316
+ });
281
317
 
282
318
  // First request with no redirects — falls through
283
- const res1 = await worker.fetch(new Request("https://example.com/v1"), EMPTY_ENV, MOCK_CTX);
319
+ const res1 = await worker.fetch(
320
+ new Request("https://example.com/v1"),
321
+ EMPTY_ENV,
322
+ MOCK_CTX,
323
+ );
284
324
  expect(res1.status).toBe(200);
285
325
 
286
326
  // Hot-reload: add a redirect
@@ -292,7 +332,11 @@ describe("CMS redirects", () => {
292
332
  });
293
333
 
294
334
  // Same path should now redirect
295
- const res2 = await worker.fetch(new Request("https://example.com/v1"), EMPTY_ENV, MOCK_CTX);
335
+ const res2 = await worker.fetch(
336
+ new Request("https://example.com/v1"),
337
+ EMPTY_ENV,
338
+ MOCK_CTX,
339
+ );
296
340
  expect(res2.status).toBe(301);
297
341
  expect(res2.headers.get("Location")).toBe("/v2");
298
342
  });
@@ -313,7 +357,9 @@ describe("degraded origin (anti-cache-poisoning)", () => {
313
357
  headers: { "X-Deco-Degraded": "true" },
314
358
  }),
315
359
  };
316
- const worker = createDecoWorkerEntry(degradedEntry, { observability: false });
360
+ const worker = createDecoWorkerEntry(degradedEntry, {
361
+ observability: false,
362
+ });
317
363
  const res = await worker.fetch(
318
364
  new Request("https://example.com/some-category"),
319
365
  EMPTY_ENV,
@@ -327,7 +373,9 @@ describe("degraded origin (anti-cache-poisoning)", () => {
327
373
 
328
374
  it("serves a healthy 200 origin without the degraded bypass", async () => {
329
375
  setBlocks({});
330
- const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, { observability: false });
376
+ const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, {
377
+ observability: false,
378
+ });
331
379
  const res = await worker.fetch(
332
380
  new Request("https://example.com/some-category"),
333
381
  EMPTY_ENV,
@@ -365,8 +413,14 @@ describe("security headers — frame-ancestors default", () => {
365
413
 
366
414
  it("applies the default frame-ancestors CSP on HTML responses and drops X-Frame-Options", async () => {
367
415
  const worker = createDecoWorkerEntry(HTML_SERVER_ENTRY);
368
- const res = await worker.fetch(new Request("https://example.com/"), EMPTY_ENV, MOCK_CTX);
369
- expect(res.headers.get("Content-Security-Policy")).toBe(DEFAULT_FRAME_ANCESTORS_CSP);
416
+ const res = await worker.fetch(
417
+ new Request("https://example.com/"),
418
+ EMPTY_ENV,
419
+ MOCK_CTX,
420
+ );
421
+ expect(res.headers.get("Content-Security-Policy")).toBe(
422
+ DEFAULT_FRAME_ANCESTORS_CSP,
423
+ );
370
424
  expect(res.headers.get("X-Frame-Options")).toBeNull();
371
425
  // Other defaults are still present.
372
426
  expect(res.headers.get("X-Content-Type-Options")).toBe("nosniff");
@@ -376,24 +430,42 @@ describe("security headers — frame-ancestors default", () => {
376
430
  const worker = createDecoWorkerEntry(HTML_SERVER_ENTRY, {
377
431
  securityHeaders: { "Content-Security-Policy": "frame-ancestors 'none'" },
378
432
  });
379
- const res = await worker.fetch(new Request("https://example.com/"), EMPTY_ENV, MOCK_CTX);
380
- expect(res.headers.get("Content-Security-Policy")).toBe("frame-ancestors 'none'");
433
+ const res = await worker.fetch(
434
+ new Request("https://example.com/"),
435
+ EMPTY_ENV,
436
+ MOCK_CTX,
437
+ );
438
+ expect(res.headers.get("Content-Security-Policy")).toBe(
439
+ "frame-ancestors 'none'",
440
+ );
381
441
  });
382
442
 
383
443
  it("emits the full site CSP report-only alongside the enforced frame-ancestors", async () => {
384
444
  const worker = createDecoWorkerEntry(HTML_SERVER_ENTRY, {
385
445
  csp: ["default-src 'self'", "img-src 'self' https:"],
386
446
  });
387
- const res = await worker.fetch(new Request("https://example.com/"), EMPTY_ENV, MOCK_CTX);
388
- expect(res.headers.get("Content-Security-Policy")).toBe(DEFAULT_FRAME_ANCESTORS_CSP);
447
+ const res = await worker.fetch(
448
+ new Request("https://example.com/"),
449
+ EMPTY_ENV,
450
+ MOCK_CTX,
451
+ );
452
+ expect(res.headers.get("Content-Security-Policy")).toBe(
453
+ DEFAULT_FRAME_ANCESTORS_CSP,
454
+ );
389
455
  expect(res.headers.get("Content-Security-Policy-Report-Only")).toBe(
390
456
  "default-src 'self'; img-src 'self' https:",
391
457
  );
392
458
  });
393
459
 
394
460
  it("omits all security headers when securityHeaders is false", async () => {
395
- const worker = createDecoWorkerEntry(HTML_SERVER_ENTRY, { securityHeaders: false });
396
- const res = await worker.fetch(new Request("https://example.com/"), EMPTY_ENV, MOCK_CTX);
461
+ const worker = createDecoWorkerEntry(HTML_SERVER_ENTRY, {
462
+ securityHeaders: false,
463
+ });
464
+ const res = await worker.fetch(
465
+ new Request("https://example.com/"),
466
+ EMPTY_ENV,
467
+ MOCK_CTX,
468
+ );
397
469
  expect(res.headers.get("Content-Security-Policy")).toBeNull();
398
470
  expect(res.headers.get("X-Content-Type-Options")).toBeNull();
399
471
  });
@@ -405,10 +477,18 @@ describe("POST /_cache/purge-loaders", () => {
405
477
  setBlocks({});
406
478
  });
407
479
 
408
- const purge = (env: Record<string, unknown>, headers: Record<string, string> = {}) => {
409
- const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, { observability: false });
480
+ const purge = (
481
+ env: Record<string, unknown>,
482
+ headers: Record<string, string> = {},
483
+ ) => {
484
+ const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, {
485
+ observability: false,
486
+ });
410
487
  return worker.fetch(
411
- new Request("https://example.com/_cache/purge-loaders", { method: "POST", headers }),
488
+ new Request("https://example.com/_cache/purge-loaders", {
489
+ method: "POST",
490
+ headers,
491
+ }),
412
492
  env,
413
493
  MOCK_CTX,
414
494
  );
@@ -420,12 +500,18 @@ describe("POST /_cache/purge-loaders", () => {
420
500
  });
421
501
 
422
502
  it("401s with the wrong token", async () => {
423
- const res = await purge({ PURGE_TOKEN: "secret" }, { Authorization: "Bearer nope" });
503
+ const res = await purge(
504
+ { PURGE_TOKEN: "secret" },
505
+ { Authorization: "Bearer nope" },
506
+ );
424
507
  expect(res.status).toBe(401);
425
508
  });
426
509
 
427
510
  it("clears the loader cache with a valid token", async () => {
428
- const res = await purge({ PURGE_TOKEN: "secret" }, { Authorization: "Bearer secret" });
511
+ const res = await purge(
512
+ { PURGE_TOKEN: "secret" },
513
+ { Authorization: "Bearer secret" },
514
+ );
429
515
  expect(res.status).toBe(200);
430
516
  expect(await res.json()).toEqual({ cleared: true, scope: "isolate" });
431
517
  });
@@ -436,7 +522,9 @@ describe("POST /_cache/purge-loaders", () => {
436
522
  purgeTokenEnv: false,
437
523
  });
438
524
  const res = await worker.fetch(
439
- new Request("https://example.com/_cache/purge-loaders", { method: "POST" }),
525
+ new Request("https://example.com/_cache/purge-loaders", {
526
+ method: "POST",
527
+ }),
440
528
  EMPTY_ENV,
441
529
  MOCK_CTX,
442
530
  );
@@ -461,10 +549,20 @@ describe("X-Cache-Segment — custom SegmentKey fields (#284)", () => {
461
549
  it("includes a custom segment field in X-Cache-Segment instead of dropping it", async () => {
462
550
  const worker = createDecoWorkerEntry(HTML_SERVER_ENTRY, {
463
551
  observability: false,
464
- buildSegment: () => ({ device: "desktop", regionId: "RJ", delivery: "store-42" }),
552
+ buildSegment: () => ({
553
+ device: "desktop",
554
+ regionId: "RJ",
555
+ delivery: "store-42",
556
+ }),
465
557
  });
466
- const res = await worker.fetch(new Request("https://example.com/"), EMPTY_ENV, MOCK_CTX);
467
- expect(res.headers.get("X-Cache-Segment")).toBe("desktop|r=RJ|delivery=store-42");
558
+ const res = await worker.fetch(
559
+ new Request("https://example.com/"),
560
+ EMPTY_ENV,
561
+ MOCK_CTX,
562
+ );
563
+ expect(res.headers.get("X-Cache-Segment")).toBe(
564
+ "desktop|r=RJ|delivery=store-42",
565
+ );
468
566
  });
469
567
 
470
568
  it("produces different X-Cache-Segment hashes for requests that only differ by a custom field", async () => {
@@ -474,11 +572,19 @@ describe("X-Cache-Segment — custom SegmentKey fields (#284)", () => {
474
572
  buildSegment: () => ({ device: "desktop", delivery }),
475
573
  });
476
574
 
477
- const first = await worker.fetch(new Request("https://example.com/"), EMPTY_ENV, MOCK_CTX);
575
+ const first = await worker.fetch(
576
+ new Request("https://example.com/"),
577
+ EMPTY_ENV,
578
+ MOCK_CTX,
579
+ );
478
580
  const firstSegment = first.headers.get("X-Cache-Segment");
479
581
 
480
582
  delivery = "store-2";
481
- const second = await worker.fetch(new Request("https://example.com/"), EMPTY_ENV, MOCK_CTX);
583
+ const second = await worker.fetch(
584
+ new Request("https://example.com/"),
585
+ EMPTY_ENV,
586
+ MOCK_CTX,
587
+ );
482
588
  const secondSegment = second.headers.get("X-Cache-Segment");
483
589
 
484
590
  expect(firstSegment).not.toBe(secondSegment);
@@ -487,24 +593,35 @@ describe("X-Cache-Segment — custom SegmentKey fields (#284)", () => {
487
593
  it("keeps the existing hash format for segments using only known fields (back-compat)", async () => {
488
594
  const worker = createDecoWorkerEntry(HTML_SERVER_ENTRY, {
489
595
  observability: false,
490
- buildSegment: () => ({ device: "desktop", salesChannel: "1", flags: ["b", "a"] }),
596
+ buildSegment: () => ({
597
+ device: "desktop",
598
+ salesChannel: "1",
599
+ flags: ["b", "a"],
600
+ }),
491
601
  });
492
- const res = await worker.fetch(new Request("https://example.com/"), EMPTY_ENV, MOCK_CTX);
602
+ const res = await worker.fetch(
603
+ new Request("https://example.com/"),
604
+ EMPTY_ENV,
605
+ MOCK_CTX,
606
+ );
493
607
  expect(res.headers.get("X-Cache-Segment")).toBe("desktop|sc=1|f=a,b");
494
608
  });
495
609
  });
496
610
 
497
611
  describe("draft preview (pull-based)", () => {
498
- const POINTER = "abc.localhost:1234@v1";
499
- const SANDBOX_URL = "http://abc.localhost:1234/_sandbox/decofile";
500
- const DRAFT_BLOCKS = { "pages-home": { name: "home", path: "/", title: "DRAFTED" } };
612
+ const POINTER = "abc.localhost:1234/api/o/decofile/m/main?token=t@v1";
613
+ const SANDBOX_URL = "http://abc.localhost:1234/api/o/decofile/m/main?token=t";
614
+ const DRAFT_BLOCKS = {
615
+ "pages-home": { name: "home", path: "/", title: "DRAFTED" },
616
+ };
501
617
 
502
618
  // A serverEntry that reflects the currently-loaded home title, so a test can
503
619
  // prove the draft override actually reached the render (not just the headers).
504
620
  const REFLECT_ENTRY = {
505
621
  fetch: async () => {
506
622
  const { loadBlocks } = await import("@decocms/blocks/cms");
507
- const home = (loadBlocks()["pages-home"] as { title?: string } | undefined) ?? {};
623
+ const home =
624
+ (loadBlocks()["pages-home"] as { title?: string } | undefined) ?? {};
508
625
  return new Response(home.title ?? "PUBLISHED", { status: 200 });
509
626
  },
510
627
  };
@@ -512,7 +629,9 @@ describe("draft preview (pull-based)", () => {
512
629
  let realFetch: typeof fetch;
513
630
  afterEach(async () => {
514
631
  globalThis.fetch = realFetch;
515
- const { setDraftPreviewHosts, clearDraftCache } = await import("@decocms/blocks/cms");
632
+ const { setDraftPreviewHosts, clearDraftCache } = await import(
633
+ "@decocms/blocks/cms"
634
+ );
516
635
  setDraftPreviewHosts([]);
517
636
  clearDraftCache();
518
637
  __resetKvHydrationStateForTests();
@@ -520,8 +639,12 @@ describe("draft preview (pull-based)", () => {
520
639
  });
521
640
 
522
641
  async function enable() {
523
- const { setDraftPreviewHosts, clearDraftCache } = await import("@decocms/blocks/cms");
524
- setBlocks({ "pages-home": { name: "home", path: "/", title: "PUBLISHED" } });
642
+ const { setDraftPreviewHosts, clearDraftCache } = await import(
643
+ "@decocms/blocks/cms"
644
+ );
645
+ setBlocks({
646
+ "pages-home": { name: "home", path: "/", title: "PUBLISHED" },
647
+ });
525
648
  setDraftPreviewHosts(["example.com"]);
526
649
  clearDraftCache();
527
650
  realFetch = globalThis.fetch;
@@ -538,7 +661,9 @@ describe("draft preview (pull-based)", () => {
538
661
 
539
662
  it("binds the draft, bypasses the edge cache, and stamps the anti-leak headers", async () => {
540
663
  await enable();
541
- const worker = createDecoWorkerEntry(REFLECT_ENTRY, { observability: false });
664
+ const worker = createDecoWorkerEntry(REFLECT_ENTRY, {
665
+ observability: false,
666
+ });
542
667
  const res = await worker.fetch(
543
668
  new Request(`https://example.com/?__draft=${POINTER}`, {
544
669
  headers: { host: "example.com" },
@@ -562,7 +687,9 @@ describe("draft preview (pull-based)", () => {
562
687
 
563
688
  it("carries the draft across navigation via the cookie alone", async () => {
564
689
  await enable();
565
- const worker = createDecoWorkerEntry(REFLECT_ENTRY, { observability: false });
690
+ const worker = createDecoWorkerEntry(REFLECT_ENTRY, {
691
+ observability: false,
692
+ });
566
693
  const res = await worker.fetch(
567
694
  new Request("https://example.com/other", {
568
695
  headers: { host: "example.com", cookie: `__deco_draft=${POINTER}` },
@@ -576,7 +703,9 @@ describe("draft preview (pull-based)", () => {
576
703
 
577
704
  it("is structurally inert on a non-preview host", async () => {
578
705
  await enable(); // allowlist = example.com only
579
- const worker = createDecoWorkerEntry(REFLECT_ENTRY, { observability: false });
706
+ const worker = createDecoWorkerEntry(REFLECT_ENTRY, {
707
+ observability: false,
708
+ });
580
709
  const res = await worker.fetch(
581
710
  new Request(`https://prod.example/?__draft=${POINTER}`, {
582
711
  headers: { host: "prod.example" },
@@ -590,8 +719,12 @@ describe("draft preview (pull-based)", () => {
590
719
  });
591
720
 
592
721
  it("stays inert on any host when no allowlist is configured", async () => {
593
- setBlocks({ "pages-home": { name: "home", path: "/", title: "PUBLISHED" } });
594
- const worker = createDecoWorkerEntry(REFLECT_ENTRY, { observability: false });
722
+ setBlocks({
723
+ "pages-home": { name: "home", path: "/", title: "PUBLISHED" },
724
+ });
725
+ const worker = createDecoWorkerEntry(REFLECT_ENTRY, {
726
+ observability: false,
727
+ });
595
728
  const res = await worker.fetch(
596
729
  new Request(`https://example.com/?__draft=${POINTER}`, {
597
730
  headers: { host: "example.com" },