@proveanything/smartlinks 2.0.0-alpha.3 → 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.
@@ -10,6 +10,7 @@ export { auth } from "./auth";
10
10
  export { form } from "./form";
11
11
  export { authKit } from "./authKit";
12
12
  export { claimSet } from "./claimSet";
13
+ export { sequence } from "./sequence";
13
14
  export { crate } from "./crate";
14
15
  export { batch } from "./batch";
15
16
  export { variant } from "./variant";
package/dist/api/index.js CHANGED
@@ -12,6 +12,7 @@ export { auth } from "./auth";
12
12
  export { form } from "./form";
13
13
  export { authKit } from "./authKit";
14
14
  export { claimSet } from "./claimSet";
15
+ export { sequence } from "./sequence";
15
16
  export { crate } from "./crate";
16
17
  export { batch } from "./batch";
17
18
  export { variant } from "./variant";
@@ -0,0 +1,35 @@
1
+ export interface AllocateSequenceInput {
2
+ /** The app that owns the sequence config. */
3
+ appId: string;
4
+ /** The configured sequence id — the key of data.sequenceConfigs on the app config. */
5
+ sequenceId: string;
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
+ */
13
+ subjectId: string;
14
+ /** Optional product scope, when the sequence config is scoped per product. */
15
+ productId?: string;
16
+ }
17
+ export interface AllocatedSequence {
18
+ /** The allocated (or already-held) number. */
19
+ number: number;
20
+ /** True when this call allocated a new number; false when the subject already had one. */
21
+ isNew: boolean;
22
+ }
23
+ export declare namespace sequence {
24
+ /**
25
+ * Allocate (or return the existing) sequence number for a subject. Idempotent — safe to
26
+ * call on load (auto-enter) and on a button tap; a subject that already has a number gets
27
+ * it back with `isNew: false`.
28
+ *
29
+ * @example
30
+ * const { number, isNew } = await sequence.allocate(collectionId, {
31
+ * appId: 'raffle-app', sequenceId: 'raffle', subjectId: claimSetId,
32
+ * })
33
+ */
34
+ function allocate(collectionId: string, input: AllocateSequenceInput): Promise<AllocatedSequence>;
35
+ }
@@ -0,0 +1,26 @@
1
+ // src/api/sequence.ts
2
+ //
3
+ // Sequences — allocate a guaranteed-unique, monotonic number (raffle tickets, "Nth to
4
+ // claim", queue positions) and stamp it onto a record. The sequence (counter key, target,
5
+ // field, scope) is configured server-side in app config (data.sequenceConfigs[sequenceId]);
6
+ // this call supplies only the subject and is idempotent — a re-tap returns the same number.
7
+ // See docs/sequences.md.
8
+ import { post } from "../http";
9
+ export var sequence;
10
+ (function (sequence) {
11
+ const base = (collectionId) => `/public/collection/${encodeURIComponent(collectionId)}/sequence`;
12
+ /**
13
+ * Allocate (or return the existing) sequence number for a subject. Idempotent — safe to
14
+ * call on load (auto-enter) and on a button tap; a subject that already has a number gets
15
+ * it back with `isNew: false`.
16
+ *
17
+ * @example
18
+ * const { number, isNew } = await sequence.allocate(collectionId, {
19
+ * appId: 'raffle-app', sequenceId: 'raffle', subjectId: claimSetId,
20
+ * })
21
+ */
22
+ async function allocate(collectionId, input) {
23
+ return post(`${base(collectionId)}/allocate`, input);
24
+ }
25
+ sequence.allocate = allocate;
26
+ })(sequence || (sequence = {}));
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.0-alpha.3 | Generated: 2026-09-14T14:38:59.597Z
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
 
@@ -150,6 +150,7 @@ The Smartlinks SDK is organized into the following namespaces:
150
150
  - **realtime** - Functions for realtime operations
151
151
  - **research** - Functions for research operations
152
152
  - **secrets** - Functions for secrets operations
153
+ - **sequence** - Functions for sequence operations
153
154
  - **tags** - Functions for tags operations
154
155
  - **template** - Functions for template operations
155
156
  - **translations** - Functions for translations operations
@@ -158,6 +159,12 @@ The Smartlinks SDK is organized into the following namespaces:
158
159
 
159
160
  Core HTTP functions for API configuration and communication:
