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