@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.
@@ -5,10 +5,15 @@
5
5
  // isIdbAvailable() and wrapped in try/catch so that failures in private-
6
6
  // browsing, quota-exceeded situations, or server-side environments are always
7
7
  // silent — they never propagate errors to the caller.
8
+ //
9
+ // The connection itself is managed by idbConnection.ts, which re-opens it when
10
+ // the browser closes it behind our back (iOS Safari does this when the tab is
11
+ // backgrounded) instead of leaving the cache dead for the rest of the page.
12
+ import { createIdbConnection } from './idbConnection.js';
8
13
  const DB_NAME = 'smartlinks-sdk-cache';
9
14
  const STORE_NAME = 'responses';
10
15
  const DB_VERSION = 1;
11
- let dbPromise = null;
16
+ const connection = createIdbConnection(DB_NAME, STORE_NAME, DB_VERSION);
12
17
  /**
13
18
  * Returns true only in environments where IndexedDB is genuinely available.
14
19
  * False in Node.js, and in some private-browsing contexts that stub indexedDB
@@ -22,36 +27,13 @@ export function isIdbAvailable() {
22
27
  return false;
23
28
  }
24
29
  }
25
- function openDb() {
26
- if (dbPromise)
27
- return dbPromise;
28
- dbPromise = new Promise((resolve, reject) => {
29
- try {
30
- const req = indexedDB.open(DB_NAME, DB_VERSION);
31
- req.onupgradeneeded = (event) => {
32
- const db = event.target.result;
33
- if (!db.objectStoreNames.contains(STORE_NAME)) {
34
- db.createObjectStore(STORE_NAME);
35
- }
36
- };
37
- req.onsuccess = (event) => {
38
- resolve(event.target.result);
39
- };
40
- req.onerror = () => {
41
- dbPromise = null;
42
- reject(req.error);
43
- };
44
- req.onblocked = () => {
45
- dbPromise = null;
46
- reject(new Error('[smartlinks] IndexedDB open blocked'));
47
- };
48
- }
49
- catch (err) {
50
- dbPromise = null;
51
- reject(err);
52
- }
30
+ /** Resolve when the transaction settles, whatever the outcome. */
31
+ function settled(tx) {
32
+ return new Promise((resolve) => {
33
+ tx.oncomplete = () => resolve();
34
+ tx.onerror = () => resolve(); // quota exceeded etc — fail silently
35
+ tx.onabort = () => resolve();
53
36
  });
54
- return dbPromise;
55
37
  }
