@proveanything/smartlinks 2.0.0-alpha.4 → 2.0.0-alpha.5

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.
@@ -3,7 +3,13 @@ export interface AllocateSequenceInput {
3
3
  appId: string;
4
4
  /** The configured sequence id — the key of data.sequenceConfigs on the app config. */
5
5
  sequenceId: string;
6
- /** The STABLE subject identity — the claim-set id from the tap (NOT a per-tap virtual id). */
6
+ /**
7
+ * The STABLE subject identity. For a `claimSet` sequence, pass the tap's virtual-proof id
8
+ * `<claimSetId>-<code>` (e.g. "23-oOkf8o") for a per-wristband raffle — the code is the tag's
9
+ * permanent id, so re-taps of the same wristband return the same number; the number is stamped
10
+ * on that code's doc and surfaces in `tagData`. Pass a bare claim-set id for one number per
11
+ * group. Never a value that changes per tap.
12
+ */
7
13
  subjectId: string;
8
14
  /** Optional product scope, when the sequence config is scoped per product. */
9
15
  productId?: string;
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.0-alpha.4 | Generated: 2026-09-14T15:08:56.614Z
3
+ Version: 2.0.0-alpha.5 | Generated: 2026-09-14T19:49:23.024Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -159,6 +159,12 @@ The Smartlinks SDK is organized into the following namespaces:
159
159
 
160
160
  Core HTTP functions for API configuration and communication:
161
161
 
162
+ **getHttpCacheDiagnostics**() → `void`
163
+ Snapshot of cache counters + current keys. Call from the console via `window.__slHttpDiag()`.
164
+
165
+ **resetHttpCacheDiagnostics**() → `void`
166
+ Reset the diagnostics counters (does not touch the cache itself).
167
+
162
168
  **isProxyEnabled**() → `boolean`
163
169
  Return whether proxy mode is currently enabled.
164
170
 
@@ -45,22 +45,27 @@ subject first and return the number it already has, rather than allocating a sec
45
45
  ## Idempotency subject — use a *stable* id
46
46
 
47
47
  The number is deduped by the **subject id** you pass, so it must be the **stable** identity:
48
- - **Claim set id** (the wristband's permanent record) — the right key when people aren't
49
- logged in. A re-tap resolves to the same claim set same number.
48
+ - **Virtual-proof id `<claimSetId>-<code>`** (e.g. `23-oOkf8o`) — the right key for a
49
+ per-wristband raffle. The `code` is the physical tag's permanent id, so a re-tap of the same
50
+ wristband resolves to the same code doc → same number. The virtual proof record is transient,
51
+ but the **code** inside its id is stable — that's what the sink keys on (it splits the id and
52
+ uses only `<claimSetId>` + `<code>`).
53
+ - **Bare claim-set id** — when one number per *group* is intended.
50
54
  - The authenticated **user/contact** — if they sign in / claim.
51
55
 
52
- Do **not** key on a value that changes per interaction (e.g. a virtual proof id minted fresh
53
- on each tap) that would let one person take several numbers.
56
+ Do **not** key on the transient part of a per-tap identity (a freshly-minted virtual proof
57
+ record) — key on the stable **code**, which is exactly what the `<claimSetId>-<code>` subject
58
+ carries.
54
59
 
55
60
  ## Where the number is stored (the sink)
56
61
 
57
62
  The stamp target is configurable — the number lives wherever you'll read it:
58
63
 
59
- | Target | Use |
60
- |---|---|
61
- | `claimSet` | The Firestore wristband record fast, no proof mint on the hot path. |
62
- | `proof` | A minted proof (value or attestation) — when the number should travel with the proof. |
63
- | `appRecord` | A structured app record — for queryable, per-app data. |
64
+ | Target | Subject id | Use |
65
+ |---|---|---|
66
+ | `claimSet` | the virtual-proof id `<claimSetId>-<code>` (e.g. `23-oOkf8o`) **or** a bare `<claimSetId>` | The Firestore claim-set world. **The subject decides the exact doc:** a `<claimSetId>-<code>` id stamps the individual **code** doc (per-wristband — rides back as `tagData` on the next tap, every wristband its own number); a bare id stamps the whole **set** doc (one shared value). Claim-set ids are hyphen-free, so a hyphen unambiguously means the per-code form. |
67
+ | `proof` | the proof id | A minted proof (value or attestation) — when the number should travel with the proof. |
68
+ | `appRecord` | the record id | A structured app record — for queryable, per-app data. |
64
69
 
65
70
  ## Configure the sequence (once, server-side)
66
71
 
@@ -72,14 +77,19 @@ makes the public endpoint safe: the target/field/scope are set by you, not the c
72
77
  {
73
78
  "raffle": {
74
79
  "key": "raffle:2026-cup", // the named counter (data.sequences.<key>)
75
- "target": "claimSet", // claimSet | proof | appRecord — where the number is stamped
76
- "field": "raffleNumber", // the property written on the target
77
- "productId": "wristbands-2026", // counter scope (optional; else collection-wide)
80
+ "target": "claimSet", // claimSet | proof | appRecord — the ledger domain
81
+ "field": "raffleNumber", // the property written on the target (comes back in tagData)
78
82
  "start": 1 // first number (optional, default 1)
83
+ // no productId → one collection-wide counter (all wristbands share the sequence)
79
84
  }
80
85
  }
81
86
  ```
82
87
 
88
+ > **Per-wristband raffle.** Keep `target: "claimSet"` and pass the tap's virtual-proof id
89
+ > (`<claimSetId>-<code>`) as the subject — the sink writes the individual tag's **code** doc, so
90
+ > each wristband gets its own number and it surfaces in `tagData` on the next tap. Pass a bare
91
+ > claim-set id instead only if you want one number for the whole group.
92
+
83
93
  ## Call it (the widget)
84
94
 
85
95
  Your widget calls one bounded, public endpoint on tap. It passes only the **subject id** —
@@ -87,12 +97,13 @@ never the target/field:
87
97
 
88
98
  ```
89
99
  POST /api/v1/public/collection/:collectionId/sequence/allocate
90
- { "appId": "raffle-app", "sequenceId": "raffle", "subjectId": "<claim set id>" }
100
+ { "appId": "liveWidgets", "sequenceId": "raffle", "subjectId": "23-oOkf8o" }
91
101
  → { "number": 42, "isNew": true } // re-tap → { "number": 42, "isNew": false }
92
102
  ```
93
103
 
94
- - **`subjectId`** is the STABLE identity the claim-set id from the tap. Re-taps collapse to
95
- one number.
104
+ - **`subjectId`** is the STABLE identity. For `claimCode` it's the virtual-proof id
105
+ `<claimSetId>-<code>` (e.g. `23-oOkf8o`) straight off the tap — re-taps of the same wristband
106
+ collapse to one number. (For `claimSet` it's the claim-set id.)
96
107
  - **Idempotent**, so both your flows are the *same call*:
97
108
  - **Auto:** on load, call allocate → get your number (existing or freshly minted).
98
109
  - **Button:** click → animate → same call → "Your raffle number is 42."
package/dist/http.d.ts CHANGED
@@ -1,3 +1,23 @@
1
+ /** Snapshot of cache counters + current keys. Call from the console via `window.__slHttpDiag()`. */
2
+ export declare function getHttpCacheDiagnostics(): {
3
+ size: number;
4
+ keys: string[];
5
+ inflight: string[];
6
+ cacheEnabled: boolean;
7
+ cacheDefaultTtlMs: number;
8
+ cachePersistence: "none" | "indexeddb";
9
+ cacheClearOnPageLoad: boolean;
10
+ l1Hits: number;
11
+ inflightDedups: number;
12
+ l2Hits: number;
13
+ networkFetches: number;
14
+ skips: number;
15
+ clears: number;
16
+ lastClearReason: string | null;
17
+ lastClearAt: number;
18
+ };
19
+ /** Reset the diagnostics counters (does not touch the cache itself). */
20
+ export declare function resetHttpCacheDiagnostics(): void;
1
21
  type Logger = {
2
22
  debug?: (...args: any[]) => void;
3
23
  info?: (...args: any[]) => void;
package/dist/http.js CHANGED
@@ -79,6 +79,68 @@ const TOKEN_STORAGE_KEY = 'sl:token';
79
79
  let cachePersistenceTtlMs = 7 * 24 * 60 * 60000;
80
80
  /** When true (default), serve stale L2 data via SmartlinksOfflineError on network failure. */
81
81
  let cacheServeStaleOnOffline = true;
82
+ const httpCacheStats = {
83
+ l1Hits: 0, inflightDedups: 0, l2Hits: 0, networkFetches: 0, skips: 0,
84
+ clears: 0, lastClearReason: null, lastClearAt: 0,
85
+ };
86
+ function httpDebugEnabled() {
87
+ var _a;
88
+ try {
89
+ if (typeof window !== 'undefined') {
90
+ if (window.__SL_HTTP_DEBUG__)
91
+ return true;
92
+ // URL opt-in so boot-time GETs can be traced on a deployed app without any
93
+ // code change: just append ?__slhttpdebug=1 (or &…) to the page URL and reload.
94
+ if (/[?&]__slhttpdebug=1\b/.test(((_a = window.location) === null || _a === void 0 ? void 0 : _a.search) || ''))
95
+ return true;
96
+ }
97
+ }
98
+ catch (_b) { }
99
+ return !!logger;
100
+ }
101
+ function httpDebug(...args) {
102
+ if (!httpDebugEnabled())
103
+ return;
104
+ try {
105
+ (console.info || console.log).call(console, '[sl-http]', ...args);
106
+ }
107
+ catch (_a) { }
108
+ }
109
+ /**
110
+ * Single choke-point for wiping the in-memory GET cache. Routing every clear
111
+ * through here means each one is counted and logged with a reason — so a test
112
+ * run shows exactly which cache clears fired and why (e.g. token change on a
113
+ * public page that should never have cleared).
114
+ */
115
+ function clearHttpCache(reason) {
116
+ httpCacheStats.clears++;
117
+ httpCacheStats.lastClearReason = reason;
118
+ httpCacheStats.lastClearAt = Date.now();
119
+ httpDebug('cache CLEAR', { reason, hadEntries: httpCache.size });
120
+ httpCache.clear();
121
+ }
122
+ /** Snapshot of cache counters + current keys. Call from the console via `window.__slHttpDiag()`. */
123
+ export function getHttpCacheDiagnostics() {
124
+ const keys = [...httpCache.keys()];
125
+ return Object.assign(Object.assign({}, httpCacheStats), { size: httpCache.size, keys, inflight: [...httpCache.entries()].filter(([, v]) => v.promise).map(([k]) => k), cacheEnabled, cacheDefaultTtlMs, cachePersistence, cacheClearOnPageLoad });
126
+ }
127
+ /** Reset the diagnostics counters (does not touch the cache itself). */
128
+ export function resetHttpCacheDiagnostics() {
129
+ httpCacheStats.l1Hits = 0;
130
+ httpCacheStats.inflightDedups = 0;
131
+ httpCacheStats.l2Hits = 0;
132
+ httpCacheStats.networkFetches = 0;
133
+ httpCacheStats.skips = 0;
134
+ httpCacheStats.clears = 0;
135
+ httpCacheStats.lastClearReason = null;
136
+ httpCacheStats.lastClearAt = 0;
137
+ }
138
+ // Expose on window for zero-wiring console access after a build.
139
+ try {
140
+ if (typeof window !== 'undefined')
141
+ window.__slHttpDiag = getHttpCacheDiagnostics;
142
+ }
143
+ catch (_a) { }
82
144
  /**
83
145
  * Per-resource TTL overrides — checked in order, first match wins.
84
146
  *
@@ -143,7 +205,7 @@ function evictLruIfNeeded() {
143
205
  function clearSessionCachesOnPageLoad() {
144
206
  if (typeof window === 'undefined')
145
207
  return; // Node.js environment
146
- httpCache.clear();
208
+ clearHttpCache('pageLoad session reset');
147
209
  try {
148
210
  if (typeof sessionStorage !== 'undefined') {
149
211
  const sessionKeys = Object.keys(sessionStorage).filter(k => k.startsWith('smartlinks:cache:'));
@@ -421,7 +483,7 @@ export function initializeApi(options) {
421
483
  // Clear both cache tiers on forced re-initialization so stale data
422
484
  // from the previous configuration cannot bleed through.
423
485
  if (options.force) {
424
- httpCache.clear();
486
+ clearHttpCache('initializeApi(force)');
425
487
  idbClear().catch(() => { });
426
488
  }
427
489
  logger = options.logger;
@@ -468,7 +530,7 @@ export function setBearerToken(token) {
468
530
  catch (_b) { }
469
531
  }
470
532
  }
471
- httpCache.clear();
533
+ clearHttpCache(`setBearerToken(${token ? 'set' : 'cleared'})`);
472
534
  if (cachePersistence !== 'none')
473
535
  idbClear().catch(() => { });
474
536
  }
@@ -492,7 +554,7 @@ export function setGrantToken(token) {
492
554
  if (token === grantToken)
493
555
  return;
494
556
  grantToken = token;
495
- httpCache.clear();
557
+ clearHttpCache(`setGrantToken(${token ? 'set' : 'cleared'})`);
496
558
  if (cachePersistence !== 'none')
497
559
  idbClear().catch(() => { });
498
560
  }
@@ -612,7 +674,7 @@ export function configureSdkCache(options) {
612
674
  */
613
675
  export function invalidateCache(urlPattern) {
614
676
  if (!urlPattern) {
615
- httpCache.clear();
677
+ clearHttpCache('invalidateCache(all)');
616
678
  if (cachePersistence !== 'none')
617
679
  idbClear().catch(() => { });
618
680
  return;
@@ -1094,19 +1156,28 @@ export async function request(path) {
1094
1156
  const skipCache = shouldSkipCache(path);
1095
1157
  const cacheKey = buildCacheKey(path);
1096
1158
  const ttl = skipCache ? 0 : getTtlForPath(path);
1159
+ if (skipCache) {
1160
+ httpCacheStats.skips++;
1161
+ httpDebug('GET skip-cache', { path });
1162
+ }
1097
1163
  if (!skipCache) {
1098
1164
  // 1. L1 hit — return from memory immediately
1099
1165
  const l1 = getHttpCacheHit(cacheKey, ttl);
1100
1166
  if (l1 !== null) {
1167
+ httpCacheStats.l1Hits++;
1168
+ httpDebug('GET L1 hit', { path, ttlMs: ttl });
1101
1169
  logDebug('[smartlinks] GET cache hit (L1)', { path });
1102
1170
  return l1;
1103
1171
  }
1104
1172
  // 2. In-flight deduplication — share an already-pending promise
1105
1173
  const inflight = httpCache.get(cacheKey);
1106
1174
  if (inflight === null || inflight === void 0 ? void 0 : inflight.promise) {
1175
+ httpCacheStats.inflightDedups++;
1176
+ httpDebug('GET in-flight dedup', { path });
1107
1177
  logDebug('[smartlinks] GET in-flight dedup', { path });
1108
1178
  return inflight.promise;
1109
1179
  }
1180
+ httpDebug('GET MISS → will fetch', { path, ttlMs: ttl, cacheKey });
1110
1181
  }
1111
1182
  // 3. Build the fetch promise.
1112
1183
  // The IIFE starts synchronously until its first `await`, then the outer
@@ -1117,6 +1188,8 @@ export async function request(path) {
1117
1188
  if (!skipCache && cachePersistence !== 'none') {
1118
1189
  const l2 = await idbGet(cacheKey);
1119
1190
  if (l2 && Date.now() - l2.timestamp <= ttl) {
1191
+ httpCacheStats.l2Hits++;
1192
+ httpDebug('GET L2 hit', { path });
1120
1193
  logDebug('[smartlinks] GET cache hit (L2)', { path });
1121
1194
  setHttpCacheEntry(cacheKey, l2.data);
1122
1195
  return l2.data;
@@ -1124,6 +1197,8 @@ export async function request(path) {
1124
1197
  }
1125
1198
  // 3b. Network fetch
1126
1199
  try {
1200
+ httpCacheStats.networkFetches++;
1201
+ httpDebug('GET NETWORK fetch', { path, skipCache, ttlMs: ttl });
1127
1202
  let data;
1128
1203
  if (proxyMode) {
1129
1204
  logDebug('[smartlinks] GET via proxy', { path });
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { initializeApi, isInitialized, hasAuthCredentials, configureSdkCache, invalidateCache, request, post, put, patch, del, sendCustomProxyMessage, getApiHeaders, isProxyEnabled, setBearerToken, getBearerToken, setGrantToken, getGrantToken } from "./http";
1
+ export { initializeApi, isInitialized, hasAuthCredentials, configureSdkCache, invalidateCache, getHttpCacheDiagnostics, resetHttpCacheDiagnostics, request, post, put, patch, del, sendCustomProxyMessage, getApiHeaders, isProxyEnabled, setBearerToken, getBearerToken, setGrantToken, getGrantToken } from "./http";
2
2
  export * from "./api";
3
3
  export * from "./types";
4
4
  export { iframe } from "./iframe";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  // Top-level entrypoint of the npm package. Re-export initializeApi + all namespaces.
3
- export { initializeApi, isInitialized, hasAuthCredentials, configureSdkCache, invalidateCache, request, post, put, patch, del, sendCustomProxyMessage, getApiHeaders, isProxyEnabled, setBearerToken, getBearerToken, setGrantToken, getGrantToken } from "./http";
3
+ export { initializeApi, isInitialized, hasAuthCredentials, configureSdkCache, invalidateCache, getHttpCacheDiagnostics, resetHttpCacheDiagnostics, request, post, put, patch, del, sendCustomProxyMessage, getApiHeaders, isProxyEnabled, setBearerToken, getBearerToken, setGrantToken, getGrantToken } from "./http";
4
4
  export * from "./api";
5
5
  export * from "./types";
6
6
  // Iframe namespace
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.0-alpha.4 | Generated: 2026-09-14T15:08:56.614Z
3
+ Version: 2.0.0-alpha.5 | Generated: 2026-09-14T19:49:23.024Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -159,6 +159,12 @@ The Smartlinks SDK is organized into the following namespaces:
159
159
 
160
160
  Core HTTP functions for API configuration and communication:
161
161
 
162
+ **getHttpCacheDiagnostics**() → `void`
163
+ Snapshot of cache counters + current keys. Call from the console via `window.__slHttpDiag()`.
164
+
165
+ **resetHttpCacheDiagnostics**() → `void`
166
+ Reset the diagnostics counters (does not touch the cache itself).
167
+
162
168
  **isProxyEnabled**() → `boolean`
163
169
  Return whether proxy mode is currently enabled.
164
170
 
package/docs/sequences.md CHANGED
@@ -45,22 +45,27 @@ subject first and return the number it already has, rather than allocating a sec
45
45
  ## Idempotency subject — use a *stable* id
46
46
 
47
47
  The number is deduped by the **subject id** you pass, so it must be the **stable** identity:
48
- - **Claim set id** (the wristband's permanent record) — the right key when people aren't
49
- logged in. A re-tap resolves to the same claim set same number.
48
+ - **Virtual-proof id `<claimSetId>-<code>`** (e.g. `23-oOkf8o`) — the right key for a
49
+ per-wristband raffle. The `code` is the physical tag's permanent id, so a re-tap of the same
50
+ wristband resolves to the same code doc → same number. The virtual proof record is transient,
51
+ but the **code** inside its id is stable — that's what the sink keys on (it splits the id and
52
+ uses only `<claimSetId>` + `<code>`).
53
+ - **Bare claim-set id** — when one number per *group* is intended.
50
54
  - The authenticated **user/contact** — if they sign in / claim.
51
55
 
52
- Do **not** key on a value that changes per interaction (e.g. a virtual proof id minted fresh
53
- on each tap) that would let one person take several numbers.
56
+ Do **not** key on the transient part of a per-tap identity (a freshly-minted virtual proof
57
+ record) — key on the stable **code**, which is exactly what the `<claimSetId>-<code>` subject
58
+ carries.
54
59
 
55
60
  ## Where the number is stored (the sink)
56
61
 
57
62
  The stamp target is configurable — the number lives wherever you'll read it:
58
63
 
59
- | Target | Use |
60
- |---|---|
61
- | `claimSet` | The Firestore wristband record fast, no proof mint on the hot path. |
62
- | `proof` | A minted proof (value or attestation) — when the number should travel with the proof. |
63
- | `appRecord` | A structured app record — for queryable, per-app data. |
64
+ | Target | Subject id | Use |
65
+ |---|---|---|
66
+ | `claimSet` | the virtual-proof id `<claimSetId>-<code>` (e.g. `23-oOkf8o`) **or** a bare `<claimSetId>` | The Firestore claim-set world. **The subject decides the exact doc:** a `<claimSetId>-<code>` id stamps the individual **code** doc (per-wristband — rides back as `tagData` on the next tap, every wristband its own number); a bare id stamps the whole **set** doc (one shared value). Claim-set ids are hyphen-free, so a hyphen unambiguously means the per-code form. |
67
+ | `proof` | the proof id | A minted proof (value or attestation) — when the number should travel with the proof. |
68
+ | `appRecord` | the record id | A structured app record — for queryable, per-app data. |
64
69
 
65
70
  ## Configure the sequence (once, server-side)
66
71
 
@@ -72,14 +77,19 @@ makes the public endpoint safe: the target/field/scope are set by you, not the c
72
77
  {
73
78
  "raffle": {
74
79
  "key": "raffle:2026-cup", // the named counter (data.sequences.<key>)
75
- "target": "claimSet", // claimSet | proof | appRecord — where the number is stamped
76
- "field": "raffleNumber", // the property written on the target
77
- "productId": "wristbands-2026", // counter scope (optional; else collection-wide)
80
+ "target": "claimSet", // claimSet | proof | appRecord — the ledger domain
81
+ "field": "raffleNumber", // the property written on the target (comes back in tagData)
78
82
  "start": 1 // first number (optional, default 1)
83
+ // no productId → one collection-wide counter (all wristbands share the sequence)
79
84
  }
80
85
  }
81
86
  ```
82
87
 
88
+ > **Per-wristband raffle.** Keep `target: "claimSet"` and pass the tap's virtual-proof id
89
+ > (`<claimSetId>-<code>`) as the subject — the sink writes the individual tag's **code** doc, so
90
+ > each wristband gets its own number and it surfaces in `tagData` on the next tap. Pass a bare
91
+ > claim-set id instead only if you want one number for the whole group.
92
+
83
93
  ## Call it (the widget)
84
94
 
85
95
  Your widget calls one bounded, public endpoint on tap. It passes only the **subject id** —
@@ -87,12 +97,13 @@ never the target/field:
87
97
 
88
98
  ```
89
99
  POST /api/v1/public/collection/:collectionId/sequence/allocate
90
- { "appId": "raffle-app", "sequenceId": "raffle", "subjectId": "<claim set id>" }
100
+ { "appId": "liveWidgets", "sequenceId": "raffle", "subjectId": "23-oOkf8o" }
91
101
  → { "number": 42, "isNew": true } // re-tap → { "number": 42, "isNew": false }
92
102
  ```
93
103
 
94
- - **`subjectId`** is the STABLE identity the claim-set id from the tap. Re-taps collapse to
95
- one number.
104
+ - **`subjectId`** is the STABLE identity. For `claimCode` it's the virtual-proof id
105
+ `<claimSetId>-<code>` (e.g. `23-oOkf8o`) straight off the tap — re-taps of the same wristband
106
+ collapse to one number. (For `claimSet` it's the claim-set id.)
96
107
  - **Idempotent**, so both your flows are the *same call*:
97
108
  - **Auto:** on load, call allocate → get your number (existing or freshly minted).
98
109
  - **Button:** click → animate → same call → "Your raffle number is 42."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "2.0.0-alpha.4",
3
+ "version": "2.0.0-alpha.5",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",