@snowcone-app/sdk 0.1.14 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#220](https://github.com/snowcone-app/snowcone-monorepo/pull/220) [`aa318b5`](https://github.com/snowcone-app/snowcone-monorepo/commit/aa318b51a15e977ecb993c81ae30b6afcf6864b8) Thanks [@kevinsproles](https://github.com/kevinsproles)! - Add realtime saved-design-state API: `createDesignState` (top-level export for building a design state payload) and `RenderSession.renderSavedState(placement, stateId)` (render a previously-saved design state in a realtime session). These were already in `main` and documented on developers.snowcone.app but were missing from the published `@snowcone-app/sdk@0.1.15`, so docs examples failed to compile against the npm package.
8
+
3
9
  All notable changes to the Merch SDK will be documented in this file.
4
10
 
5
11
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
@@ -8,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
14
  ## [0.1.9] - 2025-10-16
9
15
 
10
16
  ### Added
17
+
11
18
  - **DOM-Based URL Resolution** - Intelligent URL resolution for transformed assets
12
19
  - Added `resolveUrlFromDOM()` to find actual rendered URLs in the DOM
13
20
  - Automatically resolves Vercel Blob Storage URLs from v0.dev
@@ -15,18 +22,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
22
  - Perfect for environments where image URLs are transformed at runtime
16
23
 
17
24
  ### Changed
25
+
18
26
  - **Enhanced URL Normalization** - Multi-tier resolution strategy
19
27
  1. First attempts to find actual URL in DOM (for v0/Blob Storage)
20
28
  2. Falls back to `window.location.origin` conversion
21
29
  3. Warns about localhost URLs for better developer experience
22
30
 
23
31
  ### Fixed
32
+
24
33
  - Relative URLs now work correctly in v0.dev and Vercel Blob Storage environments
25
34
  - Mockup generation can access transformed image URLs with hash suffixes
26
35
 
27
36
  ## [0.1.8] - 2025-10-16
28
37
 
29
38
  ### Added
39
+
30
40
  - **Automatic URL Normalization** - Relative artwork URLs are now automatically converted to absolute URLs
31
41
  - Added `normalizeImageUrl()` utility function in `utils/url.ts`
32
42
  - Supports `/images/art.jpg`, `./art.jpg`, `../art.jpg` patterns
@@ -34,6 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
34
44
  - SSR-safe with helpful warnings
35
45
 
36
46
  ### Changed
47
+
37
48
  - **Design Generation** - All artwork URLs are normalized in `createDesignForPlacements()`
38
49
  - Handles placement designs, provided images, and default URLs
39
50
  - Single source of truth for URL normalization
@@ -42,12 +53,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
42
53
  - Clear error messages for invalid formats
43
54
 
44
55
  ### Fixed
56
+
45
57
  - Artwork URLs no longer require absolute paths - relative paths work seamlessly
46
58
  - Better developer experience matching Next.js image patterns
47
59
 
48
60
  ## [0.1.7] - 2025-10-16
49
61
 
50
62
  ### Changed
63
+
51
64
  - Version bump to maintain parity with @snowcone-app/ui 0.1.11
52
65
  - No functional changes in this release
53
66
 
package/README.md CHANGED
@@ -2,7 +2,11 @@
2
2
 
3
3
  **JavaScript/TypeScript SDK for product mockups and print-on-demand**
4
4
 