160
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
+
161
168
  **isProxyEnabled**() → `boolean`
162
169
  Return whether proxy mode is currently enabled.
163
170
 
@@ -8535,6 +8542,26 @@ type VerifyTokenResponse = {
8535
8542
  }
8536
8543
  ```
8537
8544
 
8545
+ ### sequence (api)
8546
+
8547
+ **AllocateSequenceInput** (interface)
8548
+ ```typescript
8549
+ interface AllocateSequenceInput {
8550
+ appId: string
8551
+ sequenceId: string
8552
+ subjectId: string
8553
+ productId?: string
8554
+ }
8555
+ ```
8556
+
8557
+ **AllocatedSequence** (interface)
8558
+ ```typescript
8559
+ interface AllocatedSequence {
8560
+ number: number
8561
+ isNew: boolean
8562
+ }
8563
+ ```
8564
+
8538
8565
  ### conditions (utils)
8539
8566
 
8540
8567
  **BaseCondition** (interface)
@@ -10791,6 +10818,11 @@ Soft-delete a secret. DELETE /secrets/:ref
10791
10818
  id: string,
10792
10819
  query: { limit?: number; offset?: number } = {}) → `Promise<SegmentRecipientsResponse>`
10793
10820
 
10821
+ ### sequence
10822
+
10823
+ **allocate**(collectionId: string, input: AllocateSequenceInput) → `Promise<AllocatedSequence>`
10824
+ Allocate (or return the existing) sequence number for a subject. Idempotent — safe to call on load (auto-enter) and on a button tap; a subject that already has a number gets it back with `isNew: false`. const { number, isNew } = await sequence.allocate(collectionId, { appId: 'raffle-app', sequenceId: 'raffle', subjectId: claimSetId, })
10825
+
10794
10826
  ### sessions
10795
10827
 
10796
10828
  **stats**(collectionId: string) → `Promise<SessionStatistics>`
@@ -45,47 +45,75 @@ 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
- ## Worked example a free raffle on NFC wristbands
70
+ ## Configure the sequence (once, server-side)
66
71
 
67
- Everyone taps a wristband and hits **Enter the raffle**. Each tap allocates the next number
68
- and writes it onto that wristband's claim set fast, unique, no proof mint:
72
+ You define the sequence **in your app config**, under `data.sequenceConfigs`. This is what
73
+ makes the public endpoint safe: the target/field/scope are set by you, not the caller.
69
74
 
70
75
  ```jsonc
71
- // allocate-and-stamp (conceptual shape)
76
+ // app config: data.sequenceConfigs
72
77
  {
73
- "appId": "raffle-app",
74
- "productId": "wristbands-2026", // the counter's scope
75
- "key": "raffle:2026-cup", // the named sequence
76
- "start": 1,
77
- "subjectId": "<claim set id>", // STABLE identity — re-taps collapse to one number
78
- "target": "claimSet",
79
- "field": "raffleNumber"
78
+ "raffle": {
79
+ "key": "raffle:2026-cup", // the named counter (data.sequences.<key>)
80
+ "target": "claimSet", // claimSet | proof | appRecord — the ledger domain
81
+ "field": "raffleNumber", // the property written on the target (comes back in tagData)
82
+ "start": 1 // first number (optional, default 1)
83
+ // no productId → one collection-wide counter (all wristbands share the sequence)
84
+ }
80
85
  }
