@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.
- package/dist/api/claimSet.d.ts +37 -1
- package/dist/api/claimSet.js +83 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/partner.d.ts +19 -0
- package/dist/api/partner.js +35 -0
- package/dist/docs/API_SUMMARY.md +275 -35
- package/dist/docs/tag-ranges.md +75 -0
- package/dist/docs/theme-tokens.md +8 -0
- package/dist/http.js +3 -0
- package/dist/idbConnection.d.ts +14 -0
- package/dist/idbConnection.js +91 -0
- package/dist/openapi.yaml +1494 -680
- package/dist/persistentCache.js +36 -95
- package/dist/theme.css +20 -0
- package/dist/translationCache.js +49 -114
- package/dist/types/claimSet.d.ts +185 -0
- package/docs/API_SUMMARY.md +275 -35
- package/docs/tag-ranges.md +75 -0
- package/docs/theme-tokens.md +8 -0
- package/openapi.yaml +1494 -680
- package/package.json +87 -87
- package/scripts/publish.mjs +22 -7
|
@@ -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
|
+
}
|