56
38
  /**
57
39
  * Read an entry from IndexedDB.
@@ -62,18 +44,11 @@ export async function idbGet(key) {
62
44
  if (!isIdbAvailable())
63
45
  return null;
64
46
  try {
65
- const db = await openDb();
66
- return new Promise((resolve) => {
67
- try {
68
- const tx = db.transaction(STORE_NAME, 'readonly');
69
- const req = tx.objectStore(STORE_NAME).get(key);
70
- req.onsuccess = () => { var _a; return resolve((_a = req.result) !== null && _a !== void 0 ? _a : null); };
71
- req.onerror = () => resolve(null);
72
- }
73
- catch (_a) {
74
- resolve(null);
75
- }
76
- });
47
+ return await connection.run('readonly', (store) => new Promise((resolve) => {
48
+ const req = store.get(key);
49
+ req.onsuccess = () => { var _a; return resolve((_a = req.result) !== null && _a !== void 0 ? _a : null); };
50
+ req.onerror = () => resolve(null);
51
+ }));
77
52
  }
78
53
  catch (_a) {
79
54
  return null;
@@ -88,18 +63,9 @@ export async function idbSet(key, entry) {
88
63
  if (!isIdbAvailable())
89
64
  return;
90
65
  try {
91
- const db = await openDb();
92
- await new Promise((resolve) => {
93
- try {
94
- const tx = db.transaction(STORE_NAME, 'readwrite');
95
- tx.objectStore(STORE_NAME).put(entry, key);
96
- tx.oncomplete = () => resolve();
97
- tx.onerror = () => resolve(); // quota exceeded etc — fail silently
98
- tx.onabort = () => resolve();
99
- }
100
- catch (_a) {
101
- resolve();
102
- }
66
+ await connection.run('readwrite', (store, tx) => {
67
+ store.put(entry, key);
68
+ return settled(tx);
103
69
  });
104
70
  }
105
71
  catch (_a) {
@@ -114,18 +80,9 @@ export async function idbDelete(key) {
114
80
  if (!isIdbAvailable())
115
81
  return;
116
82
  try {
117
- const db = await openDb();
118
- await new Promise((resolve) => {
119
- try {
120
- const tx = db.transaction(STORE_NAME, 'readwrite');
121
- tx.objectStore(STORE_NAME).delete(key);
122
- tx.oncomplete = () => resolve();
123
- tx.onerror = () => resolve();
124
- tx.onabort = () => resolve();
125
- }
126
- catch (_a) {
127
- resolve();
128
- }
83
+ await connection.run('readwrite', (store, tx) => {
84
+ store.delete(key);
85
+ return settled(tx);
129
86
  });
130
87
  }
131
88
  catch (_a) { }
@@ -138,41 +95,25 @@ export async function idbClear(pattern) {
138
95
  if (!isIdbAvailable())
139
96
  return;
140
97
  try {
141
- const db = await openDb();
142
98
  if (!pattern) {
143
- await new Promise((resolve) => {
144
- try {
145
- const tx = db.transaction(STORE_NAME, 'readwrite');
146
- tx.objectStore(STORE_NAME).clear();
147
- tx.oncomplete = () => resolve();
148
- tx.onerror = () => resolve();
149
- tx.onabort = () => resolve();
150
- }
151
- catch (_a) {
152
- resolve();
153
- }
99
+ await connection.run('readwrite', (store, tx) => {
100
+ store.clear();
101
+ return settled(tx);
154
102
  });
155
103
  return;
156
104
  }
157
105
  // Pattern-based: enumerate all keys and delete matching ones.
158
- await new Promise((resolve) => {
159
- try {
160
- const tx = db.transaction(STORE_NAME, 'readwrite');
161
- const store = tx.objectStore(STORE_NAME);
162
- const req = store.getAllKeys();
163
- req.onsuccess = () => {
164
- for (const key of req.result) {
165
- if (key.includes(pattern))
166
- store.delete(key);
167
- }
168
- resolve();
169
- };
170
- req.onerror = () => resolve();
171
- }
172
- catch (_a) {
106
+ await connection.run('readwrite', (store) => new Promise((resolve) => {
107
+ const req = store.getAllKeys();
108
+ req.onsuccess = () => {
109
+ for (const key of req.result) {
110
+ if (key.includes(pattern))
111
+ store.delete(key);
112
+ }
173
113
  resolve();
174
- }
175
- });
114
+ };
115
+ req.onerror = () => resolve();
116
+ }));
176
117
  }
177
118
  catch (_a) { }
178
119
  }
package/dist/theme.css CHANGED
@@ -41,6 +41,26 @@
41
41
  --shadow-md: var(--sl-shadow-md, 0 0 #0000);
42
42
  }
43
43
 
44
+ /* Dark mode without values. A host in dark mode should send a dark value-set (the --sl-* tokens); one
45
+ * that only sets the hook (.dark / data-theme="dark") left every colour on its light fallback — white
46
+ * cards and near-black text on a dark page. Under the hook the fallbacks are dark too; whatever the host
47
+ * does send still wins. (Re-declared on the hook's element, so it works wherever the host puts it.) */
48
+ .dark,
49
+ [data-theme="dark"] {
50
+ --color-background: var(--sl-color-bg, #09090b);
51
+ --color-surface: var(--sl-color-surface, #18181b);
52
+ --color-surface-raised:var(--sl-color-surface-raised, var(--sl-color-surface, #27272a));
53
+ --color-foreground: var(--sl-color-fg, #fafafa);
54
+ --color-muted: var(--sl-color-muted, #a1a1aa);
55
+ --color-border: var(--sl-color-border, #3f3f46);
56
+ --color-primary: var(--sl-color-accent, #6366f1);
57
+ --color-on-primary: var(--sl-color-on-accent, #ffffff);
58
+ --color-primary-soft: var(--sl-color-accent-soft, color-mix(in oklab, var(--sl-color-accent, #6366f1) 20%, transparent));
59
+ --color-success: var(--sl-color-success, #22c55e);
60
+ --color-danger: var(--sl-color-danger, #f87171);
61
+ color-scheme: dark;
62
+ }
63
+
44
64
  /* shadcn bridge (optional): if the app uses shadcn's --primary/--background/etc. HSL variables,
45
65
  * uncomment to drive them from the SmartLinks tokens too. Left commented so it never overrides an
46
66
  * app that has deliberately customised its shadcn palette — opt in per app (see migration step 17).
@@ -1,8 +1,9 @@
1
+ import { createIdbConnection } from './idbConnection.js';
1
2
  const DB_NAME = 'smartlinks-sdk-translations';
2
3
  const STORE_NAME = 'translations';
3
4
  const DB_VERSION = 1;
4
5
  const DEFAULT_LOCAL_TRANSLATION_TTL_MS = 90 * 24 * 60 * 60000;
5
- let dbPromise = null;
6
+ const connection = createIdbConnection(DB_NAME, STORE_NAME, DB_VERSION);
6
7
  const memoryTranslationCache = new Map();
7
8
  export function getDefaultTranslationCacheTtlMs() {
8
9
  return DEFAULT_LOCAL_TRANSLATION_TTL_MS;
@@ -15,36 +16,13 @@ function isIndexedDbAvailable() {
15
16
  return false;
16
17
  }
17
18
  }
18
- function openDb() {
19
- if (dbPromise)
20
- return dbPromise;
21
- dbPromise = new Promise((resolve, reject) => {
22
- try {
23
- const request = indexedDB.open(DB_NAME, DB_VERSION);
24
- request.onupgradeneeded = (event) => {
25
- const db = event.target.result;
26
- if (!db.objectStoreNames.contains(STORE_NAME)) {
27
- db.createObjectStore(STORE_NAME);
28
- }
29
- };
30
- request.onsuccess = (event) => {
31
- resolve(event.target.result);
32
- };
33
- request.onerror = () => {
34
- dbPromise = null;
35
- reject(request.error);
36
- };
37
- request.onblocked = () => {
38
- dbPromise = null;
39
- reject(new Error('[smartlinks] Translation IndexedDB open blocked'));
40
- };
41
- }
42
- catch (error) {
43
- dbPromise = null;
44
- reject(error);
45
- }
19
+ /** Resolve when the transaction settles, whatever the outcome. */
20
+ function settled(tx) {
21
+ return new Promise((resolve) => {
22
+ tx.oncomplete = () => resolve();
23
+ tx.onerror = () => resolve();
24
+ tx.onabort = () => resolve();
46
25
  });
47
- return dbPromise;
48
26
  }
49
27
  function stableContextKey(context) {
50
28
  if (!context)
@@ -123,33 +101,26 @@ async function getEntry(key) {
123
101
  if (!isIndexedDbAvailable())
124
102
  return null;
125
103
  try {
126
- const db = await openDb();
127
- return await new Promise((resolve) => {
128
- try {
129
- const tx = db.transaction(STORE_NAME, 'readonly');
130
- const request = tx.objectStore(STORE_NAME).get(key);
131
- request.onsuccess = () => {
132
- var _a;
133
- const result = (_a = request.result) !== null && _a !== void 0 ? _a : null;
134
- if (!result) {
135
- resolve(null);
136
- return;
137
- }
138
- if (isExpired(result)) {
139
- memoryTranslationCache.delete(key);
140
- void deleteEntries([key]);
141
- resolve(null);
142
- return;
143
- }
144
- memoryTranslationCache.set(key, result);
145
- resolve(result);
146
- };
147
- request.onerror = () => resolve(null);
148
- }
149
- catch (_a) {
150
- resolve(null);
151
- }
152
- });
104
+ return await connection.run('readonly', (store) => new Promise((resolve) => {
105
+ const request = store.get(key);
106
+ request.onsuccess = () => {
107
+ var _a;
108
+ const result = (_a = request.result) !== null && _a !== void 0 ? _a : null;
109
+ if (!result) {
110
+ resolve(null);
111
+ return;
112
+ }
113
+ if (isExpired(result)) {
114
+ memoryTranslationCache.delete(key);
115
+ void deleteEntries([key]);
116
+ resolve(null);
117
+ return;
118
+ }
119
+ memoryTranslationCache.set(key, result);
120
+ resolve(result);
121
+ };
122
+ request.onerror = () => resolve(null);
123
+ }));
153
124
  }
154
125
  catch (_a) {
155
126
  return null;
@@ -170,21 +141,11 @@ async function setEntries(entries) {
170
141
  if (!isIndexedDbAvailable())
171
142
  return;
172
143
  try {
173
- const db = await openDb();
174
- await new Promise((resolve) => {
175
- try {
176
- const tx = db.transaction(STORE_NAME, 'readwrite');
177
- const store = tx.objectStore(STORE_NAME);
178
- for (const [key, entry] of entries) {
179
- store.put(entry, key);
180
- }
181
- tx.oncomplete = () => resolve();
182
- tx.onerror = () => resolve();
183
- tx.onabort = () => resolve();
184
- }
185
- catch (_a) {
186
- resolve();
144
+ await connection.run('readwrite', (store, tx) => {
145
+ for (const [key, entry] of entries) {
146
+ store.put(entry, key);
187
147
  }
148
+ return settled(tx);
188
149
  });
189
150
  }
190
151
  catch (_a) {
@@ -226,21 +187,11 @@ async function deleteEntries(keys) {
226
187
  if (!isIndexedDbAvailable() || keys.length === 0)
227
188
  return;
228
189
  try {
229
- const db = await openDb();
230
- await new Promise((resolve) => {
231
- try {
232
- const tx = db.transaction(STORE_NAME, 'readwrite');
233
- const store = tx.objectStore(STORE_NAME);
234
- for (const key of keys) {
235
- store.delete(key);
236
- }
237
- tx.oncomplete = () => resolve();
238
- tx.onerror = () => resolve();
239
- tx.onabort = () => resolve();
240
- }
241
- catch (_a) {
242
- resolve();
190
+ await connection.run('readwrite', (store, tx) => {
191
+ for (const key of keys) {
192
+ store.delete(key);
243
193
  }
194
+ return settled(tx);
244
195
  });
245
196
  }
246
197
  catch (_a) {
@@ -256,41 +207,25 @@ export async function clearCachedTranslations(collectionId) {
256
207
  if (!isIndexedDbAvailable())
257
208
  return;
258
209
  try {
259
- const db = await openDb();
260
210
  if (!collectionId) {
261
- await new Promise((resolve) => {
262
- try {
263
- const tx = db.transaction(STORE_NAME, 'readwrite');
264
- tx.objectStore(STORE_NAME).clear();
265
- tx.oncomplete = () => resolve();
266
- tx.onerror = () => resolve();
267
- tx.onabort = () => resolve();
268
- }
269
- catch (_a) {
270
- resolve();
271
- }
211
+ await connection.run('readwrite', (store, tx) => {
212
+ store.clear();
213
+ return settled(tx);
272
214
  });
273
215
  return;
274
216
  }
275
- await new Promise((resolve) => {
276
- try {
277
- const tx = db.transaction(STORE_NAME, 'readwrite');
278
- const store = tx.objectStore(STORE_NAME);
279
- const request = store.getAllKeys();
280
- request.onsuccess = () => {
281
- for (const key of request.result) {
282
- if (key.startsWith(`${collectionId}::`)) {
283
- store.delete(key);
284
- }
217
+ await connection.run('readwrite', (store) => new Promise((resolve) => {
218
+ const request = store.getAllKeys();
219
+ request.onsuccess = () => {
220
+ for (const key of request.result) {
221
+ if (key.startsWith(`${collectionId}::`)) {
222
+ store.delete(key);
285
223
  }
286
- resolve();
287
- };
288
- request.onerror = () => resolve();
289
- }
290
- catch (_a) {
224
+ }
291
225
  resolve();
292
- }
293
- });
226
+ };
227
+ request.onerror = () => resolve();
228
+ }));
294
229
  }
295
230
  catch (_a) {
296
231
  // Ignore deletion failures.
@@ -122,3 +122,188 @@ export interface CreateClaimSetTagResponse {
122
122
  /** Arbitrary extra fields */
123
123
  data?: Record<string, unknown>;
124
124
  }
125
+ /** Who a range call acts as: a collection on its own sets, or a partner on the partner's sets. */
126
+ export type ClaimSetScope = {
127
+ collectionId: string;
128
+ } | {
129
+ partnerId: string;
130
+ };
131
+ export type ClaimSetTagType = 'ntag424' | 'ntag21x' | 'qr';
132
+ export type ClaimSetRangeStatus = 'minting' | 'minted' | 'sent' | 'received' | 'assigned';
133
+ export interface ClaimSetRange {
134
+ id: string;
135
+ claimSetId: string;
136
+ namespace: string;
137
+ name: string;
138
+ notes: string | null;
139
+ startIndex: number;
140
+ endIndex: number;
141
+ quantity: number;
142
+ tagType: ClaimSetTagType;
143
+ fulfilment: 'printer' | 'sidekick';
144
+ /** The stored step: minting | minted | sent | received */
145
+ stage: 'minting' | 'minted' | 'sent' | 'received';
146
+ /** What to show: the stage, or 'assigned' once every tag is assigned */
147
+ status: ClaimSetRangeStatus;
148
+ intendedCollectionId: string | null;
149
+ intendedProductId: string | null;
150
+ intendedBatchId: string | null;
151
+ manufacturer: string | null;
152
+ orderRef: string | null;
153
+ counts: {
154
+ programmed: number;
155
+ assigned: number;
156
+ claimed: number;
157
+ };
158
+ sentAt: string | null;
159
+ receivedAt: string | null;
160
+ createdBy: string | null;
161
+ createdAt: string;
162
+ updatedAt: string;
163
+ }
164
+ export interface CreateClaimSetRangeRequest {
165
+ name: string;
166
+ /** 1 to 10,000 */
167
+ quantity: number;
168
+ /** Pre-assign every tag at mint time (you must administer the collection) */
169
+ intendedCollectionId?: string;
170
+ intendedProductId?: string;
171
+ intendedBatchId?: string;
172
+ fulfilment?: 'printer' | 'sidekick';
173
+ notes?: string;
174
+ manufacturer?: string;
175
+ orderRef?: string;
176
+ }
177
+ export interface UpdateClaimSetRangeRequest {
178
+ name?: string;
179
+ notes?: string | null;
180
+ manufacturer?: string | null;
181
+ orderRef?: string | null;
182
+ fulfilment?: 'printer' | 'sidekick';
183
+ stage?: 'minted' | 'sent' | 'received';
184
+ }
185
+ export interface ClaimSetRangeTag {
186
+ code: string;
187
+ /** The printed code: 3-character set id + code */
188
+ printed: string;
189
+ index: number;
190
+ uid: string | null;
191
+ /** Only for owners/admins, and only when asked for */
192
+ key?: string;
193
+ collectionId: string | null;
194
+ productId: string | null;
195
+ batchId: string | null;
196
+ proofId: string | null;
197
+ programmedAt: string | null;
198
+ scans: number;
199
+ }
200
+ export interface ClaimSetRangeTagsPage {
201
+ tags: ClaimSetRangeTag[];
202
+ /** The index to pass as `from` for the next page, or null */
203
+ next: number | null;
204
+ }
205
+ /** The next tag to program on a phone, with what to write. */
206
+ export interface ClaimSetRangeNextTag extends ClaimSetRangeTag {
207
+ url: string;
208
+ piccOffset?: number;
209
+ mirrorOffset?: number;
210
+ remaining: number;
211
+ }
212
+ export type ClaimSetImportResult = 'ok' | 'unchanged' | 'unknown_code' | 'duplicate_row' | 'index_mismatch' | 'no_uid' | 'bad_uid' | 'duplicate_uid' | 'uid_in_use' | 'uid_conflict';
213
+ export interface ImportClaimSetRangeUidsRequest {
214
+ /** The manufacturer's file as CSV text (the file we sent, with UID filled in) */
215
+ csv?: string;
216
+ /** …or already-parsed rows: [{ Code, Index?, UID }] */
217
+ rows?: Array<Record<string, string | number>>;
218
+ /** false (default) only checks; true writes the UIDs that matched */
219
+ commit?: boolean;
220
+ }
221
+ export interface ImportClaimSetRangeUidsResponse {
222
+ summary: {
223
+ rows: number;
224
+ ok: number;
225
+ unchanged: number;
226
+ problems: number;
227
+ notInFile: number;
228
+ committed: boolean;
229
+ };
230
+ rows: Array<{
231
+ line: number;
232
+ code: string;
233
+ index: number | null;
234
+ uid: string;
235
+ result: ClaimSetImportResult;
236
+ message?: string;
237
+ }>;
238
+ range: ClaimSetRange;
239
+ }
240
+ export interface AssignClaimSetRangeRequest {
241
+ /** Required unless clear */
242
+ collectionId?: string;
243
+ productId?: string;
244
+ batchId?: string;
245
+ /** Default: the whole range */
246
+ start?: number;
247
+ end?: number;
248
+ /** Unassign instead (tags bound to a proof are never cleared) */
249
+ clear?: boolean;
250
+ data?: Record<string, unknown>;
251
+ }
252
+ export interface AssignClaimSetRangeResponse {
253
+ changed: number;
254
+ skipped: Array<{
255
+ code: string;
256
+ index: number;
257
+ reason: 'other_range' | 'has_proof' | 'assigned_elsewhere';
258
+ collectionId?: string;
259
+ }>;
260
+ range: ClaimSetRange;
261
+ }
262
+ export interface ClaimSetRangeFile {
263
+ filename: string;
264
+ contentType: string;
265
+ /** export: the printer file */
266
+ csv?: string;
267
+ /** instructions as PDF: the file, base64 */
268
+ base64?: string;
269
+ /** instructions as Markdown */
270
+ markdown?: string;
271
+ }
272
+ export interface Partner {
273
+ id: string;
274
+ name: string;
275
+ status: 'active' | 'suspended';
276
+ tapDomains: string[];
277
+ features: Record<string, boolean>;
278
+ /** Your role (from partner.list / partner.get) */
279
+ role?: 'owner' | 'admin' | 'assigner';
280
+ /** Owners only */
281
+ members?: Array<{
282
+ userId: string;
283
+ role: 'owner' | 'admin' | 'assigner';
284
+ grantedBy: string | null;
285
+ grantedAt: string;
286
+ }>;
287
+ createdAt: string;
288
+ updatedAt: string;
289
+ }
290
+ export interface PartnerClaimSet {
291
+ id: string;
292
+ namespace: string;
293
+ name: string | null;
294
+ tagType: ClaimSetTagType;
295
+ tapDomain: string;
296
+ owner: {
297
+ type: 'platform' | 'partner' | 'collection' | 'user';
298
+ id: string | null;
299
+ };
300
+ codes: number;
301
+ deleted: boolean;
302
+ createdAt: string | null;
303
+ }
304
+ export interface CreatePartnerClaimSetRequest {
305
+ name: string;
306
+ tagType: ClaimSetTagType;
307
+ /** One of the partner's tap domains; default smartlinks.app */
308
+ tapDomain?: string;
309
+ }