@decocms/blocks 7.31.0 → 7.31.2

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/blocks",
3
- "version": "7.31.0",
3
+ "version": "7.31.2",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -75,27 +75,45 @@ describe("previewApiOriginForHost", () => {
75
75
  expect(previewApiOriginForHost("abc.local.studio.decocms.com", {})).toBe(
76
76
  "https://abc.local.studio.decocms.com",
77
77
  );
78
- expect(previewApiOriginForHost("abc.localhost:60534", {})).toBe("http://abc.localhost:60534");
78
+ expect(previewApiOriginForHost("abc.localhost:60534", {})).toBe(
79
+ "http://abc.localhost:60534",
80
+ );
81
+ // -stg is its own suffix, not a substring match of .preview-studio.decocms.com —
82
+ // both must be listed explicitly (regressed once: staging draft pointers
83
+ // silently fell back to published content with no visible error).
84
+ expect(
85
+ previewApiOriginForHost("abc.preview-studio-stg.decocms.com", {}),
86
+ ).toBe("https://abc.preview-studio-stg.decocms.com");
79
87
  });
80
88
 
81
89
  it("rejects hosts outside the domains — the token proposes, config disposes", () => {
82
90
  expect(previewApiOriginForHost("evil.example", {})).toBeNull();
83
91
  // Dot-prefixed suffixes guarantee a label boundary: a lookalike domain
84
92
  // that merely ends with the same characters cannot pass.
85
- expect(previewApiOriginForHost("evil-preview-studio.decocms.com", {})).toBeNull();
93
+ expect(
94
+ previewApiOriginForHost("evil-preview-studio.decocms.com", {}),
95
+ ).toBeNull();
86
96
  // The domain itself (no label in front) is not a draft host.
87
- expect(previewApiOriginForHost("preview-studio.decocms.com", {})).toBeNull();
97
+ expect(
98
+ previewApiOriginForHost("preview-studio.decocms.com", {}),
99
+ ).toBeNull();
88
100
  });
89
101
 
90
102
  it("allows an explicit port only under localhost-ish domains", () => {
91
103
  // A public-domain token must not steer the fetch at odd ports.
92
- expect(previewApiOriginForHost("abc.preview-studio.decocms.com:8500", {})).toBeNull();
104
+ expect(
105
+ previewApiOriginForHost("abc.preview-studio.decocms.com:8500", {}),
106
+ ).toBeNull();
93
107
  });
94
108
 
95
109
  it("honours a configured override instead of the defaults", () => {
96
110
  const env = { DECO_PREVIEW_API_DOMAINS: ".staging.example" };
97
- expect(previewApiOriginForHost("abc.staging.example", env)).toBe("https://abc.staging.example");
98
- expect(previewApiOriginForHost("abc.preview-studio.decocms.com", env)).toBeNull();
111
+ expect(previewApiOriginForHost("abc.staging.example", env)).toBe(
112
+ "https://abc.staging.example",
113
+ );
114
+ expect(
115
+ previewApiOriginForHost("abc.preview-studio.decocms.com", env),
116
+ ).toBeNull();
99
117
  });
100
118
  });
101
119
 
@@ -129,7 +147,9 @@ describe("resolveDraftDecofile", () => {
129
147
  });
130
148
 
131
149
  expect(blocks).toEqual({ "pages-home": { title: "draft" } });
132
- expect(calls).toEqual(["https://abc.preview-studio.decocms.com/_sandbox/decofile"]);
150
+ expect(calls).toEqual([
151
+ "https://abc.preview-studio.decocms.com/_sandbox/decofile",
152
+ ]);
133
153
  });
134
154
 
