@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.js CHANGED
@@ -1,10 +1,23 @@
1
1
  // libs/sdk/src/errors.ts
2
2
  var WebsiteError = class extends Error {
3
3
  code;
4
- constructor(message, code = "WEBSITE_ERROR") {
4
+ status;
5
+ retryAfterSeconds;
6
+ constructor(message, code = "WEBSITE_ERROR", options) {
5
7
  super(message);
6
8
  this.name = "WebsiteError";
7
9
  this.code = code;
10
+ this.status = options?.status ?? null;
11
+ this.retryAfterSeconds = options?.retryAfterSeconds ?? null;
12
+ }
13
+ };
14
+ var RateLimitError = class extends WebsiteError {
15
+ constructor(message, retryAfterSeconds) {
16
+ super(message, "RATE_LIMITED", {
17
+ status: 429,
18
+ retryAfterSeconds
19
+ });
20
+ this.name = "RateLimitError";
8
21
  }
9
22
  };
10
23
  var NotInitializedError = class extends WebsiteError {
@@ -40,7 +53,109 @@ var GraphQLRequestError = class extends WebsiteError {
40
53
  }
41
54
  };
42
55
 
56
+ // libs/sdk/src/media.ts
57
+ var DEFAULT_RESPONSIVE_WIDTHS = [240, 384, 640, 960, 1400];
58
+ var CONTENT_HERO_WIDTHS = [640, 960, 1400, 1920, 2560];
59
+ var CONTENT_HERO_MAX_WIDTH = 1920;
60
+ var MEDIA_VARIANTS = {
61
+ thumbnail: { width: 240, quality: 75, format: "webp" },
62
+ card: { width: 640, quality: 80, format: "webp" },
63
+ detail: { width: 1400, quality: 85, format: "webp" }
64
+ };
65
+ function toSafeInteger(value) {
66
+ if (!Number.isFinite(value)) return void 0;
67
+ return Math.max(1, Math.floor(value));
68
+ }
69
+ function buildQuery(options) {
70
+ const params = new URLSearchParams();
71
+ const width = toSafeInteger(options.width);
72
+ const height = toSafeInteger(options.height);
73
+ const quality = toSafeInteger(options.quality);
74
+ if (width) params.set("w", String(width));
75
+ if (height) params.set("h", String(height));
76
+ if (quality) params.set("q", String(Math.min(100, quality)));
77
+ if (options.format) params.set("f", options.format);
78
+ const query = params.toString();
79
+ return query ? `?${query}` : "";
80
+ }
81
+ function buildStorageFileUrl(storageUrl, fileId, variant = "original") {
82
+ if (!storageUrl || !fileId) return "";
83
+ const baseUrl = `${storageUrl.replace(/\/+$/, "")}/files/${fileId}`;
84
+ if (variant === "original") return baseUrl;
85
+ return `${baseUrl}${buildQuery(MEDIA_VARIANTS[variant])}`;
86
+ }
87
+ function buildStorageTransformedFileUrl(storageUrl, fileId, options) {
88
+ if (!storageUrl || !fileId) return "";
89
+ const baseUrl = `${storageUrl.replace(/\/+$/, "")}/files/${fileId}`;
90
+ return `${baseUrl}${buildQuery(options)}`;
91
+ }
92
+ function buildStorageResponsiveSources(storageUrl, fileId, options = {}) {
93
+ const widths = options.widths ?? [...DEFAULT_RESPONSIVE_WIDTHS];
94
+ const uniqueWidths = [...new Set(widths.map(toSafeInteger))].filter((width) => Boolean(width)).sort((left, right) => left - right);
95
+ return uniqueWidths.map((width) => ({
96
+ width,
97
+ url: buildStorageTransformedFileUrl(storageUrl, fileId, {
98
+ width,
99
+ quality: options.quality ?? 80,
100
+ format: options.format ?? "webp"
101
+ })
102
+ }));
103
+ }
104
+ function formatMediaSrcSet(sources) {
105
+ return sources.filter((source) => source.url && source.width > 0).map((source) => `${source.url} ${source.width}w`).join(", ");
106
+ }
107
+ function buildStorageMediaUrls(storageUrl, fileId) {
108
+ return {
109
+ originalUrl: buildStorageFileUrl(storageUrl, fileId, "original"),
110
+ thumbnailUrl: buildStorageFileUrl(storageUrl, fileId, "thumbnail"),
111
+ cardUrl: buildStorageFileUrl(storageUrl, fileId, "card"),
112
+ detailUrl: buildStorageFileUrl(storageUrl, fileId, "detail"),
113
+ responsiveSources: buildStorageResponsiveSources(storageUrl, fileId)
114
+ };
115
+ }
116
+ function buildStorageContentMediaUrls(storageUrl, fileId, originalWidth) {
117
+ const legacyUrls = buildStorageMediaUrls(storageUrl, fileId);
118
+ const safeOriginalWidth = toSafeInteger(originalWidth ?? void 0);
119
+ if (!safeOriginalWidth) {
120
+ return {
121
+ ...legacyUrls,
122
+ heroUrl: legacyUrls.originalUrl,
123
+ responsiveSources: []
124
+ };
125
+ }
126
+ const responsiveWidths = [
127
+ ...CONTENT_HERO_WIDTHS.filter((width) => width <= safeOriginalWidth),
128
+ safeOriginalWidth
129
+ ];
130
+ const heroWidth = Math.min(safeOriginalWidth, CONTENT_HERO_MAX_WIDTH);
131
+ return {
132
+ ...legacyUrls,
133
+ heroUrl: buildStorageTransformedFileUrl(storageUrl, fileId, {
134
+ width: heroWidth,
135
+ quality: 88,
136
+ format: "webp"
137
+ }),
138
+ responsiveSources: buildStorageResponsiveSources(storageUrl, fileId, {
139
+ widths: responsiveWidths,
140
+ quality: 85,
141
+ format: "webp"
142
+ })
143
+ };
144
+ }
145
+
43
146
  // libs/sdk/src/modules/auth.ts
147
+ function getWebsiteContextFromPublicConfig(config) {
148
+ if (config.contextSchemaVersion !== 1) return void 0;
149
+ return {
150
+ websiteId: config.websiteId,
151
+ companyId: config.companyId,
152
+ chainId: config.chainId ?? null,
153
+ name: config.name,
154
+ slug: config.slug,
155
+ domain: config.domain,
156
+ config: config.config ?? null
157
+ };
158
+ }
44
159
  function createAuthHeaders(accessToken) {
45
160
  return accessToken ? { Authorization: `Bearer ${accessToken}` } : {};
46
161
  }
@@ -62,14 +177,18 @@ function createRequestHeadersProvider(websiteSlug, getAccessToken) {
62
177
  }
63
178
  async function getWebsitePublicConfig(options) {
64
179
  const apiUrl = options.apiUrl.replace(/\/+$/, "");
65
- const headers = options.origin ? { Origin: options.origin } : void 0;
180
+ const headers = {};
181
+ if (options.origin) headers.Origin = options.origin;
182
+ if (options.previewToken) {
183
+ headers["X-Bacano-Preview-Token"] = options.previewToken;
184
+ }
66
185
  const params = new URLSearchParams({ websiteSlug: options.websiteSlug });
67
186
  let response;
68
187
  try {
69
188
  response = await fetch(
70
189
  `${apiUrl}/api/v1/website/public-config?${params.toString()}`,
71
190
  {
72
- headers,
191
+ headers: Object.keys(headers).length > 0 ? headers : void 0,
73
192
  cache: "no-store"
74
193
  }
75
194
  );
@@ -93,12 +212,71 @@ async function getWebsitePublicConfig(options) {
93
212
  require_document: false,
94
213
  document_types: []
95
214
  };
215
+ const marketing = raw.marketing ?? {
216
+ schema_version: 1,
217
+ enabled: false,
218
+ currency_code: "COP",
219
+ time_zone: "America/Bogota",
220
+ attribution_window_days: 90,
221
+ consent_policy: null,
222
+ destinations: { ga4: null, meta: null, clarity: null }
223
+ };
224
+ const legal = raw.legal ?? {
225
+ schema_version: 1,
226
+ version: 0,
227
+ content_hash: null,
228
+ documents: {
229
+ terms: null,
230
+ shipping: null,
231
+ returns: null,
232
+ privacy: null,
233
+ cookies: null
234
+ },
235
+ inquiries_channel: null,
236
+ direct_marketing_consent: null,
237
+ cookie_notice: {
238
+ enabled: false,
239
+ message: "Usamos cookies propias y de terceros para mejorar tu experiencia y medir el funcionamiento del sitio.",
240
+ button_label: "Entendido"
241
+ }
242
+ };
243
+ const mapConstructionImage = (image) => {
244
+ if (!image) return null;
245
+ const urls = buildStorageContentMediaUrls(
246
+ `${apiUrl}/storage/v1`,
247
+ image.file_id,
248
+ image.width
249
+ );
250
+ return {
251
+ fileId: image.file_id,
252
+ name: image.name,
253
+ altText: image.alt_text,
254
+ width: image.width,
255
+ height: image.height,
256
+ originalUrl: urls.originalUrl,
257
+ heroUrl: urls.heroUrl,
258
+ responsiveSources: urls.responsiveSources
259
+ };
260
+ };
261
+ const construction = raw.construction ?? {
262
+ schema_version: 1,
263
+ enabled: false,
264
+ enforced: false,
265
+ preview_access_granted: false,
266
+ title: null,
267
+ message: null,
268
+ desktop_image: null,
269
+ mobile_image: null
270
+ };
96
271
  return {
97
272
  websiteId: raw.website_id,
98
273
  companyId: raw.company_id,
274
+ contextSchemaVersion: raw.context_schema_version === 1 ? 1 : null,
275
+ chainId: raw.chain_id ?? null,
99
276
  slug: raw.slug,
100
277
  name: raw.name,
101
278
  domain: raw.domain,
279
+ config: raw.config ?? null,
102
280
  buyerAccountsEnabled: raw.buyer_accounts_enabled,
103
281
  clerkPublishableKey: raw.clerk_publishable_key,
104
282
  customerIdentification: {
@@ -114,56 +292,67 @@ async function getWebsitePublicConfig(options) {
114
292
  isDefault: documentType.is_default
115
293
  })
116
294
  )
295
+ },
296
+ marketing: {
297
+ schemaVersion: marketing.schema_version,
298
+ enabled: marketing.enabled,
299
+ currencyCode: marketing.currency_code,
300
+ timeZone: marketing.time_zone,
301
+ attributionWindowDays: marketing.attribution_window_days,
302
+ consentPolicy: marketing.consent_policy ? {
303
+ id: marketing.consent_policy.id,
304
+ version: marketing.consent_policy.version,
305
+ statement: marketing.consent_policy.statement,
306
+ statementHash: marketing.consent_policy.statement_hash
307
+ } : null,
308
+ destinations: {
309
+ ga4: marketing.destinations.ga4 ? {
310
+ measurementId: marketing.destinations.ga4.measurement_id,
311
+ diagnosticMode: marketing.destinations.ga4.diagnostic_mode ?? false
312
+ } : null,
313
+ meta: marketing.destinations.meta ? {
314
+ pixelId: marketing.destinations.meta.pixel_id,
315
+ diagnosticMode: marketing.destinations.meta.diagnostic_mode ?? false
316
+ } : null,
317
+ clarity: marketing.destinations.clarity ? { projectId: marketing.destinations.clarity.project_id } : null
318
+ }
319
+ },
320
+ legal: {
321
+ schemaVersion: legal.schema_version,
322
+ version: legal.version,
323
+ contentHash: legal.content_hash,
324
+ documents: {
325
+ terms: legal.documents.terms,
326
+ shipping: legal.documents.shipping ?? null,
327
+ returns: legal.documents.returns ?? null,
328
+ privacy: legal.documents.privacy,
329
+ cookies: legal.documents.cookies
330
+ },
331
+ inquiriesChannel: legal.inquiries_channel,
332
+ directMarketingConsent: legal.direct_marketing_consent ? {
333
+ version: legal.direct_marketing_consent.version,
334
+ statement: legal.direct_marketing_consent.statement,
335
+ statementHash: legal.direct_marketing_consent.statement_hash
336
+ } : null,
337
+ cookieNotice: {
338
+ enabled: legal.cookie_notice.enabled,
339
+ message: legal.cookie_notice.message,
340
+ buttonLabel: legal.cookie_notice.button_label
341
+ }
342
+ },
343
+ construction: {
344
+ schemaVersion: construction.schema_version,
345
+ enabled: construction.enabled,
346
+ enforced: construction.enforced,
347
+ previewAccessGranted: construction.preview_access_granted,
348
+ title: construction.title,
349
+ message: construction.message,
350
+ desktopImage: mapConstructionImage(construction.desktop_image),
351
+ mobileImage: mapConstructionImage(construction.mobile_image)
117
352
  }
118
353
  };
119
354
  }
