@decocms/apps-vtex 8.1.0-next.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 (139) hide show
  1. package/package.json +7 -52
  2. package/src/index.ts +10 -28
  3. package/src/utils/constants.ts +2 -56
  4. package/src/utils/cookieSanitizer.ts +59 -59
  5. package/src/utils/operationRouter.ts +75 -65
  6. package/src/utils/types.ts +709 -1557
  7. package/src/vtexClient.ts +354 -343
  8. package/src/README.md +0 -6
  9. package/src/__conformance__/upstream.test.ts +0 -158
  10. package/src/__tests__/client-segment-cookie.test.ts +0 -255
  11. package/src/__tests__/client-set-cookie-forward.test.ts +0 -257
  12. package/src/__tests__/schemas.test.ts +0 -71
  13. package/src/actions/address.ts +0 -250
  14. package/src/actions/analytics/sendEvent.ts +0 -86
  15. package/src/actions/auth.ts +0 -333
  16. package/src/actions/checkout.ts +0 -659
  17. package/src/actions/index.ts +0 -11
  18. package/src/actions/masterData.ts +0 -168
  19. package/src/actions/misc.ts +0 -188
  20. package/src/actions/newsletter.ts +0 -105
  21. package/src/actions/orders.ts +0 -34
  22. package/src/actions/profile.ts +0 -201
  23. package/src/actions/session.ts +0 -85
  24. package/src/actions/trigger.ts +0 -43
  25. package/src/actions/wishlist.ts +0 -114
  26. package/src/client.ts +0 -724
  27. package/src/commerceLoaders.ts +0 -267
  28. package/src/hooks/__tests__/createCart.render.test.ts +0 -114
  29. package/src/hooks/__tests__/createCart.test.ts +0 -92
  30. package/src/hooks/__tests__/createUseCart.test.ts +0 -184
  31. package/src/hooks/__tests__/createUseUser.test.ts +0 -48
  32. package/src/hooks/__tests__/createUseWishlist.test.ts +0 -87
  33. package/src/hooks/cartQuery.ts +0 -204
  34. package/src/hooks/createCart.ts +0 -427
  35. package/src/hooks/createUseCart.ts +0 -360
  36. package/src/hooks/createUseUser.ts +0 -153
  37. package/src/hooks/createUseWishlist.ts +0 -242
  38. package/src/hooks/index.ts +0 -25
  39. package/src/hooks/useAutocomplete.ts +0 -83
  40. package/src/hooks/useCart.ts +0 -243
  41. package/src/hooks/useUser.ts +0 -78
  42. package/src/hooks/useWishlist.ts +0 -119
  43. package/src/invoke.ts +0 -235
  44. package/src/loaders/ProductDetailsPage.ts +0 -1
  45. package/src/loaders/ProductList.ts +0 -1
  46. package/src/loaders/ProductListingPage.ts +0 -1
  47. package/src/loaders/__tests__/legacyProductList.test.ts +0 -157
  48. package/src/loaders/address.ts +0 -115
  49. package/src/loaders/autocomplete.ts +0 -56
  50. package/src/loaders/brands.ts +0 -44
  51. package/src/loaders/cart/attachments.ts +0 -53
  52. package/src/loaders/cart/full.ts +0 -67
  53. package/src/loaders/cart/gifts.ts +0 -43
  54. package/src/loaders/cart/orderFormId.ts +0 -31
  55. package/src/loaders/cart/shipping.test.ts +0 -125
  56. package/src/loaders/cart/shipping.ts +0 -136
  57. package/src/loaders/cart/summary.ts +0 -36
  58. package/src/loaders/cart.ts +0 -52
  59. package/src/loaders/catalog.ts +0 -163
  60. package/src/loaders/collections.ts +0 -55
  61. package/src/loaders/index.ts +0 -20
  62. package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +0 -51
  63. package/src/loaders/intelligentSearch/productDetailsPage.ts +0 -111
  64. package/src/loaders/intelligentSearch/productList.ts +0 -170
  65. package/src/loaders/intelligentSearch/productListingPage.ts +0 -525
  66. package/src/loaders/intelligentSearch/suggestions.ts +0 -44
  67. package/src/loaders/legacy/productDetailsPage.ts +0 -1
  68. package/src/loaders/legacy/productList.ts +0 -1
  69. package/src/loaders/legacy/relatedProductsLoader.ts +0 -79
  70. package/src/loaders/legacy.ts +0 -710
  71. package/src/loaders/logistics.ts +0 -111
  72. package/src/loaders/minicart.ts +0 -78
  73. package/src/loaders/navbar.ts +0 -26
  74. package/src/loaders/orders.ts +0 -92
  75. package/src/loaders/pageType.ts +0 -68
  76. package/src/loaders/paths/PDPDefaultPath.ts +0 -51
  77. package/src/loaders/payment.ts +0 -97
  78. package/src/loaders/productListFull.ts +0 -157
  79. package/src/loaders/profile.ts +0 -138
  80. package/src/loaders/promotion.ts +0 -30
  81. package/src/loaders/search.ts +0 -124
  82. package/src/loaders/session.ts +0 -83
  83. package/src/loaders/user.ts +0 -87
  84. package/src/loaders/wishlist.ts +0 -89
  85. package/src/loaders/wishlistProducts.ts +0 -69
  86. package/src/loaders/workflow/products.ts +0 -62
  87. package/src/loaders/workflow.ts +0 -316
  88. package/src/logo.png +0 -0
  89. package/src/manifest.gen.ts +0 -89
  90. package/src/middleware.cacheHeaders.test.ts +0 -44
  91. package/src/middleware.ts +0 -240
  92. package/src/mod.ts +0 -173
  93. package/src/registry.ts +0 -9
  94. package/src/schemas.gen.ts +0 -3191
  95. package/src/schemas.ts +0 -30
  96. package/src/types.ts +0 -248
  97. package/src/utils/__tests__/cartProjection.test.ts +0 -118
  98. package/src/utils/__tests__/cookieSanitizer.test.ts +0 -162
  99. package/src/utils/__tests__/fetch.test.ts +0 -80
  100. package/src/utils/__tests__/fetchCache.test.ts +0 -155
  101. package/src/utils/__tests__/instrumentedFetch.test.ts +0 -158
  102. package/src/utils/__tests__/intelligentSearch.test.ts +0 -88
  103. package/src/utils/__tests__/minicart.test.ts +0 -184
  104. package/src/utils/__tests__/operationRouter.test.ts +0 -227
  105. package/src/utils/__tests__/resilience.test.ts +0 -205
  106. package/src/utils/__tests__/resourceRange.test.ts +0 -32
  107. package/src/utils/__tests__/simulationCache.test.ts +0 -40
  108. package/src/utils/__tests__/sitemap.test.ts +0 -185
  109. package/src/utils/__tests__/slugify.test.ts +0 -31
  110. package/src/utils/__tests__/storefrontBaseUrl.test.ts +0 -61
  111. package/src/utils/__tests__/transform.test.ts +0 -1111
  112. package/src/utils/accountLoaders.ts +0 -203
  113. package/src/utils/authHelpers.ts +0 -85
  114. package/src/utils/batch.ts +0 -18
  115. package/src/utils/buildOfferShelf.test.ts +0 -59
  116. package/src/utils/cartProjection.ts +0 -96
  117. package/src/utils/cookies.ts +0 -167
  118. package/src/utils/enrichment.ts +0 -556
  119. package/src/utils/fetch.ts +0 -111
  120. package/src/utils/fetchCache.ts +0 -55
  121. package/src/utils/index.ts +0 -19
  122. package/src/utils/instrumentedFetch.ts +0 -98
  123. package/src/utils/intelligentSearch.ts +0 -96
  124. package/src/utils/legacy.ts +0 -139
  125. package/src/utils/minicart.ts +0 -139
  126. package/src/utils/pickAndOmit.ts +0 -25
  127. package/src/utils/proxy.ts +0 -495
  128. package/src/utils/resilience.ts +0 -352
  129. package/src/utils/resourceRange.ts +0 -10
  130. package/src/utils/segment.ts +0 -152
  131. package/src/utils/similars.ts +0 -35
  132. package/src/utils/simulationCache.ts +0 -73
  133. package/src/utils/sitemap.ts +0 -283
  134. package/src/utils/slugCache.ts +0 -28
  135. package/src/utils/slugify.ts +0 -13
  136. package/src/utils/transform.ts +0 -1691
  137. package/src/utils/vtexId.ts +0 -151
  138. package/src/vtexClient.test.ts +0 -204
  139. 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,59 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import type { AggregateOffer, Offer, UnitPriceSpecification } from "@decocms/apps-commerce/types";
