@proveanything/smartlinks 1.2.3 → 1.3.1

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/README.md CHANGED
@@ -80,6 +80,81 @@ const jwt = await auth.requestAdminJWT('collectionId')
80
80
  const publicJwt = await auth.requestPublicJWT('collectionId', 'productId', 'proofId')
81
81
  ```
82
82
 
83
+ ## Error Handling
84
+
85
+ The SDK throws `SmartlinksApiError` for all API errors, providing structured access to:
86
+ - **HTTP status code** (`statusCode` / `code`) - Numeric HTTP status (400, 401, 500, etc.)
87
+ - **Server error code** (`errorCode`) - String identifier ("NOT_AUTHORIZED", "broadcasts.topic.invalid", etc.)
88
+ - **Error message** (`message`) - Human-readable description
89
+ - **Additional details** (`details`) - Server-specific fields
90
+
91
+ ### Automatic Error Normalization
92
+
93
+ The SDK automatically normalizes various server error response formats into a consistent structure:
94
+
95
+ ```ts
96
+ // Server may return errors in different formats:
97
+ // { errorText: "...", errorCode: "..." }
98
+ // { error: "...", message: "..." }
99
+ // { ok: false, error: "..." }
100
+ // { error: "..." }
101
+
102
+ // All are normalized to SmartlinksApiError with consistent access:
103
+ import { SmartlinksApiError, product } from '@proveanything/smartlinks'
104
+
105
+ try {
106
+ const item = await product.get('collectionId', 'productId', false)
107
+ } catch (error) {
108
+ if (error instanceof SmartlinksApiError) {
109
+ console.error({
110
+ message: error.message, // "Error 404: Product not found"
111
+ statusCode: error.statusCode, // 404 (HTTP status)
112
+ code: error.code, // 404 (same as statusCode)
113
+ errorCode: error.errorCode, // "NOT_FOUND" (server error code string)
114
+ details: error.details, // Additional server details
115
+ url: error.url, // Failed URL
116
+ })
117
+
118
+ // Handle specific server error codes (primary identifier)
119
+ switch (error.errorCode) {
120
+ case 'NOT_AUTHORIZED':
121
+ // Invalid credentials
122
+ break
123
+ case 'broadcasts.topic.invalid':
124
+ // Invalid broadcast topic
125
+ break
126
+ default:
127
+ // Fall back to HTTP status code
128
+ if (error.isNotFound()) {
129
+ // Handle 404
130
+ } else if (error.isAuthError()) {
131
+ // Handle 401/403 - redirect to login
132
+ }
133
+ }
134
+
135
+ // Use helper methods for HTTP status-based handling
136
+ if (error.isRateLimited()) {
137
+ // Handle 429 - implement retry logic
138
+ } else if (error.isServerError()) {
139
+ // Handle 5xx - show maintenance message
140
+ }
141
+ }
142
+ }
143
+ ```
144
+
145
+ ### Helper Methods
146
+
147
+ - `error.isClientError()` - 4xx status codes
148
+ - `error.isServerError()` - 5xx status codes
149
+ - `error.isAuthError()` - 401 or 403
150
+ - `error.isNotFound()` - 404
151
+ - `error.isRateLimited()` - 429
152
+ - `error.toJSON()` - Serializable object for logging
153
+
154
+ For comprehensive error handling examples and migration guidance, see:
155
+ - [examples/error-handling-demo.ts](examples/error-handling-demo.ts) - Complete error handling patterns
156
+ - [docs/ERROR_HANDLING_MIGRATION.md](docs/ERROR_HANDLING_MIGRATION.md) - Migration guide from old error handling
157
+
83
158
  ## Common tasks
84
159
 
85
160
  ### Products
package/dist/README.md ADDED
@@ -0,0 +1,569 @@
1
+ # @proveanything/smartlinks
2
+
3
+ Official JavaScript/TypeScript SDK for the Smartlinks API.
4
+
5
+ Build Smartlinks-powered apps in Node.js or the browser: list collections and products, authenticate users, manage assets and attestations, and call admin endpoints with a clean, typed API.
6
+
7
+ • TypeScript-first types and intellisense
8
+ • Works in Node.js and modern browsers
9
+ • Simple auth helpers (login, verify token, request admin/public JWTs)
10
+ • Rich resources: collections, products, proofs, assets, attestations, batches, variants, AI, and more
11
+ • Optional iframe proxy mode for embedded apps
12
+
13
+ For the full list of functions and types, see the API summary:
14
+ → API Summary (API_SUMMARY.md)
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install @proveanything/smartlinks
20
+ ```
21
+
22
+ ## Quick start
23
+
24
+ Initialize once at app startup with your API base URL. Trailing slashes are optional (normalized). In Node, you can also provide an API key for server-to-server calls. You can enable an ngrok header or supply custom headers.
25
+
26
+ ```ts
27
+ import { initializeApi } from '@proveanything/smartlinks'
28
+
29
+ initializeApi({
30
+ baseURL: 'https://smartlinks.app/api/v1', // or 'https://smartlinks.app/api/v1/'
31
+ // logger: console, // optional: verbose logging of requests/responses and proxy messages
32
+ // apiKey: process.env.SMARTLINKS_API_KEY, // Node/server only (optional)
33
+ // ngrokSkipBrowserWarning: true, // adds 'ngrok-skip-browser-warning: true'
34
+ // extraHeaders: { 'X-Debug': '1' } // merged into every request
35
+ })
36
+ ```
37
+
38
+ List public collections and fetch products:
39
+
40
+ ```ts
41
+ import { collection, product } from '@proveanything/smartlinks'
42
+
43
+ const collections = await collection.list(false) // public endpoint
44
+ const first = collections[0]
45
+ if (first) {
46
+ const products = await product.list(first.id, false) // public endpoint
47
+ console.log('First product:', products[0])
48
+ }
49
+ ```
50
+
51
+ ## Authentication
52
+
53
+ Use the built-in helpers to log in and verify tokens. After a successful login, the SDK stores the bearer token for subsequent calls.
54
+
55
+ ```ts
56
+ import { initializeApi } from '@proveanything/smartlinks'
57
+ import { auth } from '@proveanything/smartlinks'
58
+
59
+ initializeApi({ baseURL: 'https://smartlinks.app/api/v1/' }) // trailing slash OK
60
+
61
+ // Email + password login (browser or Node)
62
+ const user = await auth.login('user@example.com', 'password')
63
+ console.log('Hello,', user.name)
64
+
65
+ // Verify an existing token (and set it if valid)
66
+ const verified = await auth.verifyToken(user.bearerToken)
67
+ console.log('Token valid?', verified.valid)
68
+
69
+ // Later, clear token
70
+ auth.logout()
71
+ ```
72
+
73
+ Admin flows:
74
+
75
+ ```ts
76
+ // Request an admin JWT for a collection (requires prior auth)
77
+ const jwt = await auth.requestAdminJWT('collectionId')
78
+
79
+ // Or request a public JWT authorized for a collection/product/proof
80
+ const publicJwt = await auth.requestPublicJWT('collectionId', 'productId', 'proofId')
81
+ ```
82
+
83
+ ## Error Handling
84
+
85
+ The SDK throws `SmartlinksApiError` for all API errors, providing structured access to:
86
+ - **HTTP status code** (`statusCode` / `code`) - Numeric HTTP status (400, 401, 500, etc.)
87
+ - **Server error code** (`errorCode`) - String identifier ("NOT_AUTHORIZED", "broadcasts.topic.invalid", etc.)
88
+ - **Error message** (`message`) - Human-readable description
89
+ - **Additional details** (`details`) - Server-specific fields
90
+
91
+ ### Automatic Error Normalization
92
+
93
+ The SDK automatically normalizes various server error response formats into a consistent structure:
94
+
95
+ ```ts
96
+ // Server may return errors in different formats:
97
+ // { errorText: "...", errorCode: "..." }
98
+ // { error: "...", message: "..." }
99
+ // { ok: false, error: "..." }
100
+ // { error: "..." }
101
+
102
+ // All are normalized to SmartlinksApiError with consistent access:
103
+ import { SmartlinksApiError, product } from '@proveanything/smartlinks'
104
+
105
+ try {
106
+ const item = await product.get('collectionId', 'productId', false)
107
+ } catch (error) {
108
+ if (error instanceof SmartlinksApiError) {
109
+ console.error({
110
+ message: error.message, // "Error 404: Product not found"
111
+ statusCode: error.statusCode, // 404 (HTTP status)
112
+ code: error.code, // 404 (same as statusCode)
113
+ errorCode: error.errorCode, // "NOT_FOUND" (server error code string)
114
+ details: error.details, // Additional server details
115
+ url: error.url, // Failed URL
116
+ })
117
+
118
+ // Handle specific server error codes (primary identifier)
119
+ switch (error.errorCode) {
120
+ case 'NOT_AUTHORIZED':
121
+ // Invalid credentials
122
+ break
123
+ case 'broadcasts.topic.invalid':
124
+ // Invalid broadcast topic
125
+ break
126
+ default:
127
+ // Fall back to HTTP status code
128
+ if (error.isNotFound()) {
129
+ // Handle 404
130
+ } else if (error.isAuthError()) {
131
+ // Handle 401/403 - redirect to login
132
+ }
133
+ }
134
+
135
+ // Use helper methods for HTTP status-based handling
136
+ if (error.isRateLimited()) {
137
+ // Handle 429 - implement retry logic
138
+ } else if (error.isServerError()) {
139
+ // Handle 5xx - show maintenance message
140
+ }
141
+ }
142
+ }
143
+ ```
144
+
145
+ ### Helper Methods
146
+
147
+ - `error.isClientError()` - 4xx status codes
148
+ - `error.isServerError()` - 5xx status codes
149
+ - `error.isAuthError()` - 401 or 403
150
+ - `error.isNotFound()` - 404
151
+ - `error.isRateLimited()` - 429
152
+ - `error.toJSON()` - Serializable object for logging
153
+
154
+ For comprehensive error handling examples and migration guidance, see:
155
+ - [examples/error-handling-demo.ts](examples/error-handling-demo.ts) - Complete error handling patterns
156
+ - [docs/ERROR_HANDLING_MIGRATION.md](docs/ERROR_HANDLING_MIGRATION.md) - Migration guide from old error handling
157
+
158
+ ## Common tasks
159
+
160
+ ### Products
161
+
162
+ ```ts
163
+ import { product } from '@proveanything/smartlinks'
164
+
165
+ // Public fetch
166
+ const item = await product.get('collectionId', 'productId', false)
167
+
168
+ // Admin create/update/delete (requires auth)
169
+ await product.create('collectionId', { name: 'New product' })
170
+ await product.update('collectionId', 'productId', { description: 'Updated' })
171
+ await product.remove('collectionId', 'productId')
172
+ ```
173
+
174
+ ### Assets
175
+
176
+ Upload, list, get, and remove assets within a scope (collection/product/proof).
177
+
178
+ #### Upload (new)
179
+
180
+ ```ts
181
+ import { asset } from '@proveanything/smartlinks'
182
+
183
+ const uploaded = await asset.upload({
184
+ file, // File from an <input type="file">
185
+ scope: { type: 'proof', collectionId, productId, proofId },
186
+ name: 'hero.png',
187
+ metadata: { description: 'Uploaded via SDK' },
188
+ onProgress: (p) => console.log(`Upload: ${p}%`),
189
+ // appId: 'microapp-123' // optional
190
+ })
191
+ console.log('Uploaded asset:', uploaded)
192
+ ```
193
+
194
+ Deprecated upload helper (wraps to the new `upload` internally):
195
+
196
+ ```ts
197
+ // @deprecated Use asset.upload(options)
198
+ const legacy = await asset.uploadAsset(collectionId, productId, proofId, file)
199
+ ```
200
+
201
+ #### List
202
+
203
+ ```ts
204
+ // Collection assets, first 20 images
205
+ const list1 = await asset.list({
206
+ scope: { type: 'collection', collectionId },
207
+ mimeTypePrefix: 'image/',
208
+ limit: 20,
209
+ })
210
+
211
+ // Product assets, filter by appId
212
+ const list2 = await asset.list({
213
+ scope: { type: 'product', collectionId, productId },
214
+ appId: 'microapp-123',
215
+ })
216
+ ```
217
+
218
+ #### Get (scoped)
219
+
220
+ ```ts
221
+ const a = await asset.get({
222
+ assetId,
223
+ scope: { type: 'proof', collectionId, productId, proofId },
224
+ })
225
+ ```
226
+
227
+ #### Remove (scoped, admin)
228
+
229
+ ```ts
230
+ await asset.remove({
231
+ assetId,
232
+ scope: { type: 'product', collectionId, productId },
233
+ })
234
+ ```
235
+
236
+ #### Asset response example
237
+
238
+ ```json
239
+ {
240
+ "name": "Screenshot 2025-09-15 at 15.21.14",
241
+ "assetType": "Image",
242
+ "type": "png",
243
+ "collectionId": "ChaseAtlantic",
244
+ "url": "https://cdn.smartlinks.app/sites%2FChaseAtlantic%2Fimages%2F2025%2F9%2FScreenshot%202025-09-15%20at%2015%2C21%2C14-1757946214537.png",
245
+ "createdAt": "2005-10-10T23:15:03",
246
+ "hash": "fb98140a6b41ee69b824f29cc8b6795444246f871e4ab2379528b34a4d16284e",
247
+ "thumbnails": {
248
+ "x100": "https://cdn.smartlinks.app/..._100x100.png",
249
+ "x200": "https://cdn.smartlinks.app/..._200x200.png",
250
+ "x512": "https://cdn.smartlinks.app/..._512x512.png"
251
+ },
252
+ "id": "7k1cGErrlmQ94J8yDlVj",
253
+ "site": "ChaseAtlantic",
254
+ "cleanName": "Screenshot 2025-09-15 at 15.21"
255
+ }
256
+ ```
257
+
258
+ #### Proxy mode and uploads
259
+
260
+ When `initializeApi({ proxyMode: true })` is active (e.g., SDK runs inside an iframe and the parent performs API calls), the SDK serializes `FormData` for upload into a proxy-safe shape and posts a message to the parent window. The parent proxy handler should detect this payload and reconstruct a native `FormData` before performing the actual HTTP request.
261
+
262
+ - SDK proxy message body shape for uploads:
263
+ - `{ _isFormData: true, entries: Array<[name: string, value: string | File]> }`
264
+ - Each entry’s value is either a string or a `File` object (structured-cloneable).
265
+ - Direct XHR uploads are only used when NOT in proxy mode. In proxy mode, the SDK uses the proxy channel and `postMessage`.
266
+
267
+ Progress support in proxy mode:
268
+
269
+ - If you pass `onProgress`, the SDK uses an enhanced upload protocol with chunked messages and progress events.
270
+ - Unified envelope protocol (single message shape):
271
+ - Iframe → Parent
272
+ - `{ _smartlinksProxyUpload: true, phase: 'start', id, path, method: 'POST', headers, fields, fileInfo }`
273
+ - `{ _smartlinksProxyUpload: true, phase: 'chunk', id, seq, chunk: ArrayBuffer }`
274
+ - `{ _smartlinksProxyUpload: true, phase: 'end', id }`
275
+ - Parent → Iframe
276
+ - `{ _smartlinksProxyUpload: true, phase: 'ack', id, seq }` (reply via `event.source.postMessage`)
277
+ - `{ _smartlinksProxyUpload: true, phase: 'progress', id, percent }` (reply via `event.source.postMessage`)
278
+ - `{ _smartlinksProxyUpload: true, phase: 'done', id, ok, data? | error? }` (reply via `event.source.postMessage`)
279
+
280
+ Example parent handler (buffered, simple):
281
+
282
+ ```html
283
+ <script>
284
+ const uploads = new Map();
285
+ window.addEventListener('message', async (e) => {
286
+ const m = e.data;
287
+ if (!m || m._smartlinksProxyUpload !== true) return;
288
+ const target = e.source; // reply to the sender iframe
289
+ const origin = e.origin; // consider validating and passing a strict origin
290
+ switch (m.phase) {
291
+ case 'start': {
292
+ uploads.set(m.id, { chunks: [], fields: m.fields, fileInfo: m.fileInfo, path: m.path, headers: m.headers });
293
+ break;
294
+ }
295
+ case 'chunk': {
296
+ const u = uploads.get(m.id);
297
+ if (!u) break;
298
+ u.chunks.push(new Uint8Array(m.chunk));
299
+ target && target.postMessage({ _smartlinksProxyUpload: true, phase: 'ack', id: m.id, seq: m.seq }, origin);
300
+ break;
301
+ }
302
+ case 'end': {
303
+ const u = uploads.get(m.id);
304
+ if (!u) break;
305
+ const blob = new Blob(u.chunks, { type: u.fileInfo.type || 'application/octet-stream' });
306
+ const fd = new FormData();
307
+ for (const [k, v] of u.fields) fd.append(k, v);
308
+ fd.append(u.fileInfo.key || 'file', blob, u.fileInfo.name || 'upload.bin');
309
+
310
+ const xhr = new XMLHttpRequest();
311
+ xhr.open('POST', (window.SMARTLINKS_API_BASEURL || '') + u.path);
312
+ for (const [k, v] of Object.entries(u.headers || {})) xhr.setRequestHeader(k, v);
313
+ xhr.upload.onprogress = (ev) => {
314
+ if (ev.lengthComputable) {
315
+ const pct = Math.round((ev.loaded / ev.total) * 100);
316
+ target && target.postMessage({ _smartlinksProxyUpload: true, phase: 'progress', id: m.id, percent: pct }, origin);
317
+ }
318
+ };
319
+ xhr.onload = () => {
320
+ const ok = xhr.status >= 200 && xhr.status < 300;
321
+ try {
322
+ const data = JSON.parse(xhr.responseText);
323
+ target && target.postMessage({ _smartlinksProxyUpload: true, phase: 'done', id: m.id, ok, data, error: ok ? undefined : data?.message }, origin);
324
+ } catch (err) {
325
+ target && target.postMessage({ _smartlinksProxyUpload: true, phase: 'done', id: m.id, ok: false, error: 'Invalid server response' }, origin);
326
+ }
327
+ };
328
+ xhr.onerror = () => target && target.postMessage({ _smartlinksProxyUpload: true, phase: 'done', id: m.id, ok: false, error: 'Network error' }, origin);
329
+ xhr.send(fd);
330
+ uploads.delete(m.id);
331
+ break;
332
+ }
333
+ }
334
+ });
335
+ </script>
336
+ ```
337
+
338
+ If you maintain the parent proxy, ensure its handler for `_smartlinksProxyRequest`:
339
+ - Detects `body._isFormData === true` and rebuilds `const fd = new FormData(); for (const [k,v] of body.entries) fd.append(k, v);`
340
+ - Issues the HTTP request with that `FormData` and returns the parsed JSON response back to the iframe.
341
+
342
+ ### AI helpers
343
+
344
+ ```ts
345
+ import { ai } from '@proveanything/smartlinks'
346
+
347
+ const content = await ai.generateContent('collectionId', {
348
+ contents: 'Write a friendly product blurb for our coffee beans.',
349
+ responseMimeType: 'text/plain',
350
+ provider: 'openai',
351
+ model: 'gpt-4o'
352
+ })
353
+ console.log(content)
354
+ ```
355
+
356
+ ### Analytics (Actions & Broadcasts)
357
+
358
+ Track user actions and broadcast deliveries per collection. These endpoints live under admin and require valid auth (API key or bearer token).
359
+
360
+ ```ts
361
+ import { actions, broadcasts } from '@proveanything/smartlinks'
362
+
363
+ // List action events for a user
364
+ const actionEvents = await actions.byUser('collectionId', {
365
+ userId: 'user_123',
366
+ from: '2025-01-01T00:00:00Z',
367
+ to: '2025-12-31T23:59:59Z',
368
+ limit: 100,
369
+ })
370
+
371
+ // Outcome counts, optionally deduping latest per actor
372
+ const counts = await actions.countsByOutcome('collectionId', {
373
+ actionId: 'click',
374
+ dedupeLatest: true,
375
+ idField: 'userId',
376
+ })
377
+
378
+ // Append a single action event
379
+ await actions.append('collectionId', {
380
+ userId: 'user_123',
381
+ actionId: 'click',
382
+ outcome: 'success',
383
+ })
384
+
385
+ // Broadcast recipients and filters
386
+ const recipients = await broadcasts.recipientIds('collectionId', {
387
+ broadcastId: 'br_456',
388
+ idField: 'contactId',
389
+ })
390
+
391
+ // Append broadcast recipients in bulk (preferred shape)
392
+ await broadcasts.appendBulk('collectionId', {
393
+ params: { broadcastId: 'br_456', channel: 'email' },
394
+ ids: ['contact_1','contact_2'],
395
+ idField: 'contactId',
396
+ })
397
+ ```
398
+
399
+ ### Broadcast sending and previews
400
+
401
+ ```ts
402
+ import { broadcasts } from '@proveanything/smartlinks'
403
+
404
+ // Preview a broadcast (HTML)
405
+ const preview = await broadcasts.preview('collectionId', 'broadcastId', {
406
+ contactId: 'contact_123',
407
+ props: { firstName: 'Sam' },
408
+ })
409
+
410
+ // Send a test email
411
+ await broadcasts.sendTest('collectionId', 'broadcastId', {
412
+ to: 'test@example.com',
413
+ subject: 'Test subject',
414
+ props: { foo: 'bar' },
415
+ })
416
+
417
+ // Enqueue broadcast sending (background)
418
+ await broadcasts.send('collectionId', 'broadcastId', {
419
+ pageSize: 100,
420
+ sharedContext: { campaign: 'summer' },
421
+ })
422
+
423
+ // Manual page send (for testing/UX)
424
+ const manual = await broadcasts.sendManual('collectionId', 'broadcastId', {
425
+ limit: 50,
426
+ dryRun: true,
427
+ })
428
+ ```
429
+
430
+ ## Communications Overview
431
+
432
+ End-to-end explainer covering comms settings (unsubscribe, types), Web Push registration, and multi-channel broadcasts with SDK examples: [src/docs/smartlinks/comms-explainer.md](src/docs/smartlinks/comms-explainer.md)
433
+
434
+ ### Async jobs
435
+
436
+ ```ts
437
+ import { async as slAsync, jobs } from '@proveanything/smartlinks'
438
+
439
+ // Enqueue an async job
440
+ const queued = await slAsync.enqueueAsyncJob('collectionId', {
441
+ task: 'email:daily-digest',
442
+ payload: { segmentId: 'seg_1' },
443
+ priority: 5,
444
+ key: 'digest:seg_1',
445
+ })
446
+
447
+ // Check job status
448
+ const status = await slAsync.getAsyncJobStatus('collectionId', queued.id)
449
+
450
+ // List recent jobs
451
+ const recent = await jobs.listJobs('collectionId', { state: 'queued', limit: 20 })
452
+
453
+ // Get a single job
454
+ const job = await jobs.getJob('collectionId', queued.id)
455
+ ```
456
+
457
+ ## Browser and React
458
+
459
+ The SDK works in modern browsers. Initialize once and call public endpoints without an API key; authenticate to access protected/admin endpoints.
460
+
461
+ ```ts
462
+ import { initializeApi } from '@proveanything/smartlinks'
463
+ import { collection } from '@proveanything/smartlinks'
464
+
465
+ initializeApi({ baseURL: 'https://smartlinks.app/api/v1' })
466
+ const collections = await collection.list(false)
467
+ ```
468
+
469
+ For a fuller UI example, see `examples/react-demo.tsx`.
470
+
471
+ ## Iframe Integration
472
+
473
+ When embedding Smartlinks inside an iframe (set `proxyMode: true` in `initializeApi` if you need parent-proxy API calls), you can also send UI/control messages to the parent window. The SDK provides lightweight helpers in `iframe.ts`:
474
+
475
+ ```ts
476
+ import { enableAutoIframeResize, disableAutoIframeResize, redirectParent, sendParentCustom, isIframe } from '@proveanything/smartlinks'
477
+
478
+ // Automatically push height changes to parent so it can resize the iframe.
479
+ enableAutoIframeResize({ intervalMs: 150 })
480
+
481
+ // Later disable if not needed:
482
+ disableAutoIframeResize()
483
+
484
+ // Redirect parent window to a URL (e.g. after login):
485
+ redirectParent('https://app.example.com/dashboard')
486
+
487
+ // Send any custom event + payload:
488
+ sendParentCustom('smartlinks:navigate', { url: '/profile' })
489
+
490
+ if (isIframe()) {
491
+ console.log('Running inside an iframe')
492
+ }
493
+ ```
494
+
495
+ Parent page can listen for these messages:
496
+
497
+ ```ts
498
+ window.addEventListener('message', (e) => {
499
+ const msg = e.data
500
+ if (!msg || !msg._smartlinksIframeMessage) return
501
+ switch (msg.type) {
502
+ case 'smartlinks:resize':
503
+ // adjust iframe height
504
+ const iframeEl = document.getElementById('smartlinks-frame') as HTMLIFrameElement
505
+ if (iframeEl) iframeEl.style.height = msg.payload.height + 'px'
506
+ break
507
+ case 'smartlinks:redirect':
508
+ window.location.href = msg.payload.url
509
+ break
510
+ case 'smartlinks:navigate':
511
+ // Custom example
512
+ console.log('Navigate request:', msg.payload.url)
513
+ break
514
+ }
515
+ })
516
+ ```
517
+
518
+ Notes:
519
+ - Auto-resize uses `ResizeObserver` when available, falling back to `MutationObserver` + polling.
520
+ - In non-browser or Node environments these helpers safely no-op.
521
+ - Use `sendParentCustom` for any domain-specific integration events.
522
+
523
+ ## Configuration reference
524
+
525
+ ```ts
526
+ initializeApi({
527
+ baseURL: string, // with or without trailing slash
528
+ apiKey?: string, // Node/server only
529
+ bearerToken?: string, // optional at init; set by auth.login/verifyToken
530
+ proxyMode?: boolean // set true if running inside an iframe and using parent proxy
531
+ ngrokSkipBrowserWarning?: boolean // forces header 'ngrok-skip-browser-warning: true'
532
+ extraHeaders?: Record<string,string> // custom headers merged in each request
533
+ iframeAutoResize?: boolean // default true when embedded in an iframe
534
+ logger?: Function | { debug?: Function; info?: Function; warn?: Function; error?: Function; log?: Function } // optional verbose logging
535
+ })
536
+
537
+ // Auto-detection: If baseURL contains '.ngrok.io' or '.ngrok-free.dev' the header is added automatically
538
+ // unless you explicitly set ngrokSkipBrowserWarning: false.
539
+ // Auto iframe resize: When in an iframe, resize messages are sent by default unless iframeAutoResize: false.
540
+ // Verbose logging: Pass a logger (e.g. console) to log outbound requests/responses and proxy postMessages.
541
+ ```
542
+
543
+ When embedding the SDK in an iframe with `proxyMode: true`, you can also use:
544
+
545
+ ```ts
546
+ import { sendCustomProxyMessage } from '@proveanything/smartlinks'
547
+ const data = await sendCustomProxyMessage('my-action', { foo: 'bar' })
548
+
549
+ // Toggle ngrok header or update custom headers later:
550
+ import { setNgrokSkipBrowserWarning, setExtraHeaders } from '@proveanything/smartlinks'
551
+ setNgrokSkipBrowserWarning(true)
552
+ setExtraHeaders({ 'X-Debug': '1' })
553
+ ```
554
+
555
+ ## Full API surface
556
+
557
+ Explore every function, parameter, and type here:
558
+
559
+ - API Summary (API_SUMMARY.md)
560
+
561
+ ## Requirements
562
+
563
+ - Node.js 16+ or modern browsers
564
+ - TypeScript 4.9+ (if using TS)
565
+
566
+ ## License
567
+
568
+ MIT © Prove Anything
569
+
package/dist/api/asset.js CHANGED
@@ -13,15 +13,16 @@ export var asset;
13
13
  }
