@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
@@ -1,316 +0,0 @@
1
- /**
2
- * VTEX Workflow loaders (internal/back-office use).
3
- * These transform raw VTEX Catalog data into schema.org-compatible Product types.
4
- * NOT intended for storefront rendering — used in data pipelines and workflows.
5
- *
6
- * Pure async functions — require configureVtex() to have been called.
7
- *
8
- * Ported from deco-cx/apps:
9
- * vtex/loaders/workflow/product.ts
10
- * vtex/loaders/workflow/products.ts
11
- *
12
- * @see https://developers.vtex.com/docs/api-reference/catalog-api
13
- */
14
- import type {
15
- Offer,
16
- Product,
17
- PropertyValue,
18
- UnitPriceSpecification,
19
- } from "@decocms/apps-commerce/types";
20
- import { vtexFetch } from "../client";
21
- import {
22
- aggregateOffers,
23
- toAdditionalPropertyCategory,
24
- toAdditionalPropertyCluster,
25
- toAdditionalPropertyReferenceId,
26
- toAdditionalPropertySpecification,
27
- } from "../utils/transform";
28
-
29
- /** VTEX prices come in cents — divide by this to get the currency value. */
30
- const CENTS_DIVISOR = 100;
31
-
32
- // ---------------------------------------------------------------------------
33
- // Types for pvt Catalog APIs
34
- // ---------------------------------------------------------------------------
35
-
36
- interface SkuImage {
37
- ImageUrl: string;
38
- ImageName?: string;
39
- FileId?: string;
40
- }
41
-
42
- interface SkuSpecification {
43
- FieldName: string;
44
- FieldValues: string[];
45
- FieldValueIds: number[];
46
- }
47
-
48
- interface SkuSeller {
49
- SellerId: string;
50
- }
51
-
52
- interface SkuAlternateIds {
53
- RefId?: string;
54
- Ean?: string;
55
- }
56
-
57
- interface PvtSku {
58
- Id: number;
59
- ProductId: number;
60
- IsActive: boolean;
61
- SkuName: string;
62
- ProductName: string;
63
- ProductDescription: string;
64
- DetailUrl: string;
65
- BrandId: string;
66
- BrandName: string;
67
- ReleaseDate?: string;
68
- Images: SkuImage[];
69
- SkuSpecifications: SkuSpecification[];
70
- ProductSpecifications: SkuSpecification[];
71
- ProductCategories: Record<string, string>;
72
- ProductClusterNames: Record<string, string>;
73
- SalesChannels: number[];
74
- AlternateIds: SkuAlternateIds;
75
- SkuSellers: SkuSeller[];
76
- }
77
-
78
- interface PvtSkuListItem {
79
- Id: number;
80
- IsActive: boolean;
81
- }
82
-
83
- interface SalesChannel {
84
- Id: number;
85
- CurrencyCode: string;
86
- }
87
-
88
- interface SimulationItem {
89
- sellingPrice: number;
90
- listPrice: number;
91
- price: number;
92
- seller: string;
93
- priceValidUntil: string;
94
- availability: string;
95
- }
96
-
97
- interface SimulationPaymentOption {
98
- paymentName: string;
99
- installments: Array<{ count: number; value: number; total: number }>;
100
- }
101
-
102
- interface SimulationResponse {
103
- items?: SimulationItem[];
104
- paymentData?: { installmentOptions?: SimulationPaymentOption[] };
105
- }
106
-
107
- // ---------------------------------------------------------------------------
108
- // workflowProduct
109
- // ---------------------------------------------------------------------------
110
-
111
- export interface WorkflowProductOptions {
112
- /** The SKU ID (stockKeepingUnitId) to load */
113
- productID: string;
114
- /** Sales channel for simulation. Defaults to 1. */
115
- salesChannel?: number;
116
- }
117
-
118
- /**
119
- * Transform a single VTEX SKU (via private Catalog API) into a commerce Product.
120
- *
121
- * Fetches the SKU details, all sibling SKUs for the same product, sales channels,
122
- * and runs checkout simulation for each seller to build offer data.
123
- *
124
- * Ported from: vtex/loaders/workflow/product.ts
125
- */
126
- export async function workflowProduct(opts: WorkflowProductOptions): Promise<Product | null> {
127
- const sc = opts.salesChannel ?? 1;
128
-
129
- const sku = await vtexFetch<PvtSku>(
130
- `/api/catalog_system/pvt/sku/stockkeepingunitbyid/${opts.productID}`,
131
- );
132
-
133
- if (!sku.IsActive) return null;
134
-
135
- const [skus, salesChannels, ...simulations] = await Promise.all([
136
- vtexFetch<PvtSkuListItem[]>(
137
- `/api/catalog_system/pvt/sku/stockkeepingunitByProductId/${sku.ProductId}`,
138
- ),
139
- vtexFetch<SalesChannel[]>("/api/catalog_system/pvt/saleschannel/list"),
140
- ...sku.SkuSellers.map(({ SellerId }) =>
141
- vtexFetch<SimulationResponse>(
142
- `/api/checkout/pub/orderForms/simulation?RnbBehavior=1&sc=${sc}`,
143
- {
144
- method: "POST",
145
- body: JSON.stringify({
146
- items: [{ id: `${sku.Id}`, seller: SellerId, quantity: 1 }],
147
- }),
148
- },
149
- ),
150
- ),
151
- ]);
152
-
153
- const channel = salesChannels.find((c) => c.Id === sc);
154
- const productGroupID = `${sku.ProductId}`;
155
- const productID = `${sku.Id}`;
156
-
157
- const additionalProperty = [
158
- sku.AlternateIds.RefId
159
- ? toAdditionalPropertyReferenceId({
160
- name: "RefId",
161
- value: sku.AlternateIds.RefId,
162
- })
163
- : null,
164
- ...Object.entries(sku.ProductCategories ?? {}).map(([propertyID, value]) =>
165
- toAdditionalPropertyCategory({ propertyID, value }),
166
- ),
167
- ...Object.entries(sku.ProductClusterNames ?? {}).map(([propertyID, value]) =>
168
- toAdditionalPropertyCluster({ propertyID, value }),
169
- ),
170
- ...sku.SkuSpecifications.flatMap((spec) =>
171
- spec.FieldValues.map((value, it) =>
172
- toAdditionalPropertySpecification({
173
- propertyID: spec.FieldValueIds[it]?.toString(),
174
- name: spec.FieldName,
175
- value,
176
- }),
177
- ),
178
- ),
179
- ...sku.SalesChannels.map(
180
- (ch): PropertyValue => ({
181
- "@type": "PropertyValue",
182
- name: "salesChannel",
183
- propertyID: ch.toString(),
184
- }),
185
- ),
186
- ].filter((p): p is PropertyValue => Boolean(p));
187
-
188
- const groupAdditionalProperty = sku.ProductSpecifications.flatMap((spec) =>
189
- spec.FieldValues.map((value, it) =>
190
- toAdditionalPropertySpecification({
191
- propertyID: spec.FieldValueIds[it]?.toString(),
192
- name: spec.FieldName,
193
- value,
194
- }),
195
- ),
196
- );
197
-
198
- const offers = simulations
199
- .flatMap(({ items, paymentData }) =>
200
- items?.map((item): Offer | null => {
201
- const { sellingPrice, listPrice, price, seller, priceValidUntil, availability } = item;
202
- const spotPrice = sellingPrice || price;
203
- if (!spotPrice || !listPrice) return null;
204
-
205
- return {
206
- "@type": "Offer",
207
- price: spotPrice / CENTS_DIVISOR,
208
- seller,
209
- priceValidUntil,
210
- inventoryLevel: {},
211
- availability:
212
- availability === "available"
213
- ? "https://schema.org/InStock"
214
- : "https://schema.org/OutOfStock",
215
- priceSpecification: [
216
- {
217
- "@type": "UnitPriceSpecification",
218
- priceType: "https://schema.org/ListPrice",
219
- price: listPrice / CENTS_DIVISOR,
220
- },
221
- {
222
- "@type": "UnitPriceSpecification",
223
- priceType: "https://schema.org/SalePrice",
224
- price: spotPrice / CENTS_DIVISOR,
225
- },
226
- ...(paymentData?.installmentOptions?.flatMap((option): UnitPriceSpecification[] =>
227
- option.installments.map((i) => ({
228
- "@type": "UnitPriceSpecification",
229
- priceType: "https://schema.org/SalePrice",
230
- priceComponentType: "https://schema.org/Installment",
231
- name: option.paymentName,
232
- billingDuration: i.count,
233
- billingIncrement: i.value / CENTS_DIVISOR,
234
- price: i.total / CENTS_DIVISOR,
235
- })),
236
- ) ?? []),
237
- ],
238
- };
239
- }),
240
- )
241
- .filter((o): o is Offer => Boolean(o));
242
-
243
- return {
244
- "@type": "Product",
245
- productID,
246
- sku: productID,
247
- inProductGroupWithID: productGroupID,
248
- category: Object.values(sku.ProductCategories ?? {}).join(" > "),
249
- url: `${sku.DetailUrl}?skuId=${productID}`,
250
- name: sku.SkuName,
251
- gtin: sku.AlternateIds.Ean,
252
- image: sku.Images.map((img) => ({
253
- "@type": "ImageObject",
254
- encodingFormat: "image",
255
- alternateName: img.ImageName ?? img.FileId,
256
- url: img.ImageUrl,
257
- })),
258
- isVariantOf: {
259
- "@type": "ProductGroup",
260
- url: sku.DetailUrl,
261
- hasVariant:
262
- skus
263
- ?.filter((x) => x.IsActive)
264
- .map(({ Id }) => ({
265
- "@type": "Product",
266
- productID: `${Id}`,
267
- sku: `${Id}`,
268
- })) ?? [],
269
- additionalProperty: groupAdditionalProperty,
270
- productGroupID,
271
- name: sku.ProductName,
272
- description: sku.ProductDescription,
273
- },
274
- additionalProperty,
275
- releaseDate: sku.ReleaseDate ? new Date(sku.ReleaseDate).toISOString() : undefined,
276
- brand: {
277
- "@type": "Brand",
278
- "@id": sku.BrandId,
279
- name: sku.BrandName,
280
- },
281
- offers: aggregateOffers(offers, channel?.CurrencyCode),
282
- };
283
- }
284
-
285
- // ---------------------------------------------------------------------------
286
- // workflowProducts
287
- // ---------------------------------------------------------------------------
288
-
289
- export interface WorkflowProductsOptions {
290
- page: number;
291
- pagesize: number;
292
- }
293
-
294
- /**
295
- * Fetch a page of SKU IDs and return minimal Product stubs.
296
- * Use in batch workflows to enumerate the catalog; call workflowProduct
297
- * for each ID if full details are needed.
298
- *
299
- * Ported from: vtex/loaders/workflow/products.ts
300
- */
301
- export async function workflowProducts(opts: WorkflowProductsOptions): Promise<Product[]> {
302
- const params = new URLSearchParams({
303
- page: String(opts.page),
304
- pagesize: String(opts.pagesize),
305
- });
306
-
307
- const ids = await vtexFetch<number[]>(
308
- `/api/catalog_system/pvt/sku/stockkeepingunitids?${params}`,
309
- );
310
-
311
- return ids.map((productID) => ({
312
- "@type": "Product",
313
- productID: `${productID}`,
314
- sku: `${productID}`,
315
- }));
316
- }
package/src/logo.png DELETED
Binary file
@@ -1,89 +0,0 @@
1
- // AUTO-GENERATED by scripts/generate-manifests.ts — DO NOT EDIT
2
- // This file is checked into source control and updated via: npm run generate:manifests
3
-
4
- import * as actions_address from "./actions/address";
5
- import * as actions_analytics_sendEvent from "./actions/analytics/sendEvent";
6
- import * as actions_auth from "./actions/auth";
7
- import * as actions_checkout from "./actions/checkout";
8
- import * as actions_masterData from "./actions/masterData";
9
- import * as actions_misc from "./actions/misc";
10
- import * as actions_newsletter from "./actions/newsletter";
11
- import * as actions_orders from "./actions/orders";
12
- import * as actions_profile from "./actions/profile";
13
- import * as actions_session from "./actions/session";
14
- import * as actions_trigger from "./actions/trigger";
15
- import * as actions_wishlist from "./actions/wishlist";
16
- import * as loaders_address from "./loaders/address";
17
- import * as loaders_autocomplete from "./loaders/autocomplete";
18
- import * as loaders_brands from "./loaders/brands";
19
- import * as loaders_cart from "./loaders/cart";
20
- import * as loaders_cart_attachments from "./loaders/cart/attachments";
21
- import * as loaders_cart_full from "./loaders/cart/full";
22
- import * as loaders_cart_gifts from "./loaders/cart/gifts";
23
- import * as loaders_cart_shipping from "./loaders/cart/shipping";
24
- import * as loaders_cart_summary from "./loaders/cart/summary";
25
- import * as loaders_catalog from "./loaders/catalog";
26
- import * as loaders_collections from "./loaders/collections";
27
- import * as loaders_legacy from "./loaders/legacy";
28
- import * as loaders_logistics from "./loaders/logistics";
29
- import * as loaders_navbar from "./loaders/navbar";
30
- import * as loaders_orders from "./loaders/orders";
31
- import * as loaders_pageType from "./loaders/pageType";
32
- import * as loaders_payment from "./loaders/payment";
33
- import * as loaders_profile from "./loaders/profile";
34
- import * as loaders_promotion from "./loaders/promotion";
35
- import * as loaders_search from "./loaders/search";
36
- import * as loaders_session from "./loaders/session";
37
- import * as loaders_user from "./loaders/user";
38
- import * as loaders_wishlist from "./loaders/wishlist";
39
- import * as loaders_wishlistProducts from "./loaders/wishlistProducts";
40
- import * as loaders_workflow from "./loaders/workflow";
41
-
42
- const manifest = {
43
- name: "vtex",
44
- loaders: {
45
- "vtex/loaders/address": loaders_address,
46
- "vtex/loaders/autocomplete": loaders_autocomplete,
47
- "vtex/loaders/brands": loaders_brands,
48
- "vtex/loaders/cart": loaders_cart,
49
- "vtex/loaders/cart/summary": loaders_cart_summary,
50
- "vtex/loaders/cart/full": loaders_cart_full,
51
- "vtex/loaders/cart/shipping": loaders_cart_shipping,
52
- "vtex/loaders/cart/gifts": loaders_cart_gifts,
53
- "vtex/loaders/cart/attachments": loaders_cart_attachments,
54
- "vtex/loaders/catalog": loaders_catalog,
55
- "vtex/loaders/collections": loaders_collections,
56
- "vtex/loaders/legacy": loaders_legacy,
57
- "vtex/loaders/logistics": loaders_logistics,
58
- "vtex/loaders/navbar": loaders_navbar,
59
- "vtex/loaders/orders": loaders_orders,
60
- "vtex/loaders/pageType": loaders_pageType,
61
- "vtex/loaders/payment": loaders_payment,
62
- "vtex/loaders/profile": loaders_profile,
63
- "vtex/loaders/promotion": loaders_promotion,
64
- "vtex/loaders/search": loaders_search,
65
- "vtex/loaders/session": loaders_session,
66
- "vtex/loaders/user": loaders_user,
67
- "vtex/loaders/wishlist": loaders_wishlist,
68
- "vtex/loaders/wishlistProducts": loaders_wishlistProducts,
69
- "vtex/loaders/workflow": loaders_workflow,
70
- },
71
- actions: {
72
- "vtex/actions/address": actions_address,
73
- "vtex/actions/analytics/sendEvent": actions_analytics_sendEvent,
74
- "vtex/actions/auth": actions_auth,
75
- "vtex/actions/checkout": actions_checkout,
76
- "vtex/actions/masterData": actions_masterData,
77
- "vtex/actions/misc": actions_misc,
78
- "vtex/actions/newsletter": actions_newsletter,
79
- "vtex/actions/orders": actions_orders,
80
- "vtex/actions/profile": actions_profile,
81
- "vtex/actions/session": actions_session,
82
- "vtex/actions/trigger": actions_trigger,
83
- "vtex/actions/wishlist": actions_wishlist,
84
- },
85
- sections: {},
86
- } as const;
87
-
88
- export type Manifest = typeof manifest;
89
- export default manifest;
@@ -1,44 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { vtexMiddleware } from "./mod";
3
-
4
- /**
5
- * The VTEX app middleware wraps the framework's entire edge-cache layer, so it
6
- * is the last writer of Cache-Control on every response — including cache HITs.
7
- * These tests pin the two things that made that dangerous.
8
- */
9
-
10
- // A response as the framework's cache layer would hand it over: public headers
11
- // resolved from the page's cache profile, plus the CDN header.
12
- function cachedResponse(): Response {
13
- return new Response("page", {
14
- headers: {
15
- "Cache-Control": "public, max-age=120, s-maxage=900, stale-while-revalidate=1800",
16
- "CDN-Cache-Control": "public, max-age=900",
17
- },
18
- });
19
- }
20
-
21
- const next = async () => cachedResponse();
22
-
23
- describe("vtexMiddleware cache headers", () => {
24
- it("leaves the cache layer's headers alone for an anonymous request", async () => {
25
- const res = await vtexMiddleware(new Request("https://store.com/"), next);
26
- // Used to be downgraded to vtexCacheControl's generic `s-maxage=60`,
27
- // throwing away the profile the cache layer had resolved.
28
- expect(res.headers.get("Cache-Control")).toContain("s-maxage=900");
29
- });
30
-
31
- it("forces private headers and clears the CDN header when logged in", async () => {
32
- const req = new Request("https://store.com/", {
33
- // extractVtexContext treats any VtexIdclientAutCookie* as authenticated.
34
- headers: { cookie: "VtexIdclientAutCookie_store=abc123" },
35
- });
36
- const res = await vtexMiddleware(req, next);
37
-
38
- expect(res.headers.get("Cache-Control")).toContain("no-store");
39
- // Cloudflare gives CDN-Cache-Control precedence, so leaving the public
40
- // value behind would cache a personalized page at the CDN regardless of
41
- // the private Cache-Control next to it.
42
- expect(res.headers.get("CDN-Cache-Control")).toBeNull();
43
- });
44
- });
package/src/middleware.ts DELETED
@@ -1,240 +0,0 @@
1
- /**
2
- * VTEX Middleware utilities for TanStack Start.
3
- *
4
- * Extracts segment information from cookies/URL params, detects login state,
5
- * propagates Intelligent Search cookies, and provides cache-control decisions.
6
- *
7
- * Use with TanStack Start's createMiddleware() in the storefront:
8
- *
9
- * @example
10
- * ```ts
11
- * import { createMiddleware } from "@tanstack/react-start";
12
- * import {
13
- * extractVtexContext,
14
- * vtexCacheControl,
15
- * } from "@decocms/apps/vtex/middleware";
16
- *
17
- * const vtexMiddleware = createMiddleware().server(async ({ next, request }) => {
18
- * const vtexCtx = extractVtexContext(request);
19
- * const response = await next();
20
- * response.headers.set("Cache-Control", vtexCacheControl(vtexCtx));
21
- * propagateISCookies(request, response);
22
- * return response;
23
- * });
24
- * ```
25
- */
26
-
27
- import { ANONYMOUS_COOKIE, SESSION_COOKIE } from "./utils/intelligentSearch";
28
- import {
29
- buildSegmentFromParams,
30
- DEFAULT_SEGMENT,
31
- parseSegment,
32
- SALES_CHANNEL_COOKIE,
33
- SEGMENT_COOKIE_NAME,
34
- serializeSegment,
35
- } from "./utils/segment";
36
- import type { Segment } from "./utils/types";
37
- import { extractVtexAuthCookie, parseVtexAuthToken } from "./utils/vtexId";
38
-
39
- // -------------------------------------------------------------------------
40
- // Types
41
- // -------------------------------------------------------------------------
42
-
43
- export interface VtexRequestContext {
44
- /** Decoded segment from cookie or URL params. */
45
- segment: Partial<Segment>;
46
- /** Serialized segment token for cache key use. */
47
- segmentToken: string;
48
- /** Whether the user has a valid (non-expired) VTEX auth cookie. */
49
- isLoggedIn: boolean;
50
- /** Extracted email from the auth JWT, if available. */
51
- email?: string;
52
- /** Sales channel derived from segment. */
53
- salesChannel: string;
54
- /**
55
- * VTEX region ID from the segment cookie.
56
- * Present when the user has set a postal code (CEP) for regionalization.
57
- * Null when no region is set (anonymous default segment).
58
- */
59
- regionId: string | null;
60
- /** Whether this request carries price tables (B2B). */
61
- hasCustomPricing: boolean;
62
- /** Intelligent Search session cookie. */
63
- isSessionId: string;
64
- /** Intelligent Search anonymous cookie. */
65
- isAnonymousId: string;
66
- /** Whether IS cookies were freshly generated (browser didn't send them). */
67
- needsISCookies: boolean;
68
- }
69
-
70
- // -------------------------------------------------------------------------
71
- // Cookie helpers
72
- // -------------------------------------------------------------------------
73
-
74
- const _IS_COOKIE_PREFIX = "vtex_is_";
75
-
76
- /** Seconds in one day (86 400). Used for cookie Max-Age and stale-if-error. */
77
- const ONE_DAY_SECONDS = 86_400;
78
-
79
- /** Seconds in one year (~365 days). Used for long-lived IS cookie Max-Age. */
80
- const ONE_YEAR_SECONDS = 365 * 24 * 60 * 60;
81
-
82
- function getCookieValue(cookieHeader: string, name: string): string | null {
83
- const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`));
84
- return match?.[1] ?? null;
85
- }
86
-
87
- // -------------------------------------------------------------------------
88
- // Core extraction
89
- // -------------------------------------------------------------------------
90
-
91
- /**
92
- * Extract VTEX context from an incoming request.
93
- *
94
- * Reads the segment cookie, URL params (utm_*, sc), and auth cookie
95
- * to build a complete picture of the user's VTEX session state.
96
- */
97
- function generateUUID(): string {
98
- if (typeof crypto !== "undefined" && crypto.randomUUID) {
99
- return crypto.randomUUID();
100
- }
101
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
102
- const r = (Math.random() * 16) | 0;
103
- return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
104
- });
105
- }
106
-
107
- export function extractVtexContext(request: Request): VtexRequestContext {
108
- const cookies = request.headers.get("cookie") ?? "";
109
- const url = new URL(request.url);
110
-
111
- const segmentCookie = getCookieValue(cookies, SEGMENT_COOKIE_NAME);
112
- const cookieSegment = segmentCookie ? parseSegment(segmentCookie) : null;
113
-
114
- const paramSegment = buildSegmentFromParams(url.searchParams);
115
-
116
- const vtexsc = getCookieValue(cookies, SALES_CHANNEL_COOKIE);
117
-
118
- const segment: Partial<Segment> = {
119
- ...DEFAULT_SEGMENT,
120
- ...cookieSegment,
121
- ...paramSegment,
122
- };
123
- if (vtexsc) segment.channel = vtexsc;
124
-
125
- const segmentToken = serializeSegment(segment);
126
-
127
- const authToken = extractVtexAuthCookie(cookies);
128
- const authInfo = authToken ? parseVtexAuthToken(authToken) : null;
129
-
130
- const existingSessionId = getCookieValue(cookies, SESSION_COOKIE);
131
- const existingAnonymousId = getCookieValue(cookies, ANONYMOUS_COOKIE);
132
- const needsISCookies = !existingSessionId || !existingAnonymousId;
133
-
134
- return {
135
- segment,
136
- segmentToken,
137
- isLoggedIn: authInfo?.isLoggedIn ?? false,
138
- email: authInfo?.email,
139
- salesChannel: segment.channel ?? "1",
140
- regionId: segment.regionId ?? null,
141
- hasCustomPricing: Boolean(segment.priceTables && segment.priceTables.length > 0),
142
- isSessionId: existingSessionId ?? generateUUID(),
143
- isAnonymousId: existingAnonymousId ?? generateUUID(),
144
- needsISCookies,
145
- };
146
- }
147
-
148
- // -------------------------------------------------------------------------
149
- // Cache control
150
- // -------------------------------------------------------------------------
151
-
152
- /**
153
- * Determine the appropriate Cache-Control header based on VTEX context.
154
- *
155
- * Rules:
156
- * - Logged-in users: private (personalized prices, wishlists, etc.)
157
- * - Custom pricing (B2B): private (price table specific)
158
- * - Anonymous default segment: public with CDN caching
159
- */
160
- export function vtexCacheControl(
161
- ctx: VtexRequestContext,
162
- options?: {
163
- /** Max age for public (anonymous) responses in seconds. @default 60 */
164
- publicMaxAge?: number;
165
- /** Stale-while-revalidate for public responses in seconds. @default 3600 */
166
- publicSWR?: number;
167
- },
168
- ): string {
169
- if (ctx.isLoggedIn || ctx.hasCustomPricing) {
170
- return "private, no-cache, no-store, must-revalidate";
171
- }
172
-
173
- const maxAge = options?.publicMaxAge ?? 60;
174
- const swr = options?.publicSWR ?? 3600;
175
-
176
- return `public, s-maxage=${maxAge}, stale-while-revalidate=${swr}, stale-if-error=${ONE_DAY_SECONDS}`;
177
- }
178
-
179
- // -------------------------------------------------------------------------
180
- // Cookie propagation
181
- // -------------------------------------------------------------------------
182
-
183
- /**
184
- * Set Intelligent Search cookies on the response only when the browser
185
- * doesn't already have them. On subsequent requests where the cookies
186
- * exist, this is a no-op — keeping the response free of Set-Cookie
187
- * headers so it remains cacheable at the CDN edge.
188
- */
189
- export function propagateISCookies(ctx: VtexRequestContext, response: Response): void {
190
- if (!ctx.needsISCookies) return;
191
-
192
- const maxAge = ONE_YEAR_SECONDS;
193
- response.headers.append(
194
- "Set-Cookie",
195
- `${SESSION_COOKIE}=${ctx.isSessionId}; Path=/; SameSite=Lax; Max-Age=${maxAge}`,
196
- );
197
- response.headers.append(
198
- "Set-Cookie",
199
- `${ANONYMOUS_COOKIE}=${ctx.isAnonymousId}; Path=/; SameSite=Lax; Max-Age=${maxAge}`,
200
- );
201
- }
202
-
203
- /**
204
- * Build a segment cookie Set-Cookie header for the response.
205
- *
206
- * Use this when URL params change the segment (e.g., ?sc=2) so the
207
- * browser persists the new segment for subsequent requests.
208
- */
209
- export function buildSegmentSetCookie(segment: Partial<Segment>, domain?: string): string {
210
- const token = serializeSegment(segment);
211
- let cookie = `${SEGMENT_COOKIE_NAME}=${token}; Path=/; SameSite=Lax; Max-Age=${ONE_DAY_SECONDS}`;
212
- if (domain) cookie += `; Domain=${domain}`;
213
- return cookie;
214
- }
215
-
216
- // -------------------------------------------------------------------------
217
- // Cache key helpers
218
- // -------------------------------------------------------------------------
219
-
220
- /**
221
- * Build a cache key suffix from the VTEX context.
222
- *
223
- * This is used in the Cloudflare Worker entry to differentiate cached
224
- * responses by segment. Two anonymous users on the same sales channel
225
- * get the same cache key; a logged-in user gets a unique (uncached) key.
226
- */
227
- export function vtexCacheKeySuffix(ctx: VtexRequestContext): string {
228
- if (ctx.isLoggedIn) return "__vtex_auth";
229
- const parts = [`sc=${ctx.salesChannel}`];
230
- if (ctx.regionId) parts.push(`r=${ctx.regionId}`);
231
- return `__vtex_${parts.join("_")}`;
232
- }
233
-
234
- // -------------------------------------------------------------------------
235
- // Re-exports for convenience
236
- // -------------------------------------------------------------------------
237
-
238
- export type { Segment } from "./utils/types";
239
- export type { VtexAuthInfo } from "./utils/vtexId";
240
- export { isVtexLoggedIn } from "./utils/vtexId";