@proveanything/smartlinks 1.3.35 → 1.3.38

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/http.js CHANGED
@@ -173,6 +173,13 @@ export function initializeApi(options) {
173
173
  apiKey = options.apiKey;
174
174
  bearerToken = options.bearerToken;
175
175
  proxyMode = !!options.proxyMode;
176
+ console.log('[SmartLinks] initializeApi called', {
177
+ baseURL,
178
+ proxyMode,
179
+ hasApiKey: !!apiKey,
180
+ hasBearerToken: !!bearerToken,
181
+ isIframe: typeof window !== 'undefined' && window.parent !== window
182
+ });
176
183
  // Auto-enable ngrok skip header if domain contains .ngrok.io and user did not explicitly set the flag.
177
184
  // Infer ngrok usage from common domains (.ngrok.io or .ngrok-free.dev)
178
185
  const inferredNgrok = /(\.ngrok\.io|\.ngrok-free\.dev)(\b|\/)/i.test(baseURL);
@@ -226,19 +233,33 @@ function ensureProxyListener() {
226
233
  return;
227
234
  window.addEventListener("message", (event) => {
228
235
  const msg = event.data;
236
+ // Log all messages to help debug
237
+ if (msg && msg._smartlinksProxyResponse) {
238
+ console.log('[SmartLinks:Child] 📨 Received proxy response from parent', {
239
+ id: msg.id,
240
+ hasError: !!msg.error,
241
+ hasData: !!msg.data,
242
+ origin: event.origin
243
+ });
244
+ }
229
245
  if (!msg || !msg._smartlinksProxyResponse || !msg.id)
230
246
  return;
231
247
  logDebug('[smartlinks] proxy:response', { id: msg.id, ok: !msg.error, keys: Object.keys(msg) });
232
248
  const pending = proxyPending[msg.id];
233
249
  if (pending) {
234
250
  if (msg.error) {
251
+ console.error('[SmartLinks:Child] ❌ Proxy request failed', { id: msg.id, error: msg.error });
235
252
  pending.reject(new Error(msg.error));
236
253
  }
237
254
  else {
255
+ console.log('[SmartLinks:Child] ✅ Proxy request succeeded', { id: msg.id });
238
256
  pending.resolve(msg.data);
239
257
  }
240
258
  delete proxyPending[msg.id];
241
259
  }
260
+ else {
261
+ console.warn('[SmartLinks:Child] ⚠️ Received response for unknown request', { id: msg.id });
262
+ }
242
263
  });
243
264
  window._smartlinksProxyListener = true;
244
265
  }
@@ -266,10 +287,25 @@ async function proxyRequest(method, path, body, headers, options) {
266
287
  headers,
267
288
  options,
268
289
  };
290
+ console.log('[SmartLinks:Child] 🚀 Sending proxy request to parent', {
291
+ id,
292
+ method,
293
+ path,
294
+ hasBody: !!body,
295
+ bodyType: body ? body.constructor.name : 'none',
296
+ headerCount: headers ? Object.keys(headers).length : 0
297
+ });
269
298
  logDebug('[smartlinks] proxy:postMessage', { id, method, path, headers: headers ? redactHeaders(headers) : undefined, hasBody: !!body });
270
299
  return new Promise((resolve, reject) => {
271
300
  proxyPending[id] = { resolve, reject };
272
- window.parent.postMessage(msg, "*");
301
+ try {
302
+ window.parent.postMessage(msg, "*");
303
+ console.log('[SmartLinks:Child] ✅ postMessage sent successfully', { id });
304
+ }
305
+ catch (error) {
306
+ console.error('[SmartLinks:Child] ❌ postMessage failed', { id, error });
307
+ throw error;
308
+ }
273
309
  // Optionally: add a timeout here to reject if no response
274
310
  });
275
311
  }
