@bacano/sdk 1.0.0 → 1.1.0

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 (68) hide show
  1. package/README.md +342 -7
  2. package/STOREFRONT_CLARITY.md +140 -0
  3. package/STOREFRONT_CONSTRUCTION.md +92 -0
  4. package/STOREFRONT_LEADS.md +130 -0
  5. package/STOREFRONT_LEGAL.md +66 -0
  6. package/STOREFRONT_MARKETING.md +286 -0
  7. package/STOREFRONT_PERFORMANCE.md +265 -0
  8. package/STOREFRONT_PREVIEW.md +474 -0
  9. package/client.d.ts +7 -1
  10. package/errors.d.ts +9 -1
  11. package/graphql.cjs +63 -0
  12. package/graphql.d.ts +2 -2
  13. package/graphql.js +63 -0
  14. package/index.cjs +1542 -192
  15. package/index.d.ts +8 -6
  16. package/index.js +1542 -192
  17. package/marketing/attribution.d.ts +51 -0
  18. package/marketing/clarity.d.ts +3 -0
  19. package/marketing/domain.d.ts +1 -0
  20. package/marketing/ga4.d.ts +2 -0
  21. package/marketing/index.d.ts +5 -0
  22. package/marketing/runtime.d.ts +2 -0
  23. package/marketing/sanitize.d.ts +2 -0
  24. package/marketing/types.d.ts +109 -0
  25. package/media.d.ts +45 -2
  26. package/modules/auth.d.ts +3 -2
  27. package/modules/availability.d.ts +2 -2
  28. package/modules/cart-session.d.ts +2 -2
  29. package/modules/cart.d.ts +3 -3
  30. package/modules/catalog.d.ts +15 -3
  31. package/modules/content.d.ts +2 -2
  32. package/modules/marketing.d.ts +5 -0
  33. package/modules/orders.d.ts +2 -2
  34. package/modules/profile.d.ts +1 -1
  35. package/modules/static-catalog.d.ts +13 -0
  36. package/modules/website.d.ts +7 -1
  37. package/package.json +14 -3
  38. package/react/context-registry.d.ts +9 -0
  39. package/react/context.d.ts +1 -1
  40. package/react/hooks.d.ts +23 -2
  41. package/react/marketing-context.d.ts +3 -0
  42. package/react/marketing-provider.d.ts +10 -0
  43. package/react/preview/cache.d.ts +23 -0
  44. package/react/preview/context.d.ts +5 -0
  45. package/react/preview/navigation.d.ts +9 -0
  46. package/react/preview/provider.d.ts +3 -0
  47. package/react/preview/shell.d.ts +3 -0
  48. package/react/preview/toolbar.d.ts +3 -0
  49. package/react/preview/types.d.ts +60 -0
  50. package/react/preview.cjs +742 -0
  51. package/react/preview.d.ts +14 -0
  52. package/react/preview.js +730 -0
  53. package/react/website-gate.d.ts +22 -0
  54. package/react.cjs +3432 -1780
  55. package/react.d.ts +8 -4
  56. package/react.js +3428 -1772
  57. package/skills/bacano-web/SKILL.md +109 -0
  58. package/skills/bacano-web/agents/openai.yaml +4 -0
  59. package/skills/bacano-web/references/common.md +91 -0
  60. package/skills/bacano-web/references/informational.md +39 -0
  61. package/skills/bacano-web/references/report.md +52 -0
  62. package/skills/bacano-web/references/storefront.md +116 -0
  63. package/skills/bacano-web/scripts/audit-http.mjs +247 -0
  64. package/skills/bacano-web/scripts/cli.mjs +222 -0
  65. package/skills/bacano-web/scripts/postinstall.mjs +8 -0
  66. package/transport/documents.d.ts +8 -6
  67. package/transport/graphql.d.ts +13 -1
  68. package/types.d.ts +202 -0
package/index.cjs CHANGED
@@ -23,28 +23,55 @@ __export(index_exports, {
23
23
  AuthError: () => AuthError,
24
24
  CheckoutError: () => CheckoutError,
25
25
  GraphQLRequestError: () => GraphQLRequestError,
26
+ MARKETING_SCHEMA_VERSION: () => MARKETING_SCHEMA_VERSION,
26
27
  NotInitializedError: () => NotInitializedError,
28
+ RateLimitError: () => RateLimitError,
29
+ WEBSITE_MARKETING_CONTACT_PLACEMENTS: () => WEBSITE_MARKETING_CONTACT_PLACEMENTS,
27
30
  WebsiteError: () => WebsiteError,
31
+ buildStorageContentMediaUrls: () => buildStorageContentMediaUrls,
28
32
  buildStorageFileUrl: () => buildStorageFileUrl,
29
33
  buildStorageMediaUrls: () => buildStorageMediaUrls,
34
+ buildStorageResponsiveSources: () => buildStorageResponsiveSources,
35
+ buildStorageTransformedFileUrl: () => buildStorageTransformedFileUrl,
36
+ captureWebsiteMarketingAttribution: () => captureWebsiteMarketingAttribution,
30
37
  createBacanoClient: () => createBacanoClient,
38
+ createWebsiteMarketingRuntime: () => createWebsiteMarketingRuntime,
39
+ formatMediaSrcSet: () => formatMediaSrcSet,
40
+ getAllowedMarketingParameters: () => getAllowedMarketingParameters,
31
41
  getPrimaryProductImage: () => getPrimaryProductImage,
42
+ getProductDetailVariants: () => getProductDetailVariants,
32
43
  getProductImages: () => getProductImages,
33
44
  getProductMediaByKind: () => getProductMediaByKind,
34
45
  getProductMediaKind: () => getProductMediaKind,
35
46
  getProductVideos: () => getProductVideos,
47
+ getWebsiteContextFromPublicConfig: () => getWebsiteContextFromPublicConfig,
48
+ getWebsiteMarketingAttribution: () => getWebsiteMarketingAttribution,
36
49
  getWebsitePublicConfig: () => getWebsitePublicConfig,
37
- resolveProductPricing: () => resolveProductPricing
50
+ resolveProductPricing: () => resolveProductPricing,
51
+ sanitizeMarketingUrl: () => sanitizeMarketingUrl
38
52
  });
39
53
  module.exports = __toCommonJS(index_exports);
40
54
 
41
55
  // libs/sdk/src/errors.ts