3
- import { useOffer } from "@decocms/apps-commerce/sdk/useOffer";
4
- import { buildOfferShelf } from "./transform";
5
-
6
- const installment = (
7
- over: Partial<UnitPriceSpecification>,
8
- ): UnitPriceSpecification =>
9
- ({
10
- "@type": "UnitPriceSpecification",
11
- priceType: "https://schema.org/SalePrice",
12
- priceComponentType: "https://schema.org/Installment",
13
- ...over,
14
- }) as UnitPriceSpecification;
15
-
16
- const offerWith = (specs: UnitPriceSpecification[]): Offer =>
17
- ({
18
- "@type": "Offer",
19
- price: 100,
20
- priceSpecification: [
21
- { "@type": "UnitPriceSpecification", priceType: "https://schema.org/ListPrice", price: 120 },
22
- { "@type": "UnitPriceSpecification", priceType: "https://schema.org/SalePrice", price: 100 },
23
- ...specs,
24
- ],
25
- }) as Offer;
26
-
27
- const pickInstallment = (offer: Offer) =>
28
- useOffer({ "@type": "AggregateOffer", offers: [offer] } as AggregateOffer).installment;
29
-
30
- describe("buildOfferShelf installment selection", () => {
31
- it("keeps the same installment useOffer picks on PLP/PDP (shelf == detail)", () => {
32
- // 12x whose stated total (100) is the lowest and duration the highest, but VTEX
33
- // rounding makes 12 * 8.34 = 100.08 — the old 1-cent isNoInterest check rejected
34
- // it and the shelf fell back to 10x, diverging from the detail page's 12x.
35
- const full = offerWith([
36
- installment({ name: "PIX", billingDuration: 1, billingIncrement: 100, price: 100 }),
37
- installment({ name: "Visa", billingDuration: 10, billingIncrement: 10, price: 100 }),
38
- installment({ name: "Visa", billingDuration: 12, billingIncrement: 8.34, price: 100 }),
39
- installment({ name: "Visa", billingDuration: 6, billingIncrement: 17.67, price: 106 }),
40
- ]);
41
-
42
- const lean = buildOfferShelf(full);
43
-
44
- expect(pickInstallment(lean)).toEqual(pickInstallment(full));
45
- expect(pickInstallment(lean)?.billingDuration).toBe(12);
46
- });
47
-
48
- it("keeps the lowest-total plan even when every installment has interest", () => {
49
- const full = offerWith([
50
- installment({ name: "Visa", billingDuration: 3, billingIncrement: 34, price: 102 }),
51
- installment({ name: "Visa", billingDuration: 6, billingIncrement: 18, price: 108 }),
52
- ]);
53
-
54
- const lean = buildOfferShelf(full);
55
-
56
- expect(pickInstallment(lean)).toEqual(pickInstallment(full));
57
- expect(pickInstallment(lean)?.price).toBe(102);
58
- });
59
- });
@@ -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
- }