@bacano/sdk 1.0.1 → 1.1.8

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 (72) hide show
  1. package/README.md +439 -18
  2. package/STOREFRONT_CLARITY.md +140 -0
  3. package/STOREFRONT_CONSTRUCTION.md +105 -0
  4. package/STOREFRONT_LEADS.md +130 -0
  5. package/STOREFRONT_LEGAL.md +66 -0
  6. package/STOREFRONT_MARKETING.md +291 -0
  7. package/STOREFRONT_PERFORMANCE.md +304 -0
  8. package/STOREFRONT_PREVIEW.md +499 -0
  9. package/STOREFRONT_PRODUCT_SPECIFICATIONS.md +127 -0
  10. package/STOREFRONT_VOLUME_PRICING.md +131 -0
  11. package/client.d.ts +8 -1
  12. package/errors.d.ts +9 -1
  13. package/graphql.cjs +266 -3
  14. package/graphql.d.ts +2 -2
  15. package/graphql.js +266 -3
  16. package/index.cjs +2035 -255
  17. package/index.d.ts +8 -6
  18. package/index.js +2035 -255
  19. package/marketing/attribution.d.ts +53 -0
  20. package/marketing/clarity.d.ts +3 -0
  21. package/marketing/domain.d.ts +1 -0
  22. package/marketing/ga4.d.ts +2 -0
  23. package/marketing/index.d.ts +5 -0
  24. package/marketing/runtime.d.ts +2 -0
  25. package/marketing/sanitize.d.ts +2 -0
  26. package/marketing/types.d.ts +111 -0
  27. package/media.d.ts +45 -2
  28. package/modules/auth.d.ts +3 -2
  29. package/modules/availability.d.ts +2 -2
  30. package/modules/cart-errors.d.ts +2 -0
  31. package/modules/cart-session.d.ts +2 -2
  32. package/modules/cart.d.ts +4 -3
  33. package/modules/catalog.d.ts +15 -3
  34. package/modules/content.d.ts +2 -2
  35. package/modules/marketing.d.ts +5 -0
  36. package/modules/orders.d.ts +2 -2
  37. package/modules/profile.d.ts +1 -1
  38. package/modules/static-catalog.d.ts +13 -0
  39. package/modules/website.d.ts +7 -1
  40. package/package.json +14 -3
  41. package/react/context-registry.d.ts +9 -0
  42. package/react/context.d.ts +1 -1
  43. package/react/hooks.d.ts +24 -2
  44. package/react/marketing-context.d.ts +3 -0
  45. package/react/marketing-provider.d.ts +10 -0
  46. package/react/preview/cache.d.ts +23 -0
  47. package/react/preview/context.d.ts +5 -0
  48. package/react/preview/navigation.d.ts +9 -0
  49. package/react/preview/provider.d.ts +3 -0
  50. package/react/preview/shell.d.ts +3 -0
  51. package/react/preview/toolbar.d.ts +3 -0
  52. package/react/preview/types.d.ts +60 -0
  53. package/react/preview.cjs +742 -0
  54. package/react/preview.d.ts +14 -0
  55. package/react/preview.js +730 -0
  56. package/react/website-gate.d.ts +22 -0
  57. package/react.cjs +3893 -1795
  58. package/react.d.ts +8 -4
  59. package/react.js +3891 -1789
  60. package/skills/bacano-web/SKILL.md +116 -0
  61. package/skills/bacano-web/agents/openai.yaml +4 -0
  62. package/skills/bacano-web/references/architecture-runtime.md +109 -0
  63. package/skills/bacano-web/references/common.md +92 -0
  64. package/skills/bacano-web/references/informational.md +39 -0
  65. package/skills/bacano-web/references/report.md +70 -0
  66. package/skills/bacano-web/references/storefront.md +116 -0
  67. package/skills/bacano-web/scripts/audit-http.mjs +284 -0
  68. package/skills/bacano-web/scripts/cli.mjs +222 -0
  69. package/skills/bacano-web/scripts/postinstall.mjs +8 -0
  70. package/transport/documents.d.ts +23 -18
  71. package/transport/graphql.d.ts +13 -1
  72. package/types.d.ts +338 -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,80 @@ 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 branding = raw.branding ?? {
279
+ schema_version: 1,
280
+ favicon: null
281
+ };
282
+ const legal = raw.legal ?? {
283
+ schema_version: 1,
284
+ version: 0,
285
+ content_hash: null,
286
+ documents: {
287
+ terms: null,
288
+ shipping: null,
289
+ returns: null,
290
+ privacy: null,
291
+ cookies: null
292
+ },
293
+ inquiries_channel: null,
294
+ direct_marketing_consent: null,
295
+ cookie_notice: {
296
+ enabled: false,
297
+ message: "Usamos cookies propias y de terceros para mejorar tu experiencia y medir el funcionamiento del sitio.",
298
+ button_label: "Entendido"
299
+ }
300
+ };
301
+ const mapConstructionImage = (image) => {
302
+ if (!image) return null;
303
+ const urls = buildStorageContentMediaUrls(
304
+ `${apiUrl}/storage/v1`,
305
+ image.file_id,
306
+ image.width
307
+ );
308
+ return {
309
+ fileId: image.file_id,
310
+ name: image.name,
311
+ altText: image.alt_text,
312
+ width: image.width,
313
+ height: image.height,
314
+ originalUrl: urls.originalUrl,
315
+ heroUrl: urls.heroUrl,
316
+ responsiveSources: urls.responsiveSources
317
+ };
318
+ };
319
+ const construction = raw.construction ?? {
320
+ schema_version: 1,
321
+ enabled: false,
322
+ enforced: false,
323
+ preview_access_granted: false,
324
+ title: null,
325
+ message: null,
326
+ desktop_image: null,
327
+ mobile_image: null
328
+ };
329
+ const volumePricing = raw.volume_pricing ?? {
330
+ schema_version: 1,
331
+ enabled: false,
332
+ tiers: []
333
+ };
136
334
  return {
137
335
  websiteId: raw.website_id,
138
336
  companyId: raw.company_id,
337
+ contextSchemaVersion: raw.context_schema_version === 1 ? 1 : null,
338
+ chainId: raw.chain_id ?? null,
139
339
  slug: raw.slug,
140
340
  name: raw.name,
141
341
  domain: raw.domain,
342
+ config: raw.config ?? null,
142
343
  buyerAccountsEnabled: raw.buyer_accounts_enabled,
143
344
  clerkPublishableKey: raw.clerk_publishable_key,
144
345
  customerIdentification: {
@@ -154,56 +355,89 @@ async function getWebsitePublicConfig(options) {
154
355
  isDefault: documentType.is_default
155
356
  })
156
357
  )
358
+ },
359
+ branding: {
360
+ schemaVersion: branding.schema_version,
361
+ favicon: branding.favicon ? {
362
+ fileId: branding.favicon.file_id,
363
+ name: branding.favicon.name,
364
+ altText: branding.favicon.alt_text,
365
+ width: branding.favicon.width,
366
+ height: branding.favicon.height,
367
+ originalUrl: buildStorageFileUrl(
368
+ `${apiUrl}/storage/v1`,
369
+ branding.favicon.file_id
370
+ )
371
+ } : null
372
+ },
373
+ marketing: {
374
+ schemaVersion: marketing.schema_version,
375
+ enabled: marketing.enabled,
376
+ currencyCode: marketing.currency_code,
377
+ timeZone: marketing.time_zone,
378
+ attributionWindowDays: marketing.attribution_window_days,
379
+ consentPolicy: marketing.consent_policy ? {
380
+ id: marketing.consent_policy.id,
381
+ version: marketing.consent_policy.version,
382
+ statement: marketing.consent_policy.statement,
383
+ statementHash: marketing.consent_policy.statement_hash
384
+ } : null,
385
+ destinations: {
386
+ ga4: marketing.destinations.ga4 ? {
387
+ measurementId: marketing.destinations.ga4.measurement_id,
388
+ diagnosticMode: marketing.destinations.ga4.diagnostic_mode ?? false
389
+ } : null,
390
+ meta: marketing.destinations.meta ? {
391
+ pixelId: marketing.destinations.meta.pixel_id,
392
+ diagnosticMode: marketing.destinations.meta.diagnostic_mode ?? false
393
+ } : null,
394
+ clarity: marketing.destinations.clarity ? { projectId: marketing.destinations.clarity.project_id } : null
395
+ }
396
+ },
397
+ legal: {
398
+ schemaVersion: legal.schema_version,
399
+ version: legal.version,
400
+ contentHash: legal.content_hash,
401
+ documents: {
402
+ terms: legal.documents.terms,
403
+ shipping: legal.documents.shipping ?? null,
404
+ returns: legal.documents.returns ?? null,
405
+ privacy: legal.documents.privacy,
406
+ cookies: legal.documents.cookies
407
+ },
408
+ inquiriesChannel: legal.inquiries_channel,
409
+ directMarketingConsent: legal.direct_marketing_consent ? {
410
+ version: legal.direct_marketing_consent.version,
411
+ statement: legal.direct_marketing_consent.statement,
412
+ statementHash: legal.direct_marketing_consent.statement_hash
413
+ } : null,
414
+ cookieNotice: {
415
+ enabled: legal.cookie_notice.enabled,
416
+ message: legal.cookie_notice.message,
417
+ buttonLabel: legal.cookie_notice.button_label
418
+ }
419
+ },
420
+ construction: {
421
+ schemaVersion: construction.schema_version,
422
+ enabled: construction.enabled,
423
+ enforced: construction.enforced,
424
+ previewAccessGranted: construction.preview_access_granted,
425
+ title: construction.title,
426
+ message: construction.message,
427
+ desktopImage: mapConstructionImage(construction.desktop_image),
428
+ mobileImage: mapConstructionImage(construction.mobile_image)
429
+ },
430
+ volumePricing: {
431
+ schemaVersion: volumePricing.schema_version,
432
+ enabled: volumePricing.enabled,
433
+ tiers: volumePricing.tiers.map((tier) => ({
434
+ priceType: tier.price_type,
435
+ minQuantity: tier.min_quantity
436
+ }))
157
437
  }
158
438
  };
159
439
  }
160
440
 
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
441
  // libs/sdk/src/transport/documents.ts
