@decocms/apps-vtex 7.31.2 → 7.31.4

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/apps-vtex",
3
- "version": "7.31.2",
3
+ "version": "7.31.4",
4
4
  "type": "module",
5
5
  "description": "Deco commerce app: VTEX integration",
6
6
  "repository": {
@@ -51,10 +51,10 @@
51
51
  "lint:unused": "knip"
52
52
  },
53
53
  "dependencies": {
54
- "@decocms/blocks": "7.31.2",
55
- "@decocms/apps-commerce": "7.31.2",
56
- "@decocms/apps-website": "7.31.2",
57
- "@decocms/tanstack": "7.31.2"
54
+ "@decocms/blocks": "7.31.4",
55
+ "@decocms/apps-commerce": "7.31.4",
56
+ "@decocms/apps-website": "7.31.4",
57
+ "@decocms/tanstack": "7.31.4"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "@tanstack/react-query": ">=5.0.0",
@@ -45,6 +45,14 @@ export interface VtexProxyOptions {
45
45
  * Custom headers to inject into every proxied request.
46
46
  */
47
47
  extraHeaders?: Record<string, string>;
48
+
49
+ /**
50
+ * Force `Cache-Control: private, no-store` + `CDN-Cache-Control: no-store`
51
+ * on authenticated proxy responses (everything except static assets), so a
52
+ * personalized VTEX response can never be cached and served across users.
53
+ * @default true
54
+ */
55
+ hardenAuthenticatedCache?: boolean;
48
56
  }
49
57
 
50
58
  const DEFAULT_PROXY_PATHS = [
@@ -79,6 +87,42 @@ const HOP_BY_HOP_HEADERS = new Set([
79
87
  "upgrade",
80
88
  ]);
81
89
 
90
+ /**
91
+ * Path prefixes whose proxied responses are safe to cache publicly — genuinely
92
+ * public static assets (media files, VTEX store-theme bundles). Everything else
93
+ * routed through the VTEX proxy is authenticated/personalized by nature
94
+ * (account, checkout, /api/sessions, orders, graphql, VTEX ID) and MUST NOT be
95
+ * cached by shared/edge layers.
96
+ */
97
+ const PUBLIC_PROXY_ASSET_PREFIXES = [
98
+ "/files/",
99
+ "/arquivos/",
100
+ "/assets/vtex",
101
+ "/XMLData/",
102
+ ];
103
+
104
+ function isPublicProxyAsset(pathname: string): boolean {
105
+ return PUBLIC_PROXY_ASSET_PREFIXES.some((p) => pathname.startsWith(p));
106
+ }
107
+
108
+ /**
109
+ * Force authenticated proxy responses to be non-cacheable on every shared layer.
110
+ *
111
+ * VTEX marks some endpoints `no-store` (e.g. `/api/sessions`) but not all
112
+ * (`/account` comes back `public`), and the raw origin `Cache-Control` must
113
+ * never let a personalized response be cached across users. Without this, a
114
+ * logged-in user's response can be stored at the edge and served to another
115
+ * user — the root cause of a cross-customer data leak (see decocms/blocks#412).
116
+ * Static assets are exempt so store-theme/media caching is preserved.
117
+ */
118
+ function hardenProxyCacheHeaders(headers: Headers, pathname: string): void {
119
+ if (isPublicProxyAsset(pathname)) return;
120
+ headers.set("Cache-Control", "private, no-store, no-cache, must-revalidate");
121
+ // CDN-Cache-Control takes precedence for Cloudflare's CDN layer.
122
+ headers.set("CDN-Cache-Control", "no-store");
123
+ headers.delete("Surrogate-Control");
124
+ }
125
+
82
126
  /**
83
127
  * Returns all path prefixes that should be proxied to VTEX.
84
128
  */
@@ -199,6 +243,10 @@ export async function proxyToVtex(request: Request, options?: VtexProxyOptions):
199
243
  }
200
244
  }
201
245
 
246
+ if (options?.hardenAuthenticatedCache !== false) {
247
+ hardenProxyCacheHeaders(responseHeaders, requestUrl.pathname);
248
+ }
249
+
202
250
  return new Response(originResponse.body, {
203
251
  status: originResponse.status,
204
252
  statusText: originResponse.statusText,
@@ -254,6 +302,14 @@ export interface VtexCheckoutProxyConfig {
254
302
  * Receives the full HTML string and should return the modified version.
255
303
  */
256
304
  htmlTransform?: (html: string) => string;
305
+
306
+ /**
307
+ * Force `Cache-Control: private, no-store` + `CDN-Cache-Control: no-store`
308
+ * on authenticated proxy responses (everything except static assets), so a
309
+ * personalized VTEX response can never be cached and served across users.
310
+ * @default true
311
+ */
312
+ hardenAuthenticatedCache?: boolean;
257
313
  }
258
314
 
259
315
  const CF_INTERNAL_HEADERS = new Set([
@@ -414,6 +470,10 @@ export function createVtexCheckoutProxy(
414
470
  }
415
471
  }
416
472
 
473
+ if (config.hardenAuthenticatedCache !== false) {
474
+ hardenProxyCacheHeaders(resHeaders, url.pathname);
475
+ }
476
+
417
477
  // HTML transform for checkout pages
418
478
  const ct = originRes.headers.get("content-type") ?? "";
419
479
  if (config.htmlTransform && ct.includes("text/html")) {
@@ -20,9 +20,17 @@ const VTEX_AUTH_COOKIE = "VtexIdclientAutCookie";
20
20
 
21
21
  /**
22
22
  * Extract the VtexIdclientAutCookie value from a cookie string.
23
+ *
24
+ * Matches BOTH the base cookie `VtexIdclientAutCookie=` and the account-suffixed
25
+ * variant `VtexIdclientAutCookie_{account}=`. VTEX frequently sets ONLY the
26
+ * suffixed variant on the storefront domain — matching just the base name makes
27
+ * genuinely logged-in users look anonymous, which defeats any logged-in cache
28
+ * bypass built on top of this and can leak personalized/cached responses.
23
29
  */
24
30
  export function extractVtexAuthCookie(cookieHeader: string): string | null {
25
- const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${VTEX_AUTH_COOKIE}=([^;]+)`));
31
+ const match = cookieHeader.match(
32
+ new RegExp(`(?:^|;\\s*)${VTEX_AUTH_COOKIE}(?:_[^=;\\s]+)?=([^;]+)`),
33
+ );
26
34
  return match?.[1] ?? null;
27
35
  }
28
36
 
@@ -48,7 +56,12 @@ function decodeJwtPayload(token: string): Record<string, unknown> | null {
48
56
  export function parseVtexAuthToken(token: string): VtexAuthInfo {
49
57
  const payload = decodeJwtPayload(token);
50
58
  if (!payload) {
51
- return { isLoggedIn: false, isExpired: true };
59
+ // Opaque (non-JWT) token. VTEX auth cookies are frequently opaque strings
60
+ // rather than JWTs — we cannot read `exp`, but the mere presence of the
61
+ // cookie means the user IS authenticated. Treating it as logged-out (the
62
+ // previous behavior) makes real sessions look anonymous and defeats any
63
+ // logged-in cache bypass. Fail towards "logged in" (i.e. do-not-cache).
64
+ return { isLoggedIn: true, isExpired: false };
52
65
  }
53
66
 
54
67
  const exp = typeof payload.exp === "number" ? payload.exp : undefined;