@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.
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/sequence.d.ts +35 -0
- package/dist/api/sequence.js +26 -0
- package/dist/docs/API_SUMMARY.md +33 -1
- package/dist/docs/sequences.md +55 -27
- package/dist/http.d.ts +20 -0
- package/dist/http.js +80 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/openapi.yaml +25 -0
- package/docs/API_SUMMARY.md +33 -1
- package/docs/sequences.md +55 -27
- package/openapi.yaml +25 -0
- package/package.json +1 -1
package/dist/api/index.d.ts
CHANGED
|
@@ -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 = {}));
|
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.0.0-alpha.
|
|
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/dist/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
|
-
- **
|
|
49
|
-
|
|
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
|
|
53
|
-
on
|
|
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
|
|
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
|
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
//
|
|
76
|
+
// app config: data.sequenceConfigs
|
|
72
77
|
{
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
##
|
|
93
|
+
## Call it (the widget)
|
|
88
94
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.0.0-alpha.
|
|
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
|
-
- **
|
|
49
|
-
|
|
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
|
|
53
|
-
on
|
|
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
|
|
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
|
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
//
|
|
76
|
+
// app config: data.sequenceConfigs
|
|
72
77
|
{
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
##
|
|
93
|
+
## Call it (the widget)
|
|
88
94
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|