@decocms/apps-vtex 8.0.0 → 8.1.0-next.1

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.
Files changed (133) hide show
  1. package/package.json +7 -52
  2. package/src/index.ts +12 -15
  3. package/src/utils/constants.ts +2 -56
  4. package/src/utils/cookieSanitizer.ts +59 -59
  5. package/src/utils/operationRouter.ts +76 -72
  6. package/src/utils/types.ts +709 -1557
  7. package/src/vtexClient.ts +444 -0
  8. package/src/README.md +0 -6
  9. package/src/__tests__/client-segment-cookie.test.ts +0 -255
  10. package/src/__tests__/client-set-cookie-forward.test.ts +0 -257
  11. package/src/__tests__/schemas.test.ts +0 -71
  12. package/src/actions/address.ts +0 -250
  13. package/src/actions/analytics/sendEvent.ts +0 -86
  14. package/src/actions/auth.ts +0 -333
  15. package/src/actions/checkout.ts +0 -659
  16. package/src/actions/index.ts +0 -11
  17. package/src/actions/masterData.ts +0 -168
  18. package/src/actions/misc.ts +0 -188
  19. package/src/actions/newsletter.ts +0 -105
  20. package/src/actions/orders.ts +0 -34
  21. package/src/actions/profile.ts +0 -201
  22. package/src/actions/session.ts +0 -85
  23. package/src/actions/trigger.ts +0 -43
  24. package/src/actions/wishlist.ts +0 -114
  25. package/src/client.ts +0 -667
  26. package/src/commerceLoaders.ts +0 -267
  27. package/src/hooks/__tests__/createCart.render.test.ts +0 -114
  28. package/src/hooks/__tests__/createCart.test.ts +0 -92
  29. package/src/hooks/__tests__/createUseCart.test.ts +0 -184
  30. package/src/hooks/__tests__/createUseUser.test.ts +0 -48
  31. package/src/hooks/__tests__/createUseWishlist.test.ts +0 -87
  32. package/src/hooks/cartQuery.ts +0 -204
  33. package/src/hooks/createCart.ts +0 -427
  34. package/src/hooks/createUseCart.ts +0 -360
  35. package/src/hooks/createUseUser.ts +0 -153
  36. package/src/hooks/createUseWishlist.ts +0 -242
  37. package/src/hooks/index.ts +0 -25
  38. package/src/hooks/useAutocomplete.ts +0 -83
  39. package/src/hooks/useCart.ts +0 -243
  40. package/src/hooks/useUser.ts +0 -78
  41. package/src/hooks/useWishlist.ts +0 -119
  42. package/src/invoke.ts +0 -235
  43. package/src/loaders/ProductDetailsPage.ts +0 -1
  44. package/src/loaders/ProductList.ts +0 -1
  45. package/src/loaders/ProductListingPage.ts +0 -1
  46. package/src/loaders/address.ts +0 -115
  47. package/src/loaders/autocomplete.ts +0 -58
  48. package/src/loaders/brands.ts +0 -44
  49. package/src/loaders/cart/attachments.ts +0 -53
  50. package/src/loaders/cart/full.ts +0 -67
  51. package/src/loaders/cart/gifts.ts +0 -43
  52. package/src/loaders/cart/orderFormId.ts +0 -31
  53. package/src/loaders/cart/shipping.test.ts +0 -125
  54. package/src/loaders/cart/shipping.ts +0 -136
  55. package/src/loaders/cart/summary.ts +0 -36
  56. package/src/loaders/cart.ts +0 -52
  57. package/src/loaders/catalog.ts +0 -163
  58. package/src/loaders/collections.ts +0 -55
  59. package/src/loaders/index.ts +0 -20
  60. package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +0 -51
  61. package/src/loaders/intelligentSearch/productDetailsPage.ts +0 -113
  62. package/src/loaders/intelligentSearch/productList.ts +0 -154
  63. package/src/loaders/intelligentSearch/productListingPage.ts +0 -526
  64. package/src/loaders/intelligentSearch/suggestions.ts +0 -46
  65. package/src/loaders/legacy/productDetailsPage.ts +0 -1
  66. package/src/loaders/legacy/productList.ts +0 -1
  67. package/src/loaders/legacy/relatedProductsLoader.ts +0 -81
  68. package/src/loaders/legacy.ts +0 -635
  69. package/src/loaders/logistics.ts +0 -111
  70. package/src/loaders/minicart.ts +0 -78
  71. package/src/loaders/navbar.ts +0 -26
  72. package/src/loaders/orders.ts +0 -92
  73. package/src/loaders/pageType.ts +0 -68
  74. package/src/loaders/paths/PDPDefaultPath.ts +0 -51
  75. package/src/loaders/payment.ts +0 -97
  76. package/src/loaders/productListFull.ts +0 -159
  77. package/src/loaders/profile.ts +0 -138
  78. package/src/loaders/promotion.ts +0 -30
  79. package/src/loaders/search.ts +0 -124
  80. package/src/loaders/session.ts +0 -83
  81. package/src/loaders/user.ts +0 -87
  82. package/src/loaders/wishlist.ts +0 -89
  83. package/src/loaders/wishlistProducts.ts +0 -69
  84. package/src/loaders/workflow/products.ts +0 -64
  85. package/src/loaders/workflow.ts +0 -316
  86. package/src/logo.png +0 -0
  87. package/src/manifest.gen.ts +0 -89
  88. package/src/middleware.ts +0 -240
  89. package/src/mod.ts +0 -157
  90. package/src/registry.ts +0 -9
  91. package/src/schemas.gen.ts +0 -3191
  92. package/src/schemas.ts +0 -30
  93. package/src/types.ts +0 -248
  94. package/src/utils/__tests__/cartProjection.test.ts +0 -118
  95. package/src/utils/__tests__/cookieSanitizer.test.ts +0 -162
  96. package/src/utils/__tests__/fetch.test.ts +0 -80
  97. package/src/utils/__tests__/fetchCache.test.ts +0 -155
  98. package/src/utils/__tests__/instrumentedFetch.test.ts +0 -158
  99. package/src/utils/__tests__/intelligentSearch.test.ts +0 -88
  100. package/src/utils/__tests__/minicart.test.ts +0 -184
  101. package/src/utils/__tests__/operationRouter.test.ts +0 -227
  102. package/src/utils/__tests__/resilience.test.ts +0 -205
  103. package/src/utils/__tests__/resourceRange.test.ts +0 -32
  104. package/src/utils/__tests__/simulationCache.test.ts +0 -40
  105. package/src/utils/__tests__/sitemap.test.ts +0 -185
  106. package/src/utils/__tests__/slugify.test.ts +0 -31
  107. package/src/utils/__tests__/transform.test.ts +0 -769
  108. package/src/utils/accountLoaders.ts +0 -203
  109. package/src/utils/authHelpers.ts +0 -85
  110. package/src/utils/batch.ts +0 -18
  111. package/src/utils/cartProjection.ts +0 -96
  112. package/src/utils/cookies.ts +0 -167
  113. package/src/utils/enrichment.ts +0 -560
  114. package/src/utils/fetch.ts +0 -107
  115. package/src/utils/fetchCache.ts +0 -199
  116. package/src/utils/index.ts +0 -19
  117. package/src/utils/instrumentedFetch.ts +0 -97
  118. package/src/utils/intelligentSearch.ts +0 -96
  119. package/src/utils/legacy.ts +0 -139
  120. package/src/utils/minicart.ts +0 -139
  121. package/src/utils/pickAndOmit.ts +0 -25
  122. package/src/utils/proxy.ts +0 -435
  123. package/src/utils/resilience.ts +0 -352
  124. package/src/utils/resourceRange.ts +0 -10
  125. package/src/utils/segment.ts +0 -152
  126. package/src/utils/similars.ts +0 -37
  127. package/src/utils/simulationCache.ts +0 -73
  128. package/src/utils/sitemap.ts +0 -282
  129. package/src/utils/slugCache.ts +0 -28
  130. package/src/utils/slugify.ts +0 -13
  131. package/src/utils/transform.ts +0 -1522
  132. package/src/utils/vtexId.ts +0 -138
  133. package/tsconfig.json +0 -7