81
- // → { "number": 42, "isNew": true } (re-tap → { "number": 42, "isNew": false })
82
86
  ```
83
87
 
84
- Drawing the winner needs no separate ledger either — query the claim sets (or proofs) where
85
- `raffleNumber` is set; that field is your entry list, in allocation order.
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.
86
92
 
87
- ## Status
93
+ ## Call it (the widget)
88
94
 
89
- The allocator + stamping run server-side today. The **public "enter" action** an app widget
90
- calls on tap (and its SDK wrapper) is being wired in the 2.0.0-alpha line — this doc is the
91
- contract it will expose.
95
+ Your widget calls one bounded, public endpoint on tap. It passes only the **subject id**
96
+ never the target/field:
97
+
98
+ ```
99
+ POST /api/v1/public/collection/:collectionId/sequence/allocate
100
+ { "appId": "liveWidgets", "sequenceId": "raffle", "subjectId": "23-oOkf8o" }
101
+ → { "number": 42, "isNew": true } // re-tap → { "number": 42, "isNew": false }
102
+ ```
103
+
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.)
107
+ - **Idempotent**, so both your flows are the *same call*:
108
+ - **Auto:** on load, call allocate → get your number (existing or freshly minted).
109
+ - **Button:** click → animate → same call → "Your raffle number is 42."
110
+ - **Refresh:** just read the `raffleNumber` field back off the claim set (or proof) — it's the
111
+ ledger. Or call allocate again; you'll get the same number with `isNew: false`.
112
+
113
+ Errors: `404 SEQUENCE_NOT_FOUND` (not configured), `404 CLAIMSET_NOT_FOUND` (bad subject),
114
+ `400 BAD_REQUEST` (missing fields).
115
+
116
+ ## Drawing the winner
117
+
118
+ No separate ledger — query the claim sets (or proofs) where `raffleNumber` is set; that field
119
+ is your entry list, in allocation order.
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
package/dist/openapi.yaml CHANGED
@@ -27897,3 +27897,28 @@ components:
27897
27897
  additionalProperties: true
27898
27898
  required:
27899
27899
  - valid
27900
+ AllocateSequenceInput:
27901
+ type: object
27902
+ properties:
27903
+ appId:
27904
+ type: string
27905
+ sequenceId:
27906
+ type: string
27907
+ subjectId:
27908
+ type: string
27909
+ productId:
27910
+ type: string
27911
+ required:
27912
+ - appId
27913
+ - sequenceId
27914
+ - subjectId
27915
+ AllocatedSequence:
27916
+ type: object
27917
+ properties:
27918
+ number:
27919
+ type: number
27920
+ isNew:
27921
+ type: boolean
27922
+ required:
27923
+ - number
27924
+ - isNew
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.0-alpha.3 | Generated: 2026-09-14T14:38:59.597Z
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
 
@@ -150,6 +150,7 @@ The Smartlinks SDK is organized into the following namespaces:
150
150
  - **realtime** - Functions for realtime operations
151
151
  - **research** - Functions for research operations
152
152
  - **secrets** - Functions for secrets operations
153
+ - **sequence** - Functions for sequence operations
153
154
  - **tags** - Functions for tags operations
154
155
  - **template** - Functions for template operations
155
156
  - **translations** - Functions for translations operations
@@ -158,6 +159,12 @@ The Smartlinks SDK is organized into the following namespaces:
158
159
 
159
160
  Core HTTP functions for API configuration and communication:
160
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
+
161
168
  **isProxyEnabled**() → `boolean`
162
169
  Return whether proxy mode is currently enabled.
163
170
 
@@ -8535,6 +8542,26 @@ type VerifyTokenResponse = {
8535
8542
  }
8536
8543
  ```
8537
8544
 
8545
+ ### sequence (api)
8546
+
8547
+ **AllocateSequenceInput** (interface)
8548
+ ```typescript
8549
+ interface AllocateSequenceInput {
8550
+ appId: string
8551
+ sequenceId: string
8552
+ subjectId: string
8553
+ productId?: string
8554
+ }
8555
+ ```
8556
+
8557
+ **AllocatedSequence** (interface)
8558
+ ```typescript
8559
+ interface AllocatedSequence {
8560
+ number: number
8561
+ isNew: boolean
8562
+ }
8563
+ ```
8564
+
8538
8565
  ### conditions (utils)
8539
8566
 
8540
8567
  **BaseCondition** (interface)
@@ -10791,6 +10818,11 @@ Soft-delete a secret. DELETE /secrets/:ref
10791
10818
  id: string,
10792
10819
  query: { limit?: number; offset?: number } = {}) → `Promise<SegmentRecipientsResponse>`
10793
10820
 
10821
+ ### sequence
10822
+
10823
+ **allocate**(collectionId: string, input: AllocateSequenceInput) → `Promise<AllocatedSequence>`
10824
+ Allocate (or return the existing) sequence number for a subject. Idempotent — safe to call on load (auto-enter) and on a button tap; a subject that already has a number gets it back with `isNew: false`. const { number, isNew } = await sequence.allocate(collectionId, { appId: 'raffle-app', sequenceId: 'raffle', subjectId: claimSetId, })
10825
+
10794
10826
  ### sessions
10795
10827
 
