@snowcone-app/sdk 0.1.12 → 0.1.14

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.
package/dist/index.cjs CHANGED
@@ -38,6 +38,7 @@ __export(index_exports, {
38
38
  DEFAULT_ARTWORK_URL: () => DEFAULT_ARTWORK_URL,
39
39
  DEFAULT_ASPECT_RATIO: () => DEFAULT_ASPECT_RATIO,
40
40
  DEFAULT_COLOR: () => DEFAULT_COLOR,
41
+ DEFAULT_MOCKUP_BASE: () => DEFAULT_MOCKUP_BASE,
41
42
  DEFAULT_PLACEMENT_DIMENSIONS: () => DEFAULT_PLACEMENT_DIMENSIONS,
42
43
  Elements: () => Elements,
43
44
  ErrorManager: () => ErrorManager,
@@ -62,9 +63,9 @@ __export(index_exports, {
62
63
  VueAdapter: () => VueAdapter,
63
64
  adapterRegistry: () => adapterRegistry,
64
65
  autoRegister: () => autoRegister,
66
+ buildMockupUrl: () => buildMockupUrl2,
65
67
  componentRegistry: () => componentRegistry,
66
68
  computeDisabledChoices: () => computeDisabledChoices,
67
- config: () => config,
68
69
  createAddToCartEvent: () => createAddToCartEvent,
69
70
  createAddToCartHandler: () => createAddToCartHandler,
70
71
  createCartDetail: () => createCartDetail,
@@ -104,6 +105,7 @@ __export(index_exports, {
104
105
  getEffectiveAlignment: () => getEffectiveAlignment,
105
106
  getIncompleteSelectionMessage: () => getIncompleteSelectionMessage,
106
107
  getMissingSelections: () => getMissingSelections,
108
+ getMockupUrl: () => getMockupUrl,
107
109
  getOptionRenderType: () => getOptionRenderType,
108
110
  getPricePreview: () => getPricePreview,
109
111
  getProduct: () => getProduct,
@@ -121,7 +123,6 @@ __export(index_exports, {
121
123
  prepareOptionRenderData: () => prepareOptionRenderData,
122
124
  registerStandardComponents: () => registerStandardComponents,
123
125
  resolveBestCombination: () => resolveBestCombination,
124
- resolveMockupConfig: () => resolveMockupConfig,
125
126
  resolveMockupId: () => resolveMockupId,
126
127
  resolveVariantId: () => resolveVariantId,
127
128
  retryOperation: () => retryOperation,
@@ -185,10 +186,19 @@ var CatalogProductSchema = import_zod.z.object({
185
186
  ).optional(),
186
187
  placements: import_zod.z.array(
187
188
  import_zod.z.object({
189
+ // The one additive multi-placement field (rev 5 P1): a URL-safe slug
190
+ // used as the public param key (`asset.front`). Optional — older catalog
191
+ // docs predate it.
192
+ key: import_zod.z.string().optional(),
188
193
  label: import_zod.z.string(),
189
- type: import_zod.z.enum(["image", "color"]),
190
- width: import_zod.z.number().int(),
191
- height: import_zod.z.number().int(),
194
+ // Live data has placements with `type: null` (and ingestion may omit it
195
+ // entirely), not just "image"/"color". The previous strict enum threw on
196
+ // real catalog docs. Accept image | color | null | absent.
197
+ type: import_zod.z.enum(["image", "color"]).nullable().optional(),
198
+ // Live data carries width/height of 0 (and `.int()` rejected nothing of
199
+ // import here). Drop `.int()` and allow nullable so 0 / null / float pass.
200
+ width: import_zod.z.number().nullable().optional(),
201
+ height: import_zod.z.number().nullable().optional(),
192
202
  defaultScaleMode: import_zod.z.enum(["fill", "fit"]).optional(),
193
203
  fitMarginTop: import_zod.z.number().int().optional(),
194
204
  fitMarginRight: import_zod.z.number().int().optional(),
@@ -209,7 +219,7 @@ var SignatureCache = class {
209
219
  memoryCache = /* @__PURE__ */ new Map();
210
220
  maxMemoryEntries;
211
221
  maxLocalStorageEntries;
212
- localStorageKey = "merchify_signature_cache";
222
+ localStorageKey = "snowcone_signature_cache";
213
223
  localStorageAvailable;
214
224
  constructor(maxMemoryEntries = 500, maxLocalStorageEntries = 100) {
215
225
  this.maxMemoryEntries = maxMemoryEntries;
@@ -306,8 +316,8 @@ var SignatureCache = class {
306
316
  };
307
317
 
308
318
  // src/mockup/service.ts
309
- var DEFAULT_IMAGE_URL = typeof process !== "undefined" && process.env?.MERCHIFY_IMAGE_URL || "https://i.snowcone.app";
310
- var DEFAULT_SIGNER_URL = typeof process !== "undefined" && process.env?.MERCHIFY_SIGNER_URL || "https://s.snowcone.app/sign";
319
+ var DEFAULT_IMAGE_URL = typeof process !== "undefined" && process.env?.SNOWCONE_IMAGE_URL || "https://cdn.snowcone.app";
320
+ var DEFAULT_SIGNER_URL = typeof process !== "undefined" && process.env?.SNOWCONE_SIGNER_URL || "https://s.snowcone.app/sign";
311
321
  var RATE_LIMIT_PER_MINUTE = 450;
312
322
  var RATE_LIMIT_PER_SECOND = 20;
313
323
  var MockupServiceImpl = class {
@@ -315,11 +325,11 @@ var MockupServiceImpl = class {
315
325
  cache;
316
326
  rateLimitState;
317
327
  fetch;
318
- constructor(config2, customFetch) {
328
+ constructor(config, customFetch) {
319
329
  this.config = {
320
- imageUrl: config2.imageUrl || DEFAULT_IMAGE_URL,
321
- signerUrl: config2.signerUrl || DEFAULT_SIGNER_URL,
322
- accountId: config2.accountId || typeof process !== "undefined" && process.env?.MERCHIFY_ACCOUNT_ID || ""
330
+ imageUrl: config.imageUrl || DEFAULT_IMAGE_URL,
331
+ signerUrl: config.signerUrl || DEFAULT_SIGNER_URL,
332
+ shop: config.shop || typeof process !== "undefined" && process.env?.SNOWCONE_SHOP_ID || ""
323
333
  };
324
334
  this.cache = new SignatureCache();
325
335
  this.fetch = customFetch || fetch.bind(globalThis);
@@ -332,12 +342,21 @@ var MockupServiceImpl = class {
332
342
  if (!options.design || !options.product) {
333
343
  throw new Error("Missing required options: design and product are required");
334
344
  }
335
- if (!this.config.accountId) {
336
- throw new Error("Account ID is required for mockup generation");
345
+ if (!this.config.shop) {
346
+ throw new Error("Shop ID is required for mockup generation");
347
+ }
348
+ for (const element of options.design) {
349
+ if (element.type === "image" && element.imageUrl) {
350
+ if (element.imageUrl.startsWith("blob:")) {
351
+ throw new Error(
352
+ `[snowcone-sdk] Cannot generate mockup: design element for placement "${element.placement}" uses a blob URL ("${element.imageUrl.slice(0, 60)}..."). Blob URLs are local to this browser tab and cannot be downloaded by the mockup rendering server. Upload the image to a publicly accessible URL or convert it to a data URL before requesting a mockup.`
353
+ );
354
+ }
355
+ }
337
356
  }
338
357
  this.checkRateLimits();
339
358
  const relativeUrl = this.buildMockupUrlWithParams(options);
340
- const cacheKey = `${relativeUrl}:${this.config.accountId}`;
359
+ const cacheKey = `${relativeUrl}:${this.config.shop}`;
341
360
  const cachedUrl = this.cache.get(cacheKey);
342
361
  if (cachedUrl) {
343
362
  return cachedUrl;
@@ -422,17 +441,12 @@ var MockupServiceImpl = class {
422
441
  }
423
442
  }
424
443
  async getSignedUrl(relativeUrl) {
425
- const urlWithAccountId = relativeUrl + (relativeUrl.includes("?") ? "&" : "?") + `accountId=${encodeURIComponent(this.config.accountId)}`;
444
+ const urlWithAccountId = relativeUrl + (relativeUrl.includes("?") ? "&" : "?") + `shop=${encodeURIComponent(this.config.shop)}`;
426
445
  const cacheKey = urlWithAccountId;
427
446
  const cachedUrl = this.cache.get(cacheKey);
428
447
  if (cachedUrl) {
429
448
  return cachedUrl;
430
449
  }
431
- if (this.config.signerUrl?.includes("localhost") || this.config.signerUrl === "mock") {
432
- const signedUrl = this.config.imageUrl + urlWithAccountId + "&sig=bypass-sig-for-k6-load-test";
433
- this.cache.set(cacheKey, signedUrl);
434
- return signedUrl;
435
- }
436
450
  const signerUrl = new URL(this.config.signerUrl);
437
451
  signerUrl.searchParams.set("url", urlWithAccountId);
438
452
  try {
@@ -488,6 +502,456 @@ function createDevFetcher(baseUrl) {
488
502
  };
489
503
  }
490
504
 
505
+ // ../mockup-url/src/index.ts
506
+ var PARAMS = {
507
+ shop: "shop",
508
+ asset: "asset",
509
+ assets: "assets",
510
+ placement: "placement",
511
+ variant: "variant",
512
+ mockup: "mockup",
513
+ width: "width",
514
+ grain: "grain",
515
+ aspect: "aspect",
516
+ /** Live preview/calibration mask overrides (not cached). */
517
+ maskOverrides: "maskOverrides",
518
+ /**
519
+ * Render-only option picks (the `affectsCombinations:false` attributes), as a
520
+ * base64 JSON map `{ attributeName: choiceLabel }`. rendercenter already parses
521
+ * this and resolves per-choice underlays from it; the builder emits it so the
522
+ * edge resolver can forward render-only picks (e.g. a cap's Crown/Strap color)
523
+ * without changing rendercenter's grammar.
524
+ */
525
+ optionSelections: "optionSelections",
526
+ signature: "signature",
527
+ seal: "seal"
528
+ };
529
+ function asSimpleAsset(design) {
530
+ if (design.length !== 1) return null;
531
+ const el = design[0];
532
+ if (el.type === "color" || el.hex) return null;
533
+ if (!el.imageUrl) return null;
534
+ if (el.position || el.size || el.tiles != null) return null;
535
+ if (el.alignment && el.alignment !== "center") return null;
536
+ return { imageUrl: el.imageUrl, placement: el.placement };
537
+ }
538
+ var ALLOWED_WIDTHS = [
539
+ 400,
540
+ 600,
541
+ 800,
542
+ 1e3,
543
+ 1200,
544
+ 1400,
545
+ 1600,
546
+ 1800,
547
+ 2e3,
548
+ 2500,
549
+ 3e3,
550
+ 4e3
551
+ ];
552
+ function normalizeWidth(width) {
553
+ for (let i = ALLOWED_WIDTHS.length - 1; i >= 0; i--) {
554
+ if (ALLOWED_WIDTHS[i] <= width) return ALLOWED_WIDTHS[i];
555
+ }
556
+ return ALLOWED_WIDTHS[0];
557
+ }
558
+ function normalizeDesignElements(design) {
559
+ return design.map((element) => {
560
+ const normalized = {};
561
+ const keys = Object.keys(element).sort();
562
+ for (const key of keys) {
563
+ if (key === "width" || key === "height" || key === "type") continue;
564
+ if (key === "alignment" && element[key] === "center") {
565
+ continue;
566
+ }
567
+ const value = element[key];
568
+ if (value !== void 0 && value !== null) normalized[key] = value;
569
+ }
570
+ return normalized;
571
+ });
572
+ }
573
+ function buildMockupUrl(options, cfg) {
574
+ for (const element of options.design) {
575
+ if (element.type === "image" && element.imageUrl?.startsWith("blob:")) {
576
+ throw new Error(
577
+ `[snowcone] Cannot generate mockup: design element for placement "${element.placement}" uses a blob URL ("${element.imageUrl.slice(0, 60)}..."). Blob URLs are local to this browser tab and cannot be downloaded by the mockup rendering server. Upload the image to a publicly accessible URL or convert it to a data URL before requesting a mockup.`
578
+ );
579
+ }
580
+ }
581
+ const width = normalizeWidth(options.width);
582
+ const base = cfg.mockupBaseUrl.replace(/\/+$/, "");
583
+ const path = `/${encodeURIComponent(options.productId)}`;
584
+ let queryString = `${PARAMS.shop}=${encodeURIComponent(cfg.shop)}`;
585
+ const simple = asSimpleAsset(options.design);
586
+ if (simple) {
587
+ queryString += `&${PARAMS.asset}=${encodeURIComponent(simple.imageUrl)}`;
588
+ if (simple.placement) {
589
+ queryString += `&${PARAMS.placement}=${encodeURIComponent(simple.placement)}`;
590
+ }
591
+ } else {
592
+ const encodedAssets = encodeURIComponent(
593
+ btoa(JSON.stringify(normalizeDesignElements(options.design)))
594
+ );
595
+ queryString += `&${PARAMS.assets}=${encodedAssets}`;
596
+ }
597
+ queryString += `&${PARAMS.variant}=${encodeURIComponent(options.variantId)}`;
598
+ queryString += `&${PARAMS.mockup}=${encodeURIComponent(options.mockupId)}`;
599
+ queryString += `&${PARAMS.width}=${width}`;
600
+ if (options.optionSelections && Object.keys(options.optionSelections).length > 0) {
601
+ const sorted = {};
602
+ for (const k of Object.keys(options.optionSelections).sort()) {
603
+ sorted[k] = options.optionSelections[k];
604
+ }
605
+ const optionSelectionsBase64 = btoa(JSON.stringify(sorted));
606
+ queryString += `&${PARAMS.optionSelections}=${encodeURIComponent(
607
+ optionSelectionsBase64
608
+ )}`;
609
+ }
610
+ if (options.effects?.grain) {
611
+ queryString += `&${PARAMS.grain}=${options.effects.grain}`;
612
+ }
613
+ if (options.aspect && options.aspect !== "16:9") {
614
+ queryString += `&${PARAMS.aspect}=${encodeURIComponent(options.aspect)}`;
615
+ }
616
+ if (options.maskOverrides && options.maskOverrides.length > 0) {
617
+ const maskOverridesBase64 = btoa(JSON.stringify(options.maskOverrides));
618
+ queryString += `&${PARAMS.maskOverrides}=${encodeURIComponent(maskOverridesBase64)}`;
619
+ }
620
+ return `${base}${path}?${queryString}`;
621
+ }
622
+ var PARAM_PREFIX = {
623
+ asset: "asset.",
624
+ color: "color.",
625
+ tile: "tile.",
626
+ align: "align.",
627
+ opt: "opt."
628
+ };
629
+ var DEFAULT_PUBLIC_BASE_URL = "https://img.snowcone.app";
630
+ function buildPublicMockupUrl(options, cfg = {}) {
631
+ const base = (cfg.baseUrl ?? DEFAULT_PUBLIC_BASE_URL).replace(/\/+$/, "");
632
+ const path = `/${encodeURIComponent(options.productCode)}`;
633
+ const enc = encodeURIComponent;
634
+ const parts = [];
635
+ parts.push(`${PARAMS.shop}=${enc(options.shop)}`);
636
+ if (options.design) {
637
+ for (const key of Object.keys(options.design).sort()) {
638
+ const fill = options.design[key];
639
+ const k = enc(key);
640
+ if (typeof fill === "string") {
641
+ parts.push(`${PARAM_PREFIX.asset}${k}=${enc(fill)}`);
642
+ } else if ("color" in fill) {
643
+ parts.push(`${PARAM_PREFIX.color}${k}=${enc(fill.color)}`);
644
+ } else {
645
+ parts.push(`${PARAM_PREFIX.asset}${k}=${enc(fill.src)}`);
646
+ if (fill.tile !== void 0) {
647
+ parts.push(`${PARAM_PREFIX.tile}${k}=${fill.tile}`);
648
+ }
649
+ if (fill.align !== void 0) {
650
+ parts.push(`${PARAM_PREFIX.align}${k}=${enc(fill.align)}`);
651
+ }
652
+ }
653
+ }
654
+ } else if (options.assetUrl !== void 0) {
655
+ parts.push(`${PARAMS.asset}=${enc(options.assetUrl)}`);
656
+ }
657
+ if (options.placement !== void 0) {
658
+ parts.push(`${PARAMS.placement}=${enc(options.placement)}`);
659
+ }
660
+ if (options.options) {
661
+ for (const attr of Object.keys(options.options).sort()) {
662
+ parts.push(`${PARAM_PREFIX.opt}${enc(attr)}=${enc(options.options[attr])}`);
663
+ }
664
+ }
665
+ if (options.variant !== void 0) {
666
+ parts.push(`${PARAMS.variant}=${enc(options.variant)}`);
667
+ }
668
+ if (options.mockup !== void 0) {
669
+ parts.push(`${PARAMS.mockup}=${enc(options.mockup)}`);
670
+ }
671
+ if (options.width !== void 0) parts.push(`${PARAMS.width}=${options.width}`);
672
+ if (options.aspect !== void 0 && options.aspect !== "16:9") {
673
+ parts.push(`${PARAMS.aspect}=${enc(options.aspect)}`);
674
+ }
675
+ return `${base}${path}?${parts.join("&")}`;
676
+ }
677
+ function canonicalForSig(url) {
678
+ const hashIdx = url.indexOf("#");
679
+ const noHash = hashIdx === -1 ? url : url.slice(0, hashIdx);
680
+ const qIdx = noHash.indexOf("?");
681
+ const beforeQuery = qIdx === -1 ? noHash : noHash.slice(0, qIdx);
682
+ const rawQuery = qIdx === -1 ? "" : noHash.slice(qIdx + 1);
683
+ let pathname;
684
+ const schemeMatch = beforeQuery.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//);
685
+ if (schemeMatch) {
686
+ const afterScheme = beforeQuery.slice(schemeMatch[0].length);
687
+ const slashIdx = afterScheme.indexOf("/");
688
+ pathname = slashIdx === -1 ? "/" : afterScheme.slice(slashIdx);
689
+ } else {
690
+ pathname = beforeQuery;
691
+ }
692
+ const pairs = [];
693
+ if (rawQuery.length > 0) {
694
+ for (const segment of rawQuery.split("&")) {
695
+ if (segment.length === 0) continue;
696
+ const eq = segment.indexOf("=");
697
+ const rawKey = eq === -1 ? segment : segment.slice(0, eq);
698
+ const rawValue = eq === -1 ? "" : segment.slice(eq + 1);
699
+ const key = decodeURIComponent(rawKey);
700
+ if (key === "signature") continue;
701
+ pairs.push({ key, raw: rawValue });
702
+ }
703
+ }
704
+ pairs.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
705
+ const canonicalQuery = pairs.map((p) => `${p.key}=${p.raw}`).join("&");
706
+ return `${pathname}?${canonicalQuery}`;
707
+ }
708
+
709
+ // src/mockup/hmac.ts
710
+ var K = new Uint32Array([
711
+ 1116352408,
712
+ 1899447441,
713
+ 3049323471,
714
+ 3921009573,
715
+ 961987163,
716
+ 1508970993,
717
+ 2453635748,
718
+ 2870763221,
719
+ 3624381080,
720
+ 310598401,
721
+ 607225278,
722
+ 1426881987,
723
+ 1925078388,
724
+ 2162078206,
725
+ 2614888103,
726
+ 3248222580,
727
+ 3835390401,
728
+ 4022224774,
729
+ 264347078,
730
+ 604807628,
731
+ 770255983,
732
+ 1249150122,
733
+ 1555081692,
734
+ 1996064986,
735
+ 2554220882,
736
+ 2821834349,
737
+ 2952996808,
738
+ 3210313671,
739
+ 3336571891,
740
+ 3584528711,
741
+ 113926993,
742
+ 338241895,
743
+ 666307205,
744
+ 773529912,
745
+ 1294757372,
746
+ 1396182291,
747
+ 1695183700,
748
+ 1986661051,
749
+ 2177026350,
750
+ 2456956037,
751
+ 2730485921,
752
+ 2820302411,
753
+ 3259730800,
754
+ 3345764771,
755
+ 3516065817,
756
+ 3600352804,
757
+ 4094571909,
758
+ 275423344,
759
+ 430227734,
760
+ 506948616,
761
+ 659060556,
762
+ 883997877,
763
+ 958139571,
764
+ 1322822218,
765
+ 1537002063,
766
+ 1747873779,
767
+ 1955562222,
768
+ 2024104815,
769
+ 2227730452,
770
+ 2361852424,
771
+ 2428436474,
772
+ 2756734187,
773
+ 3204031479,
774
+ 3329325298
775
+ ]);
776
+ function rotr(x, n) {
777
+ return x >>> n | x << 32 - n;
778
+ }
779
+ function sha256(bytes) {
780
+ const h = new Uint32Array([
781
+ 1779033703,
782
+ 3144134277,
783
+ 1013904242,
784
+ 2773480762,
785
+ 1359893119,
786
+ 2600822924,
787
+ 528734635,
788
+ 1541459225
789
+ ]);
790
+ const bitLen = bytes.length * 8;
791
+ const withOne = bytes.length + 1;
792
+ const total = withOne + (56 - withOne % 64 + 64) % 64 + 8;
793
+ const msg = new Uint8Array(total);
794
+ msg.set(bytes);
795
+ msg[bytes.length] = 128;
796
+ const hi = Math.floor(bitLen / 4294967296);
797
+ const lo = bitLen >>> 0;
798
+ msg[total - 8] = hi >>> 24 & 255;
799
+ msg[total - 7] = hi >>> 16 & 255;
800
+ msg[total - 6] = hi >>> 8 & 255;
801
+ msg[total - 5] = hi & 255;
802
+ msg[total - 4] = lo >>> 24 & 255;
803
+ msg[total - 3] = lo >>> 16 & 255;
804
+ msg[total - 2] = lo >>> 8 & 255;
805
+ msg[total - 1] = lo & 255;
806
+ const w = new Uint32Array(64);
807
+ for (let off = 0; off < total; off += 64) {
808
+ for (let i = 0; i < 16; i++) {
809
+ const j = off + i * 4;
810
+ w[i] = (msg[j] << 24 | msg[j + 1] << 16 | msg[j + 2] << 8 | msg[j + 3]) >>> 0;
811
+ }
812
+ for (let i = 16; i < 64; i++) {
813
+ const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ w[i - 15] >>> 3;
814
+ const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ w[i - 2] >>> 10;
815
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
816
+ }
817
+ let a = h[0];
818
+ let b = h[1];
819
+ let c = h[2];
820
+ let d = h[3];
821
+ let e = h[4];
822
+ let f = h[5];
823
+ let g = h[6];
824
+ let hh = h[7];
825
+ for (let i = 0; i < 64; i++) {
826
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
827
+ const ch = e & f ^ ~e & g;
828
+ const t1 = hh + S1 + ch + K[i] + w[i] >>> 0;
829
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
830
+ const maj = a & b ^ a & c ^ b & c;
831
+ const t2 = S0 + maj >>> 0;
832
+ hh = g;
833
+ g = f;
834
+ f = e;
835
+ e = d + t1 >>> 0;
836
+ d = c;
837
+ c = b;
838
+ b = a;
839
+ a = t1 + t2 >>> 0;
840
+ }
841
+ h[0] = h[0] + a >>> 0;
842
+ h[1] = h[1] + b >>> 0;
843
+ h[2] = h[2] + c >>> 0;
844
+ h[3] = h[3] + d >>> 0;
845
+ h[4] = h[4] + e >>> 0;
846
+ h[5] = h[5] + f >>> 0;
847
+ h[6] = h[6] + g >>> 0;
848
+ h[7] = h[7] + hh >>> 0;
849
+ }
850
+ const out = new Uint8Array(32);
851
+ for (let i = 0; i < 8; i++) {
852
+ out[i * 4] = h[i] >>> 24 & 255;
853
+ out[i * 4 + 1] = h[i] >>> 16 & 255;
854
+ out[i * 4 + 2] = h[i] >>> 8 & 255;
855
+ out[i * 4 + 3] = h[i] & 255;
856
+ }
857
+ return out;
858
+ }
859
+ function utf8(str) {
860
+ if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(str);
861
+ const bytes = [];
862
+ for (let i = 0; i < str.length; i++) {
863
+ let c = str.charCodeAt(i);
864
+ if (c < 128) bytes.push(c);
865
+ else if (c < 2048) {
866
+ bytes.push(192 | c >> 6, 128 | c & 63);
867
+ } else if (c >= 55296 && c <= 56319) {
868
+ const c2 = str.charCodeAt(++i);
869
+ c = 65536 + ((c & 1023) << 10) + (c2 & 1023);
870
+ bytes.push(
871
+ 240 | c >> 18,
872
+ 128 | c >> 12 & 63,
873
+ 128 | c >> 6 & 63,
874
+ 128 | c & 63
875
+ );
876
+ } else {
877
+ bytes.push(
878
+ 224 | c >> 12,
879
+ 128 | c >> 6 & 63,
880
+ 128 | c & 63
881
+ );
882
+ }
883
+ }
884
+ return new Uint8Array(bytes);
885
+ }
886
+ function toHex(bytes) {
887
+ let hex = "";
888
+ for (let i = 0; i < bytes.length; i++) {
889
+ hex += bytes[i].toString(16).padStart(2, "0");
890
+ }
891
+ return hex;
892
+ }
893
+ function hmacSha256Hex(secret, message) {
894
+ const blockSize = 64;
895
+ let key = utf8(secret);
896
+ if (key.length > blockSize) key = sha256(key);
897
+ if (key.length < blockSize) {
898
+ const padded = new Uint8Array(blockSize);
899
+ padded.set(key);
900
+ key = padded;
901
+ }
902
+ const oKeyPad = new Uint8Array(blockSize);
903
+ const iKeyPad = new Uint8Array(blockSize);
904
+ for (let i = 0; i < blockSize; i++) {
905
+ oKeyPad[i] = key[i] ^ 92;
906
+ iKeyPad[i] = key[i] ^ 54;
907
+ }
908
+ const msg = utf8(message);
909
+ const inner = new Uint8Array(blockSize + msg.length);
910
+ inner.set(iKeyPad);
911
+ inner.set(msg, blockSize);
912
+ const innerHash = sha256(inner);
913
+ const outer = new Uint8Array(blockSize + innerHash.length);
914
+ outer.set(oKeyPad);
915
+ outer.set(innerHash, blockSize);
916
+ return toHex(sha256(outer));
917
+ }
918
+
919
+ // src/mockup/getMockupUrl.ts
920
+ var DEFAULT_MOCKUP_BASE = "https://img.snowcone.app";
921
+ function getMockupUrl(a, b, c) {
922
+ let productCode;
923
+ let opts;
924
+ let legacyAsset;
925
+ if (typeof b === "string") {
926
+ legacyAsset = a;
927
+ productCode = b;
928
+ opts = c ?? {};
929
+ } else {
930
+ productCode = a;
931
+ opts = b;
932
+ }
933
+ const base = (opts.base ?? DEFAULT_MOCKUP_BASE).replace(/\/+$/, "");
934
+ const assetUrl = opts.asset ?? legacyAsset;
935
+ const url = buildPublicMockupUrl(
936
+ {
937
+ productCode,
938
+ shop: opts.shop,
939
+ ...opts.design ? { design: opts.design } : {},
940
+ ...assetUrl !== void 0 ? { assetUrl } : {},
941
+ ...opts.options ? { options: opts.options } : {},
942
+ ...opts.width !== void 0 ? { width: opts.width } : {},
943
+ ...opts.aspect !== void 0 ? { aspect: opts.aspect } : {},
944
+ ...opts.mockup ?? opts.view ? { mockup: opts.mockup ?? opts.view } : {},
945
+ ...opts.variant !== void 0 ? { variant: opts.variant } : {},
946
+ ...opts.placement !== void 0 ? { placement: opts.placement } : {}
947
+ },
948
+ { baseUrl: base }
949
+ );
950
+ if (!opts.secret) return url;
951
+ const signature = hmacSha256Hex(opts.secret, canonicalForSig(url)).slice(0, 16);
952
+ return `${url}&signature=${signature}`;
953
+ }
954
+
491
955
  // src/state/optionSelection.ts
492
956
  function resolveBestCombination(selection, attributes, combinations) {
493
957
  const relevant = Object.fromEntries(
@@ -1158,7 +1622,7 @@ function resolveUrlFromDOM(relativeUrl) {
1158
1622
  const filename = pathParts[pathParts.length - 1];
1159
1623
  if (!filename) return null;
1160
1624
  const filenameWithoutExt = filename.replace(/\.[^.]+$/, "");
1161
- const images = document.querySelectorAll("img");
1625
+ const images = Array.from(document.querySelectorAll("img"));
1162
1626
  for (const img of images) {
1163
1627
  const src = img.getAttribute("src");
1164
1628
  if (!src) continue;
@@ -1170,6 +1634,9 @@ function resolveUrlFromDOM(relativeUrl) {
1170
1634
  }
1171
1635
  function normalizeImageUrl(url) {
1172
1636
  if (!url) return null;
1637
+ if (url.startsWith("blob:")) {
1638
+ return null;
1639
+ }
1173
1640
  if (url.startsWith("http://") || url.startsWith("https://")) {
1174
1641
  return url;
1175
1642
  }
@@ -1188,18 +1655,18 @@ function normalizeImageUrl(url) {
1188
1655
  const absoluteUrl = new URL(url, window.location.origin).href;
1189
1656
  if (absoluteUrl.includes("localhost") || absoluteUrl.includes("127.0.0.1")) {
1190
1657
  console.warn(
1191
- `[merchify-sdk] Artwork URL "${absoluteUrl}" uses localhost. This will work in your browser but won't work for mockup generation. Deploy your app or use publicly accessible URLs for production mockups.`
1658
+ `[snowcone-sdk] Artwork URL "${absoluteUrl}" uses localhost. This will work in your browser but won't work for mockup generation. Deploy your app or use publicly accessible URLs for production mockups.`
1192
1659
  );
1193
1660
  }
1194
1661
  return absoluteUrl;
1195
1662
  } catch (e) {
1196
- console.warn(`[merchify-sdk] Failed to normalize URL "${url}":`, e);
1663
+ console.warn(`[snowcone-sdk] Failed to normalize URL "${url}":`, e);
1197
1664
  return url;
1198
1665
  }
1199
1666
  }
1200
1667
  if (url.startsWith("/") || url.startsWith(".")) {
1201
1668
  console.warn(
1202
- `[merchify-sdk] Relative URL "${url}" detected during SSR. URL will be resolved on the client. Consider using absolute URLs for SSR compatibility.`
1669
+ `[snowcone-sdk] Relative URL "${url}" detected during SSR. URL will be resolved on the client. Consider using absolute URLs for SSR compatibility.`
1203
1670
  );
1204
1671
  }
1205
1672
  return url;
@@ -1302,95 +1769,39 @@ function filterImagePlacements(placements) {
1302
1769
  }
1303
1770
 
1304
1771
  // src/mockup/urlGenerator.ts
1305
- var cachedMockupConfig = null;
1306
- function resolveDefaultConfig() {
1307
- const DEFAULT_MOCKUP_URL = "https://MOCKUP_URL_NOT_CONFIGURED.invalid";
1308
- const DEFAULT_ACCOUNT_ID = "ACCOUNT_ID_NOT_CONFIGURED";
1309
- const winConfig = typeof window !== "undefined" && window.merchify || {};
1310
- const env = typeof process !== "undefined" ? process.env : void 0;
1311
- const envEndpoint = env?.CATALOG_API_BASE_URL;
1312
- const envImageUrl = env?.MERCHIFY_IMAGE_URL || env?.NEXT_PUBLIC_MERCH_MOCKUP_URL;
1313
- const envSignerUrl = env?.MERCHIFY_SIGNER_URL || env?.NEXT_PUBLIC_MERCH_SIGNER_URL;
1314
- const envAccountId = env?.MERCHIFY_ACCOUNT_ID || env?.NEXT_PUBLIC_MERCH_ACCOUNT_ID;
1315
- return {
1316
- endpoint: winConfig.endpoint || envEndpoint || void 0,
1317
- mockupUrl: winConfig.mockupUrl || envImageUrl || DEFAULT_MOCKUP_URL,
1318
- signerUrl: winConfig.signerUrl || envSignerUrl || void 0,
1319
- accountId: winConfig.accountId || envAccountId || DEFAULT_ACCOUNT_ID,
1320
- mode: winConfig.mode || "mock"
1321
- };
1772
+ var maxWidthCache = /* @__PURE__ */ new Map();
1773
+ function buildMockupUrl2(options, cfg) {
1774
+ const { ar, ...rest } = options;
1775
+ return buildMockupUrl(
1776
+ { ...rest, aspect: ar },
1777
+ { mockupBaseUrl: cfg.mockupBaseUrl, shop: cfg.shop }
1778
+ );
1322
1779
  }
1323
- var localhostWarningShown = false;
1324
- function config(overrides) {
1325
- if (overrides) {
1326
- const base = resolveDefaultConfig();
1327
- cachedMockupConfig = { ...base, ...overrides };
1328
- } else if (!cachedMockupConfig) {
1329
- cachedMockupConfig = resolveDefaultConfig();
1330
- }
1331
- const hostname = typeof window !== "undefined" ? window.location.hostname : "";
1332
- const isLocalDev = hostname === "localhost" || hostname === "127.0.0.1" || hostname.includes("192.168.") || hostname.endsWith(".local") || hostname.endsWith(".lvh.me") || hostname === "lvh.me";
1333
- if (!localhostWarningShown && typeof window !== "undefined" && !isLocalDev && cachedMockupConfig?.mockupUrl?.includes("localhost")) {
1334
- console.error(
1335
- `[@snowcone-app/sdk] WARNING: Using localhost mockup URL in production environment.
1336
- mockupUrl: ${cachedMockupConfig.mockupUrl}
1337
- This was likely caused by building with .env.local present.
1338
- Redeploy with "pnpm run deploy" to fix.`
1339
- );
1340
- localhostWarningShown = true;
1341
- }
1342
- return cachedMockupConfig;
1780
+ function resolveMockupBaseUrl() {
1781
+ const env = typeof process !== "undefined" ? process.env : void 0;
1782
+ const winConfig = typeof window !== "undefined" && window.snowcone || {};
1783
+ return winConfig.mockupUrl || env?.SNOWCONE_IMAGE_URL || env?.NEXT_PUBLIC_MERCH_MOCKUP_URL || "https://cdn.snowcone.app";
1343
1784
  }
1344
- var resolveMockupConfig = config;
1345
- function normalizeDesignElements(design) {
1346
- return design.map((element) => {
1347
- const normalized = {};
1348
- const keys = Object.keys(element).sort();
1349
- for (const key of keys) {
1350
- if (key === "width" || key === "height" || key === "type") {
1351
- continue;
1352
- }
1353
- if (key === "alignment" && element[key] === "center") {
1354
- continue;
1355
- }
1356
- const value = element[key];
1357
- if (value !== void 0 && value !== null) {
1358
- normalized[key] = value;
1359
- }
1360
- }
1361
- return normalized;
1362
- });
1785
+ function resolveShop() {
1786
+ const env = typeof process !== "undefined" ? process.env : void 0;
1787
+ const winConfig = typeof window !== "undefined" && window.snowcone || {};
1788
+ return winConfig.shop || env?.SNOWCONE_SHOP_ID || env?.NEXT_PUBLIC_SNOWCONE_SHOP_ID || "SHOP_NOT_CONFIGURED";
1363
1789
  }
1364
1790
  function mockupUrl(options) {
1365
- const mockupConfig = config();
1366
- const mockupBaseUrl = mockupConfig.mockupUrl || "https://MOCKUP_URL_NOT_CONFIGURED.invalid";
1367
- const accountId = mockupConfig.accountId || "ACCOUNT_ID_NOT_CONFIGURED";
1368
- const normalizedDesign = normalizeDesignElements(options.design);
1369
- const designBase64 = btoa(JSON.stringify(normalizedDesign));
1370
- const encodedDesign = encodeURIComponent(designBase64);
1371
- let queryString = `productId=${encodeURIComponent(options.productId)}`;
1372
- queryString += `&mockupId=${encodeURIComponent(options.mockupId)}`;
1373
- queryString += `&variantId=${encodeURIComponent(options.variantId)}`;
1374
- queryString += `&design=${encodedDesign}`;
1375
- queryString += `&width=${options.width}`;
1376
- queryString += `&accountId=${encodeURIComponent(accountId)}`;
1377
- if (options.effects?.grain) {
1378
- queryString += `&grain=${options.effects.grain}`;
1379
- }
1380
- if (options.ar && options.ar !== "16:9") {
1381
- queryString += `&ar=${encodeURIComponent(options.ar)}`;
1382
- }
1383
- if (options.maskOverrides && options.maskOverrides.length > 0) {
1384
- const maskOverridesBase64 = btoa(JSON.stringify(options.maskOverrides));
1385
- queryString += `&maskOverrides=${encodeURIComponent(maskOverridesBase64)}`;
1386
- }
1387
- if (mockupConfig.mode === "mock") {
1388
- const mockSignature = "bypass-sig-for-k6-load-test";
1389
- queryString += `&sig=${encodeURIComponent(mockSignature)}`;
1791
+ const mockupBaseUrl = resolveMockupBaseUrl();
1792
+ const shop = resolveShop();
1793
+ const encodedDesign = encodeURIComponent(
1794
+ btoa(JSON.stringify(normalizeDesignElements(options.design)))
1795
+ );
1796
+ let width = normalizeWidth(options.width);
1797
+ const cacheKey = `${options.productId}:${options.mockupId}:${options.variantId}:${encodedDesign}`;
1798
+ const cachedWidth = maxWidthCache.get(cacheKey);
1799
+ if (cachedWidth !== void 0 && cachedWidth >= width) {
1800
+ width = cachedWidth;
1390
1801
  } else {
1391
- console.warn("Live mode signature generation not implemented in mockupUrl");
1802
+ maxWidthCache.set(cacheKey, width);
1392
1803
  }
1393
- return `${mockupBaseUrl}/mockup?${queryString}`;
1804
+ return buildMockupUrl2({ ...options, width }, { mockupBaseUrl, shop });
1394
1805
  }
1395
1806
  function getVariant(selection, product) {
1396
1807
  const combination = findBestCombination(
@@ -1556,6 +1967,12 @@ function validateImageUrl(url) {
1556
1967
  message: "Image URL is required",
1557
1968
  value: url
1558
1969
  });
1970
+ } else if (url.startsWith("blob:")) {
1971
+ errors.push({
1972
+ field: "imageUrl",
1973
+ message: "Blob URLs (blob:...) cannot be used for mockup generation \u2014 they are local to the browser and not accessible by the rendering server. Upload the image to a public URL first, or export the canvas to a data URL.",
1974
+ value: url
1975
+ });
1559
1976
  } else if (
1560
1977
  // Allow absolute URLs
1561
1978
  !url.startsWith("http://") && !url.startsWith("https://") && // Allow data URLs
@@ -2554,11 +2971,11 @@ var UniversalContextProvider = class extends EventEmitter {
2554
2971
  config;
2555
2972
  consumers = /* @__PURE__ */ new Set();
2556
2973
  initialized = false;
2557
- constructor(config2 = {}) {
2974
+ constructor(config = {}) {
2558
2975
  super();
2559
- this.config = config2;
2976
+ this.config = config;
2560
2977
  this.contextManager = new ProductContextManager();
2561
- this.loader = new ProductLoader(this.contextManager, config2.fetcher);
2978
+ this.loader = new ProductLoader(this.contextManager, config.fetcher);
2562
2979
  this.setupSubscriptions();
2563
2980
  }
2564
2981
  /**
@@ -2676,7 +3093,7 @@ var ContextInjector = class {
2676
3093
  static forVue(provider) {
2677
3094
  return {
2678
3095
  install(app) {
2679
- app.provide("merchifyContext", provider);
3096
+ app.provide("snowconeContext", provider);
2680
3097
  },
2681
3098
  inject() {
2682
3099
  return provider;
@@ -2704,7 +3121,7 @@ var ContextInjector = class {
2704
3121
  // Attach to element
2705
3122
  attach(element) {
2706
3123
  element.__contextProvider = provider;
2707
- element.addEventListener("merchify:request-context", (event) => {
3124
+ element.addEventListener("snowcone:request-context", (event) => {
2708
3125
  event.detail.context = provider.getContext();
2709
3126
  });
2710
3127
  },
@@ -2722,8 +3139,8 @@ var ContextInjector = class {
2722
3139
  };
2723
3140
  }
2724
3141
  };
2725
- function createUniversalProvider(config2) {
2726
- return new UniversalContextProvider(config2);
3142
+ function createUniversalProvider(config) {
3143
+ return new UniversalContextProvider(config);
2727
3144
  }
2728
3145
  function withContext(Base) {
2729
3146
  return class extends Base {
@@ -3728,8 +4145,8 @@ var StandardComponents = {
3728
4145
  Product: {
3729
4146
  metadata: {
3730
4147
  name: "Product",
3731
- tagName: "merchify-product",
3732
- displayName: "MerchifyProduct",
4148
+ tagName: "snowcone-product",
4149
+ displayName: "SnowconeProduct",
3733
4150
  description: "Product context provider component",
3734
4151
  props: {
3735
4152
  productId: { type: "string" },
@@ -3755,8 +4172,8 @@ var StandardComponents = {
3755
4172
  ProductOptions: {
3756
4173
  metadata: {
3757
4174
  name: "ProductOptions",
3758
- tagName: "merchify-product-options",
3759
- displayName: "MerchifyProductOptions",
4175
+ tagName: "snowcone-product-options",
4176
+ displayName: "SnowconeProductOptions",
3760
4177
  description: "Product options selection component",
3761
4178
  props: {
3762
4179
  attributes: { type: "object" },
@@ -3776,8 +4193,8 @@ var StandardComponents = {
3776
4193
  ProductPrice: {
3777
4194
  metadata: {
3778
4195
  name: "ProductPrice",
3779
- tagName: "merchify-product-price",
3780
- displayName: "MerchifyProductPrice",
4196
+ tagName: "snowcone-product-price",
4197
+ displayName: "SnowconeProductPrice",
3781
4198
  description: "Product price display component",
3782
4199
  props: {
3783
4200
  price: { type: "number" },
@@ -3793,8 +4210,8 @@ var StandardComponents = {
3793
4210
  ProductImage: {
3794
4211
  metadata: {
3795
4212
  name: "ProductImage",
3796
- tagName: "merchify-product-image",
3797
- displayName: "MerchifyProductImage",
4213
+ tagName: "snowcone-product-image",
4214
+ displayName: "SnowconeProductImage",
3798
4215
  description: "Product mockup image component",
3799
4216
  props: {
3800
4217
  productId: { type: "string" },
@@ -3813,8 +4230,8 @@ var StandardComponents = {
3813
4230
  AddToCart: {
3814
4231
  metadata: {
3815
4232
  name: "AddToCart",
3816
- tagName: "merchify-add-to-cart",
3817
- displayName: "MerchifyAddToCart",
4233
+ tagName: "snowcone-add-to-cart",
4234
+ displayName: "SnowconeAddToCart",
3818
4235
  description: "Add to cart button component",
3819
4236
  props: {
3820
4237
  text: { type: "string", default: "Add to Cart" },
@@ -4395,7 +4812,9 @@ var RealtimeMockupService = class {
4395
4812
  logs = [];
4396
4813
  lastError = null;
4397
4814
  callbacks = {};
4815
+ lastBlobSentAt = 0;
4398
4816
  canvasBlobs = /* @__PURE__ */ new Map();
4817
+ canvasStates = /* @__PURE__ */ new Map();
4399
4818
  colors = /* @__PURE__ */ new Map();
4400
4819
  lastSendTime = {};
4401
4820
  throttleTimeouts = {};
@@ -4405,11 +4824,24 @@ var RealtimeMockupService = class {
4405
4824
  lastSentVersion = 0;
4406
4825
  // Track latest sent version per placement to detect stale responses
4407
4826
  latestSentVersionByPlacement = {};
4827
+ // Track latest accepted (displayed) version per placement — only drop results
4828
+ // older than what we've already shown, not older than what we've sent.
4829
+ // This prevents the "version racing" problem during drag where sent versions
4830
+ // advance faster than the server can render.
4831
+ latestAcceptedVersionByPlacement = {};
4408
4832
  // Feature flag: server now supports version in blob message
4409
4833
  sendVersionInBlob = true;
4834
+ // Session-grant auth: when set, connect() fetches a short-lived token, opens
4835
+ // the WS with `?token=`, and renews ~15s before expiry via a `renew` message.
4836
+ tokenProvider;
4837
+ renewTimer = null;
4410
4838
  setCallbacks(callbacks) {
4411
4839
  this.callbacks = callbacks;
4412
4840
  }
4841
+ /** Provide a grant fetcher to authorize the session (per-shop, renewable). */
4842
+ setTokenProvider(fn) {
4843
+ this.tokenProvider = fn;
4844
+ }
4413
4845
  getState() {
4414
4846
  return {
4415
4847
  isConnected: this.ws?.readyState === WebSocket.OPEN,
@@ -4432,9 +4864,24 @@ var RealtimeMockupService = class {
4432
4864
  if (this.ws?.readyState === WebSocket.OPEN) {
4433
4865
  return;
4434
4866
  }
4867
+ if (this.tokenProvider) {
4868
+ this.tokenProvider().then((grant) => {
4869
+ this.openSocket(grant.token);
4870
+ this.scheduleRenew(grant.expiresAt);
4871
+ }).catch((err) => {
4872
+ this.addLog(`Failed to obtain realtime grant: ${err}`);
4873
+ this.status = "Disconnected";
4874
+ });
4875
+ return;
4876
+ }
4877
+ this.openSocket();
4878
+ }
4879
+ openSocket(token) {
4880
+ const url = token ? `${this.wsUrl}${this.wsUrl.includes("?") ? "&" : "?"}token=${encodeURIComponent(token)}` : this.wsUrl;
4435
4881
  this.addLog(`Connecting to ${this.wsUrl}...`);
4436
- this.ws = new WebSocket(this.wsUrl);
4882
+ this.ws = new WebSocket(url);
4437
4883
  this.ws.onopen = () => {
4884
+ console.log(`[WS] connection OPENED to ${this.wsUrl}`);
4438
4885
  this.addLog("WebSocket connection opened");
4439
4886
  this.status = "Connected";
4440
4887
  };
@@ -4448,11 +4895,13 @@ var RealtimeMockupService = class {
4448
4895
  }
4449
4896
  };
4450
4897
  this.ws.onclose = (event) => {
4898
+ console.log(`[WS] connection CLOSED (code: ${event.code}, reason: "${event.reason}", wasClean: ${event.wasClean})`);
4451
4899
  this.addLog(`WebSocket connection closed (code: ${event.code})`);
4452
4900
  this.status = "Disconnected";
4453
4901
  this.sessionId = null;
4454
4902
  this.isConfigured = false;
4455
4903
  this.configSent = false;
4904
+ this.clearRenew();
4456
4905
  this.callbacks.onDisconnected?.();
4457
4906
  };
4458
4907
  this.ws.onerror = (error) => {
@@ -4460,6 +4909,29 @@ var RealtimeMockupService = class {
4460
4909
  this.status = "Disconnected";
4461
4910
  };
4462
4911
  }
4912
+ scheduleRenew(expiresAt) {
4913
+ this.clearRenew();
4914
+ if (!this.tokenProvider) return;
4915
+ const ms = Math.max(1e3, expiresAt * 1e3 - Date.now() - 15e3);
4916
+ this.renewTimer = setTimeout(() => void this.renew(), ms);
4917
+ }
4918
+ clearRenew() {
4919
+ if (this.renewTimer) {
4920
+ clearTimeout(this.renewTimer);
4921
+ this.renewTimer = null;
4922
+ }
4923
+ }
4924
+ async renew() {
4925
+ if (!this.tokenProvider || this.ws?.readyState !== WebSocket.OPEN) return;
4926
+ try {
4927
+ const grant = await this.tokenProvider();
4928
+ this.ws.send(JSON.stringify({ type: "renew", token: grant.token }));
4929
+ this.addLog("\u{1F504} Renewed realtime session token", "sent");
4930
+ this.scheduleRenew(grant.expiresAt);
4931
+ } catch (err) {
4932
+ this.addLog(`Failed to renew realtime grant: ${err}`);
4933
+ }
4934
+ }
4463
4935
  handleMessage(data) {
4464
4936
  switch (data.type) {
4465
4937
  case "connected":
@@ -4511,15 +4983,18 @@ var RealtimeMockupService = class {
4511
4983
  this.addLog("\u{1F3A8} Mockup rendering has started...");
4512
4984
  break;
4513
4985
  case "mockup_rendered":
4986
+ console.log(`[WS] mockup_rendered received: mockupId=${data.mockupId} hasImageUrl=${!!data.imageUrl} v=${data.requestVersion} placement="${data.placement}" renderMs=${data.renderMs}`);
4514
4987
  if (data.imageUrl && data.mockupId) {
4515
4988
  const responseVersion = data.requestVersion;
4516
4989
  const responsePlacement = data.placement;
4517
4990
  if (responseVersion !== void 0 && responsePlacement) {
4518
- const latestVersion = this.latestSentVersionByPlacement[responsePlacement];
4519
- if (latestVersion !== void 0 && responseVersion < latestVersion) {
4520
- this.addLog(`\u23ED\uFE0F Ignoring stale mockup v${responseVersion} for "${responsePlacement}" (latest sent: v${latestVersion})`);
4991
+ const lastAccepted = this.latestAcceptedVersionByPlacement[responsePlacement];
4992
+ if (lastAccepted !== void 0 && responseVersion < lastAccepted) {
4993
+ console.log(`[WS] STALE mockup dropped: v${responseVersion} for "${responsePlacement}" (already displayed: v${lastAccepted}, latest sent: v${this.latestSentVersionByPlacement[responsePlacement]})`);
4994
+ this.addLog(`\u23ED\uFE0F Ignoring stale mockup v${responseVersion} for "${responsePlacement}" (displayed: v${lastAccepted})`);
4521
4995
  break;
4522
4996
  }
4997
+ this.latestAcceptedVersionByPlacement[responsePlacement] = responseVersion;
4523
4998
  }
4524
4999
  const mockupResult = {
4525
5000
  mockupId: data.mockupId,
@@ -4527,7 +5002,10 @@ var RealtimeMockupService = class {
4527
5002
  renderUrl: data.renderUrl || data.imageUrl,
4528
5003
  imageSize: data.imageSize || 0,
4529
5004
  requestVersion: responseVersion,
4530
- placement: responsePlacement
5005
+ placement: responsePlacement,
5006
+ renderMs: data.renderMs,
5007
+ blobToRenderMs: data.blobToRenderMs,
5008
+ canvasRenderTiming: data.canvasRenderTiming
4531
5009
  };
4532
5010
  const existingIndex = this.mockupResults.findIndex((m) => m.mockupId === data.mockupId);
4533
5011
  if (existingIndex >= 0) {
@@ -4546,14 +5024,16 @@ var RealtimeMockupService = class {
4546
5024
  }
4547
5025
  break;
4548
5026
  case "all_mockups_rendered":
5027
+ console.log(`[WS] all_mockups_rendered received: ${data.mockups?.length ?? 0} mockups`);
4549
5028
  if (data.mockups) {
4550
5029
  const freshMockups = data.mockups.filter((mockup) => {
4551
5030
  if (mockup.requestVersion !== void 0 && mockup.placement) {
4552
- const latestVersion = this.latestSentVersionByPlacement[mockup.placement];
4553
- if (latestVersion !== void 0 && mockup.requestVersion < latestVersion) {
4554
- this.addLog(`\u23ED\uFE0F Filtering stale mockup v${mockup.requestVersion} for "${mockup.placement}" (latest: v${latestVersion})`);
5031
+ const lastAccepted = this.latestAcceptedVersionByPlacement[mockup.placement];
5032
+ if (lastAccepted !== void 0 && mockup.requestVersion < lastAccepted) {
5033
+ this.addLog(`\u23ED\uFE0F Filtering stale mockup v${mockup.requestVersion} for "${mockup.placement}" (displayed: v${lastAccepted})`);
4555
5034
  return false;
4556
5035
  }
5036
+ this.latestAcceptedVersionByPlacement[mockup.placement] = mockup.requestVersion;
4557
5037
  }
4558
5038
  return true;
4559
5039
  });
@@ -4576,31 +5056,32 @@ var RealtimeMockupService = class {
4576
5056
  }
4577
5057
  }
4578
5058
  disconnect() {
5059
+ this.clearRenew();
4579
5060
  if (this.ws) {
4580
5061
  this.ws.close();
4581
5062
  this.ws = null;
4582
5063
  }
4583
5064
  }
4584
- sendConfig(config2) {
5065
+ sendConfig(config) {
4585
5066
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
4586
5067
  this.addLog("WebSocket not connected, caching config");
4587
- this.config = config2;
5068
+ this.config = config;
4588
5069
  this.configSent = false;
4589
5070
  return false;
4590
5071
  }
4591
5072
  if (!this.sessionId) {
4592
5073
  this.addLog("WebSocket connected but no session yet, caching config");
4593
- this.config = config2;
5074
+ this.config = config;
4594
5075
  this.configSent = false;
4595
5076
  return false;
4596
5077
  }
4597
- const hasConfigChanged = !this.config || JSON.stringify(this.config) !== JSON.stringify(config2);
5078
+ const hasConfigChanged = !this.config || JSON.stringify(this.config) !== JSON.stringify(config);
4598
5079
  if (this.configSent && !hasConfigChanged) {
4599
5080
  this.addLog("Config already sent and unchanged, skipping duplicate");
4600
5081
  return false;
4601
5082
  }
4602
5083
  if (this.configSent && hasConfigChanged) {
4603
- const isOnlyMockupIdsChange = this.config && this.config.productId === config2.productId && this.config.variantId === config2.variantId;
5084
+ const isOnlyMockupIdsChange = this.config && this.config.productId === config.productId && this.config.variantId === config.variantId;
4604
5085
  if (isOnlyMockupIdsChange) {
4605
5086
  this.addLog("\u{1F504} MockupIds changed, keeping cached blobs (server will reuse)");
4606
5087
  this.isConfigured = false;
@@ -4610,6 +5091,7 @@ var RealtimeMockupService = class {
4610
5091
  this.isConfigured = false;
4611
5092
  this.mockupResults = [];
4612
5093
  this.canvasBlobs.clear();
5094
+ this.canvasStates.clear();
4613
5095
  this.colors.clear();
4614
5096
  this.lastSendTime = {};
4615
5097
  Object.values(this.throttleTimeouts).forEach((timeout) => clearTimeout(timeout));
@@ -4617,14 +5099,15 @@ var RealtimeMockupService = class {
4617
5099
  this.requestVersion = 0;
4618
5100
  this.lastSentVersion = 0;
4619
5101
  this.latestSentVersionByPlacement = {};
5102
+ this.latestAcceptedVersionByPlacement = {};
4620
5103
  this.addLog("\u{1F9F9} Cleared all cached canvas/color data for new product");
4621
5104
  }
4622
5105
  }
4623
- this.config = config2;
5106
+ this.config = config;
4624
5107
  this.configSent = true;
4625
5108
  const message = {
4626
5109
  type: "config",
4627
- config: config2
5110
+ config
4628
5111
  };
4629
5112
  const messageStr = JSON.stringify(message);
4630
5113
  this.addLog(`\u{1F4E4} Sending config: ${JSON.stringify(message, null, 2)}`, "sent");
@@ -4652,6 +5135,30 @@ var RealtimeMockupService = class {
4652
5135
  this.addLog(`\u{1F3AF} Updating mockupIds to: [${mockupIds.join(", ")}]`);
4653
5136
  return this.sendConfig(updatedConfig);
4654
5137
  }
5138
+ /**
5139
+ * Update render width without changing other config.
5140
+ * Used for low-res preview during rapid edits (e.g., 600 while dragging, 1200 on release).
5141
+ * Preserves blobs since product/variant don't change.
5142
+ */
5143
+ updateWidth(width) {
5144
+ if (!this.config) {
5145
+ this.addLog("Cannot update width: no config set");
5146
+ return false;
5147
+ }
5148
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
5149
+ this.addLog("Cannot update width: WebSocket not connected");
5150
+ return false;
5151
+ }
5152
+ if (this.config.width === width) {
5153
+ return false;
5154
+ }
5155
+ const updatedConfig = {
5156
+ ...this.config,
5157
+ width
5158
+ };
5159
+ this.addLog(`\u{1F4D0} Updating render width: ${this.config.width} \u2192 ${width}`);
5160
+ return this.sendConfig(updatedConfig);
5161
+ }
4655
5162
  /**
4656
5163
  * Update placementSettings without changing other config.
4657
5164
  * Used to override scaleMode when canvas editor is active.
@@ -4674,6 +5181,8 @@ var RealtimeMockupService = class {
4674
5181
  sendCanvasBlob(placement, blob, mockupCount = 1, baseThrottleMs = 1e3, notifyCallback = true) {
4675
5182
  this.canvasBlobs.set(placement, blob);
4676
5183
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
5184
+ const reason = !this.ws ? "no WebSocket" : this.ws.readyState !== WebSocket.OPEN ? `ws.readyState=${this.ws.readyState}` : "not configured";
5185
+ console.log(`[WS] sendCanvasBlob BLOCKED for "${placement}" (${blob.size}B): ${reason} (cached for later)`);
4677
5186
  return false;
4678
5187
  }
4679
5188
  if (baseThrottleMs <= 0) {
@@ -4681,16 +5190,25 @@ var RealtimeMockupService = class {
4681
5190
  this.lastSendTime[placement] = Date.now();
4682
5191
  return true;
4683
5192
  }
4684
- const throttleMs = baseThrottleMs * mockupCount;
4685
- const now = Date.now();
4686
- const lastSendTime = this.lastSendTime[placement] || 0;
4687
- const timeSinceLastSend = now - lastSendTime;
4688
- const hasNeverSent = lastSendTime === 0;
4689
- if (hasNeverSent || timeSinceLastSend >= throttleMs) {
4690
- this.sendBlobImmediately(placement, blob, notifyCallback);
4691
- this.lastSendTime[placement] = now;
4692
- } else if (!this.throttleTimeouts[placement]) {
4693
- const delayTime = throttleMs - timeSinceLastSend;
5193
+ const debounceMs = baseThrottleMs * mockupCount;
5194
+ const lastSendTime = this.lastSendTime[placement];
5195
+ const timeSinceLastSend = lastSendTime ? Date.now() - lastSendTime : 0;
5196
+ const isActiveInteraction = lastSendTime && timeSinceLastSend < debounceMs * 3;
5197
+ if (isActiveInteraction && timeSinceLastSend >= debounceMs) {
5198
+ if (this.throttleTimeouts[placement]) {
5199
+ clearTimeout(this.throttleTimeouts[placement]);
5200
+ delete this.throttleTimeouts[placement];
5201
+ }
5202
+ const latestBlob = this.canvasBlobs.get(placement);
5203
+ if (latestBlob && this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
5204
+ this.sendBlobImmediately(placement, latestBlob, notifyCallback);
5205
+ this.lastSendTime[placement] = Date.now();
5206
+ }
5207
+ } else {
5208
+ if (this.throttleTimeouts[placement]) {
5209
+ clearTimeout(this.throttleTimeouts[placement]);
5210
+ }
5211
+ const delayTime = isActiveInteraction ? debounceMs - timeSinceLastSend : debounceMs;
4694
5212
  this.throttleTimeouts[placement] = setTimeout(() => {
4695
5213
  const latestBlob = this.canvasBlobs.get(placement);
4696
5214
  if (latestBlob && this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
@@ -4702,6 +5220,72 @@ var RealtimeMockupService = class {
4702
5220
  }
4703
5221
  return true;
4704
5222
  }
5223
+ /**
5224
+ * Send canvas state JSON for server-side rendering.
5225
+ * Alternative to sendCanvasBlob — the server renders the PNG instead of the client.
5226
+ */
5227
+ sendCanvasState(placement, state, mockupCount = 1, baseThrottleMs = 1e3) {
5228
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
5229
+ const reason = !this.ws ? "no WebSocket" : this.ws.readyState !== WebSocket.OPEN ? `ws.readyState=${this.ws.readyState}` : "not configured";
5230
+ console.log(`[WS] sendCanvasState BLOCKED for "${placement}": ${reason}`);
5231
+ return false;
5232
+ }
5233
+ this.canvasStates.set(placement, state);
5234
+ if (baseThrottleMs <= 0) {
5235
+ this.sendCanvasStateImmediately(placement, state);
5236
+ this.lastSendTime[placement] = Date.now();
5237
+ return true;
5238
+ }
5239
+ const debounceMs = baseThrottleMs * mockupCount;
5240
+ const lastSendTime = this.lastSendTime[placement];
5241
+ const timeSinceLastSend = lastSendTime ? Date.now() - lastSendTime : 0;
5242
+ const isActiveInteraction = lastSendTime && timeSinceLastSend < debounceMs * 3;
5243
+ if (isActiveInteraction && timeSinceLastSend >= debounceMs) {
5244
+ if (this.throttleTimeouts[placement]) {
5245
+ clearTimeout(this.throttleTimeouts[placement]);
5246
+ delete this.throttleTimeouts[placement];
5247
+ }
5248
+ const latestState = this.canvasStates.get(placement);
5249
+ if (latestState && this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
5250
+ console.log(`[WS] sendCanvasState "${placement}": max-wait flush (${timeSinceLastSend}ms since last)`);
5251
+ this.sendCanvasStateImmediately(placement, latestState);
5252
+ this.lastSendTime[placement] = Date.now();
5253
+ }
5254
+ } else {
5255
+ if (this.throttleTimeouts[placement]) {
5256
+ clearTimeout(this.throttleTimeouts[placement]);
5257
+ }
5258
+ const delayTime = isActiveInteraction ? debounceMs - timeSinceLastSend : debounceMs;
5259
+ this.throttleTimeouts[placement] = setTimeout(() => {
5260
+ const latestState = this.canvasStates.get(placement);
5261
+ if (latestState && this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
5262
+ console.log(`[WS] sendCanvasState "${placement}": debounce firing (${debounceMs}ms)`);
5263
+ this.sendCanvasStateImmediately(placement, latestState);
5264
+ this.lastSendTime[placement] = Date.now();
5265
+ }
5266
+ delete this.throttleTimeouts[placement];
5267
+ }, delayTime);
5268
+ }
5269
+ return true;
5270
+ }
5271
+ sendCanvasStateImmediately(placement, state) {
5272
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
5273
+ console.log(`[WS] sendCanvasStateImmediately ABORTED: ws not open`);
5274
+ return;
5275
+ }
5276
+ this.lastSentVersion = ++this.requestVersion;
5277
+ this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
5278
+ const message = JSON.stringify({
5279
+ type: "canvas_state",
5280
+ placement,
5281
+ version: this.lastSentVersion,
5282
+ state
5283
+ });
5284
+ this.ws.send(message);
5285
+ this.lastBlobSentAt = Date.now();
5286
+ this.addLog(`Sent canvas state for "${placement}" (${(message.length / 1024).toFixed(1)}KB, v${this.lastSentVersion})`, "sent");
5287
+ this.callbacks.onBlobSent?.(placement);
5288
+ }
4705
5289
  sendBlobImmediately(placement, blob, notifyCallback = true) {
4706
5290
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
4707
5291
  this.lastSentVersion = ++this.requestVersion;
@@ -4727,6 +5311,7 @@ ${versionToSend}
4727
5311
  combined.set(imageBytes, headerBytes.length);
4728
5312
  }
4729
5313
  this.ws.send(combined.buffer);
5314
+ this.lastBlobSentAt = Date.now();
4730
5315
  this.addLog(`Sent canvas blob for placement "${placement}" (${imageBytes.length} bytes, v${versionToSend})`, "sent");
4731
5316
  if (notifyCallback) {
4732
5317
  this.callbacks.onBlobSent?.(placement);
@@ -4863,8 +5448,8 @@ ${versionToSend}
4863
5448
  };
4864
5449
 
4865
5450
  // src/index.ts
4866
- function getFetcher(config2) {
4867
- return config2?.fetcher || globalThis.fetch.bind(globalThis);
5451
+ function getFetcher(config) {
5452
+ return config?.fetcher || globalThis.fetch.bind(globalThis);
4868
5453
  }
4869
5454
  function validateProductLoose(product) {
4870
5455
  try {
@@ -4873,11 +5458,11 @@ function validateProductLoose(product) {
4873
5458
  return product;
4874
5459
  }
4875
5460
  }
4876
- async function listProducts(config2) {
4877
- const f = getFetcher(config2);
4878
- const meilisearchHost = config2?.meilisearch?.host || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_HOST || "https://ms-e5d999b2eaca-15654.sfo.meilisearch.io";
4879
- const meilisearchApiKey = config2?.meilisearch?.apiKey || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_API_KEY || "eee819b849798ad9091228c486ec05d0931e5292";
4880
- const meilisearchIndex = config2?.meilisearch?.index || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_INDEX || "merchify";
5461
+ async function listProducts(config) {
5462
+ const f = getFetcher(config);
5463
+ const meilisearchHost = config?.meilisearch?.host || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_HOST || "https://search.snowcone.app";
5464
+ const meilisearchApiKey = config?.meilisearch?.apiKey || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_API_KEY || "eee819b849798ad9091228c486ec05d0931e5292";
5465
+ const meilisearchIndex = config?.meilisearch?.index || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_INDEX || "snowcone";
4881
5466
  const headers = {
4882
5467
  "Content-Type": "application/json"
4883
5468
  };
@@ -4890,7 +5475,7 @@ async function listProducts(config2) {
4890
5475
  headers,
4891
5476
  body: JSON.stringify({
4892
5477
  q: "",
4893
- limit: config2?.limit || 1e3,
5478
+ limit: config?.limit || 1e3,
4894
5479
  filter: "mockups IS NOT EMPTY"
4895
5480
  })
4896
5481
  });
@@ -4899,11 +5484,11 @@ async function listProducts(config2) {
4899
5484
  const items = (data.hits || []).map((p) => validateProductLoose(p));
4900
5485
  return { items, total: data.estimatedTotalHits || items.length };
4901
5486
  }
4902
- async function getProduct(idOrSlug, config2) {
4903
- const f = getFetcher(config2);
4904
- const meilisearchHost = config2?.meilisearch?.host || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_HOST || "https://ms-e5d999b2eaca-15654.sfo.meilisearch.io";
4905
- const meilisearchApiKey = config2?.meilisearch?.apiKey || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_API_KEY || "eee819b849798ad9091228c486ec05d0931e5292";
4906
- const meilisearchIndex = config2?.meilisearch?.index || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_INDEX || "merchify";
5487
+ async function getProduct(idOrSlug, config) {
5488
+ const f = getFetcher(config);
5489
+ const meilisearchHost = config?.meilisearch?.host || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_HOST || "https://search.snowcone.app";
5490
+ const meilisearchApiKey = config?.meilisearch?.apiKey || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_API_KEY || "eee819b849798ad9091228c486ec05d0931e5292";
5491
+ const meilisearchIndex = config?.meilisearch?.index || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_INDEX || "snowcone";
4907
5492
  const headers = {
4908
5493
  "Content-Type": "application/json"
4909
5494
  };
@@ -4918,13 +5503,13 @@ async function getProduct(idOrSlug, config2) {
4918
5503
  const raw = await res.json();
4919
5504
  return validateProductLoose(raw);
4920
5505
  }
4921
- function createClient(config2) {
4922
- const fetcher = getFetcher(config2);
4923
- const mockupService = new MockupServiceImpl(config2.mockup || {}, fetcher);
5506
+ function createClient(config) {
5507
+ const fetcher = getFetcher(config);
5508
+ const mockupService = new MockupServiceImpl(config.mockup || {}, fetcher);
4924
5509
  return {
4925
5510
  catalog: {
4926
- listProducts: (overrides) => listProducts({ ...config2, ...overrides }),
4927
- getProduct: (idOrSlug, overrides) => getProduct(idOrSlug, { ...config2, ...overrides })
5511
+ listProducts: (overrides) => listProducts({ ...config, ...overrides }),
5512
+ getProduct: (idOrSlug, overrides) => getProduct(idOrSlug, { ...config, ...overrides })
4928
5513
  },
4929
5514
  mockup: mockupService
4930
5515
  };
@@ -4949,6 +5534,7 @@ function createClient(config2) {
4949
5534
  DEFAULT_ARTWORK_URL,
4950
5535
  DEFAULT_ASPECT_RATIO,
4951
5536
  DEFAULT_COLOR,
5537
+ DEFAULT_MOCKUP_BASE,
4952
5538
  DEFAULT_PLACEMENT_DIMENSIONS,
4953
5539
  Elements,
4954
5540
  ErrorManager,
@@ -4973,9 +5559,9 @@ function createClient(config2) {
4973
5559
  VueAdapter,
4974
5560
  adapterRegistry,
4975
5561
  autoRegister,
5562
+ buildMockupUrl,
4976
5563
  componentRegistry,
4977
5564
  computeDisabledChoices,
4978
- config,
4979
5565
  createAddToCartEvent,
4980
5566
  createAddToCartHandler,
4981
5567
  createCartDetail,
@@ -5015,6 +5601,7 @@ function createClient(config2) {
5015
5601
  getEffectiveAlignment,
5016
5602
  getIncompleteSelectionMessage,
5017
5603
  getMissingSelections,
5604
+ getMockupUrl,
5018
5605
  getOptionRenderType,
5019
5606
  getPricePreview,
5020
5607
  getProduct,
@@ -5032,7 +5619,6 @@ function createClient(config2) {
5032
5619
  prepareOptionRenderData,
5033
5620
  registerStandardComponents,
5034
5621
  resolveBestCombination,
5035
- resolveMockupConfig,
5036
5622
  resolveMockupId,
5037
5623
  resolveVariantId,
5038
5624
  retryOperation,