@proveanything/smartlinks 1.6.7 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/attestation.d.ts +22 -0
- package/dist/api/attestation.js +22 -0
- package/dist/api/attestations.d.ts +292 -0
- package/dist/api/attestations.js +405 -0
- package/dist/api/containers.d.ts +236 -0
- package/dist/api/containers.js +316 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/tags.d.ts +20 -1
- package/dist/api/tags.js +30 -0
- package/dist/docs/API_SUMMARY.md +701 -7
- package/dist/docs/app-manifest.md +430 -0
- package/dist/docs/attestations.md +498 -0
- package/dist/docs/container-tracking.md +437 -0
- package/dist/docs/deep-link-discovery.md +6 -6
- package/dist/docs/executor.md +554 -0
- package/dist/index.d.ts +3 -0
- package/dist/openapi.yaml +3110 -1323
- package/dist/types/appManifest.d.ts +152 -0
- package/dist/types/attestation.d.ts +12 -0
- package/dist/types/attestations.d.ts +237 -0
- package/dist/types/attestations.js +11 -0
- package/dist/types/containers.d.ts +186 -0
- package/dist/types/containers.js +10 -0
- package/dist/types/tags.d.ts +47 -3
- package/docs/API_SUMMARY.md +701 -7
- package/docs/app-manifest.md +430 -0
- package/docs/attestations.md +498 -0
- package/docs/container-tracking.md +437 -0
- package/docs/deep-link-discovery.md +6 -6
- package/docs/executor.md +554 -0
- package/openapi.yaml +3110 -1323
- package/package.json +1 -1
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# Container Tracking
|
|
2
|
+
|
|
3
|
+
> Physical or logical groupings with hierarchical nesting, item membership, and attestation history.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Overview](#overview)
|
|
10
|
+
- [TypeScript Interfaces](#typescript-interfaces)
|
|
11
|
+
- [SDK Usage](#sdk-usage)
|
|
12
|
+
- [Admin — Containers](#admin--containers)
|
|
13
|
+
- [Admin — Item Membership](#admin--item-membership)
|
|
14
|
+
- [Public — Read-Only](#public--read-only)
|
|
15
|
+
- [REST API Reference](#rest-api-reference)
|
|
16
|
+
- [Attestations on Containers](#attestations-on-containers)
|
|
17
|
+
- [Cold-Chain Tracking Example](#cold-chain-tracking-example)
|
|
18
|
+
- [Design Notes](#design-notes)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Overview
|
|
23
|
+
|
|
24
|
+
A **container** is any physical or logical grouping that can hold items and accumulate a history. Examples:
|
|
25
|
+
|
|
26
|
+
| `containerType` | What it represents |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `'pallet'` | A pallet of goods |
|
|
29
|
+
| `'fridge'` | A refrigerated unit |
|
|
30
|
+
| `'cask'` | A whiskey cask |
|
|
31
|
+
| `'shipping_container'` | A maritime shipping container |
|
|
32
|
+
| `'warehouse'` | A storage facility |
|
|
33
|
+
| `'ship'` | A vessel carrying other containers |
|
|
34
|
+
|
|
35
|
+
Containers support:
|
|
36
|
+
|
|
37
|
+
- **Hierarchical nesting** — a ship can contain shipping containers, which contain pallets, which contain individual items.
|
|
38
|
+
- **Item membership** — any `tag`, `proof`, `serial`, `order_item`, or nested `container` can be placed in and removed from a container. Membership is tracked with timestamps so the full history is available.
|
|
39
|
+
- **Attestations** — measurements and facts (temperature readings, ABV checks, condition reports, etc.) can be appended to any container using the `attestations` namespace.
|
|
40
|
+
- **Soft-delete** — deleting a container only sets `deletedAt`; the record and full item history are preserved.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## TypeScript Interfaces
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
type ContainerStatus = 'active' | 'archived' | string
|
|
48
|
+
type ContainerItemType = 'tag' | 'proof' | 'serial' | 'order_item' | 'container'
|
|
49
|
+
|
|
50
|
+
interface Container {
|
|
51
|
+
id: string
|
|
52
|
+
orgId: string
|
|
53
|
+
collectionId: string
|
|
54
|
+
containerType: string // 'pallet' | 'fridge' | 'cask' | …
|
|
55
|
+
ref?: string // Human-readable identifier / barcode
|
|
56
|
+
name?: string
|
|
57
|
+
description?: string
|
|
58
|
+
status: ContainerStatus // default 'active'
|
|
59
|
+
metadata?: Record<string, any>
|
|
60
|
+
parentContainerId?: string // null = top-level
|
|
61
|
+
children?: Container[] // Populated when ?tree=true
|
|
62
|
+
items?: ContainerItem[] // Populated when ?includeContents=true
|
|
63
|
+
createdAt: string
|
|
64
|
+
updatedAt: string
|
|
65
|
+
deletedAt?: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface ContainerItem {
|
|
69
|
+
id: string
|
|
70
|
+
orgId: string
|
|
71
|
+
containerId: string
|
|
72
|
+
collectionId?: string
|
|
73
|
+
itemType: ContainerItemType
|
|
74
|
+
itemId: string
|
|
75
|
+
productId?: string
|
|
76
|
+
proofId?: string
|
|
77
|
+
addedAt: string
|
|
78
|
+
removedAt?: string // null = currently inside; present in history view
|
|
79
|
+
metadata?: Record<string, any>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface CreateContainerInput {
|
|
83
|
+
containerType: string // required
|
|
84
|
+
ref?: string
|
|
85
|
+
name?: string
|
|
86
|
+
description?: string
|
|
87
|
+
status?: ContainerStatus
|
|
88
|
+
metadata?: Record<string, any>
|
|
89
|
+
parentContainerId?: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface UpdateContainerInput {
|
|
93
|
+
containerType?: string
|
|
94
|
+
ref?: string
|
|
95
|
+
name?: string
|
|
96
|
+
description?: string
|
|
97
|
+
status?: ContainerStatus
|
|
98
|
+
metadata?: Record<string, any>
|
|
99
|
+
parentContainerId?: string | null // null promotes to top-level
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface AddContainerItemsInput {
|
|
103
|
+
items: Array<{
|
|
104
|
+
itemType: ContainerItemType // required
|
|
105
|
+
itemId: string // required
|
|
106
|
+
productId?: string
|
|
107
|
+
proofId?: string
|
|
108
|
+
metadata?: Record<string, any>
|
|
109
|
+
}>
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface RemoveContainerItemsInput {
|
|
113
|
+
ids: string[] // ContainerItem UUIDs to soft-remove
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## SDK Usage
|
|
120
|
+
|
|
121
|
+
Import via the top-level SDK export:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
import { containers } from '@proveanything/smartlinks'
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Admin — Containers
|
|
128
|
+
|
|
129
|
+
#### Create a container
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
const cask = await containers.create('coll_123', {
|
|
133
|
+
containerType: 'cask',
|
|
134
|
+
ref: 'CASK-0042',
|
|
135
|
+
name: 'Cask 42 — Single Malt',
|
|
136
|
+
metadata: { distilleryYear: 2019, capacityLitres: 200 },
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### List containers
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// All active pallets
|
|
144
|
+
const { containers: pallets } = await containers.list('coll_123', {
|
|
145
|
+
containerType: 'pallet',
|
|
146
|
+
status: 'active',
|
|
147
|
+
limit: 50,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// Top-level containers only (no parent)
|
|
151
|
+
const { containers: roots } = await containers.list('coll_123', { topLevel: true })
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### Get a container — flat, tree, or with contents
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// Flat (default)
|
|
158
|
+
const cask = await containers.get('coll_123', 'cask-uuid')
|
|
159
|
+
|
|
160
|
+
// Full hierarchy tree (3 levels deep) with current contents
|
|
161
|
+
const tree = await containers.get('coll_123', 'warehouse-uuid', {
|
|
162
|
+
tree: true,
|
|
163
|
+
treeDepth: 3,
|
|
164
|
+
includeContents: true,
|
|
165
|
+
})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### Find containers currently holding an item
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
const { containers: holding } = await containers.findForItem('coll_123', {
|
|
172
|
+
itemType: 'proof',
|
|
173
|
+
itemId: 'proof-uuid',
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### Update a container
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const updated = await containers.update('coll_123', 'cask-uuid', {
|
|
181
|
+
status: 'archived',
|
|
182
|
+
metadata: { bottledAt: '2025-04-01' },
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// Promote to top-level (remove parent)
|
|
186
|
+
await containers.update('coll_123', 'cask-uuid', { parentContainerId: null })
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### Delete (soft) a container
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
await containers.remove('coll_123', 'cask-uuid')
|
|
193
|
+
// deletedAt is set; record remains queryable by admins
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Admin — Item Membership
|
|
197
|
+
|
|
198
|
+
#### List current contents
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
const { items } = await containers.listItems('coll_123', 'pallet-uuid')
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### List full membership history (including removed items)
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
const { items: history } = await containers.listItems('coll_123', 'pallet-uuid', {
|
|
208
|
+
history: true,
|
|
209
|
+
})
|
|
210
|
+
// history includes items with removedAt !== null
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
#### Add items to a container
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
const { items } = await containers.addItems('coll_123', 'pallet-uuid', {
|
|
217
|
+
items: [
|
|
218
|
+
{ itemType: 'tag', itemId: 'NFC-00AABBCC' },
|
|
219
|
+
{ itemType: 'proof', itemId: 'proof-uuid', productId: 'product-id' },
|
|
220
|
+
{ itemType: 'container', itemId: 'inner-crate-uuid' },
|
|
221
|
+
],
|
|
222
|
+
})
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
#### Remove items from a container (soft)
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
const result = await containers.removeItems('coll_123', 'pallet-uuid', {
|
|
229
|
+
ids: ['container-item-uuid-1', 'container-item-uuid-2'],
|
|
230
|
+
})
|
|
231
|
+
console.log(`Removed ${result.removedCount} items`)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Public — Read-Only
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
// List (excludes soft-deleted and non-public containers)
|
|
238
|
+
const { containers: list } = await containers.publicList('coll_123', {
|
|
239
|
+
containerType: 'cask',
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
// Get with tree
|
|
243
|
+
const tree = await containers.publicGet('coll_123', 'warehouse-uuid', {
|
|
244
|
+
tree: true,
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// Current contents only (no history on public side)
|
|
248
|
+
const { items } = await containers.publicListItems('coll_123', 'cask-uuid')
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## REST API Reference
|
|
254
|
+
|
|
255
|
+
### Admin endpoints
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
POST /api/v1/admin/collection/:collectionId/containers
|
|
259
|
+
GET /api/v1/admin/collection/:collectionId/containers
|
|
260
|
+
GET /api/v1/admin/collection/:collectionId/containers/find-for-item
|
|
261
|
+
GET /api/v1/admin/collection/:collectionId/containers/:containerId
|
|
262
|
+
PATCH /api/v1/admin/collection/:collectionId/containers/:containerId
|
|
263
|
+
DELETE /api/v1/admin/collection/:collectionId/containers/:containerId
|
|
264
|
+
GET /api/v1/admin/collection/:collectionId/containers/:containerId/items
|
|
265
|
+
POST /api/v1/admin/collection/:collectionId/containers/:containerId/items
|
|
266
|
+
DELETE /api/v1/admin/collection/:collectionId/containers/:containerId/items
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
#### GET /containers query parameters
|
|
270
|
+
|
|
271
|
+
| Parameter | Description |
|
|
272
|
+
|---|---|
|
|
273
|
+
| `containerType` | Filter by type string |
|
|
274
|
+
| `status` | Filter by status |
|
|
275
|
+
| `ref` | Filter by reference |
|
|
276
|
+
| `parentContainerId` | Filter by parent UUID |
|
|
277
|
+
| `topLevel` | `true` to return only root containers |
|
|
278
|
+
| `limit` | Default `100` |
|
|
279
|
+
| `offset` | Default `0` |
|
|
280
|
+
|
|
281
|
+
#### GET /containers/:id query parameters
|
|
282
|
+
|
|
283
|
+
| Parameter | Description |
|
|
284
|
+
|---|---|
|
|
285
|
+
| `tree` | `true` to recursively embed child containers |
|
|
286
|
+
| `treeDepth` | Max nesting depth (default: unlimited) |
|
|
287
|
+
| `includeContents` | `true` to embed current items |
|
|
288
|
+
|
|
289
|
+
#### GET /containers/:id/items query parameters
|
|
290
|
+
|
|
291
|
+
| Parameter | Description |
|
|
292
|
+
|---|---|
|
|
293
|
+
| `history` | `true` to include removed items |
|
|
294
|
+
| `limit` | Default `100` |
|
|
295
|
+
| `offset` | Default `0` |
|
|
296
|
+
|
|
297
|
+
#### POST /containers/:id/items body
|
|
298
|
+
|
|
299
|
+
```json
|
|
300
|
+
{
|
|
301
|
+
"items": [
|
|
302
|
+
{ "itemType": "tag", "itemId": "NFC-00AABBCC" },
|
|
303
|
+
{ "itemType": "proof", "itemId": "proof-uuid", "productId": "product-id" }
|
|
304
|
+
]
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
#### DELETE /containers/:id/items body
|
|
309
|
+
|
|
310
|
+
```json
|
|
311
|
+
{ "ids": ["container-item-uuid-1", "container-item-uuid-2"] }
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Public endpoints
|
|
315
|
+
|
|
316
|
+
```
|
|
317
|
+
GET /api/v1/public/collection/:collectionId/containers
|
|
318
|
+
GET /api/v1/public/collection/:collectionId/containers/:containerId
|
|
319
|
+
GET /api/v1/public/collection/:collectionId/containers/:containerId/items
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Same query parameters as admin (minus `history`). Soft-deleted containers and containers with `metadata.publicListing === false` are excluded from list results.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Attestations on Containers
|
|
327
|
+
|
|
328
|
+
Attestations (sensor readings, condition reports, etc.) are managed via the `attestations` namespace. The admin router does **not** expose attestation sub-routes — use the standalone `/admin/.../attestations` router for all admin attestation access.
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
import { attestations } from '@proveanything/smartlinks'
|
|
332
|
+
|
|
333
|
+
// Record a temperature reading against a cask
|
|
334
|
+
await attestations.create('coll_123', {
|
|
335
|
+
subjectType: 'container',
|
|
336
|
+
subjectId: 'cask-uuid',
|
|
337
|
+
attestationType: 'temperature',
|
|
338
|
+
recordedAt: new Date().toISOString(),
|
|
339
|
+
value: { celsius: 12.4 },
|
|
340
|
+
unit: '°C',
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
// Latest readings on all attestation types
|
|
344
|
+
const { latest } = await attestations.latest('coll_123', {
|
|
345
|
+
subjectType: 'container',
|
|
346
|
+
subjectId: 'cask-uuid',
|
|
347
|
+
})
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Public shortcuts pre-scoped to a container are also available:
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
const { latest } = await attestations.publicContainerLatest('coll_123', 'cask-uuid')
|
|
354
|
+
const { summary } = await attestations.publicContainerSummary('coll_123', 'cask-uuid', {
|
|
355
|
+
attestationType: 'temperature',
|
|
356
|
+
valueField: 'celsius',
|
|
357
|
+
groupBy: 'hour',
|
|
358
|
+
})
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Cold-Chain Tracking Example
|
|
364
|
+
|
|
365
|
+
**Goal:** determine whether a refrigerated item was inside a fridge when the temperature exceeded a threshold.
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
import { containers, attestations } from '@proveanything/smartlinks'
|
|
369
|
+
|
|
370
|
+
const collectionId = 'coll_123'
|
|
371
|
+
const fridgeId = 'fridge-uuid'
|
|
372
|
+
const proofId = 'bottle-proof-uuid'
|
|
373
|
+
const threshold = 8 // °C
|
|
374
|
+
|
|
375
|
+
// 1. Get the full item membership history for the fridge
|
|
376
|
+
const { items: history } = await containers.listItems(collectionId, fridgeId, {
|
|
377
|
+
history: true,
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
// 2. Find the window(s) when our bottle was inside the fridge
|
|
381
|
+
const bottleIntervals = history
|
|
382
|
+
.filter(item => item.itemType === 'proof' && item.itemId === proofId)
|
|
383
|
+
.map(item => ({
|
|
384
|
+
addedAt: new Date(item.addedAt),
|
|
385
|
+
removedAt: item.removedAt ? new Date(item.removedAt) : new Date(),
|
|
386
|
+
}))
|
|
387
|
+
|
|
388
|
+
// 3. Get temperature attestations for the fridge
|
|
389
|
+
const { attestations: temps } = await attestations.list(collectionId, {
|
|
390
|
+
subjectType: 'container',
|
|
391
|
+
subjectId: fridgeId,
|
|
392
|
+
attestationType: 'temperature',
|
|
393
|
+
limit: 1000,
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
// 4. Cross-reference
|
|
397
|
+
const excursions = temps.filter(a => {
|
|
398
|
+
const celsius = a.value?.celsius as number
|
|
399
|
+
const recorded = new Date(a.recordedAt)
|
|
400
|
+
if (celsius <= threshold) return false
|
|
401
|
+
return bottleIntervals.some(
|
|
402
|
+
({ addedAt, removedAt }) => recorded >= addedAt && recorded <= removedAt
|
|
403
|
+
)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
if (excursions.length > 0) {
|
|
407
|
+
console.warn(
|
|
408
|
+
`⚠️ ${excursions.length} temperature excursion(s) recorded while the bottle was in the fridge.`
|
|
409
|
+
)
|
|
410
|
+
} else {
|
|
411
|
+
console.log('✅ No temperature excursions during the bottle\'s time in the fridge.')
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## Design Notes
|
|
418
|
+
|
|
419
|
+
### Soft-delete
|
|
420
|
+
|
|
421
|
+
`DELETE /containers/:id` only sets `deletedAt`. The container and its full item history remain queryable by admins. The public API automatically excludes deleted containers from all responses.
|
|
422
|
+
|
|
423
|
+
### Membership history
|
|
424
|
+
|
|
425
|
+
`ContainerItem` records are never deleted. When an item is removed, `removedAt` is set. This means you always have a full audit trail of what was in a container and when, which is essential for provenance and compliance use-cases.
|
|
426
|
+
|
|
427
|
+
### Public visibility
|
|
428
|
+
|
|
429
|
+
Containers with `metadata.publicListing === false` are excluded from public list endpoints but remain accessible by direct ID if the caller knows the UUID. Use this to hide containers from general browsing while still allowing deep-link access.
|
|
430
|
+
|
|
431
|
+
### Nesting
|
|
432
|
+
|
|
433
|
+
Containers can be nested arbitrarily. A container with `containerType='container'` in the `items` list is a child container. The `findForItem` endpoint performs a flat lookup across all containers — it does not traverse the hierarchy.
|
|
434
|
+
|
|
435
|
+
### Attestation ownership for containers
|
|
436
|
+
|
|
437
|
+
Owner elevation on attestation public endpoints resolves ownership via `container.metadata.proofId`. If you want individual users to receive owner-tier attestation data for a container, set `metadata.proofId` to a proof they own.
|
|
@@ -32,7 +32,7 @@ Deep-linkable states come from **two sources** depending on their nature:
|
|
|
32
32
|
|
|
33
33
|
| Source | What goes here | When it changes |
|
|
34
34
|
|--------|---------------|-----------------|
|
|
35
|
-
| **App manifest** (`app.
|
|
35
|
+
| **App manifest** (`app.manifest.json`) | Fixed routes built into the app | Only when the app itself is updated |
|
|
36
36
|
| **App config** (`appConfig.linkable`) | Content-driven entries that vary by collection | When admins create, remove, or rename content |
|
|
37
37
|
|
|
38
38
|
Consumers merge both sources to get the full set of navigable states for an app.
|
|
@@ -40,7 +40,7 @@ Consumers merge both sources to get the full set of navigable states for an app.
|
|
|
40
40
|
```text
|
|
41
41
|
┌─────────────────────────────────────────────────────────────────────────┐
|
|
42
42
|
│ App Definition (build time) │
|
|
43
|
-
│ app.
|
|
43
|
+
│ app.manifest.json → "linkable": [ │
|
|
44
44
|
│ { "title": "Gallery", "path": "/gallery" }, │
|
|
45
45
|
│ { "title": "Settings", "path": "/settings" } │
|
|
46
46
|
│ ] │
|
|
@@ -74,7 +74,7 @@ Consumers merge both sources to get the full set of navigable states for an app.
|
|
|
74
74
|
|
|
75
75
|
### Static Links — App Manifest
|
|
76
76
|
|
|
77
|
-
Static links are routes that are **built into the app itself** — they exist regardless of what content admins have created. They belong in `app.
|
|
77
|
+
Static links are routes that are **built into the app itself** — they exist regardless of what content admins have created. They belong in `app.manifest.json` as a top-level `linkable` array, as a peer to `widgets` and `containers`:
|
|
78
78
|
|
|
79
79
|
```json
|
|
80
80
|
{
|
|
@@ -172,7 +172,7 @@ interface DeepLinkEntry {
|
|
|
172
172
|
|
|
173
173
|
**An app with both static routes and dynamic content pages:**
|
|
174
174
|
|
|
175
|
-
`app.
|
|
175
|
+
`app.manifest.json` — declare static routes once, at build time:
|
|
176
176
|
```json
|
|
177
177
|
{
|
|
178
178
|
"linkable": [
|
|
@@ -441,7 +441,7 @@ if (entry) {
|
|
|
441
441
|
```typescript
|
|
442
442
|
/**
|
|
443
443
|
* A single navigable state exposed by a SmartLinks app.
|
|
444
|
-
* Used in both app.
|
|
444
|
+
* Used in both app.manifest.json `linkable` (static) and appConfig `linkable` (dynamic).
|
|
445
445
|
*/
|
|
446
446
|
export interface DeepLinkEntry {
|
|
447
447
|
/** Human-readable label shown in menus and offered to AI agents */
|
|
@@ -469,7 +469,7 @@ export type DeepLinkRegistry = DeepLinkEntry[];
|
|
|
469
469
|
|
|
470
470
|
### Rules
|
|
471
471
|
|
|
472
|
-
1. **Static routes belong in the manifest** — if a route exists regardless of content, declare it in `app.
|
|
472
|
+
1. **Static routes belong in the manifest** — if a route exists regardless of content, declare it in `app.manifest.json`. Do not write it to `appConfig` on first run.
|
|
473
473
|
2. **`appConfig.linkable` is for dynamic content only** — it should contain entries generated from, and varying with, the app's data.
|
|
474
474
|
3. **`linkable` is reserved** — do not use this key for other purposes in either the manifest or `appConfig`.
|
|
475
475
|
4. **No platform context params in entries** — `collectionId`, `appId`, `productId`, `proofId`, `lang`, `theme` are injected by the platform automatically.
|