@proveanything/smartlinks 2.1.8 → 2.1.11

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: 2.1.8 | Generated: 2026-11-02T13:05:01.425Z
3
+ Version: 2.1.11 | Generated: 2026-11-08T09:25:58.482Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -153,6 +153,7 @@ The Smartlinks SDK is organized into the following namespaces:
153
153
  - **lots** - Functions for lots operations
154
154
  - **navigation** - Functions for navigation operations
155
155
  - **order** - Functions for order operations
156
+ - **partner** - Functions for partner operations
156
157
  - **products** - Functions for products operations
157
158
  - **realtime** - Functions for realtime operations
158
159
  - **research** - Functions for research operations
@@ -186,17 +187,17 @@ The release channel explicitly set for this app build (initializeApi({ appChanne
186
187
  **setAppChannel**(channel: string | undefined) → `void`
187
188
  Set (or clear) the release channel SL.functions calls target.
188
189
 
189
- **initializeApi**(options: {
190
- baseURL: string
191
- apiKey?: string
192
- bearerToken?: string
193
- proxyMode?: boolean
194
- ngrokSkipBrowserWarning?: boolean
195
- extraHeaders?: Record<string, string>
196
- /**
197
- * Declares the host platform. Set to `'native'` on native/Capacitor hosts to opt into
198
- * AuthKit refresh tokens — the SDK then sends `X-Client-Platform: native` on every
199
- * request, so login endpoints return `refreshToken`/`refreshTokenExpiresAt` and a
190
+ **initializeApi**(options: {
191
+ baseURL: string
192
+ apiKey?: string
193
+ bearerToken?: string
194
+ proxyMode?: boolean
195
+ ngrokSkipBrowserWarning?: boolean
196
+ extraHeaders?: Record<string, string>
197
+ /**
198
+ * Declares the host platform. Set to `'native'` on native/Capacitor hosts to opt into
199
+ * AuthKit refresh tokens — the SDK then sends `X-Client-Platform: native` on every
200
+ * request, so login endpoints return `refreshToken`/`refreshTokenExpiresAt` and a
200
201
  * short-lived access token. Omit (or `'web'`) → `void`
201
202
  Call this once (e.g. at app startup) to configure baseURL/auth.
202
203
 
@@ -227,56 +228,56 @@ Returns true if initializeApi() has been called at least once. Useful for guards
227
228
  **hasAuthCredentials**() → `boolean`
228
229
  Returns true if the SDK currently has any auth credential set (bearer token or API key). Use this as a cheap pre-flight check before calling endpoints that require authentication, to avoid issuing a network request that you already know will return a 401. ```ts if (hasAuthCredentials()) { const account = await auth.getAccount() } ```
229
230
 
230
- **configureSdkCache**(options: {
231
- enabled?: boolean
232
- ttlMs?: number
233
- maxEntries?: number
234
- persistence?: 'none' | 'indexeddb'
235
- persistenceTtlMs?: number
236
- serveStaleOnOffline?: boolean
237
- clearOnPageLoad?: boolean
231
+ **configureSdkCache**(options: {
232
+ enabled?: boolean
233
+ ttlMs?: number
234
+ maxEntries?: number
235
+ persistence?: 'none' | 'indexeddb'
236
+ persistenceTtlMs?: number
237
+ serveStaleOnOffline?: boolean
238
+ clearOnPageLoad?: boolean
238
239
  }) → `void`
239
240
  Configure the SDK's built-in in-memory GET cache. The cache is transparent — it sits inside the HTTP layer and requires no changes to your existing API calls. All GET requests benefit automatically. Per-resource rules (collections/products → 1 h, proofs → 30 s, etc.) override this value. in-memory only (`'none'`, default). Ignored in Node.js. fallback, from the original fetch time (default: 7 days). `SmartlinksOfflineError` with stale data instead of propagating the network error. caches on page load/refresh. IndexedDB persists for offline. ```ts // Enable IndexedDB persistence for offline support configureSdkCache({ persistence: 'indexeddb' }) // Disable cache entirely in test environments configureSdkCache({ enabled: false }) // Keep caches across page refreshes (not recommended for production) configureSdkCache({ clearOnPageLoad: false }) ```
240
241
 
241
242
  **invalidateCache**(urlPattern?: string, options?: InvalidateCacheOptions) → `void`
242
243
  Manually invalidate entries in the SDK's GET cache. Note: the GET cache is **in-memory, per page load** (with an optional L2 IndexedDB layer when persistence is enabled) — it does not persist across reloads unless you opt into persistence, so it rarely needs disabling "for correctness". *contains* this string is removed). With `{ exact: true }`, matches the path precisely. Omit to wipe the entire cache. ```ts invalidateCache() // clear everything invalidateCache('/collection/abc123') // that collection AND everything under it invalidateCache('/collection/abc123', { exact: true }) // ONLY that collection entry invalidateCache('/products/') // all canonical plural product responses ```
243
244
 
244
- **proxyUploadFormData**(path: string,
245
- formData: FormData,
245
+ **proxyUploadFormData**(path: string,
246
+ formData: FormData,
246
247
  onProgress?: (percent: number) → `void`
247
248
  Upload a FormData payload via proxy with progress events using chunked postMessage. Parent is expected to implement the counterpart protocol.
248
249
 
249
250
  **request**(path: string) → `Promise<T>`
250
251
  Internal helper that performs a GET request to `${baseURL}${path}`, injecting headers for apiKey or bearerToken if present. Cache pipeline (when caching is not skipped): L1 hit → return from memory (no I/O) L2 hit → return from IndexedDB, promote to L1 (no network) Miss → fetch from network, store in L1 + L2 Offline → serve stale L2 entry via SmartlinksOfflineError (if persistence enabled) Concurrent identical GETs share one in-flight promise (deduplication). Node-safe: IndexedDB calls are no-ops when IDB is unavailable.
251
252
 
252
- **post**(path: string,
253
- body: any,
253
+ **post**(path: string,
254
+ body: any,
254
255
  extraHeaders?: Record<string, string>) → `Promise<T>`
255
256
  Internal helper that performs a POST request to `${baseURL}${path}`, injecting headers for apiKey or bearerToken if present. If body is FormData, Content-Type is not set. Returns the parsed JSON as T, or throws an Error.
256
257
 
257
- **put**(path: string,
258
- body: any,
258
+ **put**(path: string,
259
+ body: any,
259
260
  extraHeaders?: Record<string, string>) → `Promise<T>`
260
261
  Internal helper that performs a PUT request to `${baseURL}${path}`, injecting headers for apiKey or bearerToken if present. If body is FormData, Content-Type is not set. Returns the parsed JSON as T, or throws an Error.
261
262
 
262
- **patch**(path: string,
263
- body: any,
263
+ **patch**(path: string,
264
+ body: any,
264
265
  extraHeaders?: Record<string, string>) → `Promise<T>`
265
266
  Internal helper that performs a PATCH request to `${baseURL}${path}`, injecting headers for apiKey or bearerToken if present. If body is FormData, Content-Type is not set. Returns the parsed JSON as T, or throws an Error.
266
267
 
267
- **requestWithOptions**(path: string,
268
+ **requestWithOptions**(path: string,
268
269
  options: RequestInit) → `Promise<T>`
269
270
  Internal helper that performs a request to `${baseURL}${path}` with custom options, injecting headers for apiKey or bearerToken if present. Returns the parsed JSON as T, or throws an Error.
270
271
 
271
- **requestStream**(path: string,
272
- options?: {
273
- method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
274
- body?: any
275
- headers?: Record<string, string>
272
+ **requestStream**(path: string,
273
+ options?: {
274
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
275
+ body?: any
276
+ headers?: Record<string, string>
276
277
  }) → `Promise<AsyncIterable<T>>`
277
278
  Internal helper that performs a streaming request using the shared auth and proxy transport. The response is expected to be `text/event-stream` with JSON payloads in `data:` frames.
278
279
 
279
- **del**(path: string,
280
+ **del**(path: string,
280
281
  extraHeaders?: Record<string, string>) → `Promise<T>`
281
282
  Internal helper that performs a DELETE request to `${baseURL}${path}`, injecting headers for apiKey or bearerToken if present. Returns the parsed JSON as T, or throws an Error.
282
283
 
@@ -5731,6 +5732,188 @@ interface CreateClaimSetTagResponse {
5731
5732
  }
5732
5733
  ```
5733
5734
 
5735
+ **ClaimSetRange** (interface)
5736
+ ```typescript
5737
+ interface ClaimSetRange {
5738
+ id: string
5739
+ claimSetId: string
5740
+ namespace: string
5741
+ name: string
5742
+ notes: string | null
5743
+ startIndex: number
5744
+ endIndex: number
5745
+ quantity: number
5746
+ tagType: ClaimSetTagType
5747
+ fulfilment: 'printer' | 'sidekick'
5748
+ stage: 'minting' | 'minted' | 'sent' | 'received'
5749
+ status: ClaimSetRangeStatus
5750
+ intendedCollectionId: string | null
5751
+ intendedProductId: string | null
5752
+ intendedBatchId: string | null
5753
+ manufacturer: string | null
5754
+ orderRef: string | null
5755
+ counts: { programmed: number; assigned: number; claimed: number }
5756
+ sentAt: string | null
5757
+ receivedAt: string | null
5758
+ createdBy: string | null
5759
+ createdAt: string
5760
+ updatedAt: string
5761
+ }
5762
+ ```
5763
+
5764
+ **CreateClaimSetRangeRequest** (interface)
5765
+ ```typescript
5766
+ interface CreateClaimSetRangeRequest {
5767
+ name: string
5768
+ quantity: number
5769
+ intendedCollectionId?: string
5770
+ intendedProductId?: string
5771
+ intendedBatchId?: string
5772
+ fulfilment?: 'printer' | 'sidekick'
5773
+ notes?: string
5774
+ manufacturer?: string
5775
+ orderRef?: string
5776
+ }
5777
+ ```
5778
+
5779
+ **UpdateClaimSetRangeRequest** (interface)
5780
+ ```typescript
5781
+ interface UpdateClaimSetRangeRequest {
5782
+ name?: string
5783
+ notes?: string | null
5784
+ manufacturer?: string | null
5785
+ orderRef?: string | null
5786
+ fulfilment?: 'printer' | 'sidekick'
5787
+ stage?: 'minted' | 'sent' | 'received'
5788
+ }
5789
+ ```
5790
+
5791
+ **ClaimSetRangeTag** (interface)
5792
+ ```typescript
5793
+ interface ClaimSetRangeTag {
5794
+ code: string
5795
+ printed: string
5796
+ index: number
5797
+ uid: string | null
5798
+ key?: string
5799
+ collectionId: string | null
5800
+ productId: string | null
5801
+ batchId: string | null
5802
+ proofId: string | null
5803
+ programmedAt: string | null
5804
+ scans: number
5805
+ }
5806
+ ```
5807
+
5808
+ **ClaimSetRangeTagsPage** (interface)
5809
+ ```typescript
5810
+ interface ClaimSetRangeTagsPage {
5811
+ tags: ClaimSetRangeTag[]
5812
+ next: number | null
5813
+ }
5814
+ ```
5815
+
5816
+ **ImportClaimSetRangeUidsRequest** (interface)
5817
+ ```typescript
5818
+ interface ImportClaimSetRangeUidsRequest {
5819
+ csv?: string
5820
+ rows?: Array<Record<string, string | number>>
5821
+ commit?: boolean
5822
+ }
5823
+ ```
5824
+
5825
+ **ImportClaimSetRangeUidsResponse** (interface)
5826
+ ```typescript
5827
+ interface ImportClaimSetRangeUidsResponse {
5828
+ summary: { rows: number; ok: number; unchanged: number; problems: number; notInFile: number; committed: boolean }
5829
+ rows: Array<{ line: number; code: string; index: number | null; uid: string; result: ClaimSetImportResult; message?: string }>
5830
+ range: ClaimSetRange
5831
+ }
5832
+ ```
5833
+
5834
+ **AssignClaimSetRangeRequest** (interface)
5835
+ ```typescript
5836
+ interface AssignClaimSetRangeRequest {
5837
+ collectionId?: string
5838
+ productId?: string
5839
+ batchId?: string
5840
+ start?: number
5841
+ end?: number
5842
+ clear?: boolean
5843
+ data?: Record<string, unknown>
5844
+ }
5845
+ ```
5846
+
5847
+ **AssignClaimSetRangeResponse** (interface)
5848
+ ```typescript
5849
+ interface AssignClaimSetRangeResponse {
5850
+ changed: number
5851
+ skipped: Array<{ code: string; index: number; reason: 'other_range' | 'has_proof' | 'assigned_elsewhere'; collectionId?: string }>
5852
+ range: ClaimSetRange
5853
+ }
5854
+ ```
5855
+
5856
+ **ClaimSetRangeFile** (interface)
5857
+ ```typescript
5858
+ interface ClaimSetRangeFile {
5859
+ filename: string
5860
+ contentType: string
5861
+ csv?: string
5862
+ base64?: string
5863
+ markdown?: string
5864
+ }
5865
+ ```
5866
+
5867
+ **Partner** (interface)
5868
+ ```typescript
5869
+ interface Partner {
5870
+ id: string
5871
+ name: string
5872
+ status: 'active' | 'suspended'
5873
+ tapDomains: string[]
5874
+ features: Record<string, boolean>
5875
+ role?: 'owner' | 'admin' | 'assigner'
5876
+ members?: Array<{ userId: string; role: 'owner' | 'admin' | 'assigner'; grantedBy: string | null; grantedAt: string }>
5877
+ createdAt: string
5878
+ updatedAt: string
5879
+ }
5880
+ ```
5881
+
5882
+ **PartnerClaimSet** (interface)
5883
+ ```typescript
5884
+ interface PartnerClaimSet {
5885
+ id: string
5886
+ namespace: string
5887
+ name: string | null
5888
+ tagType: ClaimSetTagType
5889
+ tapDomain: string
5890
+ owner: { type: 'platform' | 'partner' | 'collection' | 'user'; id: string | null }
5891
+ codes: number
5892
+ deleted: boolean
5893
+ createdAt: string | null
5894
+ }
5895
+ ```
5896
+
5897
+ **CreatePartnerClaimSetRequest** (interface)
5898
+ ```typescript
5899
+ interface CreatePartnerClaimSetRequest {
5900
+ name: string
5901
+ tagType: ClaimSetTagType
5902
+ tapDomain?: string
5903
+ }
5904
+ ```
5905
+
5906
+ **ClaimSetScope** (type)
5907
+ ```typescript
5908
+ type ClaimSetScope = { collectionId: string }
5909
+ ```
5910
+
5911
+ **ClaimSetTagType** = `'ntag424' | 'ntag21x' | 'qr'`
5912
+
5913
+ **ClaimSetRangeStatus** = `'minting' | 'minted' | 'sent' | 'received' | 'assigned'`
5914
+
5915
+ **ClaimSetImportResult** = ``
5916
+
5734
5917
  ### collection
5735
5918
 
5736
5919
  **Collection** (interface)
@@ -12119,6 +12302,46 @@ Assign claims to codes or ranges within a collection.
12119
12302
  **updateClaimData**(collectionId: string, data: UpdateClaimDataRequest) → `Promise<any>`
12120
12303
  Update claim data for a claim set (collection-scoped).
12121
12304
 
12305
+ ### claimSet.ranges
12306
+
12307
+ **list**(scope: ClaimSetScope, claimSetId: string) → `Promise<ClaimSetRange[]>`
12308
+ The ranges in a set, lowest index first.
12309
+
12310
+ **create**(scope: ClaimSetScope, claimSetId: string, params: CreateClaimSetRangeRequest) → `Promise<ClaimSetRange>`
12311
+ Reserve the set's next indexes and mint a code (and, for NTAG 424, a key) for each.
12312
+
12313
+ **get**(scope: ClaimSetScope, claimSetId: string, rangeId: string) → `Promise<ClaimSetRange>`
12314
+ One range, with its counts refreshed.
12315
+
12316
+ **update**(scope: ClaimSetScope, claimSetId: string, rangeId: string, params: UpdateClaimSetRangeRequest) → `Promise<ClaimSetRange>`
12317
+ Name, notes, order details, or a stage move (sent to the printer, received).
12318
+
12319
+ **resume**(scope: ClaimSetScope, claimSetId: string, rangeId: string) → `Promise<ClaimSetRange>`
12320
+ Finish minting a range that was interrupted.
12321
+
12322
+ **tags**(scope: ClaimSetScope, claimSetId: string, rangeId: string,
12323
+ options: { from?: number; limit?: number; keys?: boolean } = {}) → `Promise<ClaimSetRangeTagsPage>`
12324
+ The range's tags in index order, a page at a time (keys: owners/admins only).
12325
+
12326
+ **next**(scope: ClaimSetScope, claimSetId: string, rangeId: string) → `Promise<ClaimSetRangeNextTag>`
12327
+ The next tag to program on a phone (lowest index without a chip), with its URL and key.
12328
+
12329
+ **recordTag**(scope: ClaimSetScope, claimSetId: string, rangeId: string, code: string,
12330
+ params: { uid: string; data?: Record<string, unknown> }) → `Promise<`
12331
+ Record the chip a phone just programmed with this code.
12332
+
12333
+ **exportFile**(scope: ClaimSetScope, claimSetId: string, rangeId: string) → `Promise<ClaimSetRangeFile>`
12334
+ The printer file (CSV text in `csv`). Carries the tags' keys: don't keep it around.
12335
+
12336
+ **instructions**(scope: ClaimSetScope, claimSetId: string, rangeId: string, format: 'pdf' | 'md' = 'pdf') → `Promise<ClaimSetRangeFile>`
12337
+ The manufacturer's instruction sheet: PDF (base64) or Markdown.
12338
+
12339
+ **importUids**(scope: ClaimSetScope, claimSetId: string, rangeId: string, params: ImportClaimSetRangeUidsRequest) → `Promise<ImportClaimSetRangeUidsResponse>`
12340
+ The manufacturer's file back: check it (commit false), then write the chip UIDs (commit true).
12341
+
12342
+ **assign**(scope: ClaimSetScope, claimSetId: string, rangeId: string, params: AssignClaimSetRangeRequest) → `Promise<AssignClaimSetRangeResponse>`
12343
+ Point the range (or from–to within it) at a collection / product / batch, or clear it.
12344
+
12122
12345
  ### collection
12123
12346
 
12124
12347
  **get**(collectionId: string, admin?: boolean) → `Promise<CollectionResponse>`
@@ -13030,6 +13253,23 @@ Get individual order items for a specific product. Returns all matching items wi
13030
13253
  params?: GetOrderIdsParams) → `Promise<GetOrderIdsResponse>`
13031
13254
  Get unique order IDs containing items for a specific product. Lightweight query that only returns order IDs, not full order objects. ```typescript // Get order IDs for a product const productOrders = await order.getOrderIdsByAttribute( 'coll_123', 'productId', 'prod_789', { limit: 500 } ) ```
13032
13255
 
13256
+ ### partner
13257
+
13258
+ **list**() → `Promise<Partner[]>`
13259
+ The partners you belong to, with your role in each. Empty for everyone else.
13260
+
13261
+ **get**(partnerId: string) → `Promise<Partner>`
13262
+ One partner (owners also see its members).
13263
+
13264
+ **claimSets**(partnerId: string) → `Promise<PartnerClaimSet[]>`
13265
+ The partner's claim sets.
13266
+
13267
+ **getClaimSet**(partnerId: string, claimSetId: string) → `Promise<PartnerClaimSet>`
13268
+ One of the partner's claim sets.
13269
+
13270
+ **createClaimSet**(partnerId: string, params: CreatePartnerClaimSetRequest) → `Promise<PartnerClaimSet>`
13271
+ A new claim set for the partner, on one of its tap domains (owners and admins).
13272
+
13033
13273
  ### podcast
13034
13274
 
13035
13275
  **generate**(collectionId: string,
@@ -0,0 +1,75 @@
1
+ # NFC tag ranges and partners
2
+
3
+ For partners and enterprise clients who run their own tag programmes: mint a batch of tags, send them
4
+ to a manufacturer with instructions, get the chip UIDs back, then put the tags to work for a client or
5
+ product. Ordinary business collections don't use this.
6
+
7
+ ## The words
8
+
9
+ | Word | Means |
10
+ |---|---|
11
+ | **Claim set** | A pool of codes on one tap domain and one tag type (NTAG 424 DNA, NTAG 213/215/216, or QR). Most partners need one. |
12
+ | **Range** | A named, contiguous block of tags inside a set: "Tap to donate - February", 100 tags, indexes 201-300. |
13
+ | **Index** | A tag's place in its set. Ranges never overlap and indexes are never reused. |
14
+ | **Partner** | An organisation above collections that owns sets and assigns their tags into collections its people administer. Set up by SmartLinks staff. |
15
+
16
+ ## Who can do what
17
+
18
+ `scope` is `{ partnerId }` for a partner's sets, or `{ collectionId }` for a collection's own sets.
19
+
20
+ | Role | Can |
21
+ |---|---|
22
+ | Partner owner / admin, or a collection admin | Make sets, mint ranges, download the printer file and instructions, import UIDs, program tags, assign |
23
+ | Partner assigner | See ranges and tags (never keys), assign |
24
+
25
+ Assigning always needs you to be an admin of the collection the tags go to.
26
+
27
+ ## The flow
28
+
29
+ ```ts
30
+ import { partner, claimSet } from '@proveanything/smartlinks'
31
+
32
+ const [p] = await partner.list() // the partners you belong to
33
+ const scope = { partnerId: p.id }
34
+ const [set] = await partner.claimSets(p.id) // or partner.createClaimSet(p.id, { name, tagType: 'ntag424', tapDomain })
35
+
36
+ // 1. Mint
37
+ const range = await claimSet.ranges.create(scope, set.id, { name: 'Tap to donate - February', quantity: 100 })
38
+
39
+ // 2. Send to print: the CSV (carries each tag's key) and the instruction sheet
40
+ const file = await claimSet.ranges.exportFile(scope, set.id, range.id) // { filename, csv }
41
+ const sheet = await claimSet.ranges.instructions(scope, set.id, range.id, 'pdf') // { filename, base64 }
42
+ await claimSet.ranges.update(scope, set.id, range.id, { stage: 'sent', manufacturer: 'Print Co', orderRef: 'PO-123' })
43
+
44
+ // 3. Receive: the manufacturer sends back the same file with UID filled in
45
+ const check = await claimSet.ranges.importUids(scope, set.id, range.id, { csv: returnedCsv })
46
+ // check.rows: ok / index_mismatch / bad_uid / uid_in_use / … ; nothing written yet
47
+ await claimSet.ranges.importUids(scope, set.id, range.id, { csv: returnedCsv, commit: true })
48
+
49
+ // 4. Assign all of it, or from-to
50
+ await claimSet.ranges.assign(scope, set.id, range.id, { collectionId: 'reading-fc', productId: 'scarf', start: 201, end: 250 })
51
+ ```
52
+
53
+ **Programming on a phone instead of a manufacturer** (Sidekick): `ranges.next()` gives the lowest
54
+ tag without a chip, with its URL and key; write it, then `ranges.recordTag(scope, set.id, range.id, code, { uid })`.
55
+
56
+ ## The printer file
57
+
58
+ One row per tag, in index order. The manufacturer fills in `UID` and sends the same file back; rows are
59
+ matched by `Code`, and `Index` is checked.
60
+
61
+ | Tag type | Columns |
62
+ |---|---|
63
+ | NTAG 424 DNA | `Index, Code, URL, PiccOffset, Key, UID` |
64
+ | NTAG 213/215/216 | `Index, Code, URL, MirrorOffset, MirrorPage, MirrorByte, UID` |
65
+ | QR | `Index, Code, URL` |
66
+
67
+ `URL` is `https://{tap domain}/t/{set}/{code}` plus the placeholder the chip fills on every read
68
+ (`?p=` + 32 zeros for 424 SUN, `?m=00000000000000x000000` for the 21x UID and counter mirror). The
69
+ offsets are byte positions in the NDEF data, assuming the `https://` URI prefix code and a short record.
70
+
71
+ ## Notes
72
+
73
+ - Range responses are never cached by the SDK: tags change as they're programmed and several carry keys.
74
+ - A tag bound to a proof (minted or claimed) is never moved or cleared by `assign`; it's reported in `skipped`.
75
+ - `status` is the stage (`minted`, `sent`, `received`), or `assigned` once every tag is assigned.
@@ -99,6 +99,14 @@ renders correctly even against a host that sets none of them.
99
99
 
100
100
  Dark mode is a *theme* (a different value-set), not a separate token set — the host swaps the values.
101
101
 
102
+ - **Host:** in dark mode, send a dark value-set — in the iframe address (`#slt=` with `theme: "dark"`, and the
103
+ legacy `theme=` payload with `m: "d"`) or on the mount root. Sending only the hook (`dark=1`, `.dark`)
104
+ leaves the app to guess.
105
+ - **App:** `theme.css` has dark fallbacks under `.dark` / `[data-theme="dark"]`, so an app built on it stays
106
+ readable even when a host sends the hook and no values. An app with its own CSS variables should do the
107
+ same: a light fallback at the root, a dark one under the hook — never a light colour that also stands
108
+ in for dark.
109
+
102
110
  ---
103
111
 
104
112
  ## 3. Theme value-sets (what the AI emits, what the lookbook stores)