5
- Merch provides a simple SDK for fetching product data and generating real-time merchandise mockups. Use it to visualize artwork on t-shirts, posters, mugs, and more.
5
+ A small, isomorphic SDK for fetching product data and building real-time
6
+ merchandise mockup URLs. Use it to visualize artwork on t-shirts, posters, mugs,
7
+ and more — in the browser or on the server.
8
+
9
+ 📖 **Full documentation: [developers.snowcone.app/sdk](https://developers.snowcone.app/sdk)**
6
10
 
7
11
  ## Installation
8
12
 
@@ -33,9 +37,9 @@ const url = getMockupUrl('hoodie-black', {
33
37
  // <img src={url} />
34
38
  ```
35
39
 
36
- > The `key` is your **publishable** token — public and safe to expose (like
37
- > Cloudinary's cloud name). It defaults to your `shop.id`. See the security
38
- > ladder below if you want to lock things down.
40
+ > The `shop` ID is your **publishable** token — public and safe to expose (like
41
+ > Cloudinary's cloud name). It defaults to your `shop.id`. See [Public or
42
+ > signed](#public-or-signed) below if you want to lock things down.
39
43
 
40
44
  ## Core Functions
41
45
 
@@ -94,7 +98,7 @@ const url = getMockupUrl(productCode: string, opts: {
94
98
  asset?: string; // Single default-placement image (get-started shorthand)
95
99
  design?: Design; // Multi-placement: { [placementKey]: Fill } — takes precedence over `asset`
96
100
  options?: Record<string, string>; // Variant picks: { size: "m" } → opt.size=m
97
- secret?: string; // Optional: per-shop secret → appends an L3 &signature (server only)
101
+ secret?: string; // Optional: per-shop secret → appends a signed &signature (server only)
98
102
  base?: string; // Optional: override the host (default img.snowcone.app)
99
103
  width?: number; // Optional: display width
100
104
  view?: string; // Optional: camera view / mockup scene (alias: `mockup`)
@@ -140,18 +144,16 @@ const cap = getMockupUrl('RQNU68', {
140
144
  > accepted (it builds the same URL as `getMockupUrl(productCode, { asset, … })`).
141
145
  > Prefer the code-first form above for new code.
142
146
 
143
- ### Security ladder
147
+ ### Public or signed
144
148
 
145
- The default is open and frictionless; climb only as far as you need:
149
+ The default is open and frictionless. There's one decision to make up front:
146
150
 
147
- | Level | Control | Effect |
148
- |---|---|---|
149
- | **L0** (default) | none | public token, any domain, any asset |
150
- | **L1** | domain allowlist | token only renders from your domains |
151
- | **L2** | asset allowlist | restrict which asset sources composite |
152
- | **L3** | signed URLs | URL must carry a valid `&s` (HMAC of a per-shop secret) |
151
+ - **Public** the Shop ID alone, safe to expose. Lock it to your **asset
152
+ origins** so a stolen ID can only re-render your own catalog.
153
+ - **Signed** your server appends an `&signature` (HMAC of a per-shop secret),
154
+ so only you can mint valid URLs. The secret stays server-side.
153
155
 
154
- For **L3**, pass `secret` (server-side only) and `getMockupUrl` appends the
156
+ To sign, pass `secret` (server-side only) and `getMockupUrl` appends the
155
157
  verified `&signature`:
156
158
 
157
159
  ```typescript
@@ -163,25 +165,62 @@ const url = getMockupUrl('hoodie-black', {
163
165
  });
164
166
  ```
165
167
 
166
- ### React Hooks (Optional)
168
+ See [Public or signed](https://developers.snowcone.app/get-started) for the full picture.
169
+
170
+ ### Realtime: render a saved canvas, no pixel upload
167
171
 
168
- For React applications, use the React-specific exports:
172
+ `getMockupUrl` composites **one** artwork onto a product. For a full multi-layer
173
+ design — the kind the Snowcone canvas editor produces — use a **`RenderSession`**:
174
+ the browser sends a ~1 KB description of the canvas (or just a saved design's id)
175
+ over a WebSocket, and the server rasterizes it and **fetches the referenced assets
176
+ itself**. No per-placement PNG is ever uploaded, so it's fast on thin/mobile
177
+ clients. This is exactly how the Snowcone PDP renders mockups (see ADR-0079).
178
+
179
+ Authorize the session with a short-lived **grant**. Mint it server-side with a
180
+ secret API key (`sk_…`, scope `mockups:realtime`) so the key never reaches the
181
+ browser. Create the key — and find your publishable `shop.id` — on the
182
+ [API keys page](https://snowcone.app/api-keys):
169
183
 
170
184
  ```typescript
171
- import { useRealtimeMockup } from '@snowcone-app/sdk/react';
172
-
173
- function MyComponent() {
174
- const { mockupUrl, isGenerating } = useRealtimeMockup({
175
- productId: 'BEEB77',
176
- artworkUrl: artwork.src,
177
- placement: 'Front',
178
- alignment: 'center'
185
+ // YOUR backend route, e.g. POST /api/realtime/grant
186
+ import { mintRealtimeGrant } from '@snowcone-app/sdk';
187
+
188
+ export async function POST(req: Request) {
189
+ const { shop } = await req.json();
190
+ const grant = await mintRealtimeGrant({
191
+ apiKey: process.env.SNOWCONE_API_KEY!, // secret — server only
192
+ shop, // = shop.id (publishable)
179
193
  });
180
-
181
- return <img src={mockupUrl} alt="Product mockup" />;
194
+ return Response.json(grant); // { token, expiresAt }
182
195
  }
183
196
  ```
184
197
 
198
+ In the browser, point a `RenderSession` at that proxy and render. Render a **saved**
199
+ design by id (no canvas JSON needed) or a live `serializeStateForServer(...)` state:
200
+
201
+ ```typescript
202
+ import { RenderSession } from '@snowcone-app/sdk';
203
+
204
+ const session = new RenderSession({
205
+ shop: 'YOUR_SHOP_ID', // publishable, like Stripe pk_
206
+ grantUrl: '/api/realtime/grant', // your proxy above
207
+ product: { productId: 'BEEB77', mockupIds: ['Front'] },
208
+ });
209
+
210
+ session.onMockups((results) => { img.src = results[0].imageUrl; });
211
+
212
+ // (a) render a SAVED canvas by id — ideal for fulfillment / agents
213
+ await session.renderSavedState('Front', 'design-state-id');
214
+
215
+ // (b) or render a live canvas state from @snowcone-app/canvas
216
+ // import { serializeStateForServer } from '@snowcone-app/canvas';
217
+ // await session.renderState('Front', serializeStateForServer(canvasState));
218
+ ```
219
+
220
+ React apps can use the lower-level `useRealtimeMockup({ getToken })` hook from
221
+ `@snowcone-app/sdk/react` instead. Full walkthrough:
222
+ [Realtime server-side render](https://developers.snowcone.app/realtime).
223
+
185
224
  ## TypeScript Support
186
225
 
187
226
  Full TypeScript definitions are included. Import types:
@@ -211,14 +250,14 @@ See: [@snowcone-app/ui on npm](https://www.npmjs.com/package/@snowcone-app/ui)
211
250
 
212
251
  ## Examples
213
252
 
214
- - [Next.js Example](https://github.com/snowcone-app/ui-components/tree/main/examples/next-ecommerce)
215
- - [Basic Product Display](https://github.com/snowcone-app/ui-components/tree/main/examples/next-ecommerce/app/demos)
253
+ - [Getting started](https://developers.snowcone.app/get-started)
254
+ - [Mockup URLs reference](https://developers.snowcone.app/mockups)
255
+ - [Live playground](https://developers.snowcone.app/playground)
216
256
 
217
257
  ## Support
218
258
 
219
- - **Issues:** [GitHub Issues](https://github.com/snowcone-app/ui-components/issues)
220
- - **Discussions:** [GitHub Discussions](https://github.com/snowcone-app/ui-components/discussions)
221
- - **Docs:** [developers.snowcone.app](https://developers.snowcone.app)
259
+ - **Docs:** [developers.snowcone.app/sdk](https://developers.snowcone.app/sdk)
260
+ - **Issues:** [GitHub Issues](https://github.com/snowcone-app/snowcone-monorepo/issues)
222
261
 
223
262
  ## License
224
263
 
@@ -1,6 +1,9 @@
1
+ // src/realtime/constants.ts
2
+ var REALTIME_WS_URL = "wss://cdn.snowcone.app/realtime";
3
+
1
4
  // src/realtime/websocket.ts
2
5
  var RealtimeMockupService = class {
3
- constructor(wsUrl = "wss://WS_URL_NOT_CONFIGURED.invalid/realtime") {
6
+ constructor(wsUrl = REALTIME_WS_URL) {
4
7
  this.wsUrl = wsUrl;
5
8
  }
6
9
  ws = null;
@@ -469,6 +472,32 @@ var RealtimeMockupService = class {
469
472
  }
470
473
  return true;
471
474
  }
475
+ /**
476
+ * Render a SAVED canvas by reference (ADR-0079 Phase 4). Sends a `stateId`
477
+ * instead of inline state; the server resolves it, fetches the referenced
478
+ * assets, and renders — nothing is uploaded and the client need not hold the
479
+ * canvas JSON. One-shot (no throttle). Results arrive like any other render.
480
+ */
481
+ sendCanvasStateRef(placement, stateId) {
482
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
483
+ const reason = !this.ws ? "no WebSocket" : this.ws.readyState !== WebSocket.OPEN ? `ws.readyState=${this.ws.readyState}` : "not configured";
484
+ console.log(`[WS] sendCanvasStateRef BLOCKED for "${placement}": ${reason}`);
485
+ return false;
486
+ }
487
+ this.lastSentVersion = ++this.requestVersion;
488
+ this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
489
+ const message = JSON.stringify({
490
+ type: "canvas_state",
491
+ placement,
492
+ version: this.lastSentVersion,
493
+ stateId
494
+ });
495
+ this.ws.send(message);
496
+ this.lastBlobSentAt = Date.now();
497
+ this.addLog(`Sent canvas stateId "${stateId}" for "${placement}" (v${this.lastSentVersion})`, "sent");
498
+ this.callbacks.onBlobSent?.(placement);
499
+ return true;
500
+ }
472
501
  sendCanvasStateImmediately(placement, state) {
473
502
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
474
503
  console.log(`[WS] sendCanvasStateImmediately ABORTED: ws not open`);
@@ -649,5 +678,6 @@ ${versionToSend}
649
678
  };
650
679
 
651
680
  export {
681
+ REALTIME_WS_URL,
652
682
  RealtimeMockupService
653
683
  };
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_GRANT_BASE: () => DEFAULT_GRANT_BASE,
41
42
  DEFAULT_MOCKUP_BASE: () => DEFAULT_MOCKUP_BASE,
42
43
  DEFAULT_PLACEMENT_DIMENSIONS: () => DEFAULT_PLACEMENT_DIMENSIONS,
43
44
  Elements: () => Elements,
@@ -51,7 +52,9 @@ __export(index_exports, {
51
52
  ProductLoader: () => ProductLoader,
52
53
  ProductProps: () => ProductProps,
53
54
  PropertyManager: () => PropertyManager,
55
+ REALTIME_WS_URL: () => REALTIME_WS_URL,
54
56
  RealtimeMockupService: () => RealtimeMockupService,
57
+ RenderSession: () => RenderSession,
55
58
  StandardComponents: () => StandardComponents,
56
59
  StandardEvents: () => StandardEvents,
57
60
  StateManager: () => StateManager,
@@ -73,6 +76,7 @@ __export(index_exports, {
73
76
  createComponent: () => createComponent,
74
77
  createContextProvider: () => createContextProvider,
75
78
  createDesignForPlacements: () => createDesignForPlacements,
79
+ createDesignState: () => createDesignState,
76
80
  createDevFetcher: () => createDevFetcher,
77
81
  createErrorHandler: () => createErrorHandler,
78
82
  createEventManager: () => createEventManager,
@@ -95,6 +99,7 @@ __export(index_exports, {
95
99
  describeProductPrice: () => describeProductPrice,
96
100
  describeProductTitle: () => describeProductTitle,
97
101
  extractProductId: () => extractProductId,
102
+ fetchRealtimeGrant: () => fetchRealtimeGrant,
98
103
  filterImagePlacements: () => filterImagePlacements,
99
104
  findBestCombination: () => findBestCombination,
100
105
  findClosestSnapPoint: () => findClosestSnapPoint,
@@ -118,6 +123,7 @@ __export(index_exports, {
118
123
  isValidAlignment: () => isValidAlignment,
119
124
  isValidTileCount: () => isValidTileCount,
120
125
  listProducts: () => listProducts,
126
+ mintRealtimeGrant: () => mintRealtimeGrant,
121
127
  mockupUrl: () => mockupUrl,
122
128
  normalizeChoice: () => normalizeChoice,
123
129
  prepareOptionRenderData: () => prepareOptionRenderData,
@@ -4797,9 +4803,12 @@ function createSvelteComponent(descriptor, svelte) {
4797
4803
  };
4798
4804
  }
4799
4805
 
4806
+ // src/realtime/constants.ts
4807
+ var REALTIME_WS_URL = "wss://cdn.snowcone.app/realtime";
4808
+
4800
4809
  // src/realtime/websocket.ts
4801
4810
  var RealtimeMockupService = class {
4802
- constructor(wsUrl = "wss://WS_URL_NOT_CONFIGURED.invalid/realtime") {
4811
+ constructor(wsUrl = REALTIME_WS_URL) {
4803
4812
  this.wsUrl = wsUrl;
4804
4813
  }
4805
4814
  ws = null;
@@ -5268,6 +5277,32 @@ var RealtimeMockupService = class {
5268
5277
  }
5269
5278
  return true;
5270
5279
  }
5280
+ /**
5281
+ * Render a SAVED canvas by reference (ADR-0079 Phase 4). Sends a `stateId`
5282
+ * instead of inline state; the server resolves it, fetches the referenced
5283
+ * assets, and renders — nothing is uploaded and the client need not hold the
5284
+ * canvas JSON. One-shot (no throttle). Results arrive like any other render.
5285
+ */
5286
+ sendCanvasStateRef(placement, stateId) {
5287
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
5288
+ const reason = !this.ws ? "no WebSocket" : this.ws.readyState !== WebSocket.OPEN ? `ws.readyState=${this.ws.readyState}` : "not configured";
5289
+ console.log(`[WS] sendCanvasStateRef BLOCKED for "${placement}": ${reason}`);
5290
+ return false;
5291
+ }
5292
+ this.lastSentVersion = ++this.requestVersion;
5293
+ this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
5294
+ const message = JSON.stringify({
5295
+ type: "canvas_state",
5296
+ placement,
5297
+ version: this.lastSentVersion,
5298
+ stateId
5299
+ });
5300
+ this.ws.send(message);
5301
+ this.lastBlobSentAt = Date.now();
5302
+ this.addLog(`Sent canvas stateId "${stateId}" for "${placement}" (v${this.lastSentVersion})`, "sent");
5303
+ this.callbacks.onBlobSent?.(placement);
5304
+ return true;
5305
+ }
5271
5306
  sendCanvasStateImmediately(placement, state) {
5272
5307
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
5273
5308
  console.log(`[WS] sendCanvasStateImmediately ABORTED: ws not open`);
@@ -5447,6 +5482,198 @@ ${versionToSend}
5447
5482
  }
5448
5483
  };
5449
5484
 
5485
+ // src/realtime/grant.ts
5486
+ var DEFAULT_GRANT_BASE = "https://api.snowcone.app";
5487
+ async function mintRealtimeGrant(opts) {
5488
+ const f = opts.fetch ?? globalThis.fetch;
5489
+ const res = await f(`${opts.base ?? DEFAULT_GRANT_BASE}/realtime/grant`, {
5490
+ method: "POST",
5491
+ headers: {
5492
+ "Content-Type": "application/json",
5493
+ Authorization: `Bearer ${opts.apiKey}`
5494
+ },
5495
+ body: JSON.stringify({ shop: opts.shop })
5496
+ });
5497
+ if (!res.ok) {
5498
+ const detail = await res.text().catch(() => "");
5499
+ throw new Error(`realtime grant failed: ${res.status}${detail ? ` ${detail}` : ""}`);
5500
+ }
5501
+ return await res.json();
5502
+ }
5503
+ async function fetchRealtimeGrant(grantUrl, shop, fetchImpl) {
5504
+ const f = fetchImpl ?? globalThis.fetch;
5505
+ const res = await f(grantUrl, {
5506
+ method: "POST",
5507
+ headers: { "Content-Type": "application/json" },
5508
+ body: JSON.stringify({ shop })
5509
+ });
5510
+ if (!res.ok) throw new Error(`realtime grant failed: ${res.status}`);
5511
+ return await res.json();
5512
+ }
5513
+
5514
+ // src/realtime/session.ts
5515
+ var RenderSession = class {
5516
+ svc;
5517
+ opts;
5518
+ product;
5519
+ mockupCb = null;
5520
+ errorCb = null;
5521
+ ready = null;
5522
+ constructor(opts) {
5523
+ if (!opts.shop) throw new Error("RenderSession: `shop` is required");
5524
+ if (!opts.getToken && !opts.grantUrl) {
5525
+ throw new Error("RenderSession: provide `grantUrl` or `getToken` to authorize the session");
5526
+ }
5527
+ this.opts = opts;
5528
+ this.product = opts.product ?? null;
5529
+ this.svc = new RealtimeMockupService(opts.wsUrl ?? REALTIME_WS_URL);
5530
+ const getToken = opts.getToken ?? (() => fetchRealtimeGrant(opts.grantUrl, opts.shop, opts.fetch));
5531
+ this.svc.setTokenProvider(getToken);
5532
+ }
5533
+ /** Register a callback fired whenever rendered mockup URLs update. */
5534
+ onMockups(cb) {
5535
+ this.mockupCb = cb;
5536
+ return this;
5537
+ }
5538
+ /** Register an error callback (server/transport errors). */
5539
+ onError(cb) {
5540
+ this.errorCb = cb;
5541
+ return this;
5542
+ }
5543
+ /** Set or update the product. Sends config immediately if already connected. */
5544
+ setProduct(product) {
5545
+ this.product = product;
5546
+ this.svc.sendConfig(this.toConfig(product));
5547
+ }
5548
+ /**
5549
+ * Connect, authorize, and configure. Resolves once the session is ready to
5550
+ * accept {@link RenderSession.renderState}. Idempotent.
5551
+ */
5552
+ connect() {
5553
+ if (this.ready) return this.ready;
5554
+ if (!this.product) {
5555
+ return Promise.reject(
5556
+ new Error("RenderSession: set a product (via options.product or setProduct) before connect()")
5557
+ );
5558
+ }
5559
+ const product = this.product;
5560
+ this.ready = new Promise((resolve, reject) => {
5561
+ let settled = false;
5562
+ this.svc.setCallbacks({
5563
+ onConnected: () => {
5564
+ this.svc.sendConfig(this.toConfig(product));
5565
+ },
5566
+ onConfigReceived: () => {
5567
+ if (!settled) {
5568
+ settled = true;
5569
+ resolve();
5570
+ }
5571
+ },
5572
+ onMockupRendered: () => {
5573
+ this.mockupCb?.(this.svc.getState().mockupResults);
5574
+ },
5575
+ onAllMockupsRendered: (results) => {
5576
+ this.mockupCb?.(results);
5577
+ },
5578
+ onError: (error) => {
5579
+ this.errorCb?.(error);
5580
+ if (!settled) {
5581
+ settled = true;
5582
+ reject(new Error(error));
5583
+ }
5584
+ }
5585
+ });
5586
+ this.svc.sendConfig(this.toConfig(product));
5587
+ this.svc.connect();
5588
+ });
5589
+ return this.ready;
5590
+ }
5591
+ /**
5592
+ * Render a canvas state for a placement. The server rasterizes it and fetches
5593
+ * the assets referenced inside — no pixels are uploaded. Results arrive via
5594
+ * {@link RenderSession.onMockups}. Safe to call repeatedly (e.g. on every
5595
+ * canvas edit); pass `throttleMs` to debounce during live dragging.
5596
+ *
5597
+ * `placement` is the print-area NAME (e.g. `'Front'`, the product's
5598
+ * `placements[].label`) — a different id from the product's `mockupIds`, and it
5599
+ * must match an artboard in `state`. The server renders only once it has a state
5600
+ * for EVERY required placement, so on a multi-placement product call this once
5601
+ * per placement; a missing one surfaces as an `incomplete_canvas_placements`
5602
+ * error on {@link RenderSession.onError}.
5603
+ *
5604
+ * `state` is the flattened `serializeStateForServer()` shape — NOT the
5605
+ * un-flattened shape `createDesignState` takes.
5606
+ */
5607
+ async renderState(placement, state, throttleMs = 0) {
5608
+ await this.connect();
5609
+ this.svc.sendCanvasState(placement, state, this.product?.mockupIds.length ?? 1, throttleMs);
5610
+ }
5611
+ /**
5612
+ * Render a SAVED canvas by reference (ADR-0079 Phase 4). The server resolves
5613
+ * the `stateId`, fetches the referenced assets, and renders — no pixels are
5614
+ * uploaded and you don't need to hold the canvas JSON. Ideal for re-rendering
5615
+ * a stored design (fulfillment, agents, server-to-server). Results arrive via
5616
+ * {@link RenderSession.onMockups}.
5617
+ */
5618
+ async renderSavedState(placement, stateId) {
5619
+ await this.connect();
5620
+ this.svc.sendCanvasStateRef(placement, stateId);
5621
+ }
5622
+ /** Update only the mockup ids to render (reuses the current state). */
5623
+ updateMockupIds(mockupIds) {
5624
+ if (this.product) this.product = { ...this.product, mockupIds };
5625
+ this.svc.updateMockupIds(mockupIds);
5626
+ }
5627
+ /** Current rendered results. */
5628
+ getMockups() {
5629
+ return this.svc.getState().mockupResults;
5630
+ }
5631
+ /** Close the WebSocket and stop auto-renew. */
5632
+ close() {
5633
+ this.svc.disconnect();
5634
+ this.ready = null;
5635
+ }
5636
+ /** Escape hatch: the underlying low-level service. */
5637
+ get service() {
5638
+ return this.svc;
5639
+ }
5640
+ toConfig(product) {
5641
+ return {
5642
+ productId: product.productId,
5643
+ mockupIds: product.mockupIds,
5644
+ // shop comes from the grant token server-side; sent for completeness.
5645
+ shop: this.opts.shop,
5646
+ // WebSocketConfig requires these; the server treats empty variant as "none".
5647
+ variantId: product.variantId ?? "",
5648
+ width: product.width ?? 1e3,
5649
+ ...product.placementSettings ? { placementSettings: product.placementSettings } : {}
5650
+ };
5651
+ }
5652
+ };
5653
+
5654
+ // src/realtime/design-state.ts
5655
+ var TRANSPARENT_1X1_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
5656
+ async function createDesignState(input) {
5657
+ const f = input.fetch ?? globalThis.fetch;
5658
+ const res = await f(`${input.base ?? DEFAULT_GRANT_BASE}/snowcone/design-states`, {
5659
+ method: "POST",
5660
+ headers: { "Content-Type": "application/json" },
5661
+ body: JSON.stringify({
5662
+ productId: input.productId,
5663
+ stateJson: JSON.stringify(input.state),
5664
+ previewBase64: input.previewBase64 ?? TRANSPARENT_1X1_PNG
5665
+ })
5666
+ });
5667
+ if (!res.ok) {
5668
+ const detail = await res.text().catch(() => "");
5669
+ throw new Error(`createDesignState failed: ${res.status}${detail ? ` ${detail}` : ""}`);
5670
+ }
5671
+ const body = await res.json();
5672
+ const ds = body?.designState;
5673
+ if (!ds?.id) throw new Error("createDesignState: response missing designState.id");
5674
+ return { stateId: ds.id, stateUrl: ds.stateUrl ?? "", previewUrl: ds.previewUrl ?? "" };
5675
+ }
5676
+
5450
5677
  // src/index.ts
5451
5678
  function getFetcher(config) {
5452
5679
  return config?.fetcher || globalThis.fetch.bind(globalThis);
@@ -5534,6 +5761,7 @@ function createClient(config) {
5534
5761
  DEFAULT_ARTWORK_URL,
5535
5762
  DEFAULT_ASPECT_RATIO,
5536
5763
  DEFAULT_COLOR,
5764
+ DEFAULT_GRANT_BASE,
5537
5765
  DEFAULT_MOCKUP_BASE,
5538
5766
  DEFAULT_PLACEMENT_DIMENSIONS,
5539
5767
  Elements,
@@ -5547,7 +5775,9 @@ function createClient(config) {
5547
5775
  ProductLoader,
5548
5776
  ProductProps,
5549
5777
  PropertyManager,
5778
+ REALTIME_WS_URL,
5550
5779
  RealtimeMockupService,
5780
+ RenderSession,
5551
5781
  StandardComponents,
5552
5782
  StandardEvents,
5553
5783
  StateManager,
@@ -5569,6 +5799,7 @@ function createClient(config) {
5569
5799
  createComponent,
5570
5800
  createContextProvider,
5571
5801
  createDesignForPlacements,
5802
+ createDesignState,
5572
5803
  createDevFetcher,
5573
5804
  createErrorHandler,
5574
5805
  createEventManager,
@@ -5591,6 +5822,7 @@ function createClient(config) {
5591
5822
  describeProductPrice,
5592
5823
  describeProductTitle,
5593
5824
  extractProductId,
5825
+ fetchRealtimeGrant,
5594
5826
  filterImagePlacements,
5595
5827
  findBestCombination,
5596
5828
  findClosestSnapPoint,
@@ -5614,6 +5846,7 @@ function createClient(config) {
5614
5846
  isValidAlignment,
5615
5847
  isValidTileCount,
5616
5848
  listProducts,
5849
+ mintRealtimeGrant,
5617
5850
  mockupUrl,
5618
5851
  normalizeChoice,
5619
5852
  prepareOptionRenderData,