@proveanything/smartlinks 1.6.4 → 1.6.6

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.
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.6.4 | Generated: 2026-02-26T12:33:27.813Z
3
+ Version: 1.6.6 | Generated: 2026-03-03T14:13:59.227Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -21,6 +21,7 @@ For detailed guides on specific features:
21
21
  - **[App Data Storage](app-data-storage.md)** - User-specific and collection-scoped app data storage
22
22
  - **[App Objects: Cases, Threads & Records](app-objects.md)** - Generic app-scoped building blocks for support cases, discussions, bookings, registrations, and more
23
23
  - **[Communications](comms.md)** - Transactional sends, multi-channel broadcasts, consent management, push registration, and analytics
24
+ - **[Deep Link Discovery](deep-link-discovery.md)** - Registering and discovering navigable app states for portal menus and AI orchestration
24
25
  - **[AI Guide Template](ai-guide-template.md)** - A sample for an app on how to build an AI setup guide
25
26
 
26
27
  ## API Namespaces
@@ -35,7 +36,7 @@ The Smartlinks SDK is organized into the following namespaces:
35
36
  - **batch** - Group products into batches; manage serial number ranges and lookups.
36
37
  - **crate** - Organize products in containers/crates for logistics and grouping.
37
38
  - **form** - Build and manage dynamic forms used by apps and workflows.
38
- - **appConfiguration** - Read/write app configuration and scoped data (collection/product/proof).
39
+ - **appConfiguration** - Read/write app configuration and scoped data (collection/product/proof); hosts the deep-link registry. → [Guide](deep-link-discovery.md)
39
40
 
40
41
  — Identity & Access —
41
42
  - **auth** - Admin authentication and account ops: login/logout, tokens, account info.