135
155
  it("is inert without a host allowlist — no fetch at all", async () => {
@@ -168,8 +188,16 @@ describe("resolveDraftDecofile", () => {
168
188
  }) as unknown as typeof fetch;
169
189
  const P = "abc.preview-studio.decocms.com";
170
190
 
171
- const a = await resolveDraftDecofile({ pointer: `${P}@v1`, env: ENV_ON, fetchImpl });
172
- const b = await resolveDraftDecofile({ pointer: `${P}@v1`, env: ENV_ON, fetchImpl });
191
+ const a = await resolveDraftDecofile({
192
+ pointer: `${P}@v1`,
193
+ env: ENV_ON,
194
+ fetchImpl,
195
+ });
196
+ const b = await resolveDraftDecofile({
197
+ pointer: `${P}@v1`,
198
+ env: ENV_ON,
199
+ fetchImpl,
200
+ });
173
201
  expect(fetches).toBe(1);
174
202
  expect(b).toBe(a);
175
203
 
@@ -186,7 +214,11 @@ describe("resolveDraftDecofile", () => {
186
214
  const P = "abc.preview-studio.decocms.com";
187
215
 
188
216
  for (const v of ["v1", "v2", "v3", "v4"]) {
189
- await resolveDraftDecofile({ pointer: `${P}@${v}`, env: ENV_ON, fetchImpl });
217
+ await resolveDraftDecofile({
218
+ pointer: `${P}@${v}`,
219
+ env: ENV_ON,
220
+ fetchImpl,
221
+ });
190
222
  }
191
223
  expect(fetches).toBe(4);
192
224
  await resolveDraftDecofile({ pointer: `${P}@v1`, env: ENV_ON, fetchImpl });
@@ -225,6 +257,7 @@ describe("DEFAULT_PREVIEW_API_DOMAINS", () => {
225
257
  it("ships the deco-operated origins, dot-prefixed", () => {
226
258
  expect(DEFAULT_PREVIEW_API_DOMAINS).toEqual([
227
259
  ".preview-studio.decocms.com",
260
+ ".preview-studio-stg.decocms.com",
228
261
  ".local.studio.decocms.com",
229
262
  ".localhost",
230
263
  ]);
@@ -261,10 +294,17 @@ describe("site-block preview hosts", () => {
261
294
 
262
295
  describe("draftPointerFromRequest", () => {
263
296
  it("reads the pointer from the __deco_draft cookie (in-preview navigation)", () => {
264
- const req = new Request("https://preview.example/deco/invoke/site/loaders/x.ts", {
265
- headers: { cookie: "a=1; __deco_draft=abc.preview-studio.decocms.com@v1; b=2" },
266
- });
267
- expect(draftPointerFromRequest(req)).toBe("abc.preview-studio.decocms.com@v1");
297
+ const req = new Request(
298
+ "https://preview.example/deco/invoke/site/loaders/x.ts",
299
+ {
300
+ headers: {
301
+ cookie: "a=1; __deco_draft=abc.preview-studio.decocms.com@v1; b=2",
302
+ },
303
+ },
304
+ );
305
+ expect(draftPointerFromRequest(req)).toBe(
306
+ "abc.preview-studio.decocms.com@v1",
307
+ );
268
308
  });
269
309
 
270
310
  it("lets ?__draft= win over the cookie", () => {
@@ -282,26 +322,35 @@ describe("draftPointerFromRequest", () => {
282
322
  });
283
323
 
284
324
  it("returns null with neither param nor cookie", () => {
285
- expect(draftPointerFromRequest(new Request("https://preview.example/p"))).toBeNull();
325
+ expect(
326
+ draftPointerFromRequest(new Request("https://preview.example/p")),
327
+ ).toBeNull();
286
328
  });
287
329
  });
288
330
 
289
331
  describe("resolveDraftForRequest", () => {
290
332
  const ENV = { DECO_ALLOWED_PREVIEW_HOSTS: "preview.example" };
291
333
  function invokeReq(host = "preview.example"): Request {
292
- return new Request("https://preview.example/deco/invoke/site/loaders/x.ts", {
293
- method: "POST",
294
- headers: {
295
- "x-forwarded-host": host,
296
- cookie: "__deco_draft=abc.preview-studio.decocms.com@v1",
334
+ return new Request(
335
+ "https://preview.example/deco/invoke/site/loaders/x.ts",
336
+ {
337
+ method: "POST",
338
+ headers: {
339
+ "x-forwarded-host": host,
340
+ cookie: "__deco_draft=abc.preview-studio.decocms.com@v1",
341
+ },
297
342
  },
298
- });
343
+ );
299
344
  }
300
345
 
301
346
  it("binds the draft when host is allowed and the pointer resolves", async () => {
302
347
  const fetchImpl = (async () =>
303
- jsonResponse({ "site/x": { value: "draft" } })) as unknown as typeof fetch;
304
- expect(await resolveDraftForRequest(invokeReq(), { env: ENV, fetchImpl })).toEqual({
348
+ jsonResponse({
349
+ "site/x": { value: "draft" },
350
+ })) as unknown as typeof fetch;
351
+ expect(
352
+ await resolveDraftForRequest(invokeReq(), { env: ENV, fetchImpl }),
353
+ ).toEqual({
305
354
  "site/x": { value: "draft" },
306
355
  });
307
356
  });
@@ -312,7 +361,9 @@ describe("resolveDraftForRequest", () => {
312
361
  called = true;
313
362
  return jsonResponse({});
314
363
  }) as unknown as typeof fetch;
315
- expect(await resolveDraftForRequest(invokeReq(), { env: {}, fetchImpl })).toBeNull();
364
+ expect(
365
+ await resolveDraftForRequest(invokeReq(), { env: {}, fetchImpl }),
366
+ ).toBeNull();
316
367
  expect(called).toBe(false);
317
368
  });
318
369
 
@@ -323,21 +374,29 @@ describe("resolveDraftForRequest", () => {
323
374
  return jsonResponse({});
324
375
  }) as unknown as typeof fetch;
325
376
  expect(
326
- await resolveDraftForRequest(invokeReq("prod.example"), { env: ENV, fetchImpl }),
377
+ await resolveDraftForRequest(invokeReq("prod.example"), {
378
+ env: ENV,
379
+ fetchImpl,
380
+ }),
327
381
  ).toBeNull();
328
382
  expect(called).toBe(false);
329
383
  });
330
384
 
331
385
  it("returns null when the request carries no draft pointer", async () => {
332
- const req = new Request("https://preview.example/deco/invoke/site/loaders/x.ts", {
333
- headers: { "x-forwarded-host": "preview.example" },
334
- });
386
+ const req = new Request(
387
+ "https://preview.example/deco/invoke/site/loaders/x.ts",
388
+ {
389
+ headers: { "x-forwarded-host": "preview.example" },
390
+ },
391
+ );
335
392
  let called = false;
336
393
  const fetchImpl = (async () => {
337
394
  called = true;
338
395
  return jsonResponse({});
339
396
  }) as unknown as typeof fetch;
340
- expect(await resolveDraftForRequest(req, { env: ENV, fetchImpl })).toBeNull();
397
+ expect(
398
+ await resolveDraftForRequest(req, { env: ENV, fetchImpl }),
399
+ ).toBeNull();
341
400
  expect(called).toBe(false);
342
401
  });
343
402
  });
@@ -39,7 +39,8 @@ export interface DraftPointer {
39
39
  }
40
40
 
41
41
  /** Lowercase DNS hostname, at least two labels (a bare label can't match any domain). */
42
- const HOST_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
42
+ const HOST_RE =
43
+ /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
43
44
  const PORT_RE = /^[0-9]{1,5}$/;
44
45
  const VERSION_RE = /^[A-Za-z0-9._-]{1,64}$/;
45
46
 
@@ -48,7 +49,9 @@ const VERSION_RE = /^[A-Za-z0-9._-]{1,64}$/;
48
49
  * back to published content. Requires EXACTLY one `@`: a naive split accepts
49
50
  * `a@b@c` and silently uses the first two segments.
50
51
  */
51
- export function parseDraftPointer(raw: string | null | undefined): DraftPointer | null {
52
+ export function parseDraftPointer(
53
+ raw: string | null | undefined,
54
+ ): DraftPointer | null {
52
55
  if (!raw) return null;
53
56
  const parts = raw.split("@");
54
57
  if (parts.length !== 2) return null;
@@ -71,6 +74,7 @@ export function parseDraftPointer(raw: string | null | undefined): DraftPointer
71
74
  */
72
75
  export const DEFAULT_PREVIEW_API_DOMAINS = [
73
76
  ".preview-studio.decocms.com",
77
+ ".preview-studio-stg.decocms.com",
74
78
  ".local.studio.decocms.com",
75
79
  ".localhost",
76
80
  ];
@@ -161,7 +165,9 @@ export function isDraftHostAllowed(
161
165
  env?: Record<string, string | undefined>,
162
166
  ): boolean {
163
167
  if (!host) return false;
164
- return readAllowedHosts(envOrProcess(env)).includes(host.trim().toLowerCase());
168
+ return readAllowedHosts(envOrProcess(env)).includes(
169
+ host.trim().toLowerCase(),
170
+ );
165
171
  }
166
172
 
167
173
  /**
@@ -170,7 +176,9 @@ export function isDraftHostAllowed(
170
176
  * unconfigured site never loses static/ISR rendering. The per-request host
171
177
  * match happens later, in `isDraftHostAllowed`.
172
178
  */
173
- export function isDraftPreviewEnabled(env?: Record<string, string | undefined>): boolean {
179
+ export function isDraftPreviewEnabled(
180
+ env?: Record<string, string | undefined>,
181
+ ): boolean {
174
182
  return readAllowedHosts(envOrProcess(env)).length > 0;
175
183
  }
176
184
 
@@ -179,7 +187,8 @@ function envOrProcess(
179
187
  ): Record<string, string | undefined> {
180
188
  return (
181
189
  env ??
182
- (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ??
190
+ (globalThis as { process?: { env?: Record<string, string | undefined> } })
191
+ .process?.env ??
183
192
  {}
184
193
  );
185
194
  }
@@ -277,7 +286,10 @@ export const DRAFT_COOKIE_NAME = "__deco_draft";
277
286
  export const DRAFT_QUERY_PARAM = "__draft";
278
287
 
279
288
  /** Read one cookie value out of a raw `Cookie:` header. */
280
- function readCookieValue(cookieHeader: string | null | undefined, name: string): string | null {
289
+ function readCookieValue(
290
+ cookieHeader: string | null | undefined,
291
+ name: string,
292
+ ): string | null {
281
293
  if (!cookieHeader) return null;
282
294
  for (const part of cookieHeader.split(";")) {
283
295
  const eq = part.indexOf("=");
@@ -331,7 +343,8 @@ export async function resolveDraftForRequest(
331
343
  if (readAllowedHosts(env).length === 0) return null;
332
344
  const pointer = draftPointerFromRequest(request);
333
345
  if (!pointer) return null;
334
- const host = request.headers.get("x-forwarded-host") ?? request.headers.get("host");
346
+ const host =
347
+ request.headers.get("x-forwarded-host") ?? request.headers.get("host");
335
348
  if (!isDraftHostAllowed(host, env)) return null;
336
349
  return resolveDraftDecofile({ pointer, env, fetchImpl: options.fetchImpl });
337
350
  }
@@ -357,6 +370,9 @@ export function setDraftOverrideGetter(getter: DraftOverrideGetter): void {
357
370
  }
358
371
 
359
372
  /** The current request's draft blocks, if a binding registered one. */
360
- export function getRequestDraftOverride(): Record<string, unknown> | null | undefined {
373
+ export function getRequestDraftOverride():
374
+ | Record<string, unknown>
375
+ | null
376
+ | undefined {
361
377
  return getDraftOverride();
362
378
  }