@decocms/apps-vtex 8.1.0-next.0 → 8.1.0-next.2

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 (163) hide show
  1. package/dist/index.d.ts +6 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +6 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/utils/constants.d.ts +28 -0
  6. package/dist/utils/constants.d.ts.map +1 -0
  7. package/dist/utils/constants.js +17 -0
  8. package/dist/utils/constants.js.map +1 -0
  9. package/dist/utils/cookieSanitizer.d.ts +87 -0
  10. package/dist/utils/cookieSanitizer.d.ts.map +1 -0
  11. package/dist/utils/cookieSanitizer.js +136 -0
  12. package/dist/utils/cookieSanitizer.js.map +1 -0
  13. package/dist/utils/operationRouter.d.ts +25 -0
  14. package/dist/utils/operationRouter.d.ts.map +1 -0
  15. package/dist/utils/operationRouter.js +120 -0
  16. package/dist/utils/operationRouter.js.map +1 -0
  17. package/dist/utils/types.d.ts +944 -0
  18. package/dist/utils/types.d.ts.map +1 -0
  19. package/dist/utils/types.js +25 -0
  20. package/dist/utils/types.js.map +1 -0
  21. package/dist/vtexClient.d.ts +173 -0
  22. package/dist/vtexClient.d.ts.map +1 -0
  23. package/dist/vtexClient.js +242 -0
  24. package/dist/vtexClient.js.map +1 -0
  25. package/package.json +16 -57
  26. package/src/index.ts +11 -29
  27. package/src/utils/constants.ts +2 -56
  28. package/src/utils/cookieSanitizer.ts +59 -59
  29. package/src/utils/operationRouter.ts +75 -65
  30. package/src/utils/types.ts +709 -1557
  31. package/src/vtexClient.ts +358 -347
  32. package/src/README.md +0 -6
  33. package/src/__conformance__/upstream.test.ts +0 -158
  34. package/src/__tests__/client-segment-cookie.test.ts +0 -255
  35. package/src/__tests__/client-set-cookie-forward.test.ts +0 -257
  36. package/src/__tests__/schemas.test.ts +0 -71
  37. package/src/actions/address.ts +0 -250
  38. package/src/actions/analytics/sendEvent.ts +0 -86
  39. package/src/actions/auth.ts +0 -333
  40. package/src/actions/checkout.ts +0 -659
  41. package/src/actions/index.ts +0 -11
  42. package/src/actions/masterData.ts +0 -168
  43. package/src/actions/misc.ts +0 -188
  44. package/src/actions/newsletter.ts +0 -105
  45. package/src/actions/orders.ts +0 -34
  46. package/src/actions/profile.ts +0 -201
  47. package/src/actions/session.ts +0 -85
  48. package/src/actions/trigger.ts +0 -43
  49. package/src/actions/wishlist.ts +0 -114
  50. package/src/client.ts +0 -724
  51. package/src/commerceLoaders.ts +0 -267
  52. package/src/hooks/__tests__/createCart.render.test.ts +0 -114
  53. package/src/hooks/__tests__/createCart.test.ts +0 -92
  54. package/src/hooks/__tests__/createUseCart.test.ts +0 -184
  55. package/src/hooks/__tests__/createUseUser.test.ts +0 -48
  56. package/src/hooks/__tests__/createUseWishlist.test.ts +0 -87
  57. package/src/hooks/cartQuery.ts +0 -204
  58. package/src/hooks/createCart.ts +0 -427
  59. package/src/hooks/createUseCart.ts +0 -360
  60. package/src/hooks/createUseUser.ts +0 -153
  61. package/src/hooks/createUseWishlist.ts +0 -242
  62. package/src/hooks/index.ts +0 -25
  63. package/src/hooks/useAutocomplete.ts +0 -83
  64. package/src/hooks/useCart.ts +0 -243
  65. package/src/hooks/useUser.ts +0 -78
  66. package/src/hooks/useWishlist.ts +0 -119
  67. package/src/invoke.ts +0 -235
  68. package/src/loaders/ProductDetailsPage.ts +0 -1
  69. package/src/loaders/ProductList.ts +0 -1
  70. package/src/loaders/ProductListingPage.ts +0 -1
  71. package/src/loaders/__tests__/legacyProductList.test.ts +0 -157
  72. package/src/loaders/address.ts +0 -115
  73. package/src/loaders/autocomplete.ts +0 -56
  74. package/src/loaders/brands.ts +0 -44
  75. package/src/loaders/cart/attachments.ts +0 -53
  76. package/src/loaders/cart/full.ts +0 -67
  77. package/src/loaders/cart/gifts.ts +0 -43
  78. package/src/loaders/cart/orderFormId.ts +0 -31
  79. package/src/loaders/cart/shipping.test.ts +0 -125
  80. package/src/loaders/cart/shipping.ts +0 -136
  81. package/src/loaders/cart/summary.ts +0 -36
  82. package/src/loaders/cart.ts +0 -52
  83. package/src/loaders/catalog.ts +0 -163
  84. package/src/loaders/collections.ts +0 -55
  85. package/src/loaders/index.ts +0 -20
  86. package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +0 -51
  87. package/src/loaders/intelligentSearch/productDetailsPage.ts +0 -111
  88. package/src/loaders/intelligentSearch/productList.ts +0 -170
  89. package/src/loaders/intelligentSearch/productListingPage.ts +0 -525
  90. package/src/loaders/intelligentSearch/suggestions.ts +0 -44
  91. package/src/loaders/legacy/productDetailsPage.ts +0 -1
  92. package/src/loaders/legacy/productList.ts +0 -1
  93. package/src/loaders/legacy/relatedProductsLoader.ts +0 -79
  94. package/src/loaders/legacy.ts +0 -710
  95. package/src/loaders/logistics.ts +0 -111
  96. package/src/loaders/minicart.ts +0 -78
  97. package/src/loaders/navbar.ts +0 -26
  98. package/src/loaders/orders.ts +0 -92
  99. package/src/loaders/pageType.ts +0 -68
  100. package/src/loaders/paths/PDPDefaultPath.ts +0 -51
  101. package/src/loaders/payment.ts +0 -97
  102. package/src/loaders/productListFull.ts +0 -157
  103. package/src/loaders/profile.ts +0 -138
  104. package/src/loaders/promotion.ts +0 -30
  105. package/src/loaders/search.ts +0 -124
  106. package/src/loaders/session.ts +0 -83
  107. package/src/loaders/user.ts +0 -87
  108. package/src/loaders/wishlist.ts +0 -89
  109. package/src/loaders/wishlistProducts.ts +0 -69
  110. package/src/loaders/workflow/products.ts +0 -62
  111. package/src/loaders/workflow.ts +0 -316
  112. package/src/logo.png +0 -0
  113. package/src/manifest.gen.ts +0 -89
  114. package/src/middleware.cacheHeaders.test.ts +0 -44
  115. package/src/middleware.ts +0 -240
  116. package/src/mod.ts +0 -173
  117. package/src/registry.ts +0 -9
  118. package/src/schemas.gen.ts +0 -3191
  119. package/src/schemas.ts +0 -30
  120. package/src/types.ts +0 -248
  121. package/src/utils/__tests__/cartProjection.test.ts +0 -118
  122. package/src/utils/__tests__/cookieSanitizer.test.ts +0 -162
  123. package/src/utils/__tests__/fetch.test.ts +0 -80
  124. package/src/utils/__tests__/fetchCache.test.ts +0 -155
  125. package/src/utils/__tests__/instrumentedFetch.test.ts +0 -158
  126. package/src/utils/__tests__/intelligentSearch.test.ts +0 -88
  127. package/src/utils/__tests__/minicart.test.ts +0 -184
  128. package/src/utils/__tests__/operationRouter.test.ts +0 -227
  129. package/src/utils/__tests__/resilience.test.ts +0 -205
  130. package/src/utils/__tests__/resourceRange.test.ts +0 -32
  131. package/src/utils/__tests__/simulationCache.test.ts +0 -40
  132. package/src/utils/__tests__/sitemap.test.ts +0 -185
  133. package/src/utils/__tests__/slugify.test.ts +0 -31
  134. package/src/utils/__tests__/storefrontBaseUrl.test.ts +0 -61
  135. package/src/utils/__tests__/transform.test.ts +0 -1111
  136. package/src/utils/accountLoaders.ts +0 -203
  137. package/src/utils/authHelpers.ts +0 -85
  138. package/src/utils/batch.ts +0 -18
  139. package/src/utils/buildOfferShelf.test.ts +0 -59
  140. package/src/utils/cartProjection.ts +0 -96
  141. package/src/utils/cookies.ts +0 -167
  142. package/src/utils/enrichment.ts +0 -556
  143. package/src/utils/fetch.ts +0 -111
  144. package/src/utils/fetchCache.ts +0 -55
  145. package/src/utils/index.ts +0 -19
  146. package/src/utils/instrumentedFetch.ts +0 -98
  147. package/src/utils/intelligentSearch.ts +0 -96
  148. package/src/utils/legacy.ts +0 -139
  149. package/src/utils/minicart.ts +0 -139
  150. package/src/utils/pickAndOmit.ts +0 -25
  151. package/src/utils/proxy.ts +0 -495
  152. package/src/utils/resilience.ts +0 -352
  153. package/src/utils/resourceRange.ts +0 -10
  154. package/src/utils/segment.ts +0 -152
  155. package/src/utils/similars.ts +0 -35
  156. package/src/utils/simulationCache.ts +0 -73
  157. package/src/utils/sitemap.ts +0 -283
  158. package/src/utils/slugCache.ts +0 -28
  159. package/src/utils/slugify.ts +0 -13
  160. package/src/utils/transform.ts +0 -1691
  161. package/src/utils/vtexId.ts +0 -151
  162. package/src/vtexClient.test.ts +0 -204
  163. package/tsconfig.json +0 -7
