@decocms/blocks 7.64.3 → 7.65.1
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 +2 -1
- package/src/cms/draftSource.test.ts +30 -30
- package/src/cms/draftSource.ts +1 -1
- package/src/cms/layoutCacheRace.test.ts +1 -1
- package/src/cms/loader.test.ts +11 -11
- package/src/cms/loader.ts +3 -3
- package/src/cms/resolve.test.ts +1 -1
- package/src/cms/resolve.ts +1 -1
- package/src/hooks/LiveControls.tsx +4 -1
- package/src/middleware/observability.test.ts +1 -1
- package/src/middleware/observability.ts +1 -1
- package/src/preview/DraftPreviewBadge.render.test.tsx +4 -4
- package/src/preview/DraftPreviewBadge.test.ts +10 -10
- package/src/sdk/abTesting.test.ts +2 -2
- package/src/sdk/experiments.test.ts +5 -5
- package/src/sdk/logger.test.ts +3 -3
- package/src/sdk/nonce.ts +46 -0
- package/src/sdk/normalizeUrls.ts +2 -2
- package/src/sdk/otel.test.ts +2 -2
- package/src/sdk/otel.ts +1 -1
- package/src/sdk/useSuggestions.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/blocks",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.65.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"./sdk/invoke": "./src/sdk/invoke.ts",
|
|
66
66
|
"./sdk/logger": "./src/sdk/logger.ts",
|
|
67
67
|
"./sdk/mergeCacheControl": "./src/sdk/mergeCacheControl.ts",
|
|
68
|
+
"./sdk/nonce": "./src/sdk/nonce.ts",
|
|
68
69
|
"./sdk/normalizeUrls": "./src/sdk/normalizeUrls.ts",
|
|
69
70
|
"./sdk/observability": "./src/sdk/observability.ts",
|
|
70
71
|
"./sdk/otel": "./src/sdk/otel.ts",
|
|
@@ -32,11 +32,11 @@ describe("parseDraftPointer", () => {
|
|
|
32
32
|
it("parses <authority><path>@<version>, lowercasing the authority", () => {
|
|
33
33
|
expect(
|
|
34
34
|
parseDraftPointer(
|
|
35
|
-
"Studio.decocms.com/api/
|
|
35
|
+
"Studio.decocms.com/api/acme/decofile/vm-1/main?token=Tok.abc@8c1d44e",
|
|
36
36
|
),
|
|
37
37
|
).toEqual({
|
|
38
38
|
host: "studio.decocms.com",
|
|
39
|
-
path: "/api/
|
|
39
|
+
path: "/api/acme/decofile/vm-1/main?token=Tok.abc",
|
|
40
40
|
version: "8c1d44e",
|
|
41
41
|
});
|
|
42
42
|
});
|
|
@@ -138,18 +138,18 @@ describe("gating", () => {
|
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
it("matches request hosts verbatim, port included, case-insensitively", () => {
|
|
141
|
-
const env = { DECO_ALLOWED_PREVIEW_HOSTS: "
|
|
142
|
-
expect(isDraftHostAllowed("
|
|
141
|
+
const env = { DECO_ALLOWED_PREVIEW_HOSTS: "acme.vtex.app, localhost:3100" };
|
|
142
|
+
expect(isDraftHostAllowed("ACME.VTEX.APP", env)).toBe(true);
|
|
143
143
|
expect(isDraftHostAllowed("localhost:3100", env)).toBe(true);
|
|
144
|
-
expect(isDraftHostAllowed("
|
|
144
|
+
expect(isDraftHostAllowed("acme.com.br", env)).toBe(false);
|
|
145
145
|
expect(isDraftHostAllowed("localhost", env)).toBe(false);
|
|
146
146
|
expect(isDraftHostAllowed(null, env)).toBe(false);
|
|
147
|
-
expect(isDraftHostAllowed("
|
|
147
|
+
expect(isDraftHostAllowed("acme.vtex.app", {})).toBe(false);
|
|
148
148
|
});
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
// Canonical Studio decofile-API pointer prefix (authority + path, no version).
|
|
152
|
-
const P = "studio.decocms.com/api/
|
|
152
|
+
const P = "studio.decocms.com/api/acme/decofile/vm-1/main?token=tok.abc";
|
|
153
153
|
|
|
154
154
|
describe("resolveDraftDecofile", () => {
|
|
155
155
|
it("fetches the token's path on its validated origin", async () => {
|
|
@@ -168,7 +168,7 @@ describe("resolveDraftDecofile", () => {
|
|
|
168
168
|
// content-addressed so Studio can answer with CDN-cacheable immutable
|
|
169
169
|
// headers when it matches the served sha.
|
|
170
170
|
expect(calls).toEqual([
|
|
171
|
-
"https://studio.decocms.com/api/
|
|
171
|
+
"https://studio.decocms.com/api/acme/decofile/vm-1/main?token=tok.abc&v=v1",
|
|
172
172
|
]);
|
|
173
173
|
});
|
|
174
174
|
|
|
@@ -298,10 +298,10 @@ describe("DEFAULT_PREVIEW_API_DOMAINS", () => {
|
|
|
298
298
|
|
|
299
299
|
describe("site-block preview hosts", () => {
|
|
300
300
|
it("enables the feature from the site block alone — no env needed", () => {
|
|
301
|
-
setDraftPreviewHosts(["
|
|
301
|
+
setDraftPreviewHosts(["acme.vtex.app", "LOCALHOST:3100", 42, " "]);
|
|
302
302
|
try {
|
|
303
303
|
expect(isDraftPreviewEnabled({})).toBe(true);
|
|
304
|
-
expect(isDraftHostAllowed("
|
|
304
|
+
expect(isDraftHostAllowed("acme.vtex.app", {})).toBe(true);
|
|
305
305
|
// Sanitized: lowercased, non-strings and blanks dropped.
|
|
306
306
|
expect(isDraftHostAllowed("localhost:3100", {})).toBe(true);
|
|
307
307
|
expect(isDraftHostAllowed("evil.example", {})).toBe(false);
|
|
@@ -311,13 +311,13 @@ describe("site-block preview hosts", () => {
|
|
|
311
311
|
});
|
|
312
312
|
|
|
313
313
|
it("env REPLACES the block hosts when set — the operational escape hatch", () => {
|
|
314
|
-
setDraftPreviewHosts(["
|
|
314
|
+
setDraftPreviewHosts(["acme.vtex.app"]);
|
|
315
315
|
try {
|
|
316
316
|
const env = { DECO_ALLOWED_PREVIEW_HOSTS: "other.example" };
|
|
317
317
|
expect(isDraftHostAllowed("other.example", env)).toBe(true);
|
|
318
318
|
// Not merged: env is a kill switch / override, so the block value must
|
|
319
319
|
// not survive alongside it.
|
|
320
|
-
expect(isDraftHostAllowed("
|
|
320
|
+
expect(isDraftHostAllowed("acme.vtex.app", env)).toBe(false);
|
|
321
321
|
} finally {
|
|
322
322
|
setDraftPreviewHosts([]);
|
|
323
323
|
}
|
|
@@ -326,41 +326,41 @@ describe("site-block preview hosts", () => {
|
|
|
326
326
|
|
|
327
327
|
describe("deco-hosted preview domains (setDecoSiteHost)", () => {
|
|
328
328
|
it("infers <site>.deco.site and enables the feature", () => {
|
|
329
|
-
setDecoSiteHost("
|
|
329
|
+
setDecoSiteHost("acme");
|
|
330
330
|
try {
|
|
331
331
|
expect(isDraftPreviewEnabled({})).toBe(true);
|
|
332
|
-
expect(isDraftHostAllowed("
|
|
332
|
+
expect(isDraftHostAllowed("acme.deco.site", {})).toBe(true);
|
|
333
333
|
expect(isDraftHostAllowed("other.deco.site", {})).toBe(false);
|
|
334
334
|
// A custom production domain is never inferred.
|
|
335
|
-
expect(isDraftHostAllowed("www.
|
|
335
|
+
expect(isDraftHostAllowed("www.acme.com", {})).toBe(false);
|
|
336
336
|
} finally {
|
|
337
337
|
setDecoSiteHost(null);
|
|
338
338
|
}
|
|
339
339
|
});
|
|
340
340
|
|
|
341
341
|
it("infers the <site>.deco-cx.workers.dev deploy host", () => {
|
|
342
|
-
setDecoSiteHost("
|
|
342
|
+
setDecoSiteHost("acme-tanstack");
|
|
343
343
|
try {
|
|
344
|
-
expect(isDraftHostAllowed("
|
|
344
|
+
expect(isDraftHostAllowed("acme-tanstack.deco-cx.workers.dev", {})).toBe(true);
|
|
345
345
|
// Exact match only — another worker on the same account is not admitted.
|
|
346
346
|
expect(isDraftHostAllowed("other-site.deco-cx.workers.dev", {})).toBe(false);
|
|
347
347
|
// No nested subdomain widens the match.
|
|
348
348
|
expect(
|
|
349
|
-
isDraftHostAllowed("
|
|
349
|
+
isDraftHostAllowed("acme-tanstack.evil.deco-cx.workers.dev", {}),
|
|
350
350
|
).toBe(false);
|
|
351
351
|
// Wrong apex.
|
|
352
|
-
expect(isDraftHostAllowed("
|
|
352
|
+
expect(isDraftHostAllowed("acme-tanstack.deco-cx.workers.example", {})).toBe(false);
|
|
353
353
|
} finally {
|
|
354
354
|
setDecoSiteHost(null);
|
|
355
355
|
}
|
|
356
356
|
});
|
|
357
357
|
|
|
358
358
|
it("is merged ON TOP of the site block, not replacing it", () => {
|
|
359
|
-
setDraftPreviewHosts(["
|
|
360
|
-
setDecoSiteHost("
|
|
359
|
+
setDraftPreviewHosts(["acme.vtex.app"]);
|
|
360
|
+
setDecoSiteHost("acme");
|
|
361
361
|
try {
|
|
362
|
-
expect(isDraftHostAllowed("
|
|
363
|
-
expect(isDraftHostAllowed("
|
|
362
|
+
expect(isDraftHostAllowed("acme.vtex.app", {})).toBe(true);
|
|
363
|
+
expect(isDraftHostAllowed("acme.deco.site", {})).toBe(true);
|
|
364
364
|
} finally {
|
|
365
365
|
setDraftPreviewHosts([]);
|
|
366
366
|
setDecoSiteHost(null);
|
|
@@ -368,11 +368,11 @@ describe("deco-hosted preview domains (setDecoSiteHost)", () => {
|
|
|
368
368
|
});
|
|
369
369
|
|
|
370
370
|
it("is merged ON TOP of the env escape hatch too", () => {
|
|
371
|
-
setDecoSiteHost("
|
|
371
|
+
setDecoSiteHost("acme");
|
|
372
372
|
try {
|
|
373
373
|
const env = { DECO_ALLOWED_PREVIEW_HOSTS: "other.example" };
|
|
374
374
|
expect(isDraftHostAllowed("other.example", env)).toBe(true);
|
|
375
|
-
expect(isDraftHostAllowed("
|
|
375
|
+
expect(isDraftHostAllowed("acme.deco.site", env)).toBe(true);
|
|
376
376
|
} finally {
|
|
377
377
|
setDecoSiteHost(null);
|
|
378
378
|
}
|
|
@@ -392,16 +392,16 @@ describe("deco-hosted preview domains (setDecoSiteHost)", () => {
|
|
|
392
392
|
});
|
|
393
393
|
|
|
394
394
|
it("DECO_ALLOWED_PREVIEW_HOSTS=none kills the inferred hosts too", () => {
|
|
395
|
-
setDraftPreviewHosts(["
|
|
396
|
-
setDecoSiteHost("
|
|
395
|
+
setDraftPreviewHosts(["acme.vtex.app"]);
|
|
396
|
+
setDecoSiteHost("acme");
|
|
397
397
|
try {
|
|
398
398
|
// The kill switch wins over the inferred hosts AND the site block, so a
|
|
399
399
|
// bad rollout can be stopped without a deploy.
|
|
400
400
|
const env = { DECO_ALLOWED_PREVIEW_HOSTS: "none" };
|
|
401
401
|
expect(isDraftPreviewEnabled(env)).toBe(false);
|
|
402
|
-
expect(isDraftHostAllowed("
|
|
403
|
-
expect(isDraftHostAllowed("
|
|
404
|
-
expect(isDraftHostAllowed("
|
|
402
|
+
expect(isDraftHostAllowed("acme.deco.site", env)).toBe(false);
|
|
403
|
+
expect(isDraftHostAllowed("acme.deco-cx.workers.dev", env)).toBe(false);
|
|
404
|
+
expect(isDraftHostAllowed("acme.vtex.app", env)).toBe(false);
|
|
405
405
|
} finally {
|
|
406
406
|
setDraftPreviewHosts([]);
|
|
407
407
|
setDecoSiteHost(null);
|
package/src/cms/draftSource.ts
CHANGED
|
@@ -179,7 +179,7 @@ export function setDraftPreviewHosts(hosts: readonly unknown[]): void {
|
|
|
179
179
|
*
|
|
180
180
|
* - `<site>.deco.site` — the stable deco-hosted domain.
|
|
181
181
|
* - `<site>.deco-cx.workers.dev` — the workers.dev deploy URL
|
|
182
|
-
* (e.g. `
|
|
182
|
+
* (e.g. `acme-tanstack.deco-cx.workers.dev`).
|
|
183
183
|
*
|
|
184
184
|
* Both are MERGED with the site-block/env list rather than replacing it: they
|
|
185
185
|
* are deco-operated infra, so a signed draft grant can preview there out of the
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
* Regression test for the layout-cache index-corruption bug that shipped in
|
|
14
14
|
* @decocms/start@6.12.1 and was fixed in 6.12.2 (blocks commit 73d4f19,
|
|
15
15
|
* "fix(cms): don't mutate shared cached section objects when stamping
|
|
16
|
-
* index"). Two live production sites
|
|
16
|
+
* index"). Two live production sites
|
|
17
17
|
* hit this in the wild as an intermittent "footer renders above other
|
|
18
18
|
* sections" bug under concurrent traffic.
|
|
19
19
|
*
|
package/src/cms/loader.test.ts
CHANGED
|
@@ -78,32 +78,32 @@ describe("matchPath", () => {
|
|
|
78
78
|
// These are the cases that issue #213 documents as broken.
|
|
79
79
|
|
|
80
80
|
it("matches with the optional group present", () => {
|
|
81
|
-
expect(matchPath("/{
|
|
81
|
+
expect(matchPath("/{acme/}?*", "/acme/perfumaria")).toEqual({
|
|
82
82
|
"0": "perfumaria",
|
|
83
83
|
});
|
|
84
84
|
});
|
|
85
85
|
|
|
86
86
|
it("matches with the optional group absent", () => {
|
|
87
|
-
expect(matchPath("/{
|
|
87
|
+
expect(matchPath("/{acme/}?*", "/perfumaria")).toEqual({
|
|
88
88
|
"0": "perfumaria",
|
|
89
89
|
});
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
it("matches root when optional prefix and splat collapse to empty", () => {
|
|
93
|
-
expect(matchPath("/{
|
|
93
|
+
expect(matchPath("/{acme/}?*", "/")).toEqual({ "0": "" });
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
it("matches with an optional prefix before a literal segment", () => {
|
|
97
97
|
expect(
|
|
98
98
|
matchPath(
|
|
99
|
-
"/{
|
|
100
|
-
"/
|
|
99
|
+
"/{acme/}?campanhas/*",
|
|
100
|
+
"/acme/campanhas/destaques-2023",
|
|
101
101
|
),
|
|
102
102
|
).toEqual({
|
|
103
103
|
"0": "destaques-2023",
|
|
104
104
|
});
|
|
105
105
|
expect(
|
|
106
|
-
matchPath("/{
|
|
106
|
+
matchPath("/{acme/}?campanhas/*", "/campanhas/destaques-2023"),
|
|
107
107
|
).toEqual({
|
|
108
108
|
"0": "destaques-2023",
|
|
109
109
|
});
|
|
@@ -158,7 +158,7 @@ describe("findPageByPath specificity", () => {
|
|
|
158
158
|
},
|
|
159
159
|
"pages-pdp-plp": {
|
|
160
160
|
name: "PDP & PLP",
|
|
161
|
-
path: "/{
|
|
161
|
+
path: "/{acme/}?*",
|
|
162
162
|
sections: [],
|
|
163
163
|
},
|
|
164
164
|
"pages-product": {
|
|
@@ -179,8 +179,8 @@ describe("findPageByPath specificity", () => {
|
|
|
179
179
|
});
|
|
180
180
|
|
|
181
181
|
it("prefers the home page over an optional-group splat catch-all", () => {
|
|
182
|
-
// Regression: /{
|
|
183
|
-
// because the `{
|
|
182
|
+
// Regression: /{acme/}?* matches "/" and was out-ranking the home
|
|
183
|
+
// because the `{acme` segment counted as a param. The home block
|
|
184
184
|
// is a literal-only `/` path and must always win.
|
|
185
185
|
setBlocks({
|
|
186
186
|
"pages-home": {
|
|
@@ -190,7 +190,7 @@ describe("findPageByPath specificity", () => {
|
|
|
190
190
|
},
|
|
191
191
|
"pages-pdp-plp": {
|
|
192
192
|
name: "PDP & PLP",
|
|
193
|
-
path: "/{
|
|
193
|
+
path: "/{acme/}?*",
|
|
194
194
|
sections: [],
|
|
195
195
|
},
|
|
196
196
|
});
|
|
@@ -228,7 +228,7 @@ describe("loadBlocks draft override — key percent-encoding", () => {
|
|
|
228
228
|
// (`pages-Home (principal)-1`). Under snapshot semantics the draft REPLACES
|
|
229
229
|
// the file-backed base wholesale, so an encoded/raw twin pair can never
|
|
230
230
|
// coexist — these regression tests (from the merge era, when ~73% of
|
|
231
|
-
//
|
|
231
|
+
// one production site's pages silently ignored drafts) now pin that property.
|
|
232
232
|
|
|
233
233
|
afterEach(() => {
|
|
234
234
|
setBlocks({});
|
package/src/cms/loader.ts
CHANGED
|
@@ -254,13 +254,13 @@ export function withDraftBlocks<T>(
|
|
|
254
254
|
// `/` (literals=0) vs the catch-all `/{prefix/}?*` (literals=0, params=1).
|
|
255
255
|
// Without this, the URLPattern fix (#213/#214) inadvertently lets a
|
|
256
256
|
// `/{group/}?*` catch-all out-rank an exact `/` home page because the
|
|
257
|
-
// `{group` segment counted as a param. See
|
|
258
|
-
// where `/` was being routed to
|
|
257
|
+
// `{group` segment counted as a param. See a production storefront
|
|
258
|
+
// where `/` was being routed to a brand-prefixed PDP/PLP block's NotFound
|
|
259
259
|
// fallback.
|
|
260
260
|
//
|
|
261
261
|
// Order produced:
|
|
262
262
|
// /foo/bar (no wildcard, literals=2) > /foo/:x (no wildcard, lit=1, param=1)
|
|
263
|
-
// /foo (no wildcard) > /{
|
|
263
|
+
// /foo (no wildcard) > /{acme/}?* (has wildcard) > /*
|
|
264
264
|
function pathSpecificityKey(path: string): [number, number, number] {
|
|
265
265
|
const parts = path.split("/").filter(Boolean);
|
|
266
266
|
let literals = 0;
|
package/src/cms/resolve.test.ts
CHANGED
|
@@ -816,7 +816,7 @@ describe("extractSeoFromProps — commerce jsonLD structured data", () => {
|
|
|
816
816
|
});
|
|
817
817
|
|
|
818
818
|
it("derives title/description/canonical and emits the ItemList JSON-LD (PLP)", () => {
|
|
819
|
-
// The
|
|
819
|
+
// The acme.com.br/eletroportateis regression: a page.seo pointing at
|
|
820
820
|
// commerce/sections/Seo/SeoPLPV2.tsx resolved its jsonLD but emitted no
|
|
821
821
|
// schema.org. There is no section component in start to run this transform.
|
|
822
822
|
const seo = extractSeoFromProps({
|
package/src/cms/resolve.ts
CHANGED
|
@@ -1982,7 +1982,7 @@ function pruneCommerceJsonLD(
|
|
|
1982
1982
|
* ship loaders/types, not sections), so nothing else runs this transform.
|
|
1983
1983
|
* Without it, PLP/PDP pages whose `page.seo` points at those types emit a
|
|
1984
1984
|
* <title>/canonical (from top-level override props) but NO schema.org
|
|
1985
|
-
* structured data — the
|
|
1985
|
+
* structured data — the acme.com.br/eletroportateis regression.
|
|
1986
1986
|
*
|
|
1987
1987
|
* Manual override fields already read into `seo` always win; this only fills
|
|
1988
1988
|
* gaps and appends the JSON-LD. The caller guards on `seo.jsonLDs` so a real
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useEffect } from "react";
|
|
2
|
+
import { getRequestNonce } from "../sdk/nonce";
|
|
2
3
|
|
|
3
4
|
interface LiveControlsProps {
|
|
4
5
|
site?: string;
|
|
@@ -135,5 +136,7 @@ function LiveControlsScript() {
|
|
|
135
136
|
})();
|
|
136
137
|
`;
|
|
137
138
|
|
|
138
|
-
return
|
|
139
|
+
return (
|
|
140
|
+
<script type="module" nonce={getRequestNonce()} dangerouslySetInnerHTML={{ __html: script }} />
|
|
141
|
+
);
|
|
139
142
|
}
|
|
@@ -309,7 +309,7 @@ describe("normalizePath cardinality", () => {
|
|
|
309
309
|
it("collapses CMS content slugs", () => {
|
|
310
310
|
expect(normalizePath("/mochila-de-couro")).toBe("/:slug");
|
|
311
311
|
expect(normalizePath("/moveis/quarto-infantil")).toBe("/moveis/:slug");
|
|
312
|
-
expect(normalizePath("/
|
|
312
|
+
expect(normalizePath("/acme/eau-de-toilette-spritz-100ml")).toBe("/acme/:slug");
|
|
313
313
|
expect(normalizePath("/moveis/quarto-adulto/cabeceiras-mesa-de-cabeceiras")).toBe(
|
|
314
314
|
"/moveis/:slug/:slug",
|
|
315
315
|
);
|
|
@@ -737,7 +737,7 @@ export function normalizePath(path: string): string {
|
|
|
737
737
|
//
|
|
738
738
|
// /mochila-de-couro
|
|
739
739
|
// /moveis/quarto-infantil
|
|
740
|
-
// /
|
|
740
|
+
// /acme/eau-de-toilette-spritz-100ml
|
|
741
741
|
//
|
|
742
742
|
// Measured on the production ClickHouse, `http.route` reached 20,714
|
|
743
743
|
// distinct values across only 6 tenants — on `http.server.request.duration`,
|
|
@@ -11,7 +11,7 @@ describe("DraftPreviewBadge (render)", () => {
|
|
|
11
11
|
// pass, actually ships. The badge fails closed: nothing here, by design
|
|
12
12
|
// (see the module doc for why render-then-hide is worse).
|
|
13
13
|
it("renders nothing without a client to run the reveal effect", () => {
|
|
14
|
-
const html = renderToString(<DraftPreviewBadge pointer="
|
|
14
|
+
const html = renderToString(<DraftPreviewBadge pointer="acme.vtex.app@v1" />);
|
|
15
15
|
expect(html).toBe("");
|
|
16
16
|
});
|
|
17
17
|
|
|
@@ -34,14 +34,14 @@ describe("DraftPreviewBadge (render)", () => {
|
|
|
34
34
|
|
|
35
35
|
it("reveals the preview-mode label after mount, once confirmed unframed", () => {
|
|
36
36
|
act(() => {
|
|
37
|
-
root.render(<DraftPreviewBadge pointer="
|
|
37
|
+
root.render(<DraftPreviewBadge pointer="acme.vtex.app@v1" />);
|
|
38
38
|
});
|
|
39
39
|
expect(container.textContent).toContain("Preview mode");
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
it("embeds the deco mark inline (no external asset that could 404 on a consumer site)", () => {
|
|
43
43
|
act(() => {
|
|
44
|
-
root.render(<DraftPreviewBadge pointer="
|
|
44
|
+
root.render(<DraftPreviewBadge pointer="acme.vtex.app@v1" />);
|
|
45
45
|
});
|
|
46
46
|
expect(container.innerHTML).toContain("data:image/png;base64,");
|
|
47
47
|
});
|
|
@@ -58,7 +58,7 @@ describe("DraftPreviewBadge (render)", () => {
|
|
|
58
58
|
it("stays hidden — Studio's own preview surface already has its own chrome", () => {
|
|
59
59
|
Object.defineProperty(window, "top", { value: {}, configurable: true });
|
|
60
60
|
act(() => {
|
|
61
|
-
root.render(<DraftPreviewBadge pointer="
|
|
61
|
+
root.render(<DraftPreviewBadge pointer="acme.vtex.app@v1" />);
|
|
62
62
|
});
|
|
63
63
|
expect(container.textContent).toBe("");
|
|
64
64
|
});
|
|
@@ -4,34 +4,34 @@ import { buildExitUrl, buildShareUrl, isFramed } from "./DraftPreviewBadge";
|
|
|
4
4
|
|
|
5
5
|
describe("buildShareUrl", () => {
|
|
6
6
|
it("pins the exact draft version so a recipient sees the same working tree", () => {
|
|
7
|
-
expect(buildShareUrl("https://
|
|
8
|
-
"https://
|
|
7
|
+
expect(buildShareUrl("https://acme.vtex.app/pdp/shoe", "abc.localhost:60534@v7")).toBe(
|
|
8
|
+
"https://acme.vtex.app/pdp/shoe?__draft=abc.localhost%3A60534%40v7",
|
|
9
9
|
);
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
it("preserves existing query params, only setting the draft token", () => {
|
|
13
|
-
expect(buildShareUrl("https://
|
|
14
|
-
"https://
|
|
13
|
+
expect(buildShareUrl("https://acme.vtex.app/plp?sort=price", "h@v1")).toBe(
|
|
14
|
+
"https://acme.vtex.app/plp?sort=price&__draft=h%40v1",
|
|
15
15
|
);
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
it("replaces a stale token already in the URL rather than appending a second", () => {
|
|
19
|
-
expect(buildShareUrl("https://
|
|
20
|
-
"https://
|
|
19
|
+
expect(buildShareUrl("https://acme.vtex.app/p?__draft=h@old", "h@new")).toBe(
|
|
20
|
+
"https://acme.vtex.app/p?__draft=h%40new",
|
|
21
21
|
);
|
|
22
22
|
});
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
describe("buildExitUrl", () => {
|
|
26
26
|
it("sets the ?__draft=off exit sentinel", () => {
|
|
27
|
-
expect(buildExitUrl("https://
|
|
28
|
-
"https://
|
|
27
|
+
expect(buildExitUrl("https://acme.vtex.app/pdp/shoe")).toBe(
|
|
28
|
+
"https://acme.vtex.app/pdp/shoe?__draft=off",
|
|
29
29
|
);
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
it("overrides an active token with off (leaves preview from anywhere)", () => {
|
|
33
|
-
expect(buildExitUrl("https://
|
|
34
|
-
"https://
|
|
33
|
+
expect(buildExitUrl("https://acme.vtex.app/p?__draft=h@v1")).toBe(
|
|
34
|
+
"https://acme.vtex.app/p?__draft=off",
|
|
35
35
|
);
|
|
36
36
|
});
|
|
37
37
|
});
|
|
@@ -12,8 +12,8 @@ import {
|
|
|
12
12
|
// Test helpers
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
|
|
15
|
-
const REAL_HOST = "www.
|
|
16
|
-
const FALLBACK_HOST = "
|
|
15
|
+
const REAL_HOST = "www.acme.com.br";
|
|
16
|
+
const FALLBACK_HOST = "lojaacme.deco.site";
|
|
17
17
|
|
|
18
18
|
function makeUrl(path = "/x"): URL {
|
|
19
19
|
return new URL(`https://${REAL_HOST}${path}`);
|
|
@@ -305,16 +305,16 @@ describe("readExperimentConfig", () => {
|
|
|
305
305
|
const kv = (value: unknown) => ({ get: async () => value as never });
|
|
306
306
|
|
|
307
307
|
it("reads contract 1's document", async () => {
|
|
308
|
-
expect(await readExperimentConfig(kv(CONFIG), "www.
|
|
308
|
+
expect(await readExperimentConfig(kv(CONFIG), "www.acme.com")).toEqual(CONFIG);
|
|
309
309
|
});
|
|
310
310
|
|
|
311
311
|
it("returns null instead of throwing when the binding is missing", async () => {
|
|
312
|
-
expect(await readExperimentConfig(undefined, "www.
|
|
312
|
+
expect(await readExperimentConfig(undefined, "www.acme.com")).toBeNull();
|
|
313
313
|
});
|
|
314
314
|
|
|
315
315
|
it("returns null on an unset key or a malformed document", async () => {
|
|
316
|
-
expect(await readExperimentConfig(kv(null), "www.
|
|
317
|
-
expect(await readExperimentConfig(kv({ version: 1 }), "www.
|
|
316
|
+
expect(await readExperimentConfig(kv(null), "www.acme.com")).toBeNull();
|
|
317
|
+
expect(await readExperimentConfig(kv({ version: 1 }), "www.acme.com")).toBeNull();
|
|
318
318
|
});
|
|
319
319
|
|
|
320
320
|
it("returns null when KV itself fails — never a thrown request", async () => {
|
|
@@ -323,6 +323,6 @@ describe("readExperimentConfig", () => {
|
|
|
323
323
|
throw new Error("KV unavailable");
|
|
324
324
|
},
|
|
325
325
|
};
|
|
326
|
-
expect(await readExperimentConfig(broken, "www.
|
|
326
|
+
expect(await readExperimentConfig(broken, "www.acme.com")).toBeNull();
|
|
327
327
|
});
|
|
328
328
|
});
|
package/src/sdk/logger.test.ts
CHANGED
|
@@ -185,7 +185,7 @@ describe("setLoggerAttributeFloor", () => {
|
|
|
185
185
|
});
|
|
186
186
|
|
|
187
187
|
it("applies the floor even when the caller passes no attrs", () => {
|
|
188
|
-
setLoggerAttributeFloor({ tenant: "
|
|
188
|
+
setLoggerAttributeFloor({ tenant: "acme" });
|
|
189
189
|
const calls: Array<Record<string, unknown> | undefined> = [];
|
|
190
190
|
configureLogger({
|
|
191
191
|
log(_level, _msg, attrs) {
|
|
@@ -195,11 +195,11 @@ describe("setLoggerAttributeFloor", () => {
|
|
|
195
195
|
|
|
196
196
|
logger.info("no-attrs");
|
|
197
197
|
|
|
198
|
-
expect(calls[0]).toEqual({ tenant: "
|
|
198
|
+
expect(calls[0]).toEqual({ tenant: "acme" });
|
|
199
199
|
});
|
|
200
200
|
|
|
201
201
|
it("clearing the floor restores the no-op fast path", () => {
|
|
202
|
-
setLoggerAttributeFloor({ tenant: "
|
|
202
|
+
setLoggerAttributeFloor({ tenant: "acme" });
|
|
203
203
|
setLoggerAttributeFloor({});
|
|
204
204
|
const calls: Array<Record<string, unknown> | undefined> = [];
|
|
205
205
|
configureLogger({
|
package/src/sdk/nonce.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-request CSP nonce.
|
|
3
|
+
*
|
|
4
|
+
* When the TanStack worker entry runs with `cspMode: "enforce"`, it generates a
|
|
5
|
+
* fresh nonce per request, stashes it in the RequestContext bag (below), and
|
|
6
|
+
* threads it into:
|
|
7
|
+
*
|
|
8
|
+
* - the TanStack router (`router.options.ssr.nonce`), so `ScriptOnce` and the
|
|
9
|
+
* framework's own hydration scripts carry `nonce=…`;
|
|
10
|
+
* - the hand-written inline `<script>` tags in the deco layout / live-controls
|
|
11
|
+
* components (they read it via `getRequestNonce()`);
|
|
12
|
+
* - the enforced `Content-Security-Policy` header's `script-src`.
|
|
13
|
+
*
|
|
14
|
+
* Lives in `runtime` (not `tanstack`) so both the tanstack worker and the
|
|
15
|
+
* runtime components (e.g. LiveControls) can reach it without crossing the
|
|
16
|
+
* one-way package dependency graph.
|
|
17
|
+
*
|
|
18
|
+
* The nonce is only produced on the server and only in enforce mode; in the
|
|
19
|
+
* browser the RequestContext is a no-op stub, so `getRequestNonce()` returns
|
|
20
|
+
* `undefined` and every consumer degrades to "no nonce attribute".
|
|
21
|
+
*/
|
|
22
|
+
import { RequestContext } from "./requestContext";
|
|
23
|
+
|
|
24
|
+
/** RequestContext bag key holding the current request's CSP nonce. */
|
|
25
|
+
export const NONCE_BAG_KEY = "deco.csp.nonce";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate a CSP nonce: 128 bits of CSPRNG randomness, base64-encoded. Fresh
|
|
29
|
+
* per request — never reuse across responses, or the nonce loses all value.
|
|
30
|
+
*/
|
|
31
|
+
export function generateNonce(): string {
|
|
32
|
+
const bytes = new Uint8Array(16);
|
|
33
|
+
crypto.getRandomValues(bytes);
|
|
34
|
+
let binary = "";
|
|
35
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
36
|
+
return btoa(binary);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The current request's CSP nonce, or `undefined` when CSP is not enforced
|
|
41
|
+
* (report-only mode) or on the client. Read this in an SSR component to nonce
|
|
42
|
+
* an inline `<script>`: `<script nonce={getRequestNonce()} … />`.
|
|
43
|
+
*/
|
|
44
|
+
export function getRequestNonce(): string | undefined {
|
|
45
|
+
return RequestContext.getBag<string>(NONCE_BAG_KEY);
|
|
46
|
+
}
|
package/src/sdk/normalizeUrls.ts
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* // In site's setup.ts:
|
|
14
14
|
* import { registerProductionOrigins } from "@decocms/start/sdk/normalizeUrls";
|
|
15
15
|
* registerProductionOrigins([
|
|
16
|
-
* "https://www.
|
|
17
|
-
* "https://
|
|
16
|
+
* "https://www.acme.com.br",
|
|
17
|
+
* "https://acme.com.br",
|
|
18
18
|
* ]);
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
package/src/sdk/otel.test.ts
CHANGED
|
@@ -169,13 +169,13 @@ describe("instrumentWorker — identity floor", () => {
|
|
|
169
169
|
|
|
170
170
|
it("stamps service.name and service.version on the logger floor", async () => {
|
|
171
171
|
const handler = { fetch: vi.fn().mockResolvedValue(new Response("ok")) };
|
|
172
|
-
const wrapped = instrumentWorker(handler, { serviceName: "
|
|
172
|
+
const wrapped = instrumentWorker(handler, { serviceName: "acme-tanstack" });
|
|
173
173
|
|
|
174
174
|
const env: TestEnv = { CF_VERSION_METADATA: { id: "abc123" } };
|
|
175
175
|
await wrapped.fetch(new Request("https://example.test/"), env, fakeCtx());
|
|
176
176
|
|
|
177
177
|
const floor = logger._getLoggerAttributeFloorForTests();
|
|
178
|
-
expect(floor["service.name"]).toBe("
|
|
178
|
+
expect(floor["service.name"]).toBe("acme-tanstack");
|
|
179
179
|
expect(floor["service.version"]).toBe("abc123");
|
|
180
180
|
});
|
|
181
181
|
|
package/src/sdk/otel.ts
CHANGED
|
@@ -312,7 +312,7 @@ interface WorkerHandler {
|
|
|
312
312
|
// into Copy A's `otlpMeter` while the per-request `recordRequestMetric`
|
|
313
313
|
// closure reads from Copy B's empty `otlpMeter`. Net effect in prod:
|
|
314
314
|
// `bootObservability` runs (we observed the very first `POST /v1/metrics`
|
|
315
|
-
// from
|
|
315
|
+
// from a production storefront), but every subsequent request's `flush()` finds an
|
|
316
316
|
// empty buffer because the meter the framework writes into is a different
|
|
317
317
|
// instance from the one the exporter drains.
|
|
318
318
|
//
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Search-suggestions hook factory.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Two production storefronts independently
|
|
5
5
|
* invented the same shape for autocomplete-style suggestions:
|
|
6
6
|
*
|
|
7
7
|
* - module-level signal for the current payload + loading flag
|