42
56
  var WebsiteError = class extends Error {
43
57
  code;
44
- constructor(message, code = "WEBSITE_ERROR") {
58
+ status;
59
+ retryAfterSeconds;
60
+ constructor(message, code = "WEBSITE_ERROR", options) {
45
61
  super(message);
46
62
  this.name = "WebsiteError";
47
63
  this.code = code;
64
+ this.status = options?.status ?? null;
65
+ this.retryAfterSeconds = options?.retryAfterSeconds ?? null;
66
+ }
67
+ };
68
+ var RateLimitError = class extends WebsiteError {
69
+ constructor(message, retryAfterSeconds) {
70
+ super(message, "RATE_LIMITED", {
71
+ status: 429,
72
+ retryAfterSeconds
73
+ });
74
+ this.name = "RateLimitError";
48
75
  }
49
76
  };
50
77
  var NotInitializedError = class extends WebsiteError {
@@ -80,7 +107,109 @@ var GraphQLRequestError = class extends WebsiteError {
80
107
  }
81
108
  };
82
109
 
110
+ // libs/sdk/src/media.ts
111
+ var DEFAULT_RESPONSIVE_WIDTHS = [240, 384, 640, 960, 1400];
112
+ var CONTENT_HERO_WIDTHS = [640, 960, 1400, 1920, 2560];
113
+ var CONTENT_HERO_MAX_WIDTH = 1920;
114
+ var MEDIA_VARIANTS = {
115
+ thumbnail: { width: 240, quality: 75, format: "webp" },
116
+ card: { width: 640, quality: 80, format: "webp" },
117
+ detail: { width: 1400, quality: 85, format: "webp" }
118
+ };
119
+ function toSafeInteger(value) {
120
+ if (!Number.isFinite(value)) return void 0;
121
+ return Math.max(1, Math.floor(value));
122
+ }
123
+ function buildQuery(options) {
124
+ const params = new URLSearchParams();
125
+ const width = toSafeInteger(options.width);
126
+ const height = toSafeInteger(options.height);
127
+ const quality = toSafeInteger(options.quality);
128
+ if (width) params.set("w", String(width));
129
+ if (height) params.set("h", String(height));
130
+ if (quality) params.set("q", String(Math.min(100, quality)));
131
+ if (options.format) params.set("f", options.format);
132
+ const query = params.toString();
133
+ return query ? `?${query}` : "";
134
+ }
135
+ function buildStorageFileUrl(storageUrl, fileId, variant = "original") {
136
+ if (!storageUrl || !fileId) return "";
137
+ const baseUrl = `${storageUrl.replace(/\/+$/, "")}/files/${fileId}`;
138
+ if (variant === "original") return baseUrl;
139
+ return `${baseUrl}${buildQuery(MEDIA_VARIANTS[variant])}`;
140
+ }
141
+ function buildStorageTransformedFileUrl(storageUrl, fileId, options) {
142
+ if (!storageUrl || !fileId) return "";
143
+ const baseUrl = `${storageUrl.replace(/\/+$/, "")}/files/${fileId}`;
144
+ return `${baseUrl}${buildQuery(options)}`;
145
+ }
146
+ function buildStorageResponsiveSources(storageUrl, fileId, options = {}) {
147
+ const widths = options.widths ?? [...DEFAULT_RESPONSIVE_WIDTHS];
148
+ const uniqueWidths = [...new Set(widths.map(toSafeInteger))].filter((width) => Boolean(width)).sort((left, right) => left - right);
149
+ return uniqueWidths.map((width) => ({
150
+ width,
151
+ url: buildStorageTransformedFileUrl(storageUrl, fileId, {
152
+ width,
153
+ quality: options.quality ?? 80,
154
+ format: options.format ?? "webp"
155
+ })
156
+ }));
157
+ }
158
+ function formatMediaSrcSet(sources) {
159
+ return sources.filter((source) => source.url && source.width > 0).map((source) => `${source.url} ${source.width}w`).join(", ");
160
+ }
161
+ function buildStorageMediaUrls(storageUrl, fileId) {
162
+ return {
163
+ originalUrl: buildStorageFileUrl(storageUrl, fileId, "original"),
164
+ thumbnailUrl: buildStorageFileUrl(storageUrl, fileId, "thumbnail"),
165
+ cardUrl: buildStorageFileUrl(storageUrl, fileId, "card"),
166
+ detailUrl: buildStorageFileUrl(storageUrl, fileId, "detail"),
167
+ responsiveSources: buildStorageResponsiveSources(storageUrl, fileId)
168
+ };
169
+ }
170
+ function buildStorageContentMediaUrls(storageUrl, fileId, originalWidth) {
171
+ const legacyUrls = buildStorageMediaUrls(storageUrl, fileId);
172
+ const safeOriginalWidth = toSafeInteger(originalWidth ?? void 0);
173
+ if (!safeOriginalWidth) {
174
+ return {
175
+ ...legacyUrls,
176
+ heroUrl: legacyUrls.originalUrl,
177
+ responsiveSources: []
178
+ };
179
+ }
180
+ const responsiveWidths = [
181
+ ...CONTENT_HERO_WIDTHS.filter((width) => width <= safeOriginalWidth),
182
+ safeOriginalWidth
183
+ ];
184
+ const heroWidth = Math.min(safeOriginalWidth, CONTENT_HERO_MAX_WIDTH);
185
+ return {
186
+ ...legacyUrls,
187
+ heroUrl: buildStorageTransformedFileUrl(storageUrl, fileId, {
188
+ width: heroWidth,
189
+ quality: 88,
190
+ format: "webp"
191
+ }),
192
+ responsiveSources: buildStorageResponsiveSources(storageUrl, fileId, {
193
+ widths: responsiveWidths,
194
+ quality: 85,
195
+ format: "webp"
196
+ })
197
+ };
198
+ }
199
+
83
200
  // libs/sdk/src/modules/auth.ts
201
+ function getWebsiteContextFromPublicConfig(config) {
202
+ if (config.contextSchemaVersion !== 1) return void 0;
203
+ return {
204
+ websiteId: config.websiteId,
205
+ companyId: config.companyId,
206
+ chainId: config.chainId ?? null,
207
+ name: config.name,
208
+ slug: config.slug,
209
+ domain: config.domain,
210
+ config: config.config ?? null
211
+ };
212
+ }
84
213
  function createAuthHeaders(accessToken) {
85
214
  return accessToken ? { Authorization: `Bearer ${accessToken}` } : {};
86
215
  }
@@ -102,14 +231,18 @@ function createRequestHeadersProvider(websiteSlug, getAccessToken) {
102
231
  }
103
232
  async function getWebsitePublicConfig(options) {
104
233
  const apiUrl = options.apiUrl.replace(/\/+$/, "");
105
- const headers = options.origin ? { Origin: options.origin } : void 0;
234
+ const headers = {};
235
+ if (options.origin) headers.Origin = options.origin;
236
+ if (options.previewToken) {
237
+ headers["X-Bacano-Preview-Token"] = options.previewToken;
238
+ }
106
239
  const params = new URLSearchParams({ websiteSlug: options.websiteSlug });
107
240
  let response;
108
241
  try {
109
242
  response = await fetch(
110
243
  `${apiUrl}/api/v1/website/public-config?${params.toString()}`,
111
244
  {
112
- headers,
245
+ headers: Object.keys(headers).length > 0 ? headers : void 0,
113
246
  cache: "no-store"
114
247
  }
115
248
  );
@@ -133,12 +266,71 @@ async function getWebsitePublicConfig(options) {
133
266
  require_document: false,
134
267
  document_types: []
135
268
  };
269
+ const marketing = raw.marketing ?? {
270
+ schema_version: 1,
271
+ enabled: false,
272
+ currency_code: "COP",
273
+ time_zone: "America/Bogota",
274
+ attribution_window_days: 90,
275
+ consent_policy: null,
276
+ destinations: { ga4: null, meta: null, clarity: null }
277
+ };
278
+ const legal = raw.legal ?? {
279
+ schema_version: 1,
280
+ version: 0,
281
+ content_hash: null,
282
+ documents: {
283
+ terms: null,
284
+ shipping: null,
285
+ returns: null,
286
+ privacy: null,
287
+ cookies: null
288
+ },
289
+ inquiries_channel: null,
290
+ direct_marketing_consent: null,
291
+ cookie_notice: {
292
+ enabled: false,
293
+ message: "Usamos cookies propias y de terceros para mejorar tu experiencia y medir el funcionamiento del sitio.",
294
+ button_label: "Entendido"
295
+ }
296
+ };
297
+ const mapConstructionImage = (image) => {
298
+ if (!image) return null;
299
+ const urls = buildStorageContentMediaUrls(
300
+ `${apiUrl}/storage/v1`,
301
+ image.file_id,
302
+ image.width
303
+ );
304
+ return {
305
+ fileId: image.file_id,
306
+ name: image.name,
307
+ altText: image.alt_text,
308
+ width: image.width,
309
+ height: image.height,
310
+ originalUrl: urls.originalUrl,
311
+ heroUrl: urls.heroUrl,
312
+ responsiveSources: urls.responsiveSources
313
+ };
314
+ };
315
+ const construction = raw.construction ?? {
316
+ schema_version: 1,
317
+ enabled: false,
318
+ enforced: false,
319
+ preview_access_granted: false,
320
+ title: null,
321
+ message: null,
322
+ desktop_image: null,
323
+ mobile_image: null
324
+ };
136
325
  return {
137
326
  websiteId: raw.website_id,
138
327
  companyId: raw.company_id,
328
+ contextSchemaVersion: raw.context_schema_version === 1 ? 1 : null,
329
+ chainId: raw.chain_id ?? null,
139
330
  slug: raw.slug,
140
331
  name: raw.name,
141
332
  domain: raw.domain,
333
+ config: raw.config ?? null,
142
334
  buyerAccountsEnabled: raw.buyer_accounts_enabled,
143
335
  clerkPublishableKey: raw.clerk_publishable_key,
144
336
  customerIdentification: {
@@ -154,56 +346,67 @@ async function getWebsitePublicConfig(options) {
154
346
  isDefault: documentType.is_default
155
347
  })
156
348
  )
349
+ },
350
+ marketing: {
351
+ schemaVersion: marketing.schema_version,
352
+ enabled: marketing.enabled,
353
+ currencyCode: marketing.currency_code,
354
+ timeZone: marketing.time_zone,
355
+ attributionWindowDays: marketing.attribution_window_days,
356
+ consentPolicy: marketing.consent_policy ? {
357
+ id: marketing.consent_policy.id,
358
+ version: marketing.consent_policy.version,
359
+ statement: marketing.consent_policy.statement,
360
+ statementHash: marketing.consent_policy.statement_hash
361
+ } : null,
362
+ destinations: {
363
+ ga4: marketing.destinations.ga4 ? {
364
+ measurementId: marketing.destinations.ga4.measurement_id,
365
+ diagnosticMode: marketing.destinations.ga4.diagnostic_mode ?? false
366
+ } : null,
367
+ meta: marketing.destinations.meta ? {
368
+ pixelId: marketing.destinations.meta.pixel_id,
369
+ diagnosticMode: marketing.destinations.meta.diagnostic_mode ?? false
370
+ } : null,
371
+ clarity: marketing.destinations.clarity ? { projectId: marketing.destinations.clarity.project_id } : null
372
+ }
373
+ },
374
+ legal: {
375
+ schemaVersion: legal.schema_version,
376
+ version: legal.version,
377
+ contentHash: legal.content_hash,
378
+ documents: {
379
+ terms: legal.documents.terms,
380
+ shipping: legal.documents.shipping ?? null,
381
+ returns: legal.documents.returns ?? null,
382
+ privacy: legal.documents.privacy,
383
+ cookies: legal.documents.cookies
384
+ },
385
+ inquiriesChannel: legal.inquiries_channel,
386
+ directMarketingConsent: legal.direct_marketing_consent ? {
387
+ version: legal.direct_marketing_consent.version,
388
+ statement: legal.direct_marketing_consent.statement,
389
+ statementHash: legal.direct_marketing_consent.statement_hash
390
+ } : null,
391
+ cookieNotice: {
392
+ enabled: legal.cookie_notice.enabled,
393
+ message: legal.cookie_notice.message,
394
+ buttonLabel: legal.cookie_notice.button_label
395
+ }
396
+ },
397
+ construction: {
398
+ schemaVersion: construction.schema_version,
399
+ enabled: construction.enabled,
400
+ enforced: construction.enforced,
401
+ previewAccessGranted: construction.preview_access_granted,
402
+ title: construction.title,
403
+ message: construction.message,
404
+ desktopImage: mapConstructionImage(construction.desktop_image),
405
+ mobileImage: mapConstructionImage(construction.mobile_image)
157
406
  }
158
407
  };
159
408
  }
160
409
 
161
- // libs/sdk/src/transport/graphql.ts
162
- async function executeGraphQL(url, query, variables, headers) {
163
- const resolvedHeaders = typeof headers === "function" ? await headers() : headers ?? {};
164
- let res;
165
- try {
166
- res = await fetch(url, {
167
- method: "POST",
168
- headers: {
169
- "Content-Type": "application/json",
170
- ...resolvedHeaders
171
- },
172
- body: JSON.stringify({ query, variables })
173
- });
174
- } catch (err) {
175
- throw new WebsiteError(
176
- `Network error connecting to ${url}: ${err instanceof Error ? err.message : String(err)}`,
177
- "NETWORK_ERROR"
178
- );
179
- }
180
- if (res.status === 429) {
181
- throw new WebsiteError(
182
- "Rate limit exceeded. Please try again later.",
183
- "RATE_LIMITED"
184
- );
185
- }
186
- let body;
187
- try {
188
- body = await res.json();
189
- } catch {
190
- throw new WebsiteError(
191
- `Non-JSON response from ${url} (status ${res.status})`,
192
- "INVALID_RESPONSE"
193
- );
194
- }
195
- if (body.errors && body.errors.length > 0) {
196
- throw new GraphQLRequestError(body.errors);
197
- }
198
- if (!body.data) {
199
- throw new WebsiteError(
200
- "GraphQL response contains no data",
201
- "EMPTY_RESPONSE"
202
- );
203
- }
204
- return body.data;
205
- }
206
-
207
410
  // libs/sdk/src/transport/documents.ts
208
411
  var RESOLVE_WEBSITE_BY_SLUG = (
209
412
  /* GraphQL */
@@ -1029,6 +1232,62 @@ var CHECK_AVAILABILITY = (
1029
1232
  product_variant_id
1030
1233
  in_stock
1031
1234
  label
1235
+ max_purchasable_quantity
1236
+ }
1237
+ }
1238
+ `
1239
+ );
1240
+ var GET_PRODUCT_LIVE_STATE = (
1241
+ /* GraphQL */
1242
+ `
1243
+ query GetProductLiveState(
1244
+ $variantIds: [uuid!]!
1245
+ $companyId: uuid!
1246
+ $chainId: uuid!
1247
+ $websiteId: uuid!
1248
+ ) {
1249
+ product_variants(
1250
+ where: {
1251
+ id: { _in: $variantIds }
1252
+ is_disabled: { _eq: false }
1253
+ product: {
1254
+ company_id: { _eq: $companyId }
1255
+ product_website_publications: {
1256
+ website_id: { _eq: $websiteId }
1257
+ is_visible: { _eq: true }
1258
+ }
1259
+ chain_products: {
1260
+ chain_id: { _eq: $chainId }
1261
+ is_enabled: { _eq: true }
1262
+ status: { _eq: active }
1263
+ }
1264
+ }
1265
+ }
1266
+ ) {
1267
+ id
1268
+ product_variant_prices(
1269
+ where: {
1270
+ price_type: { _in: [CLIENT, SALE, WHOLESALE, WHOLESALE_2] }
1271
+ _or: [
1272
+ { chain_id: { _eq: $chainId } }
1273
+ { chain_id: { _is_null: true } }
1274
+ ]
1275
+ }
1276
+ ) {
1277
+ id
1278
+ price
1279
+ price_type
1280
+ chain_id
1281
+ }
1282
+ }
1283
+ websiteProductAvailability(
1284
+ product_variant_ids: $variantIds
1285
+ chain_id: $chainId
1286
+ ) {
1287
+ product_variant_id
1288
+ in_stock
1289
+ label
1290
+ max_purchasable_quantity
1032
1291
  }
1033
1292
  }
1034
1293
  `
@@ -1138,6 +1397,7 @@ var START_GUEST_CHECKOUT = (
1138
1397
  $contact: WebsiteGuestContactInput!
1139
1398
  $items: [WebsiteCheckoutItemInput!]!
1140
1399
  $fulfillmentFamily: String
1400
+ $marketingAttribution: WebsiteMarketingAttributionInput
1141
1401
  ) {
1142
1402
  websiteStartGuestCheckout(
1143
1403
  website_id: $websiteId
@@ -1145,6 +1405,7 @@ var START_GUEST_CHECKOUT = (
1145
1405
  contact: $contact
1146
1406
  items: $items
1147
1407
  fulfillment_family: $fulfillmentFamily
1408
+ marketing_attribution: $marketingAttribution
1148
1409
  ) {
1149
1410
  success
1150
1411
  order_id
@@ -1302,6 +1563,8 @@ var CHECKOUT = (
1302
1563
  $paymentMethodId: uuid
1303
1564
  $buyerAccountOptIn: Boolean
1304
1565
  $initialPayment: WebsiteInitialPaymentInput
1566
+ $marketingAttribution: WebsiteMarketingAttributionInput
1567
+ $advertisingConsent: WebsiteAdvertisingConsentInput
1305
1568
  ) {
1306
1569
  websiteCheckout(
1307
1570
  order_id: $orderId
@@ -1314,6 +1577,8 @@ var CHECKOUT = (
1314
1577
  payment_method_id: $paymentMethodId
1315
1578
  buyer_account_opt_in: $buyerAccountOptIn
1316
1579
  initial_payment: $initialPayment
1580
+ marketing_attribution: $marketingAttribution
1581
+ advertising_consent: $advertisingConsent
1317
1582
  ) {
1318
1583
  success
1319
1584
  order_id
@@ -2240,13 +2505,121 @@ var GET_SEARCH_SUGGESTIONS = (
2240
2505
  `