package/src/schemas.ts DELETED
@@ -1,30 +0,0 @@
1
- /**
2
- * Registers the real props schemas for VTEX loaders/actions into the CMS
3
- * schema registry, so `GET /deco/meta` publishes full props forms (enums,
4
- * titles, required) instead of the `__resolveType`-only stubs that
5
- * registerCommerceLoaders() auto-registers.
6
- *
7
- * The schemas come from `schemas.gen.ts`, generated at build time by
8
- * @decocms/blocks-cli's generate-app-schemas.ts (`bun run generate:schemas`)
9
- * — Props are TypeScript types, erased at runtime, so extraction cannot
10
- * happen in the site.
11
- *
12
- * Called from every server entrypoint a site can wire VTEX through
13
- * (createVtexCommerceLoaders, mod.configure). Idempotent; real schemas also
14
- * take precedence over stubs inside the registry, so call order relative to
15
- * registerCommerceLoaders() doesn't matter.
16
- */
17
- import { registerAppSchemas } from "@decocms/blocks/cms/client";
18
- import { actionSchemas, loaderSchemas } from "./schemas.gen";
19
-
20
- let registered = false;
21
-
22
- export function registerVtexSchemas(): void {
23
- if (registered) return;
24
- registered = true;
25
- registerAppSchemas({
26
- namespace: "vtex",
27
- loaders: loaderSchemas,
28
- actions: actionSchemas,
29
- });
30
- }
package/src/types.ts DELETED
@@ -1,248 +0,0 @@
1
- /**
2
- * VTEX Checkout / OrderForm types.
3
- * These mirror the VTEX Checkout API response shapes.
4
- * Aligned with deco-cx/apps vtex/utils/types.ts
5
- */
6
-
7
- export interface OrderFormItem {
8
- id: string;
9
- productId: string;
10
- productRefId?: string;
11
- refId?: string;
12
- ean?: string | null;
13
- name: string;
14
- skuName: string;
15
- imageUrl: string;
16
- detailUrl: string;
17
- price: number;
18
- listPrice: number;
19
- manualPrice?: number | null;
20
- quantity: number;
21
- sellingPrice: number;
22
- rewardValue?: number;
23
- isGift?: boolean;
24
- tax?: number;
25
- seller: string;
26
- sellerChain?: string[];
27
- uniqueId: string;
28
- parentItemIndex?: number | null;
29
- parentAssemblyBinding?: string | null;
30
- availability?: string;
31
- measurementUnit?: string;
32
- unitMultiplier?: number;
33
- productCategoryIds?: string;
34
- productCategories?: Record<string, string>;
35
- additionalInfo?: {
36
- brandName?: string;
37
- brandId?: string;
38
- dimension?: Record<string, string> | null;
39
- offeringInfo?: unknown | null;
40
- offeringType?: unknown | null;
41
- offeringTypeId?: unknown | null;
42
- };
43
- attachments?: unknown[];
44
- attachmentOfferings?: Array<{
45
- name: string;
46
- required: boolean;
47
- schema: Record<string, unknown>;
48
- }>;
49
- offerings?: Array<{
50
- type: string;
51
- id: string;
52
- name: string;
53
- price: number;
54
- }>;
55
- priceTags?: Array<{
56
- name: string;
57
- value: number;
58
- rawValue: number;
59
- isPercentual: boolean;
60
- identifier: string | null;
61
- }>;
62
- components?: unknown[];
63
- bundleItems?: unknown[];
64
- priceDefinition?: {
65
- calculatedSellingPrice: number;
66
- total: number;
67
- sellingPrices: Array<{
68
- value: number;
69
- quantity: number;
70
- }>;
71
- };
72
- }
73
-
74
- export interface Totalizer {
75
- id: string;
76
- name: string;
77
- value: number;
78
- }
79
-
80
- export interface Message {
81
- code: string;
82
- text: string;
83
- status: string;
84
- fields?: Record<string, string>;
85
- }
86
-
87
- export interface MarketingData {
88
- utmSource?: string;
89
- utmMedium?: string;
90
- utmCampaign?: string;
91
- utmiPage?: string;
92
- utmiPart?: string;
93
- utmiCampaign?: string;
94
- coupon?: string;
95
- marketingTags?: string[];
96
- }
97
-
98
- export interface ClientProfileData {
99
- email: string;
100
- firstName?: string | null;
101
- lastName?: string | null;
102
- document?: string | null;
103
- phone?: string | null;
104
- corporateName?: string | null;
105
- isCorporate?: boolean;
106
- }
107
-
108
- export interface StorePreferencesData {
109
- countryCode: string;
110
- saveUserData?: boolean;
111
- timeZone?: string;
112
- currencyCode: string;
113
- currencyLocale?: number;
114
- currencySymbol: string;
115
- currencyFormatInfo?: {
116
- currencyDecimalDigits: number;
117
- currencyDecimalSeparator: string;
118
- currencyGroupSeparator: string;
119
- currencyGroupSize: number;
120
- startsWithCurrencySymbol: boolean;
121
- };
122
- }
123
-
124
- export interface ClientPreferencesData {
125
- locale: string;
126
- optinNewsLetter?: boolean;
127
- }
128
-
129
- export interface ShippingData {
130
- address?: {
131
- postalCode?: string;
132
- city?: string;
133
- state?: string;
134
- country?: string;
135
- street?: string;
136
- number?: string;
137
- neighborhood?: string;
138
- complement?: string;
139
- reference?: string;
140
- } | null;
141
- selectedAddresses?: Array<{
142
- postalCode?: string;
143
- city?: string;
144
- state?: string;
145
- country?: string;
146
- }>;
147
- logisticsInfo?: Array<{
148
- itemIndex: number;
149
- selectedSla?: string;
150
- selectedDeliveryChannel?: string;
151
- slas?: Sla[];
152
- }>;
153
- }
154
-
155
- export interface PaymentData {
156
- updateStatus?: string;
157
- installmentOptions?: unknown[];
158
- paymentSystems?: unknown[];
159
- payments?: unknown[];
160
- giftCards?: unknown[];
161
- availableAccounts?: unknown[];
162
- }
163
-
164
- export interface OrderForm {
165
- orderFormId: string;
166
- salesChannel: string;
167
- loggedIn: boolean;
168
- isCheckedIn: boolean;
169
- storeId?: unknown | null;
170
- checkedInPickupPointId?: unknown | null;
171
- allowManualPrice: boolean;
172
- canEditData: boolean;
173
- userProfileId?: unknown | null;
174
- userType?: unknown | null;
175
- ignoreProfileData: boolean;
176
- value: number;
177
- messages: Message[];
178
- items: OrderFormItem[];
179
- selectableGifts?: unknown[];
180
- totalizers: Totalizer[];
181
- shippingData: ShippingData | null;
182
- clientProfileData: ClientProfileData | null;
183
- paymentData: PaymentData | null;
184
- marketingData: MarketingData | null;
185
- sellers?: Array<{ id: string; name: string; logo?: string }>;
186
- clientPreferencesData?: ClientPreferencesData | null;
187
- commercialConditionData?: unknown | null;
188
- storePreferencesData?: StorePreferencesData | null;
189
- giftRegistryData?: unknown | null;
190
- openTextField?: unknown | null;
191
- invoiceData?: unknown | null;
192
- customData?: unknown | null;
193
- itemMetadata?: unknown | null;
194
- hooksData?: unknown | null;
195
- ratesAndBenefitsData?: {
196
- rateAndBenefitsIdentifiers?: unknown[];
197
- teaser?: unknown[];
198
- } | null;
199
- subscriptionData?: unknown | null;
200
- merchantContextData?: unknown | null;
201
- itemsOrdination?: unknown | null;
202
- }
203
-
204
- export interface SimulationOrderForm {
205
- items: Array<{
206
- id: string;
207
- quantity: number;
208
- seller: string;
209
- price?: number;
210
- listPrice?: number;
211
- offerings?: any[];
212
- priceTags?: any[];
213
- availability?: string;
214
- }>;
215
- logisticsInfo?: Array<{
216
- itemIndex: number;
217
- slas: Sla[];
218
- selectedSla?: string;
219
- selectedDeliveryChannel?: string;
220
- }>;
221
- paymentData?: {
222
- installmentOptions?: any[];
223
- };
224
- }
225
-
226
- export interface Sla {
227
- id: string;
228
- name: string;
229
- price: number;
230
- shippingEstimate: string;
231
- deliveryChannel?: string;
232
- }
233
-
234
- export interface SKU {
235
- id: string;
236
- seller: string;
237
- quantity: number;
238
- }
239
-
240
- export interface VtexProduct {
241
- productId: string;
242
- productName: string;
243
- brand: string;
244
- categoryId: string;
245
- categories: string[];
246
- items: any[];
247
- [key: string]: any;
248
- }
@@ -1,118 +0,0 @@
1
- /**
2
- * Tests for `projectOrderForm` — the server-side fragmentation of a VTEX
3
- * OrderForm into the requested `CartProjection`. Pure function, no I/O.
4
- */
5
-
6
- import type { Minicart } from "@decocms/apps-commerce/types";
7
- import { describe, expect, it } from "vitest";
8
- import type { OrderForm } from "../../types";
9
- import { projectOrderForm } from "../cartProjection";
10
-
11
- function makeOrderForm(overrides: Partial<OrderForm> = {}): OrderForm {
12
- return {
13
- orderFormId: "of-123",
14
- salesChannel: "1",
15
- loggedIn: false,
16
- isCheckedIn: false,
17
- allowManualPrice: false,
18
- canEditData: true,
19
- ignoreProfileData: false,
20
- value: 15000, // R$150.00 in cents
21
- messages: [],
22
- items: [
23
- {
24
- id: "sku-1",
25
- productId: "prod-1",
26
- name: "Camiseta",
27
- skuName: "Camiseta P Azul",
28
- imageUrl: "http://img.example/x.jpg",
29
- detailUrl: "/camiseta/p",
30
- price: 10000,
31
- listPrice: 12000,
32
- sellingPrice: 10000,
33
- quantity: 1,
34
- seller: "1",
35
- uniqueId: "u1",
36
- },
37
- {
38
- id: "sku-2",
39
- productId: "prod-2",
40
- name: "Boné",
41
- skuName: "Boné U Preto",
42
- imageUrl: "https://img.example/y.jpg",
43
- detailUrl: "/bone/u",
44
- price: 5000,
45
- listPrice: 5000,
46
- sellingPrice: 5000,
47
- quantity: 2,
48
- seller: "1",
49
- uniqueId: "u2",
50
- },
51
- ],
52
- totalizers: [
53
- { id: "Items", name: "Items", value: 20000 },
54
- { id: "Discounts", name: "Discounts", value: -5000 },
55
- ],
56
- shippingData: null,
57
- clientProfileData: null,
58
- paymentData: null,
59
- marketingData: null,
60
- ...overrides,
61
- } as OrderForm;
62
- }
63
-
64
- describe("projectOrderForm", () => {
65
- it("none → { ok: true } and discards the payload", () => {
66
- expect(projectOrderForm(makeOrderForm(), "none")).toEqual({ ok: true });
67
- });
68
-
69
- it("summary → orderFormId, summed quantities, total in major units", () => {
70
- const r = projectOrderForm(makeOrderForm(), "summary") as {
71
- orderFormId: string;
72
- totalItems: number;
73
- total: number;
74
- };
75
- expect(r.orderFormId).toBe("of-123");
76
- expect(r.totalItems).toBe(3); // 1 + 2
77
- expect(r.total).toBe(150); // 15000 cents → 150
78
- });
79
-
80
- it("summary+items → summary plus slim, https-normalized items", () => {
81
- const r = projectOrderForm(makeOrderForm(), "summary+items") as {
82
- totalItems: number;
83
- items: Array<{ item_name: string; image: string; price: number; quantity: number }>;
84
- };
85
- expect(r.totalItems).toBe(3);
86
- expect(r.items).toHaveLength(2);
87
- expect(r.items[0]).toMatchObject({
88
- item_name: "Camiseta",
89
- item_variant: "Camiseta P Azul",
90
- image: "https://img.example/x.jpg", // http → https
91
- price: 100, // 10000 cents
92
- quantity: 1,
93
- });
94
- // slim projection must NOT leak the full VTEX item fields
95
- expect(r.items[0]).not.toHaveProperty("uniqueId");
96
- });
97
-
98
- it("minicart → canonical Minicart with major-unit totals", () => {
99
- const r = projectOrderForm(makeOrderForm(), "minicart") as Minicart<OrderForm>;
100
- expect(r.storefront.items).toHaveLength(2);
101
- expect(r.storefront.total).toBe(150);
102
- expect(r.original.orderFormId).toBe("of-123");
103
- });
104
-
105
- it("raw → the untouched OrderForm", () => {
106
- const of = makeOrderForm();
107
- expect(projectOrderForm(of, "raw")).toBe(of);
108
- });
109
-
110
- it("summary handles an empty cart without NaN", () => {
111
- const r = projectOrderForm(
112
- makeOrderForm({ items: [], value: 0, totalizers: [] }),
113
- "summary",
114
- ) as { totalItems: number; total: number };
115
- expect(r.totalItems).toBe(0);
116
- expect(r.total).toBe(0);
117
- });
118
- });
@@ -1,162 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import {
3
- _resetCookieWarnDedupForTests,
4
- extractVtexCookies,
5
- sanitizeOutboundCookieHeader,
6
- VTEX_COOKIE_PREFIXES,
7
- warnDroppedCookies,
8
- } from "../cookieSanitizer";
9
-
10
- describe("sanitizeOutboundCookieHeader", () => {
11
- it("returns empty result for empty input", () => {
12
- expect(sanitizeOutboundCookieHeader("")).toEqual({ cookies: "", dropped: [] });
13
- });
14
-
15
- it("preserves a clean ASCII cookie header unchanged in content", () => {
16
- const raw = "checkout.vtex.com=__ofid=abc; vtex_segment=eyJ0b2tlbiI6IjEyMyJ9";
17
- const { cookies, dropped } = sanitizeOutboundCookieHeader(raw);
18
- expect(dropped).toEqual([]);
19
- expect(cookies).toBe("checkout.vtex.com=__ofid=abc; vtex_segment=eyJ0b2tlbiI6IjEyMyJ9");
20
- });
21
-
22
- it("drops a cookie whose value contains non-ASCII bytes — a production VTEX storefront repro", () => {
23
- // `á` is the UTF-8 encoding of `á` interpreted as Latin-1 — bytes 0xC3 0xA1.
24
- // VTEX's janus gateway returns 503 deterministically when this reaches it.
25
- const raw = "checkout.vtex.com=__ofid=abc; category_click=Eletroportáteis; vtex_segment=ok";
26
- const { cookies, dropped } = sanitizeOutboundCookieHeader(raw);
27
- expect(cookies).toBe("checkout.vtex.com=__ofid=abc; vtex_segment=ok");
28
- expect(dropped).toEqual([{ name: "category_click", reason: "non_ascii" }]);
29
- });
30
-
31
- it("drops cookies with raw multi-byte UTF-8 characters", () => {
32
- const { cookies, dropped } = sanitizeOutboundCookieHeader(
33
- "checkout.vtex.com=ok; pref=日本語; analytics=tracked",
34
- );
35
- expect(cookies).toBe("checkout.vtex.com=ok; analytics=tracked");
36
- expect(dropped).toEqual([{ name: "pref", reason: "non_ascii" }]);
37
- });
38
-
39
- it("treats malformed pairs (no `=`, blank name) as dropped", () => {
40
- const { cookies, dropped } = sanitizeOutboundCookieHeader("justaname; =headless; ok=1; ;");
41
- expect(cookies).toBe("ok=1");
42
- expect(dropped.map((d) => d.reason)).toEqual(["malformed", "malformed"]);
43
- });
44
-
45
- it("preserves URL-encoded values containing `=` signs (JWT-shaped tokens)", () => {
46
- const raw =
47
- "VtexIdclientAutCookie=eyJhbGciOiJSUzI1NiIs.eyJzdWIiOiJxIn0.signature; CheckoutOrderFormOwnership=Vk1B%3D%3D";
48
- const { cookies, dropped } = sanitizeOutboundCookieHeader(raw);
49
- expect(dropped).toEqual([]);
50
- expect(cookies).toBe(raw);
51
- });
52
-
53
- it("rejects cookie names with separators (e.g. spaces) as malformed", () => {
54
- const { dropped } = sanitizeOutboundCookieHeader("bad name=v; ok=1");
55
- expect(dropped[0]).toEqual({ name: "bad name", reason: "malformed" });
56
- });
57
-
58
- it("trims surrounding whitespace between pairs", () => {
59
- const { cookies } = sanitizeOutboundCookieHeader(" a=1 ; b=2 ;c=3 ");
60
- expect(cookies).toBe("a=1; b=2; c=3");
61
- });
62
-
63
- it("preserves cookie order across the input", () => {
64
- const { cookies } = sanitizeOutboundCookieHeader("a=1; b=2; c=3; d=4");
65
- expect(cookies).toBe("a=1; b=2; c=3; d=4");
66
- });
67
-
68
- it("with allowlist=true keeps only VTEX-prefixed cookies and reports the rest", () => {
69
- const raw =
70
- "FPID=abc; checkout.vtex.com=__ofid=xyz; _ga=tracking; vtex_segment=eyJ9; CheckoutOrderFormOwnership=token; __cf_bm=cf";
71
- const { cookies, dropped } = sanitizeOutboundCookieHeader(raw, { allowlist: true });
72
- expect(cookies).toBe(
73
- "checkout.vtex.com=__ofid=xyz; vtex_segment=eyJ9; CheckoutOrderFormOwnership=token",
74
- );
75
- expect(dropped.map((d) => d.name).sort()).toEqual(["FPID", "__cf_bm", "_ga"]);
76
- expect(dropped.every((d) => d.reason === "not_in_allowlist")).toBe(true);
77
- });
78
-
79
- it("allowlist mode still drops a VTEX-prefixed cookie that has a non-ASCII value", () => {
80
- const raw = "vtex_segment=okãvalue; checkout.vtex.com=ok";
81
- const { cookies, dropped } = sanitizeOutboundCookieHeader(raw, { allowlist: true });
82
- expect(cookies).toBe("checkout.vtex.com=ok");
83
- expect(dropped).toEqual([{ name: "vtex_segment", reason: "non_ascii" }]);
84
- });
85
-
86
- it("VTEX_COOKIE_PREFIXES covers the cookies VTEX actions actually depend on", () => {
87
- // Sanity check — if anyone removes one of these, every action that
88
- // depends on it will silently 401/redirect on production.
89
- for (const required of [
90
- "VtexIdclientAutCookie",
91
- "checkout.vtex.com",
92
- "CheckoutOrderFormOwnership",
93
- "vtex_session",
94
- "vtex_segment",
95
- ]) {
96
- expect(VTEX_COOKIE_PREFIXES).toContain(required);
97
- }
98
- });
99
- });
100
-
101
- describe("extractVtexCookies (allowlist convenience wrapper)", () => {
102
- it("matches sanitizeOutboundCookieHeader(_, { allowlist: true })", () => {
103
- const raw =
104
- "FPID=abc; checkout.vtex.com=__ofid=xyz; category_click=Eletroportáteis; vtex_segment=ok";
105
- expect(extractVtexCookies(raw)).toBe(
106
- sanitizeOutboundCookieHeader(raw, { allowlist: true }).cookies,
107
- );
108
- });
109
-
110
- it("returns empty string for empty input", () => {
111
- expect(extractVtexCookies("")).toBe("");
112
- });
113
- });
114
-
115
- describe("warnDroppedCookies", () => {
116
- beforeEach(() => {
117
- _resetCookieWarnDedupForTests();
118
- });
119
-
120
- afterEach(() => {
121
- vi.restoreAllMocks();
122
- });
123
-
124
- it("emits one console.warn per dropped cookie with structured fields", () => {
125
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
126
- warnDroppedCookies(
127
- [
128
- { name: "category_click", reason: "non_ascii" },
129
- { name: "_ga", reason: "not_in_allowlist" },
130
- ],
131
- "acct.vtexcommercestable.com.br",
132
- );
133
- expect(warn).toHaveBeenCalledTimes(2);
134
- expect(warn.mock.calls[0]?.[0]).toMatch(
135
- /host=acct\.vtexcommercestable\.com\.br name=category_click reason=non_ascii/,
136
- );
137
- expect(warn.mock.calls[1]?.[0]).toMatch(/name=_ga reason=not_in_allowlist/);
138
- });
139
-
140
- it("dedupes repeated drops of the same name+reason+host within the isolate lifetime", () => {
141
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
142
- const drop = [{ name: "category_click", reason: "non_ascii" as const }];
143
- warnDroppedCookies(drop, "acct.vtexcommercestable.com.br");
144
- warnDroppedCookies(drop, "acct.vtexcommercestable.com.br");
145
- warnDroppedCookies(drop, "acct.vtexcommercestable.com.br");
146
- expect(warn).toHaveBeenCalledTimes(1);
147
- });
148
-
149
- it("does not dedupe across different hosts", () => {
150
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
151
- const drop = [{ name: "category_click", reason: "non_ascii" as const }];
152
- warnDroppedCookies(drop, "acct.vtexcommercestable.com.br");
153
- warnDroppedCookies(drop, "secure.example.com.br");
154
- expect(warn).toHaveBeenCalledTimes(2);
155
- });
156
-
157
- it("is a no-op when nothing was dropped", () => {
158
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
159
- warnDroppedCookies([], "any.host");
160
- expect(warn).not.toHaveBeenCalled();
161
- });
162
- });
@@ -1,80 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { fetchAPI, fetchSafe, HttpError } from "../fetch";
3
-
4
- const realFetch = globalThis.fetch;
5
-
6
- function mockResponse(body: unknown, status = 200): Response {
7
- return {
8
- ok: status >= 200 && status < 300,
9
- status,
10
- statusText: status === 200 ? "OK" : "Error",
11
- url: "https://example.com",
12
- json: () => Promise.resolve(body),
13
- } as Response;
14
- }
15
-
16
- describe("fetchSafe", () => {
17
- beforeEach(() => {
18
- globalThis.fetch = vi.fn() as unknown as typeof fetch;
19
- });
20
-
21
- afterEach(() => {
22
- globalThis.fetch = realFetch;
23
- });
24
-
25
- it("returns the response on 2xx", async () => {
26
- (globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue(mockResponse({ ok: true }));
27
- const res = await fetchSafe("https://example.com/api");
28
- expect(res.status).toBe(200);
29
- });
30
-
31
- it("throws HttpError on non-2xx", async () => {
32
- (globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue(mockResponse({}, 500));
33
- await expect(fetchSafe("https://example.com/api")).rejects.toBeInstanceOf(HttpError);
34
- });
35
-
36
- it("sanitizes utm_* and map params (drops <, > and non-Latin1)", async () => {
37
- const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
38
- fetchMock.mockResolvedValue(mockResponse({}));
39
-
40
- await fetchSafe("https://example.com/api?utm_source=<script>café</script>&keep=ok");
41
-
42
- const callArg = fetchMock.mock.calls[0]?.[0] as string;
43
- expect(callArg).toContain("utm_source=script");
44
- expect(callArg).not.toContain("<");
45
- expect(callArg).not.toContain("café");
46
- expect(callArg).toContain("keep=ok");
47
- });
48
-
49
- it("forwards init options to fetch", async () => {
50
- const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
51
- fetchMock.mockResolvedValue(mockResponse({}));
52
-
53
- await fetchSafe("https://example.com/api", {
54
- method: "POST",
55
- headers: { "x-test": "1" },
56
- });
57
-
58
- const init = fetchMock.mock.calls[0]?.[1] as RequestInit;
59
- expect(init.method).toBe("POST");
60
- expect((init.headers as Record<string, string>)["x-test"]).toBe("1");
61
- });
62
- });
63
-
64
- describe("fetchAPI", () => {
65
- beforeEach(() => {
66
- globalThis.fetch = vi.fn() as unknown as typeof fetch;
67
- });
68
-
69
- afterEach(() => {
70
- globalThis.fetch = realFetch;
71
- });
72
-
73
- it("parses JSON on success", async () => {
74
- (globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue(
75
- mockResponse({ hello: "world" }),
76
- );
77
- const data = await fetchAPI<{ hello: string }>("https://example.com/api");
78
- expect(data).toEqual({ hello: "world" });
79
- });
80
- });