@decocms/start 1.3.5 → 1.3.6
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/routes/cmsRoute.ts +0 -1
- package/src/sdk/workerEntry.ts +11 -230
package/package.json
CHANGED
package/src/routes/cmsRoute.ts
CHANGED
package/src/sdk/workerEntry.ts
CHANGED
|
@@ -179,7 +179,7 @@ export interface DecoWorkerEntryOptions {
|
|
|
179
179
|
/**
|
|
180
180
|
* Paths that should always bypass the edge cache, even if the
|
|
181
181
|
* profile detector would otherwise cache them.
|
|
182
|
-
* Defaults include `/_build`, `/
|
|
182
|
+
* Defaults include `/_server`, `/_build`, `/assets`, `/deco/`.
|
|
183
183
|
*/
|
|
184
184
|
bypassPaths?: string[];
|
|
185
185
|
|
|
@@ -295,63 +295,6 @@ export interface DecoWorkerEntryOptions {
|
|
|
295
295
|
* @default true
|
|
296
296
|
*/
|
|
297
297
|
autoInjectGeoCookies?: boolean;
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Cookie names considered "safe" for caching — these are public/anonymous
|
|
301
|
-
* cookies that do not carry per-user session or auth data.
|
|
302
|
-
*
|
|
303
|
-
* When a response contains ONLY safe cookies, it is still eligible for
|
|
304
|
-
* Cache API storage. The safe cookies are stripped from the cached copy
|
|
305
|
-
* but kept on the response served to the current user.
|
|
306
|
-
*
|
|
307
|
-
* If the response contains ANY cookie NOT in this list, the response
|
|
308
|
-
* bypasses caching entirely (existing behavior).
|
|
309
|
-
*
|
|
310
|
-
* @default DEFAULT_SAFE_COOKIES (vtex_is_session, vtex_is_anonymous, vtex_segment, _deco_bucket)
|
|
311
|
-
*
|
|
312
|
-
* @example
|
|
313
|
-
* ```ts
|
|
314
|
-
* createDecoWorkerEntry(serverEntry, {
|
|
315
|
-
* safeCookies: [
|
|
316
|
-
* ...DEFAULT_SAFE_COOKIES,
|
|
317
|
-
* "my_custom_analytics_cookie",
|
|
318
|
-
* ],
|
|
319
|
-
* });
|
|
320
|
-
* ```
|
|
321
|
-
*/
|
|
322
|
-
safeCookies?: string[];
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Additional static paths (beyond fingerprinted assets) that should
|
|
326
|
-
* receive long-lived immutable cache headers.
|
|
327
|
-
*
|
|
328
|
-
* Useful for non-fingerprinted resources like fonts that live at
|
|
329
|
-
* stable URLs (e.g., `/fonts/Lato-Regular.woff2`).
|
|
330
|
-
*
|
|
331
|
-
* @default ["/fonts/"]
|
|
332
|
-
*
|
|
333
|
-
* @example
|
|
334
|
-
* ```ts
|
|
335
|
-
* createDecoWorkerEntry(serverEntry, {
|
|
336
|
-
* staticPaths: ["/fonts/", "/static/", "/images/icons/"],
|
|
337
|
-
* });
|
|
338
|
-
* ```
|
|
339
|
-
*/
|
|
340
|
-
staticPaths?: string[];
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* CDN-Cache-Control header strategy.
|
|
344
|
-
*
|
|
345
|
-
* - `"no-store"` (default): CDN never caches; every request invokes the Worker.
|
|
346
|
-
* Correct when segment-based cache keys differ from the original URL.
|
|
347
|
-
* - `"match-profile"`: Set CDN-Cache-Control to a short TTL matching the
|
|
348
|
-
* profile's edge.fresh value. Only safe when you are NOT using segment-based
|
|
349
|
-
* cache keys (i.e., no `buildSegment` and `deviceSpecificKeys: false`).
|
|
350
|
-
* - A function: Return a CDN-Cache-Control value per profile, or `null` for no-store.
|
|
351
|
-
*
|
|
352
|
-
* @default "no-store"
|
|
353
|
-
*/
|
|
354
|
-
cdnCacheControl?: "no-store" | "match-profile" | ((profile: CacheProfileName) => string | null);
|
|
355
298
|
}
|
|
356
299
|
|
|
357
300
|
// ---------------------------------------------------------------------------
|
|
@@ -436,110 +379,7 @@ export const DEFAULT_SECURITY_HEADERS: Record<string, string> = {
|
|
|
436
379
|
"Cross-Origin-Opener-Policy": "same-origin-allow-popups",
|
|
437
380
|
};
|
|
438
381
|
|
|
439
|
-
const DEFAULT_BYPASS_PATHS = ["/_build", "/deco/", "/live/", "/.decofile"
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Cookie names that are safe for caching — they carry anonymous/public
|
|
443
|
-
* segment data, not per-user auth tokens.
|
|
444
|
-
*
|
|
445
|
-
* VTEX Intelligent Search sets `vtex_is_session` and `vtex_is_anonymous`
|
|
446
|
-
* on every response. `vtex_segment` encodes the sales channel.
|
|
447
|
-
* `_deco_bucket` is the A/B test cohort cookie.
|
|
448
|
-
*/
|
|
449
|
-
export const DEFAULT_SAFE_COOKIES: string[] = [
|
|
450
|
-
"vtex_is_session",
|
|
451
|
-
"vtex_is_anonymous",
|
|
452
|
-
"vtex_segment",
|
|
453
|
-
"_deco_bucket",
|
|
454
|
-
];
|
|
455
|
-
|
|
456
|
-
const DEFAULT_STATIC_PATHS = ["/fonts/"];
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Parse Set-Cookie header values and return cookie names.
|
|
460
|
-
*/
|
|
461
|
-
function parseCookieNames(response: Response): string[] {
|
|
462
|
-
const names: string[] = [];
|
|
463
|
-
// getSetCookie() returns individual Set-Cookie values (available in Workers runtime)
|
|
464
|
-
const setCookies = (response.headers as any).getSetCookie?.() as string[] | undefined;
|
|
465
|
-
if (setCookies) {
|
|
466
|
-
for (const sc of setCookies) {
|
|
467
|
-
const eqIdx = sc.indexOf("=");
|
|
468
|
-
if (eqIdx > 0) names.push(sc.slice(0, eqIdx).trim());
|
|
469
|
-
}
|
|
470
|
-
} else {
|
|
471
|
-
// Fallback: parse from combined header (less reliable but covers edge cases)
|
|
472
|
-
const combined = response.headers.get("set-cookie") ?? "";
|
|
473
|
-
for (const part of combined.split(",")) {
|
|
474
|
-
const eqIdx = part.indexOf("=");
|
|
475
|
-
if (eqIdx > 0) {
|
|
476
|
-
const name = part.slice(0, eqIdx).trim();
|
|
477
|
-
// Skip attributes like "Expires=..." that appear after semicolons
|
|
478
|
-
if (!name.includes(";") && name.length > 0) names.push(name);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
return names;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Check if ALL cookies in a response are in the safe list.
|
|
487
|
-
* Returns true if the response has no cookies or only safe cookies.
|
|
488
|
-
*/
|
|
489
|
-
function hasOnlySafeCookies(response: Response, safeCookieSet: Set<string>): boolean {
|
|
490
|
-
if (!response.headers.has("set-cookie")) return true;
|
|
491
|
-
const names = parseCookieNames(response);
|
|
492
|
-
if (names.length === 0) return true;
|
|
493
|
-
return names.every((name) => safeCookieSet.has(name));
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* Clone a response, stripping Set-Cookie headers that match the safe list.
|
|
498
|
-
* Uses response.clone() to preserve the original body for the served response.
|
|
499
|
-
* The returned copy is intended for cache storage only.
|
|
500
|
-
*/
|
|
501
|
-
function stripSafeCookiesForCache(response: Response, safeCookieSet: Set<string>): Response {
|
|
502
|
-
const clone = response.clone();
|
|
503
|
-
const setCookies = (response.headers as any).getSetCookie?.() as string[] | undefined;
|
|
504
|
-
if (!setCookies || setCookies.length === 0) return clone;
|
|
505
|
-
|
|
506
|
-
// Remove all Set-Cookie headers, then re-add only unsafe ones
|
|
507
|
-
clone.headers.delete("set-cookie");
|
|
508
|
-
for (const sc of setCookies) {
|
|
509
|
-
const eqIdx = sc.indexOf("=");
|
|
510
|
-
const name = eqIdx > 0 ? sc.slice(0, eqIdx).trim() : "";
|
|
511
|
-
if (name && !safeCookieSet.has(name)) {
|
|
512
|
-
clone.headers.append("set-cookie", sc);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
return clone;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Deduplicate Set-Cookie headers — keep only the LAST occurrence of
|
|
520
|
-
* each cookie name. Multiple layers (VTEX middleware, invoke handlers,
|
|
521
|
-
* etc.) may independently append the same cookie.
|
|
522
|
-
*/
|
|
523
|
-
function deduplicateSetCookies(response: Response): void {
|
|
524
|
-
const setCookies = (response.headers as any).getSetCookie?.() as string[] | undefined;
|
|
525
|
-
if (!setCookies || setCookies.length <= 1) return;
|
|
526
|
-
|
|
527
|
-
// Build map: cookie name → last Set-Cookie value
|
|
528
|
-
const seen = new Map<string, string>();
|
|
529
|
-
for (const sc of setCookies) {
|
|
530
|
-
const eqIdx = sc.indexOf("=");
|
|
531
|
-
const name = eqIdx > 0 ? sc.slice(0, eqIdx).trim() : sc;
|
|
532
|
-
seen.set(name, sc);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// If no duplicates, nothing to do
|
|
536
|
-
if (seen.size === setCookies.length) return;
|
|
537
|
-
|
|
538
|
-
response.headers.delete("set-cookie");
|
|
539
|
-
for (const sc of seen.values()) {
|
|
540
|
-
response.headers.append("set-cookie", sc);
|
|
541
|
-
}
|
|
542
|
-
}
|
|
382
|
+
const DEFAULT_BYPASS_PATHS = ["/_server", "/_build", "/deco/", "/live/", "/.decofile"];
|
|
543
383
|
|
|
544
384
|
const FINGERPRINTED_ASSET_RE = /(?:\/_build)?\/assets\/.*-[a-zA-Z0-9_-]{8,}\.\w+$/;
|
|
545
385
|
|
|
@@ -548,7 +388,6 @@ const IMMUTABLE_HEADERS: Record<string, string> = {
|
|
|
548
388
|
Vary: "Accept-Encoding",
|
|
549
389
|
};
|
|
550
390
|
|
|
551
|
-
|
|
552
391
|
// ---------------------------------------------------------------------------
|
|
553
392
|
// Factory
|
|
554
393
|
// ---------------------------------------------------------------------------
|
|
@@ -582,13 +421,8 @@ export function createDecoWorkerEntry(
|
|
|
582
421
|
securityHeaders: securityHeadersOpt,
|
|
583
422
|
csp: cspOpt,
|
|
584
423
|
autoInjectGeoCookies: geoOpt = true,
|
|
585
|
-
safeCookies: safeCookiesOpt = DEFAULT_SAFE_COOKIES,
|
|
586
|
-
staticPaths: staticPathsOpt = DEFAULT_STATIC_PATHS,
|
|
587
|
-
cdnCacheControl: cdnCacheControlOpt = "no-store",
|
|
588
424
|
} = options;
|
|
589
425
|
|
|
590
|
-
const safeCookieSet = new Set(safeCookiesOpt);
|
|
591
|
-
|
|
592
426
|
// Build the final security headers map (merged defaults + custom + CSP)
|
|
593
427
|
const secHeaders: Record<string, string> | null = (() => {
|
|
594
428
|
if (securityHeadersOpt === false) return null;
|
|
@@ -622,9 +456,7 @@ export function createDecoWorkerEntry(
|
|
|
622
456
|
}
|
|
623
457
|
|
|
624
458
|
function isStaticAsset(pathname: string): boolean {
|
|
625
|
-
|
|
626
|
-
// Non-fingerprinted static paths (e.g., /fonts/)
|
|
627
|
-
return staticPathsOpt.some((sp) => pathname.startsWith(sp));
|
|
459
|
+
return fingerprintedAssetPattern.test(pathname);
|
|
628
460
|
}
|
|
629
461
|
|
|
630
462
|
function isCacheable(request: Request, url: URL): boolean {
|
|
@@ -924,10 +756,6 @@ export function createDecoWorkerEntry(
|
|
|
924
756
|
return handleRequest(request, env, ctx);
|
|
925
757
|
});
|
|
926
758
|
|
|
927
|
-
// Deduplicate Set-Cookie headers — multiple layers (VTEX middleware,
|
|
928
|
-
// invoke handlers, etc.) may independently append the same cookie.
|
|
929
|
-
deduplicateSetCookies(response);
|
|
930
|
-
|
|
931
759
|
return applySecurityHeaders(response);
|
|
932
760
|
},
|
|
933
761
|
};
|
|
@@ -1022,28 +850,10 @@ export function createDecoWorkerEntry(
|
|
|
1022
850
|
}
|
|
1023
851
|
|
|
1024
852
|
const resp = new Response(origin.body, origin);
|
|
1025
|
-
|
|
1026
|
-
// Responses with private Set-Cookie headers carry per-user tokens —
|
|
1027
|
-
// never expose them with public cache headers.
|
|
1028
|
-
// Safe/public cookies (e.g., vtex_is_session) are allowed through.
|
|
1029
|
-
if (origin.headers.has("set-cookie") && !hasOnlySafeCookies(origin, safeCookieSet)) {
|
|
1030
|
-
resp.headers.set("Cache-Control", "private, no-cache, no-store, must-revalidate");
|
|
1031
|
-
resp.headers.delete("CDN-Cache-Control");
|
|
1032
|
-
resp.headers.set("X-Cache", "BYPASS");
|
|
1033
|
-
resp.headers.set("X-Cache-Reason", "private-set-cookie");
|
|
1034
|
-
return resp;
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
// Set cache headers from the detected profile so the response
|
|
1038
|
-
// is explicit about cacheability (avoids ambiguous empty header).
|
|
1039
|
-
const hdrsNc = cacheHeaders(profile);
|
|
1040
|
-
for (const [k, v] of Object.entries(hdrsNc)) resp.headers.set(k, v);
|
|
1041
|
-
|
|
1042
853
|
const reason = request.method !== "GET"
|
|
1043
854
|
? `method:${request.method}`
|
|
1044
855
|
: "bypass-path";
|
|
1045
856
|
resp.headers.set("X-Cache", "BYPASS");
|
|
1046
|
-
resp.headers.set("X-Cache-Profile", profile);
|
|
1047
857
|
resp.headers.set("X-Cache-Reason", reason);
|
|
1048
858
|
return resp;
|
|
1049
859
|
}
|
|
@@ -1075,22 +885,7 @@ export function createDecoWorkerEntry(
|
|
|
1075
885
|
const out = new Response(resp.body, resp);
|
|
1076
886
|
const hdrs = cacheHeaders(profile);
|
|
1077
887
|
for (const [k, v] of Object.entries(hdrs)) out.headers.set(k, v);
|
|
1078
|
-
|
|
1079
|
-
// CDN-Cache-Control: controls Cloudflare's automatic CDN layer
|
|
1080
|
-
// (separate from Cache API which the worker manages directly).
|
|
1081
|
-
if (cdnCacheControlOpt === "no-store") {
|
|
1082
|
-
out.headers.set("CDN-Cache-Control", "no-store");
|
|
1083
|
-
} else if (cdnCacheControlOpt === "match-profile") {
|
|
1084
|
-
if (edgeConfig.isPublic && edgeConfig.fresh > 0) {
|
|
1085
|
-
out.headers.set("CDN-Cache-Control", `public, max-age=${edgeConfig.fresh}`);
|
|
1086
|
-
} else {
|
|
1087
|
-
out.headers.set("CDN-Cache-Control", "no-store");
|
|
1088
|
-
}
|
|
1089
|
-
} else if (typeof cdnCacheControlOpt === "function") {
|
|
1090
|
-
const val = cdnCacheControlOpt(profile);
|
|
1091
|
-
out.headers.set("CDN-Cache-Control", val ?? "no-store");
|
|
1092
|
-
}
|
|
1093
|
-
|
|
888
|
+
out.headers.set("CDN-Cache-Control", "no-store");
|
|
1094
889
|
out.headers.set("X-Cache", xCache);
|
|
1095
890
|
out.headers.set("X-Cache-Profile", profile);
|
|
1096
891
|
if (segment) out.headers.set("X-Cache-Segment", hashSegment(segment));
|
|
@@ -1122,15 +917,8 @@ export function createDecoWorkerEntry(
|
|
|
1122
917
|
function revalidateInBackground() {
|
|
1123
918
|
ctx.waitUntil(
|
|
1124
919
|
Promise.resolve(serverEntry.fetch(request, env, ctx)).then((origin) => {
|
|
1125
|
-
if (origin.status === 200) {
|
|
1126
|
-
|
|
1127
|
-
// Strip safe cookies from the cached copy.
|
|
1128
|
-
if (hasOnlySafeCookies(origin, safeCookieSet)) {
|
|
1129
|
-
const cleanOrigin = origin.headers.has("set-cookie")
|
|
1130
|
-
? stripSafeCookiesForCache(origin, safeCookieSet)
|
|
1131
|
-
: origin;
|
|
1132
|
-
storeInCache(cleanOrigin);
|
|
1133
|
-
}
|
|
920
|
+
if (origin.status === 200 && !origin.headers.has("set-cookie")) {
|
|
921
|
+
storeInCache(origin);
|
|
1134
922
|
}
|
|
1135
923
|
}).catch(() => {
|
|
1136
924
|
// Background revalidation failed — stale entry stays until SIE expires
|
|
@@ -1210,16 +998,14 @@ export function createDecoWorkerEntry(
|
|
|
1210
998
|
return resp;
|
|
1211
999
|
}
|
|
1212
1000
|
|
|
1213
|
-
// Responses with
|
|
1214
|
-
//
|
|
1215
|
-
|
|
1216
|
-
// cached copy but kept on the response served to the current user.
|
|
1217
|
-
if (origin.headers.has("set-cookie") && !hasOnlySafeCookies(origin, safeCookieSet)) {
|
|
1001
|
+
// Responses with Set-Cookie must never be cached — they carry
|
|
1002
|
+
// per-user session/auth tokens that would leak to other users.
|
|
1003
|
+
if (origin.headers.has("set-cookie")) {
|
|
1218
1004
|
const resp = new Response(origin.body, origin);
|
|
1219
1005
|
resp.headers.set("Cache-Control", "private, no-cache, no-store, must-revalidate");
|
|
1220
1006
|
resp.headers.delete("CDN-Cache-Control");
|
|
1221
1007
|
resp.headers.set("X-Cache", "BYPASS");
|
|
1222
|
-
resp.headers.set("X-Cache-Reason", "
|
|
1008
|
+
resp.headers.set("X-Cache-Reason", "set-cookie");
|
|
1223
1009
|
appendResourceHints(resp);
|
|
1224
1010
|
return resp;
|
|
1225
1011
|
}
|
|
@@ -1239,12 +1025,7 @@ export function createDecoWorkerEntry(
|
|
|
1239
1025
|
// dressResponse() calls new Response(resp.body, resp) which locks
|
|
1240
1026
|
// the ReadableStream. Calling clone() on a locked body corrupts
|
|
1241
1027
|
// the stream in Workers runtime, causing Error 1101.
|
|
1242
|
-
|
|
1243
|
-
// to other users, but the current user still gets them.
|
|
1244
|
-
const cacheOrigin = origin.headers.has("set-cookie")
|
|
1245
|
-
? stripSafeCookiesForCache(origin, safeCookieSet)
|
|
1246
|
-
: origin;
|
|
1247
|
-
storeInCache(cacheOrigin);
|
|
1028
|
+
storeInCache(origin);
|
|
1248
1029
|
return dressResponse(origin, "MISS");
|
|
1249
1030
|
}
|
|
1250
1031
|
}
|