@proveanything/smartlinks 1.11.0 → 1.11.2

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.11.0 | Generated: 2026-04-30T11:35:44.821Z
3
+ Version: 1.11.2 | Generated: 2026-04-30T13:11:24.914Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -103,8 +103,8 @@ The manifest is loaded automatically by the platform for every collection page.
103
103
  {
104
104
  "name": "WarehousePickContainer",
105
105
  "description": "In-field operator admin surface.",
106
- "audience": "admin-mobile",
107
- "capabilities": ["nfc", "qr", "offline-queue"]
106
+ "capabilities": ["nfc", "qr"],
107
+ "offline": true
108
108
  }
109
109
  ]
110
110
  },
@@ -117,14 +117,18 @@ The manifest is loaded automatically by the platform for every collection page.
117
117
 
118
118
  "records": {
119
119
  "nutrition": {
120
- "scopes": ["product", "facet", "batch"],
121
- "defaultScope": "facet",
122
- "label": "Nutrition info"
120
+ "label": "Nutrition info",
121
+ "cardinality": "singleton",
122
+ "allowFacetRules": true,
123
+ "scopes": ["collection", "rule", "product", "facet", "batch"],
124
+ "defaultScope": "product"
123
125
  },
124
126
  "cooking_steps": {
125
- "scopes": ["product", "facet"],
126
- "defaultScope": "product",
127
- "label": "Cooking steps"
127
+ "label": "Cooking steps",
128
+ "cardinality": "singleton",
129
+ "allowFacetRules": false,
130
+ "scopes": ["collection", "product"],
131
+ "defaultScope": "product"
128
132
  }
129
133
  }
130
134
  }
@@ -236,8 +240,8 @@ See [mobile-admin-container.md](mobile-admin-container.md) for the `AdminMobileH
236
240
  {
237
241
  "name": "WarehousePickContainer",
238
242
  "description": "Pick orders by scanning NFC tags",
239
- "audience": "admin-mobile",
240
- "capabilities": ["nfc", "qr", "offline-queue"]
243
+ "capabilities": ["nfc", "qr"],
244
+ "offline": true
241
245
  }
242
246
  ]
243
247
  }
@@ -250,8 +254,8 @@ See [mobile-admin-container.md](mobile-admin-container.md) for the `AdminMobileH
250
254
  | `files.css` | CSS bundle path — set to `null` if no styles |
251
255
  | `components[].name` | Exported component name (must match the UMD bundle export) |
252
256
  | `components[].description` | Shown in the mobile launcher's app picker |