@@ -0,0 +1,514 @@
1
+ # Deep Link Discovery
2
+
3
+ Complete guide to registering and discovering navigable states in SmartLinks apps, enabling portal menus, AI orchestrators, and other apps to deep-link directly into specific views.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ - [Overview](#overview)
10
+ - [Two Sources of Truth](#two-sources-of-truth)
11
+ - [Static Links — App Manifest](#static-links--app-manifest)
12
+ - [Dynamic Links — App Config](#dynamic-links--app-config)
13
+ - [Schema: DeepLinkEntry](#schema-deeplinkentry)
14
+ - [Quick Start](#quick-start)
15
+ - [URL Resolution](#url-resolution)
16
+ - [Syncing Dynamic Links](#syncing-dynamic-links)
17
+ - [Consumer Patterns](#consumer-patterns)
18
+ - [Merging Both Sources](#merging-both-sources)
19
+ - [Portal Navigation Menu](#portal-navigation-menu)
20
+ - [AI Orchestration](#ai-orchestration)
21
+ - [Cross-App Navigation](#cross-app-navigation)
22
+ - [TypeScript Types](#typescript-types)
23
+ - [Rules & Best Practices](#rules--best-practices)
24
+
25
+ ---
26
+
27
+ ## Overview
28
+
29
+ SmartLinks apps can expose **deep-linkable states** — specific views, pages, or configurations that other apps, the portal, or AI orchestrators can navigate to directly without loading the app first.
30
+
31
+ This is done by publishing a `linkable` array in the app's `appConfig`. Any consumer (portal shell, AI agent, another app) can read this array to discover what navigable states the app currently offers.
32
+
33
+ ```text
34
+ ┌─────────────────────────────────────────────────────────────────────┐
35
+ │ Your SmartLinks App │
36
+ │ │
37
+ │ ┌───────────────────────┐ writes ┌────────────────────────┐│
38
+ │ │ Admin / Data Events │ ─────────────► │ appConfig.linkable ││
39
+ │ └───────────────────────┘ └────────────────────────┘│
40
+ └─────────────────────────────────────────────────────────────────────┘
41
+
42
+ reads linkable[]
43
+
44
+ ┌───────────────────────────────────┼───────────────────────┐
45
+ ▼ ▼ ▼
46
+ ┌─────────────────────┐ ┌────────────────────┐ ┌─────────────────────┐
47
+ │ Portal Nav Menu │ │ AI Orchestrator │ │ Another App │
48
+ │ (sidebar / drawer) │ │ ("Go to About Us")│ │ (cross-app nav) │
49
+ └─────────────────────┘ └────────────────────┘ └─────────────────────┘
50
+ ```
51
+
52
+ ### Key Benefits
53
+
54
+ - ✅ **Discoverable** — consumers don't need to know the app's internal routing
55
+ - ✅ **Decoupled** — the app owns the registry; consumers just read it
56
+ - ✅ **Dynamic** — the registry updates automatically when content changes
57
+ - ✅ **AI-friendly** — machine-readable titles make states naturally addressable by LLMs
58
+ - ✅ **No extra endpoints** — built on top of the existing `appConfiguration` API
59
+
60
+ ---
61
+
62
+ ## Quick Start
63
+
64
+ **App side — publish linkable states:**
65
+
66
+ ```typescript
67
+ import { appConfiguration } from '@proveanything/smartlinks';
68
+
69
+ await appConfiguration.setConfig({
70
+ collectionId,
71
+ appId,
72
+ config: {
73
+ // ... other app config ...
74
+ linkable: [
75
+ { title: 'Gallery', path: '/gallery' },
76
+ { title: 'About Us', params: { pageId: 'about-us' } },
77
+ { title: 'FAQ', params: { pageId: 'faq' } },
78
+ ]
79
+ },
80
+ admin: true
81
+ });
82
+ ```
83
+
84
+ **Consumer side — read linkable states:**
85
+
86
+ ```typescript
87
+ import { appConfiguration } from '@proveanything/smartlinks';
88
+
89
+ const config = await appConfiguration.getConfig({ collectionId, appId });
90
+ const linkable = config?.linkable ?? [];
91
+
92
+ // linkable = [
93
+ // { title: 'Gallery', path: '/gallery' },
94
+ // { title: 'About Us', params: { pageId: 'about-us' } },
95
+ // { title: 'FAQ', params: { pageId: 'faq' } },
96
+ // ]
97
+ ```
98
+
99
+ ---
100
+
101
+ ## The `linkable` Registry
102
+
103
+ Each app **MAY** store a `linkable` array at the top level of its `appConfig`. This array describes all the deep-linkable states the app currently exposes.
104
+
105
+ ### Storage Location
106
+
107
+ The `linkable` key is **reserved** at the top level of `appConfig` for this purpose. Do not use it for anything else.
108
+
109
+ ```typescript
110
+ // Writing
111
+ await appConfiguration.setConfig({
112
+ collectionId,
113
+ appId,
114
+ config: {
115
+ // ... other app config ...
116
+ linkable: [ /* DeepLinkEntry[] */ ]
117
+ },
118
+ admin: true
119
+ });
120
+
121
+ // Reading
122
+ const config = await appConfiguration.getConfig({ collectionId, appId });
123
+ const linkable: DeepLinkEntry[] = config?.linkable ?? [];
124
+ ```
125
+
126
+ ### Admin vs Public Access
127
+
128
+ - **Writing** always requires `admin: true` — only the app itself (running with admin credentials) should update its own registry.
129
+ - **Reading** can be done publicly — portals and consumers typically read the registry without admin credentials.
130
+
131
+ ---
132
+
133
+ ## Schema: DeepLinkEntry
134
+
135
+ Each entry in the `linkable` array describes one navigable state:
136
+
137
+ ```typescript
138
+ interface DeepLinkEntry {
139
+ /** Human-readable label for this state (required) */
140
+ title: string;
141
+
142
+ /**
143
+ * Hash-route path within the app (optional).
144
+ * Omit or set to "/" for the app's default route.
145
+ *
146
+ * Examples: "/gallery", "/settings", "/settings/advanced"
147
+ */
148
+ path?: string;
149
+
150
+ /**
151
+ * App-specific query parameters (optional).
152
+ * Appended to the URL as search params inside the hash route.
153
+ * Used to specify content (e.g. which page, tab, or filter to show).
154
+ *
155
+ * Platform context params (collectionId, appId, productId, etc.)
156
+ * are injected automatically — do NOT include them here.
157
+ */
158
+ params?: Record<string, string>;
159
+ }
160
+ ```
161
+
162
+ ### Field Notes
163
+
164
+ | Field | Required | Notes |
165
+ |-------|----------|-------|
166
+ | `title` | ✅ | Displayed in menus and offered to AI agents. Should be concise and human-readable. |
167
+ | `path` | ❌ | Hash route within the app. Defaults to `/` if omitted. |
168
+ | `params` | ❌ | App-specific query params only. Platform params are injected automatically. |
169
+
170
+ > **Tip:** An entry with only a `title` (no `path` or `params`) is valid — it navigates to the app's default view. This is rarely useful unless the app's home screen is itself a meaningful destination distinct from other entries.
171
+
172
+ ---
173
+
174
+ ## Entry Types
175
+
176
+ ### 1. Data-Driven Entries (Dynamic)
177
+
178
+ Generated from app data — e.g., CMS pages, product guides, FAQ items. These are typically re-synced whenever the underlying data changes.
179
+
180
+ ```json
181
+ [
182
+ { "title": "About Us", "params": { "pageId": "about-us" } },
183
+ { "title": "Care Guide", "params": { "pageId": "care-guide" } },
184
+ { "title": "FAQ", "params": { "pageId": "faq" } }
185
+ ]
186
+ ```
187
+
188
+ **When to use:** The set of linkable states depends on admin-created content. The registry is rebuilt whenever pages are added, removed, published, or renamed.
189
+
190
+ ### 2. Route-Based Entries (Static)
191
+
192
+ Fixed routes that are built into the app. Declared once and rarely change.
193
+
194
+ ```json
195
+ [
196
+ { "title": "Gallery", "path": "/gallery" },
197
+ { "title": "Settings", "path": "/settings" },
198
+ { "title": "Advanced Settings", "path": "/settings", "params": { "tab": "advanced" } }
199
+ ]
200
+ ```
201
+
202
+ **When to use:** The app has multiple built-in views (tabs, sections) that should always be directly navigable, regardless of content.
203
+
204
+ > **Note:** Different entries can share the same `path` if they differ by `params` — as in the Settings example above.
205
+
206
+ ### 3. Mixed
207
+
208
+ Apps can freely combine both patterns. Static routes can appear alongside dynamic content entries:
209
+
210
+ ```json
211
+ [
212
+ { "title": "Gallery", "path": "/gallery" },
213
+ { "title": "About Us", "params": { "pageId": "about-us" } },
214
+ { "title": "FAQ", "params": { "pageId": "faq" } },
215
+ { "title": "Settings", "path": "/settings" }
216
+ ]
217
+ ```
218
+
219
+ **Ordering:** Entries are displayed in array order. Place the most commonly navigated states first.
220
+
221
+ ---
222
+
223
+ ## URL Resolution
224
+
225
+ The parent platform constructs the final navigation URL from an entry by combining:
226
+
227
+ 1. The app's **base URL**
228
+ 2. The **entry point** (`index.html` for public routes, `admin.html` for admin routes)
229
+ 3. The **hash route path** (defaults to `/`)
230
+ 4. Platform **context params** (`collectionId`, `appId`, `productId`, etc.)
231
+ 5. The entry's `params`
232
+
233
+ ### Examples
234
+
235
+ | Entry | Resolved URL |
236
+ |-------|--------------|
237
+ | `{ title: "About Us", params: { pageId: "about-us" } }` | `https://app.example.com/#/?collectionId=abc&appId=my-app&pageId=about-us` |
238
+ | `{ title: "Advanced Settings", path: "/settings", params: { tab: "advanced" } }` | `https://app.example.com/admin.html#/settings?collectionId=abc&appId=my-app&tab=advanced` |
239
+ | `{ title: "Gallery", path: "/gallery" }` | `https://app.example.com/#/gallery?collectionId=abc&appId=my-app` |
240
+ | `{ title: "Home" }` | `https://app.example.com/#/?collectionId=abc&appId=my-app` |
241
+
242
+ ### Injected Context Params
243
+
244
+ The platform automatically injects the following — **never include these in `params`**:
245
+
246
+ | Param | Source |
247
+ |-------|--------|
248
+ | `collectionId` | Active collection |
249
+ | `appId` | The app being navigated to |
250
+ | `productId` | Active product (if set) |
251
+ | `proofId` | Active proof (if set) |
252
+ | `lang` | Current language preference |
253
+ | `theme` | Active theme name |
254
+
255
+ ---
256
+
257
+ ## Syncing the Registry
258
+
259
+ ### When to Sync
260
+
261
+ Apps **MUST** update the `linkable` array whenever the set of navigable states changes:
262
+
263
+ - A page is created, deleted, published, or unpublished
264
+ - A page's `deepLinkable` flag is toggled
265
+ - A page title changes
266
+ - App routes are added or removed
267
+
268
+ ### How to Sync
269
+
270
+ The sync pattern is a **full replace** — read current config, overwrite `linkable`, save back. This avoids orphaned entries and keeps the logic simple.
271
+
272
+ ```typescript
273
+ import { appConfiguration } from '@proveanything/smartlinks';
274
+
275
+ async function syncLinkable(collectionId: string, appId: string): Promise<void> {
276
+ // 1. Fetch the current set of linkable states from your data source
277
+ // Always fetch fresh — never rely on a local cache
278
+ const entries = await fetchLinkableEntries(); // your app-specific logic
279
+
280
+ // 2. Read the existing appConfig to preserve other keys
281
+ let config: Record<string, unknown> = {};
282
+ try {
283
+ config = await appConfiguration.getConfig({
284
+ collectionId,
285
+ appId,
286
+ admin: true,
287
+ }) ?? {};
288
+ } catch {
289
+ // Config may not exist yet on first run — that's fine
290
+ }
291
+
292
+ // 3. Merge and save
293
+ await appConfiguration.setConfig({
294
+ collectionId,
295
+ appId,
296
+ config: { ...config, linkable: entries },
297
+ admin: true,
298
+ });
299
+ }
300
+ ```
301
+
302
+ ### What `fetchLinkableEntries` looks like
303
+
304
+ This function is app-specific. A typical CMS app might look like:
305
+
306
+ ```typescript
307
+ async function fetchLinkableEntries(): Promise<DeepLinkEntry[]> {
308
+ // Fetch your pages/content from the server (not from cache)
309
+ const pages = await myApi.getPublishedPages({ forceRefresh: true });
310
+
311
+ return pages
312
+ .filter(page => page.deepLinkable)
313
+ .map(page => ({
314
+ title: page.title,
315
+ params: { pageId: page.slug },
316
+ }));
317
+ }
318
+ ```
319
+
320
+ A route-based app simply returns a static array:
321
+
322
+ ```typescript
323
+ async function fetchLinkableEntries(): Promise<DeepLinkEntry[]> {
324
+ return [
325
+ { title: 'Gallery', path: '/gallery' },
326
+ { title: 'Settings', path: '/settings' },
327
+ ];
328
+ }
329
+ ```
330
+
331
+ ### Idempotency
332
+
333
+ The sync is always a full replace of the `linkable` array. Running it multiple times with the same data produces the same result — no duplicates, no orphans.
334
+
335
+ ### Clearing the Registry
336
+
337
+ To remove all linkable states (e.g., when an app is deactivated):
338
+
339
+ ```typescript
340
+ await appConfiguration.setConfig({
341
+ collectionId,
342
+ appId,
343
+ config: { ...config, linkable: [] },
344
+ admin: true,
345
+ });
346
+ ```
347
+
348
+ ---
349
+
350
+ ## Consumer Patterns
351
+
352
+ ### Portal Navigation Menu
353
+
354
+ A portal shell can aggregate linkable states from all apps in a collection to build a unified navigation menu:
355
+
356
+ ```typescript
357
+ import { appConfiguration } from '@proveanything/smartlinks';
358
+
359
+ interface NavLink {
360
+ appId: string;
361
+ title: string;
362
+ path?: string;
363
+ params?: Record<string, string>;
364
+ }
365
+
366
+ async function buildNavMenu(collectionId: string, appIds: string[]): Promise<NavLink[]> {
367
+ const links: NavLink[] = [];
368
+
369
+ for (const appId of appIds) {
370
+ const config = await appConfiguration.getConfig({ collectionId, appId });
371
+ const entries: DeepLinkEntry[] = config?.linkable ?? [];
372
+
373
+ links.push(
374
+ ...entries.map(entry => ({ ...entry, appId }))
375
+ );
376
+ }
377
+
378
+ return links;
379
+ }
380
+ ```
381
+
382
+ ### AI Orchestration
383
+
384
+ An AI agent can discover what states an app exposes and offer navigation as part of a conversation:
385
+
386
+ ```typescript
387
+ import { appConfiguration } from '@proveanything/smartlinks';
388
+
389
+ async function getNavigationOptions(collectionId: string, appId: string) {
390
+ const config = await appConfiguration.getConfig({ collectionId, appId });
391
+ const linkable: DeepLinkEntry[] = config?.linkable ?? [];
392
+
393
+ // The AI now knows what's available:
394
+ // "I can navigate you to: Gallery, About Us, or FAQ.
395
+ // Which would you prefer?"
396
+ return linkable.map(entry => entry.title);
397
+ }
398
+ ```
399
+
400
+ The `title` field is intentionally human-readable so it can be injected directly into LLM prompts without additional transformation.
401
+
402
+ ### Cross-App Navigation
403
+
404
+ An app can use another app's `linkable` registry to construct a `NavigationRequest` for the portal's `onNavigate` callback:
405
+
406
+ ```typescript
407
+ // Inside a widget or container's onNavigate handler
408
+ const targetConfig = await appConfiguration.getConfig({
409
+ collectionId,
410
+ appId: 'target-app-id',
411
+ });
412
+
413
+ const entry = targetConfig?.linkable?.find(e => e.title === 'About Us');
414
+
415
+ if (entry) {
416
+ onNavigate({
417
+ appId: 'target-app-id',
418
+ deepLink: entry.path,
419
+ params: entry.params,
420
+ });
421
+ }
422
+ ```
423
+
424
+ ---
425
+
426
+ ## Manifest Declaration
427
+
428
+ Apps that support deep linking **SHOULD** declare the `linkable` key in their `app.admin.json` config schema. This signals to platform tooling that the key is auto-managed and prevents accidental manual edits:
429
+
430
+ ```json
431
+ {
432
+ "setup": {
433
+ "configSchema": {
434
+ "properties": {
435
+ "linkable": {
436
+ "type": "array",
437
+ "description": "Auto-managed deep link registry. Do not edit manually.",
438
+ "readOnly": true,
439
+ "items": {
440
+ "type": "object",
441
+ "required": ["title"],
442
+ "properties": {
443
+ "title": { "type": "string", "description": "Human-readable label" },
444
+ "path": { "type": "string", "description": "Hash route path, e.g. '/gallery'" },
445
+ "params": {
446
+ "type": "object",
447
+ "description": "App-specific query params. Do not include platform context params.",
448
+ "additionalProperties": { "type": "string" }
449
+ }
450
+ }
451
+ }
452
+ }
453
+ }
454
+ }
455
+ }
456
+ }
457
+ ```
458
+
459
+ > **Note:** The `readOnly: true` flag is a hint to the admin UI to hide or disable this field in manual config editors.
460
+
461
+ ---
462
+
463
+ ## TypeScript Types
464
+
465
+ Copy these types into your app, or import them if they become part of the SDK's public API:
466
+
467
+ ```typescript
468
+ /**
469
+ * A single navigable state exposed by a SmartLinks app.
470
+ * Stored as an array at `appConfig.linkable`.
471
+ */
472
+ export interface DeepLinkEntry {
473
+ /** Human-readable label shown in menus and offered to AI agents */
474
+ title: string;
475
+ /**
476
+ * Hash route path within the app.
477
+ * Defaults to "/" if omitted.
478
+ * Examples: "/gallery", "/settings"
479
+ */
480
+ path?: string;
481
+ /**
482
+ * App-specific query params appended to the hash route URL.
483
+ * Do NOT include platform context params (collectionId, appId, etc.).
484
+ */
485
+ params?: Record<string, string>;
486
+ }
487
+
488
+ /**
489
+ * The shape of the `linkable` key in appConfig.
490
+ * Alias for convenience.
491
+ */
492
+ export type DeepLinkRegistry = DeepLinkEntry[];
493
+ ```
494
+
495
+ ---
496
+
497
+ ## Rules & Best Practices
498
+
499
+ ### Rules
500
+
501
+ 1. **`linkable` is reserved** — do not use this key for other purposes in `appConfig`.
502
+ 2. **No platform context params in entries** — `collectionId`, `appId`, `productId`, `proofId`, `lang`, `theme` are injected by the platform automatically.
503
+ 3. **`title` is required** — every entry must have a human-readable label.
504
+ 4. **Sync from server** — always fetch fresh data when syncing; never read from a local cache or previously fetched state.
505
+ 5. **Full replace** — always overwrite the entire `linkable` array; never attempt to diff or patch individual entries.
506
+
507
+ ### Best Practices
508
+
509
+ - **Keep titles short and scannable** — they appear in navigation menus and AI prompts. Prefer "About Us" over "Our Company Story & Mission".
510
+ - **Order entries by relevance** — the first few entries are most likely to be shown in compact menus; place the most important states first.
511
+ - **Sync eagerly** — trigger a sync immediately after any content change rather than on a schedule. The operation is cheap and idempotent.
512
+ - **Handle missing `linkable` gracefully** — older apps won't have a `linkable` key. Always use `config?.linkable ?? []` rather than `config.linkable`.
513
+ - **Don't duplicate the default route** — if your app has only one view, an empty `linkable` array (or omitting it) is better than a single entry that just opens the app.
514
+ - **Test URL resolution** — confirm that your entries produce the correct URLs in the platform before publishing. Check that params are correctly appended and context params are not doubled up.