120
355
 
121
- // libs/sdk/src/transport/graphql.ts
122
- async function executeGraphQL(url, query, variables, headers) {
123
- const resolvedHeaders = typeof headers === "function" ? await headers() : headers ?? {};
124
- let res;
125
- try {
126
- res = await fetch(url, {
127
- method: "POST",
128
- headers: {
129
- "Content-Type": "application/json",
130
- ...resolvedHeaders
131
- },
132
- body: JSON.stringify({ query, variables })
133
- });
134
- } catch (err) {
135
- throw new WebsiteError(
136
- `Network error connecting to ${url}: ${err instanceof Error ? err.message : String(err)}`,
137
- "NETWORK_ERROR"
138
- );
139
- }
140
- if (res.status === 429) {
141
- throw new WebsiteError(
142
- "Rate limit exceeded. Please try again later.",
143
- "RATE_LIMITED"
144
- );
145
- }
146
- let body;
147
- try {
148
- body = await res.json();
149
- } catch {
150
- throw new WebsiteError(
151
- `Non-JSON response from ${url} (status ${res.status})`,
152
- "INVALID_RESPONSE"
153
- );
154
- }
155
- if (body.errors && body.errors.length > 0) {
156
- throw new GraphQLRequestError(body.errors);
157
- }
158
- if (!body.data) {
159
- throw new WebsiteError(
160
- "GraphQL response contains no data",
161
- "EMPTY_RESPONSE"
162
- );
163
- }
164
- return body.data;
165
- }
166
-
167
356
  // libs/sdk/src/transport/documents.ts
168
357
  var RESOLVE_WEBSITE_BY_SLUG = (
169
358
  /* GraphQL */
@@ -989,6 +1178,62 @@ var CHECK_AVAILABILITY = (
989
1178
  product_variant_id
990
1179
  in_stock
991
1180
  label
1181
+ max_purchasable_quantity
1182
+ }
1183
+ }
1184
+ `
1185
+ );
1186
+ var GET_PRODUCT_LIVE_STATE = (
1187
+ /* GraphQL */
1188
+ `
1189
+ query GetProductLiveState(
1190
+ $variantIds: [uuid!]!
1191
+ $companyId: uuid!
1192
+ $chainId: uuid!
1193
+ $websiteId: uuid!
1194
+ ) {
1195
+ product_variants(
1196
+ where: {
1197
+ id: { _in: $variantIds }
1198
+ is_disabled: { _eq: false }
1199
+ product: {
1200
+ company_id: { _eq: $companyId }
1201
+ product_website_publications: {
1202
+ website_id: { _eq: $websiteId }
1203
+ is_visible: { _eq: true }
1204
+ }
1205
+ chain_products: {
1206
+ chain_id: { _eq: $chainId }
1207
+ is_enabled: { _eq: true }
1208
+ status: { _eq: active }
1209
+ }
1210
+ }
1211
+ }
1212
+ ) {
1213
+ id
1214
+ product_variant_prices(
1215
+ where: {
1216
+ price_type: { _in: [CLIENT, SALE, WHOLESALE, WHOLESALE_2] }
1217
+ _or: [
1218
+ { chain_id: { _eq: $chainId } }
1219
+ { chain_id: { _is_null: true } }
1220
+ ]
1221
+ }
1222
+ ) {
1223
+ id
1224
+ price
1225
+ price_type
1226
+ chain_id
1227
+ }
1228
+ }
1229
+ websiteProductAvailability(
1230
+ product_variant_ids: $variantIds
1231
+ chain_id: $chainId
1232
+ ) {
1233
+ product_variant_id
1234
+ in_stock
1235
+ label
1236
+ max_purchasable_quantity
992
1237
  }
993
1238
  }
994
1239
  `
@@ -1098,6 +1343,7 @@ var START_GUEST_CHECKOUT = (
1098
1343
  $contact: WebsiteGuestContactInput!
1099
1344
  $items: [WebsiteCheckoutItemInput!]!
1100
1345
  $fulfillmentFamily: String
1346
+ $marketingAttribution: WebsiteMarketingAttributionInput
1101
1347
  ) {
1102
1348
  websiteStartGuestCheckout(
1103
1349
  website_id: $websiteId
@@ -1105,6 +1351,7 @@ var START_GUEST_CHECKOUT = (
1105
1351
  contact: $contact
1106
1352
  items: $items
1107
1353
  fulfillment_family: $fulfillmentFamily
1354
+ marketing_attribution: $marketingAttribution
1108
1355
  ) {
1109
1356
  success
1110
1357
  order_id
@@ -1262,6 +1509,8 @@ var CHECKOUT = (
1262
1509
  $paymentMethodId: uuid
1263
1510
  $buyerAccountOptIn: Boolean
1264
1511
  $initialPayment: WebsiteInitialPaymentInput
1512
+ $marketingAttribution: WebsiteMarketingAttributionInput
1513
+ $advertisingConsent: WebsiteAdvertisingConsentInput
1265
1514
  ) {
1266
1515
  websiteCheckout(
1267
1516
  order_id: $orderId
@@ -1274,6 +1523,8 @@ var CHECKOUT = (
1274
1523
  payment_method_id: $paymentMethodId
1275
1524
  buyer_account_opt_in: $buyerAccountOptIn
1276
1525
  initial_payment: $initialPayment
1526
+ marketing_attribution: $marketingAttribution
1527
+ advertising_consent: $advertisingConsent
1277
1528
  ) {
1278
1529
  success
1279
1530
  order_id
@@ -2200,13 +2451,121 @@ var GET_SEARCH_SUGGESTIONS = (
2200
2451
  `
2201
2452
  );
2202
2453
 
2454
+ // libs/sdk/src/transport/graphql.ts
2455
+ var DEFAULT_MAX_ATTEMPTS = 4;
2456
+ var DEFAULT_MAX_TOTAL_WAIT_MS = 3 * 60 * 1e3;
2457
+ var DEFAULT_JITTER_RATIO = 0.1;
2458
+ function parseRetryAfterSeconds(res, body) {
2459
+ const header = res.headers?.get?.("Retry-After")?.trim();
2460
+ if (header) {
2461
+ const seconds = Number(header);
2462
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds);
2463
+ const retryAt = Date.parse(header);
2464
+ if (Number.isFinite(retryAt)) {
2465
+ return Math.max(Math.ceil((retryAt - Date.now()) / 1e3), 0);
2466
+ }
2467
+ }
2468
+ if (body && typeof body === "object") {
2469
+ const seconds = Number(
2470
+ body.retry_after_seconds
2471
+ );
2472
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds);
2473
+ }
2474
+ return 1;
2475
+ }
2476
+ async function createRateLimitError(res) {
2477
+ let body;
2478
+ try {
2479
+ body = await res.json();
2480
+ } catch {
2481
+ body = null;
2482
+ }
2483
+ const message = body && typeof body === "object" && typeof body.message === "string" ? body.message : "Rate limit exceeded. Please try again later.";
2484
+ return new RateLimitError(message, parseRetryAfterSeconds(res, body));
2485
+ }
2486
+ function getRetryDelayMs(error, options) {
2487
+ const retryAfterMs = (error.retryAfterSeconds ?? 1) * 1e3;
2488
+ const jitterRatio = Math.max(
2489
+ 0,
2490
+ Math.min(options.jitterRatio ?? DEFAULT_JITTER_RATIO, 1)
2491
+ );
2492
+ return Math.ceil(retryAfterMs * (1 + Math.random() * jitterRatio));
2493
+ }
2494
+ function wait(delayMs) {
2495
+ return new Promise((resolve) => setTimeout(resolve, delayMs));
2496
+ }
2497
+ async function executeGraphQL(url, query, variables, headers, options) {
2498
+ const retry = options?.retry;
2499
+ const maxAttempts = Math.max(
2500
+ 1,
2501
+ Math.floor(retry?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS)
2502
+ );
2503
+ const maxTotalWaitMs = Math.max(
2504
+ 0,
2505
+ retry?.maxTotalWaitMs ?? DEFAULT_MAX_TOTAL_WAIT_MS
2506
+ );
2507
+ let totalWaitMs = 0;
2508
+ let res = null;
2509
+ for (let attempt = 1; attempt <= (retry ? maxAttempts : 1); attempt += 1) {
2510
+ const resolvedHeaders = typeof headers === "function" ? await headers() : headers ?? {};
2511
+ try {
2512
+ res = await fetch(url, {
2513
+ method: "POST",
2514
+ headers: {
2515
+ "Content-Type": "application/json",
2516
+ ...resolvedHeaders
2517
+ },
2518
+ body: JSON.stringify({ query, variables })
2519
+ });
2520
+ } catch (err) {
2521
+ throw new WebsiteError(
2522
+ `Network error connecting to ${url}: ${err instanceof Error ? err.message : String(err)}`,
2523
+ "NETWORK_ERROR"
2524
+ );
2525
+ }
2526
+ if (res.status !== 429) break;
2527
+ const rateLimitError = await createRateLimitError(res);
2528
+ const delayMs = getRetryDelayMs(rateLimitError, retry ?? {});
2529
+ const canRetry = Boolean(retry) && attempt < maxAttempts && totalWaitMs + delayMs <= maxTotalWaitMs;
2530
+ if (!canRetry) throw rateLimitError;
2531
+ totalWaitMs += delayMs;
2532
+ await wait(delayMs);
2533
+ }
2534
+ if (!res) {
2535
+ throw new WebsiteError(
2536
+ "GraphQL request did not produce a response",
2537
+ "EMPTY_RESPONSE"
2538
+ );
2539
+ }
2540
+ let body;
2541
+ try {
2542
+ body = await res.json();
2543
+ } catch {
2544
+ throw new WebsiteError(
2545
+ `Non-JSON response from ${url} (status ${res.status})`,
2546
+ "INVALID_RESPONSE"
2547
+ );
2548
+ }
2549
+ if (body.errors && body.errors.length > 0) {
2550
+ throw new GraphQLRequestError(body.errors);
2551
+ }
2552
+ if (!body.data) {
2553
+ throw new WebsiteError(
2554
+ "GraphQL response contains no data",
2555
+ "EMPTY_RESPONSE"
2556
+ );
2557
+ }
2558
+ return body.data;
2559
+ }
2560
+
2203
2561
  // libs/sdk/src/modules/availability.ts