253
- | `components[].audience` | Must be `"admin-mobile"` |
254
- | `components[].capabilities` | Hardware/feature capabilities this component needs or can use. See [capability list](mobile-admin-container.md#hardware-capabilities--the-capability-matrix). |
257
+ | `components[].capabilities` | Hardware capabilities this component needs or can use. See [capability list](mobile-admin-container.md#hardware-capabilities--the-capability-matrix). |
258
+ | `components[].offline` | Set to `true` if this component queues writes locally and needs offline sync support. |
255
259
  #### `linkable`
256
260
 
257
261
  Static deep-linkable states built into the app — fixed routes that exist regardless of per-collection content. Declared once at build time.
@@ -268,25 +272,29 @@ See the [Deep Link Discovery guide](deep-link-discovery.md) for the full dual-so
268
272
 
269
273
  Declares which `app.records` record types the app stores, and which scopes each type supports. Required for any app that follows the [Records-Based Admin Pattern](records-admin-pattern.md). Omit if the app does not use scoped records.
270
274
 
271
- The platform and the `<RecordsAdminShell>` from `@proveanything/ui-utils` read this block to render only the relevant tabs and affordances.
275
+ The platform and the `<RecordsAdminShell>` from `@proveanything/smartlinks-utils-ui` read this block to render the right scope tabs, rule editor, and cardinality-appropriate right pane.
272
276
 
273
277
  ```json
274
278
  "records": {
275
279
  "<recordType>": {
276
- "scopes": ["product", "facet", "batch"],
277
- "defaultScope": "facet",
278
- "label": "Human-readable label"
280
+ "label": "Human-readable label",
281
+ "cardinality": "singleton",
282
+ "allowFacetRules": false,
283
+ "scopes": ["collection", "product", "variant", "batch", "facet"],
284
+ "defaultScope": "product"
279
285
  }
280
286
  }
281
287
  ```
282
288
 
283
- | Field | Type | Required | Description |
284
- |----------------|----------|----------|-------------|
285
- | `scopes` | string[] | | Allowed scope kinds in resolution order. Valid values: `"product"`, `"variant"`, `"batch"`, `"facet"`, `"proof"`, `"default"`. |
286
- | `defaultScope` | string | | The scope the "Create new" button targets in the admin shell. Must be one of the declared `scopes`. |
287
- | `label` | string | | Human-readable label for the record type, used in headings and tabs. |
289
+ | Field | Type | Default | Description |
290
+ |-------------------|----------|---------|-------------|
291
+ | `label` | string | | Human-readable label for the record type, used in headings and tabs. |
292
+ | `cardinality` | string | `'singleton'` | `'singleton'` — one record wins per scope (e.g. ingredients, nutrition). `'collection'` every matching record is returned in resolution order (e.g. FAQs, recipes). Drives which hook to use on the public side (`useResolvedRecord` vs `useCollectedRecords`) and how the shell lays out the right pane. |
293
+ | `allowFacetRules` | boolean | `false` | When `true`, the shell renders a **Rule** scope tab and embeds `<FacetRuleEditor>`. Add `'rule'` to `scopes` when setting this. |
294
+ | `scopes` | string[] | — | Allowed scope kinds in resolution order. Valid values: `"collection"`, `"product"`, `"variant"`, `"batch"`, `"facet"`, `"proof"`, `"rule"`. `'rule'` is a synthetic scope holding `facetRule`-targeted records. `'collection'` replaces the legacy empty-ref catch-all — **there is no `'global'` scope**. |
295
+ | `defaultScope` | string | — | The scope the "Create new" button targets in the admin shell. Must be one of the declared `scopes`. |
288
296
 
289
- An app may declare multiple record types under different keys (e.g. `"nutrition"` and `"cooking_steps"`).
297
+ An app may declare multiple record types under different keys (e.g. `"nutrition"` and `"cooking_steps"`). See [records-admin-pattern.md](records-admin-pattern.md) for the full admin + public pattern.
290
298
 
291
299
  #### `executor`
292
300
 
@@ -495,7 +495,7 @@ The `ref` field is derived automatically from anchor fields when omitted:
495
495
  ```
496
496
  productId: 'prod_abc' → ref: 'product:prod_abc'
497
497
  productId: 'prod_abc', variantId: 'var_x' → ref: 'product:prod_abc/variant:var_x'
498
- (no anchor fields) → ref: '' (universal)
498
+ (no anchor fields) → ref: '' (collection-level catch-all)
499
499
  facetRule: { ... } → ref: 'rule:<ulid>'
500
500
  ```
501
501
 
@@ -513,6 +513,20 @@ When multiple scoped records match a context, they are ordered by `specificity`.
513
513
  | Per `anyOf` value | +1 |
514
514
  | No anchors / no rule | 0 |
515
515
 
516
+ ### Resolution order
517
+
518
+ When the public side of a records-based app needs "the data that applies to this product context", the platform walks a canonical chain from most-specific to least-specific:
519
+
520
+ ```
521
+ proof → batch → variant → product → rule(*) → facet(*) → collection
522
+ ```
523
+
524
+ - `rule(*)` — `facetRule`-targeted records are scored by **specificity** (number of clauses + number of constrained values). The most specific rule wins at its tier.
525
+ - `facet(*)` — legacy single-facet anchors, walked alphabetically. Prefer `facetRule` for new work.
526
+ - `collection` — the top of the chain and the catch-all for any record with no anchor fields. **There is no `'global'` tier above collection.**
527
+
528
+ For a **singleton** record type (one answer wins), use `useResolvedRecord` — it performs this walk server-side and returns the first match plus a `matchedAt` tag. For a **collection** record type (every match is shown), use `useCollectedRecords`. See [records-admin-pattern.md §2](records-admin-pattern.md#2-resolution-order-one-canonical-chain) for the full guide.
529
+
516
530
  ### Singleton Cardinality
517
531
 
518
532
  By default, `create` always inserts a new row — calling it twice produces two records with identical anchor fields. **Singleton cardinality** changes that: pass `singletonPer` on creation and the server will **upsert** instead, ensuring at most one record of a given `recordType` exists per scope boundary.
@@ -592,12 +606,11 @@ for (const entry of data) {
592
606
  case 'product': /* "Inherited from product" */ break;
593
607
  case 'facet': /* "Tier-specific" */ break;
594
608
  case 'collection': /* "Collection default" */ break;
595
- case 'universal': /* "Default" */ break;
596
609
  }
597
610
  }
598
611
  ```
599
612
 
600
- Precedence follows: `rule > proof > batch > variant > product > facet > collection > universal`.
613
+ Precedence follows: `proof > batch > variant > product > rule > facet > collection`. There is no scope above `collection` — a record with no anchor fields is a collection-level catch-all.
601
614
 
602
615
  #### React — `useResolvedRecord`
603
616
 
@@ -806,7 +819,7 @@ Examples:
806
819
  | `productId: 'prod_abc', variantId: 'var_500ml'` | `product:prod_abc/variant:var_500ml` |
807
820
  | `batchId: 'batch_q1'` | `batch:batch_q1` |
808
821
  | `facetRule: { ... }` | `rule:<ulid>` |
809
- | *(no anchor fields)* | `''` (universal) |
822
+ | *(no anchor fields)* | `''` (collection-level catch-all) |
810
823
 
811
824
  `parseRef` / `buildRef` in `data/refs.ts` should be used for **display and URL round-tripping only**, never as upsert keys. For ETL use cases, set an explicit `ref` using a stable external key (see [External ID / ETL Workflow](#external-id--etl-workflow)).
812
825
 
@@ -16,12 +16,13 @@ This document describes how to build a **Mobile Admin Container** — a SmartLin
16
16
  4. [Hardware Capabilities & the Capability Matrix](#hardware-capabilities--the-capability-matrix)
17
17
  5. [Capacitor Plugin Baseline](#capacitor-plugin-baseline)
18
18
  6. [Manifest Declaration](#manifest-declaration)
19
- 7. [Event Stream](#event-stream)
20
- 8. [Error Handling](#error-handling)
21
- 9. [Lifecycle](#lifecycle)
22
- 10. [Build & Bundle Requirements](#build--bundle-requirements)
23
- 11. [Example: Minimal Mobile Admin Container](#example-minimal-mobile-admin-container)
24
- 12. [Best Practices](#best-practices)
19
+ 7. [Launcher Discovery](#launcher-discovery)
20
+ 8. [Event Stream](#event-stream)
21
+ 9. [Error Handling](#error-handling)
22
+ 10. [Lifecycle](#lifecycle)
23
+ 11. [Build & Bundle Requirements](#build--bundle-requirements)
24
+ 12. [Example: Minimal Mobile Admin Container](#example-minimal-mobile-admin-container)
25
+ 13. [Best Practices](#best-practices)
25
26
 
26
27
  ---
27
28
 
@@ -57,6 +58,8 @@ Your container **never** detects the host directly. It receives a `host` prop fr
57
58
 
58
59
  Every container mounted by the SmartLinks Mobile launcher receives a single `host` prop — `AdminMobileHostContext`. Do not reach for `window.SmartlinksScanner` or `window.Capacitor` directly; both are wrapped here.
59
60
 
61
+ > **SDK export** — `AdminMobileHostContext`, `AdminMobileCapability`, `AdminMobileHostId`, `AdminMobileEvent`, `ScannerEventSubscriber`, `MobileAdminComponentManifest`, and `MobileAdminBundleManifest` are all exported from `@proveanything/smartlinks`. Import via `import type { AdminMobileHostContext } from '@proveanything/smartlinks'` — no local mirror needed.
62
+
60
63
  ```typescript
61
64
  interface AdminMobileHostContext {
62
65
  // Identity
@@ -106,7 +109,8 @@ interface AdminMobileHostContext {
106
109
  network: { isOnline: () => boolean }
107
110
  device: { info: () => Promise<{ model: string; platform: string }> }
108
111
 
109
- // Host context version — for future feature-detection
112
+ // Informational host version — use for logging/diagnostics, not feature detection.
113
+ // For feature detection prefer existence checks: 'requestNfcTap' in host.actions
110
114
  _version: number
111
115
  }
112
116
  ```
@@ -114,7 +118,15 @@ interface AdminMobileHostContext {
114
118
  ### Contract rules
115
119
 
116
120
  1. **Check `host.hardware.X` before calling `host.actions.X`.** Calls to unavailable capabilities reject with `HostCapabilityUnavailableError`.
117
- 2. **Do not call `initializeApi`.** `host.SL` is already configured with the right `baseURL`, auth, and logger.
121
+ 2. **Do not call `initializeApi`, and do not use top-level SDK imports for API calls.** `host.SL` is already configured with the right `baseURL`, auth, and logger. Code that does `import * as SL from "@proveanything/smartlinks"` and calls `SL.attestation.create(...)` will silently hit the wrong base URL — the top-level import is uninitialised in the launcher's runtime and produces no loud error. Always go through `host.SL`:
122
+ ```typescript
123
+ // ❌ Silent footgun — uninitialised SDK, wrong baseURL
124
+ import * as SL from '@proveanything/smartlinks'
125
+ await SL.attestation.create(...)
126
+
127
+ // ✅ Correct
128
+ await host.SL.attestation.create(...)
129
+ ```
118
130
  3. **Do not access `window.SmartlinksScanner` or `window.Capacitor` directly.** The host wraps both; direct access produces containers that only work on one shell.
119
131
  4. **Externalise all shared deps** (React, ReactDOM, SL, Radix, etc.) — the launcher provides them as globals. See [Build & Bundle Requirements](#build--bundle-requirements).
120
132
 
@@ -129,6 +141,36 @@ if (host.hardware.nfc) {
129
141
  }
130
142
  ```
131
143
 
144
+ ### `host.ui` — native helpers vs. your own components
145
+
146
+ `host.ui.setHeaderTitle()` and `host.ui.navigateBack()` are **host-only** — there is no in-container equivalent. Call them via `host.ui` or omit them.
147
+
148
+ `host.ui.toast()` and `host.ui.haptic()` are **optional conveniences**. Use them when you want native system feedback. When rendering in Storybook, unit tests, or a plain browser tab, your own `<Toaster />` is a perfectly valid substitute — you do not need to stub the entire `host.ui` surface just to get toast notifications.
149
+
150
+ **Stub pattern for testing and Storybook:**
151
+
152
+ ```typescript
153
+ const stubHost: Partial<AdminMobileHostContext> = {
154
+ hardware: { nfc: false, rfid: false, qr: true, camera: true, keyboard: false },
155
+ actions: {
156
+ requestQrScan: async () => 'STUB_QR_CODE',
157
+ requestNfcTap: async () => ({ uid: 'STUB_UID' }),
158
+ requestCameraPhoto: async () => new Blob(),
159
+ share: async () => {},
160
+ clipboard: { read: async () => '', write: async () => {} },
161
+ },
162
+ ui: {
163
+ toast: (opts) => console.log('[stub toast]', opts.title),
164
+ haptic: () => {},
165
+ setHeaderTitle: (t) => { if (t) document.title = t },
166
+ navigateBack: () => history.back(),
167
+ },
168
+ network: { isOnline: () => true },
169
+ user: { isAdmin: true, displayName: 'Test User', email: 'test@example.com' },
170
+ SL: undefined as any, // replace with your test SL instance
171
+ }
172
+ ```
173
+
132
174
  ---
133
175
 
134
176
  ## Hardware Capabilities & the Capability Matrix
@@ -148,7 +190,6 @@ Containers declare which capabilities they need (or can use) in the manifest `ca
148
190
  | `"camera"` | Photo capture, gallery picker |
149
191
  | `"keyboard"` | Physical hardware trigger/action buttons |
150
192
  | `"geolocation"` | GPS coordinates |
151
- | `"offline-queue"` | Container needs local write queuing + sync |
152
193
  | `"push"` | Remote push notifications (FCM/APNs) |
153
194
 
154
195
  The full hardware capability matrix by host:
@@ -196,7 +237,7 @@ The custom Kotlin shell and both Capacitor shells ship the same baseline plugin
196
237
  | `@capgo/capacitor-nfc` | `"nfc"` |
197
238
  | `@capacitor/geolocation` | `"geolocation"` |
198
239
  | `@capacitor/push-notifications` | `"push"` |
199
- | `@capacitor/filesystem` | *(declare `"offline-queue"` if needed)* |
240
+ | `@capacitor/filesystem` | *(used when `offline: true`; bundle it in)* |
200
241
 
201
242
  ### Tier 3 — custom-android only (not Capacitor)
202
243
 
@@ -231,8 +272,8 @@ Declare the bundle under the top-level `mobileAdmin` key in `app.manifest.json`.
231
272
  {
232
273
  "name": "WarehousePickContainer",
233
274
  "description": "Pick orders by scanning NFC tags",
234
- "audience": "admin-mobile",
235
- "capabilities": ["nfc", "qr", "offline-queue"]
275
+ "capabilities": ["nfc", "qr"],
276
+ "offline": true
236
277
  }
237
278
  ]
238
279
  }
@@ -247,8 +288,29 @@ A `mobileAdmin` bundle can export multiple components targeting different operat
247
288
  |-------|------|-------------|
248
289
  | `name` | string | Exported component name (must match the UMD bundle export) |
249
290
  | `description` | string | Shown in the mobile launcher's app picker |
250
- | `audience` | `"admin-mobile"` | Required tells the launcher this is an operator surface |
251
- | `capabilities` | string[] | Capabilities this component needs or can use. See table above. |
291
+ | `capabilities` | string[] | Hardware capabilities this component needs or can use. See table above. |
292
+ | `offline` | boolean | Set to `true` if this component queues writes locally and needs offline sync support. |
293
+
294
+ ---
295
+
296
+ ## Launcher Discovery
297
+
298
+ The SmartLinks Mobile launcher loads your `mobileAdmin` bundle through the platform's app registry — you do not reference the manifest URL directly.
299
+
300
+ **How it works:**
301
+
302
+ 1. A collection admin enables the app for a collection in the SmartLinks admin console.
303
+ 2. On startup (and periodically), the launcher fetches `app.manifest.json` for every enabled app in that collection via the SmartLinks platform API. These requests are authenticated with the launcher's operator session.
304
+ 3. The launcher reads `mobileAdmin.components[]`, evaluates each component's `capabilities` against the current device's hardware flags, and shows matching components in the app picker.
305
+ 4. When the operator opens a component, the launcher resolves `mobileAdmin.files.js.umd` against the app's CDN base, fetches the bundle, and mounts the component with its `host` prop.
306
+
307
+ **File URL resolution:** `files.js.umd` / `files.js.esm` are paths relative to your app's CDN root (set when the app version is published). Provide relative paths — the launcher resolves them; do not hardcode absolute URLs.
308
+
309
+ **Auth:** Manifest and bundle fetches use the launcher's session token. Your container is already authenticated via `host.user` and `host.SL` — do not add additional auth headers.
310
+
311
+ **On load failure:** If the bundle returns a non-2xx response, the launcher shows a user-visible error and logs it — it does not crash the full launcher. If `app.manifest.json` itself cannot be fetched, the app is silently excluded from the picker for that session.
312
+
313
+ > To debug a missing or stale manifest, confirm in the SmartLinks admin console that the app version is published and that the `mobileAdmin` key is present in the uploaded manifest.
252
314
 
253
315
  ---
254
316
 
@@ -318,7 +380,7 @@ try {
318
380
  |-------|------|------------|
319
381
  | mount | User opens your container | Subscribe to events, start readers |
320
382
  | unmount | User backs out / app backgrounded > 30s | Unsubscribe; persist in-flight state |
321
- | `lifecycle: 'offline'` | Network lost | Switch to offline-queue mode |
383
+ | `lifecycle: 'offline'` | Network lost | Switch to offline mode; queue writes locally |
322
384
  | `lifecycle: 'online'` | Network restored | Flush queued writes |
323
385
  | `lifecycle: 'pause'` | App backgrounded | Pause readers to save battery |
324
386
  | `lifecycle: 'resume'` | App foregrounded | Resubscribe; refresh stale data |
@@ -400,7 +462,7 @@ vite build --config vite.config.mobile-admin.ts
400
462
  ```typescript
401
463
  // src/mobile-admin/WarehousePickContainer.tsx
402
464
  import { useEffect, useState } from 'react'
403
- import type { AdminMobileHostContext } from '@/lib/admin-mobile-host-context'
465
+ import type { AdminMobileHostContext } from '@proveanything/smartlinks'
404
466
 
405
467
  interface Props {
406
468
  host: AdminMobileHostContext
@@ -459,9 +521,10 @@ export const MOBILE_ADMIN_MANIFEST = {
459
521
 
460
522
  - **Always check `host.hardware.X` before calling `host.actions.X`** — never assume a capability is available.
461
523
  - **Always wrap action calls in try/catch** — handle `HostPermissionDeniedError`, `HostTimeoutError`, and `HostCapabilityUnavailableError`.
462
- - **Use `host.ui.toast`, `host.ui.haptic`, `host.ui.setHeaderTitle`** instead of mounting your own UI chrome — these integrate with the launcher's native UI.
524
+ - **Use `host.ui.setHeaderTitle` and `host.ui.navigateBack`** for header integration — these are host-only and have no in-container equivalent.
525
+ - **`host.ui.toast` and `host.ui.haptic` are optional** — use them for native feedback, or fall back to your own `<Toaster />` when testing in isolation.
463
526
  - **Use `host.events.subscribe` for lifecycle events** — `'offline'`/`'online'`/`'pause'`/`'resume'` fire consistently on every host.
464
- - **Never call `initializeApi`** — `host.SL` is already configured.
527
+ - **Never call `initializeApi`, never use top-level SDK imports for API calls** — `host.SL` is already configured. `SL.method()` instead of `host.SL.method()` silently uses the wrong baseURL.
465
528
  - **Bundle Capacitor plugins in** (do not externalise) — so the component degrades gracefully on PWA/browser without crashing.
466
- - **Declare `offline-queue` in capabilities** if your container queues writes — this signals the launcher to provision local storage support.
467
- - **Use `host._version`** to feature-detect newer host context fields breaking changes bump the major version and the launcher gates incompatible combinations.
529
+ - **Declare `offline: true`** on a component if it queues writes locally — this signals the launcher to provision offline sync support.
530
+ - **Use `'methodName' in host.actions`** to feature-detect new host capabilities rather than comparing `host._version`. The version is informational only.