208
442
  var RESOLVE_WEBSITE_BY_SLUG = (
209
443
  /* GraphQL */
@@ -347,7 +581,6 @@ var GET_PRODUCTS = (
347
581
  ) {
348
582
  id
349
583
  related_product_id
350
- display_label
351
584
  sort_order
352
585
  related_product {
353
586
  id
@@ -369,6 +602,52 @@ var GET_PRODUCTS = (
369
602
  }
370
603
  }
371
604
  }
605
+ product_option_group_members(limit: 1) {
606
+ group {
607
+ id
608
+ display_title
609
+ members(
610
+ where: {
611
+ product: {
612
+ product_website_publications: {
613
+ website_id: { _eq: $websiteId }
614
+ is_visible: { _eq: true }
615
+ }
616
+ chain_products: {
617
+ chain_id: { _eq: $chainId }
618
+ is_enabled: { _eq: true }
619
+ status: { _eq: active }
620
+ }
621
+ }
622
+ }
623
+ order_by: { sort_order: asc }
624
+ ) {
625
+ id
626
+ product_id
627
+ display_label
628
+ sort_order
629
+ product {
630
+ id
631
+ slug
632
+ name
633
+ sku
634
+ product_media(
635
+ where: { file: { mimeType: { _ilike: "image/%" } } }
636
+ order_by: { order: asc }
637
+ limit: 1
638
+ ) {
639
+ id
640
+ file_id
641
+ order
642
+ file {
643
+ mimeType
644
+ name
645
+ }
646
+ }
647
+ }
648
+ }
649
+ }
650
+ }
372
651
  product_attributes {
373
652
  id
374
653
  value
@@ -453,6 +732,52 @@ var GET_PRODUCT_BY_PK = (
453
732
  limit: 1
454
733
  ) {
455
734
  id
735
+ product_option_group_members(limit: 1) {
736
+ group {
737
+ id
738
+ display_title
739
+ members(
740
+ where: {
741
+ product: {
742
+ product_website_publications: {
743
+ website_id: { _eq: $websiteId }
744
+ is_visible: { _eq: true }
745
+ }
746
+ chain_products: {
747
+ chain_id: { _eq: $chainId }
748
+ is_enabled: { _eq: true }
749
+ status: { _eq: active }
750
+ }
751
+ }
752
+ }
753
+ order_by: { sort_order: asc }
754
+ ) {
755
+ id
756
+ product_id
757
+ display_label
758
+ sort_order
759
+ product {
760
+ id
761
+ slug
762
+ name
763
+ sku
764
+ product_media(
765
+ where: { file: { mimeType: { _ilike: "image/%" } } }
766
+ order_by: { order: asc }
767
+ limit: 1
768
+ ) {
769
+ id
770
+ file_id
771
+ order
772
+ file {
773
+ mimeType
774
+ name
775
+ }
776
+ }
777
+ }
778
+ }
779
+ }
780
+ }
456
781
  name
457
782
  description
458
783
  long_description
@@ -539,7 +864,6 @@ var GET_PRODUCT_BY_PK = (
539
864
  ) {
540
865
  id
541
866
  related_product_id
542
- display_label
543
867
  sort_order
544
868
  related_product {
545
869
  id
@@ -913,6 +1237,24 @@ var WEBSITE_GET_PRODUCT_RECOMMENDATIONS = (
913
1237
  }
914
1238
  `
915
1239
  );
1240
+ var WEBSITE_GET_PRODUCT_SPECIFICATIONS = (
1241
+ /* GraphQL */
1242
+ `
1243
+ mutation WebsiteGetProductSpecifications(
1244
+ $websiteId: uuid!
1245
+ $productIds: [uuid!]!
1246
+ ) {
1247
+ result: websiteGetProductSpecifications(
1248
+ website_id: $websiteId
1249
+ product_ids: $productIds
1250
+ ) {
1251
+ success
1252
+ specifications
1253
+ errors
1254
+ }
1255
+ }
1256
+ `
1257
+ );
916
1258
  var GET_BRANDS = (
917
1259
  /* GraphQL */
918
1260
  `
@@ -1021,14 +1363,76 @@ var WEBSITE_GET_PRODUCT_CONTENT = (
1021
1363
  var CHECK_AVAILABILITY = (
1022
1364
  /* GraphQL */
1023
1365
  `
1024
- query CheckAvailability($variantIds: [uuid!]!, $chainId: uuid!) {
1366
+ query CheckAvailability(
1367
+ $variantIds: [uuid!]!
1368
+ $chainId: uuid!
1369
+ $websiteId: uuid
1370
+ ) {
1371
+ websiteProductAvailability(
1372
+ product_variant_ids: $variantIds
1373
+ chain_id: $chainId
1374
+ website_id: $websiteId
1375
+ ) {
1376
+ product_variant_id
1377
+ in_stock
1378
+ label
1379
+ max_purchasable_quantity
1380
+ }
1381
+ }
1382
+ `
1383
+ );
1384
+ var GET_PRODUCT_LIVE_STATE = (
1385
+ /* GraphQL */
1386
+ `
1387
+ query GetProductLiveState(
1388
+ $variantIds: [uuid!]!
1389
+ $companyId: uuid!
1390
+ $chainId: uuid!
1391
+ $websiteId: uuid!
1392
+ ) {
1393
+ product_variants(
1394
+ where: {
1395
+ id: { _in: $variantIds }
1396
+ is_disabled: { _eq: false }
1397
+ product: {
1398
+ company_id: { _eq: $companyId }
1399
+ product_website_publications: {
1400
+ website_id: { _eq: $websiteId }
1401
+ is_visible: { _eq: true }
1402
+ }
1403
+ chain_products: {
1404
+ chain_id: { _eq: $chainId }
1405
+ is_enabled: { _eq: true }
1406
+ status: { _eq: active }
1407
+ }
1408
+ }
1409
+ }
1410
+ ) {
1411
+ id
1412
+ product_variant_prices(
1413
+ where: {
1414
+ price_type: { _in: [CLIENT, SALE, WHOLESALE, WHOLESALE_2] }
1415
+ _or: [
1416
+ { chain_id: { _eq: $chainId } }
1417
+ { chain_id: { _is_null: true } }
1418
+ ]
1419
+ }
1420
+ ) {
1421
+ id
1422
+ price
1423
+ price_type
1424
+ chain_id
1425
+ }
1426
+ }
1025
1427
  websiteProductAvailability(
1026
1428
  product_variant_ids: $variantIds
1027
1429
  chain_id: $chainId
1430
+ website_id: $websiteId
1028
1431
  ) {
1029
1432
  product_variant_id
1030
1433
  in_stock
1031
1434
  label
1435
+ max_purchasable_quantity
1032
1436
  }
1033
1437
  }
1034
1438
  `
@@ -1061,10 +1465,28 @@ var CREATE_CART = (
1061
1465
  total_with_discount
1062
1466
  created_at
1063
1467
  updated_at
1468
+ volume_pricing {
1469
+ enabled
1470
+ eligibleQuantity
1471
+ appliedPriceType
1472
+ appliedMinQuantity
1473
+ nextPriceType
1474
+ nextMinQuantity
1475
+ remainingQuantity
1476
+ totalSavings
1477
+ }
1064
1478
  items {
1065
1479
  id
1066
1480
  product_variant_id
1067
1481
  quantity
1482
+ unit_price
1483
+ line_total
1484
+ base_unit_price
1485
+ unit_savings
1486
+ line_savings
1487
+ volume_pricing_eligible
1488
+ resolved_price_type
1489
+ volume_pricing_min_quantity
1068
1490
  product_variant {
1069
1491
  id
1070
1492
  sku
@@ -1104,10 +1526,28 @@ var GET_CART = (
1104
1526
  total_with_discount
1105
1527
  created_at
1106
1528
  updated_at
1529
+ volume_pricing {
1530
+ enabled
1531
+ eligibleQuantity
1532
+ appliedPriceType
1533
+ appliedMinQuantity
1534
+ nextPriceType
1535
+ nextMinQuantity
1536
+ remainingQuantity
1537
+ totalSavings
1538
+ }
1107
1539
  items {
1108
1540
  id
1109
1541
  product_variant_id
1110
1542
  quantity
1543
+ unit_price
1544
+ line_total
1545
+ base_unit_price
1546
+ unit_savings
1547
+ line_savings
1548
+ volume_pricing_eligible
1549
+ resolved_price_type
1550
+ volume_pricing_min_quantity
1111
1551
  product_variant {
1112
1552
  id
1113
1553
  sku
@@ -1138,6 +1578,7 @@ var START_GUEST_CHECKOUT = (
1138
1578
  $contact: WebsiteGuestContactInput!
1139
1579
  $items: [WebsiteCheckoutItemInput!]!
1140
1580
  $fulfillmentFamily: String
1581
+ $marketingAttribution: WebsiteMarketingAttributionInput
1141
1582
  ) {
1142
1583
  websiteStartGuestCheckout(
1143
1584
  website_id: $websiteId
@@ -1145,6 +1586,7 @@ var START_GUEST_CHECKOUT = (
1145
1586
  contact: $contact
1146
1587
  items: $items
1147
1588
  fulfillment_family: $fulfillmentFamily
1589
+ marketing_attribution: $marketingAttribution
1148
1590
  ) {
1149
1591
  success
1150
1592
  order_id
@@ -1159,10 +1601,28 @@ var START_GUEST_CHECKOUT = (
1159
1601
  total_with_discount
1160
1602
  created_at
1161
1603
  updated_at
1604
+ volume_pricing {
1605
+ enabled
1606
+ eligibleQuantity
1607
+ appliedPriceType
1608
+ appliedMinQuantity
1609
+ nextPriceType
1610
+ nextMinQuantity
1611
+ remainingQuantity
1612
+ totalSavings
1613
+ }
1162
1614
  items {
1163
1615
  id
1164
1616
  product_variant_id
1165
1617
  quantity
1618
+ unit_price
1619
+ line_total
1620
+ base_unit_price
1621
+ unit_savings
1622
+ line_savings
1623
+ volume_pricing_eligible
1624
+ resolved_price_type
1625
+ volume_pricing_min_quantity
1166
1626
  product_variant {
1167
1627
  id
1168
1628
  sku
@@ -1212,10 +1672,28 @@ var SET_CART_ITEM = (
1212
1672
  total_with_discount
1213
1673
  created_at
1214
1674
  updated_at
1675
+ volume_pricing {
1676
+ enabled
1677
+ eligibleQuantity
1678
+ appliedPriceType
1679
+ appliedMinQuantity
1680
+ nextPriceType
1681
+ nextMinQuantity
1682
+ remainingQuantity
1683
+ totalSavings
1684
+ }
1215
1685
  items {
1216
1686
  id
1217
1687
  product_variant_id
1218
1688
  quantity
1689
+ unit_price
1690
+ line_total
1691
+ base_unit_price
1692
+ unit_savings
1693
+ line_savings
1694
+ volume_pricing_eligible
1695
+ resolved_price_type
1696
+ volume_pricing_min_quantity
1219
1697
  product_variant {
1220
1698
  id
1221
1699
  sku
@@ -1263,10 +1741,28 @@ var REMOVE_CART_ITEM = (
1263
1741
  total_with_discount
1264
1742
  created_at
1265
1743
  updated_at
1744
+ volume_pricing {
1745
+ enabled
1746
+ eligibleQuantity
1747
+ appliedPriceType
1748
+ appliedMinQuantity
1749
+ nextPriceType
1750
+ nextMinQuantity
1751
+ remainingQuantity
1752
+ totalSavings
1753
+ }
1266
1754
  items {
1267
1755
  id
1268
1756
  product_variant_id
1269
1757
  quantity
1758
+ unit_price
1759
+ line_total
1760
+ base_unit_price
1761
+ unit_savings
1762
+ line_savings
1763
+ volume_pricing_eligible
1764
+ resolved_price_type
1765
+ volume_pricing_min_quantity
1270
1766
  product_variant {
1271
1767
  id
1272
1768
  sku
@@ -1288,6 +1784,19 @@ var REMOVE_CART_ITEM = (
1288
1784
  }
1289
1785
  `
1290
1786
  );
1787
+ var DISCARD_CART = (
1788
+ /* GraphQL */
1789
+ `
1790
+ mutation DiscardCart($orderId: uuid!, $cartToken: String!) {
1791
+ websiteDiscardCart(order_id: $orderId, cart_token: $cartToken) {
1792
+ success
1793
+ order_id
1794
+ website_cart_status
1795
+ errors
1796
+ }
1797
+ }
1798
+ `
1799
+ );
1291
1800
  var CHECKOUT = (
1292
1801
  /* GraphQL */
1293
1802
  `
@@ -1302,6 +1811,8 @@ var CHECKOUT = (
1302
1811
  $paymentMethodId: uuid
1303
1812
  $buyerAccountOptIn: Boolean
1304
1813
  $initialPayment: WebsiteInitialPaymentInput
1814
+ $marketingAttribution: WebsiteMarketingAttributionInput
1815
+ $advertisingConsent: WebsiteAdvertisingConsentInput
1305
1816
  ) {
1306
1817
  websiteCheckout(
1307
1818
  order_id: $orderId
@@ -1314,6 +1825,8 @@ var CHECKOUT = (
1314
1825
  payment_method_id: $paymentMethodId
1315
1826
  buyer_account_opt_in: $buyerAccountOptIn
1316
1827
  initial_payment: $initialPayment
1828
+ marketing_attribution: $marketingAttribution
1829
+ advertising_consent: $advertisingConsent
1317
1830
  ) {
1318
1831
  success
1319
1832
  order_id
@@ -2240,45 +2753,157 @@ var GET_SEARCH_SUGGESTIONS = (
2240
2753
  `
2241
2754
  );
2242
2755
 
2243
- // libs/sdk/src/modules/availability.ts
2244
- var MAX_AVAILABILITY_VARIANT_IDS = 100;
2245
- function mapAvailability(raw) {
2246
- return {
2247
- productVariantId: raw.product_variant_id,
2248
- inStock: raw.in_stock,
2249
- label: raw.label
2250
- };
2756
+ // libs/sdk/src/transport/graphql.ts
2757
+ var DEFAULT_MAX_ATTEMPTS = 4;
2758
+ var DEFAULT_MAX_TOTAL_WAIT_MS = 3 * 60 * 1e3;
2759
+ var DEFAULT_JITTER_RATIO = 0.1;
2760
+ function parseRetryAfterSeconds(res, body) {
2761
+ const header = res.headers?.get?.("Retry-After")?.trim();
2762
+ if (header) {
2763
+ const seconds = Number(header);
2764
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds);
2765
+ const retryAt = Date.parse(header);
2766
+ if (Number.isFinite(retryAt)) {
2767
+ return Math.max(Math.ceil((retryAt - Date.now()) / 1e3), 0);
2768
+ }
2769
+ }
2770
+ if (body && typeof body === "object") {
2771
+ const seconds = Number(
2772
+ body.retry_after_seconds
2773
+ );
2774
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds);
2775
+ }
2776
+ return 1;
2251
2777
  }
2252
- function chunkArray(items, chunkSize) {
2253
- const chunks = [];
2254
- for (let index = 0; index < items.length; index += chunkSize) {
2255
- chunks.push(items.slice(index, index + chunkSize));
2778
+ async function createRateLimitError(res) {
2779
+ let body;
2780
+ try {
2781
+ body = await res.json();
2782
+ } catch {
2783
+ body = null;
2256
2784
  }
2257
- return chunks;
2785
+ const message = body && typeof body === "object" && typeof body.message === "string" ? body.message : "Rate limit exceeded. Please try again later.";
2786
+ return new RateLimitError(message, parseRetryAfterSeconds(res, body));
2258
2787
  }
2259
- function uniqueValues(values) {
2260
- return Array.from(new Set(values));
2788
+ function getRetryDelayMs(error, options) {
2789
+ const retryAfterMs = (error.retryAfterSeconds ?? 1) * 1e3;
2790
+ const jitterRatio = Math.max(
2791
+ 0,
2792
+ Math.min(options.jitterRatio ?? DEFAULT_JITTER_RATIO, 1)
2793
+ );
2794
+ return Math.ceil(retryAfterMs * (1 + Math.random() * jitterRatio));
2261
2795
  }
2262
- function createAvailabilityModule(graphqlUrl, getHeaders, getContext) {
2263
- return {
2264
- async checkAvailability(productVariantIds) {
2265
- const ctx = getContext();
2266
- if (!ctx.chainId) {
2267
- throw new WebsiteError(
2268
- "This website has no chain configured. Stock availability requires a chain_id.",
2269
- "NO_CHAIN"
2270
- );
2796
+ function wait(delayMs) {
2797
+ return new Promise((resolve) => setTimeout(resolve, delayMs));
2798
+ }
2799
+ async function executeGraphQL(url, query, variables, headers, options) {
2800
+ const retry = options?.retry;
2801
+ const maxAttempts = Math.max(
2802
+ 1,
2803
+ Math.floor(retry?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS)
2804
+ );
2805
+ const maxTotalWaitMs = Math.max(
2806
+ 0,
2807
+ retry?.maxTotalWaitMs ?? DEFAULT_MAX_TOTAL_WAIT_MS
2808
+ );
2809
+ let totalWaitMs = 0;
2810
+ let res = null;
2811
+ for (let attempt = 1; attempt <= (retry ? maxAttempts : 1); attempt += 1) {
2812
+ const resolvedHeaders = typeof headers === "function" ? await headers() : headers ?? {};
2813
+ try {
2814
+ res = await fetch(url, {
2815
+ method: "POST",
2816
+ headers: {
2817
+ "Content-Type": "application/json",
2818
+ ...resolvedHeaders
2819
+ },
2820
+ body: JSON.stringify({ query, variables })
2821
+ });
2822
+ } catch (err) {
2823
+ throw new WebsiteError(
2824
+ `Network error connecting to ${url}: ${err instanceof Error ? err.message : String(err)}`,
2825
+ "NETWORK_ERROR"
2826
+ );
2827
+ }
2828
+ if (res.status !== 429) break;
2829
+ const rateLimitError = await createRateLimitError(res);
2830
+ const delayMs = getRetryDelayMs(rateLimitError, retry ?? {});
2831
+ const canRetry = Boolean(retry) && attempt < maxAttempts && totalWaitMs + delayMs <= maxTotalWaitMs;
2832
+ if (!canRetry) throw rateLimitError;
2833
+ totalWaitMs += delayMs;
2834
+ await wait(delayMs);
2835
+ }
2836
+ if (!res) {
2837
+ throw new WebsiteError(
2838
+ "GraphQL request did not produce a response",
2839
+ "EMPTY_RESPONSE"
2840
+ );
2841
+ }
2842
+ let body;
2843
+ try {
2844
+ body = await res.json();
2845
+ } catch {
2846
+ throw new WebsiteError(
2847
+ `Non-JSON response from ${url} (status ${res.status})`,
2848
+ "INVALID_RESPONSE"
2849
+ );
2850
+ }
2851
+ if (body.errors && body.errors.length > 0) {
2852
+ throw new GraphQLRequestError(body.errors);
2853
+ }
2854
+ if (!body.data) {
2855
+ throw new WebsiteError(
2856
+ "GraphQL response contains no data",
2857
+ "EMPTY_RESPONSE"
2858
+ );
2859
+ }
2860
+ return body.data;
2861
+ }
2862
+
2863
+ // libs/sdk/src/modules/availability.ts
2864
+ var MAX_AVAILABILITY_VARIANT_IDS = 100;
2865
+ function mapAvailability(raw) {
2866
+ return {
2867
+ productVariantId: raw.product_variant_id,
2868
+ inStock: raw.in_stock,
2869
+ label: raw.label,
2870
+ maxPurchasableQuantity: raw.max_purchasable_quantity
2871
+ };
2872
+ }
2873
+ function chunkArray(items, chunkSize) {
2874
+ const chunks = [];
2875
+ for (let index = 0; index < items.length; index += chunkSize) {
2876
+ chunks.push(items.slice(index, index + chunkSize));
2877
+ }
2878
+ return chunks;
2879
+ }
2880
+ function uniqueValues(values) {
2881
+ return Array.from(new Set(values));
2882
+ }
2883
+ function createAvailabilityModule(graphqlUrl, getHeaders, getContext) {
2884
+ return {
2885
+ async checkAvailability(productVariantIds) {
2886
+ const ctx = getContext();
2887
+ if (!ctx.chainId) {
2888
+ throw new WebsiteError(
2889
+ "This website has no chain configured. Stock availability requires a chain_id.",
2890
+ "NO_CHAIN"
2891
+ );
2271
2892
  }
2272
2893
  if (productVariantIds.length === 0) return [];
2273
2894
  const variantIds = uniqueValues(productVariantIds);
2274
2895
  const results = [];
2275
- for (const chunk of chunkArray(variantIds, MAX_AVAILABILITY_VARIANT_IDS)) {
2896
+ for (const chunk of chunkArray(
2897
+ variantIds,
2898
+ MAX_AVAILABILITY_VARIANT_IDS
2899
+ )) {
2276
2900
  const data = await executeGraphQL(
2277
2901
  graphqlUrl,
2278
2902
  CHECK_AVAILABILITY,
2279
2903
  {
2280
2904
  variantIds: chunk,
2281
- chainId: ctx.chainId
2905
+ chainId: ctx.chainId,
2906
+ websiteId: ctx.websiteId
2282
2907
  },
2283
2908
  getHeaders
2284
2909
  );
@@ -2291,12 +2916,297 @@ function createAvailabilityModule(graphqlUrl, getHeaders, getContext) {
2291
2916
  };
2292
2917
  }
2293
2918
 
2919
+ // libs/sdk/src/marketing/sanitize.ts
2920
+ var SENSITIVE_PARAMETER_PATTERNS = [
2921
+ /token/i,
2922
+ /secret/i,
2923
+ /password/i,
2924
+ /authorization/i,
2925
+ /session/i,
2926
+ /ticket/i,
2927
+ /code_verifier/i,
2928
+ /client_secret/i,
2929
+ /payment/i,
2930
+ /order[_-]?(?:id|token)/i,
2931
+ /public[_-]?order/i,
2932
+ /clerk/i,
2933
+ /oauth/i,
2934
+ /email/i,
2935
+ /phone/i,
2936
+ /document/i
2937
+ ];
2938
+ var ALLOWED_MARKETING_PARAMETERS = /* @__PURE__ */ new Set([
2939
+ "utm_source",
2940
+ "utm_medium",
2941
+ "utm_campaign",
2942
+ "utm_term",
2943
+ "utm_content",
2944
+ "utm_source_platform",
2945
+ "utm_id",
2946
+ "bacano_origin",
2947
+ "gclid",
2948
+ "gbraid",
2949
+ "wbraid",
2950
+ "fbclid",
2951
+ "ttclid",
2952
+ "msclkid"
2953
+ ]);
2954
+ function sanitizeMarketingUrl(input, baseUrl) {
2955
+ try {
2956
+ const url = new URL(input, baseUrl);
2957
+ const safeParameters = new URLSearchParams();
2958
+ for (const [key, value] of url.searchParams.entries()) {
2959
+ if (ALLOWED_MARKETING_PARAMETERS.has(key.toLowerCase()) && !SENSITIVE_PARAMETER_PATTERNS.some((pattern) => pattern.test(key))) {
2960
+ safeParameters.set(key.toLowerCase(), value.slice(0, 500));
2961
+ }
2962
+ }
2963
+ url.search = safeParameters.toString();
2964
+ url.hash = "";
2965
+ return url.toString();
2966
+ } catch {
2967
+ return "";
2968
+ }
2969
+ }
2970
+ function getAllowedMarketingParameters(input, baseUrl) {
2971
+ const sanitized = sanitizeMarketingUrl(input, baseUrl);
2972
+ if (!sanitized) return {};
2973
+ const url = new URL(sanitized);
2974
+ return Object.fromEntries(url.searchParams.entries());
2975
+ }
2976
+
2977
+ // libs/sdk/src/marketing/attribution.ts
2978
+ var STORAGE_PREFIX = "bacano:marketing:attribution:v1";
2979
+ var GA4_CLIENT_ID_PATTERN = /^\d+\.\d+$/;
2980
+ var META_BROWSER_ID_PATTERN = /^fb\.\d+\.\d+\.[A-Za-z0-9_-]+$/;
2981
+ var SALES_ORIGIN_KEY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2982
+ function storageKey(websiteId) {
2983
+ return `${STORAGE_PREFIX}:${websiteId}`;
2984
+ }
2985
+ function readStored(websiteId) {
2986
+ if (typeof window === "undefined") return null;
2987
+ try {
2988
+ const value = window.localStorage.getItem(storageKey(websiteId));
2989
+ if (!value) return null;
2990
+ const parsed = JSON.parse(value);
2991
+ if (new Date(parsed.expiresAt).getTime() <= Date.now()) {
2992
+ window.localStorage.removeItem(storageKey(websiteId));
2993
+ return null;
2994
+ }
2995
+ return parsed;
2996
+ } catch {
2997
+ return null;
2998
+ }
2999
+ }
3000
+ function writeStored(websiteId, value) {
3001
+ if (typeof window === "undefined") return;
3002
+ try {
3003
+ window.localStorage.setItem(storageKey(websiteId), JSON.stringify(value));
3004
+ } catch {
3005
+ }
3006
+ }
3007
+ function readCookie(name) {
3008
+ if (typeof document === "undefined") return void 0;
3009
+ try {
3010
+ const prefix = `${name}=`;
3011
+ return document.cookie.split(";").map((part) => part.trim()).find((part) => part.startsWith(prefix))?.slice(prefix.length);
3012
+ } catch {
3013
+ return void 0;
3014
+ }
3015
+ }
3016
+ function validMetaBrowserId(value) {
3017
+ const normalized = value?.trim();
3018
+ return normalized && normalized.length <= 512 && META_BROWSER_ID_PATTERN.test(normalized) ? normalized : void 0;
3019
+ }
3020
+ function optional(value) {
3021
+ const normalized = value?.trim();
3022
+ return normalized ? normalized.slice(0, 500) : void 0;
3023
+ }
3024
+ function validSalesOriginKey(value) {
3025
+ const normalized = value?.trim().toLowerCase();
3026
+ return normalized && normalized.length <= 100 && SALES_ORIGIN_KEY_PATTERN.test(normalized) ? normalized : void 0;
3027
+ }
3028
+ function buildTouchpoint() {
3029
+ if (typeof window === "undefined") return null;
3030
+ const params = getAllowedMarketingParameters(window.location.href);
3031
+ const referrerHost = (() => {
3032
+ if (!document.referrer) return void 0;
3033
+ try {
3034
+ const hostname = new URL(document.referrer).hostname;
3035
+ return hostname === window.location.hostname ? void 0 : hostname.slice(0, 255);
3036
+ } catch {
3037
+ return void 0;
3038
+ }
3039
+ })();
3040
+ return {
3041
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
3042
+ landingUrl: sanitizeMarketingUrl(window.location.href),
3043
+ referrerHost,
3044
+ source: optional(params.utm_source),
3045
+ medium: optional(params.utm_medium),
3046
+ campaign: optional(params.utm_campaign),
3047
+ term: optional(params.utm_term),
3048
+ content: optional(params.utm_content),
3049
+ sourcePlatform: optional(params.utm_source_platform),
3050
+ campaignId: optional(params.utm_id),
3051
+ salesOriginKey: validSalesOriginKey(params.bacano_origin),
3052
+ gclid: optional(params.gclid),
3053
+ gbraid: optional(params.gbraid),
3054
+ wbraid: optional(params.wbraid),
3055
+ fbclid: optional(params.fbclid),
3056
+ ttclid: optional(params.ttclid),
3057
+ msclkid: optional(params.msclkid)
3058
+ };
3059
+ }
3060
+ function isNonDirect(touchpoint) {
3061
+ return Boolean(
3062
+ touchpoint.salesOriginKey || touchpoint.source || touchpoint.medium || touchpoint.campaign || touchpoint.gclid || touchpoint.gbraid || touchpoint.wbraid || touchpoint.fbclid || touchpoint.ttclid || touchpoint.msclkid || touchpoint.referrerHost
3063
+ );
3064
+ }
3065
+ function toPublic(stored) {
3066
+ return {
3067
+ schemaVersion: 1,
3068
+ firstTouch: stored.firstTouch,
3069
+ lastNonDirectTouch: stored.lastNonDirectTouch,
3070
+ ga4ClientId: stored.ga4ClientId,
3071
+ ga4SessionId: stored.ga4SessionId,
3072
+ metaFbp: stored.metaFbp,
3073
+ metaFbc: stored.metaFbc,
3074
+ clientUserAgent: stored.clientUserAgent,
3075
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
3076
+ expiresAt: stored.expiresAt
3077
+ };
3078
+ }
3079
+ function refreshWebsiteMarketingMetaContext(websiteId) {
3080
+ try {
3081
+ const stored = readStored(websiteId);
3082
+ if (!stored || typeof window === "undefined") return false;
3083
+ const fbp = validMetaBrowserId(readCookie("_fbp"));
3084
+ const clickTouchpoint = stored.lastNonDirectTouch ?? stored.firstTouch;
3085
+ const derivedFbc = clickTouchpoint.fbclid ? `fb.1.${new Date(clickTouchpoint.capturedAt).getTime()}.${clickTouchpoint.fbclid}` : void 0;
3086
+ const fbc = validMetaBrowserId(readCookie("_fbc") ?? derivedFbc);
3087
+ const clientUserAgent = window.navigator?.userAgent.trim().slice(0, 1024);
3088
+ writeStored(websiteId, {
3089
+ ...stored,
3090
+ metaFbp: fbp ?? stored.metaFbp,
3091
+ metaFbc: fbc ?? stored.metaFbc,
3092
+ clientUserAgent: clientUserAgent || stored.clientUserAgent
3093
+ });
3094
+ return Boolean(fbp || fbc || clientUserAgent);
3095
+ } catch {
3096
+ return false;
3097
+ }
3098
+ }
3099
+ function storeWebsiteMarketingGa4ClientId(websiteId, clientId) {
3100
+ if (typeof clientId !== "string") return false;
3101
+ const normalized = clientId.trim();
3102
+ if (!GA4_CLIENT_ID_PATTERN.test(normalized)) return false;
3103
+ const stored = readStored(websiteId);
3104
+ if (!stored) return false;
3105
+ writeStored(websiteId, { ...stored, ga4ClientId: normalized });
3106
+ return true;
3107
+ }
3108
+ function storeWebsiteMarketingGa4SessionId(websiteId, sessionId) {
3109
+ const normalized = typeof sessionId === "number" && Number.isSafeInteger(sessionId) ? String(sessionId) : typeof sessionId === "string" ? sessionId.trim() : "";
3110
+ const numericValue = Number(normalized);
3111
+ if (!/^\d{1,16}$/.test(normalized) || !Number.isSafeInteger(numericValue) || numericValue <= 0) {
3112
+ return false;
3113
+ }
3114
+ const stored = readStored(websiteId);
3115
+ if (!stored) return false;
3116
+ writeStored(websiteId, { ...stored, ga4SessionId: normalized });
3117
+ return true;
3118
+ }
3119
+ function captureWebsiteMarketingAttribution(websiteId, windowDays) {
3120
+ const current = buildTouchpoint();
3121
+ if (!current) return null;
3122
+ const stored = readStored(websiteId);
3123
+ const expiresAt = stored?.expiresAt ?? new Date(Date.now() + Math.max(1, windowDays) * 864e5).toISOString();
3124
+ const next = {
3125
+ firstTouch: stored?.firstTouch ?? current,
3126
+ lastNonDirectTouch: isNonDirect(current) ? current : stored?.lastNonDirectTouch ?? null,
3127
+ ga4ClientId: stored?.ga4ClientId,
3128
+ ga4SessionId: stored?.ga4SessionId,
3129
+ metaFbp: stored?.metaFbp,
3130
+ metaFbc: stored?.metaFbc,
3131
+ clientUserAgent: stored?.clientUserAgent,
3132
+ expiresAt
3133
+ };
3134
+ writeStored(websiteId, next);
3135
+ return toPublic(next);
3136
+ }
3137
+ function getWebsiteMarketingAttribution(websiteId) {
3138
+ const stored = readStored(websiteId);
3139
+ return stored ? toPublic(stored) : null;
3140
+ }
3141
+ function serializeTouchpoint(touchpoint) {
3142
+ return {
3143
+ captured_at: touchpoint.capturedAt,
3144
+ landing_url: touchpoint.landingUrl,
3145
+ referrer_host: touchpoint.referrerHost,
3146
+ source: touchpoint.source,
3147
+ medium: touchpoint.medium,
3148
+ campaign: touchpoint.campaign,
3149
+ term: touchpoint.term,
3150
+ content: touchpoint.content,
3151
+ source_platform: touchpoint.sourcePlatform,
3152
+ campaign_id: touchpoint.campaignId,
3153
+ sales_origin_key: touchpoint.salesOriginKey,
3154
+ gclid: touchpoint.gclid,
3155
+ gbraid: touchpoint.gbraid,
3156
+ wbraid: touchpoint.wbraid,
3157
+ fbclid: touchpoint.fbclid,
3158
+ ttclid: touchpoint.ttclid,
3159
+ msclkid: touchpoint.msclkid
3160
+ };
3161
+ }
3162
+ function serializeWebsiteMarketingAttribution(attribution) {
3163
+ return {
3164
+ schema_version: attribution.schemaVersion,
3165
+ first_touch: serializeTouchpoint(attribution.firstTouch),
3166
+ last_non_direct_touch: attribution.lastNonDirectTouch ? serializeTouchpoint(attribution.lastNonDirectTouch) : null,
3167
+ ga4_client_id: attribution.ga4ClientId,
3168
+ ga4_session_id: attribution.ga4SessionId,
3169
+ meta_fbp: attribution.metaFbp,
3170
+ meta_fbc: attribution.metaFbc,
3171
+ client_user_agent: attribution.clientUserAgent,
3172
+ captured_at: attribution.capturedAt
3173
+ };
3174
+ }
3175
+
3176
+ // libs/sdk/src/modules/cart-errors.ts
3177
+ var RECOVERABLE_CART_SESSION_ERRORS = /* @__PURE__ */ new Set([
3178
+ "INVALID_CART_TOKEN",
3179
+ "CART_NOT_FOUND"
3180
+ ]);
3181
+ function firstStableErrorCode(errors) {
3182
+ const candidate = errors?.[0]?.split(":", 1)[0]?.trim();
3183
+ return candidate && /^[A-Z][A-Z0-9_]+$/.test(candidate) ? candidate : void 0;
3184
+ }
3185
+ function getStableCartActionErrorCode(errors, fallbackCode) {
3186
+ return firstStableErrorCode(errors) ?? fallbackCode;
3187
+ }
3188
+ function hasRecoverableCartSessionError(errors) {
3189
+ return Boolean(
3190
+ errors?.some(
3191
+ (error) => RECOVERABLE_CART_SESSION_ERRORS.has(error.split(":", 1)[0]?.trim())
3192
+ )
3193
+ );
3194
+ }
3195
+
2294
3196
  // libs/sdk/src/modules/cart.ts
2295
3197
  function mapCartItem(raw) {
2296
3198
  return {
2297
3199
  id: raw.id,
2298
3200
  productVariantId: raw.product_variant_id,
2299
3201
  quantity: raw.quantity,
3202
+ unitPrice: raw.unit_price ?? null,
3203
+ lineTotal: raw.line_total ?? null,
3204
+ baseUnitPrice: raw.base_unit_price ?? raw.unit_price ?? null,
3205
+ unitSavings: raw.unit_savings ?? 0,
3206
+ lineSavings: raw.line_savings ?? 0,
3207
+ volumePricingEligible: raw.volume_pricing_eligible ?? false,
3208
+ resolvedPriceType: raw.resolved_price_type ?? null,
3209
+ volumePricingMinQuantity: raw.volume_pricing_min_quantity ?? null,
2300
3210
  productVariant: {
2301
3211
  sku: raw.product_variant.sku,
2302
3212
  product: raw.product_variant.product,
@@ -2322,13 +3232,29 @@ function mapCart(raw, cartToken) {
2322
3232
  },
2323
3233
  status: raw.status,
2324
3234
  createdAt: raw.created_at,
2325
- updatedAt: raw.updated_at
3235
+ updatedAt: raw.updated_at,
3236
+ volumePricing: raw.volume_pricing ? {
3237
+ ...raw.volume_pricing,
3238
+ totalSavings: raw.volume_pricing.totalSavings ?? 0
3239
+ } : {
3240
+ enabled: false,
3241
+ eligibleQuantity: 0,
3242
+ appliedPriceType: null,
3243
+ appliedMinQuantity: null,
3244
+ nextPriceType: null,
3245
+ nextMinQuantity: null,
3246
+ remainingQuantity: null,
3247
+ totalSavings: 0
3248
+ }
2326
3249
  };
2327
3250
  }
2328
3251
  function assertActionSuccess(result, fallbackCode) {
2329
3252
  if (!result.success || !result.order_id || !result.cart_token || !result.cart) {
2330
3253
  const message = result.errors?.join("; ") || "Cart action failed";
2331
- throw new WebsiteError(message, fallbackCode);
3254
+ throw new WebsiteError(
3255
+ message,
3256
+ getStableCartActionErrorCode(result.errors, fallbackCode)
3257
+ );
2332
3258
  }
2333
3259
  }
2334
3260
  function requireCartSession(storage) {
@@ -2346,7 +3272,7 @@ function createCartModule(graphqlUrl, getHeaders, getContext, cartSessionStorage
2346
3272
  const data = await executeGraphQL(graphqlUrl, GET_CART, { orderId, cartToken }, getHeaders);
2347
3273
  const result = data.websiteGetCart;
2348
3274
  if (!result.success) {
2349
- if (result.errors?.includes("CART_NOT_FOUND")) {
3275
+ if (hasRecoverableCartSessionError(result.errors)) {
2350
3276
  cartSessionStorage.clear();
2351
3277
  return null;
2352
3278
  }
@@ -2399,23 +3325,31 @@ function createCartModule(graphqlUrl, getHeaders, getContext, cartSessionStorage
2399
3325
  "NO_CHAIN"
2400
3326
  );
2401
3327
  }
3328
+ const requestStartCheckout = async (cartToken) => {
3329
+ const data = await executeGraphQL(
3330
+ graphqlUrl,
3331
+ START_GUEST_CHECKOUT,
3332
+ {
3333
+ websiteId: ctx.websiteId,
3334
+ cartToken,
3335
+ contact: input.contact,
3336
+ fulfillmentFamily: input.fulfillmentFamily,
3337
+ items: input.items.map((item) => ({
3338
+ product_variant_id: item.productVariantId,
3339
+ quantity: item.quantity
3340
+ })),
3341
+ marketingAttribution: input.marketingAttribution ? serializeWebsiteMarketingAttribution(input.marketingAttribution) : void 0
3342
+ },
3343
+ getHeaders
3344
+ );
3345
+ return data.websiteStartGuestCheckout;
3346
+ };
2402
3347
  const existing = cartSessionStorage.get();
2403
- const data = await executeGraphQL(
2404
- graphqlUrl,
2405
- START_GUEST_CHECKOUT,
2406
- {
2407
- websiteId: ctx.websiteId,
2408
- cartToken: existing?.cartToken,
2409
- contact: input.contact,
2410
- fulfillmentFamily: input.fulfillmentFamily,
2411
- items: input.items.map((item) => ({
2412
- product_variant_id: item.productVariantId,
2413
- quantity: item.quantity
2414
- }))
2415
- },
2416
- getHeaders
2417
- );
2418
- const result = data.websiteStartGuestCheckout;
3348
+ let result = await requestStartCheckout(existing?.cartToken);
3349
+ if (existing && !result.success && hasRecoverableCartSessionError(result.errors)) {
3350
+ cartSessionStorage.clear();
3351
+ result = await requestStartCheckout();
3352
+ }
2419
3353
  assertActionSuccess(result, "CHECKOUT_START_ERROR");
2420
3354
  cartSessionStorage.set({
2421
3355
  orderId: result.order_id,
@@ -2438,7 +3372,7 @@ function createCartModule(graphqlUrl, getHeaders, getContext, cartSessionStorage
2438
3372
  const session = cartSessionStorage.get();
2439
3373
  if (!session) {
2440
3374
  throw new WebsiteError(
2441
- "Start checkout with contact before syncing cart items to Bacano",
3375
+ "Create or recover a Bacano cart session before syncing cart items",
2442
3376
  "CART_SESSION_MISSING"
2443
3377
  );
2444
3378
  }
@@ -2506,11 +3440,37 @@ function createCartModule(graphqlUrl, getHeaders, getContext, cartSessionStorage
2506
3440
  );
2507
3441
  const result = data.websiteRemoveCartItem;
2508
3442
  assertActionSuccess(result, "CART_ITEM_ERROR");
3443
+ const cart = mapCart(result.cart, result.cart_token);
3444
+ if (cart.items.length === 0) {
3445
+ cartSessionStorage.clear();
3446
+ return cart;
3447
+ }
2509
3448
  cartSessionStorage.set({
2510
3449
  orderId: result.order_id,
2511
3450
  cartToken: result.cart_token
2512
3451
  });
2513
- return mapCart(result.cart, result.cart_token);
3452
+ return cart;
3453
+ },
3454
+ async clear() {
3455
+ const session = cartSessionStorage.get();
3456
+ if (!session) return;
3457
+ const data = await executeGraphQL(
3458
+ graphqlUrl,
3459
+ DISCARD_CART,
3460
+ {
3461
+ orderId: session.orderId,
3462
+ cartToken: session.cartToken
3463
+ },
3464
+ getHeaders
3465
+ );
3466
+ const result = data.websiteDiscardCart;
3467
+ if (!result.success && !hasRecoverableCartSessionError(result.errors)) {
3468
+ throw new WebsiteError(
3469
+ result.errors?.join("; ") || "Cart could not be discarded",
3470
+ getStableCartActionErrorCode(result.errors, "CART_DISCARD_ERROR")
3471
+ );
3472
+ }
3473
+ cartSessionStorage.clear();
2514
3474
  }
2515
3475
  };
2516
3476
  }
@@ -2533,7 +3493,9 @@ function createCartSessionStorage(kind, getSlug) {
2533
3493
  const key = readKey();
2534
3494
  const match = document.cookie.split("; ").find((row) => row.startsWith(`${key}=`));
2535
3495
  if (!match) return null;
2536
- return JSON.parse(decodeURIComponent(match.split("=").slice(1).join("=")));
3496
+ return JSON.parse(
3497
+ decodeURIComponent(match.split("=").slice(1).join("="))
3498
+ );
2537
3499
  } catch {
2538
3500
  return null;
2539
3501
  }
@@ -2573,46 +3535,129 @@ function createCartSessionStorage(kind, getSlug) {
2573
3535
  };
2574
3536
  }
2575
3537
 
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")
3538
+ // libs/sdk/src/modules/static-catalog.ts
3539
+ var STATIC_CATALOG_PAGE_SIZE = 100;
3540
+ var STATIC_CATALOG_MAX_PRODUCTS = 1e4;
3541
+ var STATIC_CATALOG_INITIAL_PAGE_SIZE = 24;
3542
+ async function collectStaticCatalog({
3543
+ websiteId,
3544
+ snapshot: options,
3545
+ bootstrap,
3546
+ loadPage
3547
+ }) {
3548
+ const pageSize = options.pageSize ?? STATIC_CATALOG_PAGE_SIZE;
3549
+ const maxProducts = options.maxProducts ?? STATIC_CATALOG_MAX_PRODUCTS;
3550
+ const initialPageSize = bootstrap?.initialPageSize ?? STATIC_CATALOG_INITIAL_PAGE_SIZE;
3551
+ const sortBy = bootstrap?.sortBy ?? "name_asc";
3552
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) {
3553
+ throw new WebsiteError(
3554
+ "Static catalog pageSize must be an integer between 1 and 100.",
3555
+ "INVALID_STATIC_CATALOG_PAGE_SIZE"
3556
+ );
3557
+ }
3558
+ if (!Number.isInteger(maxProducts) || maxProducts < 1 || maxProducts > STATIC_CATALOG_MAX_PRODUCTS) {
3559
+ throw new WebsiteError(
3560
+ `Static catalog maxProducts must be an integer between 1 and ${STATIC_CATALOG_MAX_PRODUCTS}.`,
3561
+ "INVALID_STATIC_CATALOG_MAX_PRODUCTS"
3562
+ );
3563
+ }
3564
+ if (bootstrap && (!Number.isInteger(initialPageSize) || initialPageSize < 1 || initialPageSize > 100)) {
3565
+ throw new WebsiteError(
3566
+ "Static catalog initialPageSize must be an integer between 1 and 100.",
3567
+ "INVALID_STATIC_CATALOG_INITIAL_PAGE_SIZE"
3568
+ );
3569
+ }
3570
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
3571
+ const products = [];
3572
+ let pageCount = 0;
3573
+ let expectedTotal = null;
3574
+ let firstPage = null;
3575
+ for (let offset = 0; ; ) {
3576
+ const includeBootstrapData = Boolean(bootstrap && offset === 0);
3577
+ const requestLimit = includeBootstrapData ? Math.max(pageSize, initialPageSize) : pageSize;
3578
+ const page = await loadPage({
3579
+ categoryListKey: options.categoryListKey,
3580
+ attributeListKey: options.attributeListKey,
3581
+ sortBy,
3582
+ limit: requestLimit,
3583
+ offset,
3584
+ include: {
3585
+ products: true,
3586
+ detailVariants: true,
3587
+ categories: includeBootstrapData,
3588
+ attributes: true,
3589
+ filters: includeBootstrapData,
3590
+ brands: includeBootstrapData,
3591
+ debug: false
3592
+ }
3593
+ });
3594
+ if (offset === 0) firstPage = page;
3595
+ pageCount += 1;
3596
+ const pageTotal = page.pagination.total;
3597
+ if (expectedTotal === null) {
3598
+ expectedTotal = pageTotal;
3599
+ } else if (pageTotal !== expectedTotal) {
3600
+ throwCatalogChanged();
3601
+ }
3602
+ if (pageTotal > maxProducts) {
3603
+ throw new WebsiteError(
3604
+ `Static catalog contains ${pageTotal} products and exceeds maxProducts ${maxProducts}.`,
3605
+ "STATIC_CATALOG_LIMIT_EXCEEDED"
3606
+ );
3607
+ }
3608
+ products.push(...page.products);
3609
+ if (!page.pagination.hasMore) break;
3610
+ if (page.products.length === 0) {
3611
+ throw new WebsiteError(
3612
+ "Static catalog pagination did not advance.",
3613
+ "STATIC_CATALOG_PAGINATION_STALLED"
3614
+ );
3615
+ }
3616
+ offset += page.products.length;
3617
+ }
3618
+ const total = expectedTotal ?? 0;
3619
+ const uniqueProductIds = new Set(products.map((product) => product.id));
3620
+ if (products.length !== total || uniqueProductIds.size !== total) {
3621
+ throwCatalogChanged();
3622
+ }
3623
+ const snapshot = {
3624
+ schemaVersion: 1,
3625
+ snapshotId: `${websiteId}:${generatedAt}`,
3626
+ generatedAt,
3627
+ websiteId,
3628
+ products,
3629
+ total,
3630
+ pageSize,
3631
+ pageCount
2610
3632
  };
3633
+ const initialPage = bootstrap && firstPage ? {
3634
+ ...firstPage,
3635
+ products: firstPage.products.slice(0, initialPageSize),
3636
+ pagination: {
3637
+ ...firstPage.pagination,
3638
+ limit: initialPageSize,
3639
+ offset: 0,
3640
+ hasMore: total > initialPageSize
3641
+ }
3642
+ } : null;
3643
+ return { snapshot, initialPage };
3644
+ }
3645
+ function throwCatalogChanged() {
3646
+ throw new WebsiteError(
3647
+ "Static catalog changed while it was being collected. Retry the build to get a consistent snapshot.",
3648
+ "STATIC_CATALOG_CHANGED_DURING_BUILD"
3649
+ );
2611
3650
  }
2612
3651
 
2613
3652
  // libs/sdk/src/modules/catalog.ts
2614
3653
  var MAX_AVAILABILITY_VARIANT_IDS2 = 100;
2615
3654
  var DEFAULT_FILTER_ATTRIBUTE_LIST_KEY = "atributos";
3655
+ var MAX_LIVE_STATE_VARIANT_IDS = 100;
3656
+ var STATIC_CATALOG_RETRY = {
3657
+ maxAttempts: 4,
3658
+ maxTotalWaitMs: 3 * 60 * 1e3,
3659
+ jitterRatio: 0.1
3660
+ };
2616
3661
  function mapPrice(raw) {
2617
3662
  return {
2618
3663
  id: raw.id,
@@ -2645,6 +3690,9 @@ function getProductImages(media) {
2645
3690
  function getProductVideos(media) {
2646
3691
  return getProductMediaByKind(media, "video");
2647
3692
  }
3693
+ function getProductDetailVariants(product) {
3694
+ return product.detailVariants ?? product.variants;
3695
+ }
2648
3696
  function getPrimaryProductImage(media) {
2649
3697
  return getProductImages(media)[0] ?? null;
2650
3698
  }
@@ -2794,7 +3842,6 @@ function mapRelatedProduct(raw, storageUrl) {
2794
3842
  slug: raw.related_product.slug,
2795
3843
  name: raw.related_product.name,
2796
3844
  sku: raw.related_product.sku,
2797
- displayLabel: raw.display_label,
2798
3845
  sortOrder: raw.sort_order ?? 0,
2799
3846
  media: raw.related_product.product_media.map(
2800
3847
  (m) => mapMedia(m, storageUrl)
@@ -2802,6 +3849,26 @@ function mapRelatedProduct(raw, storageUrl) {
2802
3849
  };
2803
3850
  }
2804
3851
  function mapProduct(raw, storageUrl, labels, chainId) {
3852
+ const detailVariants = raw.product_variants.map((variant) => mapVariant(variant, labels, chainId)).filter((variant) => variant.pricing.currentPrice != null);
3853
+ const listingVariantIds = raw.listing_variant_ids ? new Set(raw.listing_variant_ids) : null;
3854
+ const variants = listingVariantIds ? detailVariants.filter((variant) => listingVariantIds.has(variant.id)) : detailVariants;
3855
+ const rawOptionGroup = raw.product_option_group_members?.[0]?.group;
3856
+ const optionGroup = rawOptionGroup && rawOptionGroup.members.length >= 2 ? {
3857
+ id: rawOptionGroup.id,
3858
+ title: rawOptionGroup.display_title,
3859
+ members: rawOptionGroup.members.map((member) => ({
3860
+ id: member.id,
3861
+ productId: member.product_id,
3862
+ slug: member.product.slug,
3863
+ name: member.product.name,
3864
+ sku: member.product.sku,
3865
+ displayLabel: member.display_label,
3866
+ sortOrder: member.sort_order ?? 0,
3867
+ media: member.product.product_media.map(
3868
+ (media) => mapMedia(media, storageUrl)
3869
+ )
3870
+ }))
3871
+ } : null;
2805
3872
  return {
2806
3873
  id: raw.id,
2807
3874
  name: raw.name,
@@ -2818,16 +3885,17 @@ function mapProduct(raw, storageUrl, labels, chainId) {
2818
3885
  measurementUnit: raw.measurement_unit,
2819
3886
  createdAt: raw.created_at,
2820
3887
  updatedAt: raw.updated_at ?? raw.created_at,
2821
- variants: raw.product_variants.map(
2822
- (variant) => mapVariant(variant, labels, chainId)
2823
- ),
3888
+ variants,
3889
+ detailVariants,
2824
3890
  media: raw.product_media.map((m) => mapMedia(m, storageUrl)),
2825
3891
  attributes: raw.product_attributes.map(
2826
3892
  (attribute) => mapProductAttribute(attribute, labels)
2827
3893
  ),
2828
3894
  relatedProducts: (raw.product_related_products ?? []).map(
2829
3895
  (relation) => mapRelatedProduct(relation, storageUrl)
2830
- )
3896
+ ),
3897
+ optionGroup,
3898
+ specification: raw.product_specification ?? null
2831
3899
  };
2832
3900
  }
2833
3901
  function mapCategory(raw) {
@@ -3158,7 +4226,7 @@ function hasMeaningfulCategoryListFilters(filters) {
3158
4226
  )
3159
4227
  );
3160
4228
  }
3161
- async function getWebsiteAvailabilityByVariantId(graphqlUrl, getHeaders, variantIds, chainId) {
4229
+ async function getWebsiteAvailabilityByVariantId(graphqlUrl, getHeaders, variantIds, chainId, websiteId) {
3162
4230
  const uniqueVariantIds = uniqueValues2(variantIds);
3163
4231
  const availabilityByVariantId = /* @__PURE__ */ new Map();
3164
4232
  for (const chunk of chunkArray2(
@@ -3168,7 +4236,7 @@ async function getWebsiteAvailabilityByVariantId(graphqlUrl, getHeaders, variant
3168
4236
  const data = await executeGraphQL(
3169
4237
  graphqlUrl,
3170
4238
  CHECK_AVAILABILITY,
3171
- { variantIds: chunk, chainId },
4239
+ { variantIds: chunk, chainId, websiteId },
3172
4240
  getHeaders
3173
4241
  );
3174
4242
  for (const item of data.websiteProductAvailability ?? []) {
@@ -3179,7 +4247,7 @@ async function getWebsiteAvailabilityByVariantId(graphqlUrl, getHeaders, variant
3179
4247
  }
3180
4248
  return availabilityByVariantId;
3181
4249
  }
3182
- async function filterProductsForWebsiteStock(graphqlUrl, getHeaders, products, chainId) {
4250
+ async function filterProductsForWebsiteStock(graphqlUrl, getHeaders, products, chainId, websiteId) {
3183
4251
  const variantIds = products.flatMap(
3184
4252
  (product) => product.variants.map((variant) => variant.id)
3185
4253
  );
@@ -3190,7 +4258,8 @@ async function filterProductsForWebsiteStock(graphqlUrl, getHeaders, products, c
3190
4258
  graphqlUrl,
3191
4259
  getHeaders,
3192
4260
  variantIds,
3193
- chainId
4261
+ chainId,
4262
+ websiteId
3194
4263
  )).entries()
3195
4264
  ).filter(([, item]) => item.inStock).map(([variantId]) => variantId)
3196
4265
  );
@@ -3208,7 +4277,7 @@ async function filterProductsForWebsiteStock(graphqlUrl, getHeaders, products, c
3208
4277
  )
3209
4278
  })).filter((product) => product.variants.length > 0);
3210
4279
  }
3211
- async function filterSeoProductsForWebsiteStock(graphqlUrl, getHeaders, products, chainId) {
4280
+ async function filterSeoProductsForWebsiteStock(graphqlUrl, getHeaders, products, chainId, websiteId) {
3212
4281
  const variantIds = products.flatMap(
3213
4282
  (product) => product.product_variants.map((variant) => variant.id)
3214
4283
  );
@@ -3219,7 +4288,8 @@ async function filterSeoProductsForWebsiteStock(graphqlUrl, getHeaders, products
3219
4288
  graphqlUrl,
3220
4289
  getHeaders,
3221
4290
  variantIds,
3222
- chainId
4291
+ chainId,
4292
+ websiteId
3223
4293
  )).entries()
3224
4294
  ).filter(([, item]) => item.inStock).map(([variantId]) => variantId)
3225
4295
  );
@@ -3250,6 +4320,32 @@ function mapProductSeoEntry(raw, storageUrl) {
3250
4320
  };
3251
4321
  }
3252
4322
  function createCatalogModule(graphqlUrl, getHeaders, getContext, storageUrl) {
4323
+ const loadProductSpecifications = async (productIds) => {
4324
+ const ctx = getContext();
4325
+ const uniqueProductIds = [...new Set(productIds)];
4326
+ const specifications = {};
4327
+ for (let offset = 0; offset < uniqueProductIds.length; offset += 100) {
4328
+ const batch = uniqueProductIds.slice(offset, offset + 100);
4329
+ try {
4330
+ const data = await executeGraphQL(
4331
+ graphqlUrl,
4332
+ WEBSITE_GET_PRODUCT_SPECIFICATIONS,
4333
+ { websiteId: ctx.websiteId, productIds: batch },
4334
+ getHeaders
4335
+ );
4336
+ if (!data.result?.success) continue;
4337
+ Object.assign(
4338
+ specifications,
4339
+ parseActionJson(
4340
+ data.result.specifications,
4341
+ {}
4342
+ )
4343
+ );
4344
+ } catch {
4345
+ }
4346
+ }
4347
+ return specifications;
4348
+ };
3253
4349
  const mapSingleProductResult = async (data) => {
3254
4350
  const ctx = getContext();
3255
4351
  if (!ctx.chainId) {
@@ -3263,15 +4359,187 @@ function createCatalogModule(graphqlUrl, getHeaders, getContext, storageUrl) {
3263
4359
  const publicAttributeLabels = buildPublicAttributeLabels(
3264
4360
  data.catalog_attribute_lists
3265
4361
  );
4362
+ const mappedProduct = mapProduct(
4363
+ product,
4364
+ storageUrl,
4365
+ publicAttributeLabels,
4366
+ ctx.chainId
4367
+ );
3266
4368
  const filteredProducts = await filterProductsForWebsiteStock(
3267
4369
  graphqlUrl,
3268
4370
  getHeaders,
3269
- [mapProduct(product, storageUrl, publicAttributeLabels, ctx.chainId)],
3270
- ctx.chainId
4371
+ [mappedProduct],
4372
+ ctx.chainId,
4373
+ ctx.websiteId
3271
4374
  );
3272
- return filteredProducts[0] ?? null;
4375
+ if (!filteredProducts[0]) return null;
4376
+ const specifications = await loadProductSpecifications([product.id]);
4377
+ return {
4378
+ ...mappedProduct,
4379
+ variants: getProductDetailVariants(mappedProduct),
4380
+ specification: specifications[product.id] ?? null
4381
+ };
4382
+ };
4383
+ const loadCatalogPage = async (opts, retryRateLimits = false) => {
4384
+ const ctx = getContext();
4385
+ if (!ctx.chainId) {
4386
+ throw new WebsiteError(
4387
+ "This website has no chain configured. Catalog requires a chain_id.",
4388
+ "NO_CHAIN"
4389
+ );
4390
+ }
4391
+ const chainId = ctx.chainId;
4392
+ const filters = {
4393
+ category_slug: opts.categorySlug,
4394
+ subcategory_slug: opts.subcategorySlug,
4395
+ tags: opts.tags,
4396
+ search: opts.search,
4397
+ brand_id: opts.brandId,
4398
+ min_price: opts.minPrice,
4399
+ max_price: opts.maxPrice,
4400
+ attributes_by_slug: opts.attributesBySlug,
4401
+ technical_by_field: opts.technicalByField ? Object.fromEntries(
4402
+ Object.entries(opts.technicalByField).map(([fieldId, filter]) => [
4403
+ fieldId,
4404
+ {
4405
+ option_ids: filter.optionIds,
4406
+ boolean_values: filter.booleanValues,
4407
+ min: filter.min,
4408
+ max: filter.max,
4409
+ date_from: filter.dateFrom,
4410
+ date_to: filter.dateTo
4411
+ }
4412
+ ])
4413
+ ) : void 0,
4414
+ sort_by: opts.sortBy,
4415
+ limit: opts.limit,
4416
+ offset: opts.offset
4417
+ };
4418
+ const data = await executeGraphQL(
4419
+ graphqlUrl,
4420
+ WEBSITE_GET_CATALOG_PAGE,
4421
+ {
4422
+ websiteId: ctx.websiteId,
4423
+ categoryListKey: opts.categoryListKey,
4424
+ attributeListKey: opts.attributeListKey,
4425
+ filters,
4426
+ include: opts.include
4427
+ },
4428
+ getHeaders,
4429
+ retryRateLimits ? { retry: STATIC_CATALOG_RETRY } : void 0
4430
+ );
4431
+ if (!data.result.success) {
4432
+ throw new WebsiteError(
4433
+ data.result.errors.join("; ") || "Unable to resolve catalog page",
4434
+ "CATALOG_PAGE_ERROR"
4435
+ );
4436
+ }
4437
+ const products = parseActionJson(data.result.products, []);
4438
+ const categoryList = parseActionJson(
4439
+ data.result.category_list,
4440
+ null
4441
+ );
4442
+ const attributeList = parseActionJson(
4443
+ data.result.attribute_list,
4444
+ null
4445
+ );
4446
+ const filtersResult = parseActionJson(
4447
+ data.result.filters,
4448
+ null
4449
+ );
4450
+ const brands = parseActionJson(data.result.brands, []);
4451
+ const pagination = parseActionJson(
4452
+ data.result.pagination,
4453
+ {
4454
+ limit: opts.limit ?? 24,
4455
+ offset: opts.offset ?? 0,
4456
+ total: 0,
4457
+ hasMore: false
4458
+ }
4459
+ );
4460
+ const appliedFilters = parseActionJson(
4461
+ data.result.applied_filters,
4462
+ {}
4463
+ );
4464
+ const debug = parseActionJson(
4465
+ data.result.debug,
4466
+ null
4467
+ );
4468
+ const labels = buildPublicAttributeLabels(
4469
+ attributeList ? [attributeList] : []
4470
+ );
4471
+ return {
4472
+ products: products.map(
4473
+ (product) => mapProduct(product, storageUrl, labels, chainId)
4474
+ ),
4475
+ categories: categoryList ? mapCategoryList(categoryList) : null,
4476
+ attributes: attributeList ? mapAttributeList(attributeList) : null,
4477
+ filters: filtersResult,
4478
+ brands,
4479
+ pagination,
4480
+ appliedFilters,
4481
+ debug
4482
+ };
3273
4483
  };
3274
4484
  return {
4485
+ async getProductLiveState(productVariantIds) {
4486
+ const ctx = getContext();
4487
+ if (!ctx.chainId) {
4488
+ throw new WebsiteError(
4489
+ "This website has no chain configured. Product live state requires a chain_id.",
4490
+ "NO_CHAIN"
4491
+ );
4492
+ }
4493
+ const chainId = ctx.chainId;
4494
+ const variantIds = [...new Set(productVariantIds)];
4495
+ if (variantIds.length === 0) return [];
4496
+ if (variantIds.length > MAX_LIVE_STATE_VARIANT_IDS) {
4497
+ throw new WebsiteError(
4498
+ `Product live state accepts at most ${MAX_LIVE_STATE_VARIANT_IDS} variants per request.`,
4499
+ "PRODUCT_LIVE_STATE_LIMIT_EXCEEDED"
4500
+ );
4501
+ }
4502
+ const data = await executeGraphQL(
4503
+ graphqlUrl,
4504
+ GET_PRODUCT_LIVE_STATE,
4505
+ {
4506
+ variantIds,
4507
+ companyId: ctx.companyId,
4508
+ chainId,
4509
+ websiteId: ctx.websiteId
4510
+ },
4511
+ getHeaders
4512
+ );
4513
+ const variantsById = new Map(
4514
+ (data.product_variants ?? []).map((variant) => [variant.id, variant])
4515
+ );
4516
+ const availabilityById = new Map(
4517
+ (data.websiteProductAvailability ?? []).map((availability) => [
4518
+ availability.product_variant_id,
4519
+ availability
4520
+ ])
4521
+ );
4522
+ return variantIds.flatMap((variantId) => {
4523
+ const variant = variantsById.get(variantId);
4524
+ if (!variant) return [];
4525
+ const availability = availabilityById.get(variantId);
4526
+ const pricing = resolveProductPricing(
4527
+ selectPublicPrices(
4528
+ variant.product_variant_prices.map(mapPrice),
4529
+ chainId
4530
+ )
4531
+ );
4532
+ return [
4533
+ {
4534
+ productVariantId: variantId,
4535
+ inStock: availability?.in_stock ?? false,
4536
+ availabilityLabel: availability?.label ?? "Agotado",
4537
+ maxPurchasableQuantity: availability?.max_purchasable_quantity ?? 0,
4538
+ pricing
4539
+ }
4540
+ ];
4541
+ });
4542
+ },
3275
4543
  async getProductRecommendations(opts) {
3276
4544
  const ctx = getContext();
3277
4545
  const limit = opts.limit ?? 16;
@@ -3334,7 +4602,8 @@ function createCatalogModule(graphqlUrl, getHeaders, getContext, storageUrl) {
3334
4602
  graphqlUrl,
3335
4603
  getHeaders,
3336
4604
  data.products ?? [],
3337
- ctx.chainId
4605
+ ctx.chainId,
4606
+ ctx.websiteId
3338
4607
  );
3339
4608
  const entries = availableProducts.slice(0, limit).map((product) => mapProductSeoEntry(product, storageUrl)).filter((entry) => Boolean(entry));
3340
4609
  return {
@@ -3346,86 +4615,36 @@ function createCatalogModule(graphqlUrl, getHeaders, getContext, storageUrl) {
3346
4615
  }
3347
4616
  };
3348
4617
  },
3349
- async getCatalogPage(opts) {
4618
+ getCatalogPage: loadCatalogPage,
4619
+ async getStaticCatalogSnapshot(opts) {
3350
4620
  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
4621
+ return (await collectStaticCatalog({
4622
+ websiteId: ctx.websiteId,
4623
+ snapshot: opts,
4624
+ loadPage: (options) => loadCatalogPage(options, true)
4625
+ })).snapshot;
4626
+ },
4627
+ async getStaticCatalogBootstrap(opts) {
4628
+ const ctx = getContext();
4629
+ const { snapshot, initialPage } = await collectStaticCatalog({
4630
+ websiteId: ctx.websiteId,
4631
+ snapshot: opts,
4632
+ bootstrap: {
4633
+ initialPageSize: opts.initialPageSize,
4634
+ sortBy: opts.sortBy
3380
4635
  },
3381
- getHeaders
3382
- );
3383
- if (!data.result.success) {
4636
+ loadPage: (options) => loadCatalogPage(options, true)
4637
+ });
4638
+ if (!initialPage) {
3384
4639
  throw new WebsiteError(
3385
- data.result.errors.join("; ") || "Unable to resolve catalog page",
3386
- "CATALOG_PAGE_ERROR"
4640
+ "Static catalog bootstrap could not resolve its initial page.",
4641
+ "STATIC_CATALOG_BOOTSTRAP_MISSING_INITIAL_PAGE"
3387
4642
  );
3388
4643
  }
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
4644
  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
4645
+ ...snapshot,
4646
+ bootstrapSchemaVersion: 1,
4647
+ initialPage
3429
4648
  };
3430
4649
  },
3431
4650
  async getProducts(opts) {
@@ -3460,8 +4679,16 @@ function createCatalogModule(graphqlUrl, getHeaders, getContext, storageUrl) {
3460
4679
  (data.products ?? []).map(
3461
4680
  (p) => mapProduct(p, storageUrl, publicAttributeLabels, chainId)
3462
4681
  ),
3463
- chainId
4682
+ chainId,
4683
+ ctx.websiteId
3464
4684
  );
4685
+ const specifications = await loadProductSpecifications(
4686
+ products.map((product) => product.id)
4687
+ );
4688
+ products = products.map((product) => ({
4689
+ ...product,
4690
+ specification: specifications[product.id] ?? null
4691
+ }));
3465
4692
  if (opts?.minPrice !== void 0 || opts?.maxPrice !== void 0) {
3466
4693
  products = products.filter(
3467
4694
  (product) => product.variants.some((variant) => {
@@ -3572,7 +4799,8 @@ function createCatalogModule(graphqlUrl, getHeaders, getContext, storageUrl) {
3572
4799
  graphqlUrl,
3573
4800
  getHeaders,
3574
4801
  variantIds,
3575
- chainId
4802
+ chainId,
4803
+ ctx.websiteId
3576
4804
  );
3577
4805
  const applicableCategories = /* @__PURE__ */ new Set();
3578
4806
  const applicableSubcategoryIds = /* @__PURE__ */ new Set();
@@ -3690,7 +4918,8 @@ function createCatalogModule(graphqlUrl, getHeaders, getContext, storageUrl) {
3690
4918
  graphqlUrl,
3691
4919
  getHeaders,
3692
4920
  variantIds,
3693
- chainId
4921
+ chainId,
4922
+ ctx.websiteId
3694
4923
  );
3695
4924
  const publicAttributeLabels = buildPublicAttributeLabels(
3696
4925
  data.catalog_attribute_lists
@@ -3795,7 +5024,8 @@ function createCatalogModule(graphqlUrl, getHeaders, getContext, storageUrl) {
3795
5024
  min: minPrice,
3796
5025
  max: maxPrice
3797
5026
  },
3798
- attributes
5027
+ attributes,
5028
+ technical: []
3799
5029
  };
3800
5030
  },
3801
5031
  async getSearchSuggestions(opts) {
@@ -3849,6 +5079,14 @@ function mapCartItem2(raw) {
3849
5079
  id: raw.id,
3850
5080
  productVariantId: raw.product_variant_id,
3851
5081
  quantity: raw.quantity,
5082
+ unitPrice: raw.unit_price ?? null,
5083
+ lineTotal: raw.line_total ?? null,
5084
+ baseUnitPrice: raw.base_unit_price ?? raw.unit_price ?? null,
5085
+ unitSavings: raw.unit_savings ?? 0,
5086
+ lineSavings: raw.line_savings ?? 0,
5087
+ volumePricingEligible: raw.volume_pricing_eligible ?? false,
5088
+ resolvedPriceType: raw.resolved_price_type ?? null,
5089
+ volumePricingMinQuantity: raw.volume_pricing_min_quantity ?? null,
3852
5090
  productVariant: {
3853
5091
  sku: raw.product_variant.sku,
3854
5092
  product: raw.product_variant.product,
@@ -3873,7 +5111,20 @@ function mapCart2(raw) {
3873
5111
  },
3874
5112
  status: raw.status,
3875
5113
  createdAt: raw.created_at,
3876
- updatedAt: raw.updated_at
5114
+ updatedAt: raw.updated_at,
5115
+ volumePricing: raw.volume_pricing ? {
5116
+ ...raw.volume_pricing,
5117
+ totalSavings: raw.volume_pricing.totalSavings ?? 0
5118
+ } : {
5119
+ enabled: false,
5120
+ eligibleQuantity: 0,
5121
+ appliedPriceType: null,
5122
+ appliedMinQuantity: null,
5123
+ nextPriceType: null,
5124
+ nextMinQuantity: null,
5125
+ remainingQuantity: null,
5126
+ totalSavings: 0
5127
+ }
3877
5128
  };
3878
5129
  }
3879
5130
  function mapCouponResult(raw) {
@@ -3892,6 +5143,7 @@ function mapCouponResult(raw) {
3892
5143
  };
3893
5144
  }
3894
5145
  function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionStorage) {
5146
+ let shippingPolicyCache = null;
3895
5147
  return {
3896
5148
  async getLocationOptions() {
3897
5149
  const context = getContext();
@@ -3989,22 +5241,39 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
3989
5241
  }));
3990
5242
  },
3991
5243
  async getShippingPolicy() {
5244
+ const now = Date.now();
5245
+ if (shippingPolicyCache && shippingPolicyCache.expiresAt > now) {
5246
+ return shippingPolicyCache.request;
5247
+ }
3992
5248
  const context = getContext();
3993
- const data = await executeGraphQL(
5249
+ const request = executeGraphQL(
3994
5250
  graphqlUrl,
3995
5251
  GET_SHIPPING_POLICY,
3996
5252
  { websiteId: context.websiteId },
3997
5253
  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
5254
+ ).then((data) => {
5255
+ const result = data.websiteGetShippingPolicy;
5256
+ if (!result.success || !result.policy) {
5257
+ throw new CheckoutError(result.errors ?? ["SHIPPING_POLICY_FAILED"]);
5258
+ }
5259
+ return {
5260
+ freeShippingEnabled: result.policy.free_shipping_enabled,
5261
+ freeShippingThreshold: result.policy.free_shipping_threshold,
5262
+ currency: result.policy.currency
5263
+ };
5264
+ });
5265
+ shippingPolicyCache = {
5266
+ expiresAt: now + 5 * 60 * 1e3,
5267
+ request
4007
5268
  };
5269
+ try {
5270
+ return await request;
5271
+ } catch (error) {
5272
+ if (shippingPolicyCache?.request === request) {
5273
+ shippingPolicyCache = null;
5274
+ }
5275
+ throw error;
5276
+ }
4008
5277
  },
4009
5278
  async getFreeShippingProgress(input) {
4010
5279
  if (input.items.length === 0) {
@@ -4170,6 +5439,13 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
4170
5439
  initialPayment: input.initialPayment ? {
4171
5440
  amount: input.initialPayment.amount,
4172
5441
  payment_method_id: input.initialPayment.paymentMethodId
5442
+ } : void 0,
5443
+ marketingAttribution: input.marketingAttribution ? serializeWebsiteMarketingAttribution(input.marketingAttribution) : void 0,
5444
+ advertisingConsent: input.advertisingConsent ? {
5445
+ policy_id: input.advertisingConsent.policyId,
5446
+ policy_version: input.advertisingConsent.policyVersion,
5447
+ statement_hash: input.advertisingConsent.statementHash,
5448
+ granted: input.advertisingConsent.granted
4173
5449
  } : void 0
4174
5450
  },
4175
5451
  getHeaders
@@ -4305,18 +5581,23 @@ function readOptionalText(value) {
4305
5581
  function enrichMediaValue(value, storageUrl) {
4306
5582
  const fileId = typeof value.fileId === "string" && value.fileId.trim() ? value.fileId.trim() : "";
4307
5583
  if (!fileId) return value;
4308
- const urls = buildStorageMediaUrls(storageUrl, fileId);
5584
+ const isProductMedia = typeof value.id === "string" && typeof value.order === "number";
5585
+ const isEditorialImage = value.mediaKind === "image" && !isProductMedia;
5586
+ const originalWidth = typeof value.originalWidth === "number" ? value.originalWidth : null;
5587
+ const urls = isEditorialImage ? buildStorageContentMediaUrls(storageUrl, fileId, originalWidth) : buildStorageMediaUrls(storageUrl, fileId);
4309
5588
  return {
4310
5589
  ...value,
4311
5590
  ...urls,
4312
- url: value.url ?? urls.originalUrl
5591
+ url: value.url ?? urls.originalUrl,
5592
+ src: value.src ?? value.url ?? urls.originalUrl
4313
5593
  };
4314
5594
  }
4315
5595
  function normalizeRichTextNode(value, storageUrl) {
4316
5596
  const type = value.type;
4317
5597
  const attrs = isRecord(value.attrs) ? value.attrs : null;
4318
5598
  if (attrs && (type === "bacanoImage" || type === "bacanoVideo") && typeof attrs.fileId === "string") {
4319
- const urls = buildStorageMediaUrls(storageUrl, attrs.fileId);
5599
+ const originalWidth = typeof attrs.originalWidth === "number" ? attrs.originalWidth : null;
5600
+ const urls = type === "bacanoImage" ? buildStorageContentMediaUrls(storageUrl, attrs.fileId, originalWidth) : buildStorageMediaUrls(storageUrl, attrs.fileId);
4320
5601
  const altText = readOptionalText(attrs.altText) ?? readOptionalText(attrs.alt);
4321
5602
  const caption = readOptionalText(attrs.caption);
4322
5603
  const showCaption = Boolean(caption) && attrs.showCaption !== false;
@@ -4448,6 +5729,452 @@ function createContentModule(graphqlUrl, getHeaders, getContext, storageUrl) {
4448
5729
  };
4449
5730
  }
4450
5731
 
5732
+ // libs/sdk/src/marketing/clarity.ts
5733
+ var initializedDestinations = /* @__PURE__ */ new Set();
5734
+ function clarityWindow() {
5735
+ return typeof window === "undefined" ? null : window;
5736
+ }
5737
+ function prepareClarity(projectId) {
5738
+ const target = clarityWindow();
5739
+ if (!target || initializedDestinations.has(projectId)) return;
5740
+ if (!target.clarity) {
5741
+ const clarity = function() {
5742
+ clarity.q ??= [];
5743
+ clarity.q.push(arguments);
5744
+ };
5745
+ target.clarity = clarity;
5746
+ }
5747
+ initializedDestinations.add(projectId);
5748
+ }
5749
+ function loadClarityScript(projectId) {
5750
+ if (typeof document === "undefined") return;
5751
+ const id = `bacano-clarity-${projectId}`;
5752
+ if (document.getElementById(id)) return;
5753
+ const script = document.createElement("script");
5754
+ script.id = id;
5755
+ script.async = true;
5756
+ script.src = `https://www.clarity.ms/tag/${encodeURIComponent(projectId)}`;
5757
+ script.referrerPolicy = "strict-origin-when-cross-origin";
5758
+ document.head.appendChild(script);
5759
+ }
5760
+ function sendClarityEvent(eventName) {
5761
+ clarityWindow()?.clarity?.("event", eventName);
5762
+ }
5763
+
5764
+ // libs/sdk/src/marketing/domain.ts
5765
+ function matchesCanonicalMarketingHost(canonicalDomain, currentHostname) {
5766
+ if (canonicalDomain === void 0) return true;
5767
+ if (!canonicalDomain?.trim()) return false;
5768
+ try {
5769
+ const configuredHost = new URL(
5770
+ canonicalDomain.includes("://") ? canonicalDomain : `https://${canonicalDomain}`
5771
+ ).hostname.toLowerCase();
5772
+ return currentHostname.toLowerCase() === configuredHost;
5773
+ } catch {
5774
+ return false;
5775
+ }
5776
+ }
5777
+
5778
+ // libs/sdk/src/marketing/ga4.ts
5779
+ function mapGa4Items(items) {
5780
+ return items?.map((item) => ({
5781
+ item_id: item.itemId,
5782
+ item_name: item.itemName,
5783
+ item_variant: item.itemVariant,
5784
+ item_brand: item.itemBrand,
5785
+ item_category: item.itemCategory,
5786
+ price: item.price,
5787
+ quantity: item.quantity,
5788
+ index: item.index,
5789
+ sku: item.sku
5790
+ }));
5791
+ }
5792
+
5793
+ // libs/sdk/src/marketing/types.ts
5794
+ var MARKETING_SCHEMA_VERSION = 1;
5795
+ var WEBSITE_MARKETING_CONTACT_PLACEMENTS = [
5796
+ "product_detail",
5797
+ "floating",
5798
+ "header",
5799
+ "footer",
5800
+ "contact_page"
5801
+ ];
5802
+
5803
+ // libs/sdk/src/marketing/runtime.ts
5804
+ var initializedGa4Destinations = /* @__PURE__ */ new Set();
5805
+ var initializedMetaDestinations = /* @__PURE__ */ new Set();
5806
+ function marketingWindow() {
5807
+ return typeof window === "undefined" ? null : window;
5808
+ }
5809
+ function loadScriptOnce(id, source) {
5810
+ if (typeof document === "undefined" || document.getElementById(id)) return;
5811
+ const script = document.createElement("script");
5812
+ script.id = id;
5813
+ script.async = true;
5814
+ script.src = source;
5815
+ script.referrerPolicy = "strict-origin-when-cross-origin";
5816
+ document.head.appendChild(script);
5817
+ }
5818
+ function prepareGa4(measurementId) {
5819
+ const target = marketingWindow();
5820
+ if (!target) return;
5821
+ if (initializedGa4Destinations.has(measurementId)) return;
5822
+ target.dataLayer ??= [];
5823
+ target.gtag ??= function() {
5824
+ target.dataLayer?.push(arguments);
5825
+ };
5826
+ target.gtag("js", /* @__PURE__ */ new Date());
5827
+ target.gtag("config", measurementId, { send_page_view: false });
5828
+ initializedGa4Destinations.add(measurementId);
5829
+ }
5830
+ function loadGa4Script(measurementId) {
5831
+ loadScriptOnce(
5832
+ `bacano-ga4-${measurementId}`,
5833
+ `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`
5834
+ );
5835
+ }
5836
+ function captureGa4ClientId(measurementId, websiteId, diagnostic) {
5837
+ const target = marketingWindow();
5838
+ target?.gtag?.("get", measurementId, "client_id", (clientId) => {
5839
+ if (storeWebsiteMarketingGa4ClientId(websiteId, clientId)) return;
5840
+ diagnostic?.({
5841
+ level: "warning",
5842
+ code: "GA4_CLIENT_ID_UNAVAILABLE",
5843
+ message: "GA4 did not provide a valid browser client identifier"
5844
+ });
5845
+ });
5846
+ }
5847
+ function captureGa4SessionId(measurementId, websiteId, diagnostic) {
5848
+ const target = marketingWindow();
5849
+ target?.gtag?.("get", measurementId, "session_id", (sessionId) => {
5850
+ if (storeWebsiteMarketingGa4SessionId(websiteId, sessionId)) return;
5851
+ diagnostic?.({
5852
+ level: "warning",
5853
+ code: "GA4_SESSION_ID_UNAVAILABLE",
5854
+ message: "GA4 did not provide a valid browser session identifier"
5855
+ });
5856
+ });
5857
+ }
5858
+ function prepareMeta(pixelId) {
5859
+ const target = marketingWindow();
5860
+ if (!target) return;
5861
+ if (initializedMetaDestinations.has(pixelId)) return;
5862
+ if (!target.fbq) {
5863
+ const queue = [];
5864
+ const fbq = ((...args) => {
5865
+ if (fbq.callMethod) {
5866
+ fbq.callMethod(...args);
5867
+ return;
5868
+ }
5869
+ queue.push(args);
5870
+ });
5871
+ fbq.push = fbq;
5872
+ fbq.loaded = true;
5873
+ fbq.version = "2.0";
5874
+ fbq.queue = queue;
5875
+ target.fbq = fbq;
5876
+ target._fbq = fbq;
5877
+ }
5878
+ target.fbq("init", pixelId);
5879
+ initializedMetaDestinations.add(pixelId);
5880
+ }
5881
+ function loadMetaScript() {
5882
+ loadScriptOnce(
5883
+ "bacano-meta-pixel",
5884
+ "https://connect.facebook.net/en_US/fbevents.js"
5885
+ );
5886
+ }
5887
+ function scheduleScriptLoad(load, strategy) {
5888
+ const target = marketingWindow();
5889
+ if (!target || strategy !== "idle") {
5890
+ load();
5891
+ return () => void 0;
5892
+ }
5893
+ let completed = false;
5894
+ let idleHandle = null;
5895
+ let timeoutHandle = null;
5896
+ const interactionEvents = ["pointerdown", "keydown", "touchstart"];
5897
+ const cleanup = () => {
5898
+ for (const eventName of interactionEvents) {
5899
+ target.removeEventListener(eventName, finish, true);
5900
+ }
5901
+ if (idleHandle !== null) target.cancelIdleCallback?.(idleHandle);
5902
+ if (timeoutHandle !== null) target.clearTimeout(timeoutHandle);
5903
+ };
5904
+ const finish = () => {
5905
+ if (completed) return;
5906
+ completed = true;
5907
+ cleanup();
5908
+ load();
5909
+ };
5910
+ for (const eventName of interactionEvents) {
5911
+ target.addEventListener(eventName, finish, {
5912
+ capture: true,
5913
+ once: true,
5914
+ passive: true
5915
+ });
5916
+ }
5917
+ if (target.requestIdleCallback) {
5918
+ idleHandle = target.requestIdleCallback(finish, { timeout: 2e3 });
5919
+ } else {
5920
+ timeoutHandle = target.setTimeout(finish, 1500);
5921
+ }
5922
+ return cleanup;
5923
+ }
5924
+ function metaEventName(eventName) {
5925
+ const standard = {
5926
+ page_view: "PageView",
5927
+ search: "Search",
5928
+ view_item: "ViewContent",
5929
+ add_to_cart: "AddToCart",
5930
+ begin_checkout: "InitiateCheckout",
5931
+ add_payment_info: "AddPaymentInfo",
5932
+ contact: "Contact"
5933
+ };
5934
+ return standard[eventName] ?? eventName;
5935
+ }
5936
+ function isWebsiteMarketingContactPlacement(value) {
5937
+ return WEBSITE_MARKETING_CONTACT_PLACEMENTS.some(
5938
+ (candidate) => candidate === value
5939
+ );
5940
+ }
5941
+ function contactPlacementFor(event) {
5942
+ const placement = event.contactPlacement;
5943
+ if (event.eventName !== "contact" || !isWebsiteMarketingContactPlacement(placement)) {
5944
+ return void 0;
5945
+ }
5946
+ return placement;
5947
+ }
5948
+ function sendGa4(event, diagnosticMode) {
5949
+ const target = marketingWindow();
5950
+ const contactPlacement = contactPlacementFor(event);
5951
+ target?.gtag?.("event", event.eventName, {
5952
+ currency: event.currency,
5953
+ value: event.value,
5954
+ search_term: event.searchTerm,
5955
+ item_list_id: event.itemListId,
5956
+ item_list_name: event.itemListName,
5957
+ shipping_tier: event.shippingTier,
5958
+ payment_type: event.paymentType,
5959
+ method: event.contactMethod,
5960
+ ...contactPlacement ? { contact_placement: contactPlacement } : void 0,
5961
+ page_location: event.pageLocation,
5962
+ page_title: event.pageTitle,
5963
+ items: mapGa4Items(event.items),
5964
+ event_id: event.eventId,
5965
+ debug_mode: diagnosticMode ? true : void 0
5966
+ });
5967
+ }
5968
+ function sendMeta(event) {
5969
+ const target = marketingWindow();
5970
+ const eventName = metaEventName(event.eventName);
5971
+ const isStandard = eventName !== event.eventName;
5972
+ const contactPlacement = contactPlacementFor(event);
5973
+ target?.fbq?.(
5974
+ isStandard ? "track" : "trackCustom",
5975
+ eventName,
5976
+ {
5977
+ currency: event.currency,
5978
+ value: event.value,
5979
+ search_string: event.searchTerm,
5980
+ content_ids: event.items?.map((item) => item.itemId),
5981
+ contents: event.items?.map((item) => ({
5982
+ id: item.itemId,
5983
+ quantity: item.quantity ?? 1,
5984
+ item_price: item.price
5985
+ })),
5986
+ content_type: event.items?.length ? "product" : void 0,
5987
+ contact_method: event.contactMethod,
5988
+ ...contactPlacement ? { contact_placement: contactPlacement } : void 0
5989
+ },
5990
+ { eventID: event.eventId }
5991
+ );
5992
+ }
5993
+ function newEventId() {
5994
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`;
5995
+ }
5996
+ function createWebsiteMarketingRuntime(options) {
5997
+ let destroyed = false;
5998
+ let cancelScheduledLoad = () => void 0;
5999
+ const target = marketingWindow();
6000
+ const enabled = options.config.enabled && Boolean(
6001
+ target && matchesCanonicalMarketingHost(
6002
+ options.canonicalDomain,
6003
+ target.location.hostname
6004
+ )
6005
+ ) && Boolean(
6006
+ options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity
6007
+ );
6008
+ if (enabled) {
6009
+ try {
6010
+ if (options.config.destinations.ga4) {
6011
+ prepareGa4(options.config.destinations.ga4.measurementId);
6012
+ }
6013
+ if (options.config.destinations.meta) {
6014
+ prepareMeta(options.config.destinations.meta.pixelId);
6015
+ }
6016
+ if (options.config.destinations.clarity) {
6017
+ prepareClarity(options.config.destinations.clarity.projectId);
6018
+ }
6019
+ cancelScheduledLoad = scheduleScriptLoad(() => {
6020
+ if (destroyed) return;
6021
+ if (options.config.destinations.ga4) {
6022
+ loadGa4Script(options.config.destinations.ga4.measurementId);
6023
+ }
6024
+ if (options.config.destinations.meta) loadMetaScript();
6025
+ if (options.config.destinations.clarity) {
6026
+ loadClarityScript(options.config.destinations.clarity.projectId);
6027
+ }
6028
+ }, options.scriptLoadStrategy);
6029
+ captureWebsiteMarketingAttribution(
6030
+ options.websiteId,
6031
+ options.config.attributionWindowDays
6032
+ );
6033
+ if (options.config.destinations.ga4) {
6034
+ captureGa4ClientId(
6035
+ options.config.destinations.ga4.measurementId,
6036
+ options.websiteId,
6037
+ options.diagnostic
6038
+ );
6039
+ captureGa4SessionId(
6040
+ options.config.destinations.ga4.measurementId,
6041
+ options.websiteId,
6042
+ options.diagnostic
6043
+ );
6044
+ }
6045
+ if (options.config.destinations.meta) {
6046
+ refreshWebsiteMarketingMetaContext(options.websiteId);
6047
+ }
6048
+ } catch (error) {
6049
+ options.diagnostic?.({
6050
+ level: "warning",
6051
+ code: "RUNTIME_INITIALIZATION_FAILED",
6052
+ message: error instanceof Error ? error.message : "Initialization failed"
6053
+ });
6054
+ }
6055
+ }
6056
+ return {
6057
+ enabled,
6058
+ track(input) {
6059
+ if (!enabled || destroyed) return;
6060
+ const event = {
6061
+ ...input,
6062
+ schemaVersion: 1,
6063
+ eventId: input.eventId ?? newEventId(),
6064
+ occurredAt: input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
6065
+ currency: input.currency ?? options.config.currencyCode,
6066
+ pageLocation: sanitizeMarketingUrl(
6067
+ input.pageLocation ?? (typeof window === "undefined" ? "" : window.location.href)
6068
+ )
6069
+ };
6070
+ try {
6071
+ if (options.config.destinations.ga4) {
6072
+ sendGa4(event, options.config.destinations.ga4.diagnosticMode);
6073
+ }
6074
+ if (options.config.destinations.meta) sendMeta(event);
6075
+ if (options.config.destinations.clarity) {
6076
+ sendClarityEvent(event.eventName);
6077
+ }
6078
+ options.diagnostic?.({
6079
+ level: "debug",
6080
+ code: "EVENT_DISPATCHED",
6081
+ message: "Marketing event dispatched",
6082
+ eventName: event.eventName
6083
+ });
6084
+ } catch (error) {
6085
+ options.diagnostic?.({
6086
+ level: "warning",
6087
+ code: "EVENT_DISPATCH_FAILED",
6088
+ message: error instanceof Error ? error.message : "Dispatch failed",
6089
+ eventName: event.eventName
6090
+ });
6091
+ }
6092
+ },
6093
+ captureAttribution() {
6094
+ if (!enabled) return null;
6095
+ captureWebsiteMarketingAttribution(
6096
+ options.websiteId,
6097
+ options.config.attributionWindowDays
6098
+ );
6099
+ if (options.config.destinations.meta) {
6100
+ refreshWebsiteMarketingMetaContext(options.websiteId);
6101
+ }
6102
+ return getWebsiteMarketingAttribution(options.websiteId);
6103
+ },
6104
+ getAttribution() {
6105
+ if (!enabled) return null;
6106
+ if (options.config.destinations.meta) {
6107
+ refreshWebsiteMarketingMetaContext(options.websiteId);
6108
+ }
6109
+ return getWebsiteMarketingAttribution(options.websiteId);
6110
+ },
6111
+ destroy() {
6112
+ destroyed = true;
6113
+ cancelScheduledLoad();
6114
+ }
6115
+ };
6116
+ }
6117
+
6118
+ // libs/sdk/src/modules/marketing.ts
6119
+ function readRetryAfter(response) {
6120
+ const value = Number(response.headers.get("Retry-After") ?? "1");
6121
+ return Number.isFinite(value) && value >= 0 ? Math.ceil(value) : 1;
6122
+ }
6123
+ function createMarketingModule(apiUrl, websiteSlug, getContext) {
6124
+ return {
6125
+ async captureLead(input) {
6126
+ const context = getContext();
6127
+ const attribution = getWebsiteMarketingAttribution(context.websiteId);
6128
+ let response;
6129
+ try {
6130
+ response = await fetch(
6131
+ `${apiUrl.replace(/\/+$/, "")}/api/v1/website/campaign-captures`,
6132
+ {
6133
+ method: "POST",
6134
+ headers: { "Content-Type": "application/json" },
6135
+ body: JSON.stringify({
6136
+ websiteSlug,
6137
+ campaignKey: input.campaignKey,
6138
+ idempotencyKey: input.idempotencyKey,
6139
+ source: input.source ?? "WEBSITE",
6140
+ commercialConsent: input.commercialConsent,
6141
+ contact: input.contact,
6142
+ answers: input.answers ?? {},
6143
+ attribution: attribution ? serializeWebsiteMarketingAttribution(attribution) : null
6144
+ })
6145
+ }
6146
+ );
6147
+ } catch (error) {
6148
+ throw new WebsiteError(
6149
+ `Could not capture the campaign lead: ${error instanceof Error ? error.message : String(error)}`,
6150
+ "NETWORK_ERROR"
6151
+ );
6152
+ }
6153
+ if (response.status === 429) {
6154
+ throw new RateLimitError(
6155
+ "Rate limit exceeded. Please try again later.",
6156
+ readRetryAfter(response)
6157
+ );
6158
+ }
6159
+ if (!response.ok) {
6160
+ throw new WebsiteError(
6161
+ `Could not capture the campaign lead (status ${response.status})`,
6162
+ response.status === 403 ? "ORIGIN_NOT_ALLOWED" : "INVALID_RESPONSE",
6163
+ { status: response.status }
6164
+ );
6165
+ }
6166
+ const result = await response.json();
6167
+ if (typeof result.accepted !== "boolean" || typeof result.message !== "string") {
6168
+ throw new WebsiteError(
6169
+ "Campaign capture returned an invalid response",
6170
+ "INVALID_RESPONSE"
6171
+ );
6172
+ }
6173
+ return { accepted: result.accepted, message: result.message };
6174
+ }
6175
+ };
6176
+ }
6177
+
4451
6178
  // libs/sdk/src/modules/orders.ts
4452
6179
  function mapOrderItem(raw) {
4453
6180
  return {
@@ -4622,13 +6349,13 @@ function createOrdersModule(graphqlUrl, getHeaders, getContext) {
4622
6349
  )) {
4623
6350
  return order;
4624
6351
  }
4625
- await wait(intervalMs);
6352
+ await wait2(intervalMs);
4626
6353
  }
4627
6354
  throw new CheckoutError(["PAYMENT_STATUS_TIMEOUT"]);
4628
6355
  }
4629
6356
  };
4630
6357
  }
4631
- function wait(ms) {
6358
+ function wait2(ms) {
4632
6359
  return new Promise((resolve) => {
4633
6360
  setTimeout(resolve, ms);
4634
6361
  });
@@ -4787,6 +6514,9 @@ function createProfileModule(graphqlUrl, getHeaders) {
4787
6514
  }
4788
6515
 
4789
6516
  // libs/sdk/src/modules/website.ts
6517
+ var WEBSITE_CONTEXT_CACHE_TTL_MS = 5 * 60 * 1e3;
6518
+ var WEBSITE_CONTEXT_CACHE_MAX_ENTRIES = 100;
6519
+ var websiteContextCache = /* @__PURE__ */ new Map();
4790
6520
  function mapWebsite(raw) {
4791
6521
  return {
4792
6522
  websiteId: raw.id,
@@ -4800,10 +6530,7 @@ function mapWebsite(raw) {
4800
6530
  }
4801
6531
  async function resolveWebsite(graphqlUrl, slug, headers) {
4802
6532
  if (!slug.trim()) {
4803
- throw new WebsiteError(
4804
- "websiteSlug is required",
4805
- "INVALID_CONFIG"
4806
- );
6533
+ throw new WebsiteError("websiteSlug is required", "INVALID_CONFIG");
4807
6534
  }
4808
6535
  const data = await executeGraphQL(
4809
6536
  graphqlUrl,
@@ -4819,6 +6546,31 @@ async function resolveWebsite(graphqlUrl, slug, headers) {
4819
6546
  }
4820
6547
  return mapWebsite(data.websites[0]);
4821
6548
  }
6549
+ function resolveWebsiteCached(graphqlUrl, slug, headers) {
6550
+ const normalizedSlug = slug.trim();
6551
+ const cacheKey = `${graphqlUrl}::${normalizedSlug}`;
6552
+ const now = Date.now();
6553
+ const cached = websiteContextCache.get(cacheKey);
6554
+ if (cached && cached.expiresAt > now) return cached.request;
6555
+ if (cached) websiteContextCache.delete(cacheKey);
6556
+ const request = resolveWebsite(graphqlUrl, normalizedSlug, headers).catch(
6557
+ (error) => {
6558
+ if (websiteContextCache.get(cacheKey)?.request === request) {
6559
+ websiteContextCache.delete(cacheKey);
6560
+ }
6561
+ throw error;
6562
+ }
6563
+ );
6564
+ if (websiteContextCache.size >= WEBSITE_CONTEXT_CACHE_MAX_ENTRIES) {
6565
+ const oldestKey = websiteContextCache.keys().next().value;
6566
+ if (oldestKey) websiteContextCache.delete(oldestKey);
6567
+ }
6568
+ websiteContextCache.set(cacheKey, {
6569
+ expiresAt: now + WEBSITE_CONTEXT_CACHE_TTL_MS,
6570
+ request
6571
+ });
6572
+ return request;
6573
+ }
4822
6574
 
4823
6575
  // libs/sdk/src/client.ts
4824
6576
  function normalizeApiUrl(apiUrl) {
@@ -4829,10 +6581,17 @@ function createBacanoClient(config) {
4829
6581
  throw new WebsiteError("apiUrl is required", "INVALID_CONFIG");
4830
6582
  if (!config.websiteSlug?.trim())
4831
6583
  throw new WebsiteError("websiteSlug is required", "INVALID_CONFIG");
6584
+ if (config.initialContext && config.initialContext.slug.trim() !== config.websiteSlug.trim()) {
6585
+ throw new WebsiteError(
6586
+ "initialContext slug must match websiteSlug",
6587
+ "INVALID_CONFIG"
6588
+ );
6589
+ }
4832
6590
  const apiUrl = normalizeApiUrl(config.apiUrl);
4833
6591
  const graphqlUrl = `${apiUrl}/api/v1/website/graphql`;
4834
6592
  const storageUrl = `${apiUrl}/storage/v1`;
4835
- let context = null;
6593
+ let context = config.initialContext ? { ...config.initialContext } : null;
6594
+ let initialization = null;
4836
6595
  function getContext() {
4837
6596
  if (!context) throw new NotInitializedError();
4838
6597
  return context;
@@ -4877,13 +6636,27 @@ function createBacanoClient(config) {
4877
6636
  getContext,
4878
6637
  storageUrl
4879
6638
  );
6639
+ const marketingModule = createMarketingModule(
6640
+ apiUrl,
6641
+ config.websiteSlug,
6642
+ getContext
6643
+ );
4880
6644
  return {
4881
6645
  async init() {
4882
- context = await resolveWebsite(
4883
- graphqlUrl,
4884
- config.websiteSlug,
4885
- getWebsiteHeaders
4886
- );
6646
+ if (context) return;
6647
+ if (!initialization) {
6648
+ initialization = resolveWebsiteCached(
6649
+ graphqlUrl,
6650
+ config.websiteSlug,
6651
+ getWebsiteHeaders
6652
+ ).then((resolvedContext) => {
6653
+ context = resolvedContext;
6654
+ }).catch((error) => {
6655
+ initialization = null;
6656
+ throw error;
6657
+ });
6658
+ }
6659
+ await initialization;
4887
6660
  },
4888
6661
  getContext,
4889
6662
  auth: {
@@ -4898,8 +6671,11 @@ function createBacanoClient(config) {
4898
6671
  },
4899
6672
  catalog: {
4900
6673
  getCatalogPage: catalogModule.getCatalogPage.bind(catalogModule),
6674
+ getStaticCatalogSnapshot: catalogModule.getStaticCatalogSnapshot.bind(catalogModule),
6675
+ getStaticCatalogBootstrap: catalogModule.getStaticCatalogBootstrap.bind(catalogModule),
4901
6676
  getProductSeoEntries: catalogModule.getProductSeoEntries.bind(catalogModule),
4902
6677
  getProductRecommendations: catalogModule.getProductRecommendations.bind(catalogModule),
6678
+ getProductLiveState: catalogModule.getProductLiveState.bind(catalogModule),
4903
6679
  getProducts: catalogModule.getProducts.bind(catalogModule),
4904
6680
  getProduct: catalogModule.getProduct.bind(catalogModule),
4905
6681
  getProductBySlug: catalogModule.getProductBySlug.bind(catalogModule),
@@ -4924,7 +6700,8 @@ function createBacanoClient(config) {
4924
6700
  setItem: cartModule.setItem.bind(cartModule),
4925
6701
  addItem: cartModule.addItem.bind(cartModule),
4926
6702
  updateItem: cartModule.updateItem.bind(cartModule),
4927
- removeItem: cartModule.removeItem.bind(cartModule)
6703
+ removeItem: cartModule.removeItem.bind(cartModule),
6704
+ clear: cartModule.clear.bind(cartModule)
4928
6705
  },
4929
6706
  checkout: {
4930
6707
  getLocationOptions: checkoutModule.getLocationOptions.bind(checkoutModule),
@@ -4951,6 +6728,9 @@ function createBacanoClient(config) {
4951
6728
  get: profileModule.get.bind(profileModule),
4952
6729
  getAddresses: profileModule.getAddresses.bind(profileModule),
4953
6730
  update: profileModule.update.bind(profileModule)
6731
+ },
6732
+ marketing: {
6733
+ captureLead: marketingModule.captureLead.bind(marketingModule)
4954
6734
  }
4955
6735
  };
4956
6736
  }