2241
2506
  );
2242
2507
 
2508
+ // libs/sdk/src/transport/graphql.ts
2509
+ var DEFAULT_MAX_ATTEMPTS = 4;
2510
+ var DEFAULT_MAX_TOTAL_WAIT_MS = 3 * 60 * 1e3;
2511
+ var DEFAULT_JITTER_RATIO = 0.1;
2512
+ function parseRetryAfterSeconds(res, body) {
2513
+ const header = res.headers?.get?.("Retry-After")?.trim();
2514
+ if (header) {
2515
+ const seconds = Number(header);
2516
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds);
2517
+ const retryAt = Date.parse(header);
2518
+ if (Number.isFinite(retryAt)) {
2519
+ return Math.max(Math.ceil((retryAt - Date.now()) / 1e3), 0);
2520
+ }
2521
+ }
2522
+ if (body && typeof body === "object") {
2523
+ const seconds = Number(
2524
+ body.retry_after_seconds
2525
+ );
2526
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds);
2527
+ }
2528
+ return 1;
2529
+ }
2530
+ async function createRateLimitError(res) {
2531
+ let body;
2532
+ try {
2533
+ body = await res.json();
2534
+ } catch {
2535
+ body = null;
2536
+ }
2537
+ const message = body && typeof body === "object" && typeof body.message === "string" ? body.message : "Rate limit exceeded. Please try again later.";
2538
+ return new RateLimitError(message, parseRetryAfterSeconds(res, body));
2539
+ }
2540
+ function getRetryDelayMs(error, options) {
2541
+ const retryAfterMs = (error.retryAfterSeconds ?? 1) * 1e3;
2542
+ const jitterRatio = Math.max(
2543
+ 0,
2544
+ Math.min(options.jitterRatio ?? DEFAULT_JITTER_RATIO, 1)
2545
+ );
2546
+ return Math.ceil(retryAfterMs * (1 + Math.random() * jitterRatio));
2547
+ }
2548
+ function wait(delayMs) {
2549
+ return new Promise((resolve) => setTimeout(resolve, delayMs));
2550
+ }
2551
+ async function executeGraphQL(url, query, variables, headers, options) {
2552
+ const retry = options?.retry;
2553
+ const maxAttempts = Math.max(
2554
+ 1,
2555
+ Math.floor(retry?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS)
2556
+ );
2557
+ const maxTotalWaitMs = Math.max(
2558
+ 0,
2559
+ retry?.maxTotalWaitMs ?? DEFAULT_MAX_TOTAL_WAIT_MS
2560
+ );
2561
+ let totalWaitMs = 0;
2562
+ let res = null;
2563
+ for (let attempt = 1; attempt <= (retry ? maxAttempts : 1); attempt += 1) {
2564
+ const resolvedHeaders = typeof headers === "function" ? await headers() : headers ?? {};
2565
+ try {
2566
+ res = await fetch(url, {
2567
+ method: "POST",
2568
+ headers: {
2569
+ "Content-Type": "application/json",
2570
+ ...resolvedHeaders
2571
+ },
2572
+ body: JSON.stringify({ query, variables })
2573
+ });
2574
+ } catch (err) {
2575
+ throw new WebsiteError(
2576
+ `Network error connecting to ${url}: ${err instanceof Error ? err.message : String(err)}`,
2577
+ "NETWORK_ERROR"
2578
+ );
2579
+ }
2580
+ if (res.status !== 429) break;
2581
+ const rateLimitError = await createRateLimitError(res);
2582
+ const delayMs = getRetryDelayMs(rateLimitError, retry ?? {});
2583
+ const canRetry = Boolean(retry) && attempt < maxAttempts && totalWaitMs + delayMs <= maxTotalWaitMs;
2584
+ if (!canRetry) throw rateLimitError;
2585
+ totalWaitMs += delayMs;
2586
+ await wait(delayMs);
2587
+ }
2588
+ if (!res) {
2589
+ throw new WebsiteError(
2590
+ "GraphQL request did not produce a response",
2591
+ "EMPTY_RESPONSE"
2592
+ );
2593
+ }
2594
+ let body;
2595
+ try {
2596
+ body = await res.json();
2597
+ } catch {
2598
+ throw new WebsiteError(
2599
+ `Non-JSON response from ${url} (status ${res.status})`,
2600
+ "INVALID_RESPONSE"
2601
+ );
2602
+ }
2603
+ if (body.errors && body.errors.length > 0) {
2604
+ throw new GraphQLRequestError(body.errors);
2605
+ }
2606
+ if (!body.data) {
2607
+ throw new WebsiteError(
2608
+ "GraphQL response contains no data",
2609
+ "EMPTY_RESPONSE"
2610
+ );
2611
+ }
2612
+ return body.data;
2613
+ }
2614
+
2243
2615
  // libs/sdk/src/modules/availability.ts
2244
2616
  var MAX_AVAILABILITY_VARIANT_IDS = 100;
2245
2617
  function mapAvailability(raw) {
2246
2618
  return {
2247
2619
  productVariantId: raw.product_variant_id,
2248
2620
  inStock: raw.in_stock,
2249
- label: raw.label
2621
+ label: raw.label,
2622
+ maxPurchasableQuantity: raw.max_purchasable_quantity
2250
2623
  };
2251
2624
  }