14
14
  }
15
15
  asset.AssetUploadError = AssetUploadError;
16
- function buildScopeBase(scope) {
16
+ function buildScopeBase(scope, isAdmin = false) {
17
+ const prefix = isAdmin ? '/admin' : '/public';
17
18
  if (scope.type === 'collection') {
18
- return `/public/collection/${encodeURIComponent(scope.collectionId)}`;
19
+ return `${prefix}/collection/${encodeURIComponent(scope.collectionId)}`;
19
20
  }
20
21
  if (scope.type === 'product') {
21
- return `/public/collection/${encodeURIComponent(scope.collectionId)}/product/${encodeURIComponent(scope.productId)}`;
22
+ return `${prefix}/collection/${encodeURIComponent(scope.collectionId)}/product/${encodeURIComponent(scope.productId)}`;
22
23
  }
23
24
  // proof
24
- return `/public/collection/${encodeURIComponent(scope.collectionId)}/product/${encodeURIComponent(scope.productId)}/proof/${encodeURIComponent(scope.proofId)}`;
25
+ return `${prefix}/collection/${encodeURIComponent(scope.collectionId)}/product/${encodeURIComponent(scope.productId)}/proof/${encodeURIComponent(scope.proofId)}`;
25
26
  }
26
27
  /**
27
28
  * Upload an asset file
@@ -29,7 +30,7 @@ export var asset;
29
30
  * @throws AssetUploadError if upload fails
30
31
  */
31
32
  async function upload(options) {
32
- const base = buildScopeBase(options.scope);
33
+ const base = buildScopeBase(options.scope, !!options.admin);
33
34
  let path = `${base}/asset`;
34
35
  if (options.appId) {
35
36
  const qp = new URLSearchParams({ appId: options.appId });
@@ -26,4 +26,5 @@ export { qr } from "./qr";
26
26
  export { template } from "./template";
27
27
  export { interactions } from "./interactions";
28
28
  export { location } from "./location";
29
+ export * as realtime from "./realtime";
29
30
  export type { AIGenerateContentRequest, AIGenerateImageRequest, AISearchPhotosRequest, AISearchPhotosPhoto } from "./ai";
package/dist/api/index.js CHANGED
@@ -28,3 +28,5 @@ export { qr } from "./qr";
28
28
  export { template } from "./template";
29
29
  export { interactions } from "./interactions";
30
30
  export { location } from "./location";
31
+ import * as realtime_1 from "./realtime";
32
+ export { realtime_1 as realtime };