@decocms/start 1.3.3 → 1.3.5
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 -6
- package/src/sdk/cacheHeaders.ts +1 -0
- package/src/sdk/workerEntry.ts +1 -171
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";
|
|
@@ -229,11 +228,6 @@ export const loadDeferredSection = createServerFn({ method: "POST" })
|
|
|
229
228
|
});
|
|
230
229
|
const enriched = await runSingleSectionLoader(section, request);
|
|
231
230
|
|
|
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
231
|
return normalizeUrlsInObject(enriched);
|
|
238
232
|
});
|
|
239
233
|
|
package/src/sdk/cacheHeaders.ts
CHANGED
package/src/sdk/workerEntry.ts
CHANGED
|
@@ -436,7 +436,7 @@ export const DEFAULT_SECURITY_HEADERS: Record<string, string> = {
|
|
|
436
436
|
"Cross-Origin-Opener-Policy": "same-origin-allow-popups",
|
|
437
437
|
};
|
|
438
438
|
|
|
439
|
-
const DEFAULT_BYPASS_PATHS = ["/_build", "/deco/", "/live/", "/.decofile"];
|
|
439
|
+
const DEFAULT_BYPASS_PATHS = ["/_build", "/deco/", "/live/", "/.decofile", "/_server"];
|
|
440
440
|
|
|
441
441
|
/**
|
|
442
442
|
* Cookie names that are safe for caching — they carry anonymous/public
|
|
@@ -548,14 +548,6 @@ const IMMUTABLE_HEADERS: Record<string, string> = {
|
|
|
548
548
|
Vary: "Accept-Encoding",
|
|
549
549
|
};
|
|
550
550
|
|
|
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
551
|
|
|
560
552
|
// ---------------------------------------------------------------------------
|
|
561
553
|
// Factory
|
|
@@ -1015,168 +1007,6 @@ export function createDecoWorkerEntry(
|
|
|
1015
1007
|
return origin;
|
|
1016
1008
|
}
|
|
1017
1009
|
|
|
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 body = await request.text();
|
|
1038
|
-
const originReq = new Request(request, { body, method: "POST" });
|
|
1039
|
-
const origin = await serverEntry.fetch(originReq, env, ctx);
|
|
1040
|
-
const resp = new Response(origin.body, origin);
|
|
1041
|
-
resp.headers.set("Cache-Control", "private, no-cache, no-store, must-revalidate");
|
|
1042
|
-
resp.headers.set("X-Cache", "BYPASS");
|
|
1043
|
-
resp.headers.set("X-Cache-Reason", "logged-in");
|
|
1044
|
-
return resp;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
// Read body once and create a cloned request for origin fetch
|
|
1048
|
-
const body = await request.text();
|
|
1049
|
-
const bodyHash = await hashText(body);
|
|
1050
|
-
|
|
1051
|
-
// Build a synthetic GET cache key from the URL + body hash + segment
|
|
1052
|
-
// Includes device, salesChannel, regionId, flags — so users in
|
|
1053
|
-
// different regions or channels get separate cache entries.
|
|
1054
|
-
const cacheKeyUrl = new URL(request.url);
|
|
1055
|
-
cacheKeyUrl.searchParams.set("__body", bodyHash);
|
|
1056
|
-
if (cacheVersionEnv !== false) {
|
|
1057
|
-
const version = (env[cacheVersionEnv] as string) || "";
|
|
1058
|
-
if (version) cacheKeyUrl.searchParams.set("__v", version);
|
|
1059
|
-
}
|
|
1060
|
-
if (sfnSegment) {
|
|
1061
|
-
cacheKeyUrl.searchParams.set("__seg", hashSegment(sfnSegment));
|
|
1062
|
-
} else if (deviceSpecificKeys) {
|
|
1063
|
-
const device = isMobileUA(request.headers.get("user-agent") ?? "") ? "mobile" : "desktop";
|
|
1064
|
-
cacheKeyUrl.searchParams.set("__cf_device", device);
|
|
1065
|
-
}
|
|
1066
|
-
// Include CF geo data so location-based content doesn't leak across geos
|
|
1067
|
-
const cf = (request as unknown as { cf?: Record<string, string> }).cf;
|
|
1068
|
-
if (cf) {
|
|
1069
|
-
const geoParts: string[] = [];
|
|
1070
|
-
if (cf.country) geoParts.push(cf.country);
|
|
1071
|
-
if (cf.region) geoParts.push(cf.region);
|
|
1072
|
-
if (cf.city) geoParts.push(cf.city);
|
|
1073
|
-
if (geoParts.length) cacheKeyUrl.searchParams.set("__cf_geo", geoParts.join("|"));
|
|
1074
|
-
}
|
|
1075
|
-
const sfnCacheKey = new Request(cacheKeyUrl.toString(), { method: "GET" });
|
|
1076
|
-
|
|
1077
|
-
// Use "listing" profile for server function responses
|
|
1078
|
-
const sfnProfile: CacheProfileName = "listing";
|
|
1079
|
-
const sfnEdge = edgeCacheConfig(sfnProfile);
|
|
1080
|
-
|
|
1081
|
-
// Check edge cache
|
|
1082
|
-
let sfnCached: Response | undefined;
|
|
1083
|
-
if (serverFnCache) {
|
|
1084
|
-
try {
|
|
1085
|
-
sfnCached = await serverFnCache.match(sfnCacheKey) ?? undefined;
|
|
1086
|
-
} catch { /* Cache API unavailable */ }
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
if (sfnCached && sfnEdge.fresh > 0) {
|
|
1090
|
-
const storedAt = Number(sfnCached.headers.get("X-Deco-Stored-At") || "0");
|
|
1091
|
-
const ageSec = storedAt > 0 ? (Date.now() - storedAt) / 1000 : Infinity;
|
|
1092
|
-
|
|
1093
|
-
if (ageSec < sfnEdge.fresh) {
|
|
1094
|
-
const out = new Response(sfnCached.body, sfnCached);
|
|
1095
|
-
const hdrs = cacheHeaders(sfnProfile);
|
|
1096
|
-
for (const [k, v] of Object.entries(hdrs)) out.headers.set(k, v);
|
|
1097
|
-
out.headers.set("X-Cache", "HIT");
|
|
1098
|
-
out.headers.set("X-Cache-Profile", sfnProfile);
|
|
1099
|
-
return out;
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
if (ageSec < sfnEdge.fresh + sfnEdge.swr) {
|
|
1103
|
-
// Stale-while-revalidate: serve stale, refresh in background
|
|
1104
|
-
ctx.waitUntil(
|
|
1105
|
-
(async () => {
|
|
1106
|
-
try {
|
|
1107
|
-
const bgReq = new Request(request, { body, method: "POST" });
|
|
1108
|
-
const bgOrigin = await serverEntry.fetch(bgReq, env, ctx);
|
|
1109
|
-
if (
|
|
1110
|
-
bgOrigin.status === 200 &&
|
|
1111
|
-
bgOrigin.headers.get("X-Deco-Cacheable") === "true" &&
|
|
1112
|
-
!bgOrigin.headers.has("set-cookie") &&
|
|
1113
|
-
serverFnCache
|
|
1114
|
-
) {
|
|
1115
|
-
const ttl = sfnEdge.fresh + Math.max(sfnEdge.swr, sfnEdge.sie);
|
|
1116
|
-
const toStore = bgOrigin.clone();
|
|
1117
|
-
toStore.headers.set("Cache-Control", `public, max-age=${ttl}`);
|
|
1118
|
-
toStore.headers.set("X-Deco-Stored-At", String(Date.now()));
|
|
1119
|
-
toStore.headers.delete("CDN-Cache-Control");
|
|
1120
|
-
toStore.headers.delete("X-Deco-Cacheable");
|
|
1121
|
-
await serverFnCache.put(sfnCacheKey, toStore);
|
|
1122
|
-
}
|
|
1123
|
-
} catch { /* background revalidation failed */ }
|
|
1124
|
-
})(),
|
|
1125
|
-
);
|
|
1126
|
-
const out = new Response(sfnCached.body, sfnCached);
|
|
1127
|
-
const hdrs = cacheHeaders(sfnProfile);
|
|
1128
|
-
for (const [k, v] of Object.entries(hdrs)) out.headers.set(k, v);
|
|
1129
|
-
out.headers.set("X-Cache", "STALE-HIT");
|
|
1130
|
-
out.headers.set("X-Cache-Profile", sfnProfile);
|
|
1131
|
-
out.headers.set("X-Cache-Age", String(Math.round(ageSec)));
|
|
1132
|
-
return out;
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
// Cache MISS — fetch origin with the body we already read
|
|
1137
|
-
const originReq = new Request(request, { body, method: "POST" });
|
|
1138
|
-
const origin = await serverEntry.fetch(originReq, 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
1010
|
// Non-cacheable requests — pass through but protect against accidental caching
|
|
1181
1011
|
if (!isCacheable(request, url)) {
|
|
1182
1012
|
const origin = await serverEntry.fetch(request, env, ctx);
|