@@ -41,14 +41,14 @@ export class IframeResponder {
41
41
  this.resizeHandler = null;
42
42
  this.appUrl = null;
43
43
  this.resolveReady = null;
44
- console.log('[IframeResponder] Constructor called', {
44
+ console.log('[IframeResponder:Parent] 🏗️ Constructor called', {
45
45
  collectionId: options.collectionId,
46
46
  appId: options.appId,
47
47
  productId: options.productId,
48
48
  hasCache: !!options.cache,
49
49
  hasCachedApps: !!((_a = options.cache) === null || _a === void 0 ? void 0 : _a.apps),
50
50
  });
51
- console.log('[IframeResponder] SDK version check:', {
51
+ console.log('[IframeResponder:Parent] SDK version check:', {
52
52
  hasCache: typeof cache !== 'undefined',
53
53
  hasCacheGetOrFetch: typeof (cache === null || cache === void 0 ? void 0 : cache.getOrFetch) === 'function',
54
54
  });
@@ -78,19 +78,20 @@ export class IframeResponder {
78
78
  * Returns the src URL to set on the iframe.
79
79
  */
80
80
  async attach(iframe) {
81
- console.log('[IframeResponder] attach() called, waiting for ready...');
81
+ console.log('[IframeResponder:Parent] 🔗 attach() called, waiting for ready...');
82
82
  await this.ready;
83
- console.log('[IframeResponder] Ready resolved, appUrl:', this.appUrl);
83
+ console.log('[IframeResponder:Parent] Ready resolved, appUrl:', this.appUrl);
84
84
  this.iframe = iframe;
85
85
  // Set up message listener
86
86
  this.messageHandler = this.handleMessage.bind(this);
87
87
  window.addEventListener('message', this.messageHandler);
88
+ console.log('[IframeResponder:Parent] 👂 Message listener attached to window');
88
89
  // Set up resize listener for viewport-based calculations
89
90
  this.resizeHandler = this.calculateViewportHeight.bind(this);
90
91
  window.addEventListener('resize', this.resizeHandler);
91
92
  window.addEventListener('orientationchange', this.resizeHandler);
92
93
  const src = this.buildIframeSrc();
93
- console.log('[IframeResponder] Built iframe src:', src);
94
+ console.log('[IframeResponder:Parent] 🎯 Built iframe src:', src);
94
95
  return src;
95
96
  }
96
97
  /**
@@ -272,33 +273,65 @@ export class IframeResponder {
272
273
  // Message Handling
273
274
  // ===========================================================================
274
275
  async handleMessage(event) {
276
+ var _a, _b;
277
+ // Log all received messages for debugging
278
+ const data = event.data;
279
+ if (data && typeof data === 'object') {
280
+ console.log('[IframeResponder:Parent] 📨 Received message from iframe', {
281
+ type: data.type || (data._smartlinksProxyRequest ? 'proxy-request' : 'unknown'),
282
+ hasId: !!data.id,
283
+ isProxyRequest: !!data._smartlinksProxyRequest,
284
+ isCustomProxyRequest: !!data._smartlinksCustomProxyRequest,
285
+ isStandardMessage: !!data._smartlinksIframeMessage,
286
+ isRouteChange: data.type === 'smartlinks-route-change',
287
+ origin: event.origin,
288
+ source: event.source === ((_a = this.iframe) === null || _a === void 0 ? void 0 : _a.contentWindow) ? 'our-iframe' : 'other'
289
+ });
290
+ }
275
291
  // Validate source is our iframe
276
292
  if (!this.iframe || event.source !== this.iframe.contentWindow) {
293
+ console.log('[IframeResponder:Parent] ⚠️ Ignoring message - not from our iframe', {
294
+ hasIframe: !!this.iframe,
295
+ isCorrectSource: event.source === ((_b = this.iframe) === null || _b === void 0 ? void 0 : _b.contentWindow)
296
+ });
277
297
  return;
278
298
  }
279
- const data = event.data;
280
- if (!data || typeof data !== 'object')
299
+ if (!data || typeof data !== 'object') {
300
+ console.log('[IframeResponder:Parent] ⚠️ Ignoring message - invalid data');
281
301
  return;
302
+ }
282
303
  // Route changes (deep linking)
283
304
  if (data.type === 'smartlinks-route-change') {
305
+ console.log('[IframeResponder:Parent] 🔀 Handling route change');
284
306
  this.handleRouteChange(data);
285
307
  return;
286
308
  }
287
309
  // Standardized iframe messages
288
310
  if (data._smartlinksIframeMessage) {
311
+ console.log('[IframeResponder:Parent] 📋 Handling standard message');
289
312
  await this.handleStandardMessage(data, event);
290
313
  return;
291
314
  }
292
315
  // File upload proxy
293
316
  if (data._smartlinksProxyUpload) {
317
+ console.log('[IframeResponder:Parent] 📤 Handling upload proxy');
294
318
  await this.handleUpload(data, event);
295
319
  return;
296
320
  }
297
321
  // API proxy requests
298
- if (data._smartlinksProxyRequest) {
322
+ if (data._smartlinksProxyRequest || data._smartlinksCustomProxyRequest) {
323
+ console.log('[IframeResponder:Parent] 🌐 Handling API proxy request', {
324
+ id: data.id,
325
+ method: data.method,
326
+ path: data.path,
327
+ isCustom: !!data._smartlinksCustomProxyRequest
328
+ });
299
329
  await this.handleProxyRequest(data, event);
300
330
  return;
301
331
  }
332
+ console.log('[IframeResponder:Parent] ⚠️ Unhandled message type', {
333
+ keys: Object.keys(data)
334
+ });
302
335
  }
303
336
  // ===========================================================================
304
337
  // Route Changes (Deep Linking)
@@ -404,12 +437,17 @@ export class IframeResponder {
404
437
  // ===========================================================================
405
438
  async handleProxyRequest(data, event) {
406
439
  var _a, _b, _c;
440
+ console.log('[IframeResponder:Parent] 🔧 handleProxyRequest called', {
441
+ id: data.id,
442
+ hasCustomFlag: '_smartlinksCustomProxyRequest' in data
443
+ });
407
444
  const response = {
408
445
  _smartlinksProxyResponse: true,
409
446
  id: data.id,
410
447
  };
411
448
  // Handle custom proxy requests (redirects, etc.)
412
449
  if ('_smartlinksCustomProxyRequest' in data && data._smartlinksCustomProxyRequest) {
450
+ console.log('[IframeResponder:Parent] 🔄 Handling custom proxy request', { request: data.request });
413
451
  if (data.request === 'REDIRECT') {
414
452
  const url = (_a = data.params) === null || _a === void 0 ? void 0 : _a.url;
415
453
  if (url) {
@@ -421,6 +459,11 @@ export class IframeResponder {
421
459
  }
422
460
  }
423
461
  const proxyData = data;
462
+ console.log('[IframeResponder:Parent] 🌐 Processing API proxy request', {
463
+ id: proxyData.id,
464
+ method: proxyData.method,
465
+ path: proxyData.path
466
+ });
424
467
  try {
425
468
  const path = proxyData.path.startsWith('/') ? proxyData.path.slice(1) : proxyData.path;
426
469
  // Check for cached data matches on GET requests
@@ -63,6 +63,8 @@ export interface Collection {
63
63
  /** if dark mode is enabled for this collection */
64
64
  dark?: boolean;
65
65
  portalUrl?: string;
66
+ /** Allow users to claim products without providing a proof ID (auto-generates serial on-demand) */
67
+ allowAutoGenerateClaims?: boolean;
66
68
  }
67
69
  export type CollectionResponse = Collection;
68
70
  export type CollectionCreateRequest = Omit<Collection, 'id' | 'shortId'>;
@@ -38,6 +38,14 @@ export interface Product {
38
38
  data: {
39
39
  [key: string]: any;
40
40
  };
41
+ /** Admin-only configuration */
42
+ admin?: {
43
+ /** Allow users to claim this product without providing a proof ID (overrides collection setting) */
44
+ allowAutoGenerateClaims?: boolean;
45
+ /** Last generated serial ID for auto-claim functionality */
46
+ lastSerialId?: number;
47
+ [key: string]: any;
48
+ };
41
49
  }
42
50
  export type ProductResponse = Product;
43
51
  export type ProductCreateRequest = Omit<Product, 'id' | 'collectionId'> & {
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.3.35 | Generated: 2026-02-16T09:53:23.211Z
3
+ Version: 1.3.38 | Generated: 2026-02-18T19:45:24.113Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -16,6 +16,8 @@ For detailed guides on specific features:
16
16
  - **[Liquid Templates](liquid-templates.md)** - Dynamic templating for content generation
17
17
  - **[Theme System](theme.system.md)** - Theme configuration and customization
18
18
  - **[Theme Defaults](theme-defaults.md)** - Default theme values and presets
19
+ - **[Proof Claiming Methods](proof-claiming-methods.md)** - All methods for claiming/registering product ownership (NFC tags, serial numbers, auto-generated claims)
20
+ - **[App Data Storage](app-data-storage.md)** - User-specific and collection-scoped app data storage
19
21
 
20
22
  ## API Namespaces
21
23
 
@@ -1503,6 +1505,7 @@ interface Collection {
1503
1505
  shortId: string, // The shortId of this collection
1504
1506
  dark?: boolean // if dark mode is enabled for this collection
1505
1507
  portalUrl?: string // URL for the collection's portal (if applicable)
1508
+ allowAutoGenerateClaims?: boolean
1506
1509
  }
1507
1510
  ```
1508
1511
 
@@ -3381,6 +3384,11 @@ interface Product {
3381
3384
  data: {
3382
3385
  [key: string]: any
3383
3386
  } // Flexible key/value data map
3387
+ admin?: {
3388
+ allowAutoGenerateClaims?: boolean
3389
+ lastSerialId?: number
3390
+ [key: string]: any
3391
+ }
3384
3392
  }
3385
3393
  ```
3386
3394
 
@@ -3776,16 +3784,27 @@ interface TemplateRenderSourceResponse {
3776
3784
  **AppConfigOptions** (type)
3777
3785
  ```typescript
3778
3786
  type AppConfigOptions = {
3787
+ /** The app ID */
3779
3788
  appId: string
3789
+
3790
+ /** Collection ID (required for most operations) */
3780
3791
  collectionId?: string
3792
+ /** Product ID (optional - for product-scoped config) */
3781
3793
  productId?: string
3794
+ /** Variant ID (optional - for variant-scoped config) */
3782
3795
  variantId?: string
3796
+ /** Batch ID (optional - for batch-scoped config) */
3783
3797
  batchId?: string
3798
+
3799
+ /** Item ID - required for getDataItem/deleteDataItem */
3784
3800
  itemId?: string
3785
- user?: boolean
3786
- userData?: boolean
3801
+
3802
+ /** Use admin endpoints instead of public */
3787
3803
  admin?: boolean
3804
+
3805
+ /** Configuration object for setConfig */
3788
3806
  config?: any
3807
+ /** Data object for setDataItem */
3789
3808
  data?: any
3790
3809
  }
3791
3810
  ```
@@ -3941,18 +3960,25 @@ Post a chat message to the AI (admin or public)
3941
3960
  ### appConfiguration
3942
3961
 
3943
3962
  **getConfig**(opts: AppConfigOptions) → `Promise<any>`
3963
+ Get app configuration for a collection/product scope. ```typescript const config = await appConfiguration.getConfig({ appId: 'warranty-portal', collectionId: 'my-collection' }); ```
3944
3964
 
3945
3965
  **setConfig**(opts: AppConfigOptions) → `Promise<any>`
3966
+ Set app configuration for a collection/product scope. Requires admin authentication. ```typescript await appConfiguration.setConfig({ appId: 'warranty-portal', collectionId: 'my-collection', admin: true, config: { warrantyPeriod: 24, supportEmail: 'support@example.com' } }); ```
3946
3967
 
3947
3968
  **deleteConfig**(opts: AppConfigOptions) → `Promise<void>`
3969
+ Delete app configuration for a collection/product scope. Requires admin authentication. ```typescript await appConfiguration.deleteConfig({ appId: 'warranty-portal', collectionId: 'my-collection', admin: true }); ```
3948
3970
 
3949
3971
  **getData**(opts: AppConfigOptions) → `Promise<any[]>`
3972
+ Get all data items for an app within a scope. ```typescript const items = await appConfiguration.getData({ appId: 'product-docs', collectionId: 'my-collection', productId: 'product-123' }); ```
3950
3973
 
3951
3974
  **getDataItem**(opts: AppConfigOptions) → `Promise<any>`
3975
+ Get a single data item by ID within a scope. ```typescript const item = await appConfiguration.getDataItem({ appId: 'product-docs', collectionId: 'my-collection', productId: 'product-123', itemId: 'manual-1' }); ```
3952
3976
 
3953
3977
  **setDataItem**(opts: AppConfigOptions) → `Promise<any>`
3978
+ Set/create a data item within a scope. Requires admin authentication. ```typescript await appConfiguration.setDataItem({ appId: 'product-docs', collectionId: 'my-collection', productId: 'product-123', admin: true, data: { id: 'manual-1', title: 'User Manual', url: 'https://...' } }); ```
3954
3979
 
3955
3980
  **deleteDataItem**(opts: AppConfigOptions) → `Promise<void>`
3981
+ Delete a data item by ID within a scope. Requires admin authentication. ```typescript await appConfiguration.deleteDataItem({ appId: 'product-docs', collectionId: 'my-collection', productId: 'product-123', admin: true, itemId: 'manual-1' }); ```
3956
3982
 
3957
3983
  **getWidgets**(collectionId: string,
3958
3984
  options?: GetCollectionWidgetsOptions) → `Promise<CollectionWidgetsResponse>`
@@ -4832,7 +4858,12 @@ Update a proof for a product (admin only). PUT /admin/collection/:collectionId/p
4832
4858
  productId: string,
4833
4859
  proofId: string,
4834
4860
  values: ProofClaimRequest) → `Promise<ProofResponse>`
4835
- Claim a proof for a product. PUT /public/collection/:collectionId/product/:productId/proof/:proofId
4861
+ Claim a proof for a product using a proof ID (serial number, NFC tag, etc.). PUT /public/collection/:collectionId/product/:productId/proof/:proofId/claim
4862
+
4863
+ **claimProduct**(collectionId: string,
4864
+ productId: string,
4865
+ values?: ProofClaimRequest) → `Promise<ProofResponse>`
4866
+ Claim a product without providing a proof ID. System auto-generates a unique serial number on-demand. Requires allowAutoGenerateClaims to be enabled on the collection or product. PUT /public/collection/:collectionId/product/:productId/proof/claim ```typescript const proof = await proof.claimProduct( 'beauty-brand', 'moisturizer-pro', { purchaseDate: '2026-02-17', store: 'Target' } ); console.log('Auto-generated ID:', proof.id); ```
4836
4867
 
4837
4868
  **remove**(collectionId: string,
4838
4869
  productId: string,
@@ -4978,6 +5009,29 @@ Backward-compat: Public batch lookup (GET) with collectionId parameter (ignored)
4978
5009
  **renderSource**(collectionId: string,
4979
5010
  body: TemplateRenderSourceRequest) → `Promise<TemplateRenderSourceResponse>`
4980
5011
 
5012
+ ### userAppData
5013
+
5014
+ **getConfig**(appId: string) → `Promise<any>`
5015
+ Get user's config blob for an app. This is a single JSON object stored per user+app. ```typescript const config = await userAppData.getConfig('allergy-tracker'); // Returns: { allergies: ['peanuts'], notifications: true } ```
5016
+
5017
+ **setConfig**(appId: string, config: any) → `Promise<any>`
5018
+ Set user's config blob for an app. ```typescript await userAppData.setConfig('allergy-tracker', { allergies: ['peanuts', 'shellfish'], notifications: true }); ```
5019
+
5020
+ **deleteConfig**(appId: string) → `Promise<void>`
5021
+ Delete user's config blob for an app. ```typescript await userAppData.deleteConfig('allergy-tracker'); ```
5022
+
5023
+ **list**(appId: string) → `Promise<any[]>`
5024
+ List all user's data items for an app. Returns an array of objects, each with an `id` field. ```typescript const beds = await userAppData.list('garden-planner'); // Returns: [{ id: 'bed-1', name: 'Vegetables', ... }, { id: 'bed-2', ... }] ```
5025
+
5026
+ **get**(appId: string, itemId: string) → `Promise<any>`
5027
+ Get a specific user data item by ID. ```typescript const bed = await userAppData.get('garden-planner', 'bed-1'); // Returns: { id: 'bed-1', name: 'Vegetable Bed', plants: [...] } ```
5028
+
5029
+ **set**(appId: string, item: any) → `Promise<any>`
5030
+ Create or update a user data item. The item object must include an `id` field. ```typescript await userAppData.set('garden-planner', { id: 'bed-1', name: 'Vegetable Bed', plants: ['tomatoes', 'peppers'], location: { x: 10, y: 20 } }); ```
5031
+
5032
+ **remove**(appId: string, itemId: string) → `Promise<void>`
5033
+ Delete a user data item by ID. ```typescript await userAppData.remove('garden-planner', 'bed-1'); ```
5034
+
4981
5035
  ### variant
4982
5036
 
4983
5037
  **get**(collectionId: string,
@@ -0,0 +1,223 @@
1
+ # App Data Storage Guide
2
+
3
+ The SmartLinks platform provides two distinct types of data storage for apps:
4
+
5
+ ## 1. User-Specific Data (Global per User+App)
6
+
7
+ **Use the `userAppData` namespace for all user-specific data.**
8
+
9
+ User data is **shared across all collections** for a given user and app. This is perfect for storing user preferences, personal settings, and user-generated content that should persist regardless of which collection they're viewing.
10
+
11
+ ### Use Cases
12
+ - User allergies in an allergy tracking app
13
+ - Garden bed layouts in a garden planning app
14
+ - User preferences and settings
15
+ - Shopping lists, wishlists, favorites
16
+ - Personal notes and annotations
17
+
18
+ ### API Endpoints
19
+ ```
20
+ GET /public/auth/app/:appId - Get user's single config blob
21
+ POST /public/auth/app/:appId - Set user's single config blob
22
+ DELETE /public/auth/app/:appId - Delete user's config blob
23
+
24
+ GET /public/auth/app/:appId/data - Get all user's data items
25
+ GET /public/auth/app/:appId/data/:itemId - Get a specific user data item
26
+ POST /public/auth/app/:appId/data - Create/update a user data item
27
+ DELETE /public/auth/app/:appId/data/:itemId - Delete a user data item
28
+ ```
29
+
30
+ ### SDK Usage
31
+
32
+ #### Single Config Blob (Simple Key-Value)
33
+ ```typescript
34
+ import { userAppData } from '@proveanything/smartlinks';
35
+
36
+ // Get user's config
37
+ const config = await userAppData.getConfig('allergy-tracker');
38
+
39
+ // Save user's config
40
+ await userAppData.setConfig('allergy-tracker', {
41
+ allergies: ['peanuts', 'shellfish'],
42
+ notifications: true
43
+ });
44
+
45
+ // Delete user's config
46
+ await userAppData.deleteConfig('allergy-tracker');
47
+ ```
48
+
49
+ #### Multiple Keyed Data Items (Recommended)
50
+ ```typescript
51
+ // Get all user's garden beds
52
+ const beds = await userAppData.list('garden-planner');
53
+ // Returns: [{ id: 'bed-1', name: 'Vegetables', ... }, { id: 'bed-2', ... }]
54
+
55
+ // Get specific bed
56
+ const bed = await userAppData.get('garden-planner', 'bed-1');
57
+
58
+ // Save/update a bed
59
+ await userAppData.set('garden-planner', {
60
+ id: 'bed-1',
61
+ name: 'Vegetable Bed',
62
+ plants: ['tomatoes', 'peppers'],
63
+ location: { x: 10, y: 20 }
64
+ });
65
+
66
+ // Delete a bed
67
+ await userAppData.remove('garden-planner', 'bed-1');
68
+ ```
69
+
70
+ ### Important Notes
71
+ - ✅ **Clean, simple API** - Just pass the `appId` (no collection/product scoping)
72
+ - ✅ User data requires authentication (Bearer token)
73
+ - ✅ Data is automatically scoped to the authenticated user
74
+ - ✅ Impossible to accidentally scope to collections
75
+
76
+ ---
77
+
78
+ ## 2. Collection/Product-Scoped Data (Admin Configuration)
79
+
80
+ **Use the `appConfiguration` namespace for collection/product-scoped data.**
81
+
82
+ This data is scoped to specific collections, products, variants, or batches. It's typically configured by collection admins/owners and applies to all users viewing that collection/product.
83
+
84
+ ### Use Cases
85
+ - App-specific settings for a collection
86
+ - Product-level configuration
87
+ - Feature flags and toggles
88
+ - Theme and branding settings
89
+ - Public content that all users see
90
+
91
+ ### API Endpoints
92
+ ```
93
+ GET /public/collection/:collectionId/app/:appId
94
+ POST /admin/collection/:collectionId/app/:appId
95
+ DELETE /admin/collection/:collectionId/app/:appId
96
+
97
+ GET /public/collection/:collectionId/product/:productId/app/:appId/data
98
+ POST /admin/collection/:collectionId/product/:productId/app/:appId/data
99
+ ...
100
+ ```
101
+
102
+ ### SDK Usage
103
+
104
+ ```typescript
105
+ import { appConfiguration } from '@proveanything/smartlinks';
106
+
107
+ // Get collection-level app config
108
+ const collectionConfig = await appConfiguration.getConfig({
109
+ appId: 'warranty-portal',
110
+ collectionId: 'my-collection'
111
+ });
112
+
113
+ // Set collection-level config (requires admin auth)
114
+ await appConfiguration.setConfig({
115
+ appId: 'warranty-portal',
116
+ collectionId: 'my-collection',
117
+ admin: true,
118
+ config: {
119
+ warrantyPeriod: 24,
120
+ supportEmail: 'support@example.com'
121
+ }
122
+ });
123
+
124
+ // Get product-level data items
125
+ const items = await appConfiguration.getData({
126
+ appId: 'product-docs',
127
+ collectionId: 'my-collection',
128
+ productId: 'product-123'
129
+ });
130
+
131
+ // Set product-level data item (requires admin auth)
132
+ await appConfiguration.setDataItem({
133
+ appId: 'product-docs',
134
+ collectionId: 'my-collection',
135
+ productId: 'product-123',
136
+ admin: true,
137
+ data: {
138
+ id: 'manual-1',
139
+ title: 'User Manual',
140
+ url: 'https://...'
141
+ }
142
+ });
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Comparison Table
148
+
149
+ | Feature | User Data | Collection/Product Data |
150
+ |---------|-----------|------------------------|
151
+ | **Namespace** | `userAppData` | `appConfiguration` |
152
+ | **Scope** | User + App (global) | Collection/Product/Variant/Batch |
153
+ | **Set by** | Individual users | Collection admins/owners |
154
+ | **Shared across collections?** | ✅ Yes | ❌ No |
155
+ | **Requires auth?** | ✅ Yes (user token) | ✅ Yes (admin token for write) |
156
+ | **Function signature** | Simple: `set(appId, data)` | Options object: `setDataItem({ appId, collectionId, data })` |
157
+ | **Admin write required?** | ❌ No | ✅ Yes (for write operations) |
158
+
159
+ ---
160
+
161
+ ## Migration from Old SDK
162
+
163
+ ### Old SDK → New SDK
164
+
165
+ ```typescript
166
+ // OLD: Get user config
167
+ RemoteApi.get({ path: `public/auth/app/${appId}` })
168
+ // NEW:
169
+ userAppData.getConfig(appId)
170
+
171
+ // OLD: Set user config
172
+ RemoteApi.post({ path: `public/auth/app/${appId}`, data })
173
+ // NEW:
174
+ userAppData.setConfig(appId, data)
175
+
176
+ // OLD: Get user data items
177
+ RemoteApi.get({ path: `public/auth/app/${appId}/data` })
178
+ // NEW:
179
+ userAppData.list(appId)
180
+
181
+ // OLD: Get user data item
182
+ RemoteApi.get({ path: `public/auth/app/${appId}/data/${itemId}` })
183
+ // NEW:
184
+ userAppData.get(appId, itemId)
185
+
186
+ // OLD: Set user data item
187
+ RemoteApi.post({ path: `public/auth/app/${appId}/data`, data: item })
188
+ // NEW:
189
+ userAppData.set(appId, item)
190
+
191
+ // OLD: Delete user data item
192
+ RemoteApi.delete({ path: `public/auth/app/${appId}/data/${itemId}` })
193
+ // NEW:
194
+ userAppData.remove(appId, itemId)
195
+ ```
196
+
197
+ ---
198
+
199
+ ## Complete API Reference
200
+
201
+ ### `userAppData` (User-Specific Data)
202
+
203
+ | Function | Signature | Description |
204
+ |----------|-----------|-------------|
205
+ | `getConfig` | `(appId: string) => Promise<any>` | Get user's config blob |
206
+ | `setConfig` | `(appId: string, config: any) => Promise<any>` | Set user's config blob |
207
+ | `deleteConfig` | `(appId: string) => Promise<void>` | Delete user's config blob |
208
+ | `list` | `(appId: string) => Promise<any[]>` | List all user's data items |
209
+ | `get` | `(appId: string, itemId: string) => Promise<any>` | Get specific data item |
210
+ | `set` | `(appId: string, item: any) => Promise<any>` | Create/update data item |
211
+ | `remove` | `(appId: string, itemId: string) => Promise<void>` | Delete data item |
212
+
213
+ ### `appConfiguration` (Collection/Product-Scoped Data)
214
+
215
+ | Function | Signature | Description |
216
+ |----------|-----------|-------------|
217
+ | `getConfig` | `(opts: AppConfigOptions) => Promise<any>` | Get config for scope |
218
+ | `setConfig` | `(opts: AppConfigOptions) => Promise<any>` | Set config for scope |
219
+ | `deleteConfig` | `(opts: AppConfigOptions) => Promise<void>` | Delete config for scope |
220
+ | `getData` | `(opts: AppConfigOptions) => Promise<any[]>` | List data items in scope |
221
+ | `getDataItem` | `(opts: AppConfigOptions) => Promise<any>` | Get specific data item |
222
+ | `setDataItem` | `(opts: AppConfigOptions) => Promise<any>` | Create/update data item |
223
+ | `deleteDataItem` | `(opts: AppConfigOptions) => Promise<void>` | Delete data item |
@@ -39,7 +39,7 @@ A **Collection** represents a top-level business, brand, or organization. All pr
39
39
  | Field | Type | Description |
40
40
  |-------|------|-------------|
41
41
  | `collection.id` | string | Unique identifier |
42
- | `collection.name` | string | Display name of the collection |
42
+ | `collection.title` | string | Display title of the collection |
43
43
  | `collection.description` | string | Description text |
44
44
  | `collection.slug` | string | URL-friendly identifier |
45
45
  | `collection.logoUrl` | string | URL to the collection's logo image |