10796
10828
  **stats**(collectionId: string) → `Promise<SessionStatistics>`
package/docs/sequences.md CHANGED
@@ -45,47 +45,75 @@ 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
- ## Worked example a free raffle on NFC wristbands
70
+ ## Configure the sequence (once, server-side)
66
71
 
67
- Everyone taps a wristband and hits **Enter the raffle**. Each tap allocates the next number
68
- and writes it onto that wristband's claim set fast, unique, no proof mint:
72
+ You define the sequence **in your app config**, under `data.sequenceConfigs`. This is what
73
+ makes the public endpoint safe: the target/field/scope are set by you, not the caller.
69
74
 
70
75
  ```jsonc
71
- // allocate-and-stamp (conceptual shape)
76
+ // app config: data.sequenceConfigs
72
77
  {
73
- "appId": "raffle-app",
74
- "productId": "wristbands-2026", // the counter's scope
75
- "key": "raffle:2026-cup", // the named sequence
76
- "start": 1,
77
- "subjectId": "<claim set id>", // STABLE identity — re-taps collapse to one number
78
- "target": "claimSet",
79
- "field": "raffleNumber"
78
+ "raffle": {
79
+ "key": "raffle:2026-cup", // the named counter (data.sequences.<key>)
80
+ "target": "claimSet", // claimSet | proof | appRecord — the ledger domain
81
+ "field": "raffleNumber", // the property written on the target (comes back in tagData)
82
+ "start": 1 // first number (optional, default 1)
83
+ // no productId → one collection-wide counter (all wristbands share the sequence)
84
+ }
80
85
  }
81
- // → { "number": 42, "isNew": true } (re-tap → { "number": 42, "isNew": false })
82
86
  ```
83
87
 
84
- Drawing the winner needs no separate ledger either — query the claim sets (or proofs) where
85
- `raffleNumber` is set; that field is your entry list, in allocation order.
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.
86
92
 
87
- ## Status
93
+ ## Call it (the widget)
88
94
 
89
- The allocator + stamping run server-side today. The **public "enter" action** an app widget
90
- calls on tap (and its SDK wrapper) is being wired in the 2.0.0-alpha line — this doc is the
91
- contract it will expose.
95
+ Your widget calls one bounded, public endpoint on tap. It passes only the **subject id**
96
+ never the target/field:
97
+
98
+ ```
99
+ POST /api/v1/public/collection/:collectionId/sequence/allocate
100
+ { "appId": "liveWidgets", "sequenceId": "raffle", "subjectId": "23-oOkf8o" }
101
+ → { "number": 42, "isNew": true } // re-tap → { "number": 42, "isNew": false }
102
+ ```
103
+
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.)
107
+ - **Idempotent**, so both your flows are the *same call*:
108
+ - **Auto:** on load, call allocate → get your number (existing or freshly minted).
109
+ - **Button:** click → animate → same call → "Your raffle number is 42."
110
+ - **Refresh:** just read the `raffleNumber` field back off the claim set (or proof) — it's the
111
+ ledger. Or call allocate again; you'll get the same number with `isNew: false`.
112
+
113
+ Errors: `404 SEQUENCE_NOT_FOUND` (not configured), `404 CLAIMSET_NOT_FOUND` (bad subject),
114
+ `400 BAD_REQUEST` (missing fields).
115
+
116
+ ## Drawing the winner
117
+
118
+ No separate ledger — query the claim sets (or proofs) where `raffleNumber` is set; that field
119
+ is your entry list, in allocation order.
package/openapi.yaml CHANGED
@@ -27897,3 +27897,28 @@ components:
27897
27897
  additionalProperties: true
27898
27898
  required:
27899
27899
  - valid
27900
+ AllocateSequenceInput:
27901
+ type: object
27902
+ properties:
27903
+ appId:
27904
+ type: string
27905
+ sequenceId:
27906
+ type: string
27907
+ subjectId:
27908
+ type: string
27909
+ productId:
27910
+ type: string
27911
+ required:
27912
+ - appId
27913
+ - sequenceId
27914
+ - subjectId
27915
+ AllocatedSequence:
27916
+ type: object
27917
+ properties:
27918
+ number:
27919
+ type: number
27920
+ isNew:
27921
+ type: boolean
27922
+ required:
27923
+ - number
27924
+ - isNew
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "2.0.0-alpha.3",
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",