@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.
@@ -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)
package/dist/http.js CHANGED
@@ -219,6 +219,9 @@ function shouldSkipCache(path) {
219
219
  // Bookings change by the second (places left, busy times, a booking's status) — never serve them stale.
220
220
  if (/\/app\/[^/]+\/bookings(\/|\?|$)/i.test(path))
221
221
  return true;
222
+ // Tag ranges change as tags are programmed, and carry the tags' keys — never cached, never persisted.
223
+ if (/\/claimSet\/[^/]+\/ranges(\/|\?|$)/i.test(path))
224
+ return true;
222
225
  return false;
223
226
  }
224
227
  /** Evict the oldest (LRU) entry when the cache is at capacity. */
@@ -0,0 +1,14 @@
1
+ export interface IdbConnection {
2
+ /** The open connection, opening one if needed. */
3
+ open(): Promise<IDBDatabase>;
4
+ /** Forget the current connection so the next call re-opens. */
5
+ drop(closed?: IDBDatabase): void;
6
+ /**
7
+ * Run one transaction against the object store. If the connection is
8
+ * closed, re-open it and try once more. Errors from `body` are not retried.
9
+ */
10
+ run<T>(mode: IDBTransactionMode, body: (store: IDBObjectStore, tx: IDBTransaction) => T | Promise<T>): Promise<T>;
11
+ }
12
+ /** True for the error a transaction throws on a connection the browser closed. */
13
+ export declare function isClosedConnectionError(error: unknown): boolean;
14
+ export declare function createIdbConnection(dbName: string, storeName: string, version?: number): IdbConnection;
@@ -0,0 +1,91 @@
1
+ // src/idbConnection.ts
2
+ // Shared IndexedDB connection handling for the SDK's caches.
3
+ //
4
+ // The browser may close an IndexedDB connection behind our back: iOS Safari
5
+ // does it when the tab goes to the background, and any browser does it on
6
+ // storage eviction or a version change. A transaction on a closed connection
7
+ // throws "The database connection is closing", so a cache that keeps one
8
+ // connection for the life of the page silently stops working after that.
9
+ //
10
+ // createIdbConnection() forgets a connection the browser closes and re-opens
11
+ // on the next call; run() retries a transaction once when the connection
12
+ // turns out to be closed.
13
+ /** True for the error a transaction throws on a connection the browser closed. */
14
+ export function isClosedConnectionError(error) {
15
+ const e = error;
16
+ if (!e)
17
+ return false;
18
+ if (e.name === 'InvalidStateError')
19
+ return true;
20
+ const msg = String(e.message || '');
21
+ return /connection is closing|database connection|closed/i.test(msg);
22
+ }
23
+ export function createIdbConnection(dbName, storeName, version = 1) {
24
+ let db = null;
25
+ let opening = null;
26
+ const drop = (closed) => {
27
+ // A newer connection may already have replaced the one that closed.
28
+ if (closed && db && db !== closed)
29
+ return;
30
+ db = null;
31
+ opening = null;
32
+ };
33
+ const open = () => {
34
+ if (db)
35
+ return Promise.resolve(db);
36
+ if (opening)
37
+ return opening;
38
+ opening = new Promise((resolve, reject) => {
39
+ try {
40
+ const req = indexedDB.open(dbName, version);
41
+ req.onupgradeneeded = (event) => {
42
+ const upgrading = event.target.result;
43
+ if (!upgrading.objectStoreNames.contains(storeName)) {
44
+ upgrading.createObjectStore(storeName);
45
+ }
46
+ };
47
+ req.onsuccess = () => {
48
+ const opened = req.result;
49
+ opened.onclose = () => drop(opened);
50
+ opened.onversionchange = () => {
51
+ opened.close();
52
+ drop(opened);
53
+ };
54
+ db = opened;
55
+ resolve(opened);
56
+ };
57
+ req.onerror = () => {
58
+ opening = null;
59
+ reject(req.error);
60
+ };
61
+ req.onblocked = () => {
62
+ opening = null;
63
+ reject(new Error(`[smartlinks] IndexedDB open blocked (${dbName})`));
64
+ };
65
+ }
66
+ catch (err) {
67
+ opening = null;
68
+ reject(err);
69
+ }
70
+ });
71
+ return opening;
72
+ };
73
+ const run = async (mode, body) => {
74
+ for (let attempt = 0;; attempt++) {
75
+ const current = await open();
76
+ let tx;
77
+ try {
78
+ tx = current.transaction(storeName, mode);
79
+ }
80
+ catch (err) {
81
+ if (attempt === 0 && isClosedConnectionError(err)) {
82
+ drop(current);
83
+ continue;
84
+ }
85
+ throw err;
86
+ }
87
+ return await body(tx.objectStore(storeName), tx);
88
+ }
89
+ };
90
+ return { open, drop, run };
91
+ }