2252
2625
  function chunkArray(items, chunkSize) {
@@ -2272,7 +2645,10 @@ function createAvailabilityModule(graphqlUrl, getHeaders, getContext) {
2272
2645
  if (productVariantIds.length === 0) return [];
2273
2646
  const variantIds = uniqueValues(productVariantIds);
2274
2647
  const results = [];
2275
- for (const chunk of chunkArray(variantIds, MAX_AVAILABILITY_VARIANT_IDS)) {
2648
+ for (const chunk of chunkArray(
2649
+ variantIds,
2650
+ MAX_AVAILABILITY_VARIANT_IDS
2651
+ )) {
2276
2652
  const data = await executeGraphQL(
2277
2653
  graphqlUrl,
2278
2654
  CHECK_AVAILABILITY,
@@ -2291,6 +2667,255 @@ function createAvailabilityModule(graphqlUrl, getHeaders, getContext) {
2291
2667
  };
2292
2668
  }
2293
2669
 
2670
+ // libs/sdk/src/marketing/sanitize.ts
2671
+ var SENSITIVE_PARAMETER_PATTERNS = [
2672
+ /token/i,
2673
+ /secret/i,
2674
+ /password/i,
2675
+ /authorization/i,
2676
+ /session/i,
2677
+ /ticket/i,
2678
+ /code_verifier/i,
2679
+ /client_secret/i,
2680
+ /payment/i,
2681
+ /order[_-]?(?:id|token)/i,
2682
+ /public[_-]?order/i,
2683
+ /clerk/i,
2684
+ /oauth/i,
2685
+ /email/i,
2686
+ /phone/i,
2687
+ /document/i
2688
+ ];
2689
+ var ALLOWED_MARKETING_PARAMETERS = /* @__PURE__ */ new Set([
2690
+ "utm_source",
2691
+ "utm_medium",
2692
+ "utm_campaign",
2693
+ "utm_term",
2694
+ "utm_content",
2695
+ "utm_source_platform",
2696
+ "utm_id",
2697
+ "gclid",
2698
+ "gbraid",
2699
+ "wbraid",
2700
+ "fbclid",
2701
+ "ttclid",
2702
+ "msclkid"
2703
+ ]);
2704
+ function sanitizeMarketingUrl(input, baseUrl) {
2705
+ try {
2706
+ const url = new URL(input, baseUrl);
2707
+ const safeParameters = new URLSearchParams();
2708
+ for (const [key, value] of url.searchParams.entries()) {
2709
+ if (ALLOWED_MARKETING_PARAMETERS.has(key.toLowerCase()) && !SENSITIVE_PARAMETER_PATTERNS.some((pattern) => pattern.test(key))) {
2710
+ safeParameters.set(key.toLowerCase(), value.slice(0, 500));
2711
+ }
2712
+ }
2713
+ url.search = safeParameters.toString();
2714
+ url.hash = "";
2715
+ return url.toString();
2716
+ } catch {
2717
+ return "";
2718
+ }
2719
+ }
2720
+ function getAllowedMarketingParameters(input, baseUrl) {
2721
+ const sanitized = sanitizeMarketingUrl(input, baseUrl);
2722
+ if (!sanitized) return {};
2723
+ const url = new URL(sanitized);
2724
+ return Object.fromEntries(url.searchParams.entries());
2725
+ }
2726
+
2727
+ // libs/sdk/src/marketing/attribution.ts
2728
+ var STORAGE_PREFIX = "bacano:marketing:attribution:v1";
2729
+ var GA4_CLIENT_ID_PATTERN = /^\d+\.\d+$/;
2730
+ var META_BROWSER_ID_PATTERN = /^fb\.\d+\.\d+\.[A-Za-z0-9_-]+$/;
2731
+ function storageKey(websiteId) {
2732
+ return `${STORAGE_PREFIX}:${websiteId}`;
2733
+ }
2734
+ function readStored(websiteId) {
2735
+ if (typeof window === "undefined") return null;
2736
+ try {
2737
+ const value = window.localStorage.getItem(storageKey(websiteId));
2738
+ if (!value) return null;
2739
+ const parsed = JSON.parse(value);
2740
+ if (new Date(parsed.expiresAt).getTime() <= Date.now()) {
2741
+ window.localStorage.removeItem(storageKey(websiteId));
2742
+ return null;
2743
+ }
2744
+ return parsed;
2745
+ } catch {
2746
+ return null;
2747
+ }
2748
+ }
2749
+ function writeStored(websiteId, value) {
2750
+ if (typeof window === "undefined") return;
2751
+ try {
2752
+ window.localStorage.setItem(storageKey(websiteId), JSON.stringify(value));
2753
+ } catch {
2754
+ }
2755
+ }
2756
+ function readCookie(name) {
2757
+ if (typeof document === "undefined") return void 0;
2758
+ try {
2759
+ const prefix = `${name}=`;
2760
+ return document.cookie.split(";").map((part) => part.trim()).find((part) => part.startsWith(prefix))?.slice(prefix.length);
2761
+ } catch {
2762
+ return void 0;
2763
+ }
2764
+ }
2765
+ function validMetaBrowserId(value) {
2766
+ const normalized = value?.trim();
2767
+ return normalized && normalized.length <= 512 && META_BROWSER_ID_PATTERN.test(normalized) ? normalized : void 0;
2768
+ }
2769
+ function optional(value) {
2770
+ const normalized = value?.trim();
2771
+ return normalized ? normalized.slice(0, 500) : void 0;
2772
+ }
2773
+ function buildTouchpoint() {
2774
+ if (typeof window === "undefined") return null;
2775
+ const params = getAllowedMarketingParameters(window.location.href);
2776
+ const referrerHost = (() => {
2777
+ if (!document.referrer) return void 0;
2778
+ try {
2779
+ const hostname = new URL(document.referrer).hostname;
2780
+ return hostname === window.location.hostname ? void 0 : hostname.slice(0, 255);
2781
+ } catch {
2782
+ return void 0;
2783
+ }
2784
+ })();
2785
+ return {
2786
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
2787
+ landingUrl: sanitizeMarketingUrl(window.location.href),
2788
+ referrerHost,
2789
+ source: optional(params.utm_source),
2790
+ medium: optional(params.utm_medium),
2791
+ campaign: optional(params.utm_campaign),
2792
+ term: optional(params.utm_term),
2793
+ content: optional(params.utm_content),
2794
+ sourcePlatform: optional(params.utm_source_platform),
2795
+ campaignId: optional(params.utm_id),
2796
+ gclid: optional(params.gclid),
2797
+ gbraid: optional(params.gbraid),
2798
+ wbraid: optional(params.wbraid),
2799
+ fbclid: optional(params.fbclid),
2800
+ ttclid: optional(params.ttclid),
2801
+ msclkid: optional(params.msclkid)
2802
+ };
2803
+ }
2804
+ function isNonDirect(touchpoint) {
2805
+ return Boolean(
2806
+ touchpoint.source || touchpoint.medium || touchpoint.campaign || touchpoint.gclid || touchpoint.gbraid || touchpoint.wbraid || touchpoint.fbclid || touchpoint.ttclid || touchpoint.msclkid || touchpoint.referrerHost
2807
+ );
2808
+ }
2809
+ function toPublic(stored) {
2810
+ return {
2811
+ schemaVersion: 1,
2812
+ firstTouch: stored.firstTouch,
2813
+ lastNonDirectTouch: stored.lastNonDirectTouch,
2814
+ ga4ClientId: stored.ga4ClientId,
2815
+ ga4SessionId: stored.ga4SessionId,
2816
+ metaFbp: stored.metaFbp,
2817
+ metaFbc: stored.metaFbc,
2818
+ clientUserAgent: stored.clientUserAgent,
2819
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
2820
+ expiresAt: stored.expiresAt
2821
+ };
2822
+ }
2823
+ function refreshWebsiteMarketingMetaContext(websiteId) {
2824
+ try {
2825
+ const stored = readStored(websiteId);
2826
+ if (!stored || typeof window === "undefined") return false;
2827
+ const fbp = validMetaBrowserId(readCookie("_fbp"));
2828
+ const clickTouchpoint = stored.lastNonDirectTouch ?? stored.firstTouch;
2829
+ const derivedFbc = clickTouchpoint.fbclid ? `fb.1.${new Date(clickTouchpoint.capturedAt).getTime()}.${clickTouchpoint.fbclid}` : void 0;
2830
+ const fbc = validMetaBrowserId(readCookie("_fbc") ?? derivedFbc);
2831
+ const clientUserAgent = window.navigator?.userAgent.trim().slice(0, 1024);
2832
+ writeStored(websiteId, {
2833
+ ...stored,
2834
+ metaFbp: fbp ?? stored.metaFbp,
2835
+ metaFbc: fbc ?? stored.metaFbc,
2836
+ clientUserAgent: clientUserAgent || stored.clientUserAgent
2837
+ });
2838
+ return Boolean(fbp || fbc || clientUserAgent);
2839
+ } catch {
2840
+ return false;
2841
+ }
2842
+ }
2843
+ function storeWebsiteMarketingGa4ClientId(websiteId, clientId) {
2844
+ if (typeof clientId !== "string") return false;
2845
+ const normalized = clientId.trim();
2846
+ if (!GA4_CLIENT_ID_PATTERN.test(normalized)) return false;
2847
+ const stored = readStored(websiteId);
2848
+ if (!stored) return false;
2849
+ writeStored(websiteId, { ...stored, ga4ClientId: normalized });
2850
+ return true;
2851
+ }
2852
+ function storeWebsiteMarketingGa4SessionId(websiteId, sessionId) {
2853
+ const normalized = typeof sessionId === "number" && Number.isSafeInteger(sessionId) ? String(sessionId) : typeof sessionId === "string" ? sessionId.trim() : "";
2854
+ const numericValue = Number(normalized);
2855
+ if (!/^\d{1,16}$/.test(normalized) || !Number.isSafeInteger(numericValue) || numericValue <= 0) {
2856
+ return false;
2857
+ }
2858
+ const stored = readStored(websiteId);
2859
+ if (!stored) return false;
2860
+ writeStored(websiteId, { ...stored, ga4SessionId: normalized });
2861
+ return true;
2862
+ }
2863
+ function captureWebsiteMarketingAttribution(websiteId, windowDays) {
2864
+ const current = buildTouchpoint();
2865
+ if (!current) return null;
2866
+ const stored = readStored(websiteId);
2867
+ const expiresAt = stored?.expiresAt ?? new Date(Date.now() + Math.max(1, windowDays) * 864e5).toISOString();
2868
+ const next = {
2869
+ firstTouch: stored?.firstTouch ?? current,
2870
+ lastNonDirectTouch: isNonDirect(current) ? current : stored?.lastNonDirectTouch ?? null,
2871
+ ga4ClientId: stored?.ga4ClientId,
2872
+ ga4SessionId: stored?.ga4SessionId,
2873
+ metaFbp: stored?.metaFbp,
2874
+ metaFbc: stored?.metaFbc,
2875
+ clientUserAgent: stored?.clientUserAgent,
2876
+ expiresAt
2877
+ };
2878
+ writeStored(websiteId, next);
2879
+ return toPublic(next);
2880
+ }
2881
+ function getWebsiteMarketingAttribution(websiteId) {
2882
+ const stored = readStored(websiteId);
2883
+ return stored ? toPublic(stored) : null;
2884
+ }
2885
+ function serializeTouchpoint(touchpoint) {
2886
+ return {
2887
+ captured_at: touchpoint.capturedAt,
2888
+ landing_url: touchpoint.landingUrl,
2889
+ referrer_host: touchpoint.referrerHost,
2890
+ source: touchpoint.source,
2891
+ medium: touchpoint.medium,
2892
+ campaign: touchpoint.campaign,
2893
+ term: touchpoint.term,
2894
+ content: touchpoint.content,
2895
+ source_platform: touchpoint.sourcePlatform,
2896
+ campaign_id: touchpoint.campaignId,
2897
+ gclid: touchpoint.gclid,
2898
+ gbraid: touchpoint.gbraid,
2899
+ wbraid: touchpoint.wbraid,
2900
+ fbclid: touchpoint.fbclid,
2901
+ ttclid: touchpoint.ttclid,
2902
+ msclkid: touchpoint.msclkid
2903
+ };
2904
+ }
2905
+ function serializeWebsiteMarketingAttribution(attribution) {
2906
+ return {
2907
+ schema_version: attribution.schemaVersion,
2908
+ first_touch: serializeTouchpoint(attribution.firstTouch),
2909
+ last_non_direct_touch: attribution.lastNonDirectTouch ? serializeTouchpoint(attribution.lastNonDirectTouch) : null,
2910
+ ga4_client_id: attribution.ga4ClientId,
2911
+ ga4_session_id: attribution.ga4SessionId,
2912
+ meta_fbp: attribution.metaFbp,
2913
+ meta_fbc: attribution.metaFbc,
2914
+ client_user_agent: attribution.clientUserAgent,
2915
+ captured_at: attribution.capturedAt
2916
+ };
2917
+ }
2918
+
2294
2919
  // libs/sdk/src/modules/cart.ts
2295
2920
  function mapCartItem(raw) {
2296
2921
  return {
@@ -2411,7 +3036,8 @@ function createCartModule(graphqlUrl, getHeaders, getContext, cartSessionStorage
2411
3036
  items: input.items.map((item) => ({
2412
3037
  product_variant_id: item.productVariantId,
2413
3038
  quantity: item.quantity
2414
- }))
3039
+ })),
3040
+ marketingAttribution: input.marketingAttribution ? serializeWebsiteMarketingAttribution(input.marketingAttribution) : void 0
2415
3041
  },
2416
3042
  getHeaders
2417
3043
  );
@@ -2533,7 +3159,9 @@ function createCartSessionStorage(kind, getSlug) {
2533
3159
  const key = readKey();
2534
3160
  const match = document.cookie.split("; ").find((row) => row.startsWith(`${key}=`));
2535
3161
  if (!match) return null;
2536
- return JSON.parse(decodeURIComponent(match.split("=").slice(1).join("=")));
3162
+ return JSON.parse(
3163
+ decodeURIComponent(match.split("=").slice(1).join("="))
3164
+ );
2537
3165
  } catch {
2538
3166
  return null;
2539
3167
  }
@@ -2573,46 +3201,129 @@ function createCartSessionStorage(kind, getSlug) {
2573
3201
  };
2574
3202
  }
2575
3203
 
2576
- // libs/sdk/src/media.ts
2577
- var MEDIA_VARIANTS = {
2578
- thumbnail: { width: 240, quality: 75, format: "webp" },
2579
- card: { width: 640, quality: 80, format: "webp" },
2580
- detail: { width: 1400, quality: 85, format: "webp" }
2581
- };
2582
- function toSafeInteger(value) {
2583
- if (!Number.isFinite(value)) return void 0;
2584
- return Math.max(1, Math.floor(value));
2585
- }
2586
- function buildQuery(options) {
2587
- const params = new URLSearchParams();
2588
- const width = toSafeInteger(options.width);
2589
- const height = toSafeInteger(options.height);
2590
- const quality = toSafeInteger(options.quality);
2591
- if (width) params.set("w", String(width));
2592
- if (height) params.set("h", String(height));
2593
- if (quality) params.set("q", String(Math.min(100, quality)));
2594
- if (options.format) params.set("f", options.format);
2595
- const query = params.toString();
2596
- return query ? `?${query}` : "";
2597
- }
2598
- function buildStorageFileUrl(storageUrl, fileId, variant = "original") {
2599
- if (!storageUrl || !fileId) return "";
2600
- const baseUrl = `${storageUrl.replace(/\/+$/, "")}/files/${fileId}`;
2601
- if (variant === "original") return baseUrl;
2602
- return `${baseUrl}${buildQuery(MEDIA_VARIANTS[variant])}`;
2603
- }
2604
- function buildStorageMediaUrls(storageUrl, fileId) {
2605
- return {
2606
- originalUrl: buildStorageFileUrl(storageUrl, fileId, "original"),
2607
- thumbnailUrl: buildStorageFileUrl(storageUrl, fileId, "thumbnail"),
2608
- cardUrl: buildStorageFileUrl(storageUrl, fileId, "card"),
2609
- detailUrl: buildStorageFileUrl(storageUrl, fileId, "detail")
3204
+ // libs/sdk/src/modules/static-catalog.ts
3205
+ var STATIC_CATALOG_PAGE_SIZE = 100;
3206
+ var STATIC_CATALOG_MAX_PRODUCTS = 1e4;
3207
+ var STATIC_CATALOG_INITIAL_PAGE_SIZE = 24;
3208
+ async function collectStaticCatalog({
3209
+ websiteId,
3210
+ snapshot: options,
3211
+ bootstrap,
3212
+ loadPage
3213
+ }) {
3214
+ const pageSize = options.pageSize ?? STATIC_CATALOG_PAGE_SIZE;
3215
+ const maxProducts = options.maxProducts ?? STATIC_CATALOG_MAX_PRODUCTS;
3216
+ const initialPageSize = bootstrap?.initialPageSize ?? STATIC_CATALOG_INITIAL_PAGE_SIZE;
3217
+ const sortBy = bootstrap?.sortBy ?? "name_asc";
3218
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) {
3219
+ throw new WebsiteError(
3220
+ "Static catalog pageSize must be an integer between 1 and 100.",
3221
+ "INVALID_STATIC_CATALOG_PAGE_SIZE"
3222
+ );
3223
+ }
3224
+ if (!Number.isInteger(maxProducts) || maxProducts < 1 || maxProducts > STATIC_CATALOG_MAX_PRODUCTS) {
3225
+ throw new WebsiteError(
3226
+ `Static catalog maxProducts must be an integer between 1 and ${STATIC_CATALOG_MAX_PRODUCTS}.`,
3227
+ "INVALID_STATIC_CATALOG_MAX_PRODUCTS"
3228
+ );
3229
+ }
3230
+ if (bootstrap && (!Number.isInteger(initialPageSize) || initialPageSize < 1 || initialPageSize > 100)) {
3231
+ throw new WebsiteError(
3232
+ "Static catalog initialPageSize must be an integer between 1 and 100.",
3233
+ "INVALID_STATIC_CATALOG_INITIAL_PAGE_SIZE"
3234
+ );
3235
+ }
3236
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
3237
+ const products = [];
3238
+ let pageCount = 0;
3239
+ let expectedTotal = null;
3240
+ let firstPage = null;
3241
+ for (let offset = 0; ; ) {
3242
+ const includeBootstrapData = Boolean(bootstrap && offset === 0);
3243
+ const requestLimit = includeBootstrapData ? Math.max(pageSize, initialPageSize) : pageSize;
3244
+ const page = await loadPage({
3245
+ categoryListKey: options.categoryListKey,
3246
+ attributeListKey: options.attributeListKey,
3247
+ sortBy,
3248
+ limit: requestLimit,
3249
+ offset,
3250
+ include: {
3251
+ products: true,
3252
+ detailVariants: true,
3253
+ categories: includeBootstrapData,
3254
+ attributes: true,
3255
+ filters: includeBootstrapData,
3256
+ brands: includeBootstrapData,
3257
+ debug: false
3258
+ }
3259
+ });
3260
+ if (offset === 0) firstPage = page;
3261
+ pageCount += 1;
3262
+ const pageTotal = page.pagination.total;
3263
+ if (expectedTotal === null) {
3264
+ expectedTotal = pageTotal;
3265
+ } else if (pageTotal !== expectedTotal) {
3266
+ throwCatalogChanged();
3267
+ }
3268
+ if (pageTotal > maxProducts) {
3269
+ throw new WebsiteError(
3270
+ `Static catalog contains ${pageTotal} products and exceeds maxProducts ${maxProducts}.`,
3271
+ "STATIC_CATALOG_LIMIT_EXCEEDED"
3272
+ );
3273
+ }
3274
+ products.push(...page.products);
3275
+ if (!page.pagination.hasMore) break;
3276
+ if (page.products.length === 0) {
3277
+ throw new WebsiteError(
3278
+ "Static catalog pagination did not advance.",
3279
+ "STATIC_CATALOG_PAGINATION_STALLED"
3280
+ );
3281
+ }
3282
+ offset += page.products.length;
3283
+ }
3284
+ const total = expectedTotal ?? 0;
3285
+ const uniqueProductIds = new Set(products.map((product) => product.id));
3286
+ if (products.length !== total || uniqueProductIds.size !== total) {
3287
+ throwCatalogChanged();
3288
+ }
3289
+ const snapshot = {
3290
+ schemaVersion: 1,
3291
+ snapshotId: `${websiteId}:${generatedAt}`,
3292
+ generatedAt,
3293
+ websiteId,
3294
+ products,
3295
+ total,
3296
+ pageSize,
3297
+ pageCount
2610
3298
  };
3299
+ const initialPage = bootstrap && firstPage ? {
3300
+ ...firstPage,
3301
+ products: firstPage.products.slice(0, initialPageSize),
3302
+ pagination: {
3303
+ ...firstPage.pagination,
3304
+ limit: initialPageSize,
3305
+ offset: 0,
3306
+ hasMore: total > initialPageSize
3307
+ }
3308
+ } : null;
3309
+ return { snapshot, initialPage };
3310
+ }
3311
+ function throwCatalogChanged() {
3312
+ throw new WebsiteError(
3313
+ "Static catalog changed while it was being collected. Retry the build to get a consistent snapshot.",
3314
+ "STATIC_CATALOG_CHANGED_DURING_BUILD"
3315
+ );
2611
3316
  }
2612
3317
 
2613
3318
  // libs/sdk/src/modules/catalog.ts
2614
3319
  var MAX_AVAILABILITY_VARIANT_IDS2 = 100;
2615
3320
  var DEFAULT_FILTER_ATTRIBUTE_LIST_KEY = "atributos";
3321
+ var MAX_LIVE_STATE_VARIANT_IDS = 100;
3322
+ var STATIC_CATALOG_RETRY = {
3323
+ maxAttempts: 4,
3324
+ maxTotalWaitMs: 3 * 60 * 1e3,
3325
+ jitterRatio: 0.1
3326
+ };
2616
3327
  function mapPrice(raw) {
2617
3328
  return {
2618
3329
  id: raw.id,
@@ -2645,6 +3356,9 @@ function getProductImages(media) {
2645
3356
  function getProductVideos(media) {
2646
3357
  return getProductMediaByKind(media, "video");
2647
3358
  }
3359
+ function getProductDetailVariants(product) {
3360
+ return product.detailVariants ?? product.variants;
3361
+ }
2648
3362
  function getPrimaryProductImage(media) {
2649
3363
  return getProductImages(media)[0] ?? null;
2650
3364
  }
@@ -2802,6 +3516,9 @@ function mapRelatedProduct(raw, storageUrl) {
2802
3516
  };
2803
3517
  }
2804
3518
  function mapProduct(raw, storageUrl, labels, chainId) {
3519
+ const detailVariants = raw.product_variants.map((variant) => mapVariant(variant, labels, chainId)).filter((variant) => variant.pricing.currentPrice != null);
3520
+ const listingVariantIds = raw.listing_variant_ids ? new Set(raw.listing_variant_ids) : null;
3521
+ const variants = listingVariantIds ? detailVariants.filter((variant) => listingVariantIds.has(variant.id)) : detailVariants;
2805
3522
  return {
2806
3523
  id: raw.id,
2807
3524
  name: raw.name,
@@ -2818,9 +3535,8 @@ function mapProduct(raw, storageUrl, labels, chainId) {
2818
3535
  measurementUnit: raw.measurement_unit,
2819
3536
  createdAt: raw.created_at,
2820
3537
  updatedAt: raw.updated_at ?? raw.created_at,
2821
- variants: raw.product_variants.map(
2822
- (variant) => mapVariant(variant, labels, chainId)
2823
- ),
3538
+ variants,
3539
+ detailVariants,
2824
3540
  media: raw.product_media.map((m) => mapMedia(m, storageUrl)),
2825
3541
  attributes: raw.product_attributes.map(
2826
3542
  (attribute) => mapProductAttribute(attribute, labels)
@@ -3263,15 +3979,171 @@ function createCatalogModule(graphqlUrl, getHeaders, getContext, storageUrl) {
3263
3979
  const publicAttributeLabels = buildPublicAttributeLabels(
3264
3980
  data.catalog_attribute_lists
3265
3981
  );
3982
+ const mappedProduct = mapProduct(
3983
+ product,
3984
+ storageUrl,
3985
+ publicAttributeLabels,
3986
+ ctx.chainId
3987
+ );
3266
3988
  const filteredProducts = await filterProductsForWebsiteStock(
3267
3989
  graphqlUrl,
3268
3990
  getHeaders,
3269
- [mapProduct(product, storageUrl, publicAttributeLabels, ctx.chainId)],
3991
+ [mappedProduct],
3270
3992
  ctx.chainId
3271
3993
  );
3272
- return filteredProducts[0] ?? null;
3994
+ if (!filteredProducts[0]) return null;
3995
+ return {
3996
+ ...mappedProduct,
3997
+ variants: getProductDetailVariants(mappedProduct)
3998
+ };
3999
+ };
4000
+ const loadCatalogPage = async (opts, retryRateLimits = false) => {
4001
+ const ctx = getContext();
4002
+ if (!ctx.chainId) {
4003
+ throw new WebsiteError(
4004
+ "This website has no chain configured. Catalog requires a chain_id.",
4005
+ "NO_CHAIN"
4006
+ );
4007
+ }
4008
+ const chainId = ctx.chainId;
4009
+ const filters = {
4010
+ category_slug: opts.categorySlug,
4011
+ subcategory_slug: opts.subcategorySlug,
4012
+ tags: opts.tags,
4013
+ search: opts.search,
4014
+ brand_id: opts.brandId,
4015
+ min_price: opts.minPrice,
4016
+ max_price: opts.maxPrice,
4017
+ attributes_by_slug: opts.attributesBySlug,
4018
+ sort_by: opts.sortBy,
4019
+ limit: opts.limit,
4020
+ offset: opts.offset
4021
+ };
4022
+ const data = await executeGraphQL(
4023
+ graphqlUrl,
4024
+ WEBSITE_GET_CATALOG_PAGE,
4025
+ {
4026
+ websiteId: ctx.websiteId,
4027
+ categoryListKey: opts.categoryListKey,
4028
+ attributeListKey: opts.attributeListKey,
4029
+ filters,
4030
+ include: opts.include
4031
+ },
4032
+ getHeaders,
4033
+ retryRateLimits ? { retry: STATIC_CATALOG_RETRY } : void 0
4034
+ );
4035
+ if (!data.result.success) {
4036
+ throw new WebsiteError(
4037
+ data.result.errors.join("; ") || "Unable to resolve catalog page",
4038
+ "CATALOG_PAGE_ERROR"
4039
+ );
4040
+ }
4041
+ const products = parseActionJson(data.result.products, []);
4042
+ const categoryList = parseActionJson(
4043
+ data.result.category_list,
4044
+ null
4045
+ );
4046
+ const attributeList = parseActionJson(
4047
+ data.result.attribute_list,
4048
+ null
4049
+ );
4050
+ const filtersResult = parseActionJson(
4051
+ data.result.filters,
4052
+ null
4053
+ );
4054
+ const brands = parseActionJson(data.result.brands, []);
4055
+ const pagination = parseActionJson(
4056
+ data.result.pagination,
4057
+ {
4058
+ limit: opts.limit ?? 24,
4059
+ offset: opts.offset ?? 0,
4060
+ total: 0,
4061
+ hasMore: false
4062
+ }
4063
+ );
4064
+ const appliedFilters = parseActionJson(
4065
+ data.result.applied_filters,
4066
+ {}
4067
+ );
4068
+ const debug = parseActionJson(
4069
+ data.result.debug,
4070
+ null
4071
+ );
4072
+ const labels = buildPublicAttributeLabels(
4073
+ attributeList ? [attributeList] : []
4074
+ );
4075
+ return {
4076
+ products: products.map(
4077
+ (product) => mapProduct(product, storageUrl, labels, chainId)
4078
+ ),
4079
+ categories: categoryList ? mapCategoryList(categoryList) : null,
4080
+ attributes: attributeList ? mapAttributeList(attributeList) : null,
4081
+ filters: filtersResult,
4082
+ brands,
4083
+ pagination,
4084
+ appliedFilters,
4085
+ debug
4086
+ };
3273
4087
  };
3274
4088
  return {
4089
+ async getProductLiveState(productVariantIds) {
4090
+ const ctx = getContext();
4091
+ if (!ctx.chainId) {
4092
+ throw new WebsiteError(
4093
+ "This website has no chain configured. Product live state requires a chain_id.",
4094
+ "NO_CHAIN"
4095
+ );
4096
+ }
4097
+ const chainId = ctx.chainId;
4098
+ const variantIds = [...new Set(productVariantIds)];
4099
+ if (variantIds.length === 0) return [];
4100
+ if (variantIds.length > MAX_LIVE_STATE_VARIANT_IDS) {
4101
+ throw new WebsiteError(
4102
+ `Product live state accepts at most ${MAX_LIVE_STATE_VARIANT_IDS} variants per request.`,
4103
+ "PRODUCT_LIVE_STATE_LIMIT_EXCEEDED"
4104
+ );
4105
+ }
4106
+ const data = await executeGraphQL(
4107
+ graphqlUrl,
4108
+ GET_PRODUCT_LIVE_STATE,
4109
+ {
4110
+ variantIds,
4111
+ companyId: ctx.companyId,
4112
+ chainId,
4113
+ websiteId: ctx.websiteId
4114
+ },
4115
+ getHeaders
4116
+ );
4117
+ const variantsById = new Map(
4118
+ (data.product_variants ?? []).map((variant) => [variant.id, variant])
4119
+ );
4120
+ const availabilityById = new Map(
4121
+ (data.websiteProductAvailability ?? []).map((availability) => [
4122
+ availability.product_variant_id,
4123
+ availability
4124
+ ])
4125
+ );
4126
+ return variantIds.flatMap((variantId) => {
4127
+ const variant = variantsById.get(variantId);
4128
+ if (!variant) return [];
4129
+ const availability = availabilityById.get(variantId);
4130
+ const pricing = resolveProductPricing(
4131
+ selectPublicPrices(
4132
+ variant.product_variant_prices.map(mapPrice),
4133
+ chainId
4134
+ )
4135
+ );
4136
+ return [
4137
+ {
4138
+ productVariantId: variantId,
4139
+ inStock: availability?.in_stock ?? false,
4140
+ availabilityLabel: availability?.label ?? "Agotado",
4141
+ maxPurchasableQuantity: availability?.max_purchasable_quantity ?? 0,
4142
+ pricing
4143
+ }
4144
+ ];
4145
+ });
4146
+ },
3275
4147
  async getProductRecommendations(opts) {
3276
4148
  const ctx = getContext();
3277
4149
  const limit = opts.limit ?? 16;
@@ -3346,86 +4218,36 @@ function createCatalogModule(graphqlUrl, getHeaders, getContext, storageUrl) {
3346
4218
  }
3347
4219
  };
3348
4220
  },
3349
- async getCatalogPage(opts) {
4221
+ getCatalogPage: loadCatalogPage,
4222
+ async getStaticCatalogSnapshot(opts) {
3350
4223
  const ctx = getContext();
3351
- if (!ctx.chainId) {
3352
- throw new WebsiteError(
3353
- "This website has no chain configured. Catalog requires a chain_id.",
3354
- "NO_CHAIN"
3355
- );
3356
- }
3357
- const chainId = ctx.chainId;
3358
- const filters = {
3359
- category_slug: opts.categorySlug,
3360
- subcategory_slug: opts.subcategorySlug,
3361
- tags: opts.tags,
3362
- search: opts.search,
3363
- brand_id: opts.brandId,
3364
- min_price: opts.minPrice,
3365
- max_price: opts.maxPrice,
3366
- attributes_by_slug: opts.attributesBySlug,
3367
- sort_by: opts.sortBy,
3368
- limit: opts.limit,
3369
- offset: opts.offset
3370
- };
3371
- const data = await executeGraphQL(
3372
- graphqlUrl,
3373
- WEBSITE_GET_CATALOG_PAGE,
3374
- {
3375
- websiteId: ctx.websiteId,
3376
- categoryListKey: opts.categoryListKey,
3377
- attributeListKey: opts.attributeListKey,
3378
- filters,
3379
- include: opts.include
4224
+ return (await collectStaticCatalog({
4225
+ websiteId: ctx.websiteId,
4226
+ snapshot: opts,
4227
+ loadPage: (options) => loadCatalogPage(options, true)
4228
+ })).snapshot;
4229
+ },
4230
+ async getStaticCatalogBootstrap(opts) {
4231
+ const ctx = getContext();
4232
+ const { snapshot, initialPage } = await collectStaticCatalog({
4233
+ websiteId: ctx.websiteId,
4234
+ snapshot: opts,
4235
+ bootstrap: {
4236
+ initialPageSize: opts.initialPageSize,
4237
+ sortBy: opts.sortBy
3380
4238
  },
3381
- getHeaders
3382
- );
3383
- if (!data.result.success) {
4239
+ loadPage: (options) => loadCatalogPage(options, true)
4240
+ });
4241
+ if (!initialPage) {
3384
4242
  throw new WebsiteError(
3385
- data.result.errors.join("; ") || "Unable to resolve catalog page",
3386
- "CATALOG_PAGE_ERROR"
4243
+ "Static catalog bootstrap could not resolve its initial page.",
4244
+ "STATIC_CATALOG_BOOTSTRAP_MISSING_INITIAL_PAGE"
3387
4245
  );
3388
4246
  }
3389
- const products = parseActionJson(data.result.products, []);
3390
- const categoryList = parseActionJson(
3391
- data.result.category_list,
3392
- null
3393
- );
3394
- const attributeList = parseActionJson(
3395
- data.result.attribute_list,
3396
- null
3397
- );
3398
- const filtersResult = parseActionJson(
3399
- data.result.filters,
3400
- null
3401
- );
3402
- const brands = parseActionJson(data.result.brands, []);
3403
- const pagination = parseActionJson(
3404
- data.result.pagination,
3405
- { limit: opts.limit ?? 24, offset: opts.offset ?? 0, total: 0, hasMore: false }
3406
- );
3407
- const appliedFilters = parseActionJson(
3408
- data.result.applied_filters,
3409
- {}
3410
- );
3411
- const debug = parseActionJson(
3412
- data.result.debug,
3413
- null
3414
- );
3415
- const labels = buildPublicAttributeLabels(
3416
- attributeList ? [attributeList] : []
3417
- );
3418
4247
  return {
3419
- products: products.map(
3420
- (product) => mapProduct(product, storageUrl, labels, chainId)
3421
- ),
3422
- categories: categoryList ? mapCategoryList(categoryList) : null,
3423
- attributes: attributeList ? mapAttributeList(attributeList) : null,
3424
- filters: filtersResult,
3425
- brands,
3426
- pagination,
3427
- appliedFilters,
3428
- debug
4248
+ ...snapshot,
4249
+ bootstrapSchemaVersion: 1,
4250
+ initialPage
3429
4251
  };
3430
4252
  },
3431
4253
  async getProducts(opts) {
@@ -3892,6 +4714,7 @@ function mapCouponResult(raw) {
3892
4714
  };
3893
4715
  }
3894
4716
  function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionStorage) {
4717
+ let shippingPolicyCache = null;
3895
4718
  return {
3896
4719
  async getLocationOptions() {
3897
4720
  const context = getContext();
@@ -3989,22 +4812,39 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
3989
4812
  }));
3990
4813
  },
3991
4814
  async getShippingPolicy() {
4815
+ const now = Date.now();
4816
+ if (shippingPolicyCache && shippingPolicyCache.expiresAt > now) {
4817
+ return shippingPolicyCache.request;
4818
+ }
3992
4819
  const context = getContext();
3993
- const data = await executeGraphQL(
4820
+ const request = executeGraphQL(
3994
4821
  graphqlUrl,
3995
4822
  GET_SHIPPING_POLICY,
3996
4823
  { websiteId: context.websiteId },
3997
4824
  getHeaders
3998
- );
3999
- const result = data.websiteGetShippingPolicy;
4000
- if (!result.success || !result.policy) {
4001
- throw new CheckoutError(result.errors ?? ["SHIPPING_POLICY_FAILED"]);
4002
- }
4003
- return {
4004
- freeShippingEnabled: result.policy.free_shipping_enabled,
4005
- freeShippingThreshold: result.policy.free_shipping_threshold,
4006
- currency: result.policy.currency
4825
+ ).then((data) => {
4826
+ const result = data.websiteGetShippingPolicy;
4827
+ if (!result.success || !result.policy) {
4828
+ throw new CheckoutError(result.errors ?? ["SHIPPING_POLICY_FAILED"]);
4829
+ }
4830
+ return {
4831
+ freeShippingEnabled: result.policy.free_shipping_enabled,
4832
+ freeShippingThreshold: result.policy.free_shipping_threshold,
4833
+ currency: result.policy.currency
4834
+ };
4835
+ });
4836
+ shippingPolicyCache = {
4837
+ expiresAt: now + 5 * 60 * 1e3,
4838
+ request
4007
4839
  };
4840
+ try {
4841
+ return await request;
4842
+ } catch (error) {
4843
+ if (shippingPolicyCache?.request === request) {
4844
+ shippingPolicyCache = null;
4845
+ }
4846
+ throw error;
4847
+ }
4008
4848
  },
4009
4849
  async getFreeShippingProgress(input) {
4010
4850
  if (input.items.length === 0) {
@@ -4170,6 +5010,13 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
4170
5010
  initialPayment: input.initialPayment ? {
4171
5011
  amount: input.initialPayment.amount,
4172
5012
  payment_method_id: input.initialPayment.paymentMethodId
5013
+ } : void 0,
5014
+ marketingAttribution: input.marketingAttribution ? serializeWebsiteMarketingAttribution(input.marketingAttribution) : void 0,
5015
+ advertisingConsent: input.advertisingConsent ? {
5016
+ policy_id: input.advertisingConsent.policyId,
5017
+ policy_version: input.advertisingConsent.policyVersion,
5018
+ statement_hash: input.advertisingConsent.statementHash,
5019
+ granted: input.advertisingConsent.granted
4173
5020
  } : void 0
4174
5021
  },
4175
5022
  getHeaders
@@ -4305,18 +5152,23 @@ function readOptionalText(value) {
4305
5152
  function enrichMediaValue(value, storageUrl) {
4306
5153
  const fileId = typeof value.fileId === "string" && value.fileId.trim() ? value.fileId.trim() : "";
4307
5154
  if (!fileId) return value;
4308
- const urls = buildStorageMediaUrls(storageUrl, fileId);
5155
+ const isProductMedia = typeof value.id === "string" && typeof value.order === "number";
5156
+ const isEditorialImage = value.mediaKind === "image" && !isProductMedia;
5157
+ const originalWidth = typeof value.originalWidth === "number" ? value.originalWidth : null;
5158
+ const urls = isEditorialImage ? buildStorageContentMediaUrls(storageUrl, fileId, originalWidth) : buildStorageMediaUrls(storageUrl, fileId);
4309
5159
  return {
4310
5160
  ...value,
4311
5161
  ...urls,
4312
- url: value.url ?? urls.originalUrl
5162
+ url: value.url ?? urls.originalUrl,
5163
+ src: value.src ?? value.url ?? urls.originalUrl
4313
5164
  };
4314
5165
  }
4315
5166
  function normalizeRichTextNode(value, storageUrl) {
4316
5167
  const type = value.type;
4317
5168
  const attrs = isRecord(value.attrs) ? value.attrs : null;
4318
5169
  if (attrs && (type === "bacanoImage" || type === "bacanoVideo") && typeof attrs.fileId === "string") {
4319
- const urls = buildStorageMediaUrls(storageUrl, attrs.fileId);
5170
+ const originalWidth = typeof attrs.originalWidth === "number" ? attrs.originalWidth : null;
5171
+ const urls = type === "bacanoImage" ? buildStorageContentMediaUrls(storageUrl, attrs.fileId, originalWidth) : buildStorageMediaUrls(storageUrl, attrs.fileId);
4320
5172
  const altText = readOptionalText(attrs.altText) ?? readOptionalText(attrs.alt);
4321
5173
  const caption = readOptionalText(attrs.caption);
4322
5174
  const showCaption = Boolean(caption) && attrs.showCaption !== false;
@@ -4448,6 +5300,452 @@ function createContentModule(graphqlUrl, getHeaders, getContext, storageUrl) {
4448
5300
  };
4449
5301
  }
4450
5302
 
5303
+ // libs/sdk/src/marketing/clarity.ts
5304
+ var initializedDestinations = /* @__PURE__ */ new Set();
5305
+ function clarityWindow() {
5306
+ return typeof window === "undefined" ? null : window;
5307
+ }
5308
+ function prepareClarity(projectId) {
5309
+ const target = clarityWindow();
5310
+ if (!target || initializedDestinations.has(projectId)) return;
5311
+ if (!target.clarity) {
5312
+ const clarity = function() {
5313
+ clarity.q ??= [];
5314
+ clarity.q.push(arguments);
5315
+ };
5316
+ target.clarity = clarity;
5317
+ }
5318
+ initializedDestinations.add(projectId);
5319
+ }
5320
+ function loadClarityScript(projectId) {
5321
+ if (typeof document === "undefined") return;
5322
+ const id = `bacano-clarity-${projectId}`;
5323
+ if (document.getElementById(id)) return;
5324
+ const script = document.createElement("script");
5325
+ script.id = id;
5326
+ script.async = true;
5327
+ script.src = `https://www.clarity.ms/tag/${encodeURIComponent(projectId)}`;
5328
+ script.referrerPolicy = "strict-origin-when-cross-origin";
5329
+ document.head.appendChild(script);
5330
+ }
5331
+ function sendClarityEvent(eventName) {
5332
+ clarityWindow()?.clarity?.("event", eventName);
5333
+ }
5334
+
5335
+ // libs/sdk/src/marketing/domain.ts
5336
+ function matchesCanonicalMarketingHost(canonicalDomain, currentHostname) {
5337
+ if (canonicalDomain === void 0) return true;
5338
+ if (!canonicalDomain?.trim()) return false;
5339
+ try {
5340
+ const configuredHost = new URL(
5341
+ canonicalDomain.includes("://") ? canonicalDomain : `https://${canonicalDomain}`
5342
+ ).hostname.toLowerCase();
5343
+ return currentHostname.toLowerCase() === configuredHost;
5344
+ } catch {
5345
+ return false;
5346
+ }
5347
+ }
5348
+
5349
+ // libs/sdk/src/marketing/ga4.ts
5350
+ function mapGa4Items(items) {
5351
+ return items?.map((item) => ({
5352
+ item_id: item.itemId,
5353
+ item_name: item.itemName,
5354
+ item_variant: item.itemVariant,
5355
+ item_brand: item.itemBrand,
5356
+ item_category: item.itemCategory,
5357
+ price: item.price,
5358
+ quantity: item.quantity,
5359
+ index: item.index,
5360
+ sku: item.sku
5361
+ }));
5362
+ }
5363
+
5364
+ // libs/sdk/src/marketing/types.ts
5365
+ var MARKETING_SCHEMA_VERSION = 1;
5366
+ var WEBSITE_MARKETING_CONTACT_PLACEMENTS = [
5367
+ "product_detail",
5368
+ "floating",
5369
+ "header",
5370
+ "footer",
5371
+ "contact_page"
5372
+ ];
5373
+
5374
+ // libs/sdk/src/marketing/runtime.ts
5375
+ var initializedGa4Destinations = /* @__PURE__ */ new Set();
5376
+ var initializedMetaDestinations = /* @__PURE__ */ new Set();
5377
+ function marketingWindow() {
5378
+ return typeof window === "undefined" ? null : window;
5379
+ }
5380
+ function loadScriptOnce(id, source) {
5381
+ if (typeof document === "undefined" || document.getElementById(id)) return;
5382
+ const script = document.createElement("script");
5383
+ script.id = id;
5384
+ script.async = true;
5385
+ script.src = source;
5386
+ script.referrerPolicy = "strict-origin-when-cross-origin";
5387
+ document.head.appendChild(script);
5388
+ }
5389
+ function prepareGa4(measurementId) {
5390
+ const target = marketingWindow();
5391
+ if (!target) return;
5392
+ if (initializedGa4Destinations.has(measurementId)) return;
5393
+ target.dataLayer ??= [];
5394
+ target.gtag ??= function() {
5395
+ target.dataLayer?.push(arguments);
5396
+ };
5397
+ target.gtag("js", /* @__PURE__ */ new Date());
5398
+ target.gtag("config", measurementId, { send_page_view: false });
5399
+ initializedGa4Destinations.add(measurementId);
5400
+ }
5401
+ function loadGa4Script(measurementId) {
5402
+ loadScriptOnce(
5403
+ `bacano-ga4-${measurementId}`,
5404
+ `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`
5405
+ );
5406
+ }
5407
+ function captureGa4ClientId(measurementId, websiteId, diagnostic) {
5408
+ const target = marketingWindow();
5409
+ target?.gtag?.("get", measurementId, "client_id", (clientId) => {
5410
+ if (storeWebsiteMarketingGa4ClientId(websiteId, clientId)) return;
5411
+ diagnostic?.({
5412
+ level: "warning",
5413
+ code: "GA4_CLIENT_ID_UNAVAILABLE",
5414
+ message: "GA4 did not provide a valid browser client identifier"
5415
+ });
5416
+ });
5417
+ }
5418
+ function captureGa4SessionId(measurementId, websiteId, diagnostic) {
5419
+ const target = marketingWindow();
5420
+ target?.gtag?.("get", measurementId, "session_id", (sessionId) => {
5421
+ if (storeWebsiteMarketingGa4SessionId(websiteId, sessionId)) return;
5422
+ diagnostic?.({
5423
+ level: "warning",
5424
+ code: "GA4_SESSION_ID_UNAVAILABLE",
5425
+ message: "GA4 did not provide a valid browser session identifier"
5426
+ });
5427
+ });
5428
+ }
5429
+ function prepareMeta(pixelId) {
5430
+ const target = marketingWindow();
5431
+ if (!target) return;
5432
+ if (initializedMetaDestinations.has(pixelId)) return;
5433
+ if (!target.fbq) {
5434
+ const queue = [];
5435
+ const fbq = ((...args) => {
5436
+ if (fbq.callMethod) {
5437
+ fbq.callMethod(...args);
5438
+ return;
5439
+ }
5440
+ queue.push(args);
5441
+ });
5442
+ fbq.push = fbq;
5443
+ fbq.loaded = true;
5444
+ fbq.version = "2.0";
5445
+ fbq.queue = queue;
5446
+ target.fbq = fbq;
5447
+ target._fbq = fbq;
5448
+ }
5449
+ target.fbq("init", pixelId);
5450
+ initializedMetaDestinations.add(pixelId);
5451
+ }
5452
+ function loadMetaScript() {
5453
+ loadScriptOnce(
5454
+ "bacano-meta-pixel",
5455
+ "https://connect.facebook.net/en_US/fbevents.js"
5456
+ );
5457
+ }
5458
+ function scheduleScriptLoad(load, strategy) {
5459
+ const target = marketingWindow();
5460
+ if (!target || strategy !== "idle") {
5461
+ load();
5462
+ return () => void 0;
5463
+ }
5464
+ let completed = false;
5465
+ let idleHandle = null;
5466
+ let timeoutHandle = null;
5467
+ const interactionEvents = ["pointerdown", "keydown", "touchstart"];
5468
+ const cleanup = () => {
5469
+ for (const eventName of interactionEvents) {
5470
+ target.removeEventListener(eventName, finish, true);
5471
+ }
5472
+ if (idleHandle !== null) target.cancelIdleCallback?.(idleHandle);
5473
+ if (timeoutHandle !== null) target.clearTimeout(timeoutHandle);
5474
+ };
5475
+ const finish = () => {
5476
+ if (completed) return;
5477
+ completed = true;
5478
+ cleanup();
5479
+ load();
5480
+ };
5481
+ for (const eventName of interactionEvents) {
5482
+ target.addEventListener(eventName, finish, {
5483
+ capture: true,
5484
+ once: true,
5485
+ passive: true
5486
+ });
5487
+ }
5488
+ if (target.requestIdleCallback) {
5489
+ idleHandle = target.requestIdleCallback(finish, { timeout: 2e3 });
5490
+ } else {
5491
+ timeoutHandle = target.setTimeout(finish, 1500);
5492
+ }
5493
+ return cleanup;
5494
+ }
5495
+ function metaEventName(eventName) {
5496
+ const standard = {
5497
+ page_view: "PageView",
5498
+ search: "Search",
5499
+ view_item: "ViewContent",
5500
+ add_to_cart: "AddToCart",
5501
+ begin_checkout: "InitiateCheckout",
5502
+ add_payment_info: "AddPaymentInfo",
5503
+ contact: "Contact"
5504
+ };
5505
+ return standard[eventName] ?? eventName;
5506
+ }
5507
+ function isWebsiteMarketingContactPlacement(value) {
5508
+ return WEBSITE_MARKETING_CONTACT_PLACEMENTS.some(
5509
+ (candidate) => candidate === value
5510
+ );
5511
+ }
5512
+ function contactPlacementFor(event) {
5513
+ const placement = event.contactPlacement;
5514
+ if (event.eventName !== "contact" || !isWebsiteMarketingContactPlacement(placement)) {
5515
+ return void 0;
5516
+ }
5517
+ return placement;
5518
+ }
5519
+ function sendGa4(event, diagnosticMode) {
5520
+ const target = marketingWindow();
5521
+ const contactPlacement = contactPlacementFor(event);
5522
+ target?.gtag?.("event", event.eventName, {
5523
+ currency: event.currency,
5524
+ value: event.value,
5525
+ search_term: event.searchTerm,
5526
+ item_list_id: event.itemListId,
5527
+ item_list_name: event.itemListName,
5528
+ shipping_tier: event.shippingTier,
5529
+ payment_type: event.paymentType,
5530
+ method: event.contactMethod,
5531
+ ...contactPlacement ? { contact_placement: contactPlacement } : void 0,
5532
+ page_location: event.pageLocation,
5533
+ page_title: event.pageTitle,
5534
+ items: mapGa4Items(event.items),
5535
+ event_id: event.eventId,
5536
+ debug_mode: diagnosticMode ? true : void 0
5537
+ });
5538
+ }
5539
+ function sendMeta(event) {
5540
+ const target = marketingWindow();
5541
+ const eventName = metaEventName(event.eventName);
5542
+ const isStandard = eventName !== event.eventName;
5543
+ const contactPlacement = contactPlacementFor(event);
5544
+ target?.fbq?.(
5545
+ isStandard ? "track" : "trackCustom",
5546
+ eventName,
5547
+ {
5548
+ currency: event.currency,
5549
+ value: event.value,
5550
+ search_string: event.searchTerm,
5551
+ content_ids: event.items?.map((item) => item.itemId),
5552
+ contents: event.items?.map((item) => ({
5553
+ id: item.itemId,
5554
+ quantity: item.quantity ?? 1,
5555
+ item_price: item.price
5556
+ })),
5557
+ content_type: event.items?.length ? "product" : void 0,
5558
+ contact_method: event.contactMethod,
5559
+ ...contactPlacement ? { contact_placement: contactPlacement } : void 0
5560
+ },
5561
+ { eventID: event.eventId }
5562
+ );
5563
+ }
5564
+ function newEventId() {
5565
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`;
5566
+ }
5567
+ function createWebsiteMarketingRuntime(options) {
5568
+ let destroyed = false;
5569
+ let cancelScheduledLoad = () => void 0;
5570
+ const target = marketingWindow();
5571
+ const enabled = options.config.enabled && Boolean(
5572
+ target && matchesCanonicalMarketingHost(
5573
+ options.canonicalDomain,
5574
+ target.location.hostname
5575
+ )
5576
+ ) && Boolean(
5577
+ options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity
5578
+ );
5579
+ if (enabled) {
5580
+ try {
5581
+ if (options.config.destinations.ga4) {
5582
+ prepareGa4(options.config.destinations.ga4.measurementId);
5583
+ }
5584
+ if (options.config.destinations.meta) {
5585
+ prepareMeta(options.config.destinations.meta.pixelId);
5586
+ }
5587
+ if (options.config.destinations.clarity) {
5588
+ prepareClarity(options.config.destinations.clarity.projectId);
5589
+ }
5590
+ cancelScheduledLoad = scheduleScriptLoad(() => {
5591
+ if (destroyed) return;
5592
+ if (options.config.destinations.ga4) {
5593
+ loadGa4Script(options.config.destinations.ga4.measurementId);
5594
+ }
5595
+ if (options.config.destinations.meta) loadMetaScript();
5596
+ if (options.config.destinations.clarity) {
5597
+ loadClarityScript(options.config.destinations.clarity.projectId);
5598
+ }
5599
+ }, options.scriptLoadStrategy);
5600
+ captureWebsiteMarketingAttribution(
5601
+ options.websiteId,
5602
+ options.config.attributionWindowDays
5603
+ );
5604
+ if (options.config.destinations.ga4) {
5605
+ captureGa4ClientId(
5606
+ options.config.destinations.ga4.measurementId,
5607
+ options.websiteId,
5608
+ options.diagnostic
5609
+ );
5610
+ captureGa4SessionId(
5611
+ options.config.destinations.ga4.measurementId,
5612
+ options.websiteId,
5613
+ options.diagnostic
5614
+ );
5615
+ }
5616
+ if (options.config.destinations.meta) {
5617
+ refreshWebsiteMarketingMetaContext(options.websiteId);
5618
+ }
5619
+ } catch (error) {
5620
+ options.diagnostic?.({
5621
+ level: "warning",
5622
+ code: "RUNTIME_INITIALIZATION_FAILED",
5623
+ message: error instanceof Error ? error.message : "Initialization failed"
5624
+ });
5625
+ }
5626
+ }
5627
+ return {
5628
+ enabled,
5629
+ track(input) {
5630
+ if (!enabled || destroyed) return;
5631
+ const event = {
5632
+ ...input,
5633
+ schemaVersion: 1,
5634
+ eventId: input.eventId ?? newEventId(),
5635
+ occurredAt: input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
5636
+ currency: input.currency ?? options.config.currencyCode,
5637
+ pageLocation: sanitizeMarketingUrl(
5638
+ input.pageLocation ?? (typeof window === "undefined" ? "" : window.location.href)
5639
+ )
5640
+ };
5641
+ try {
5642
+ if (options.config.destinations.ga4) {
5643
+ sendGa4(event, options.config.destinations.ga4.diagnosticMode);
5644
+ }
5645
+ if (options.config.destinations.meta) sendMeta(event);
5646
+ if (options.config.destinations.clarity) {
5647
+ sendClarityEvent(event.eventName);
5648
+ }
5649
+ options.diagnostic?.({
5650
+ level: "debug",
5651
+ code: "EVENT_DISPATCHED",
5652
+ message: "Marketing event dispatched",
5653
+ eventName: event.eventName
5654
+ });
5655
+ } catch (error) {
5656
+ options.diagnostic?.({
5657
+ level: "warning",
5658
+ code: "EVENT_DISPATCH_FAILED",
5659
+ message: error instanceof Error ? error.message : "Dispatch failed",
5660
+ eventName: event.eventName
5661
+ });
5662
+ }
5663
+ },
5664
+ captureAttribution() {
5665
+ if (!enabled) return null;
5666
+ captureWebsiteMarketingAttribution(
5667
+ options.websiteId,
5668
+ options.config.attributionWindowDays
5669
+ );
5670
+ if (options.config.destinations.meta) {
5671
+ refreshWebsiteMarketingMetaContext(options.websiteId);
5672
+ }
5673
+ return getWebsiteMarketingAttribution(options.websiteId);
5674
+ },
5675
+ getAttribution() {
5676
+ if (!enabled) return null;
5677
+ if (options.config.destinations.meta) {
5678
+ refreshWebsiteMarketingMetaContext(options.websiteId);
5679
+ }
5680
+ return getWebsiteMarketingAttribution(options.websiteId);
5681
+ },
5682
+ destroy() {
5683
+ destroyed = true;
5684
+ cancelScheduledLoad();
5685
+ }
5686
+ };
5687
+ }
5688
+
5689
+ // libs/sdk/src/modules/marketing.ts
5690
+ function readRetryAfter(response) {
5691
+ const value = Number(response.headers.get("Retry-After") ?? "1");
5692
+ return Number.isFinite(value) && value >= 0 ? Math.ceil(value) : 1;
5693
+ }
5694
+ function createMarketingModule(apiUrl, websiteSlug, getContext) {
5695
+ return {
5696
+ async captureLead(input) {
5697
+ const context = getContext();
5698
+ const attribution = getWebsiteMarketingAttribution(context.websiteId);
5699
+ let response;
5700
+ try {
5701
+ response = await fetch(
5702
+ `${apiUrl.replace(/\/+$/, "")}/api/v1/website/campaign-captures`,
5703
+ {
5704
+ method: "POST",
5705
+ headers: { "Content-Type": "application/json" },
5706
+ body: JSON.stringify({
5707
+ websiteSlug,
5708
+ campaignKey: input.campaignKey,
5709
+ idempotencyKey: input.idempotencyKey,
5710
+ source: input.source ?? "WEBSITE",
5711
+ commercialConsent: input.commercialConsent,
5712
+ contact: input.contact,
5713
+ answers: input.answers ?? {},
5714
+ attribution: attribution ? serializeWebsiteMarketingAttribution(attribution) : null
5715
+ })
5716
+ }
5717
+ );
5718
+ } catch (error) {
5719
+ throw new WebsiteError(
5720
+ `Could not capture the campaign lead: ${error instanceof Error ? error.message : String(error)}`,
5721
+ "NETWORK_ERROR"
5722
+ );
5723
+ }
5724
+ if (response.status === 429) {
5725
+ throw new RateLimitError(
5726
+ "Rate limit exceeded. Please try again later.",
5727
+ readRetryAfter(response)
5728
+ );
5729
+ }
5730
+ if (!response.ok) {
5731
+ throw new WebsiteError(
5732
+ `Could not capture the campaign lead (status ${response.status})`,
5733
+ response.status === 403 ? "ORIGIN_NOT_ALLOWED" : "INVALID_RESPONSE",
5734
+ { status: response.status }
5735
+ );
5736
+ }
5737
+ const result = await response.json();
5738
+ if (typeof result.accepted !== "boolean" || typeof result.message !== "string") {
5739
+ throw new WebsiteError(
5740
+ "Campaign capture returned an invalid response",
5741
+ "INVALID_RESPONSE"
5742
+ );
5743
+ }
5744
+ return { accepted: result.accepted, message: result.message };
5745
+ }
5746
+ };
5747
+ }
5748
+
4451
5749
  // libs/sdk/src/modules/orders.ts
4452
5750
  function mapOrderItem(raw) {
4453
5751
  return {
@@ -4622,13 +5920,13 @@ function createOrdersModule(graphqlUrl, getHeaders, getContext) {
4622
5920
  )) {
4623
5921
  return order;
4624
5922
  }
4625
- await wait(intervalMs);
5923
+ await wait2(intervalMs);
4626
5924
  }
4627
5925
  throw new CheckoutError(["PAYMENT_STATUS_TIMEOUT"]);
4628
5926
  }
4629
5927
  };
4630
5928
  }
4631
- function wait(ms) {
5929
+ function wait2(ms) {
4632
5930
  return new Promise((resolve) => {
4633
5931
  setTimeout(resolve, ms);
4634
5932
  });
@@ -4787,6 +6085,9 @@ function createProfileModule(graphqlUrl, getHeaders) {
4787
6085
  }
4788
6086
 
4789
6087
  // libs/sdk/src/modules/website.ts
6088
+ var WEBSITE_CONTEXT_CACHE_TTL_MS = 5 * 60 * 1e3;
6089
+ var WEBSITE_CONTEXT_CACHE_MAX_ENTRIES = 100;
6090
+ var websiteContextCache = /* @__PURE__ */ new Map();
4790
6091
  function mapWebsite(raw) {
4791
6092
  return {
4792
6093
  websiteId: raw.id,
@@ -4800,10 +6101,7 @@ function mapWebsite(raw) {
4800
6101
  }
4801
6102
  async function resolveWebsite(graphqlUrl, slug, headers) {
4802
6103
  if (!slug.trim()) {
4803
- throw new WebsiteError(
4804
- "websiteSlug is required",
4805
- "INVALID_CONFIG"
4806
- );
6104
+ throw new WebsiteError("websiteSlug is required", "INVALID_CONFIG");
4807
6105
  }
4808
6106
  const data = await executeGraphQL(
4809
6107
  graphqlUrl,
@@ -4819,6 +6117,31 @@ async function resolveWebsite(graphqlUrl, slug, headers) {
4819
6117
  }
4820
6118
  return mapWebsite(data.websites[0]);
4821
6119
  }
6120
+ function resolveWebsiteCached(graphqlUrl, slug, headers) {
6121
+ const normalizedSlug = slug.trim();
6122
+ const cacheKey = `${graphqlUrl}::${normalizedSlug}`;
6123
+ const now = Date.now();
6124
+ const cached = websiteContextCache.get(cacheKey);
6125
+ if (cached && cached.expiresAt > now) return cached.request;
6126
+ if (cached) websiteContextCache.delete(cacheKey);
6127
+ const request = resolveWebsite(graphqlUrl, normalizedSlug, headers).catch(
6128
+ (error) => {
6129
+ if (websiteContextCache.get(cacheKey)?.request === request) {
6130
+ websiteContextCache.delete(cacheKey);
6131
+ }
6132
+ throw error;
6133
+ }
6134
+ );
6135
+ if (websiteContextCache.size >= WEBSITE_CONTEXT_CACHE_MAX_ENTRIES) {
6136
+ const oldestKey = websiteContextCache.keys().next().value;
6137
+ if (oldestKey) websiteContextCache.delete(oldestKey);
6138
+ }
6139
+ websiteContextCache.set(cacheKey, {
6140
+ expiresAt: now + WEBSITE_CONTEXT_CACHE_TTL_MS,
6141
+ request
6142
+ });
6143
+ return request;
6144
+ }
4822
6145
 
4823
6146
  // libs/sdk/src/client.ts
4824
6147
  function normalizeApiUrl(apiUrl) {
@@ -4829,10 +6152,17 @@ function createBacanoClient(config) {
4829
6152
  throw new WebsiteError("apiUrl is required", "INVALID_CONFIG");
4830
6153
  if (!config.websiteSlug?.trim())
4831
6154
  throw new WebsiteError("websiteSlug is required", "INVALID_CONFIG");
6155
+ if (config.initialContext && config.initialContext.slug.trim() !== config.websiteSlug.trim()) {
6156
+ throw new WebsiteError(
6157
+ "initialContext slug must match websiteSlug",
6158
+ "INVALID_CONFIG"
6159
+ );
6160
+ }
4832
6161
  const apiUrl = normalizeApiUrl(config.apiUrl);
4833
6162
  const graphqlUrl = `${apiUrl}/api/v1/website/graphql`;
4834
6163
  const storageUrl = `${apiUrl}/storage/v1`;
4835
- let context = null;
6164
+ let context = config.initialContext ? { ...config.initialContext } : null;
6165
+ let initialization = null;
4836
6166
  function getContext() {
4837
6167
  if (!context) throw new NotInitializedError();
4838
6168
  return context;
@@ -4877,13 +6207,27 @@ function createBacanoClient(config) {
4877
6207
  getContext,
4878
6208
  storageUrl
4879
6209
  );
6210
+ const marketingModule = createMarketingModule(
6211
+ apiUrl,
6212
+ config.websiteSlug,
6213
+ getContext
6214
+ );
4880
6215
  return {
4881
6216
  async init() {
4882
- context = await resolveWebsite(
4883
- graphqlUrl,
4884
- config.websiteSlug,
4885
- getWebsiteHeaders
4886
- );
6217
+ if (context) return;
6218
+ if (!initialization) {
6219
+ initialization = resolveWebsiteCached(
6220
+ graphqlUrl,
6221
+ config.websiteSlug,
6222
+ getWebsiteHeaders
6223
+ ).then((resolvedContext) => {
6224
+ context = resolvedContext;
6225
+ }).catch((error) => {
6226
+ initialization = null;
6227
+ throw error;
6228
+ });
6229
+ }
6230
+ await initialization;
4887
6231
  },
4888
6232
  getContext,
4889
6233
  auth: {
@@ -4898,8 +6242,11 @@ function createBacanoClient(config) {
4898
6242
  },
4899
6243
  catalog: {
4900
6244
  getCatalogPage: catalogModule.getCatalogPage.bind(catalogModule),
6245
+ getStaticCatalogSnapshot: catalogModule.getStaticCatalogSnapshot.bind(catalogModule),
6246
+ getStaticCatalogBootstrap: catalogModule.getStaticCatalogBootstrap.bind(catalogModule),
4901
6247
  getProductSeoEntries: catalogModule.getProductSeoEntries.bind(catalogModule),
4902
6248
  getProductRecommendations: catalogModule.getProductRecommendations.bind(catalogModule),
6249
+ getProductLiveState: catalogModule.getProductLiveState.bind(catalogModule),
4903
6250
  getProducts: catalogModule.getProducts.bind(catalogModule),
4904
6251
  getProduct: catalogModule.getProduct.bind(catalogModule),
4905
6252
  getProductBySlug: catalogModule.getProductBySlug.bind(catalogModule),
@@ -4951,6 +6298,9 @@ function createBacanoClient(config) {
4951
6298
  get: profileModule.get.bind(profileModule),
4952
6299
  getAddresses: profileModule.getAddresses.bind(profileModule),
4953
6300
  update: profileModule.update.bind(profileModule)
6301
+ },
6302
+ marketing: {
6303
+ captureLead: marketingModule.captureLead.bind(marketingModule)
4954
6304
  }
4955
6305
  };
4956
6306
  }