@proveanything/smartlinks 1.11.4 → 1.11.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.11.4 | Generated: 2026-04-30T13:59:47.020Z
3
+ Version: 1.11.6 | Generated: 2026-05-01T13:42:06.488Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -85,6 +85,7 @@ Zones are **automatically filtered** based on the caller's role:
85
85
  ### Zone Writing Rules
86
86
 
87
87
  - **Non-admin callers** attempting to write to the `admin` zone are silently ignored
88
+ - **Authenticated record owners** can write to `data` and `owner` by default; individual keys can be restricted via the `ownerEdit` app config policy (see [Owner Edit Policy](#owner-edit-policy) below)
88
89
  - **Public callers** can write to `data` and `owner` (if visibility allows)
89
90
  - **Admins** can write to all three zones
90
91
 
@@ -1098,6 +1099,61 @@ The `enforce` values are **merged over** the caller's request body, so you can l
1098
1099
 
1099
1100
  ---
1100
1101
 
1102
+ ## Owner Edit Policy
1103
+
1104
+ Gives per-zone, field-level control over what an **authenticated record owner** can update via `PATCH /api/v1/public/collection/:collectionId/app/:appId/records/:recordId`.
1105
+
1106
+ Set the policy in the same app config document used for `publicCreate` (stored at `sites/{collectionId}/apps/{appId}`):
1107
+
1108
+ ```json
1109
+ {
1110
+ "ownerEdit": {
1111
+ "records": {
1112
+ "data": { "allow": ["paypalEmail"] },
1113
+ "owner": { "allow": ["paypalEmail", "paypalEmailUpdatedAt"] }
1114
+ }
1115
+ }
1116
+ }
1117
+ ```
1118
+
1119
+ ### Zone visibility and write access
1120
+
1121
+ | Zone | Who can read | Who can write (owner) |
1122
+ |---------|------------------------|----------------------------------------------------------|
1123
+ | `data` | public | Allow-listed keys only (if policy set); all keys if not |
1124
+ | `owner` | owner + admin | Allow-listed keys only (if policy set); all keys if not |
1125
+ | `admin` | admin | Never — admin zone is always immutable to owners |
1126
+
1127
+ ### Allow-list semantics
1128
+
1129
+ | Config | Behaviour |
1130
+ |----------------------------|-------------------------------------------------------------------------------|
1131
+ | No `ownerEdit` key | Default-allow — both zones fully writable (no change to existing behaviour) |
1132
+ | `allow` array with keys | Only the listed keys are accepted from the PATCH body; the rest are silently ignored and their existing values preserved |
1133
+ | `allow: []` (empty array) | Zone is effectively read-only for the owner |
1134
+
1135
+ Accepted keys are **merged** onto the existing zone blob — you do not need to re-send unchanged values.
1136
+
1137
+ ### Example: commission record with protected fields
1138
+
1139
+ An app that lets owners update their payout email but not their commission total:
1140
+
1141
+ ```json
1142
+ {
1143
+ "ownerEdit": {
1144
+ "records": {
1145
+ "owner": { "allow": ["paypalEmail", "paypalEmailUpdatedAt"] }
1146
+ }
1147
+ }
1148
+ }
1149
+ ```
1150
+
1151
+ A PATCH body of `{ "owner": { "paypalEmail": "x@y.com", "totalCommission": 99 } }` will update `paypalEmail` only. `totalCommission` is silently ignored and its existing value is preserved.
1152
+
1153
+ > **App design note:** If your app creates records with sensitive fields that owners should never modify (e.g. computed totals, server-assigned fields), add an `ownerEdit` policy from the start. It is significantly easier to relax restrictions later than to tighten them after data has been mutated.
1154
+
1155
+ ---
1156
+
1101
1157
  ## Anonymous Edit Tokens
1102
1158
 
1103
1159
  Enables an anonymous caller to amend a record they just created — without authentication — by presenting a short-lived secret token.
@@ -4,7 +4,7 @@
4
4
 
5
5
  This document describes how to build a **Mobile Admin Container** — a SmartLinks microapp that provides an in-the-field operator/admin surface optimised for mobile devices. These containers ship as a **separate `mobileAdmin` bundle** (not inside the `containers` bundle) so that Capacitor plugins, offline helpers, and operator-only code never reach the public consumer bundle.
6
6
 
7
- > **See also:** [containers.md](containers.md) covers the public consumer container. The [Multiple Consumer Components](containers.md#multiple-consumer-components) section explains the consumer vs. admin bundle split.
7
+ > **See also:** [containers.md](containers.md) covers the public consumer container. The [Multiple Consumer Components](containers.md#multiple-consumer-components) section explains the consumer vs. admin bundle split. For the full `host.native` facade contract (share, clipboard, haptics, NFC, RFID, storage, etc.) see [native-facade.md](native-facade.md).
8
8
 
9
9
  ---
10
10
 
@@ -23,6 +23,7 @@ This document describes how to build a **Mobile Admin Container** — a SmartLin
23
23
  11. [Build & Bundle Requirements](#build--bundle-requirements)
24
24
  12. [Example: Minimal Mobile Admin Container](#example-minimal-mobile-admin-container)
25
25
  13. [Best Practices](#best-practices)
26
+ 14. [Native Capability Facade](#native-capability-facade)
26
27
 
27
28
  ---
28
29
 
@@ -58,7 +59,7 @@ Your container **never** detects the host directly. It receives a `host` prop fr
58
59
 
59
60
  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.
60
61
 
61
- > **SDK export** — `AdminMobileHostContext`, `AdminMobileCapability`, `ActionableCapability`, `AdminMobileHostId`, `AdminMobileEvent`, `AdminMobileEventCallback`, `AdminMobileEventSubscriber`, `AdminMobileComponentManifest`, and `AdminMobileBundleManifest` are all exported from `@proveanything/smartlinks`. Import via `import type { AdminMobileHostContext } from '@proveanything/smartlinks'` — no local mirror needed. `ScannerEventSubscriber`, `MobileAdminComponentManifest`, and `MobileAdminBundleManifest` still export as deprecated aliases.
62
+ > **SDK export** — `AdminMobileHostContext`, `AdminMobileCapability`, `ActionableCapability`, `AdminMobileHostId`, `AdminMobileEvent`, `AdminMobileEventCallback`, `AdminMobileEventSubscriber`, `AdminMobileComponentManifest`, `AdminMobileBundleManifest`, and `NativeFacade` (plus all sub-facade interfaces) are exported from `@proveanything/smartlinks`. Import via `import type { AdminMobileHostContext } from '@proveanything/smartlinks'` — no local mirror needed. `ScannerEventSubscriber`, `MobileAdminComponentManifest`, and `MobileAdminBundleManifest` still export as deprecated aliases.
62
63
 
63
64
  ```typescript
64
65
  interface AdminMobileHostContext {
@@ -112,6 +113,10 @@ interface AdminMobileHostContext {
112
113
  // Informational host version — use for logging/diagnostics, not feature detection.
113
114
  // For feature detection prefer existence checks: 'requestNfcTap' in host.actions
114
115
  _version: number
116
+
117
+ // Full native capability facade — share, clipboard, haptics, NFC, RFID, storage, etc.
118
+ // Optional: not every host stub populates it. See native-facade.md.
119
+ native?: NativeFacade
115
120
  }
116
121
  ```
117
122
 
@@ -542,3 +547,26 @@ export const MOBILE_ADMIN_MANIFEST = {
542
547
  - **Bundle Capacitor plugins in** (do not externalise) — so the component degrades gracefully on PWA/browser without crashing.
543
548
  - **Declare `offline: true`** on a component if it queues writes locally — this signals the launcher to provision offline sync support.
544
549
  - **Use `'methodName' in host.actions`** to feature-detect new host capabilities rather than comparing `host._version`. The version is informational only.
550
+
551
+ ---
552
+
553
+ ## Native Capability Facade
554
+
555
+ `host.native` gives containers access to a broader set of device capabilities — share sheet, clipboard, full haptics API, storage, QR, NFC, RFID (Kotlin only), auth, and cross-shell events — through a single interface that falls back gracefully across all host environments.
556
+
557
+ ```typescript
558
+ // Check host.hardware.* for physical availability, then call via host.native
559
+ if (host.hardware.nfc && host.native?.nfc) {
560
+ const { uid } = await host.native.nfc.read({ timeoutMs: 10_000 })
561
+ }
562
+
563
+ // Storage always available (Preferences → localStorage → in-memory Map)
564
+ await host.native?.storage.set('lastScan', uid)
565
+
566
+ // Share sheet with web fallback
567
+ await host.native?.share.share({ title: 'Found tag', url: `https://app.example/tags/${uid}` })
568
+ ```
569
+
570
+ `host.native` is optional on `AdminMobileHostContext` — host stubs (Storybook, unit tests) need not implement it. `native.rfid` is additionally optional within `NativeFacade` itself, as it only exists on `custom-android`.
571
+
572
+ For the full sub-facade table, fallback chains, and what's intentionally not wrapped, see **[native-facade.md](native-facade.md)**.
@@ -0,0 +1,170 @@
1
+ # Native Capability Facade (`host.native` / `SL.native`)
2
+
3
+ > **Version:** 1.12 · **Platform:** SmartLinks R4 · **Last updated:** 2026-04-30
4
+
5
+ The `NativeFacade` is a thin contract layer between microapps and the device capabilities available on the current host shell (Kotlin, Capacitor iOS/Android, PWA, or browser). It lets a microapp call `host.native.share.share({...})` without knowing whether it's running over `window.SmartlinksScanner`, a Capacitor plugin, or `navigator.share`.
6
+
7
+ **The SDK owns the contract** (`src/native/types.ts`, re-exported from `@proveanything/smartlinks`). Each host implementation is responsible for wiring up the sub-facades and populating:
8
+ - `AdminMobileHostContext.native` (container prop, typed in the SDK), and
9
+ - `window.SL.native` (for UMD microapps that don't receive a host prop — host concern, not SDK).
10
+
11
+ ---
12
+
13
+ ## Table of Contents
14
+
15
+ 1. [Access patterns](#access-patterns)
16
+ 2. [What's in the facade](#whats-in-the-facade)
17
+ 3. [What's NOT in the facade](#whats-not-in-the-facade)
18
+ 4. [Error contract](#error-contract)
19
+ 5. [Type reference](#type-reference)
20
+
21
+ ---
22
+
23
+ ## Access patterns
24
+
25
+ ### Inside a Mobile Admin Container (recommended)
26
+
27
+ ```typescript
28
+ import type { AdminMobileHostContext } from '@proveanything/smartlinks'
29
+
30
+ function MyContainer({ host }: { host: AdminMobileHostContext }) {
31
+ const handleShare = async () => {
32
+ await host.native?.share.share({ title: 'SmartLinks', url: window.location.href })
33
+ }
34
+
35
+ const handleScan = async () => {
36
+ // Only present when the host provides a QR reader
37
+ const code = await host.native?.qr.scan()
38
+ if (code) processCode(code)
39
+ }
40
+ }
41
+ ```
42
+
43
+ Always call via `host.native?.<facade>.<method>()` — `native` is optional because not every host exposes the full surface (e.g. a minimal browser stub may omit `rfid`).
44
+
45
+ For hardware-gated capabilities (NFC, RFID, QR, camera) also check `host.capabilities` or `host.hardware.*` before calling, as those flags reflect physical availability on the current device:
46
+
47
+ ```typescript
48
+ if (host.hardware.nfc && host.native?.nfc) {
49
+ const { uid } = await host.native.nfc.read({ timeoutMs: 10_000 })
50
+ }
51
+ ```
52
+
53
+ ### Inside a UMD microapp (no host prop)
54
+
55
+ ```typescript
56
+ // window.SL.native is populated by the host at boot — host concern, not SDK
57
+ const native = (window as any).SL?.native as import('@proveanything/smartlinks').NativeFacade | undefined
58
+ const code = await native?.qr.scan()
59
+ ```
60
+
61
+ ---
62
+
63
+ ## What's in the facade
64
+
65
+ These capabilities have meaningful fallbacks across Kotlin / Capacitor / web. Always go through the facade.
66
+
67
+ | Sub-facade | Key methods | Fallback chain |
68
+ |---|---|---|
69
+ | `native.share` | `share({ title, url, text? })`, `canShare()` | Capacitor Share → `navigator.share` → copy to clipboard + toast |
70
+ | `native.clipboard` | `write(text)`, `read()` | Capacitor Clipboard → `navigator.clipboard` → `execCommand('copy')` |
71
+ | `native.haptics` | `impact(style?)`, `notification(style)`, `selection()` | Capacitor Haptics → `navigator.vibrate` → no-op |
72
+ | `native.network` | `getStatus()`, `addListener('change', cb)` | Capacitor Network → `navigator.onLine` + `'online'`/`'offline'` events |
73
+ | `native.device` | `getInfo()`, `getId()`, `getLanguageCode()` | Capacitor Device → UA parsing + cached UUID in `localStorage` |
74
+ | `native.storage` | `get(key)`, `set(key, value)`, `remove(key)`, `keys()` | Capacitor Preferences → `localStorage` → in-memory `Map` (private mode) |
75
+ | `native.qr` | `scan(opts?)` | Capacitor MLKit Barcode → html5-qrcode / `BarcodeDetector` |
76
+ | `native.auth` | `signInWithGoogle()`, `signOut()` | Kotlin bridge → Capacitor Browser + OAuth → web redirect |
77
+ | `native.nfc` | `read(opts?)`, `writeNdef(opts)`, `programTag(opts)`, `lockTag(opts)`, `isLocked(opts)` | Kotlin bridge → Web NFC (`NDEFReader`) → `HostCapabilityUnavailableError` |
78
+ | `native.rfid` | `startScan(opts?)`, `stopScan()`, `subscribe(cb)` | Kotlin bridge only — throws `HostCapabilityUnavailableError` on all other hosts |
79
+ | `native.events` | `subscribe(type, cb)`, `emit(type, payload?)` | Internal pub/sub bridging Kotlin `onSmartlinksData` + Capacitor listeners |
80
+ | `native.webSource` | `get()`, `set({ mode, channel?, liveUrl? })` | Kotlin bridge + Capacitor Preferences mirror |
81
+
82
+ > **`native.rfid` is the only optional sub-facade** on `NativeFacade` — it is only populated on `custom-android` hosts. All other sub-facades are present on every host, though individual methods may throw `HostCapabilityUnavailableError` when the underlying capability is absent.
83
+
84
+ > **`native.device` vs `host.device`** — `host.device.info()` is a simplified convenience that returns `{ model, platform }`. `host.native?.device.getInfo()` returns the full `DeviceInfo` (adds `osVersion`, `manufacturer`, `isVirtual`) and also provides `getId()` and `getLanguageCode()`. Use `host.device` when the convenience is enough.
85
+
86
+ > **`native.network` vs `host.network`** — `host.network.isOnline()` is a boolean convenience. `host.native?.network.getStatus()` returns `{ connected, connectionType }` including `'wifi'` / `'cellular'` distinction.
87
+
88
+ ---
89
+
90
+ ## What's NOT in the facade
91
+
92
+ These capabilities intentionally have no `SL.native` wrapper. Call the Capacitor plugin directly, or use the web API. The host provides them as Tier 1 or Tier 2 plugins (see [Capacitor Plugin Baseline](mobile-admin-container.md#capacitor-plugin-baseline)).
93
+
94
+ | Capability | Why not wrapped |
95
+ |---|---|
96
+ | `@capacitor/push-notifications` | Push only exists in Capacitor builds; no cross-shell registration flow. Import and call directly; detect Capacitor first. |
97
+ | `@capacitor/local-notifications` | Same — no Kotlin or web equivalent worth abstracting. |
98
+ | `@capacitor/camera` | Plugin already abstracts iOS/Android. PWA's `<input capture>` is too different in shape to unify. |
99
+ | `@capacitor/filesystem` | No web equivalent (storage quotas, sandbox rules differ). Use `URL.createObjectURL` + download anchor on web. |
100
+ | `@capacitor/geolocation` | `navigator.geolocation` already works in WebViews; the Capacitor plugin wraps the same OS APIs. No facade adds value. |
101
+ | `@capacitor/keyboard`, `@capacitor/screen-*`, `@capacitor/splash-screen`, `@capacitor/status-bar`, `@capacitor/dialog`, `@capacitor/app` | Per-host boot-time config or platform-chrome concerns; not part of the microapp contract. |
102
+ | `@capacitor/browser` | In-app browser for OAuth is intentionally Capacitor-only. For opening external URLs use `native.share.share({ url })`. |
103
+ | `@capawesome/capacitor-file-picker` | Picker UX too divergent across web (`<input type="file">`) and native to wrap usefully. |
104
+
105
+ If a capability later needs consistent behaviour across shells (e.g. notifications reach enough hosts to warrant throttling / routing via the facade), promote it into `NativeFacade` by adding an interface to `src/native/types.ts` and a row to the table above.
106
+
107
+ ---
108
+
109
+ ## Error contract
110
+
111
+ All facade methods reject with one of the three structured errors from `@proveanything/smartlinks`. A single catch block handles every capability and host combination:
112
+
113
+ ```typescript
114
+ import {
115
+ HostCapabilityUnavailableError,
116
+ HostPermissionDeniedError,
117
+ HostTimeoutError,
118
+ } from '@proveanything/smartlinks'
119
+
120
+ try {
121
+ const { uid } = await host.native?.nfc.read({ timeoutMs: 10_000 }) ?? {}
122
+ } catch (err) {
123
+ if (err instanceof HostCapabilityUnavailableError) {
124
+ // host.capabilities won't list 'nfc' — we should have checked first
125
+ host.ui.toast?.({ title: 'NFC not available on this device', variant: 'destructive' })
126
+ } else if (err instanceof HostPermissionDeniedError) {
127
+ host.ui.toast?.({ title: 'NFC permission denied', variant: 'destructive' })
128
+ } else if (err instanceof HostTimeoutError) {
129
+ host.ui.toast?.({ title: `No tag detected after ${err.timeoutMs / 1000}s` })
130
+ } else {
131
+ throw err
132
+ }
133
+ }
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Type reference
139
+
140
+ All types below are exported from `@proveanything/smartlinks`:
141
+
142
+ ```typescript
143
+ import type {
144
+ NativeFacade, // root — use on AdminMobileHostContext.native
145
+ NativeCapability, // union of sub-facade keys: 'share' | 'clipboard' | ...
146
+ ShareFacade,
147
+ ClipboardFacade,
148
+ HapticsFacade,
149
+ HapticImpactStyle, // 'light' | 'medium' | 'heavy'
150
+ HapticNotificationStyle,
151
+ NetworkFacade,
152
+ NetworkStatus,
153
+ DeviceFacade,
154
+ DeviceInfo,
155
+ StorageFacade,
156
+ QrFacade,
157
+ QrScanOptions,
158
+ AuthFacade,
159
+ NfcFacade,
160
+ NfcReadResult,
161
+ RfidFacade,
162
+ RfidScanOptions,
163
+ EventsFacade,
164
+ WebSourceFacade,
165
+ WebSourceMode,
166
+ WebSourceConfig,
167
+ } from '@proveanything/smartlinks'
168
+ ```
169
+
170
+ The facade interfaces are contract-only — no runtime implementation is shipped in the SDK. You only need them for TypeScript type-checking (authoring containers or host stubs).
package/dist/index.d.ts CHANGED
@@ -27,3 +27,4 @@ export type { AdminMobileCapability, ActionableCapability, AdminMobileHostId, Ad
27
27
  AdminMobileHostContext, AdminMobileComponentManifest, AdminMobileBundleManifest, MobileAdminComponentManifest, // @deprecated — use AdminMobileComponentManifest
28
28
  MobileAdminBundleManifest, } from './mobile-admin/types';
29
29
  export { HostCapabilityUnavailableError, HostPermissionDeniedError, HostTimeoutError, } from './mobile-admin/errors';
30
+ export type { NativeCapability, NativeFacade, ShareFacade, ClipboardFacade, HapticImpactStyle, HapticNotificationStyle, HapticsFacade, NetworkStatus, NetworkFacade, DeviceInfo, DeviceFacade, StorageFacade, QrScanOptions, QrFacade, AuthFacade, NfcReadResult, NfcFacade, RfidScanOptions, RfidFacade, EventsFacade, WebSourceMode, WebSourceConfig, WebSourceFacade, } from './native/types';
@@ -1,3 +1,4 @@
1
+ import type { NativeFacade } from '../native/types';
1
2
  /**
2
3
  * Hardware/software capability tokens a mobile admin host may advertise.
3
4
  * Passed to `AdminMobileHostContext.capabilities` and to
@@ -185,6 +186,16 @@ export interface AdminMobileHostContext {
185
186
  * if ('requestNfcTap' in host.actions) { ... }
186
187
  */
187
188
  _version: number;
189
+ /**
190
+ * Full native capability facade populated by the host at mount time.
191
+ * Not all sub-facades are present on every host — check before calling:
192
+ * @example
193
+ * const code = await host.native?.qr.scan();
194
+ * const { identifier } = await host.native?.device.getId() ?? {};
195
+ * For UMD microapps that do not receive a `host` prop, the same object
196
+ * is available at `window.SL.native` (host concern, not SDK).
197
+ */
198
+ native?: NativeFacade;
188
199
  }
189
200
  /** Manifest metadata for a single mobile admin container component. */
190
201
  export interface AdminMobileComponentManifest {
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Contract types for the `SL.native` / `host.native` facade.
3
+ *
4
+ * These are **interface-only** — no runtime implementation lives here.
5
+ * The facade is implemented per-host (Sidekick, Capacitor builds, PWA) and
6
+ * exposed to containers via `AdminMobileHostContext.native` or (for UMD
7
+ * microapps) via `window.SL.native`. The SDK owns the shape; the host owns
8
+ * the wiring.
9
+ *
10
+ * All facade methods reject with one of the structured errors from
11
+ * `@proveanything/smartlinks` (`HostCapabilityUnavailableError`,
12
+ * `HostPermissionDeniedError`, `HostTimeoutError`) so a single catch handler
13
+ * covers every host and capability.
14
+ */
15
+ /**
16
+ * Keys of the `NativeFacade` interface — one token per sub-facade.
17
+ * Use `hasCapability(key)` on the host to check availability before calling.
18
+ */
19
+ export type NativeCapability = 'share' | 'clipboard' | 'haptics' | 'network' | 'device' | 'storage' | 'qr' | 'auth' | 'nfc' | 'rfid' | 'events' | 'webSource';
20
+ /** Share sheet / URL sharing. */
21
+ export interface ShareFacade {
22
+ /**
23
+ * Trigger the native share sheet.
24
+ * Falls back to `navigator.share`, then silently copies `url` to clipboard.
25
+ */
26
+ share(payload: {
27
+ title: string;
28
+ url: string;
29
+ text?: string;
30
+ }): Promise<void>;
31
+ /** Returns `true` when a share sheet is available on this host. */
32
+ canShare(): Promise<boolean>;
33
+ }
34
+ /** Clipboard read / write. */
35
+ export interface ClipboardFacade {
36
+ /** Write plain text to the clipboard. */
37
+ write(text: string): Promise<void>;
38
+ /** Read plain text from the clipboard. Returns `''` if empty or denied. */
39
+ read(): Promise<string>;
40
+ }
41
+ /** Haptic feedback. */
42
+ export type HapticImpactStyle = 'light' | 'medium' | 'heavy';
43
+ export type HapticNotificationStyle = 'success' | 'warning' | 'error';
44
+ export interface HapticsFacade {
45
+ /** Physical impact pulse. Falls back to `navigator.vibrate`. */
46
+ impact(style?: HapticImpactStyle): Promise<void>;
47
+ /** Notification-style feedback. Falls back to `navigator.vibrate`. */
48
+ notification(style: HapticNotificationStyle): Promise<void>;
49
+ /** Selection-changed feedback. No-op when unsupported. */
50
+ selection(): Promise<void>;
51
+ }
52
+ /** Network status. */
53
+ export interface NetworkStatus {
54
+ connected: boolean;
55
+ connectionType: 'wifi' | 'cellular' | 'none' | 'unknown';
56
+ }
57
+ export interface NetworkFacade {
58
+ /** Returns current connection status. */
59
+ getStatus(): Promise<NetworkStatus>;
60
+ /**
61
+ * Subscribe to connectivity changes. Returns an unsubscribe function.
62
+ * Falls back to `window` `'online'`/`'offline'` events on web.
63
+ */
64
+ addListener(event: 'change', cb: (status: NetworkStatus) => void): () => void;
65
+ }
66
+ /** Device metadata. */
67
+ export interface DeviceInfo {
68
+ model: string;
69
+ platform: 'ios' | 'android' | 'web';
70
+ osVersion: string;
71
+ manufacturer: string;
72
+ isVirtual: boolean;
73
+ }
74
+ export interface DeviceFacade {
75
+ /** Hardware and OS metadata. */
76
+ getInfo(): Promise<DeviceInfo>;
77
+ /**
78
+ * Stable device identifier — UUID persisted in `@capacitor/preferences`
79
+ * (native) or `localStorage` (web).
80
+ */
81
+ getId(): Promise<{
82
+ identifier: string;
83
+ }>;
84
+ /** Current locale language code, e.g. `'en'`, `'fr'`. */
85
+ getLanguageCode(): Promise<{
86
+ value: string;
87
+ }>;
88
+ }
89
+ /**
90
+ * Persistent key-value storage.
91
+ * Backed by `@capacitor/preferences` on native; `localStorage` on web;
92
+ * in-memory `Map` in private-browsing / third-party-cookie contexts.
93
+ */
94
+ export interface StorageFacade {
95
+ get(key: string): Promise<string | null>;
96
+ set(key: string, value: string): Promise<void>;
97
+ remove(key: string): Promise<void>;
98
+ keys(): Promise<string[]>;
99
+ }
100
+ /** QR / barcode scan. */
101
+ export interface QrScanOptions {
102
+ /** BarcodeFormat strings to restrict scanning (e.g. `['QR_CODE', 'EAN_13']`). */
103
+ formats?: string[];
104
+ }
105
+ export interface QrFacade {
106
+ /**
107
+ * Open the barcode scanner UI and resolve with the decoded string.
108
+ * Uses Capacitor MLKit on native; html5-qrcode (or `BarcodeDetector`) on web.
109
+ * Throws `HostTimeoutError` when the user dismisses without scanning.
110
+ */
111
+ scan(opts?: QrScanOptions): Promise<string>;
112
+ }
113
+ /** Authentication. */
114
+ export interface AuthFacade {
115
+ /**
116
+ * Initiate Google Sign-In. Returns the raw ID token for exchange with the
117
+ * SmartLinks auth backend. Falls back to an OAuth redirect flow on web.
118
+ */
119
+ signInWithGoogle(): Promise<{
120
+ idToken: string;
121
+ }>;
122
+ signOut(): Promise<void>;
123
+ }
124
+ /** NFC tag operations. */
125
+ export interface NfcReadResult {
126
+ uid: string;
127
+ /** Raw NDEF payload (hex or base64 depending on host), when present. */
128
+ ndef?: string;
129
+ }
130
+ export interface NfcFacade {
131
+ /**
132
+ * Wait for the next NFC tap and return the tag's UID + optional NDEF.
133
+ * Throws `HostTimeoutError` after `opts.timeoutMs` (default: 30 s).
134
+ */
135
+ read(opts?: {
136
+ timeoutMs?: number;
137
+ }): Promise<NfcReadResult>;
138
+ /** Write an NDEF record to a tag by UID. */
139
+ writeNdef(opts: {
140
+ uid: string;
141
+ payload: string;
142
+ }): Promise<void>;
143
+ /** Program an NTAG 21x / 424 profile. Host-specific; Kotlin only on advanced ops. */
144
+ programTag(opts: {
145
+ uid: string;
146
+ profile: string;
147
+ [key: string]: unknown;
148
+ }): Promise<void>;
149
+ /** Lock a tag against further writes. */
150
+ lockTag(opts: {
151
+ uid: string;
152
+ }): Promise<void>;
153
+ /** Returns whether the tag is currently locked. */
154
+ isLocked(opts: {
155
+ uid: string;
156
+ }): Promise<boolean>;
157
+ }
158
+ /** RFID mass-scan (UHF). Only available on `custom-android`. */
159
+ export interface RfidScanOptions {
160
+ /** Transmission power in dBm. */
161
+ power?: number;
162
+ /** Gen2 session flag (0–3). */
163
+ sessionFlag?: number;
164
+ }
165
+ export interface RfidFacade {
166
+ /** Start the RFID reader. Throws `HostCapabilityUnavailableError` on non-Kotlin hosts. */
167
+ startScan(opts?: RfidScanOptions): Promise<void>;
168
+ stopScan(): Promise<void>;
169
+ /** Subscribe to EPC bursts. Returns an unsubscribe function. */
170
+ subscribe(cb: (epcs: string[]) => void): () => void;
171
+ }
172
+ /** Cross-shell event bus. */
173
+ export interface EventsFacade {
174
+ /**
175
+ * Subscribe to named events from any of: the Kotlin bridge
176
+ * (`onSmartlinksData`), Capacitor plugin listeners, or internal pub/sub.
177
+ * Returns an unsubscribe function.
178
+ */
179
+ subscribe(type: string, cb: (payload: unknown) => void): () => void;
180
+ /** Publish an event into the same bus. Useful for container ↔ container comms. */
181
+ emit(type: string, payload?: unknown): void;
182
+ }
183
+ /** OTA / web-source switching. */
184
+ export type WebSourceMode = 'live' | 'bundled' | 'channel';
185
+ export interface WebSourceConfig {
186
+ mode: WebSourceMode;
187
+ /** Only set when `mode === 'channel'`. */
188
+ channel?: string;
189
+ /** Only set when `mode === 'live'`. */
190
+ liveUrl?: string;
191
+ }
192
+ export interface WebSourceFacade {
193
+ /** Returns the currently active web source config. */
194
+ get(): Promise<WebSourceConfig>;
195
+ /** Switch web source mode. Takes effect on next app boot. */
196
+ set(config: WebSourceConfig): Promise<void>;
197
+ }
198
+ /**
199
+ * The full native capability facade exposed by the host on
200
+ * `AdminMobileHostContext.native` (containers) and `window.SL.native` (UMD
201
+ * microapps).
202
+ *
203
+ * Each sub-facade is present only when the host implements it. Check
204
+ * `'nfc' in host.native` (or `host.capabilities` for hardware-gated features)
205
+ * before calling.
206
+ *
207
+ * @example
208
+ * const { native } = host;
209
+ * if (native?.qr) {
210
+ * const code = await native.qr.scan();
211
+ * }
212
+ */
213
+ export interface NativeFacade {
214
+ share: ShareFacade;
215
+ clipboard: ClipboardFacade;
216
+ haptics: HapticsFacade;
217
+ network: NetworkFacade;
218
+ device: DeviceFacade;
219
+ /** Cross-host persistent storage (`@capacitor/preferences` / `localStorage` fallback). */
220
+ storage: StorageFacade;
221
+ qr: QrFacade;
222
+ auth: AuthFacade;
223
+ nfc: NfcFacade;
224
+ /** RFID mass-scan. Only populated on `custom-android` hosts. */
225
+ rfid?: RfidFacade;
226
+ events: EventsFacade;
227
+ webSource: WebSourceFacade;
228
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Contract types for the `SL.native` / `host.native` facade.
3
+ *
4
+ * These are **interface-only** — no runtime implementation lives here.
5
+ * The facade is implemented per-host (Sidekick, Capacitor builds, PWA) and
6
+ * exposed to containers via `AdminMobileHostContext.native` or (for UMD
7
+ * microapps) via `window.SL.native`. The SDK owns the shape; the host owns
8
+ * the wiring.
9
+ *
10
+ * All facade methods reject with one of the structured errors from
11
+ * `@proveanything/smartlinks` (`HostCapabilityUnavailableError`,
12
+ * `HostPermissionDeniedError`, `HostTimeoutError`) so a single catch handler
13
+ * covers every host and capability.
14
+ */
15
+ export {};
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.11.4 | Generated: 2026-04-30T13:59:47.020Z
3
+ Version: 1.11.6 | Generated: 2026-05-01T13:42:06.488Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -85,6 +85,7 @@ Zones are **automatically filtered** based on the caller's role:
85
85
  ### Zone Writing Rules
86
86
 
87
87
  - **Non-admin callers** attempting to write to the `admin` zone are silently ignored
88
+ - **Authenticated record owners** can write to `data` and `owner` by default; individual keys can be restricted via the `ownerEdit` app config policy (see [Owner Edit Policy](#owner-edit-policy) below)
88
89
  - **Public callers** can write to `data` and `owner` (if visibility allows)
89
90
  - **Admins** can write to all three zones
90
91
 
@@ -1098,6 +1099,61 @@ The `enforce` values are **merged over** the caller's request body, so you can l
1098
1099
 
1099
1100
  ---
1100
1101
 
1102
+ ## Owner Edit Policy
1103
+
1104
+ Gives per-zone, field-level control over what an **authenticated record owner** can update via `PATCH /api/v1/public/collection/:collectionId/app/:appId/records/:recordId`.
1105
+
1106
+ Set the policy in the same app config document used for `publicCreate` (stored at `sites/{collectionId}/apps/{appId}`):
1107
+
1108
+ ```json
1109
+ {
1110
+ "ownerEdit": {
1111
+ "records": {
1112
+ "data": { "allow": ["paypalEmail"] },
1113
+ "owner": { "allow": ["paypalEmail", "paypalEmailUpdatedAt"] }
1114
+ }
1115
+ }
1116
+ }
1117
+ ```
1118
+
1119
+ ### Zone visibility and write access
1120
+
1121
+ | Zone | Who can read | Who can write (owner) |
1122
+ |---------|------------------------|----------------------------------------------------------|
1123
+ | `data` | public | Allow-listed keys only (if policy set); all keys if not |
1124
+ | `owner` | owner + admin | Allow-listed keys only (if policy set); all keys if not |
1125
+ | `admin` | admin | Never — admin zone is always immutable to owners |
1126
+
1127
+ ### Allow-list semantics
1128
+
1129
+ | Config | Behaviour |
1130
+ |----------------------------|-------------------------------------------------------------------------------|
1131
+ | No `ownerEdit` key | Default-allow — both zones fully writable (no change to existing behaviour) |
1132
+ | `allow` array with keys | Only the listed keys are accepted from the PATCH body; the rest are silently ignored and their existing values preserved |
1133
+ | `allow: []` (empty array) | Zone is effectively read-only for the owner |
1134
+
1135
+ Accepted keys are **merged** onto the existing zone blob — you do not need to re-send unchanged values.
1136
+
1137
+ ### Example: commission record with protected fields
1138
+
1139
+ An app that lets owners update their payout email but not their commission total:
1140
+
1141
+ ```json
1142
+ {
1143
+ "ownerEdit": {
1144
+ "records": {
1145
+ "owner": { "allow": ["paypalEmail", "paypalEmailUpdatedAt"] }
1146
+ }
1147
+ }
1148
+ }
1149
+ ```
1150
+
1151
+ A PATCH body of `{ "owner": { "paypalEmail": "x@y.com", "totalCommission": 99 } }` will update `paypalEmail` only. `totalCommission` is silently ignored and its existing value is preserved.
1152
+
1153
+ > **App design note:** If your app creates records with sensitive fields that owners should never modify (e.g. computed totals, server-assigned fields), add an `ownerEdit` policy from the start. It is significantly easier to relax restrictions later than to tighten them after data has been mutated.
1154
+
1155
+ ---
1156
+
1101
1157
  ## Anonymous Edit Tokens
1102
1158
 
1103
1159
  Enables an anonymous caller to amend a record they just created — without authentication — by presenting a short-lived secret token.
@@ -4,7 +4,7 @@
4
4
 
5
5
  This document describes how to build a **Mobile Admin Container** — a SmartLinks microapp that provides an in-the-field operator/admin surface optimised for mobile devices. These containers ship as a **separate `mobileAdmin` bundle** (not inside the `containers` bundle) so that Capacitor plugins, offline helpers, and operator-only code never reach the public consumer bundle.
6
6
 
7
- > **See also:** [containers.md](containers.md) covers the public consumer container. The [Multiple Consumer Components](containers.md#multiple-consumer-components) section explains the consumer vs. admin bundle split.
7
+ > **See also:** [containers.md](containers.md) covers the public consumer container. The [Multiple Consumer Components](containers.md#multiple-consumer-components) section explains the consumer vs. admin bundle split. For the full `host.native` facade contract (share, clipboard, haptics, NFC, RFID, storage, etc.) see [native-facade.md](native-facade.md).
8
8
 
9
9
  ---
10
10
 
@@ -23,6 +23,7 @@ This document describes how to build a **Mobile Admin Container** — a SmartLin
23
23
  11. [Build & Bundle Requirements](#build--bundle-requirements)
24
24
  12. [Example: Minimal Mobile Admin Container](#example-minimal-mobile-admin-container)
25
25
  13. [Best Practices](#best-practices)
26
+ 14. [Native Capability Facade](#native-capability-facade)
26
27
 
27
28
  ---
28
29
 
@@ -58,7 +59,7 @@ Your container **never** detects the host directly. It receives a `host` prop fr
58
59
 
59
60
  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.
60
61
 
61
- > **SDK export** — `AdminMobileHostContext`, `AdminMobileCapability`, `ActionableCapability`, `AdminMobileHostId`, `AdminMobileEvent`, `AdminMobileEventCallback`, `AdminMobileEventSubscriber`, `AdminMobileComponentManifest`, and `AdminMobileBundleManifest` are all exported from `@proveanything/smartlinks`. Import via `import type { AdminMobileHostContext } from '@proveanything/smartlinks'` — no local mirror needed. `ScannerEventSubscriber`, `MobileAdminComponentManifest`, and `MobileAdminBundleManifest` still export as deprecated aliases.
62
+ > **SDK export** — `AdminMobileHostContext`, `AdminMobileCapability`, `ActionableCapability`, `AdminMobileHostId`, `AdminMobileEvent`, `AdminMobileEventCallback`, `AdminMobileEventSubscriber`, `AdminMobileComponentManifest`, `AdminMobileBundleManifest`, and `NativeFacade` (plus all sub-facade interfaces) are exported from `@proveanything/smartlinks`. Import via `import type { AdminMobileHostContext } from '@proveanything/smartlinks'` — no local mirror needed. `ScannerEventSubscriber`, `MobileAdminComponentManifest`, and `MobileAdminBundleManifest` still export as deprecated aliases.
62
63
 
63
64
  ```typescript
64
65
  interface AdminMobileHostContext {
@@ -112,6 +113,10 @@ interface AdminMobileHostContext {
112
113
  // Informational host version — use for logging/diagnostics, not feature detection.
113
114
  // For feature detection prefer existence checks: 'requestNfcTap' in host.actions
114
115
  _version: number
116
+
117
+ // Full native capability facade — share, clipboard, haptics, NFC, RFID, storage, etc.
118
+ // Optional: not every host stub populates it. See native-facade.md.
119
+ native?: NativeFacade
115
120
  }
116
121
  ```
117
122
 
@@ -542,3 +547,26 @@ export const MOBILE_ADMIN_MANIFEST = {
542
547
  - **Bundle Capacitor plugins in** (do not externalise) — so the component degrades gracefully on PWA/browser without crashing.
543
548
  - **Declare `offline: true`** on a component if it queues writes locally — this signals the launcher to provision offline sync support.
544
549
  - **Use `'methodName' in host.actions`** to feature-detect new host capabilities rather than comparing `host._version`. The version is informational only.
550
+
551
+ ---
552
+
553
+ ## Native Capability Facade
554
+
555
+ `host.native` gives containers access to a broader set of device capabilities — share sheet, clipboard, full haptics API, storage, QR, NFC, RFID (Kotlin only), auth, and cross-shell events — through a single interface that falls back gracefully across all host environments.
556
+
557
+ ```typescript
558
+ // Check host.hardware.* for physical availability, then call via host.native
559
+ if (host.hardware.nfc && host.native?.nfc) {
560
+ const { uid } = await host.native.nfc.read({ timeoutMs: 10_000 })
561
+ }
562
+
563
+ // Storage always available (Preferences → localStorage → in-memory Map)
564
+ await host.native?.storage.set('lastScan', uid)
565
+
566
+ // Share sheet with web fallback
567
+ await host.native?.share.share({ title: 'Found tag', url: `https://app.example/tags/${uid}` })
568
+ ```
569
+
570
+ `host.native` is optional on `AdminMobileHostContext` — host stubs (Storybook, unit tests) need not implement it. `native.rfid` is additionally optional within `NativeFacade` itself, as it only exists on `custom-android`.
571
+
572
+ For the full sub-facade table, fallback chains, and what's intentionally not wrapped, see **[native-facade.md](native-facade.md)**.
@@ -0,0 +1,170 @@
1
+ # Native Capability Facade (`host.native` / `SL.native`)
2
+
3
+ > **Version:** 1.12 · **Platform:** SmartLinks R4 · **Last updated:** 2026-04-30
4
+
5
+ The `NativeFacade` is a thin contract layer between microapps and the device capabilities available on the current host shell (Kotlin, Capacitor iOS/Android, PWA, or browser). It lets a microapp call `host.native.share.share({...})` without knowing whether it's running over `window.SmartlinksScanner`, a Capacitor plugin, or `navigator.share`.
6
+
7
+ **The SDK owns the contract** (`src/native/types.ts`, re-exported from `@proveanything/smartlinks`). Each host implementation is responsible for wiring up the sub-facades and populating:
8
+ - `AdminMobileHostContext.native` (container prop, typed in the SDK), and
9
+ - `window.SL.native` (for UMD microapps that don't receive a host prop — host concern, not SDK).
10
+
11
+ ---
12
+
13
+ ## Table of Contents
14
+
15
+ 1. [Access patterns](#access-patterns)
16
+ 2. [What's in the facade](#whats-in-the-facade)
17
+ 3. [What's NOT in the facade](#whats-not-in-the-facade)
18
+ 4. [Error contract](#error-contract)
19
+ 5. [Type reference](#type-reference)
20
+
21
+ ---
22
+
23
+ ## Access patterns
24
+
25
+ ### Inside a Mobile Admin Container (recommended)
26
+
27
+ ```typescript
28
+ import type { AdminMobileHostContext } from '@proveanything/smartlinks'
29
+
30
+ function MyContainer({ host }: { host: AdminMobileHostContext }) {
31
+ const handleShare = async () => {
32
+ await host.native?.share.share({ title: 'SmartLinks', url: window.location.href })
33
+ }
34
+
35
+ const handleScan = async () => {
36
+ // Only present when the host provides a QR reader
37
+ const code = await host.native?.qr.scan()
38
+ if (code) processCode(code)
39
+ }
40
+ }
41
+ ```
42
+
43
+ Always call via `host.native?.<facade>.<method>()` — `native` is optional because not every host exposes the full surface (e.g. a minimal browser stub may omit `rfid`).
44
+
45
+ For hardware-gated capabilities (NFC, RFID, QR, camera) also check `host.capabilities` or `host.hardware.*` before calling, as those flags reflect physical availability on the current device:
46
+
47
+ ```typescript
48
+ if (host.hardware.nfc && host.native?.nfc) {
49
+ const { uid } = await host.native.nfc.read({ timeoutMs: 10_000 })
50
+ }
51
+ ```
52
+
53
+ ### Inside a UMD microapp (no host prop)
54
+
55
+ ```typescript
56
+ // window.SL.native is populated by the host at boot — host concern, not SDK
57
+ const native = (window as any).SL?.native as import('@proveanything/smartlinks').NativeFacade | undefined
58
+ const code = await native?.qr.scan()
59
+ ```
60
+
61
+ ---
62
+
63
+ ## What's in the facade
64
+
65
+ These capabilities have meaningful fallbacks across Kotlin / Capacitor / web. Always go through the facade.
66
+
67
+ | Sub-facade | Key methods | Fallback chain |
68
+ |---|---|---|
69
+ | `native.share` | `share({ title, url, text? })`, `canShare()` | Capacitor Share → `navigator.share` → copy to clipboard + toast |
70
+ | `native.clipboard` | `write(text)`, `read()` | Capacitor Clipboard → `navigator.clipboard` → `execCommand('copy')` |
71
+ | `native.haptics` | `impact(style?)`, `notification(style)`, `selection()` | Capacitor Haptics → `navigator.vibrate` → no-op |
72
+ | `native.network` | `getStatus()`, `addListener('change', cb)` | Capacitor Network → `navigator.onLine` + `'online'`/`'offline'` events |
73
+ | `native.device` | `getInfo()`, `getId()`, `getLanguageCode()` | Capacitor Device → UA parsing + cached UUID in `localStorage` |
74
+ | `native.storage` | `get(key)`, `set(key, value)`, `remove(key)`, `keys()` | Capacitor Preferences → `localStorage` → in-memory `Map` (private mode) |
75
+ | `native.qr` | `scan(opts?)` | Capacitor MLKit Barcode → html5-qrcode / `BarcodeDetector` |
76
+ | `native.auth` | `signInWithGoogle()`, `signOut()` | Kotlin bridge → Capacitor Browser + OAuth → web redirect |
77
+ | `native.nfc` | `read(opts?)`, `writeNdef(opts)`, `programTag(opts)`, `lockTag(opts)`, `isLocked(opts)` | Kotlin bridge → Web NFC (`NDEFReader`) → `HostCapabilityUnavailableError` |
78
+ | `native.rfid` | `startScan(opts?)`, `stopScan()`, `subscribe(cb)` | Kotlin bridge only — throws `HostCapabilityUnavailableError` on all other hosts |
79
+ | `native.events` | `subscribe(type, cb)`, `emit(type, payload?)` | Internal pub/sub bridging Kotlin `onSmartlinksData` + Capacitor listeners |
80
+ | `native.webSource` | `get()`, `set({ mode, channel?, liveUrl? })` | Kotlin bridge + Capacitor Preferences mirror |
81
+
82
+ > **`native.rfid` is the only optional sub-facade** on `NativeFacade` — it is only populated on `custom-android` hosts. All other sub-facades are present on every host, though individual methods may throw `HostCapabilityUnavailableError` when the underlying capability is absent.
83
+
84
+ > **`native.device` vs `host.device`** — `host.device.info()` is a simplified convenience that returns `{ model, platform }`. `host.native?.device.getInfo()` returns the full `DeviceInfo` (adds `osVersion`, `manufacturer`, `isVirtual`) and also provides `getId()` and `getLanguageCode()`. Use `host.device` when the convenience is enough.
85
+
86
+ > **`native.network` vs `host.network`** — `host.network.isOnline()` is a boolean convenience. `host.native?.network.getStatus()` returns `{ connected, connectionType }` including `'wifi'` / `'cellular'` distinction.
87
+
88
+ ---
89
+
90
+ ## What's NOT in the facade
91
+
92
+ These capabilities intentionally have no `SL.native` wrapper. Call the Capacitor plugin directly, or use the web API. The host provides them as Tier 1 or Tier 2 plugins (see [Capacitor Plugin Baseline](mobile-admin-container.md#capacitor-plugin-baseline)).
93
+
94
+ | Capability | Why not wrapped |
95
+ |---|---|
96
+ | `@capacitor/push-notifications` | Push only exists in Capacitor builds; no cross-shell registration flow. Import and call directly; detect Capacitor first. |
97
+ | `@capacitor/local-notifications` | Same — no Kotlin or web equivalent worth abstracting. |
98
+ | `@capacitor/camera` | Plugin already abstracts iOS/Android. PWA's `<input capture>` is too different in shape to unify. |
99
+ | `@capacitor/filesystem` | No web equivalent (storage quotas, sandbox rules differ). Use `URL.createObjectURL` + download anchor on web. |
100
+ | `@capacitor/geolocation` | `navigator.geolocation` already works in WebViews; the Capacitor plugin wraps the same OS APIs. No facade adds value. |
101
+ | `@capacitor/keyboard`, `@capacitor/screen-*`, `@capacitor/splash-screen`, `@capacitor/status-bar`, `@capacitor/dialog`, `@capacitor/app` | Per-host boot-time config or platform-chrome concerns; not part of the microapp contract. |
102
+ | `@capacitor/browser` | In-app browser for OAuth is intentionally Capacitor-only. For opening external URLs use `native.share.share({ url })`. |
103
+ | `@capawesome/capacitor-file-picker` | Picker UX too divergent across web (`<input type="file">`) and native to wrap usefully. |
104
+
105
+ If a capability later needs consistent behaviour across shells (e.g. notifications reach enough hosts to warrant throttling / routing via the facade), promote it into `NativeFacade` by adding an interface to `src/native/types.ts` and a row to the table above.
106
+
107
+ ---
108
+
109
+ ## Error contract
110
+
111
+ All facade methods reject with one of the three structured errors from `@proveanything/smartlinks`. A single catch block handles every capability and host combination:
112
+
113
+ ```typescript
114
+ import {
115
+ HostCapabilityUnavailableError,
116
+ HostPermissionDeniedError,
117
+ HostTimeoutError,
118
+ } from '@proveanything/smartlinks'
119
+
120
+ try {
121
+ const { uid } = await host.native?.nfc.read({ timeoutMs: 10_000 }) ?? {}
122
+ } catch (err) {
123
+ if (err instanceof HostCapabilityUnavailableError) {
124
+ // host.capabilities won't list 'nfc' — we should have checked first
125
+ host.ui.toast?.({ title: 'NFC not available on this device', variant: 'destructive' })
126
+ } else if (err instanceof HostPermissionDeniedError) {
127
+ host.ui.toast?.({ title: 'NFC permission denied', variant: 'destructive' })
128
+ } else if (err instanceof HostTimeoutError) {
129
+ host.ui.toast?.({ title: `No tag detected after ${err.timeoutMs / 1000}s` })
130
+ } else {
131
+ throw err
132
+ }
133
+ }
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Type reference
139
+
140
+ All types below are exported from `@proveanything/smartlinks`:
141
+
142
+ ```typescript
143
+ import type {
144
+ NativeFacade, // root — use on AdminMobileHostContext.native
145
+ NativeCapability, // union of sub-facade keys: 'share' | 'clipboard' | ...
146
+ ShareFacade,
147
+ ClipboardFacade,
148
+ HapticsFacade,
149
+ HapticImpactStyle, // 'light' | 'medium' | 'heavy'
150
+ HapticNotificationStyle,
151
+ NetworkFacade,
152
+ NetworkStatus,
153
+ DeviceFacade,
154
+ DeviceInfo,
155
+ StorageFacade,
156
+ QrFacade,
157
+ QrScanOptions,
158
+ AuthFacade,
159
+ NfcFacade,
160
+ NfcReadResult,
161
+ RfidFacade,
162
+ RfidScanOptions,
163
+ EventsFacade,
164
+ WebSourceFacade,
165
+ WebSourceMode,
166
+ WebSourceConfig,
167
+ } from '@proveanything/smartlinks'
168
+ ```
169
+
170
+ The facade interfaces are contract-only — no runtime implementation is shipped in the SDK. You only need them for TypeScript type-checking (authoring containers or host stubs).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.11.4",
3
+ "version": "1.11.6",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",