@decocms/apps 0.20.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 (113) hide show
  1. package/.github/workflows/release.yml +34 -0
  2. package/.releaserc.json +25 -0
  3. package/commerce/components/Image.tsx +209 -0
  4. package/commerce/components/JsonLd.tsx +285 -0
  5. package/commerce/sdk/analytics.ts +24 -0
  6. package/commerce/sdk/formatPrice.ts +23 -0
  7. package/commerce/sdk/url.ts +9 -0
  8. package/commerce/sdk/useOffer.ts +75 -0
  9. package/commerce/sdk/useVariantPossibilities.ts +43 -0
  10. package/commerce/types/commerce.ts +1105 -0
  11. package/commerce/utils/canonical.ts +11 -0
  12. package/commerce/utils/constants.ts +9 -0
  13. package/commerce/utils/filters.ts +10 -0
  14. package/commerce/utils/productToAnalyticsItem.ts +67 -0
  15. package/commerce/utils/stateByZip.ts +50 -0
  16. package/knip.json +19 -0
  17. package/package.json +77 -0
  18. package/shopify/actions/cart/addItems.ts +37 -0
  19. package/shopify/actions/cart/updateCoupons.ts +32 -0
  20. package/shopify/actions/cart/updateItems.ts +32 -0
  21. package/shopify/actions/user/signIn.ts +45 -0
  22. package/shopify/actions/user/signUp.ts +36 -0
  23. package/shopify/client.ts +58 -0
  24. package/shopify/index.ts +32 -0
  25. package/shopify/init.ts +40 -0
  26. package/shopify/loaders/ProductDetailsPage.ts +35 -0
  27. package/shopify/loaders/ProductList.ts +101 -0
  28. package/shopify/loaders/ProductListingPage.ts +180 -0
  29. package/shopify/loaders/RelatedProducts.ts +45 -0
  30. package/shopify/loaders/cart.ts +73 -0
  31. package/shopify/loaders/shop.ts +40 -0
  32. package/shopify/loaders/user.ts +44 -0
  33. package/shopify/utils/admin/admin.ts +57 -0
  34. package/shopify/utils/admin/queries.ts +29 -0
  35. package/shopify/utils/cart.ts +28 -0
  36. package/shopify/utils/cookies.ts +85 -0
  37. package/shopify/utils/enums.ts +438 -0
  38. package/shopify/utils/graphql.ts +69 -0
  39. package/shopify/utils/storefront/queries.ts +530 -0
  40. package/shopify/utils/storefront/storefront.graphql.gen.ts +113 -0
  41. package/shopify/utils/transform.ts +436 -0
  42. package/shopify/utils/types.ts +191 -0
  43. package/shopify/utils/user.ts +23 -0
  44. package/shopify/utils/utils.ts +164 -0
  45. package/tsconfig.json +11 -0
  46. package/vtex/README.md +6 -0
  47. package/vtex/actions/address.ts +211 -0
  48. package/vtex/actions/auth.ts +337 -0
  49. package/vtex/actions/checkout.ts +497 -0
  50. package/vtex/actions/index.ts +11 -0
  51. package/vtex/actions/masterData.ts +170 -0
  52. package/vtex/actions/misc.ts +196 -0
  53. package/vtex/actions/newsletter.ts +108 -0
  54. package/vtex/actions/orders.ts +37 -0
  55. package/vtex/actions/profile.ts +119 -0
  56. package/vtex/actions/session.ts +87 -0
  57. package/vtex/actions/trigger.ts +43 -0
  58. package/vtex/actions/wishlist.ts +116 -0
  59. package/vtex/client.ts +423 -0
  60. package/vtex/hooks/index.ts +4 -0
  61. package/vtex/hooks/useAutocomplete.ts +89 -0
  62. package/vtex/hooks/useCart.ts +219 -0
  63. package/vtex/hooks/useUser.ts +78 -0
  64. package/vtex/hooks/useWishlist.ts +119 -0
  65. package/vtex/index.ts +14 -0
  66. package/vtex/inline-loaders/productDetailsPage.ts +75 -0
  67. package/vtex/inline-loaders/productList.ts +163 -0
  68. package/vtex/inline-loaders/productListingPage.ts +447 -0
  69. package/vtex/inline-loaders/relatedProducts.ts +83 -0
  70. package/vtex/inline-loaders/suggestions.ts +49 -0
  71. package/vtex/inline-loaders/workflowProducts.ts +68 -0
  72. package/vtex/invoke.ts +202 -0
  73. package/vtex/loaders/address.ts +120 -0
  74. package/vtex/loaders/brands.ts +51 -0
  75. package/vtex/loaders/cart.ts +49 -0
  76. package/vtex/loaders/catalog.ts +165 -0
  77. package/vtex/loaders/collections.ts +57 -0
  78. package/vtex/loaders/index.ts +19 -0
  79. package/vtex/loaders/legacy.ts +671 -0
  80. package/vtex/loaders/logistics.ts +115 -0
  81. package/vtex/loaders/navbar.ts +29 -0
  82. package/vtex/loaders/orders.ts +103 -0
  83. package/vtex/loaders/pageType.ts +62 -0
  84. package/vtex/loaders/payment.ts +107 -0
  85. package/vtex/loaders/profile.ts +138 -0
  86. package/vtex/loaders/promotion.ts +33 -0
  87. package/vtex/loaders/search.ts +127 -0
  88. package/vtex/loaders/session.ts +91 -0
  89. package/vtex/loaders/user.ts +89 -0
  90. package/vtex/loaders/wishlist.ts +89 -0
  91. package/vtex/loaders/wishlistProducts.ts +81 -0
  92. package/vtex/loaders/workflow.ts +323 -0
  93. package/vtex/logo.png +0 -0
  94. package/vtex/middleware.ts +229 -0
  95. package/vtex/types.ts +248 -0
  96. package/vtex/utils/batch.ts +21 -0
  97. package/vtex/utils/cookies.ts +76 -0
  98. package/vtex/utils/enrichment.ts +540 -0
  99. package/vtex/utils/fetchCache.ts +150 -0
  100. package/vtex/utils/index.ts +17 -0
  101. package/vtex/utils/intelligentSearch.ts +84 -0
  102. package/vtex/utils/legacy.ts +155 -0
  103. package/vtex/utils/pickAndOmit.ts +30 -0
  104. package/vtex/utils/proxy.ts +196 -0
  105. package/vtex/utils/resourceRange.ts +10 -0
  106. package/vtex/utils/segment.ts +163 -0
  107. package/vtex/utils/similars.ts +38 -0
  108. package/vtex/utils/sitemap.ts +133 -0
  109. package/vtex/utils/slugCache.ts +32 -0
  110. package/vtex/utils/slugify.ts +13 -0
  111. package/vtex/utils/transform.ts +1331 -0
  112. package/vtex/utils/types.ts +1884 -0
  113. package/vtex/utils/vtexId.ts +103 -0