2204
2562
  var MAX_AVAILABILITY_VARIANT_IDS = 100;
2205
2563
  function mapAvailability(raw) {
2206
2564
  return {
2207
2565
  productVariantId: raw.product_variant_id,
2208
2566
  inStock: raw.in_stock,
2209
- label: raw.label
2567
+ label: raw.label,
2568
+ maxPurchasableQuantity: raw.max_purchasable_quantity
2210
2569
  };
2211
2570
  }
2212
2571
  function chunkArray(items, chunkSize) {
@@ -2232,7 +2591,10 @@ function createAvailabilityModule(graphqlUrl, getHeaders, getContext) {
2232
2591
  if (productVariantIds.length === 0) return [];
2233
2592
  const variantIds = uniqueValues(productVariantIds);
2234
2593
  const results = [];
2235
- for (const chunk of chunkArray(variantIds, MAX_AVAILABILITY_VARIANT_IDS)) {
2594
+ for (const chunk of chunkArray(
2595
+ variantIds,
2596
+ MAX_AVAILABILITY_VARIANT_IDS
2597
+ )) {
2236
2598
  const data = await executeGraphQL(
2237
2599
  graphqlUrl,
2238
2600
  CHECK_AVAILABILITY,
@@ -2251,6 +2613,255 @@ function createAvailabilityModule(graphqlUrl, getHeaders, getContext) {
2251
2613
  };
2252
2614
  }
2253
2615
 
2616
+ // libs/sdk/src/marketing/sanitize.ts
2617
+ var SENSITIVE_PARAMETER_PATTERNS = [
2618
+ /token/i,
2619
+ /secret/i,
2620
+ /password/i,
2621
+ /authorization/i,
2622
+ /session/i,
2623
+ /ticket/i,
2624
+ /code_verifier/i,
2625
+ /client_secret/i,
2626
+ /payment/i,
2627
+ /order[_-]?(?:id|token)/i,
2628
+ /public[_-]?order/i,
2629
+ /clerk/i,
2630
+ /oauth/i,
2631
+ /email/i,
2632
+ /phone/i,
2633
+ /document/i
2634
+ ];
2635
+ var ALLOWED_MARKETING_PARAMETERS = /* @__PURE__ */ new Set([
2636
+ "utm_source",
2637
+ "utm_medium",
2638
+ "utm_campaign",
2639
+ "utm_term",
2640
+ "utm_content",
2641
+ "utm_source_platform",
2642
+ "utm_id",
2643
+ "gclid",
2644
+ "gbraid",
2645
+ "wbraid",
2646
+ "fbclid",
2647
+ "ttclid",
2648
+ "msclkid"
2649
+ ]);
2650
+ function sanitizeMarketingUrl(input, baseUrl) {
2651
+ try {
2652
+ const url = new URL(input, baseUrl);
2653
+ const safeParameters = new URLSearchParams();
2654
+ for (const [key, value] of url.searchParams.entries()) {
2655
+ if (ALLOWED_MARKETING_PARAMETERS.has(key.toLowerCase()) && !SENSITIVE_PARAMETER_PATTERNS.some((pattern) => pattern.test(key))) {
2656
+ safeParameters.set(key.toLowerCase(), value.slice(0, 500));
2657
+ }
2658
+ }
2659
+ url.search = safeParameters.toString();
2660
+ url.hash = "";
2661
+ return url.toString();
2662
+ } catch {
2663
+ return "";
2664
+ }
2665
+ }
2666
+ function getAllowedMarketingParameters(input, baseUrl) {
2667
+ const sanitized = sanitizeMarketingUrl(input, baseUrl);
2668
+ if (!sanitized) return {};
2669
+ const url = new URL(sanitized);
2670
+ return Object.fromEntries(url.searchParams.entries());
2671
+ }
2672
+
2673
+ // libs/sdk/src/marketing/attribution.ts
2674
+ var STORAGE_PREFIX = "bacano:marketing:attribution:v1";
2675
+ var GA4_CLIENT_ID_PATTERN = /^\d+\.\d+$/;
2676
+ var META_BROWSER_ID_PATTERN = /^fb\.\d+\.\d+\.[A-Za-z0-9_-]+$/;
2677
+ function storageKey(websiteId) {
2678
+ return `${STORAGE_PREFIX}:${websiteId}`;
2679
+ }
2680
+ function readStored(websiteId) {
2681
+ if (typeof window === "undefined") return null;
2682
+ try {
2683
+ const value = window.localStorage.getItem(storageKey(websiteId));
2684
+ if (!value) return null;
2685
+ const parsed = JSON.parse(value);
2686
+ if (new Date(parsed.expiresAt).getTime() <= Date.now()) {
2687
+ window.localStorage.removeItem(storageKey(websiteId));
2688
+ return null;
2689
+ }
2690
+ return parsed;
2691
+ } catch {
2692
+ return null;
2693
+ }
2694
+ }
2695
+ function writeStored(websiteId, value) {
2696
+ if (typeof window === "undefined") return;
2697
+ try {
2698
+ window.localStorage.setItem(storageKey(websiteId), JSON.stringify(value));
2699
+ } catch {
2700
+ }
2701
+ }
2702
+ function readCookie(name) {
2703
+ if (typeof document === "undefined") return void 0;
2704
+ try {
2705
+ const prefix = `${name}=`;
2706
+ return document.cookie.split(";").map((part) => part.trim()).find((part) => part.startsWith(prefix))?.slice(prefix.length);
2707
+ } catch {
2708
+ return void 0;
2709
+ }
2710
+ }
2711
+ function validMetaBrowserId(value) {
2712
+ const normalized = value?.trim();
2713
+ return normalized && normalized.length <= 512 && META_BROWSER_ID_PATTERN.test(normalized) ? normalized : void 0;
2714
+ }
2715
+ function optional(value) {
2716
+ const normalized = value?.trim();
2717
+ return normalized ? normalized.slice(0, 500) : void 0;
2718
+ }
2719
+ function buildTouchpoint() {
2720
+ if (typeof window === "undefined") return null;
2721
+ const params = getAllowedMarketingParameters(window.location.href);
2722
+ const referrerHost = (() => {
2723
+ if (!document.referrer) return void 0;
2724
+ try {
2725
+ const hostname = new URL(document.referrer).hostname;
2726
+ return hostname === window.location.hostname ? void 0 : hostname.slice(0, 255);
2727
+ } catch {
2728
+ return void 0;
2729
+ }
2730
+ })();
2731
+ return {
2732
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
2733
+ landingUrl: sanitizeMarketingUrl(window.location.href),
2734
+ referrerHost,
2735
+ source: optional(params.utm_source),
2736
+ medium: optional(params.utm_medium),
2737
+ campaign: optional(params.utm_campaign),
2738
+ term: optional(params.utm_term),
2739
+ content: optional(params.utm_content),
2740
+ sourcePlatform: optional(params.utm_source_platform),
2741
+ campaignId: optional(params.utm_id),
2742
+ gclid: optional(params.gclid),
2743
+ gbraid: optional(params.gbraid),
2744
+ wbraid: optional(params.wbraid),
2745
+ fbclid: optional(params.fbclid),
2746
+ ttclid: optional(params.ttclid),
2747
+ msclkid: optional(params.msclkid)
2748
+ };
2749
+ }
2750
+ function isNonDirect(touchpoint) {
2751
+ return Boolean(
2752
+ touchpoint.source || touchpoint.medium || touchpoint.campaign || touchpoint.gclid || touchpoint.gbraid || touchpoint.wbraid || touchpoint.fbclid || touchpoint.ttclid || touchpoint.msclkid || touchpoint.referrerHost
2753
+ );
2754
+ }
2755
+ function toPublic(stored) {
2756
+ return {
2757
+ schemaVersion: 1,
2758
+ firstTouch: stored.firstTouch,
2759
+ lastNonDirectTouch: stored.lastNonDirectTouch,
2760
+ ga4ClientId: stored.ga4ClientId,
2761
+ ga4SessionId: stored.ga4SessionId,
2762
+ metaFbp: stored.metaFbp,
2763
+ metaFbc: stored.metaFbc,
2764
+ clientUserAgent: stored.clientUserAgent,
2765
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
2766
+ expiresAt: stored.expiresAt
2767
+ };
2768
+ }
2769
+ function refreshWebsiteMarketingMetaContext(websiteId) {
2770
+ try {
2771
+ const stored = readStored(websiteId);
2772
+ if (!stored || typeof window === "undefined") return false;
2773
+ const fbp = validMetaBrowserId(readCookie("_fbp"));
2774
+ const clickTouchpoint = stored.lastNonDirectTouch ?? stored.firstTouch;
2775
+ const derivedFbc = clickTouchpoint.fbclid ? `fb.1.${new Date(clickTouchpoint.capturedAt).getTime()}.${clickTouchpoint.fbclid}` : void 0;
2776
+ const fbc = validMetaBrowserId(readCookie("_fbc") ?? derivedFbc);
2777
+ const clientUserAgent = window.navigator?.userAgent.trim().slice(0, 1024);
2778
+ writeStored(websiteId, {
2779
+ ...stored,
2780
+ metaFbp: fbp ?? stored.metaFbp,
2781
+ metaFbc: fbc ?? stored.metaFbc,
2782
+ clientUserAgent: clientUserAgent || stored.clientUserAgent
2783
+ });
2784
+ return Boolean(fbp || fbc || clientUserAgent);
2785
+ } catch {
2786
+ return false;
2787
+ }
2788
+ }
2789
+ function storeWebsiteMarketingGa4ClientId(websiteId, clientId) {
2790
+ if (typeof clientId !== "string") return false;
2791
+ const normalized = clientId.trim();
2792
+ if (!GA4_CLIENT_ID_PATTERN.test(normalized)) return false;
2793
+ const stored = readStored(websiteId);
2794
+ if (!stored) return false;
2795
+ writeStored(websiteId, { ...stored, ga4ClientId: normalized });
2796
+ return true;
2797
+ }
2798
+ function storeWebsiteMarketingGa4SessionId(websiteId, sessionId) {
2799
+ const normalized = typeof sessionId === "number" && Number.isSafeInteger(sessionId) ? String(sessionId) : typeof sessionId === "string" ? sessionId.trim() : "";
2800
+ const numericValue = Number(normalized);
2801
+ if (!/^\d{1,16}$/.test(normalized) || !Number.isSafeInteger(numericValue) || numericValue <= 0) {
2802
+ return false;
2803
+ }
2804
+ const stored = readStored(websiteId);
2805
+ if (!stored) return false;
2806
+ writeStored(websiteId, { ...stored, ga4SessionId: normalized });
2807
+ return true;
2808
+ }
2809
+ function captureWebsiteMarketingAttribution(websiteId, windowDays) {
2810
+ const current = buildTouchpoint();
2811
+ if (!current) return null;
2812
+ const stored = readStored(websiteId);
2813
+ const expiresAt = stored?.expiresAt ?? new Date(Date.now() + Math.max(1, windowDays) * 864e5).toISOString();
2814
+ const next = {
2815
+ firstTouch: stored?.firstTouch ?? current,
2816
+ lastNonDirectTouch: isNonDirect(current) ? current : stored?.lastNonDirectTouch ?? null,
2817
+ ga4ClientId: stored?.ga4ClientId,
2818
+ ga4SessionId: stored?.ga4SessionId,
2819
+ metaFbp: stored?.metaFbp,
2820
+ metaFbc: stored?.metaFbc,
2821
+ clientUserAgent: stored?.clientUserAgent,
2822
+ expiresAt
2823
+ };
2824
+ writeStored(websiteId, next);
2825
+ return toPublic(next);
2826
+ }
2827
+ function getWebsiteMarketingAttribution(websiteId) {
2828
+ const stored = readStored(websiteId);
2829
+ return stored ? toPublic(stored) : null;
2830
+ }
2831
+ function serializeTouchpoint(touchpoint) {
2832
+ return {
2833
+ captured_at: touchpoint.capturedAt,
2834
+ landing_url: touchpoint.landingUrl,
2835
+ referrer_host: touchpoint.referrerHost,
2836
+ source: touchpoint.source,
2837
+ medium: touchpoint.medium,
2838
+ campaign: touchpoint.campaign,
2839
+ term: touchpoint.term,
2840
+ content: touchpoint.content,
2841
+ source_platform: touchpoint.sourcePlatform,
2842
+ campaign_id: touchpoint.campaignId,
2843
+ gclid: touchpoint.gclid,
2844
+ gbraid: touchpoint.gbraid,
2845
+ wbraid: touchpoint.wbraid,
2846
+ fbclid: touchpoint.fbclid,
2847
+ ttclid: touchpoint.ttclid,
2848
+ msclkid: touchpoint.msclkid
2849
+ };
2850
+ }
2851
+ function serializeWebsiteMarketingAttribution(attribution) {
2852
+ return {
2853
+ schema_version: attribution.schemaVersion,
2854
+ first_touch: serializeTouchpoint(attribution.firstTouch),
2855
+ last_non_direct_touch: attribution.lastNonDirectTouch ? serializeTouchpoint(attribution.lastNonDirectTouch) : null,
2856
+ ga4_client_id: attribution.ga4ClientId,
2857
+ ga4_session_id: attribution.ga4SessionId,
2858
+ meta_fbp: attribution.metaFbp,
2859
+ meta_fbc: attribution.metaFbc,
2860
+ client_user_agent: attribution.clientUserAgent,
2861
+ captured_at: attribution.capturedAt
2862
+ };
2863
+ }
2864
+
2254
2865
  // libs/sdk/src/modules/cart.ts
2255
2866
  function mapCartItem(raw) {
2256
2867
  return {
@@ -2371,7 +2982,8 @@ function createCartModule(graphqlUrl, getHeaders, getContext, cartSessionStorage
2371
2982
  items: input.items.map((item) => ({
2372
2983
  product_variant_id: item.productVariantId,
2373
2984
  quantity: item.quantity
2374
- }))
2985
+ })),
2986
+ marketingAttribution: input.marketingAttribution ? serializeWebsiteMarketingAttribution(input.marketingAttribution) : void 0
2375
2987
  },
2376
2988
  getHeaders
2377
2989
  );
@@ -2493,7 +3105,9 @@ function createCartSessionStorage(kind, getSlug) {
2493
3105
  const key = readKey();
2494
3106
  const match = document.cookie.split("; ").find((row) => row.startsWith(`${key}=`));
2495
3107
  if (!match) return null;
2496
- return JSON.parse(decodeURIComponent(match.split("=").slice(1).join("=")));
3108
+ return JSON.parse(
3109
+ decodeURIComponent(match.split("=").slice(1).join("="))
3110
+ );
2497
3111
  } catch {
2498
3112
  return null;
2499
3113
  }
@@ -2533,46 +3147,129 @@ function createCartSessionStorage(kind, getSlug) {
2533
3147
  };
2534
3148
  }
2535
3149
 
2536
- // libs/sdk/src/media.ts
2537
- var MEDIA_VARIANTS = {
2538
- thumbnail: { width: 240, quality: 75, format: "webp" },
2539
- card: { width: 640, quality: 80, format: "webp" },
2540
- detail: { width: 1400, quality: 85, format: "webp" }
2541
- };
2542
- function toSafeInteger(value) {
2543
- if (!Number.isFinite(value)) return void 0;
2544
- return Math.max(1, Math.floor(value));
2545
- }
2546
- function buildQuery(options) {
2547
- const params = new URLSearchParams();
2548
- const width = toSafeInteger(options.width);
2549
- const height = toSafeInteger(options.height);
2550
- const quality = toSafeInteger(options.quality);
2551
- if (width) params.set("w", String(width));
2552
- if (height) params.set("h", String(height));
2553
- if (quality) params.set("q", String(Math.min(100, quality)));
2554
- if (options.format) params.set("f", options.format);
2555
- const query = params.toString();
2556
- return query ? `?${query}` : "";
2557
- }
2558
- function buildStorageFileUrl(storageUrl, fileId, variant = "original") {
2559
- if (!storageUrl || !fileId) return "";
2560
- const baseUrl = `${storageUrl.replace(/\/+$/, "")}/files/${fileId}`;
2561
- if (variant === "original") return baseUrl;
2562
- return `${baseUrl}${buildQuery(MEDIA_VARIANTS[variant])}`;
2563
- }
2564
- function buildStorageMediaUrls(storageUrl, fileId) {
2565
- return {
2566
- originalUrl: buildStorageFileUrl(storageUrl, fileId, "original"),
2567
- thumbnailUrl: buildStorageFileUrl(storageUrl, fileId, "thumbnail"),
2568
- cardUrl: buildStorageFileUrl(storageUrl, fileId, "card"),
2569
- detailUrl: buildStorageFileUrl(storageUrl, fileId, "detail")
3150
+ // libs/sdk/src/modules/static-catalog.ts
3151
+ var STATIC_CATALOG_PAGE_SIZE = 100;
3152
+ var STATIC_CATALOG_MAX_PRODUCTS = 1e4;
3153
+ var STATIC_CATALOG_INITIAL_PAGE_SIZE = 24;
3154
+ async function collectStaticCatalog({
3155
+ websiteId,
3156
+ snapshot: options,
3157
+ bootstrap,
3158
+ loadPage
3159
+ }) {
3160
+ const pageSize = options.pageSize ?? STATIC_CATALOG_PAGE_SIZE;
3161
+ const maxProducts = options.maxProducts ?? STATIC_CATALOG_MAX_PRODUCTS;
3162
+ const initialPageSize = bootstrap?.initialPageSize ?? STATIC_CATALOG_INITIAL_PAGE_SIZE;
3163
+ const sortBy = bootstrap?.sortBy ?? "name_asc";
3164
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) {
3165
+ throw new WebsiteError(
3166
+ "Static catalog pageSize must be an integer between 1 and 100.",
3167
+ "INVALID_STATIC_CATALOG_PAGE_SIZE"
3168
+ );
3169
+ }
3170
+ if (!Number.isInteger(maxProducts) || maxProducts < 1 || maxProducts > STATIC_CATALOG_MAX_PRODUCTS) {
3171
+ throw new WebsiteError(
3172
+ `Static catalog maxProducts must be an integer between 1 and ${STATIC_CATALOG_MAX_PRODUCTS}.`,
3173
+ "INVALID_STATIC_CATALOG_MAX_PRODUCTS"
3174
+ );
3175
+ }
3176
+ if (bootstrap && (!Number.isInteger(initialPageSize) || initialPageSize < 1 || initialPageSize > 100)) {
3177
+ throw new WebsiteError(
3178
+ "Static catalog initialPageSize must be an integer between 1 and 100.",
3179
+ "INVALID_STATIC_CATALOG_INITIAL_PAGE_SIZE"
3180
+ );
3181
+ }
3182
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
3183
+ const products = [];
3184
+ let pageCount = 0;
3185
+ let expectedTotal = null;
3186
+ let firstPage = null;
3187
+ for (let offset = 0; ; ) {
3188
+ const includeBootstrapData = Boolean(bootstrap && offset === 0);
3189
+ const requestLimit = includeBootstrapData ? Math.max(pageSize, initialPageSize) : pageSize;
3190
+ const page = await loadPage({
3191
+ categoryListKey: options.categoryListKey,
3192
+ attributeListKey: options.attributeListKey,
3193
+ sortBy,
3194
+ limit: requestLimit,
3195
+ offset,
3196
+ include: {
3197
+ products: true,
3198
+ detailVariants: true,
3199
+ categories: includeBootstrapData,
3200
+ attributes: true,
3201
+ filters: includeBootstrapData,
3202
+ brands: includeBootstrapData,
3203
+ debug: false
3204
+ }
3205
+ });
3206
+ if (offset === 0) firstPage = page;
3207
+ pageCount += 1;
3208
+ const pageTotal = page.pagination.total;
3209
+ if (expectedTotal === null) {
3210
+ expectedTotal = pageTotal;
3211
+ } else if (pageTotal !== expectedTotal) {
3212
+ throwCatalogChanged();
3213
+ }
3214
+ if (pageTotal > maxProducts) {
3215
+ throw new WebsiteError(
3216
+ `Static catalog contains ${pageTotal} products and exceeds maxProducts ${maxProducts}.`,
3217
+ "STATIC_CATALOG_LIMIT_EXCEEDED"
3218
+ );
3219
+ }
3220
+ products.push(...page.products);
3221
+ if (!page.pagination.hasMore) break;
3222
+ if (page.products.length === 0) {
3223
+ throw new WebsiteError(
3224
+ "Static catalog pagination did not advance.",
3225
+ "STATIC_CATALOG_PAGINATION_STALLED"
3226
+ );
3227
+ }
3228
+ offset += page.products.length;
3229
+ }
3230
+ const total = expectedTotal ?? 0;
3231
+ const uniqueProductIds = new Set(products.map((product) => product.id));
3232
+ if (products.length !== total || uniqueProductIds.size !== total) {
3233
+ throwCatalogChanged();
3234
+ }
3235
+ const snapshot = {
3236
+ schemaVersion: 1,
3237
+ snapshotId: `${websiteId}:${generatedAt}`,
3238
+ generatedAt,
3239
+ websiteId,
3240
+ products,
3241
+ total,
3242
+ pageSize,
3243
+ pageCount
2570
3244
  };
3245
+ const initialPage = bootstrap && firstPage ? {
3246
+ ...firstPage,
3247
+ products: firstPage.products.slice(0, initialPageSize),
3248
+ pagination: {
3249
+ ...firstPage.pagination,
3250
+ limit: initialPageSize,
3251
+ offset: 0,
3252
+ hasMore: total > initialPageSize
3253
+ }
3254
+ } : null;
3255
+ return { snapshot, initialPage };
3256
+ }
3257
+ function throwCatalogChanged() {
3258
+ throw new WebsiteError(
3259
+ "Static catalog changed while it was being collected. Retry the build to get a consistent snapshot.",
3260
+ "STATIC_CATALOG_CHANGED_DURING_BUILD"
3261
+ );
2571
3262
  }
2572
3263
 
2573
3264
  // libs/sdk/src/modules/catalog.ts
2574
3265
  var MAX_AVAILABILITY_VARIANT_IDS2 = 100;
2575
3266
  var DEFAULT_FILTER_ATTRIBUTE_LIST_KEY = "atributos";
3267
+ var MAX_LIVE_STATE_VARIANT_IDS = 100;
3268
+ var STATIC_CATALOG_RETRY = {
3269
+ maxAttempts: 4,
3270
+ maxTotalWaitMs: 3 * 60 * 1e3,
3271
+ jitterRatio: 0.1
3272
+ };
2576
3273
  function mapPrice(raw) {
2577
3274
  return {
2578
3275
  id: raw.id,
@@ -2605,6 +3302,9 @@ function getProductImages(media) {
2605
3302
  function getProductVideos(media) {
2606
3303
  return getProductMediaByKind(media, "video");
2607
3304
  }
3305
+ function getProductDetailVariants(product) {
3306
+ return product.detailVariants ?? product.variants;
3307
+ }
2608
3308
  function getPrimaryProductImage(media) {
2609
3309
  return getProductImages(media)[0] ?? null;
2610
3310
  }
@@ -2762,6 +3462,9 @@ function mapRelatedProduct(raw, storageUrl) {
2762
3462
  };
2763
3463
  }
2764
3464
  function mapProduct(raw, storageUrl, labels, chainId) {
3465
+ const detailVariants = raw.product_variants.map((variant) => mapVariant(variant, labels, chainId)).filter((variant) => variant.pricing.currentPrice != null);
3466
+ const listingVariantIds = raw.listing_variant_ids ? new Set(raw.listing_variant_ids) : null;
3467
+ const variants = listingVariantIds ? detailVariants.filter((variant) => listingVariantIds.has(variant.id)) : detailVariants;
2765
3468
  return {
2766
3469
  id: raw.id,
2767
3470
  name: raw.name,
@@ -2778,9 +3481,8 @@ function mapProduct(raw, storageUrl, labels, chainId) {
2778
3481
  measurementUnit: raw.measurement_unit,
2779
3482
  createdAt: raw.created_at,
2780
3483
  updatedAt: raw.updated_at ?? raw.created_at,
2781
- variants: raw.product_variants.map(
2782
- (variant) => mapVariant(variant, labels, chainId)
2783
- ),
3484
+ variants,
3485
+ detailVariants,
2784
3486
  media: raw.product_media.map((m) => mapMedia(m, storageUrl)),
2785
3487
  attributes: raw.product_attributes.map(
2786
3488
  (attribute) => mapProductAttribute(attribute, labels)
@@ -3223,15 +3925,171 @@ function createCatalogModule(graphqlUrl, getHeaders, getContext, storageUrl) {
3223
3925
  const publicAttributeLabels = buildPublicAttributeLabels(
3224
3926
  data.catalog_attribute_lists
3225
3927
  );
3928
+ const mappedProduct = mapProduct(
3929
+ product,
3930
+ storageUrl,
3931
+ publicAttributeLabels,
3932
+ ctx.chainId
3933
+ );
3226
3934
  const filteredProducts = await filterProductsForWebsiteStock(
3227
3935
  graphqlUrl,
3228
3936
  getHeaders,
3229
- [mapProduct(product, storageUrl, publicAttributeLabels, ctx.chainId)],
3937
+ [mappedProduct],
3230
3938
  ctx.chainId
3231
3939
  );
3232
- return filteredProducts[0] ?? null;
3940
+ if (!filteredProducts[0]) return null;
3941
+ return {
3942
+ ...mappedProduct,
3943
+ variants: getProductDetailVariants(mappedProduct)
3944
+ };
3945
+ };
3946
+ const loadCatalogPage = async (opts, retryRateLimits = false) => {
3947
+ const ctx = getContext();
3948
+ if (!ctx.chainId) {
3949
+ throw new WebsiteError(
3950
+ "This website has no chain configured. Catalog requires a chain_id.",
3951
+ "NO_CHAIN"
3952
+ );
3953
+ }
3954
+ const chainId = ctx.chainId;
3955
+ const filters = {
3956
+ category_slug: opts.categorySlug,
3957
+ subcategory_slug: opts.subcategorySlug,
3958
+ tags: opts.tags,
3959
+ search: opts.search,
3960
+ brand_id: opts.brandId,
3961
+ min_price: opts.minPrice,
3962
+ max_price: opts.maxPrice,
3963
+ attributes_by_slug: opts.attributesBySlug,
3964
+ sort_by: opts.sortBy,
3965
+ limit: opts.limit,
3966
+ offset: opts.offset
3967
+ };
3968
+ const data = await executeGraphQL(
3969
+ graphqlUrl,
3970
+ WEBSITE_GET_CATALOG_PAGE,
3971
+ {
3972
+ websiteId: ctx.websiteId,
3973
+ categoryListKey: opts.categoryListKey,
3974
+ attributeListKey: opts.attributeListKey,
3975
+ filters,
3976
+ include: opts.include
3977
+ },
3978
+ getHeaders,
3979
+ retryRateLimits ? { retry: STATIC_CATALOG_RETRY } : void 0
3980
+ );
3981
+ if (!data.result.success) {
3982
+ throw new WebsiteError(
3983
+ data.result.errors.join("; ") || "Unable to resolve catalog page",
3984
+ "CATALOG_PAGE_ERROR"
3985
+ );
3986
+ }
3987
+ const products = parseActionJson(data.result.products, []);
3988
+ const categoryList = parseActionJson(
3989
+ data.result.category_list,
3990
+ null
3991
+ );
3992
+ const attributeList = parseActionJson(
3993
+ data.result.attribute_list,
3994
+ null
3995
+ );
3996
+ const filtersResult = parseActionJson(
3997
+ data.result.filters,
3998
+ null
3999
+ );
4000
+ const brands = parseActionJson(data.result.brands, []);
4001
+ const pagination = parseActionJson(
4002
+ data.result.pagination,
4003
+ {
4004
+ limit: opts.limit ?? 24,
4005
+ offset: opts.offset ?? 0,
4006
+ total: 0,
4007
+ hasMore: false
4008
+ }
4009
+ );
4010
+ const appliedFilters = parseActionJson(
4011
+ data.result.applied_filters,
4012
+ {}
4013
+ );
4014
+ const debug = parseActionJson(
4015
+ data.result.debug,
4016
+ null
4017
+ );
4018
+ const labels = buildPublicAttributeLabels(
4019
+ attributeList ? [attributeList] : []
4020
+ );
4021
+ return {
4022
+ products: products.map(
4023
+ (product) => mapProduct(product, storageUrl, labels, chainId)
4024
+ ),
4025
+ categories: categoryList ? mapCategoryList(categoryList) : null,
4026
+ attributes: attributeList ? mapAttributeList(attributeList) : null,
4027
+ filters: filtersResult,
4028
+ brands,
4029
+ pagination,
4030
+ appliedFilters,
4031
+ debug
4032
+ };
3233
4033
  };
3234
4034
  return {
4035
+ async getProductLiveState(productVariantIds) {
4036
+ const ctx = getContext();
4037
+ if (!ctx.chainId) {
4038
+ throw new WebsiteError(
4039
+ "This website has no chain configured. Product live state requires a chain_id.",
4040
+ "NO_CHAIN"
4041
+ );
4042
+ }
4043
+ const chainId = ctx.chainId;
4044
+ const variantIds = [...new Set(productVariantIds)];
4045
+ if (variantIds.length === 0) return [];
4046
+ if (variantIds.length > MAX_LIVE_STATE_VARIANT_IDS) {
4047
+ throw new WebsiteError(
4048
+ `Product live state accepts at most ${MAX_LIVE_STATE_VARIANT_IDS} variants per request.`,
4049
+ "PRODUCT_LIVE_STATE_LIMIT_EXCEEDED"
4050
+ );
4051
+ }
4052
+ const data = await executeGraphQL(
4053
+ graphqlUrl,
4054
+ GET_PRODUCT_LIVE_STATE,
4055
+ {
4056
+ variantIds,
4057
+ companyId: ctx.companyId,
4058
+ chainId,
4059
+ websiteId: ctx.websiteId
4060
+ },
4061
+ getHeaders
4062
+ );
4063
+ const variantsById = new Map(
4064
+ (data.product_variants ?? []).map((variant) => [variant.id, variant])
4065
+ );
4066
+ const availabilityById = new Map(
4067
+ (data.websiteProductAvailability ?? []).map((availability) => [
4068
+ availability.product_variant_id,
4069
+ availability
4070
+ ])
4071
+ );
4072
+ return variantIds.flatMap((variantId) => {
4073
+ const variant = variantsById.get(variantId);
4074
+ if (!variant) return [];
4075
+ const availability = availabilityById.get(variantId);
4076
+ const pricing = resolveProductPricing(
4077
+ selectPublicPrices(
4078
+ variant.product_variant_prices.map(mapPrice),
4079
+ chainId
4080
+ )
4081
+ );
4082
+ return [
4083
+ {
4084
+ productVariantId: variantId,
4085
+ inStock: availability?.in_stock ?? false,
4086
+ availabilityLabel: availability?.label ?? "Agotado",
4087
+ maxPurchasableQuantity: availability?.max_purchasable_quantity ?? 0,
4088
+ pricing
4089
+ }
4090
+ ];
4091
+ });
4092
+ },
3235
4093
  async getProductRecommendations(opts) {
3236
4094
  const ctx = getContext();
3237
4095
  const limit = opts.limit ?? 16;
@@ -3306,86 +4164,36 @@ function createCatalogModule(graphqlUrl, getHeaders, getContext, storageUrl) {
3306
4164
  }
3307
4165
  };
3308
4166
  },
3309
- async getCatalogPage(opts) {
4167
+ getCatalogPage: loadCatalogPage,
4168
+ async getStaticCatalogSnapshot(opts) {
3310
4169
  const ctx = getContext();
3311
- if (!ctx.chainId) {
3312
- throw new WebsiteError(
3313
- "This website has no chain configured. Catalog requires a chain_id.",
3314
- "NO_CHAIN"
3315
- );
3316
- }
3317
- const chainId = ctx.chainId;
3318
- const filters = {
3319
- category_slug: opts.categorySlug,
3320
- subcategory_slug: opts.subcategorySlug,
3321
- tags: opts.tags,
3322
- search: opts.search,
3323
- brand_id: opts.brandId,
3324
- min_price: opts.minPrice,
3325
- max_price: opts.maxPrice,
3326
- attributes_by_slug: opts.attributesBySlug,
3327
- sort_by: opts.sortBy,
3328
- limit: opts.limit,
3329
- offset: opts.offset
3330
- };
3331
- const data = await executeGraphQL(
3332
- graphqlUrl,
3333
- WEBSITE_GET_CATALOG_PAGE,
3334
- {
3335
- websiteId: ctx.websiteId,
3336
- categoryListKey: opts.categoryListKey,
3337
- attributeListKey: opts.attributeListKey,
3338
- filters,
3339
- include: opts.include
4170
+ return (await collectStaticCatalog({
4171
+ websiteId: ctx.websiteId,
4172
+ snapshot: opts,
4173
+ loadPage: (options) => loadCatalogPage(options, true)
4174
+ })).snapshot;
4175
+ },
4176
+ async getStaticCatalogBootstrap(opts) {
4177
+ const ctx = getContext();
4178
+ const { snapshot, initialPage } = await collectStaticCatalog({
4179
+ websiteId: ctx.websiteId,
4180
+ snapshot: opts,
4181
+ bootstrap: {
4182
+ initialPageSize: opts.initialPageSize,
4183
+ sortBy: opts.sortBy
3340
4184
  },
3341
- getHeaders
3342
- );
3343
- if (!data.result.success) {
4185
+ loadPage: (options) => loadCatalogPage(options, true)
4186
+ });
4187
+ if (!initialPage) {
3344
4188
  throw new WebsiteError(
3345
- data.result.errors.join("; ") || "Unable to resolve catalog page",
3346
- "CATALOG_PAGE_ERROR"
4189
+ "Static catalog bootstrap could not resolve its initial page.",
4190
+ "STATIC_CATALOG_BOOTSTRAP_MISSING_INITIAL_PAGE"
3347
4191
  );
3348
4192
  }
3349
- const products = parseActionJson(data.result.products, []);
3350
- const categoryList = parseActionJson(
3351
- data.result.category_list,
3352
- null
3353
- );
3354
- const attributeList = parseActionJson(
3355
- data.result.attribute_list,
3356
- null
3357
- );
3358
- const filtersResult = parseActionJson(
3359
- data.result.filters,
3360
- null
3361
- );
3362
- const brands = parseActionJson(data.result.brands, []);
3363
- const pagination = parseActionJson(
3364
- data.result.pagination,
3365
- { limit: opts.limit ?? 24, offset: opts.offset ?? 0, total: 0, hasMore: false }
3366
- );
3367
- const appliedFilters = parseActionJson(
3368
- data.result.applied_filters,
3369
- {}
3370
- );
3371
- const debug = parseActionJson(
3372
- data.result.debug,
3373
- null
3374
- );
3375
- const labels = buildPublicAttributeLabels(
3376
- attributeList ? [attributeList] : []
3377
- );
3378
4193
  return {
3379
- products: products.map(
3380
- (product) => mapProduct(product, storageUrl, labels, chainId)
3381
- ),
3382
- categories: categoryList ? mapCategoryList(categoryList) : null,
3383
- attributes: attributeList ? mapAttributeList(attributeList) : null,
3384
- filters: filtersResult,
3385
- brands,
3386
- pagination,
3387
- appliedFilters,
3388
- debug
4194
+ ...snapshot,
4195
+ bootstrapSchemaVersion: 1,
4196
+ initialPage
3389
4197
  };
3390
4198
  },
3391
4199
  async getProducts(opts) {
@@ -3852,6 +4660,7 @@ function mapCouponResult(raw) {
3852
4660
  };
3853
4661
  }
3854
4662
  function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionStorage) {
4663
+ let shippingPolicyCache = null;
3855
4664
  return {
3856
4665
  async getLocationOptions() {
3857
4666
  const context = getContext();
@@ -3949,22 +4758,39 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
3949
4758
  }));
3950
4759
  },
3951
4760
  async getShippingPolicy() {
4761
+ const now = Date.now();
4762
+ if (shippingPolicyCache && shippingPolicyCache.expiresAt > now) {
4763
+ return shippingPolicyCache.request;
4764
+ }
3952
4765
  const context = getContext();
3953
- const data = await executeGraphQL(
4766
+ const request = executeGraphQL(
3954
4767
  graphqlUrl,
3955
4768
  GET_SHIPPING_POLICY,
3956
4769
  { websiteId: context.websiteId },
3957
4770
  getHeaders
3958
- );
3959
- const result = data.websiteGetShippingPolicy;
3960
- if (!result.success || !result.policy) {
3961
- throw new CheckoutError(result.errors ?? ["SHIPPING_POLICY_FAILED"]);
3962
- }
3963
- return {
3964
- freeShippingEnabled: result.policy.free_shipping_enabled,
3965
- freeShippingThreshold: result.policy.free_shipping_threshold,
3966
- currency: result.policy.currency
4771
+ ).then((data) => {
4772
+ const result = data.websiteGetShippingPolicy;
4773
+ if (!result.success || !result.policy) {
4774
+ throw new CheckoutError(result.errors ?? ["SHIPPING_POLICY_FAILED"]);
4775
+ }
4776
+ return {
4777
+ freeShippingEnabled: result.policy.free_shipping_enabled,
4778
+ freeShippingThreshold: result.policy.free_shipping_threshold,
4779
+ currency: result.policy.currency
4780
+ };
4781
+ });
4782
+ shippingPolicyCache = {
4783
+ expiresAt: now + 5 * 60 * 1e3,
4784
+ request
3967
4785
  };
4786
+ try {
4787
+ return await request;
4788
+ } catch (error) {
4789
+ if (shippingPolicyCache?.request === request) {
4790
+ shippingPolicyCache = null;
4791
+ }
4792
+ throw error;
4793
+ }
3968
4794
  },
3969
4795
  async getFreeShippingProgress(input) {
3970
4796
  if (input.items.length === 0) {
@@ -4130,6 +4956,13 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
4130
4956
  initialPayment: input.initialPayment ? {
4131
4957
  amount: input.initialPayment.amount,
4132
4958
  payment_method_id: input.initialPayment.paymentMethodId
4959
+ } : void 0,
4960
+ marketingAttribution: input.marketingAttribution ? serializeWebsiteMarketingAttribution(input.marketingAttribution) : void 0,
4961
+ advertisingConsent: input.advertisingConsent ? {
4962
+ policy_id: input.advertisingConsent.policyId,
4963
+ policy_version: input.advertisingConsent.policyVersion,
4964
+ statement_hash: input.advertisingConsent.statementHash,
4965
+ granted: input.advertisingConsent.granted
4133
4966
  } : void 0
4134
4967
  },
4135
4968
  getHeaders
@@ -4265,18 +5098,23 @@ function readOptionalText(value) {
4265
5098
  function enrichMediaValue(value, storageUrl) {
4266
5099
  const fileId = typeof value.fileId === "string" && value.fileId.trim() ? value.fileId.trim() : "";
4267
5100
  if (!fileId) return value;
4268
- const urls = buildStorageMediaUrls(storageUrl, fileId);
5101
+ const isProductMedia = typeof value.id === "string" && typeof value.order === "number";
5102
+ const isEditorialImage = value.mediaKind === "image" && !isProductMedia;
5103
+ const originalWidth = typeof value.originalWidth === "number" ? value.originalWidth : null;
5104
+ const urls = isEditorialImage ? buildStorageContentMediaUrls(storageUrl, fileId, originalWidth) : buildStorageMediaUrls(storageUrl, fileId);
4269
5105
  return {
4270
5106
  ...value,
4271
5107
  ...urls,
4272
- url: value.url ?? urls.originalUrl
5108
+ url: value.url ?? urls.originalUrl,
5109
+ src: value.src ?? value.url ?? urls.originalUrl
4273
5110
  };
4274
5111
  }
4275
5112
  function normalizeRichTextNode(value, storageUrl) {
4276
5113
  const type = value.type;
4277
5114
  const attrs = isRecord(value.attrs) ? value.attrs : null;
4278
5115
  if (attrs && (type === "bacanoImage" || type === "bacanoVideo") && typeof attrs.fileId === "string") {
4279
- const urls = buildStorageMediaUrls(storageUrl, attrs.fileId);
5116
+ const originalWidth = typeof attrs.originalWidth === "number" ? attrs.originalWidth : null;
5117
+ const urls = type === "bacanoImage" ? buildStorageContentMediaUrls(storageUrl, attrs.fileId, originalWidth) : buildStorageMediaUrls(storageUrl, attrs.fileId);
4280
5118
  const altText = readOptionalText(attrs.altText) ?? readOptionalText(attrs.alt);
4281
5119
  const caption = readOptionalText(attrs.caption);
4282
5120
  const showCaption = Boolean(caption) && attrs.showCaption !== false;
@@ -4408,6 +5246,452 @@ function createContentModule(graphqlUrl, getHeaders, getContext, storageUrl) {
4408
5246
  };
4409
5247
  }
4410
5248
 
5249
+ // libs/sdk/src/marketing/clarity.ts
5250
+ var initializedDestinations = /* @__PURE__ */ new Set();
5251
+ function clarityWindow() {
5252
+ return typeof window === "undefined" ? null : window;
5253
+ }
5254
+ function prepareClarity(projectId) {
5255
+ const target = clarityWindow();
5256
+ if (!target || initializedDestinations.has(projectId)) return;
5257
+ if (!target.clarity) {
5258
+ const clarity = function() {
5259
+ clarity.q ??= [];
5260
+ clarity.q.push(arguments);
5261
+ };
5262
+ target.clarity = clarity;
5263
+ }
5264
+ initializedDestinations.add(projectId);
5265
+ }
5266
+ function loadClarityScript(projectId) {
5267
+ if (typeof document === "undefined") return;
5268
+ const id = `bacano-clarity-${projectId}`;
5269
+ if (document.getElementById(id)) return;
5270
+ const script = document.createElement("script");
5271
+ script.id = id;
5272
+ script.async = true;
5273
+ script.src = `https://www.clarity.ms/tag/${encodeURIComponent(projectId)}`;
5274
+ script.referrerPolicy = "strict-origin-when-cross-origin";
5275
+ document.head.appendChild(script);
5276
+ }
5277
+ function sendClarityEvent(eventName) {
5278
+ clarityWindow()?.clarity?.("event", eventName);
5279
+ }
5280
+
5281
+ // libs/sdk/src/marketing/domain.ts
5282
+ function matchesCanonicalMarketingHost(canonicalDomain, currentHostname) {
5283
+ if (canonicalDomain === void 0) return true;
5284
+ if (!canonicalDomain?.trim()) return false;
5285
+ try {
5286
+ const configuredHost = new URL(
5287
+ canonicalDomain.includes("://") ? canonicalDomain : `https://${canonicalDomain}`
5288
+ ).hostname.toLowerCase();
5289
+ return currentHostname.toLowerCase() === configuredHost;
5290
+ } catch {
5291
+ return false;
5292
+ }
5293
+ }
5294
+
5295
+ // libs/sdk/src/marketing/ga4.ts
5296
+ function mapGa4Items(items) {
5297
+ return items?.map((item) => ({
5298
+ item_id: item.itemId,
5299
+ item_name: item.itemName,
5300
+ item_variant: item.itemVariant,
5301
+ item_brand: item.itemBrand,
5302
+ item_category: item.itemCategory,
5303
+ price: item.price,
5304
+ quantity: item.quantity,
5305
+ index: item.index,
5306
+ sku: item.sku
5307
+ }));
5308
+ }
5309
+
5310
+ // libs/sdk/src/marketing/types.ts
5311
+ var MARKETING_SCHEMA_VERSION = 1;
5312
+ var WEBSITE_MARKETING_CONTACT_PLACEMENTS = [
5313
+ "product_detail",
5314
+ "floating",
5315
+ "header",
5316
+ "footer",
5317
+ "contact_page"
5318
+ ];
5319
+
5320
+ // libs/sdk/src/marketing/runtime.ts
5321
+ var initializedGa4Destinations = /* @__PURE__ */ new Set();
5322
+ var initializedMetaDestinations = /* @__PURE__ */ new Set();
5323
+ function marketingWindow() {
5324
+ return typeof window === "undefined" ? null : window;
5325
+ }
5326
+ function loadScriptOnce(id, source) {
5327
+ if (typeof document === "undefined" || document.getElementById(id)) return;
5328
+ const script = document.createElement("script");
5329
+ script.id = id;
5330
+ script.async = true;
5331
+ script.src = source;
5332
+ script.referrerPolicy = "strict-origin-when-cross-origin";
5333
+ document.head.appendChild(script);
5334
+ }
5335
+ function prepareGa4(measurementId) {
5336
+ const target = marketingWindow();
5337
+ if (!target) return;
5338
+ if (initializedGa4Destinations.has(measurementId)) return;
5339
+ target.dataLayer ??= [];
5340
+ target.gtag ??= function() {
5341
+ target.dataLayer?.push(arguments);
5342
+ };
5343
+ target.gtag("js", /* @__PURE__ */ new Date());
5344
+ target.gtag("config", measurementId, { send_page_view: false });
5345
+ initializedGa4Destinations.add(measurementId);
5346
+ }
5347
+ function loadGa4Script(measurementId) {
5348
+ loadScriptOnce(
5349
+ `bacano-ga4-${measurementId}`,
5350
+ `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`
5351
+ );
5352
+ }
5353
+ function captureGa4ClientId(measurementId, websiteId, diagnostic) {
5354
+ const target = marketingWindow();
5355
+ target?.gtag?.("get", measurementId, "client_id", (clientId) => {
5356
+ if (storeWebsiteMarketingGa4ClientId(websiteId, clientId)) return;
5357
+ diagnostic?.({
5358
+ level: "warning",
5359
+ code: "GA4_CLIENT_ID_UNAVAILABLE",
5360
+ message: "GA4 did not provide a valid browser client identifier"
5361
+ });
5362
+ });
5363
+ }
5364
+ function captureGa4SessionId(measurementId, websiteId, diagnostic) {
5365
+ const target = marketingWindow();
5366
+ target?.gtag?.("get", measurementId, "session_id", (sessionId) => {
5367
+ if (storeWebsiteMarketingGa4SessionId(websiteId, sessionId)) return;
5368
+ diagnostic?.({
5369
+ level: "warning",
5370
+ code: "GA4_SESSION_ID_UNAVAILABLE",
5371
+ message: "GA4 did not provide a valid browser session identifier"
5372
+ });
5373
+ });
5374
+ }
5375
+ function prepareMeta(pixelId) {
5376
+ const target = marketingWindow();
5377
+ if (!target) return;
5378
+ if (initializedMetaDestinations.has(pixelId)) return;
5379
+ if (!target.fbq) {
5380
+ const queue = [];
5381
+ const fbq = ((...args) => {
5382
+ if (fbq.callMethod) {
5383
+ fbq.callMethod(...args);
5384
+ return;
5385
+ }
5386
+ queue.push(args);
5387
+ });
5388
+ fbq.push = fbq;
5389
+ fbq.loaded = true;
5390
+ fbq.version = "2.0";
5391
+ fbq.queue = queue;
5392
+ target.fbq = fbq;
5393
+ target._fbq = fbq;
5394
+ }
5395
+ target.fbq("init", pixelId);
5396
+ initializedMetaDestinations.add(pixelId);
5397
+ }
5398
+ function loadMetaScript() {
5399
+ loadScriptOnce(
5400
+ "bacano-meta-pixel",
5401
+ "https://connect.facebook.net/en_US/fbevents.js"
5402
+ );
5403
+ }
5404
+ function scheduleScriptLoad(load, strategy) {
5405
+ const target = marketingWindow();
5406
+ if (!target || strategy !== "idle") {
5407
+ load();
5408
+ return () => void 0;
5409
+ }
5410
+ let completed = false;
5411
+ let idleHandle = null;
5412
+ let timeoutHandle = null;
5413
+ const interactionEvents = ["pointerdown", "keydown", "touchstart"];
5414
+ const cleanup = () => {
5415
+ for (const eventName of interactionEvents) {
5416
+ target.removeEventListener(eventName, finish, true);
5417
+ }
5418
+ if (idleHandle !== null) target.cancelIdleCallback?.(idleHandle);
5419
+ if (timeoutHandle !== null) target.clearTimeout(timeoutHandle);
5420
+ };
5421
+ const finish = () => {
5422
+ if (completed) return;
5423
+ completed = true;
5424
+ cleanup();
5425
+ load();
5426
+ };
5427
+ for (const eventName of interactionEvents) {
5428
+ target.addEventListener(eventName, finish, {
5429
+ capture: true,
5430
+ once: true,
5431
+ passive: true
5432
+ });
5433
+ }
5434
+ if (target.requestIdleCallback) {
5435
+ idleHandle = target.requestIdleCallback(finish, { timeout: 2e3 });
5436
+ } else {
5437
+ timeoutHandle = target.setTimeout(finish, 1500);
5438
+ }
5439
+ return cleanup;
5440
+ }
5441
+ function metaEventName(eventName) {
5442
+ const standard = {
5443
+ page_view: "PageView",
5444
+ search: "Search",
5445
+ view_item: "ViewContent",
5446
+ add_to_cart: "AddToCart",
5447
+ begin_checkout: "InitiateCheckout",
5448
+ add_payment_info: "AddPaymentInfo",
5449
+ contact: "Contact"
5450
+ };
5451
+ return standard[eventName] ?? eventName;
5452
+ }
5453
+ function isWebsiteMarketingContactPlacement(value) {
5454
+ return WEBSITE_MARKETING_CONTACT_PLACEMENTS.some(
5455
+ (candidate) => candidate === value
5456
+ );
5457
+ }
5458
+ function contactPlacementFor(event) {
5459
+ const placement = event.contactPlacement;
5460
+ if (event.eventName !== "contact" || !isWebsiteMarketingContactPlacement(placement)) {
5461
+ return void 0;
5462
+ }
5463
+ return placement;
5464
+ }
5465
+ function sendGa4(event, diagnosticMode) {
5466
+ const target = marketingWindow();
5467
+ const contactPlacement = contactPlacementFor(event);
5468
+ target?.gtag?.("event", event.eventName, {
5469
+ currency: event.currency,
5470
+ value: event.value,
5471
+ search_term: event.searchTerm,
5472
+ item_list_id: event.itemListId,
5473
+ item_list_name: event.itemListName,
5474
+ shipping_tier: event.shippingTier,
5475
+ payment_type: event.paymentType,
5476
+ method: event.contactMethod,
5477
+ ...contactPlacement ? { contact_placement: contactPlacement } : void 0,
5478
+ page_location: event.pageLocation,
5479
+ page_title: event.pageTitle,
5480
+ items: mapGa4Items(event.items),
5481
+ event_id: event.eventId,
5482
+ debug_mode: diagnosticMode ? true : void 0
5483
+ });
5484
+ }
5485
+ function sendMeta(event) {
5486
+ const target = marketingWindow();
5487
+ const eventName = metaEventName(event.eventName);
5488
+ const isStandard = eventName !== event.eventName;
5489
+ const contactPlacement = contactPlacementFor(event);
5490
+ target?.fbq?.(
5491
+ isStandard ? "track" : "trackCustom",
5492
+ eventName,
5493
+ {
5494
+ currency: event.currency,
5495
+ value: event.value,
5496
+ search_string: event.searchTerm,
5497
+ content_ids: event.items?.map((item) => item.itemId),
5498
+ contents: event.items?.map((item) => ({
5499
+ id: item.itemId,
5500
+ quantity: item.quantity ?? 1,
5501
+ item_price: item.price
5502
+ })),
5503
+ content_type: event.items?.length ? "product" : void 0,
5504
+ contact_method: event.contactMethod,
5505
+ ...contactPlacement ? { contact_placement: contactPlacement } : void 0
5506
+ },
5507
+ { eventID: event.eventId }
5508
+ );
5509
+ }
5510
+ function newEventId() {
5511
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`;
5512
+ }
5513
+ function createWebsiteMarketingRuntime(options) {
5514
+ let destroyed = false;
5515
+ let cancelScheduledLoad = () => void 0;
5516
+ const target = marketingWindow();
5517
+ const enabled = options.config.enabled && Boolean(
5518
+ target && matchesCanonicalMarketingHost(
5519
+ options.canonicalDomain,
5520
+ target.location.hostname
5521
+ )
5522
+ ) && Boolean(
5523
+ options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity
5524
+ );
5525
+ if (enabled) {
5526
+ try {
5527
+ if (options.config.destinations.ga4) {
5528
+ prepareGa4(options.config.destinations.ga4.measurementId);
5529
+ }
5530
+ if (options.config.destinations.meta) {
5531
+ prepareMeta(options.config.destinations.meta.pixelId);
5532
+ }
5533
+ if (options.config.destinations.clarity) {
5534
+ prepareClarity(options.config.destinations.clarity.projectId);
5535
+ }
5536
+ cancelScheduledLoad = scheduleScriptLoad(() => {
5537
+ if (destroyed) return;
5538
+ if (options.config.destinations.ga4) {
5539
+ loadGa4Script(options.config.destinations.ga4.measurementId);
5540
+ }
5541
+ if (options.config.destinations.meta) loadMetaScript();
5542
+ if (options.config.destinations.clarity) {
5543
+ loadClarityScript(options.config.destinations.clarity.projectId);
5544
+ }
5545
+ }, options.scriptLoadStrategy);
5546
+ captureWebsiteMarketingAttribution(
5547
+ options.websiteId,
5548
+ options.config.attributionWindowDays
5549
+ );
5550
+ if (options.config.destinations.ga4) {
5551
+ captureGa4ClientId(
5552
+ options.config.destinations.ga4.measurementId,
5553
+ options.websiteId,
5554
+ options.diagnostic
5555
+ );
5556
+ captureGa4SessionId(
5557
+ options.config.destinations.ga4.measurementId,
5558
+ options.websiteId,
5559
+ options.diagnostic
5560
+ );
5561
+ }
5562
+ if (options.config.destinations.meta) {
5563
+ refreshWebsiteMarketingMetaContext(options.websiteId);
5564
+ }
5565
+ } catch (error) {
5566
+ options.diagnostic?.({
5567
+ level: "warning",
5568
+ code: "RUNTIME_INITIALIZATION_FAILED",
5569
+ message: error instanceof Error ? error.message : "Initialization failed"
5570
+ });
5571
+ }
5572
+ }
5573
+ return {
5574
+ enabled,
5575
+ track(input) {
5576
+ if (!enabled || destroyed) return;
5577
+ const event = {
5578
+ ...input,
5579
+ schemaVersion: 1,
5580
+ eventId: input.eventId ?? newEventId(),
5581
+ occurredAt: input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
5582
+ currency: input.currency ?? options.config.currencyCode,
5583
+ pageLocation: sanitizeMarketingUrl(
5584
+ input.pageLocation ?? (typeof window === "undefined" ? "" : window.location.href)
5585
+ )
5586
+ };
5587
+ try {
5588
+ if (options.config.destinations.ga4) {
5589
+ sendGa4(event, options.config.destinations.ga4.diagnosticMode);
5590
+ }
5591
+ if (options.config.destinations.meta) sendMeta(event);
5592
+ if (options.config.destinations.clarity) {
5593
+ sendClarityEvent(event.eventName);
5594
+ }
5595
+ options.diagnostic?.({
5596
+ level: "debug",
5597
+ code: "EVENT_DISPATCHED",
5598
+ message: "Marketing event dispatched",
5599
+ eventName: event.eventName
5600
+ });
5601
+ } catch (error) {
5602
+ options.diagnostic?.({
5603
+ level: "warning",
5604
+ code: "EVENT_DISPATCH_FAILED",
5605
+ message: error instanceof Error ? error.message : "Dispatch failed",
5606
+ eventName: event.eventName
5607
+ });
5608
+ }
5609
+ },
5610
+ captureAttribution() {
5611
+ if (!enabled) return null;
5612
+ captureWebsiteMarketingAttribution(
5613
+ options.websiteId,
5614
+ options.config.attributionWindowDays
5615
+ );
5616
+ if (options.config.destinations.meta) {
5617
+ refreshWebsiteMarketingMetaContext(options.websiteId);
5618
+ }
5619
+ return getWebsiteMarketingAttribution(options.websiteId);
5620
+ },
5621
+ getAttribution() {
5622
+ if (!enabled) return null;
5623
+ if (options.config.destinations.meta) {
5624
+ refreshWebsiteMarketingMetaContext(options.websiteId);
5625
+ }
5626
+ return getWebsiteMarketingAttribution(options.websiteId);
5627
+ },
5628
+ destroy() {
5629
+ destroyed = true;
5630
+ cancelScheduledLoad();
5631
+ }
5632
+ };
5633
+ }
5634
+
5635
+ // libs/sdk/src/modules/marketing.ts
5636
+ function readRetryAfter(response) {
5637
+ const value = Number(response.headers.get("Retry-After") ?? "1");
5638
+ return Number.isFinite(value) && value >= 0 ? Math.ceil(value) : 1;
5639
+ }
5640
+ function createMarketingModule(apiUrl, websiteSlug, getContext) {
5641
+ return {
5642
+ async captureLead(input) {
5643
+ const context = getContext();
5644
+ const attribution = getWebsiteMarketingAttribution(context.websiteId);
5645
+ let response;
5646
+ try {
5647
+ response = await fetch(
5648
+ `${apiUrl.replace(/\/+$/, "")}/api/v1/website/campaign-captures`,
5649
+ {
5650
+ method: "POST",
5651
+ headers: { "Content-Type": "application/json" },
5652
+ body: JSON.stringify({
5653
+ websiteSlug,
5654
+ campaignKey: input.campaignKey,
5655
+ idempotencyKey: input.idempotencyKey,
5656
+ source: input.source ?? "WEBSITE",
5657
+ commercialConsent: input.commercialConsent,
5658
+ contact: input.contact,
5659
+ answers: input.answers ?? {},
5660
+ attribution: attribution ? serializeWebsiteMarketingAttribution(attribution) : null
5661
+ })
5662
+ }
5663
+ );
5664
+ } catch (error) {
5665
+ throw new WebsiteError(
5666
+ `Could not capture the campaign lead: ${error instanceof Error ? error.message : String(error)}`,
5667
+ "NETWORK_ERROR"
5668
+ );
5669
+ }
5670
+ if (response.status === 429) {
5671
+ throw new RateLimitError(
5672
+ "Rate limit exceeded. Please try again later.",
5673
+ readRetryAfter(response)
5674
+ );
5675
+ }
5676
+ if (!response.ok) {
5677
+ throw new WebsiteError(
5678
+ `Could not capture the campaign lead (status ${response.status})`,
5679
+ response.status === 403 ? "ORIGIN_NOT_ALLOWED" : "INVALID_RESPONSE",
5680
+ { status: response.status }
5681
+ );
5682
+ }
5683
+ const result = await response.json();
5684
+ if (typeof result.accepted !== "boolean" || typeof result.message !== "string") {
5685
+ throw new WebsiteError(
5686
+ "Campaign capture returned an invalid response",
5687
+ "INVALID_RESPONSE"
5688
+ );
5689
+ }
5690
+ return { accepted: result.accepted, message: result.message };
5691
+ }
5692
+ };
5693
+ }
5694
+
4411
5695
  // libs/sdk/src/modules/orders.ts
4412
5696
  function mapOrderItem(raw) {
4413
5697
  return {
@@ -4582,13 +5866,13 @@ function createOrdersModule(graphqlUrl, getHeaders, getContext) {
4582
5866
  )) {
4583
5867
  return order;
4584
5868
  }
4585
- await wait(intervalMs);
5869
+ await wait2(intervalMs);
4586
5870
  }
4587
5871
  throw new CheckoutError(["PAYMENT_STATUS_TIMEOUT"]);
4588
5872
  }
4589
5873
  };
4590
5874
  }
4591
- function wait(ms) {
5875
+ function wait2(ms) {
4592
5876
  return new Promise((resolve) => {
4593
5877
  setTimeout(resolve, ms);
4594
5878
  });
@@ -4747,6 +6031,9 @@ function createProfileModule(graphqlUrl, getHeaders) {
4747
6031
  }
4748
6032
 
4749
6033
  // libs/sdk/src/modules/website.ts
6034
+ var WEBSITE_CONTEXT_CACHE_TTL_MS = 5 * 60 * 1e3;
6035
+ var WEBSITE_CONTEXT_CACHE_MAX_ENTRIES = 100;
6036
+ var websiteContextCache = /* @__PURE__ */ new Map();
4750
6037
  function mapWebsite(raw) {
4751
6038
  return {
4752
6039
  websiteId: raw.id,
@@ -4760,10 +6047,7 @@ function mapWebsite(raw) {
4760
6047
  }
4761
6048
  async function resolveWebsite(graphqlUrl, slug, headers) {
4762
6049
  if (!slug.trim()) {
4763
- throw new WebsiteError(
4764
- "websiteSlug is required",
4765
- "INVALID_CONFIG"
4766
- );
6050
+ throw new WebsiteError("websiteSlug is required", "INVALID_CONFIG");
4767
6051
  }
4768
6052
  const data = await executeGraphQL(
4769
6053
  graphqlUrl,
@@ -4779,6 +6063,31 @@ async function resolveWebsite(graphqlUrl, slug, headers) {
4779
6063
  }
4780
6064
  return mapWebsite(data.websites[0]);
4781
6065
  }
6066
+ function resolveWebsiteCached(graphqlUrl, slug, headers) {
6067
+ const normalizedSlug = slug.trim();
6068
+ const cacheKey = `${graphqlUrl}::${normalizedSlug}`;
6069
+ const now = Date.now();
6070
+ const cached = websiteContextCache.get(cacheKey);
6071
+ if (cached && cached.expiresAt > now) return cached.request;
6072
+ if (cached) websiteContextCache.delete(cacheKey);
6073
+ const request = resolveWebsite(graphqlUrl, normalizedSlug, headers).catch(
6074
+ (error) => {
6075
+ if (websiteContextCache.get(cacheKey)?.request === request) {
6076
+ websiteContextCache.delete(cacheKey);
6077
+ }
6078
+ throw error;
6079
+ }
6080
+ );
6081
+ if (websiteContextCache.size >= WEBSITE_CONTEXT_CACHE_MAX_ENTRIES) {
6082
+ const oldestKey = websiteContextCache.keys().next().value;
6083
+ if (oldestKey) websiteContextCache.delete(oldestKey);
6084
+ }
6085
+ websiteContextCache.set(cacheKey, {
6086
+ expiresAt: now + WEBSITE_CONTEXT_CACHE_TTL_MS,
6087
+ request
6088
+ });
6089
+ return request;
6090
+ }
4782
6091
 
4783
6092
  // libs/sdk/src/client.ts
4784
6093
  function normalizeApiUrl(apiUrl) {
@@ -4789,10 +6098,17 @@ function createBacanoClient(config) {
4789
6098
  throw new WebsiteError("apiUrl is required", "INVALID_CONFIG");
4790
6099
  if (!config.websiteSlug?.trim())
4791
6100
  throw new WebsiteError("websiteSlug is required", "INVALID_CONFIG");
6101
+ if (config.initialContext && config.initialContext.slug.trim() !== config.websiteSlug.trim()) {
6102
+ throw new WebsiteError(
6103
+ "initialContext slug must match websiteSlug",
6104
+ "INVALID_CONFIG"
6105
+ );
6106
+ }
4792
6107
  const apiUrl = normalizeApiUrl(config.apiUrl);
4793
6108
  const graphqlUrl = `${apiUrl}/api/v1/website/graphql`;
4794
6109
  const storageUrl = `${apiUrl}/storage/v1`;
4795
- let context = null;
6110
+ let context = config.initialContext ? { ...config.initialContext } : null;
6111
+ let initialization = null;
4796
6112
  function getContext() {
4797
6113
  if (!context) throw new NotInitializedError();
4798
6114
  return context;
@@ -4837,13 +6153,27 @@ function createBacanoClient(config) {
4837
6153
  getContext,
4838
6154
  storageUrl
4839
6155
  );
6156
+ const marketingModule = createMarketingModule(
6157
+ apiUrl,
6158
+ config.websiteSlug,
6159
+ getContext
6160
+ );
4840
6161
  return {
4841
6162
  async init() {
4842
- context = await resolveWebsite(
4843
- graphqlUrl,
4844
- config.websiteSlug,
4845
- getWebsiteHeaders
4846
- );
6163
+ if (context) return;
6164
+ if (!initialization) {
6165
+ initialization = resolveWebsiteCached(
6166
+ graphqlUrl,
6167
+ config.websiteSlug,
6168
+ getWebsiteHeaders
6169
+ ).then((resolvedContext) => {
6170
+ context = resolvedContext;
6171
+ }).catch((error) => {
6172
+ initialization = null;
6173
+ throw error;
6174
+ });
6175
+ }
6176
+ await initialization;
4847
6177
  },
4848
6178
  getContext,
4849
6179
  auth: {
@@ -4858,8 +6188,11 @@ function createBacanoClient(config) {
4858
6188
  },
4859
6189
  catalog: {
4860
6190
  getCatalogPage: catalogModule.getCatalogPage.bind(catalogModule),
6191
+ getStaticCatalogSnapshot: catalogModule.getStaticCatalogSnapshot.bind(catalogModule),
6192
+ getStaticCatalogBootstrap: catalogModule.getStaticCatalogBootstrap.bind(catalogModule),
4861
6193
  getProductSeoEntries: catalogModule.getProductSeoEntries.bind(catalogModule),
4862
6194
  getProductRecommendations: catalogModule.getProductRecommendations.bind(catalogModule),
6195
+ getProductLiveState: catalogModule.getProductLiveState.bind(catalogModule),
4863
6196
  getProducts: catalogModule.getProducts.bind(catalogModule),
4864
6197
  getProduct: catalogModule.getProduct.bind(catalogModule),
4865
6198
  getProductBySlug: catalogModule.getProductBySlug.bind(catalogModule),
@@ -4911,6 +6244,9 @@ function createBacanoClient(config) {
4911
6244
  get: profileModule.get.bind(profileModule),
4912
6245
  getAddresses: profileModule.getAddresses.bind(profileModule),
4913
6246
  update: profileModule.update.bind(profileModule)
6247
+ },
6248
+ marketing: {
6249
+ captureLead: marketingModule.captureLead.bind(marketingModule)
4914
6250
  }
4915
6251
  };
4916
6252
  }
@@ -4918,16 +6254,30 @@ export {
4918
6254
  AuthError,
4919
6255
  CheckoutError,
4920
6256
  GraphQLRequestError,
6257
+ MARKETING_SCHEMA_VERSION,
4921
6258
  NotInitializedError,
6259
+ RateLimitError,
6260
+ WEBSITE_MARKETING_CONTACT_PLACEMENTS,
4922
6261
  WebsiteError,
6262
+ buildStorageContentMediaUrls,
4923
6263
  buildStorageFileUrl,
4924
6264
  buildStorageMediaUrls,
6265
+ buildStorageResponsiveSources,
6266
+ buildStorageTransformedFileUrl,
6267
+ captureWebsiteMarketingAttribution,
4925
6268
  createBacanoClient,
6269
+ createWebsiteMarketingRuntime,
6270
+ formatMediaSrcSet,
6271
+ getAllowedMarketingParameters,
4926
6272
  getPrimaryProductImage,
6273
+ getProductDetailVariants,
4927
6274
  getProductImages,
4928
6275
  getProductMediaByKind,
4929
6276
  getProductMediaKind,
4930
6277
  getProductVideos,
6278
+ getWebsiteContextFromPublicConfig,
6279
+ getWebsiteMarketingAttribution,
4931
6280
  getWebsitePublicConfig,
4932
- resolveProductPricing
6281
+ resolveProductPricing,
6282
+ sanitizeMarketingUrl
4933
6283
  };