@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "type": "module",
5
5
  "description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
6
6
  "main": "./src/index.ts",
@@ -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
 
@@ -267,6 +267,7 @@ const builtinPatterns: CachePattern[] = [
267
267
  test: (p) =>
268
268
  p.startsWith("/api/") ||
269
269
  p.startsWith("/deco/") ||
270
+ p.startsWith("/_server") ||
270
271
  p.startsWith("/_build"),
271
272
  profile: "none",
272
273
  },
@@ -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);