@@ -0,0 +1,323 @@
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 "../../commerce/types/commerce";
20
+ import { vtexFetch } from "../client";
21
+ import {
22
+ aggregateOffers,
23
+ toAdditionalPropertyCategory,
24
+ toAdditionalPropertyCluster,
25
+ toAdditionalPropertyReferenceId,
26
+ toAdditionalPropertySpecification,
27
+ } from "../utils/transform";
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Types for pvt Catalog APIs
31
+ // ---------------------------------------------------------------------------
32
+
33
+ interface SkuImage {
34
+ ImageUrl: string;
35
+ ImageName?: string;
36
+ FileId?: string;
37
+ }
38
+
39
+ interface SkuSpecification {
40
+ FieldName: string;
41
+ FieldValues: string[];
42
+ FieldValueIds: number[];
43
+ }
44
+
45
+ interface SkuSeller {
46
+ SellerId: string;
47
+ }
48
+
49
+ interface SkuAlternateIds {
50
+ RefId?: string;
51
+ Ean?: string;
52
+ }
53
+
54
+ interface PvtSku {
55
+ Id: number;
56
+ ProductId: number;
57
+ IsActive: boolean;
58
+ SkuName: string;
59
+ ProductName: string;
60
+ ProductDescription: string;
61
+ DetailUrl: string;
62
+ BrandId: string;
63
+ BrandName: string;
64
+ ReleaseDate?: string;
65
+ Images: SkuImage[];
66
+ SkuSpecifications: SkuSpecification[];
67
+ ProductSpecifications: SkuSpecification[];
68
+ ProductCategories: Record<string, string>;
69
+ ProductClusterNames: Record<string, string>;
70
+ SalesChannels: number[];
71
+ AlternateIds: SkuAlternateIds;
72
+ SkuSellers: SkuSeller[];
73
+ }
74
+
75
+ interface PvtSkuListItem {
76
+ Id: number;
77
+ IsActive: boolean;
78
+ }
79
+
80
+ interface SalesChannel {
81
+ Id: number;
82
+ CurrencyCode: string;
83
+ }
84
+
85
+ interface SimulationItem {
86
+ sellingPrice: number;
87
+ listPrice: number;
88
+ price: number;
89
+ seller: string;
90
+ priceValidUntil: string;
91
+ availability: string;
92
+ }
93
+
94
+ interface SimulationPaymentOption {
95
+ paymentName: string;
96
+ installments: Array<{ count: number; value: number; total: number }>;
97
+ }
98
+
99
+ interface SimulationResponse {
100
+ items?: SimulationItem[];
101
+ paymentData?: { installmentOptions?: SimulationPaymentOption[] };
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // workflowProduct
106
+ // ---------------------------------------------------------------------------
107
+
108
+ export interface WorkflowProductOptions {
109
+ /** The SKU ID (stockKeepingUnitId) to load */
110
+ productID: string;
111
+ /** Sales channel for simulation. Defaults to 1. */
112
+ salesChannel?: number;
113
+ }
114
+
115
+ /**
116
+ * Transform a single VTEX SKU (via private Catalog API) into a commerce Product.
117
+ *
118
+ * Fetches the SKU details, all sibling SKUs for the same product, sales channels,
119
+ * and runs checkout simulation for each seller to build offer data.
120
+ *
121
+ * Ported from: vtex/loaders/workflow/product.ts
122
+ */
123
+ export async function workflowProduct(
124
+ opts: WorkflowProductOptions,
125
+ ): Promise<Product | null> {
126
+ const sc = opts.salesChannel ?? 1;
127
+
128
+ const sku = await vtexFetch<PvtSku>(
129
+ `/api/catalog_system/pvt/sku/stockkeepingunitbyid/${opts.productID}`,
130
+ );
131
+
132
+ if (!sku.IsActive) return null;
133
+
134
+ const [skus, salesChannels, ...simulations] = await Promise.all([
135
+ vtexFetch<PvtSkuListItem[]>(
136
+ `/api/catalog_system/pvt/sku/stockkeepingunitByProductId/${sku.ProductId}`,
137
+ ),
138
+ vtexFetch<SalesChannel[]>(
139
+ "/api/catalog_system/pvt/saleschannel/list",
140
+ ),
141
+ ...sku.SkuSellers.map(({ SellerId }) =>
142
+ vtexFetch<SimulationResponse>(
143
+ `/api/checkout/pub/orderForms/simulation?RnbBehavior=1&sc=${sc}`,
144
+ {
145
+ method: "POST",
146
+ body: JSON.stringify({
147
+ items: [{ id: `${sku.Id}`, seller: SellerId, quantity: 1 }],
148
+ }),
149
+ },
150
+ ),
151
+ ),
152
+ ]);
153
+
154
+ const channel = salesChannels.find((c) => c.Id === sc);
155
+ const productGroupID = `${sku.ProductId}`;
156
+ const productID = `${sku.Id}`;
157
+
158
+ const additionalProperty = [
159
+ sku.AlternateIds.RefId
160
+ ? toAdditionalPropertyReferenceId({
161
+ name: "RefId",
162
+ value: sku.AlternateIds.RefId,
163
+ })
164
+ : null,
165
+ ...Object.entries(sku.ProductCategories ?? {}).map(([propertyID, value]) =>
166
+ toAdditionalPropertyCategory({ propertyID, value }),
167
+ ),
168
+ ...Object.entries(sku.ProductClusterNames ?? {}).map(
169
+ ([propertyID, value]) =>
170
+ toAdditionalPropertyCluster({ propertyID, value }),
171
+ ),
172
+ ...sku.SkuSpecifications.flatMap((spec) =>
173
+ spec.FieldValues.map((value, it) =>
174
+ toAdditionalPropertySpecification({
175
+ propertyID: spec.FieldValueIds[it]?.toString(),
176
+ name: spec.FieldName,
177
+ value,
178
+ }),
179
+ ),
180
+ ),
181
+ ...sku.SalesChannels.map(
182
+ (ch): PropertyValue => ({
183
+ "@type": "PropertyValue",
184
+ name: "salesChannel",
185
+ propertyID: ch.toString(),
186
+ }),
187
+ ),
188
+ ].filter((p): p is PropertyValue => Boolean(p));
189
+
190
+ const groupAdditionalProperty = sku.ProductSpecifications.flatMap((spec) =>
191
+ spec.FieldValues.map((value, it) =>
192
+ toAdditionalPropertySpecification({
193
+ propertyID: spec.FieldValueIds[it]?.toString(),
194
+ name: spec.FieldName,
195
+ value,
196
+ }),
197
+ ),
198
+ );
199
+
200
+ const offers = simulations
201
+ .flatMap(({ items, paymentData }) =>
202
+ items?.map((item): Offer | null => {
203
+ const { sellingPrice, listPrice, price, seller, priceValidUntil, availability } = item;
204
+ const spotPrice = sellingPrice || price;
205
+ if (!spotPrice || !listPrice) return null;
206
+
207
+ return {
208
+ "@type": "Offer",
209
+ price: spotPrice / 100,
210
+ seller,
211
+ priceValidUntil,
212
+ inventoryLevel: {},
213
+ availability:
214
+ availability === "available"
215
+ ? "https://schema.org/InStock"
216
+ : "https://schema.org/OutOfStock",
217
+ priceSpecification: [
218
+ {
219
+ "@type": "UnitPriceSpecification",
220
+ priceType: "https://schema.org/ListPrice",
221
+ price: listPrice / 100,
222
+ },
223
+ {
224
+ "@type": "UnitPriceSpecification",
225
+ priceType: "https://schema.org/SalePrice",
226
+ price: spotPrice / 100,
227
+ },
228
+ ...(paymentData?.installmentOptions?.flatMap(
229
+ (option): UnitPriceSpecification[] =>
230
+ option.installments.map((i) => ({
231
+ "@type": "UnitPriceSpecification",
232
+ priceType: "https://schema.org/SalePrice",
233
+ priceComponentType: "https://schema.org/Installment",
234
+ name: option.paymentName,
235
+ billingDuration: i.count,
236
+ billingIncrement: i.value / 100,
237
+ price: i.total / 100,
238
+ })),
239
+ ) ?? []),
240
+ ],
241
+ };
242
+ }),
243
+ )
244
+ .filter((o): o is Offer => Boolean(o));
245
+
246
+ return {
247
+ "@type": "Product",
248
+ productID,
249
+ sku: productID,
250
+ inProductGroupWithID: productGroupID,
251
+ category: Object.values(sku.ProductCategories ?? {}).join(" > "),
252
+ url: `${sku.DetailUrl}?skuId=${productID}`,
253
+ name: sku.SkuName,
254
+ gtin: sku.AlternateIds.Ean,
255
+ image: sku.Images.map((img) => ({
256
+ "@type": "ImageObject",
257
+ encodingFormat: "image",
258
+ alternateName: img.ImageName ?? img.FileId,
259
+ url: img.ImageUrl,
260
+ })),
261
+ isVariantOf: {
262
+ "@type": "ProductGroup",
263
+ url: sku.DetailUrl,
264
+ hasVariant:
265
+ skus
266
+ ?.filter((x) => x.IsActive)
267
+ .map(({ Id }) => ({
268
+ "@type": "Product",
269
+ productID: `${Id}`,
270
+ sku: `${Id}`,
271
+ })) ?? [],
272
+ additionalProperty: groupAdditionalProperty,
273
+ productGroupID,
274
+ name: sku.ProductName,
275
+ description: sku.ProductDescription,
276
+ },
277
+ additionalProperty,
278
+ releaseDate: sku.ReleaseDate
279
+ ? new Date(sku.ReleaseDate).toISOString()
280
+ : undefined,
281
+ brand: {
282
+ "@type": "Brand",
283
+ "@id": sku.BrandId,
284
+ name: sku.BrandName,
285
+ },
286
+ offers: aggregateOffers(offers, channel?.CurrencyCode),
287
+ };
288
+ }
289
+
290
+ // ---------------------------------------------------------------------------
291
+ // workflowProducts
292
+ // ---------------------------------------------------------------------------
293
+
294
+ export interface WorkflowProductsOptions {
295
+ page: number;
296
+ pagesize: number;
297
+ }
298
+
299
+ /**
300
+ * Fetch a page of SKU IDs and return minimal Product stubs.
301
+ * Use in batch workflows to enumerate the catalog; call workflowProduct
302
+ * for each ID if full details are needed.
303
+ *
304
+ * Ported from: vtex/loaders/workflow/products.ts
305
+ */
306
+ export async function workflowProducts(
307
+ opts: WorkflowProductsOptions,
308
+ ): Promise<Product[]> {
309
+ const params = new URLSearchParams({
310
+ page: String(opts.page),
311
+ pagesize: String(opts.pagesize),
312
+ });
313
+
314
+ const ids = await vtexFetch<number[]>(
315
+ `/api/catalog_system/pvt/sku/stockkeepingunitids?${params}`,
316
+ );
317
+
318
+ return ids.map((productID) => ({
319
+ "@type": "Product",
320
+ productID: `${productID}`,
321
+ sku: `${productID}`,
322
+ }));
323
+ }
package/vtex/logo.png ADDED
Binary file
@@ -0,0 +1,229 @@
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 {
28
+ SEGMENT_COOKIE_NAME,
29
+ SALES_CHANNEL_COOKIE,
30
+ parseSegment,
31
+ buildSegmentFromParams,
32
+ serializeSegment,
33
+ DEFAULT_SEGMENT,
34
+ type WrappedSegment,
35
+ } from "./utils/segment";
36
+ import { SESSION_COOKIE, ANONYMOUS_COOKIE } from "./utils/intelligentSearch";
37
+ import { isVtexLoggedIn, extractVtexAuthCookie, parseVtexAuthToken } from "./utils/vtexId";
38
+ import type { Segment } from "./utils/types";
39
+
40
+ // -------------------------------------------------------------------------
41
+ // Types
42
+ // -------------------------------------------------------------------------
43
+
44
+ export interface VtexRequestContext {
45
+ /** Decoded segment from cookie or URL params. */
46
+ segment: Partial<Segment>;
47
+ /** Serialized segment token for cache key use. */
48
+ segmentToken: string;
49
+ /** Whether the user has a valid (non-expired) VTEX auth cookie. */
50
+ isLoggedIn: boolean;
51
+ /** Extracted email from the auth JWT, if available. */
52
+ email?: string;
53
+ /** Sales channel derived from segment. */
54
+ salesChannel: string;
55
+ /** Whether this request carries price tables (B2B). */
56
+ hasCustomPricing: boolean;
57
+ /** Intelligent Search session cookie. */
58
+ isSessionId: string;
59
+ /** Intelligent Search anonymous cookie. */
60
+ isAnonymousId: string;
61
+ }
62
+
63
+ // -------------------------------------------------------------------------
64
+ // Cookie helpers
65
+ // -------------------------------------------------------------------------
66
+
67
+ const IS_COOKIE_PREFIX = "vtex_is_";
68
+
69
+ function getCookieValue(cookieHeader: string, name: string): string | null {
70
+ const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`));
71
+ return match?.[1] ?? null;
72
+ }
73
+
74
+ // -------------------------------------------------------------------------
75
+ // Core extraction
76
+ // -------------------------------------------------------------------------
77
+
78
+ /**
79
+ * Extract VTEX context from an incoming request.
80
+ *
81
+ * Reads the segment cookie, URL params (utm_*, sc), and auth cookie
82
+ * to build a complete picture of the user's VTEX session state.
83
+ */
84
+ function generateUUID(): string {
85
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
86
+ return crypto.randomUUID();
87
+ }
88
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
89
+ const r = (Math.random() * 16) | 0;
90
+ return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
91
+ });
92
+ }
93
+
94
+ export function extractVtexContext(request: Request): VtexRequestContext {
95
+ const cookies = request.headers.get("cookie") ?? "";
96
+ const url = new URL(request.url);
97
+
98
+ const segmentCookie = getCookieValue(cookies, SEGMENT_COOKIE_NAME);
99
+ const cookieSegment = segmentCookie ? parseSegment(segmentCookie) : null;
100
+
101
+ const paramSegment = buildSegmentFromParams(url.searchParams);
102
+
103
+ const vtexsc = getCookieValue(cookies, SALES_CHANNEL_COOKIE);
104
+
105
+ const segment: Partial<Segment> = {
106
+ ...DEFAULT_SEGMENT,
107
+ ...cookieSegment,
108
+ ...paramSegment,
109
+ };
110
+ if (vtexsc) segment.channel = vtexsc;
111
+
112
+ const segmentToken = serializeSegment(segment);
113
+
114
+ const authToken = extractVtexAuthCookie(cookies);
115
+ const authInfo = authToken ? parseVtexAuthToken(authToken) : null;
116
+
117
+ const isSessionId = getCookieValue(cookies, SESSION_COOKIE) ?? generateUUID();
118
+ const isAnonymousId = getCookieValue(cookies, ANONYMOUS_COOKIE) ?? generateUUID();
119
+
120
+ return {
121
+ segment,
122
+ segmentToken,
123
+ isLoggedIn: authInfo?.isLoggedIn ?? false,
124
+ email: authInfo?.email,
125
+ salesChannel: segment.channel ?? "1",
126
+ hasCustomPricing: Boolean(
127
+ segment.priceTables && segment.priceTables.length > 0,
128
+ ),
129
+ isSessionId,
130
+ isAnonymousId,
131
+ };
132
+ }
133
+
134
+ // -------------------------------------------------------------------------
135
+ // Cache control
136
+ // -------------------------------------------------------------------------
137
+
138
+ /**
139
+ * Determine the appropriate Cache-Control header based on VTEX context.
140
+ *
141
+ * Rules:
142
+ * - Logged-in users: private (personalized prices, wishlists, etc.)
143
+ * - Custom pricing (B2B): private (price table specific)
144
+ * - Anonymous default segment: public with CDN caching
145
+ */
146
+ export function vtexCacheControl(
147
+ ctx: VtexRequestContext,
148
+ options?: {
149
+ /** Max age for public (anonymous) responses in seconds. @default 60 */
150
+ publicMaxAge?: number;
151
+ /** Stale-while-revalidate for public responses in seconds. @default 3600 */
152
+ publicSWR?: number;
153
+ },
154
+ ): string {
155
+ if (ctx.isLoggedIn || ctx.hasCustomPricing) {
156
+ return "private, no-cache, no-store, must-revalidate";
157
+ }
158
+
159
+ const maxAge = options?.publicMaxAge ?? 60;
160
+ const swr = options?.publicSWR ?? 3600;
161
+
162
+ return `public, s-maxage=${maxAge}, stale-while-revalidate=${swr}, stale-if-error=86400`;
163
+ }
164
+
165
+ // -------------------------------------------------------------------------
166
+ // Cookie propagation
167
+ // -------------------------------------------------------------------------
168
+
169
+ /**
170
+ * Ensure Intelligent Search cookies exist on the response.
171
+ *
172
+ * If the browser already has them, they are forwarded as-is.
173
+ * If not, new UUIDs from the context are set. This ensures
174
+ * every user has IS cookies for personalization and analytics.
175
+ */
176
+ export function propagateISCookies(
177
+ ctx: VtexRequestContext,
178
+ response: Response,
179
+ ): void {
180
+ const maxAge = 365 * 24 * 60 * 60;
181
+ response.headers.append(
182
+ "Set-Cookie",
183
+ `${SESSION_COOKIE}=${ctx.isSessionId}; Path=/; SameSite=Lax; Max-Age=${maxAge}`,
184
+ );
185
+ response.headers.append(
186
+ "Set-Cookie",
187
+ `${ANONYMOUS_COOKIE}=${ctx.isAnonymousId}; Path=/; SameSite=Lax; Max-Age=${maxAge}`,
188
+ );
189
+ }
190
+
191
+ /**
192
+ * Build a segment cookie Set-Cookie header for the response.
193
+ *
194
+ * Use this when URL params change the segment (e.g., ?sc=2) so the
195
+ * browser persists the new segment for subsequent requests.
196
+ */
197
+ export function buildSegmentSetCookie(
198
+ segment: Partial<Segment>,
199
+ domain?: string,
200
+ ): string {
201
+ const token = serializeSegment(segment);
202
+ let cookie = `${SEGMENT_COOKIE_NAME}=${token}; Path=/; SameSite=Lax; Max-Age=86400`;
203
+ if (domain) cookie += `; Domain=${domain}`;
204
+ return cookie;
205
+ }
206
+
207
+ // -------------------------------------------------------------------------
208
+ // Cache key helpers
209
+ // -------------------------------------------------------------------------
210
+
211
+ /**
212
+ * Build a cache key suffix from the VTEX context.
213
+ *
214
+ * This is used in the Cloudflare Worker entry to differentiate cached
215
+ * responses by segment. Two anonymous users on the same sales channel
216
+ * get the same cache key; a logged-in user gets a unique (uncached) key.
217
+ */
218
+ export function vtexCacheKeySuffix(ctx: VtexRequestContext): string {
219
+ if (ctx.isLoggedIn) return "__vtex_auth";
220
+ return `__vtex_sc=${ctx.salesChannel}`;
221
+ }
222
+
223
+ // -------------------------------------------------------------------------
224
+ // Re-exports for convenience
225
+ // -------------------------------------------------------------------------
226
+
227
+ export { isVtexLoggedIn } from "./utils/vtexId";
228
+ export type { VtexAuthInfo } from "./utils/vtexId";
229
+ export type { Segment } from "./utils/types";