@decocms/start 1.3.4 → 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 -7
- package/src/sdk/cacheHeaders.ts +1 -0
- package/src/sdk/workerEntry.ts +11 -400
package/package.json
CHANGED
package/src/routes/cmsRoute.ts
CHANGED
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
getRequest,
|
|
28
28
|
getRequestHeader,
|
|
29
29
|
getRequestUrl,
|
|
30
|
-
setResponseHeader,
|
|
31
30
|
} from "@tanstack/react-start/server";
|
|
32
31
|
import { createElement } from "react";
|
|
33
32
|
import { preloadSectionComponents } from "../cms/registry";
|
|
@@ -228,12 +227,6 @@ export const loadDeferredSection = createServerFn({ method: "POST" })
|
|
|
228
227
|
headers: originRequest.headers,
|
|
229
228
|
});
|
|
230
229
|
const enriched = await runSingleSectionLoader(section, request);
|
|
231
|
-
|
|
232
|
-
// Signal to the worker entry that this response is safe to edge-cache.
|
|
233
|
-
// Without this header, POST _serverFn responses are passed through
|
|
234
|
-
// without caching (checkout actions, invoke mutations, etc.).
|
|
235
|
-
setResponseHeader("X-Deco-Cacheable", "true");
|
|
236
|
-
|
|
237
230
|
return normalizeUrlsInObject(enriched);
|
|
238
231
|
});
|
|
239
232
|
|
package/src/sdk/cacheHeaders.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,15 +388,6 @@ const IMMUTABLE_HEADERS: Record<string, string> = {
|
|
|
548
388
|
Vary: "Accept-Encoding",
|
|
549
389
|
};
|
|
550
390
|
|
|
551
|
-
/** SHA-256 hex hash of a string — used for POST body cache keys. */
|
|
552
|
-
async function hashText(text: string): Promise<string> {
|
|
553
|
-
const data = new TextEncoder().encode(text);
|
|
554
|
-
const buf = await crypto.subtle.digest("SHA-256", data);
|
|
555
|
-
return Array.from(new Uint8Array(buf))
|
|
556
|
-
.map((b) => b.toString(16).padStart(2, "0"))
|
|
557
|
-
.join("");
|
|
558
|
-
}
|
|
559
|
-
|
|
560
391
|
// ---------------------------------------------------------------------------
|
|
561
392
|
// Factory
|
|
562
393
|
// ---------------------------------------------------------------------------
|
|
@@ -590,13 +421,8 @@ export function createDecoWorkerEntry(
|
|
|
590
421
|
securityHeaders: securityHeadersOpt,
|
|
591
422
|
csp: cspOpt,
|
|
592
423
|
autoInjectGeoCookies: geoOpt = true,
|
|
593
|
-
safeCookies: safeCookiesOpt = DEFAULT_SAFE_COOKIES,
|
|
594
|
-
staticPaths: staticPathsOpt = DEFAULT_STATIC_PATHS,
|
|
595
|
-
cdnCacheControl: cdnCacheControlOpt = "no-store",
|
|
596
424
|
} = options;
|
|
597
425
|
|
|
598
|
-
const safeCookieSet = new Set(safeCookiesOpt);
|
|
599
|
-
|
|
600
426
|
// Build the final security headers map (merged defaults + custom + CSP)
|
|
601
427
|
const secHeaders: Record<string, string> | null = (() => {
|
|
602
428
|
if (securityHeadersOpt === false) return null;
|
|
@@ -630,9 +456,7 @@ export function createDecoWorkerEntry(
|
|
|
630
456
|
}
|
|
631
457
|
|
|
632
458
|
function isStaticAsset(pathname: string): boolean {
|
|
633
|
-
|
|
634
|
-
// Non-fingerprinted static paths (e.g., /fonts/)
|
|
635
|
-
return staticPathsOpt.some((sp) => pathname.startsWith(sp));
|
|
459
|
+
return fingerprintedAssetPattern.test(pathname);
|
|
636
460
|
}
|
|
637
461
|
|
|
638
462
|
function isCacheable(request: Request, url: URL): boolean {
|
|
@@ -932,10 +756,6 @@ export function createDecoWorkerEntry(
|
|
|
932
756
|
return handleRequest(request, env, ctx);
|
|
933
757
|
});
|
|
934
758
|
|
|
935
|
-
// Deduplicate Set-Cookie headers — multiple layers (VTEX middleware,
|
|
936
|
-
// invoke handlers, etc.) may independently append the same cookie.
|
|
937
|
-
deduplicateSetCookies(response);
|
|
938
|
-
|
|
939
759
|
return applySecurityHeaders(response);
|
|
940
760
|
},
|
|
941
761
|
};
|
|
@@ -1015,168 +835,6 @@ export function createDecoWorkerEntry(
|
|
|
1015
835
|
return origin;
|
|
1016
836
|
}
|
|
1017
837
|
|
|
1018
|
-
// -----------------------------------------------------------------
|
|
1019
|
-
// POST _serverFn — edge-cacheable using body-hash as cache key.
|
|
1020
|
-
// These carry public CMS section data (shelves, deferred sections)
|
|
1021
|
-
// that benefits from edge caching despite being POST requests.
|
|
1022
|
-
// -----------------------------------------------------------------
|
|
1023
|
-
if (
|
|
1024
|
-
request.method === "POST" &&
|
|
1025
|
-
(url.pathname.startsWith("/_serverFn/") || url.pathname.startsWith("/_server/"))
|
|
1026
|
-
) {
|
|
1027
|
-
const serverFnCache =
|
|
1028
|
-
typeof caches !== "undefined"
|
|
1029
|
-
? ((caches as unknown as { default?: Cache }).default ?? null)
|
|
1030
|
-
: null;
|
|
1031
|
-
|
|
1032
|
-
// Build segment once — used for logged-in check and cache key
|
|
1033
|
-
const sfnSegment = buildSegment ? buildSegment(request) : undefined;
|
|
1034
|
-
|
|
1035
|
-
// Logged-in users always bypass — personalized content must not leak
|
|
1036
|
-
if (sfnSegment?.loggedIn) {
|
|
1037
|
-
const origin = await serverEntry.fetch(request, env, ctx);
|
|
1038
|
-
const resp = new Response(origin.body, origin);
|
|
1039
|
-
resp.headers.set("Cache-Control", "private, no-cache, no-store, must-revalidate");
|
|
1040
|
-
resp.headers.set("X-Cache", "BYPASS");
|
|
1041
|
-
resp.headers.set("X-Cache-Reason", "logged-in");
|
|
1042
|
-
return resp;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
// Clone request before consuming body — the clone goes to origin
|
|
1046
|
-
// untouched so TanStack Start internals (cookie passthrough, etc.)
|
|
1047
|
-
// work correctly. We only read the body for the cache key hash.
|
|
1048
|
-
const originClone = request.clone();
|
|
1049
|
-
const body = await request.text();
|
|
1050
|
-
const bodyHash = await hashText(body);
|
|
1051
|
-
|
|
1052
|
-
// Build a synthetic GET cache key from the URL + body hash + segment
|
|
1053
|
-
// Includes device, salesChannel, regionId, flags — so users in
|
|
1054
|
-
// different regions or channels get separate cache entries.
|
|
1055
|
-
const cacheKeyUrl = new URL(request.url);
|
|
1056
|
-
cacheKeyUrl.searchParams.set("__body", bodyHash);
|
|
1057
|
-
if (cacheVersionEnv !== false) {
|
|
1058
|
-
const version = (env[cacheVersionEnv] as string) || "";
|
|
1059
|
-
if (version) cacheKeyUrl.searchParams.set("__v", version);
|
|
1060
|
-
}
|
|
1061
|
-
if (sfnSegment) {
|
|
1062
|
-
cacheKeyUrl.searchParams.set("__seg", hashSegment(sfnSegment));
|
|
1063
|
-
} else if (deviceSpecificKeys) {
|
|
1064
|
-
const device = isMobileUA(request.headers.get("user-agent") ?? "") ? "mobile" : "desktop";
|
|
1065
|
-
cacheKeyUrl.searchParams.set("__cf_device", device);
|
|
1066
|
-
}
|
|
1067
|
-
// Include CF geo data so location-based content doesn't leak across geos
|
|
1068
|
-
const cf = (request as unknown as { cf?: Record<string, string> }).cf;
|
|
1069
|
-
if (cf) {
|
|
1070
|
-
const geoParts: string[] = [];
|
|
1071
|
-
if (cf.country) geoParts.push(cf.country);
|
|
1072
|
-
if (cf.region) geoParts.push(cf.region);
|
|
1073
|
-
if (cf.city) geoParts.push(cf.city);
|
|
1074
|
-
if (geoParts.length) cacheKeyUrl.searchParams.set("__cf_geo", geoParts.join("|"));
|
|
1075
|
-
}
|
|
1076
|
-
const sfnCacheKey = new Request(cacheKeyUrl.toString(), { method: "GET" });
|
|
1077
|
-
|
|
1078
|
-
// Use "listing" profile for server function responses
|
|
1079
|
-
const sfnProfile: CacheProfileName = "listing";
|
|
1080
|
-
const sfnEdge = edgeCacheConfig(sfnProfile);
|
|
1081
|
-
|
|
1082
|
-
// Check edge cache
|
|
1083
|
-
let sfnCached: Response | undefined;
|
|
1084
|
-
if (serverFnCache) {
|
|
1085
|
-
try {
|
|
1086
|
-
sfnCached = await serverFnCache.match(sfnCacheKey) ?? undefined;
|
|
1087
|
-
} catch { /* Cache API unavailable */ }
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
if (sfnCached && sfnEdge.fresh > 0) {
|
|
1091
|
-
const storedAt = Number(sfnCached.headers.get("X-Deco-Stored-At") || "0");
|
|
1092
|
-
const ageSec = storedAt > 0 ? (Date.now() - storedAt) / 1000 : Infinity;
|
|
1093
|
-
|
|
1094
|
-
if (ageSec < sfnEdge.fresh) {
|
|
1095
|
-
const out = new Response(sfnCached.body, sfnCached);
|
|
1096
|
-
const hdrs = cacheHeaders(sfnProfile);
|
|
1097
|
-
for (const [k, v] of Object.entries(hdrs)) out.headers.set(k, v);
|
|
1098
|
-
out.headers.set("X-Cache", "HIT");
|
|
1099
|
-
out.headers.set("X-Cache-Profile", sfnProfile);
|
|
1100
|
-
return out;
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
if (ageSec < sfnEdge.fresh + sfnEdge.swr) {
|
|
1104
|
-
// Stale-while-revalidate: serve stale, refresh in background
|
|
1105
|
-
ctx.waitUntil(
|
|
1106
|
-
(async () => {
|
|
1107
|
-
try {
|
|
1108
|
-
const bgReq = new Request(request, { body, method: "POST" });
|
|
1109
|
-
const bgOrigin = await serverEntry.fetch(bgReq, env, ctx);
|
|
1110
|
-
if (
|
|
1111
|
-
bgOrigin.status === 200 &&
|
|
1112
|
-
bgOrigin.headers.get("X-Deco-Cacheable") === "true" &&
|
|
1113
|
-
!bgOrigin.headers.has("set-cookie") &&
|
|
1114
|
-
serverFnCache
|
|
1115
|
-
) {
|
|
1116
|
-
const ttl = sfnEdge.fresh + Math.max(sfnEdge.swr, sfnEdge.sie);
|
|
1117
|
-
const toStore = bgOrigin.clone();
|
|
1118
|
-
toStore.headers.set("Cache-Control", `public, max-age=${ttl}`);
|
|
1119
|
-
toStore.headers.set("X-Deco-Stored-At", String(Date.now()));
|
|
1120
|
-
toStore.headers.delete("CDN-Cache-Control");
|
|
1121
|
-
toStore.headers.delete("X-Deco-Cacheable");
|
|
1122
|
-
await serverFnCache.put(sfnCacheKey, toStore);
|
|
1123
|
-
}
|
|
1124
|
-
} catch { /* background revalidation failed */ }
|
|
1125
|
-
})(),
|
|
1126
|
-
);
|
|
1127
|
-
const out = new Response(sfnCached.body, sfnCached);
|
|
1128
|
-
const hdrs = cacheHeaders(sfnProfile);
|
|
1129
|
-
for (const [k, v] of Object.entries(hdrs)) out.headers.set(k, v);
|
|
1130
|
-
out.headers.set("X-Cache", "STALE-HIT");
|
|
1131
|
-
out.headers.set("X-Cache-Profile", sfnProfile);
|
|
1132
|
-
out.headers.set("X-Cache-Age", String(Math.round(ageSec)));
|
|
1133
|
-
return out;
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
// Cache MISS — fetch origin with the body we already read
|
|
1138
|
-
const origin = await serverEntry.fetch(originClone, env, ctx);
|
|
1139
|
-
|
|
1140
|
-
// Only cache responses explicitly marked as cacheable by the handler
|
|
1141
|
-
// (loadDeferredSection sets X-Deco-Cacheable: true). Checkout actions,
|
|
1142
|
-
// invoke mutations, and other server functions are passed through.
|
|
1143
|
-
const isCacheableResponse =
|
|
1144
|
-
origin.headers.get("X-Deco-Cacheable") === "true" &&
|
|
1145
|
-
!origin.headers.has("set-cookie") &&
|
|
1146
|
-
origin.status === 200;
|
|
1147
|
-
|
|
1148
|
-
if (!isCacheableResponse) {
|
|
1149
|
-
const resp = new Response(origin.body, origin);
|
|
1150
|
-
resp.headers.delete("X-Deco-Cacheable");
|
|
1151
|
-
resp.headers.set("X-Cache", "BYPASS");
|
|
1152
|
-
resp.headers.set("X-Cache-Reason", origin.headers.has("set-cookie")
|
|
1153
|
-
? "set-cookie"
|
|
1154
|
-
: "not-cacheable");
|
|
1155
|
-
return resp;
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
// Store in edge cache
|
|
1159
|
-
if (serverFnCache) {
|
|
1160
|
-
try {
|
|
1161
|
-
const ttl = sfnEdge.fresh + Math.max(sfnEdge.swr, sfnEdge.sie);
|
|
1162
|
-
const toStore = origin.clone();
|
|
1163
|
-
toStore.headers.set("Cache-Control", `public, max-age=${ttl}`);
|
|
1164
|
-
toStore.headers.set("X-Deco-Stored-At", String(Date.now()));
|
|
1165
|
-
toStore.headers.delete("CDN-Cache-Control");
|
|
1166
|
-
toStore.headers.delete("X-Deco-Cacheable");
|
|
1167
|
-
ctx.waitUntil(serverFnCache.put(sfnCacheKey, toStore));
|
|
1168
|
-
} catch { /* Cache API unavailable */ }
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
const resp = new Response(origin.body, origin);
|
|
1172
|
-
resp.headers.delete("X-Deco-Cacheable");
|
|
1173
|
-
const hdrs = cacheHeaders(sfnProfile);
|
|
1174
|
-
for (const [k, v] of Object.entries(hdrs)) resp.headers.set(k, v);
|
|
1175
|
-
resp.headers.set("X-Cache", "MISS");
|
|
1176
|
-
resp.headers.set("X-Cache-Profile", sfnProfile);
|
|
1177
|
-
return resp;
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
838
|
// Non-cacheable requests — pass through but protect against accidental caching
|
|
1181
839
|
if (!isCacheable(request, url)) {
|
|
1182
840
|
const origin = await serverEntry.fetch(request, env, ctx);
|
|
@@ -1192,28 +850,10 @@ export function createDecoWorkerEntry(
|
|
|
1192
850
|
}
|
|
1193
851
|
|
|
1194
852
|
const resp = new Response(origin.body, origin);
|
|
1195
|
-
|
|
1196
|
-
// Responses with private Set-Cookie headers carry per-user tokens —
|
|
1197
|
-
// never expose them with public cache headers.
|
|
1198
|
-
// Safe/public cookies (e.g., vtex_is_session) are allowed through.
|
|
1199
|
-
if (origin.headers.has("set-cookie") && !hasOnlySafeCookies(origin, safeCookieSet)) {
|
|
1200
|
-
resp.headers.set("Cache-Control", "private, no-cache, no-store, must-revalidate");
|
|
1201
|
-
resp.headers.delete("CDN-Cache-Control");
|
|
1202
|
-
resp.headers.set("X-Cache", "BYPASS");
|
|
1203
|
-
resp.headers.set("X-Cache-Reason", "private-set-cookie");
|
|
1204
|
-
return resp;
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
// Set cache headers from the detected profile so the response
|
|
1208
|
-
// is explicit about cacheability (avoids ambiguous empty header).
|
|
1209
|
-
const hdrsNc = cacheHeaders(profile);
|
|
1210
|
-
for (const [k, v] of Object.entries(hdrsNc)) resp.headers.set(k, v);
|
|
1211
|
-
|
|
1212
853
|
const reason = request.method !== "GET"
|
|
1213
854
|
? `method:${request.method}`
|
|
1214
855
|
: "bypass-path";
|
|
1215
856
|
resp.headers.set("X-Cache", "BYPASS");
|
|
1216
|
-
resp.headers.set("X-Cache-Profile", profile);
|
|
1217
857
|
resp.headers.set("X-Cache-Reason", reason);
|
|
1218
858
|
return resp;
|
|
1219
859
|
}
|
|
@@ -1245,22 +885,7 @@ export function createDecoWorkerEntry(
|
|
|
1245
885
|
const out = new Response(resp.body, resp);
|
|
1246
886
|
const hdrs = cacheHeaders(profile);
|
|
1247
887
|
for (const [k, v] of Object.entries(hdrs)) out.headers.set(k, v);
|
|
1248
|
-
|
|
1249
|
-
// CDN-Cache-Control: controls Cloudflare's automatic CDN layer
|
|
1250
|
-
// (separate from Cache API which the worker manages directly).
|
|
1251
|
-
if (cdnCacheControlOpt === "no-store") {
|
|
1252
|
-
out.headers.set("CDN-Cache-Control", "no-store");
|
|
1253
|
-
} else if (cdnCacheControlOpt === "match-profile") {
|
|
1254
|
-
if (edgeConfig.isPublic && edgeConfig.fresh > 0) {
|
|
1255
|
-
out.headers.set("CDN-Cache-Control", `public, max-age=${edgeConfig.fresh}`);
|
|
1256
|
-
} else {
|
|
1257
|
-
out.headers.set("CDN-Cache-Control", "no-store");
|
|
1258
|
-
}
|
|
1259
|
-
} else if (typeof cdnCacheControlOpt === "function") {
|
|
1260
|
-
const val = cdnCacheControlOpt(profile);
|
|
1261
|
-
out.headers.set("CDN-Cache-Control", val ?? "no-store");
|
|
1262
|
-
}
|
|
1263
|
-
|
|
888
|
+
out.headers.set("CDN-Cache-Control", "no-store");
|
|
1264
889
|
out.headers.set("X-Cache", xCache);
|
|
1265
890
|
out.headers.set("X-Cache-Profile", profile);
|
|
1266
891
|
if (segment) out.headers.set("X-Cache-Segment", hashSegment(segment));
|
|
@@ -1292,15 +917,8 @@ export function createDecoWorkerEntry(
|
|
|
1292
917
|
function revalidateInBackground() {
|
|
1293
918
|
ctx.waitUntil(
|
|
1294
919
|
Promise.resolve(serverEntry.fetch(request, env, ctx)).then((origin) => {
|
|
1295
|
-
if (origin.status === 200) {
|
|
1296
|
-
|
|
1297
|
-
// Strip safe cookies from the cached copy.
|
|
1298
|
-
if (hasOnlySafeCookies(origin, safeCookieSet)) {
|
|
1299
|
-
const cleanOrigin = origin.headers.has("set-cookie")
|
|
1300
|
-
? stripSafeCookiesForCache(origin, safeCookieSet)
|
|
1301
|
-
: origin;
|
|
1302
|
-
storeInCache(cleanOrigin);
|
|
1303
|
-
}
|
|
920
|
+
if (origin.status === 200 && !origin.headers.has("set-cookie")) {
|
|
921
|
+
storeInCache(origin);
|
|
1304
922
|
}
|
|
1305
923
|
}).catch(() => {
|
|
1306
924
|
// Background revalidation failed — stale entry stays until SIE expires
|
|
@@ -1380,16 +998,14 @@ export function createDecoWorkerEntry(
|
|
|
1380
998
|
return resp;
|
|
1381
999
|
}
|
|
1382
1000
|
|
|
1383
|
-
// Responses with
|
|
1384
|
-
//
|
|
1385
|
-
|
|
1386
|
-
// cached copy but kept on the response served to the current user.
|
|
1387
|
-
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")) {
|
|
1388
1004
|
const resp = new Response(origin.body, origin);
|
|
1389
1005
|
resp.headers.set("Cache-Control", "private, no-cache, no-store, must-revalidate");
|
|
1390
1006
|
resp.headers.delete("CDN-Cache-Control");
|
|
1391
1007
|
resp.headers.set("X-Cache", "BYPASS");
|
|
1392
|
-
resp.headers.set("X-Cache-Reason", "
|
|
1008
|
+
resp.headers.set("X-Cache-Reason", "set-cookie");
|
|
1393
1009
|
appendResourceHints(resp);
|
|
1394
1010
|
return resp;
|
|
1395
1011
|
}
|
|
@@ -1409,12 +1025,7 @@ export function createDecoWorkerEntry(
|
|
|
1409
1025
|
// dressResponse() calls new Response(resp.body, resp) which locks
|
|
1410
1026
|
// the ReadableStream. Calling clone() on a locked body corrupts
|
|
1411
1027
|
// the stream in Workers runtime, causing Error 1101.
|
|
1412
|
-
|
|
1413
|
-
// to other users, but the current user still gets them.
|
|
1414
|
-
const cacheOrigin = origin.headers.has("set-cookie")
|
|
1415
|
-
? stripSafeCookiesForCache(origin, safeCookieSet)
|
|
1416
|
-
: origin;
|
|
1417
|
-
storeInCache(cacheOrigin);
|
|
1028
|
+
storeInCache(origin);
|
|
1418
1029
|
return dressResponse(origin, "MISS");
|
|
1419
1030
|
}
|
|
1420
1031
|
}
|