@@ -1,203 +0,0 @@
1
- /**
2
- * Pre-built section loaders for VTEX account pages.
3
- *
4
- * Every VTEX site with account pages repeats the same pattern:
5
- * 1. Extract VTEX cookies from request
6
- * 2. Call the VTEX user/profile/address/payment API
7
- * 3. Return enriched props with { device, logged, ...data }
8
- * 4. Catch errors gracefully (return logged: false)
9
- *
10
- * These factories encapsulate that boilerplate.
11
- *
12
- * @example
13
- * ```ts
14
- * import { vtexAccountLoaders } from "@decocms/apps/vtex/utils/accountLoaders";
15
- *
16
- * registerSectionLoaders({
17
- * "site/sections/Account/PersonalData.tsx": vtexAccountLoaders.personalData(),
18
- * "site/sections/Account/MyOrders.tsx": vtexAccountLoaders.orders(),
19
- * "site/sections/Account/Cards.tsx": vtexAccountLoaders.cards(),
20
- * "site/sections/Account/Addresses.tsx": vtexAccountLoaders.addresses(),
21
- * "site/sections/Account/Auth.tsx": vtexAccountLoaders.authentication(),
22
- * "site/sections/Account/Other.tsx": vtexAccountLoaders.loggedIn(),
23
- * });
24
- * ```
25
- */
26
-
27
- import { detectDevice } from "@decocms/blocks/sdk/useDevice";
28
- import { getUserAddresses, type VtexAddress } from "../loaders/address";
29
- import { getUserPayments, type Payment } from "../loaders/payment";
30
- import { getCurrentProfile, type Profile } from "../loaders/profile";
31
- import { getUser } from "../loaders/user";
32
- import { getVtexCookies } from "./cookies";
33
-
34
- type Device = "mobile" | "tablet" | "desktop";
35
-
36
- type SectionLoaderFn = (
37
- props: Record<string, unknown>,
38
- req: Request,
39
- ) => Promise<Record<string, unknown>> | Record<string, unknown>;
40
-
41
- function getDevice(req: Request): Device {
42
- return detectDevice(req.headers.get("user-agent") ?? "");
43
- }
44
-
45
- // ---------------------------------------------------------------------------
46
- // personalData — fetches full VTEX profile for personal data sections
47
- // ---------------------------------------------------------------------------
48
-
49
- export interface PersonalDataOptions {
50
- /** Extra custom profile fields to request beyond the standard set. */
51
- extraProfileFields?: string[];
52
-
53
- /**
54
- * Transform the raw VTEX Profile into the shape your component expects.
55
- * When omitted, the raw Profile object is returned as `profile`.
56
- *
57
- * @example
58
- * ```ts
59
- * vtexAccountLoaders.personalData({
60
- * mapProfile: (p) => ({
61
- * "@id": p.userId ?? p.id,
62
- * email: p.email,
63
- * givenName: p.firstName ?? null,
64
- * familyName: p.lastName ?? null,
65
- * taxID: p.document,
66
- * }),
67
- * })
68
- * ```
69
- */
70
- mapProfile?: (profile: Profile) => Record<string, unknown>;
71
- }
72
-
73
- function personalData(options?: PersonalDataOptions): SectionLoaderFn {
74
- const { extraProfileFields, mapProfile } = options ?? {};
75
- return async (props, req) => {
76
- const cookie = getVtexCookies(req);
77
- try {
78
- const profile = await getCurrentProfile(cookie, extraProfileFields);
79
- const data = mapProfile ? mapProfile(profile) : profile;
80
- return {
81
- ...props,
82
- device: getDevice(req),
83
- logged: !!profile,
84
- loading: false,
85
- userData: data,
86
- };
87
- } catch (error) {
88
- console.error("[accountLoaders.personalData]", error);
89
- return { ...props, device: getDevice(req), logged: false, loading: false, userData: null };
90
- }
91
- };
92
- }
93
-
94
- // ---------------------------------------------------------------------------
95
- // orders — checks login status for order listing sections
96
- // ---------------------------------------------------------------------------
97
-
98
- function orders(): SectionLoaderFn {
99
- return async (props, req) => {
100
- const cookie = getVtexCookies(req);
101
- try {
102
- const userData = await getUser(cookie);
103
- return { ...props, device: getDevice(req), logged: !!userData };
104
- } catch {
105
- return { ...props, device: getDevice(req), logged: false };
106
- }
107
- };
108
- }
109
-
110
- // ---------------------------------------------------------------------------
111
- // cards — fetches saved payment tokens for card management sections
112
- // ---------------------------------------------------------------------------
113
-
114
- function cards(): SectionLoaderFn {
115
- return async (props, req) => {
116
- const cookie = getVtexCookies(req);
117
- try {
118
- const user = await getUser(cookie);
119
- const logged = !!user;
120
- let payments: Payment[] = [];
121
- if (logged) {
122
- try {
123
- payments = (await getUserPayments(cookie)) ?? [];
124
- } catch {
125
- payments = [];
126
- }
127
- }
128
- return { ...props, logged, payments };
129
- } catch {
130
- return { ...props, logged: false, payments: [] };
131
- }
132
- };
133
- }
134
-
135
- // ---------------------------------------------------------------------------
136
- // addresses — fetches address list for address management sections
137
- // ---------------------------------------------------------------------------
138
-
139
- function addresses(): SectionLoaderFn {
140
- return async (props, req) => {
141
- const cookie = getVtexCookies(req);
142
- try {
143
- const userData = await getUser(cookie);
144
- const logged = !!userData;
145
- let userAddressData: VtexAddress[] | null = null;
146
- if (logged) {
147
- try {
148
- userAddressData = await getUserAddresses(cookie);
149
- } catch {
150
- userAddressData = null;
151
- }
152
- }
153
- return { ...props, device: getDevice(req), logged, userAddressData };
154
- } catch {
155
- return { ...props, device: getDevice(req), logged: false, userAddressData: null };
156
- }
157
- };
158
- }
159
-
160
- // ---------------------------------------------------------------------------
161
- // authentication — checks login + returns user data for auth pages
162
- // ---------------------------------------------------------------------------
163
-
164
- function authentication(): SectionLoaderFn {
165
- return async (props, req) => {
166
- const cookie = getVtexCookies(req);
167
- try {
168
- const userData = await getUser(cookie);
169
- return { ...props, device: getDevice(req), logged: !!userData, userData };
170
- } catch {
171
- return { ...props, device: getDevice(req), logged: false, userData: null };
172
- }
173
- };
174
- }
175
-
176
- // ---------------------------------------------------------------------------
177
- // loggedIn — generic "is the user logged in?" loader
178
- // ---------------------------------------------------------------------------
179
-
180
- function loggedIn(): SectionLoaderFn {
181
- return async (props, req) => {
182
- const cookie = getVtexCookies(req);
183
- try {
184
- const userData = await getUser(cookie);
185
- return { ...props, device: getDevice(req), logged: !!userData };
186
- } catch {
187
- return { ...props, device: getDevice(req), logged: false };
188
- }
189
- };
190
- }
191
-
192
- // ---------------------------------------------------------------------------
193
- // Public API
194
- // ---------------------------------------------------------------------------
195
-
196
- export const vtexAccountLoaders = {
197
- personalData,
198
- orders,
199
- cards,
200
- addresses,
201
- authentication,
202
- loggedIn,
203
- } as const;
@@ -1,85 +0,0 @@
1
- /**
2
- * VTEX auth helpers — pure functions for cookie extraction, JWT parsing,
3
- * Set-Cookie forwarding, and logout.
4
- *
5
- * These are consumed by site-level createServerFn wrappers in invoke.ts.
6
- * createServerFn itself must live in site source (not node_modules) because
7
- * TanStack Start's Vite plugin only transforms source files.
8
- */
9
- import { getVtexConfig, getVtexFetch } from "../client";
10
- import { extractVtexCookies } from "./cookieSanitizer";
11
-
12
- const DOMAIN_RE = /;\s*domain=[^;]*/gi;
13
-
14
- /**
15
- * Extract VTEX-relevant cookies from a raw Cookie header string.
16
- *
17
- * Strict allowlist: drops any cookie not on `VTEX_COOKIE_PREFIXES`, plus
18
- * any cookie whose value contains non-ASCII bytes (which would otherwise
19
- * make VTEX's janus gateway return 503 Service Unavailable). Both filters
20
- * live in `./cookieSanitizer` — this is a thin compatibility wrapper.
21
- */
22
- export function extractVtexCookiesFromHeader(raw: string): string {
23
- return extractVtexCookies(raw);
24
- }
25
-
26
- /**
27
- * Strip Domain= from Set-Cookie headers so cookies are associated
28
- * with the storefront domain instead of the VTEX domain.
29
- */
30
- export function stripCookieDomain(cookies: string[]): string[] {
31
- return cookies.map((c) => c.replace(DOMAIN_RE, ""));
32
- }
33
-
34
- /** Standard VTEX cookies to expire on logout. */
35
- export const VTEX_LOGOUT_COOKIES = [
36
- "checkout.vtex.com=; Path=/; Max-Age=0; Secure; HttpOnly; SameSite=Lax",
37
- "CheckoutOrderFormOwnership=; Path=/; Max-Age=0; Secure; HttpOnly; SameSite=Lax",
38
- "checkout.vtex.com__orderFormId=; Path=/; Max-Age=0",
39
- "vtex_session=; Path=/; Max-Age=0",
40
- "vtex_segment=; Path=/; Max-Age=0",
41
- ];
42
-
43
- /**
44
- * Perform VTEX logout — calls the VTEX ID logout endpoint and returns
45
- * the Set-Cookie headers (with domain stripped) to expire auth cookies.
46
- */
47
- export async function performVtexLogout(cookies: string): Promise<{ setCookies: string[] }> {
48
- const config = getVtexConfig();
49
- const domain = config.domain ?? "com.br";
50
- const logoutUrl = `https://${config.account}.vtexcommercestable.${domain}/api/vtexid/pub/logout?scope=${config.account}&returnUrl=/`;
51
-
52
- const res = await getVtexFetch()(logoutUrl, {
53
- method: "GET",
54
- headers: { cookie: cookies },
55
- redirect: "manual",
56
- operation: "vtexid.logout",
57
- });
58
-
59
- const upstreamCookies = res.headers.getSetCookie?.() ?? [];
60
-
61
- return {
62
- setCookies: [...stripCookieDomain(upstreamCookies), ...VTEX_LOGOUT_COOKIES],
63
- };
64
- }
65
-
66
- /**
67
- * Parse VTEX auth JWT to extract email and userId.
68
- * Reads the VtexIdclientAutCookie_* cookie from a raw Cookie header.
69
- */
70
- export function parseVtexAuthJwt(rawCookies: string): { email: string; userId: string } | null {
71
- try {
72
- const match = rawCookies.match(/VtexIdclientAutCookie_[^=]+=([^;]+)/);
73
- if (!match) return null;
74
- const token = match[1];
75
- const parts = token.split(".");
76
- if (parts.length < 2) return null;
77
- const payload = JSON.parse(
78
- Buffer.from(parts[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf-8"),
79
- );
80
- if (!payload.sub) return null;
81
- return { email: payload.sub, userId: payload.userId ?? "" };
82
- } catch {
83
- return null;
84
- }
85
- }
@@ -1,18 +0,0 @@
1
- export const batch = <T>(iterable: IterableIterator<T> | T[], size: number): T[][] => {
2
- const batches: T[][] = [];
3
-
4
- let current = 0;
5
- for (const item of iterable) {
6
- if (batches[current]?.length === size) {
7
- current++;
8
- }
9
-
10
- if (!batches[current]) {
11
- batches[current] = [];
12
- }
13
-
14
- batches[current].push(item);
15
- }
16
-
17
- return batches;
18
- };
@@ -1,96 +0,0 @@
1
- /**
2
- * Project a VTEX OrderForm down to a requested `CartProjection`.
3
- *
4
- * The VTEX checkout API always returns the whole OrderForm from mutation
5
- * endpoints (bounded only by `expectedOrderFormSections`) — there is no
6
- * delta/patch response. So "return the minimum" is enforced **server-side**:
7
- * we take whatever VTEX gave us and shape it into `none` / `summary` /
8
- * `summary+items` / `minicart` / `raw` before it reaches the browser.
9
- *
10
- * Pure function — no I/O, fully unit-testable. All monetary values in the
11
- * projected output are in major units (VTEX is cents).
12
- *
13
- * @see `@decocms/apps-commerce/types/cart` for the agnostic contract.
14
- */
15
-
16
- import type {
17
- CartItemSlim,
18
- CartOk,
19
- CartProjection,
20
- CartSummary,
21
- CartSummaryWithItems,
22
- Minicart,
23
- } from "@decocms/apps-commerce/types";
24
- import type { OrderForm, OrderFormItem } from "../types";
25
- import { type VtexOrderFormToMinicartOptions, vtexOrderFormToMinicart } from "./minicart";
26
-
27
- const CENTS_PER_MAJOR = 100;
28
-
29
- function fromCents(cents: number | undefined | null): number {
30
- if (cents == null || !Number.isFinite(cents)) return 0;
31
- return cents / CENTS_PER_MAJOR;
32
- }
33
-
34
- /** Sum of line quantities. */
35
- function countItems(orderForm: OrderForm): number {
36
- return (orderForm.items ?? []).reduce((sum, i) => sum + (i.quantity ?? 0), 0);
37
- }
38
-
39
- function toSummary(orderForm: OrderForm): CartSummary {
40
- return {
41
- orderFormId: orderForm.orderFormId ?? null,
42
- totalItems: countItems(orderForm),
43
- total: fromCents(orderForm.value),
44
- };
45
- }
46
-
47
- /** Map a VTEX line to the slim, analytics-compatible `CartItemSlim`. */
48
- function toSlimItem(item: OrderFormItem): CartItemSlim {
49
- return {
50
- item_id: item.id,
51
- item_name: item.name ?? item.skuName ?? "",
52
- item_variant: item.skuName,
53
- image: item.imageUrl?.replace(/^http:/, "https:") ?? "",
54
- price: fromCents(item.sellingPrice ?? item.price),
55
- quantity: item.quantity,
56
- };
57
- }
58
-
59
- export type ProjectOrderFormOptions = VtexOrderFormToMinicartOptions;
60
-
61
- /** The projected result type, with VTEX-concrete `minicart`/`raw` payloads. */
62
- export type VtexCartProjectionResult =
63
- | CartOk
64
- | CartSummary
65
- | CartSummaryWithItems
66
- | Minicart<OrderForm>
67
- | OrderForm;
68
-
69
- /**
70
- * Shape a VTEX OrderForm into the requested projection.
71
- *
72
- * @param orderForm - Raw OrderForm from a VTEX checkout call.
73
- * @param projection - Desired client-facing shape.
74
- * @param opts - Storefront overrides forwarded to the `minicart` projection.
75
- */
76
- export function projectOrderForm(
77
- orderForm: OrderForm,
78
- projection: CartProjection,
79
- opts: ProjectOrderFormOptions = {},
80
- ): VtexCartProjectionResult {
81
- switch (projection) {
82
- case "none":
83
- return { ok: true };
84
- case "summary":
85
- return toSummary(orderForm);
86
- case "summary+items":
87
- return {
88
- ...toSummary(orderForm),
89
- items: (orderForm.items ?? []).map(toSlimItem),
90
- };
91
- case "minicart":
92
- return vtexOrderFormToMinicart(orderForm, opts);
93
- case "raw":
94
- return orderForm;
95
- }
96
- }
@@ -1,167 +0,0 @@
1
- interface Cookie {
2
- name: string;
3
- value: string;
4
- domain?: string;
5
- path?: string;
6
- expires?: Date;
7
- maxAge?: number;
8
- secure?: boolean;
9
- httpOnly?: boolean;
10
- sameSite?: "Strict" | "Lax" | "None";
11
- }
12
-
13
- function parseSingleSetCookie(raw: string): Cookie | null {
14
- const parts = raw.split(";").map((p) => p.trim());
15
- const [nameValue, ...attrs] = parts;
16
- const eqIdx = nameValue.indexOf("=");
17
- if (eqIdx < 0) return null;
18
- const cookie: Cookie = {
19
- name: nameValue.slice(0, eqIdx),
20
- value: nameValue.slice(eqIdx + 1),
21
- };
22
- for (const attr of attrs) {
23
- const eqi = attr.indexOf("=");
24
- const k = (eqi >= 0 ? attr.slice(0, eqi) : attr).trim();
25
- const v = eqi >= 0 ? attr.slice(eqi + 1).trim() : "";
26
- const lower = k.toLowerCase();
27
- if (lower === "domain") cookie.domain = v;
28
- else if (lower === "path") cookie.path = v;
29
- else if (lower === "secure") cookie.secure = true;
30
- else if (lower === "httponly") cookie.httpOnly = true;
31
- else if (lower === "samesite") cookie.sameSite = v as Cookie["sameSite"];
32
- else if (lower === "max-age") {
33
- const n = Number(v);
34
- if (!Number.isNaN(n)) cookie.maxAge = n;
35
- } else if (lower === "expires") {
36
- const d = new Date(v);
37
- if (!Number.isNaN(d.getTime())) cookie.expires = d;
38
- }
39
- }
40
- return cookie;
41
- }
42
-
43
- /**
44
- * Extract individual Set-Cookie values from a Headers object.
45
- *
46
- * Uses Headers.getSetCookie() (available in Cloudflare Workers and Node 18+)
47
- * which returns each Set-Cookie as a separate string — unlike Headers.get()
48
- * or Headers.forEach() which join multiple values with ", " and corrupt
49
- * cookie strings that contain commas in Expires dates.
50
- */
51
- function getSetCookies(headers: Headers): Cookie[] {
52
- const rawCookies: string[] =
53
- typeof headers.getSetCookie === "function"
54
- ? headers.getSetCookie()
55
- : getRawSetCookiesFallback(headers);
56
-
57
- const cookies: Cookie[] = [];
58
- for (const raw of rawCookies) {
59
- const cookie = parseSingleSetCookie(raw);
60
- if (cookie) cookies.push(cookie);
61
- }
62
- return cookies;
63
- }
64
-
65
- /**
66
- * Fallback for runtimes without Headers.getSetCookie().
67
- * Splits the comma-joined string heuristically — not perfect for cookies
68
- * with Expires containing commas, but better than the old approach.
69
- */
70
- function getRawSetCookiesFallback(headers: Headers): string[] {
71
- const joined = headers.get("set-cookie");
72
- if (!joined) return [];
73
- const results: string[] = [];
74
- let current = "";
75
- for (const segment of joined.split(",")) {
76
- const trimmed = segment.trimStart();
77
- const looksLikeNewCookie = /^[^=;]+=[^;]/.test(trimmed) && current.length > 0;
78
- if (looksLikeNewCookie) {
79
- results.push(current.trim());
80
- current = trimmed;
81
- } else {
82
- current += (current ? "," : "") + segment;
83
- }
84
- }
85
- if (current.trim()) results.push(current.trim());
86
- return results;
87
- }
88
-
89
- function setCookie(headers: Headers, cookie: Cookie): void {
90
- let str = `${cookie.name}=${cookie.value}`;
91
- if (cookie.domain) str += `; Domain=${cookie.domain}`;
92
- if (cookie.path) str += `; Path=${cookie.path}`;
93
- if (cookie.secure) str += "; Secure";
94
- if (cookie.httpOnly) str += "; HttpOnly";
95
- if (cookie.sameSite) str += `; SameSite=${cookie.sameSite}`;
96
- if (cookie.maxAge != null) str += `; Max-Age=${cookie.maxAge}`;
97
- if (cookie.expires) str += `; Expires=${cookie.expires.toUTCString()}`;
98
- headers.append("Set-Cookie", str);
99
- }
100
-
101
- export const stringify = (cookies: Record<string, string>) =>
102
- Object.entries(cookies)
103
- .map(([key, value]) => `${key}=${value}`)
104
- .join("; ");
105
-
106
- export const proxySetCookie = (from: Headers, to: Headers, toDomain?: URL | string) => {
107
- const newDomain = toDomain && new URL(toDomain);
108
-
109
- for (const cookie of getSetCookies(from)) {
110
- const newCookie = newDomain
111
- ? {
112
- ...cookie,
113
- domain: newDomain.hostname,
114
- }
115
- : cookie;
116
-
117
- setCookie(to, newCookie);
118
- }
119
- };
120
-
121
- export const CHECKOUT_DATA_ACCESS_COOKIE = "CheckoutDataAccess";
122
- export const VTEX_CHKO_AUTH = "Vtex_CHKO_Auth";
123
-
124
- // Re-export the canonical allowlist from cookieSanitizer so consumers that
125
- // previously imported it from this module keep working. The single source
126
- // of truth lives in cookieSanitizer.ts.
127
- export { VTEX_COOKIE_PREFIXES } from "./cookieSanitizer";
128
-
129
- import { extractVtexCookies } from "./cookieSanitizer";
130
-
131
- /**
132
- * Filter a request's cookies to only VTEX-relevant ones.
133
- *
134
- * Strict allowlist: drops any cookie not on `VTEX_COOKIE_PREFIXES` plus any
135
- * cookie whose value contains non-ASCII bytes (which would make VTEX's
136
- * janus gateway return 503).
137
- */
138
- export function getVtexCookies(request: Request): string {
139
- return extractVtexCookies(request.headers.get("cookie") ?? "");
140
- }
141
-
142
- /**
143
- * Ensure the unsuffixed VtexIdclientAutCookie is present alongside the
144
- * account-suffixed variant (e.g. VtexIdclientAutCookie_myaccount).
145
- *
146
- * VTEX GraphQL requires both the suffixed AND unsuffixed cookie for
147
- * authenticated mutations. The browser only stores the suffixed variant,
148
- * so server-side code must synthesize the unsuffixed one.
149
- */
150
- export function ensureUnsuffixedAuthCookie(cookieStr: string): string {
151
- if (!cookieStr) return cookieStr;
152
- const cookies = cookieStr.split(";").map((c) => c.trim());
153
- let hasUnsuffixed = false;
154
- let suffixedToken: string | null = null;
155
- for (const c of cookies) {
156
- const [name, ...rest] = c.split("=");
157
- if (name === "VtexIdclientAutCookie") {
158
- hasUnsuffixed = true;
159
- } else if (name?.startsWith("VtexIdclientAutCookie_") && !suffixedToken) {
160
- suffixedToken = rest.join("=");
161
- }
162
- }
163
- if (!hasUnsuffixed && suffixedToken) {
164
- return `VtexIdclientAutCookie=${suffixedToken}; ${cookieStr}`;
165
- }
166
- return cookieStr;
167
- }