@decocms/blocks 7.34.2 → 7.34.4
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 +1 -1
- package/src/cms/applySectionConventions.ts +8 -0
- package/src/cms/resolve.test.ts +17 -0
- package/src/cms/resolve.ts +28 -0
- package/src/sdk/crypto.test.ts +65 -0
- package/src/sdk/crypto.ts +16 -2
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@ import { registerSection, registerSectionsSync } from "./registry";
|
|
|
10
10
|
import {
|
|
11
11
|
type AsyncRenderingConfig,
|
|
12
12
|
getAsyncRenderingConfig,
|
|
13
|
+
registerAlwaysDeferSections,
|
|
13
14
|
registerEagerSections,
|
|
14
15
|
registerNeverDeferSections,
|
|
15
16
|
registerSeoSections,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
export interface SectionMetaEntry {
|
|
25
26
|
eager?: boolean;
|
|
26
27
|
neverDefer?: boolean;
|
|
28
|
+
deferred?: boolean;
|
|
27
29
|
cache?: string;
|
|
28
30
|
layout?: boolean;
|
|
29
31
|
sync?: boolean;
|
|
@@ -48,6 +50,7 @@ export function applySectionConventions(input: ApplySectionConventionsInput): vo
|
|
|
48
50
|
|
|
49
51
|
const eagerSections: string[] = [];
|
|
50
52
|
const neverDeferSections: string[] = [];
|
|
53
|
+
const alwaysDeferSections: string[] = [];
|
|
51
54
|
const layoutSections: string[] = [];
|
|
52
55
|
const seoSections: string[] = [];
|
|
53
56
|
const cacheableSections: Record<string, CacheableSectionInput> = {};
|
|
@@ -55,6 +58,7 @@ export function applySectionConventions(input: ApplySectionConventionsInput): vo
|
|
|
55
58
|
for (const [key, entry] of Object.entries(meta)) {
|
|
56
59
|
if (entry.eager) eagerSections.push(key);
|
|
57
60
|
if (entry.neverDefer) neverDeferSections.push(key);
|
|
61
|
+
if (entry.deferred) alwaysDeferSections.push(key);
|
|
58
62
|
if (entry.layout) layoutSections.push(key);
|
|
59
63
|
if (entry.seo) seoSections.push(key);
|
|
60
64
|
if (entry.cache) cacheableSections[key] = entry.cache as CacheableSectionInput;
|
|
@@ -88,6 +92,10 @@ export function applySectionConventions(input: ApplySectionConventionsInput): vo
|
|
|
88
92
|
registerNeverDeferSections(neverDeferSections);
|
|
89
93
|
}
|
|
90
94
|
|
|
95
|
+
if (alwaysDeferSections.length > 0) {
|
|
96
|
+
registerAlwaysDeferSections(alwaysDeferSections);
|
|
97
|
+
}
|
|
98
|
+
|
|
91
99
|
// Always initialize asyncConfig so CMS Lazy deferral (`respectCmsLazy`) is
|
|
92
100
|
// active — this is what makes the admin ⚡ toggle the source of truth. Without
|
|
93
101
|
// it, asyncConfig stays null, `useAsync` is false in resolveDecoPage, and
|
package/src/cms/resolve.test.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
isEagerRequest,
|
|
33
33
|
registerCommerceLoader,
|
|
34
34
|
registerEagerSections,
|
|
35
|
+
registerAlwaysDeferSections,
|
|
35
36
|
registerNeverDeferSections,
|
|
36
37
|
resolveDecoPage,
|
|
37
38
|
resolveDeferredSectionFull,
|
|
@@ -468,6 +469,22 @@ describe("shouldDeferSection — admin is the source of truth", () => {
|
|
|
468
469
|
expect(shouldDeferSection(section, 5, mkCfg({ foldThreshold: 3 }), false)).toBe(true);
|
|
469
470
|
expect(shouldDeferSection(section, 1, mkCfg({ foldThreshold: 3 }), false)).toBe(false);
|
|
470
471
|
});
|
|
472
|
+
|
|
473
|
+
it("`export const deferred = true` defers one section even with respectCmsLazy off", () => {
|
|
474
|
+
const key = "site/sections/HeavyPLP.tsx";
|
|
475
|
+
registerAlwaysDeferSections([key]);
|
|
476
|
+
const section = { __resolveType: key };
|
|
477
|
+
// Not ⚡-wrapped and respectCmsLazy disabled — the per-section flag is the
|
|
478
|
+
// only thing forcing deferral here.
|
|
479
|
+
expect(shouldDeferSection(section, 0, mkCfg({ respectCmsLazy: false }), false)).toBe(true);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it("`export const deferred = true` still stays eager for bots (SEO)", () => {
|
|
483
|
+
const key = "site/sections/HeavyPLPBot.tsx";
|
|
484
|
+
registerAlwaysDeferSections([key]);
|
|
485
|
+
const section = { __resolveType: key };
|
|
486
|
+
expect(shouldDeferSection(section, 0, mkCfg({ respectCmsLazy: false }), true)).toBe(false);
|
|
487
|
+
});
|
|
471
488
|
});
|
|
472
489
|
|
|
473
490
|
// ---------------------------------------------------------------------------
|
package/src/cms/resolve.ts
CHANGED
|
@@ -243,6 +243,28 @@ function isNeverDeferSection(key: string): boolean {
|
|
|
243
243
|
return (G.__deco.neverDeferSectionKeys as Set<string> | undefined)?.has(key) ?? false;
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// Permanent alwaysDefer section registry
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Register sections that declared `export const deferred = true`.
|
|
252
|
+
* These sections are ALWAYS deferred (rendered client-side on scroll),
|
|
253
|
+
* independent of the global `respectCmsLazy` config — the per-section opt-in
|
|
254
|
+
* for async-rendering a single heavy section (e.g. a PLP grid) without
|
|
255
|
+
* turning on deferral site-wide. Bots stay eager via the `isBotReq`
|
|
256
|
+
* short-circuit in {@link shouldDeferSection}.
|
|
257
|
+
*/
|
|
258
|
+
export function registerAlwaysDeferSections(keys: string[]): void {
|
|
259
|
+
G.__deco.alwaysDeferSectionKeys ??= new Set();
|
|
260
|
+
const set: Set<string> = G.__deco.alwaysDeferSectionKeys;
|
|
261
|
+
for (const k of keys) set.add(k);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function isAlwaysDeferSection(key: string): boolean {
|
|
265
|
+
return (G.__deco.alwaysDeferSectionKeys as Set<string> | undefined)?.has(key) ?? false;
|
|
266
|
+
}
|
|
267
|
+
|
|
246
268
|
// ---------------------------------------------------------------------------
|
|
247
269
|
// Deferred rawProps cache — keeps rawProps server-side to trim HTML payload
|
|
248
270
|
// ---------------------------------------------------------------------------
|
|
@@ -1497,6 +1519,12 @@ export function shouldDeferSection(
|
|
|
1497
1519
|
const finalKey = resolveFinalSectionKey(section, matcherCtx);
|
|
1498
1520
|
if (!finalKey) return false;
|
|
1499
1521
|
|
|
1522
|
+
// ── PER-SECTION FORCE-DEFER (`export const deferred = true`) ───────────────
|
|
1523
|
+
// Explicit code-level opt-in to defer one section regardless of the global
|
|
1524
|
+
// `respectCmsLazy` config — lets a site async-render a single heavy section
|
|
1525
|
+
// without a site-wide skeleton sweep. Bots already returned eager above.
|
|
1526
|
+
if (isAlwaysDeferSection(finalKey)) return true;
|
|
1527
|
+
|
|
1500
1528
|
// ── ADMIN IS THE SOURCE OF TRUTH ──────────────────────────────────────────
|
|
1501
1529
|
// If the editor marked the section ⚡ (wrapped in CMS Lazy/Deferred at any
|
|
1502
1530
|
// nesting level, including multivariate flags), it is deferred —
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for the CF Workers secret-decryption bug: in production/preview
|
|
3
|
+
* DECO_CRYPTO_KEY is neither in process.env nor in .dev.vars — it only reaches
|
|
4
|
+
* getEnvVar() via the `env` binding stashed in RequestContext (setRuntimeEnv).
|
|
5
|
+
* If getEnvVar() stops consulting getRuntimeEnv(), encrypted secrets silently
|
|
6
|
+
* decrypt to null in production and this test fails.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { webcrypto } from "node:crypto";
|
|
10
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
11
|
+
import { resolveSecret } from "./crypto";
|
|
12
|
+
import { setRuntimeEnv } from "./otelAdapters";
|
|
13
|
+
import { RequestContext } from "./requestContext";
|
|
14
|
+
|
|
15
|
+
// Ensure crypto.subtle exists in the Node test runtime.
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
if (!globalThis.crypto?.subtle) {
|
|
18
|
+
(globalThis as { crypto: Crypto }).crypto = webcrypto as unknown as Crypto;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/** Build a DECO_CRYPTO_KEY value + an encrypted-hex ciphertext for `plaintext`. */
|
|
23
|
+
async function makeKeyAndCiphertext(plaintext: string) {
|
|
24
|
+
const rawKey = webcrypto.getRandomValues(new Uint8Array(16));
|
|
25
|
+
const iv = webcrypto.getRandomValues(new Uint8Array(16));
|
|
26
|
+
const key = await webcrypto.subtle.importKey("raw", rawKey, "AES-CBC", false, [
|
|
27
|
+
"encrypt",
|
|
28
|
+
]);
|
|
29
|
+
const ct = new Uint8Array(
|
|
30
|
+
await webcrypto.subtle.encrypt(
|
|
31
|
+
{ name: "AES-CBC", iv },
|
|
32
|
+
key,
|
|
33
|
+
new TextEncoder().encode(plaintext),
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
const encryptedHex = Array.from(ct)
|
|
37
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
38
|
+
.join("");
|
|
39
|
+
const decoCryptoKey = btoa(
|
|
40
|
+
JSON.stringify({ key: Array.from(rawKey), iv: Array.from(iv) }),
|
|
41
|
+
);
|
|
42
|
+
return { decoCryptoKey, encryptedHex };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("resolveSecret in CF Workers (no process.env / no .dev.vars)", () => {
|
|
46
|
+
it("decrypts using DECO_CRYPTO_KEY from the RequestContext env binding", async () => {
|
|
47
|
+
// Use a unique plaintext so the module-level secretCache can't mask a
|
|
48
|
+
// regression with a value cached by another test/run.
|
|
49
|
+
const plaintext = `appToken-${Math.random().toString(36).slice(2)}`;
|
|
50
|
+
const { decoCryptoKey, encryptedHex } = await makeKeyAndCiphertext(plaintext);
|
|
51
|
+
|
|
52
|
+
// Guard: process.env must NOT carry the key, so the only source is the ALS.
|
|
53
|
+
expect(process.env.DECO_CRYPTO_KEY).toBeUndefined();
|
|
54
|
+
|
|
55
|
+
const resolved = await RequestContext.run(
|
|
56
|
+
new Request("https://site.example/"),
|
|
57
|
+
async () => {
|
|
58
|
+
setRuntimeEnv({ DECO_CRYPTO_KEY: decoCryptoKey });
|
|
59
|
+
return resolveSecret({ encrypted: encryptedHex });
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(resolved).toBe(plaintext);
|
|
64
|
+
});
|
|
65
|
+
});
|
package/src/sdk/crypto.ts
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* const apiKey = await resolveSecret(block.apiKey, "RESEND_API_KEY");
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { getRuntimeEnv } from "./otelAdapters";
|
|
19
|
+
|
|
18
20
|
const textDecoder = new TextDecoder();
|
|
19
21
|
const textEncoder = new TextEncoder();
|
|
20
22
|
|
|
@@ -150,10 +152,13 @@ export async function resolveSecret(
|
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
/**
|
|
153
|
-
* Get an environment variable, checking process.env first, then .dev.vars file
|
|
155
|
+
* Get an environment variable, checking process.env first, then .dev.vars file,
|
|
156
|
+
* then the Cloudflare Workers `env` binding stashed in RequestContext.
|
|
154
157
|
* Cloudflare Workers dev mode stores env vars in .dev.vars but they're only
|
|
155
158
|
* accessible via the `env` binding inside request handlers. During setup.ts
|
|
156
|
-
* (module-level init), we need to read the file directly.
|
|
159
|
+
* (module-level init), we need to read the file directly. In production/preview
|
|
160
|
+
* there is no .dev.vars and no process.env — the only source is the `env` object
|
|
161
|
+
* passed to the fetch handler, which workerEntry.ts stashes via setRuntimeEnv().
|
|
157
162
|
*/
|
|
158
163
|
function getEnvVar(name: string): string | undefined {
|
|
159
164
|
// 1. process.env (works in Node, may work in Workers with nodejs_compat)
|
|
@@ -181,5 +186,14 @@ function getEnvVar(name: string): string | undefined {
|
|
|
181
186
|
// fs not available (e.g., pure Worker runtime) — ignore
|
|
182
187
|
}
|
|
183
188
|
|
|
189
|
+
// 3. CF Workers `env` binding, stashed per-request in RequestContext by
|
|
190
|
+
// workerEntry.ts's setRuntimeEnv(env). This is the ONLY source in a real
|
|
191
|
+
// Workers production/preview deploy (no process.env, no .dev.vars). Returns
|
|
192
|
+
// undefined at module-init time (no active request) — that's why apps are
|
|
193
|
+
// re-configured on the first request via reconfigureAppsOnce().
|
|
194
|
+
const runtimeEnv = getRuntimeEnv();
|
|
195
|
+
const v = runtimeEnv?.[name];
|
|
196
|
+
if (typeof v === "string" && v) return v;
|
|
197
|
+
|
|
184
198
|
return undefined;
|
|
185
199
|
}
|