@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "1.3.5",
3
+ "version": "1.3.6",
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",
@@ -227,7 +227,6 @@ export const loadDeferredSection = createServerFn({ method: "POST" })
227
227
  headers: originRequest.headers,
228
228
  });
229
229
  const enriched = await runSingleSectionLoader(section, request);
230
-
231
230
  return normalizeUrlsInObject(enriched);
232
231
  });
233
232
 
@@ -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`, `/deco/`, `/live/`, `/.decofile`.
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", "/_server"];
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
- if (fingerprintedAssetPattern.test(pathname)) return true;
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
- // Only cache if response has no cookies or only safe cookies.
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 private Set-Cookie headers must never be cached —
1214
- // they carry per-user session/auth tokens that would leak to other users.
1215
- // Safe/public cookies (IS session, segment, etc.) are stripped from the
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", "private-set-cookie");
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
- // Strip safe cookies from the cached copy so they don't leak
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
  }