unshared-frontend-sdk 2.3.0 → 3.0.0-rc.15
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/README.md +142 -39
- package/dist/browser.d.ts +45 -54
- package/dist/fingerprint-agent.d.ts +3 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/shared-types.d.ts +148 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ await client.init({
|
|
|
20
20
|
const { data } = await client.checkUser();
|
|
21
21
|
if (data?.is_user_flagged) {
|
|
22
22
|
await client.triggerEmailVerification(); // emails a 6-digit code
|
|
23
|
-
const result = await client.verify(codeFromUser); //
|
|
23
|
+
const result = await client.verify(codeFromUser); // -> { verified: true }
|
|
24
24
|
}
|
|
25
25
|
```
|
|
26
26
|
|
|
@@ -34,6 +34,12 @@ Direct-mode reads are protected server-side: the publishable key only answers fo
|
|
|
34
34
|
npm install unshared-frontend-sdk
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
The bundled fallback uses exact public npm dependency `unshared-fingerprint-lib@1.5.2`
|
|
38
|
+
(Node >=18.20 for installation/tooling). No vendored tarball is required. S3-first
|
|
39
|
+
loading remains the default. Version 1.5.2 filters components only in the library's
|
|
40
|
+
own `sendFingerprint` reporter; this SDK preserves complete raw collection results
|
|
41
|
+
and applies its existing payload-size policy instead.
|
|
42
|
+
|
|
37
43
|
Or via CDN (no build step) — see [CDN / UMD usage](#cdn--umd-usage) below for the complete example.
|
|
38
44
|
|
|
39
45
|
---
|
|
@@ -54,7 +60,7 @@ const fingerprint = await client.collect();
|
|
|
54
60
|
client.submitFingerprintEvent(fingerprint, { userId: currentUser?.id });
|
|
55
61
|
```
|
|
56
62
|
|
|
57
|
-
That's it. The SDK handles
|
|
63
|
+
That's it. The SDK handles timeouts and errors without throwing. Read-only calls may retry; Trigger and Verify never retry.
|
|
58
64
|
|
|
59
65
|
> **Using this SDK alongside the Node `unsharedBoundToUser` middleware (Tier 1)?** The middleware
|
|
60
66
|
> also auto-injects an inline fingerprint script into your HTML. That is expected — the two
|
|
@@ -74,13 +80,19 @@ new UnsharedBrowser({
|
|
|
74
80
|
});
|
|
75
81
|
```
|
|
76
82
|
|
|
83
|
+
### BrowserConfig
|
|
84
|
+
|
|
77
85
|
| Option | Type | Default | Description |
|
|
78
86
|
|--------|------|---------|-------------|
|
|
79
87
|
| `baseUrl` | `string` | `undefined` | Proxy mode: base URL of your backend. Use `""` when your frontend and backend share the same domain. Omitting both `baseUrl` and `publishableKey` defaults to same-origin proxy mode (unless a key was baked into a client-specific build — see below). |
|
|
80
88
|
| `publishableKey` | `string` | — | Direct mode: publishable key (`upk_…`) issued by Unshared Labs. When set, events bypass your backend and go straight to the platform. |
|
|
81
89
|
| `apiUrl` | `string` | `https://api.unshared.ai` | Direct mode: Unshared Labs platform origin. |
|
|
82
|
-
| `maxRetries` | `number` | `3` |
|
|
90
|
+
| `maxRetries` | `number` | `3` | Retry budget for eligible operations |
|
|
83
91
|
| `timeout` | `number` | `30000` | Per-attempt timeout in milliseconds |
|
|
92
|
+
| `includePathPrefix` | `string[]` | Unset (all paths) | Literal `location.pathname` prefixes for lifecycle event submission; `[]` submits on none. This is browser-side filtering, not server enforcement. |
|
|
93
|
+
| `skipPaths` | `string[]` | Unset | Literal pathname prefixes excluded from lifecycle submission; wins over includes. Does not prevent explicit `init()` from establishing identity/collecting, or disable low-level collection methods. |
|
|
94
|
+
| `sessionId` | `() => string \| undefined` | Cookie, then UUID | Resolver runs at construction; a defined value wins over `__unshared_sid`/generated UUID and is persisted to the correlation cookie. Not an auth-session TTL setting. |
|
|
95
|
+
| `deviceId` | `() => string \| undefined` | Legacy stored device, then collected stable hash | Resolver runs at construction and init; a nonempty value overrides the device identifier, not the canonical collected hashes or separate permanent ID. |
|
|
84
96
|
| `enableInterstitial` | `boolean` | `false` | Auto-render the interstitial modal when the user is flagged (via the `unshared:flagged` event); works in direct or proxy mode. See [Interstitial modal](#interstitial-modal). |
|
|
85
97
|
| `interstitialFlowType` | `string` | `email_verification` | Flow type requested for the auto-shown interstitial. |
|
|
86
98
|
|
|
@@ -98,7 +110,14 @@ Build a client-specific distribution with:
|
|
|
98
110
|
UNSHARED_PUBLISHABLE_API_KEY=upk_test_or_client_key npm --prefix sdks/javascript/browser run build
|
|
99
111
|
```
|
|
100
112
|
|
|
101
|
-
Passing `baseUrl`, including `baseUrl: ""`,
|
|
113
|
+
Passing `baseUrl`, including `baseUrl: ""`, suppresses the packaged-key fallback
|
|
114
|
+
and keeps proxy mode unless an explicit nonempty `publishableKey` is supplied.
|
|
115
|
+
|
|
116
|
+
An explicit nonempty `publishableKey` takes precedence even when `baseUrl` is also
|
|
117
|
+
passed; `baseUrl` suppresses only the packaged-key fallback. Use HTTPS origins
|
|
118
|
+
(HTTP is accepted only for loopback development), and never put a secret `usk_` key
|
|
119
|
+
in browser config. `maxRetries` applies only to eligible reads, not ingestion,
|
|
120
|
+
Trigger or Verify.
|
|
102
121
|
|
|
103
122
|
The build applies targeted obfuscation before minification to the packaged publishable key and the default API URL so they are not present as obvious raw string literals in `dist/index.umd.js`. This is only a friction layer against casual source inspection; the browser still reconstructs those values at runtime, and server-side origin allowlists/rate limits remain the real security controls.
|
|
104
123
|
|
|
@@ -117,6 +136,44 @@ Use the confidential-docs override only for local investigation; do not commit m
|
|
|
117
136
|
|
|
118
137
|
## Methods
|
|
119
138
|
|
|
139
|
+
### Identity and Lifecycle
|
|
140
|
+
|
|
141
|
+
Call `init({ userId, emailAddress, isPaidSubscriber })` after authenticated identity
|
|
142
|
+
is available, `onRouteChange()` on SPA navigation, and `client.destroy()` on logout
|
|
143
|
+
or teardown. `isPaidSubscriber: false` suppresses collection/submission for that
|
|
144
|
+
user. `destroy()` clears cached collection/login state and detaches listeners; it
|
|
145
|
+
does not erase permanent device/hash cookies or change your application's auth TTL.
|
|
146
|
+
|
|
147
|
+
Init and MPA collection reuse a paired raw/wire fingerprint cache: the complete raw
|
|
148
|
+
JSON is in per-tab sessionStorage `__unshared_fp_raw`, and the normalized wire view
|
|
149
|
+
is in `__unshared_fp`. SPA events reuse this pair rather than recollecting; overlapping
|
|
150
|
+
init/MPA collection shares one pending collection per SDK instance. Old wire-only
|
|
151
|
+
caches are recollected. In-memory fallback keeps the current instance usable when
|
|
152
|
+
storage is blocked, but cannot persist across reloads.
|
|
153
|
+
|
|
154
|
+
Eligible init/route submission activity refreshes readable correlation cookies to
|
|
155
|
+
rolling **400 days (`Max-Age=34560000`) before deduplication**, even if no new event
|
|
156
|
+
is sent: `__unshared_stable_hash`, `__unshared_full_hash`, `__unshared_device_id`,
|
|
157
|
+
legacy stable alias `__unshared_fp_id`, and `__unshared_sid`. The permanent
|
|
158
|
+
`__unshared_device_id=upid_<uuid-v4>` is independent of mutable hashes, backed by
|
|
159
|
+
localStorage; an existing valid localStorage ID takes precedence over a cookie when
|
|
160
|
+
restoring it. Cookies use `Path=/`, `SameSite=Lax` and `Secure` on HTTPS. Browser
|
|
161
|
+
privacy controls, storage blocking and user deletion can shorten retention.
|
|
162
|
+
|
|
163
|
+
With the protection proxy, the server also writes the legacy HttpOnly
|
|
164
|
+
`__unshared_fingerprint_id`: it preserves the first full hash, while canonical
|
|
165
|
+
`__unshared_full_hash` tracks the current collection. Only a server response can
|
|
166
|
+
renew that HttpOnly alias. These correlation lifetimes do not extend authentication:
|
|
167
|
+
the SDK UID/email server cookies retain their existing 365-day TTL and the
|
|
168
|
+
verification challenge remains ten minutes. The per-tab collection cache is not a
|
|
169
|
+
400-day fingerprint snapshot.
|
|
170
|
+
|
|
171
|
+
Lifecycle submissions share `window.__unshared.lastKey` with the inline middleware
|
|
172
|
+
script and mirror the last `(user, pathname + query)` in sessionStorage. Consecutive
|
|
173
|
+
duplicates are suppressed, including reloads in the tab; A-to-B-to-A can submit
|
|
174
|
+
again. This is not guaranteed delivery: no persistent replay queue is maintained,
|
|
175
|
+
old queue entries are discarded, and failed ingestion is not retried.
|
|
176
|
+
|
|
120
177
|
### `collect()`
|
|
121
178
|
|
|
122
179
|
Collect a browser fingerprint. Returns a `FingerprintWireFormat` object ready to pass to `submitFingerprintEvent`.
|
|
@@ -125,6 +182,14 @@ Collect a browser fingerprint. Returns a `FingerprintWireFormat` object ready to
|
|
|
125
182
|
const fingerprint = await client.collect();
|
|
126
183
|
```
|
|
127
184
|
|
|
185
|
+
### `collectRaw()`
|
|
186
|
+
|
|
187
|
+
Collect the fingerprint library's complete JSON object without reducing it to the compatibility wire type.
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const rawFingerprint = await client.collectRaw();
|
|
191
|
+
```
|
|
192
|
+
|
|
128
193
|
---
|
|
129
194
|
|
|
130
195
|
### `submitFingerprintEvent(fingerprint, opts?)`
|
|
@@ -154,17 +219,50 @@ if (!result.success) {
|
|
|
154
219
|
|
|
155
220
|
---
|
|
156
221
|
|
|
222
|
+
### `submitFingerprint(payload)`
|
|
223
|
+
|
|
224
|
+
Submit any JSON object without reshaping or filtering unknown fields. Direct mode encrypts the complete object with the publishable key; proxy mode keeps the browser-to-customer hop plaintext so middleware can add trusted context before encrypting the final request to Unshared.
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
await client.submitFingerprint({
|
|
228
|
+
fingerprint: await client.collectRaw(),
|
|
229
|
+
context: {
|
|
230
|
+
identity: { user_id: currentUser?.id },
|
|
231
|
+
event: { type: location.pathname + location.search },
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The payload must be a non-null JSON object; arrays and primitives fail locally with `VALIDATION_ERROR`. Direct requests larger than 1 MiB fail locally with `REQUEST_TOO_LARGE`. Direct ingestion succeeds only on a strict `202` v3 acknowledgement.
|
|
237
|
+
|
|
238
|
+
Proxy submissions have a fixed **100 KiB (102,400 serialized UTF-8 bytes)** limit,
|
|
239
|
+
compatible with an upstream default `express.json()` without configuration or
|
|
240
|
+
middleware reordering. Structured submissions send the full snapshot if it fits;
|
|
241
|
+
otherwise they empty local/session storage lists while retaining all cookies, then
|
|
242
|
+
omit `context.browser_storage` if still too large. The complete raw fingerprint,
|
|
243
|
+
including future fields, and core identity/device/session/SDK/event metadata are
|
|
244
|
+
never trimmed. Oversized core payloads return `REQUEST_TOO_LARGE` without sending;
|
|
245
|
+
the inline submitter logs that code. Low-level `submitFingerprint(payload)` never
|
|
246
|
+
reduces arbitrary caller objects: it sends them exactly or rejects above 100 KiB.
|
|
247
|
+
Direct mode keeps full snapshots and its existing 1 MiB logical / 1.5 MiB encrypted
|
|
248
|
+
limits. Duplicate fingerprint caches and legacy queues are excluded from snapshots.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
157
252
|
### Direct-mode API (requires `publishableKey`)
|
|
158
253
|
|
|
159
254
|
All four methods default to the identity captured by `init()` (email + stable fingerprint hash as deviceId); pass `{ email, deviceId }` to override. In proxy mode they return `error.code === 'DIRECT_MODE_REQUIRED'` — proxy-mode apps get these flows from their own backend middleware.
|
|
160
255
|
|
|
161
256
|
| Method | Retries | Returns |
|
|
162
257
|
|--------|---------|---------|
|
|
163
|
-
| `checkUser(opts?)` |
|
|
164
|
-
| `triggerEmailVerification(opts?)` | **never** (each call emails a code) | `{ next_allowed_at, retry_after_seconds }`;
|
|
165
|
-
| `verify(code, opts?)` | **never** (attempts are budgeted) |
|
|
258
|
+
| `checkUser(opts?)` | bounded availability failures only | `{ is_user_flagged, decision_id, decision_code }` |
|
|
259
|
+
| `triggerEmailVerification(opts?)` | **never** (each call emails a code) | `{ verification_id, next_allowed_at, retry_after_seconds }`; the SDK retains the challenge for `verify(...)` |
|
|
260
|
+
| `verify(code, opts?)` | **never** (attempts are budgeted) | Exact verified/not-verified result; UUIDv4 challenges and 6-digit OTPs are validated locally |
|
|
261
|
+
| `verifyEmail({ verification_id, code })` | **never** | Verification for an explicit challenge ID; reconciles flagged state when retained identity is available |
|
|
166
262
|
| `emailVerificationStatus(opts?)` | yes | `{ can_send: boolean, next_allowed_at }` |
|
|
167
263
|
|
|
264
|
+
V3 methods require exact success/error envelopes and endpoint-specific status/result shapes. After `verified: true`, the SDK performs one no-retry `checkUser` reconciliation; sticky flagged state is cleared only by a strict available unflagged decision, never by Verify alone or `CHECK_UNAVAILABLE`.
|
|
265
|
+
|
|
168
266
|
---
|
|
169
267
|
|
|
170
268
|
## Interstitial modal
|
|
@@ -175,9 +273,15 @@ flow is authored in the Unshared dashboard and stored per company; the SDK fetch
|
|
|
175
273
|
published definition from `GET /v2/browser/interstitial-flow`, renders it in a
|
|
176
274
|
Shadow-DOM-isolated modal, and routes the flow's actions back through
|
|
177
275
|
`triggerEmailVerification()` / `verify()` above — so all verification logic stays
|
|
178
|
-
server-side. Direct mode
|
|
276
|
+
server-side. Direct mode uses `publishableKey`; proxy mode is described below.
|
|
179
277
|
|
|
180
|
-
**Proxy mode is supported too.** When the SDK is constructed with `baseUrl` (no `publishableKey`) and your backend runs the Node middleware, `showInterstitial()` fetches the flow through `GET {baseUrl}/__unshared/interstitial-flow` and runs the modal's actions through the middleware's `/__unshared/verify-trigger` and `/__unshared/verify` routes. The secret key never leaves your server, and the user's identity is resolved server-side
|
|
278
|
+
**Proxy mode is supported too.** When the SDK is constructed with `baseUrl` (no `publishableKey`) and your backend runs the Node middleware, `showInterstitial()` fetches the flow through `GET {baseUrl}/__unshared/interstitial-flow` and runs the modal's actions through the middleware's `/__unshared/verify-trigger` and `/__unshared/verify` routes. The secret key never leaves your server, and the user's identity is resolved server-side; the browser sends only the OTP. Configure the middleware's trusted `emailAddress` resolver (preferred over the `__unshared_email` cookie fallback).
|
|
279
|
+
|
|
280
|
+
Publish the requested web flow for your company in the dashboard before enabling
|
|
281
|
+
the modal; a draft/missing flow cannot render. A modal or blur alone is not access
|
|
282
|
+
control: use server-side gate/403 enforcement for protected content. Verification
|
|
283
|
+
rate limits and attempt budgets are existing platform controls, not new browser
|
|
284
|
+
functionality; additional custom gates/rate limits belong to your app or gateway.
|
|
181
285
|
|
|
182
286
|
### `showInterstitial(opts?)`
|
|
183
287
|
|
|
@@ -232,9 +336,34 @@ const app = express();
|
|
|
232
336
|
const client = new UnsharedClient({ apiKey: process.env.UNSHARED_API_KEY });
|
|
233
337
|
|
|
234
338
|
app.use(express.json());
|
|
235
|
-
|
|
339
|
+
// Mount your authentication/session middleware before the SDK.
|
|
340
|
+
const middleware = unsharedBoundToUser(client, {
|
|
341
|
+
userId: (req) => req.session?.user?.id,
|
|
342
|
+
emailAddress: (req) => req.session?.user?.email,
|
|
343
|
+
});
|
|
344
|
+
app.use(middleware);
|
|
345
|
+
// Call middleware.destroy() on server shutdown/test teardown/before re-mounting.
|
|
236
346
|
```
|
|
237
347
|
|
|
348
|
+
Node `includePathPrefix` scopes ordinary checks/enforcement/events, not identity
|
|
349
|
+
bootstrap or eligible HTML injection. For example, include `/api/article/` to gate
|
|
350
|
+
article data while `/api/me/list` remains outside enforcement; the surrounding HTML
|
|
351
|
+
can still bootstrap fingerprinting. Node `skipPaths` truly bypasses the SDK for
|
|
352
|
+
matching application requests. These server options are separate from browser
|
|
353
|
+
lifecycle path filters. See the [complete protection config](../node/README.md#protectionconfig).
|
|
354
|
+
|
|
355
|
+
The Node middleware serves both `/__unshared/fp.js` (browser SDK) and
|
|
356
|
+
`/__unshared/fingerprint.js` (agent); unavailable bundles return 503 with
|
|
357
|
+
`Cache-Control: no-store`. The Web Standard handler instead requires a manually
|
|
358
|
+
supplied `fingerprintSdkBundle` for `fp.js` and does not automatically serve
|
|
359
|
+
`fingerprint.js` or `interstitial-flow`; wire those separately when needed.
|
|
360
|
+
|
|
361
|
+
For the injected script, middleware `debug: true` exposes PII-free reason codes and
|
|
362
|
+
optional HTTP status in `window.__unshared.lastDecision` and `[Unshared]` debug logs
|
|
363
|
+
(for example `MISSING_UID`, `DEDUP_SKIPPED`, `ASSET_LOAD_ERROR`, `SUBMIT_SUCCESS`).
|
|
364
|
+
Middleware `disableBotFilter: true` also reaches the inline submitter for E2E tests.
|
|
365
|
+
Neither is a `BrowserConfig` option; the standalone SDK's bot filter is unchanged.
|
|
366
|
+
|
|
238
367
|
---
|
|
239
368
|
|
|
240
369
|
## Error handling
|
|
@@ -248,7 +377,7 @@ const result = await client.submitFingerprintEvent(fingerprint);
|
|
|
248
377
|
// result.error.code === 'DELIVERY_FAILED'
|
|
249
378
|
```
|
|
250
379
|
|
|
251
|
-
|
|
380
|
+
Read-only direct calls retry eligible network and 5xx failures. Fingerprint ingestion, Trigger, and Verify are never retried because an ambiguous request may already have committed. Failed ingestion is not queued for replay.
|
|
252
381
|
|
|
253
382
|
---
|
|
254
383
|
|
|
@@ -257,7 +386,7 @@ Retries happen automatically on network errors, timeouts, and server errors (5xx
|
|
|
257
386
|
The UMD bundle exposes a `window.UnsharedBrowser` namespace object. Destructure the class from it first. If the bundle was built with `UNSHARED_PUBLISHABLE_API_KEY`, the client does not pass a key:
|
|
258
387
|
|
|
259
388
|
```html
|
|
260
|
-
<script src="https://unpkg.com/unshared-frontend-sdk@
|
|
389
|
+
<script src="https://unpkg.com/unshared-frontend-sdk@3.0.0/dist/index.umd.js"></script>
|
|
261
390
|
<script>
|
|
262
391
|
const { UnsharedBrowser } = window.UnsharedBrowser;
|
|
263
392
|
|
|
@@ -276,29 +405,6 @@ For proxy mode instead, pass `baseUrl` explicitly.
|
|
|
276
405
|
|
|
277
406
|
---
|
|
278
407
|
|
|
279
|
-
## Local retry queue encryption
|
|
280
|
-
|
|
281
|
-
When a fingerprint event still fails after all retry attempts, the browser SDK stores it in `localStorage` under `__unshared_event_queue` and retries it before the next regular fingerprint submission. Queue records are encrypted before storage so the event payload is not directly readable from browser devtools.
|
|
282
|
-
|
|
283
|
-
Encryption details:
|
|
284
|
-
|
|
285
|
-
- Algorithm: AES-GCM.
|
|
286
|
-
- IV: random 12-byte IV per queued event.
|
|
287
|
-
- Stored format: `v1:<base64 iv>:<base64 ciphertext>`.
|
|
288
|
-
- Queue key derivation:
|
|
289
|
-
|
|
290
|
-
```text
|
|
291
|
-
SHA-256("unshared-browser-queue:" + (publishableKey || baseUrl) + ":" + sessionId)
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
That SHA-256 digest is imported as the AES-GCM key through Web Crypto. The SDK does **not** use `API_KEY_ENCRYPTION_SECRET`; that kind of secret must stay server-side and must not be shipped in browser code.
|
|
295
|
-
|
|
296
|
-
This is intentionally best-effort local confidentiality, not a strong security boundary. In direct mode, the publishable key is public, and the session ID is browser-local state. That means the queue encryption protects against casual plaintext inspection of `localStorage`, but it does not protect against a user who can run JavaScript in their own browser context or inspect the loaded SDK. The browser SDK therefore treats queue encryption as obfuscation plus integrity protection for local retry data, not as secret storage.
|
|
297
|
-
|
|
298
|
-
Using the publishable key in this derivation is acceptable for this limited purpose because no server secret is available in the browser. It should not be described as making local data unrecoverable from the end user. If stronger browser-local protection is required, the SDK would need a different design, such as server-held queued events, short-lived server-issued wrapping keys, or platform storage that never exposes plaintext to arbitrary page JavaScript.
|
|
299
|
-
|
|
300
|
-
---
|
|
301
|
-
|
|
302
408
|
## Real-browser acceptance coverage
|
|
303
409
|
|
|
304
410
|
For Boston Globe style script-tag integration, split client-side browser tests into host-site flows and SDK-owned delivery behavior.
|
|
@@ -313,7 +419,4 @@ For Boston Globe style script-tag integration, split client-side browser tests i
|
|
|
313
419
|
| Password reset completion failures | Wrong current password; mismatched new-password entries; password-rule failures; missing required values. | Host-site auth behavior. Test in the client's browser suite. |
|
|
314
420
|
| Page navigation | Full page loads with script tag; link clicks; forward/back; refresh with same script tag injected. | SDK supports script-tag load, `init`, MPA `DOMContentLoaded`, and SPA route-change submission. Real-browser tests should assert the UMD bundle loads and the client page still renders. |
|
|
315
421
|
| Event firing | Event fires where applicable after user identity is present. | Implemented by `init`, `onRouteChange`, MPA listener, and direct/proxy submit endpoints. Covered by unit tests; should also be verified in real browser against mocked or test backend responses. |
|
|
316
|
-
| Retry |
|
|
317
|
-
| Encrypted local queue | After retry exhaustion, failed events are stored in local cache and are not directly readable as plaintext. | Implemented with AES-GCM encrypted `localStorage` queue and unit-tested. |
|
|
318
|
-
| Local queue storage failure | Storage unavailable, quota exceeded, or crypto unavailable. | Implemented as never-throw best effort; event returns delivery failure and storage failure is swallowed. Unit-tested for storage failure. |
|
|
319
|
-
| Queue flush | Stored events transmit on the next regular event opportunity. | Implemented by flushing the encrypted queue before the next fingerprint submission. Unit-tested. |
|
|
422
|
+
| Retry | Read-only checks may retry; ingestion, Trigger, and Verify make one attempt and are never queued. | Implemented and unit-tested. |
|
package/dist/browser.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FingerprintWireFormat } from '
|
|
1
|
+
import type { CheckUserResult, FingerprintWireFormat, IngestResult, JSONObject, VerifyEmailRequest, VerifyEmailResult } from './shared-types';
|
|
2
2
|
export interface BrowserConfig {
|
|
3
3
|
/**
|
|
4
4
|
* Base URL of the customer's own backend.
|
|
@@ -78,11 +78,15 @@ export interface InitOptions {
|
|
|
78
78
|
export interface SubmitFingerprintOptions {
|
|
79
79
|
userId: string;
|
|
80
80
|
eventType?: string;
|
|
81
|
+
permanentDeviceId?: string;
|
|
81
82
|
}
|
|
82
83
|
export interface SubmitFingerprintResult {
|
|
83
84
|
hash: string;
|
|
84
85
|
stable_hash: string;
|
|
85
86
|
collected_at: string;
|
|
87
|
+
version: string;
|
|
88
|
+
event_id?: string;
|
|
89
|
+
endpoint_version?: 'v3';
|
|
86
90
|
}
|
|
87
91
|
export interface BrowserApiResult<T = unknown> {
|
|
88
92
|
success: boolean;
|
|
@@ -101,18 +105,22 @@ export interface BrowserApiResult<T = unknown> {
|
|
|
101
105
|
export interface DirectIdentityOptions {
|
|
102
106
|
email?: string;
|
|
103
107
|
deviceId?: string;
|
|
108
|
+
permanentDeviceId?: string;
|
|
109
|
+
stableHash?: string;
|
|
110
|
+
fullHash?: string;
|
|
104
111
|
}
|
|
105
|
-
export interface CheckUserData {
|
|
106
|
-
is_user_flagged: boolean;
|
|
112
|
+
export interface CheckUserData extends CheckUserResult {
|
|
107
113
|
}
|
|
108
114
|
export interface TriggerVerificationData {
|
|
109
115
|
message?: string;
|
|
116
|
+
verification_id?: string;
|
|
110
117
|
next_allowed_at?: string;
|
|
111
118
|
retry_after_seconds?: number;
|
|
112
119
|
}
|
|
113
120
|
export interface VerifyData {
|
|
114
121
|
verified: boolean;
|
|
115
122
|
reason?: string;
|
|
123
|
+
verification_scope?: 'email';
|
|
116
124
|
}
|
|
117
125
|
export interface VerificationStatusData {
|
|
118
126
|
can_send: boolean;
|
|
@@ -121,6 +129,11 @@ export interface VerificationStatusData {
|
|
|
121
129
|
export interface FlaggedInterceptorOptions {
|
|
122
130
|
onFlagged: () => void;
|
|
123
131
|
}
|
|
132
|
+
interface CollectedFingerprint {
|
|
133
|
+
raw: JSONObject;
|
|
134
|
+
wire: FingerprintWireFormat;
|
|
135
|
+
source: 'remote' | 'bundled';
|
|
136
|
+
}
|
|
124
137
|
/**
|
|
125
138
|
* Browser SDK for Unshared Labs.
|
|
126
139
|
*
|
|
@@ -153,12 +166,13 @@ export declare class UnsharedBrowser {
|
|
|
153
166
|
private readonly _resolveDeviceId?;
|
|
154
167
|
private _sessionId;
|
|
155
168
|
private _deviceId;
|
|
169
|
+
private _permanentDeviceId;
|
|
156
170
|
private _userId;
|
|
157
171
|
private _emailAddress;
|
|
158
172
|
/** True after init() with isPaidSubscriber:false — blocks every submission path. */
|
|
159
173
|
private _doNotCollect;
|
|
160
174
|
private _mpaHandler;
|
|
161
|
-
private
|
|
175
|
+
private readonly _lifecycleState;
|
|
162
176
|
/** Interstitial auto-show config + live state. */
|
|
163
177
|
private readonly _enableInterstitial;
|
|
164
178
|
private readonly _interstitialFlowType;
|
|
@@ -166,22 +180,6 @@ export declare class UnsharedBrowser {
|
|
|
166
180
|
private _interstitialHandle;
|
|
167
181
|
/** Guards against rendering the modal more than once per flagged signal. */
|
|
168
182
|
private _interstitialOpen;
|
|
169
|
-
/**
|
|
170
|
-
* Dedup key for the last fingerprint submission: `${userId}|${route}`.
|
|
171
|
-
* Modern SPAs fire pushState/replaceState multiple times during hydration
|
|
172
|
-
* with the same URL — without this guard each call would generate a
|
|
173
|
-
* redundant FP row with an identical stable_hash.
|
|
174
|
-
*
|
|
175
|
-
* This is the IN-MEMORY layer (collapses bursts from this one instance). It
|
|
176
|
-
* works alongside two other client-side layers keyed on the same identity:
|
|
177
|
-
* the sessionStorage `__unshared_last_submit` mirror (survives hard reloads
|
|
178
|
-
* and SDK re-instantiation within the tab) and the page-scoped
|
|
179
|
-
* `window.__unshared.lastKey` guard shared with the auto-injected inline
|
|
180
|
-
* script (see getSharedDedup). Dedup is CLIENT-SIDE ONLY: the middleware
|
|
181
|
-
* appends a timestamp to X-Idempotency-Key (submit-fp.ts), so the backend
|
|
182
|
-
* drops only PubSub redeliveries, never two distinct submissions.
|
|
183
|
-
*/
|
|
184
|
-
private _lastSubmitKey;
|
|
185
183
|
constructor(config?: BrowserConfig);
|
|
186
184
|
/**
|
|
187
185
|
* Initialize the SDK after user login.
|
|
@@ -211,17 +209,32 @@ export declare class UnsharedBrowser {
|
|
|
211
209
|
*/
|
|
212
210
|
collect(options?: {
|
|
213
211
|
exclude?: string[];
|
|
212
|
+
[key: string]: unknown;
|
|
214
213
|
}): Promise<FingerprintWireFormat>;
|
|
214
|
+
/** Returns the fingerprint agent's JSON object without selecting or renaming fields. */
|
|
215
|
+
collectRaw(options?: {
|
|
216
|
+
exclude?: string[];
|
|
217
|
+
[key: string]: unknown;
|
|
218
|
+
}): Promise<JSONObject>;
|
|
219
|
+
/** Collect once, keeping the agent's raw object separate from normalized SDK metadata. */
|
|
220
|
+
collectWithMetadata(options?: {
|
|
221
|
+
exclude?: string[];
|
|
222
|
+
[key: string]: unknown;
|
|
223
|
+
}): Promise<CollectedFingerprint>;
|
|
224
|
+
private _collectFingerprint;
|
|
215
225
|
/**
|
|
216
226
|
* Submit a fingerprint event directly.
|
|
217
227
|
* @deprecated Prefer sdk.init() and sdk.onRouteChange().
|
|
218
228
|
*/
|
|
219
229
|
submitFingerprintEvent(fingerprint: FingerprintWireFormat, opts: SubmitFingerprintOptions): Promise<BrowserApiResult<SubmitFingerprintResult>>;
|
|
230
|
+
/** Submit any JSON object to the v3 fingerprint endpoint without changing it. */
|
|
231
|
+
submitFingerprint(payload: JSONObject): Promise<BrowserApiResult<IngestResult>>;
|
|
220
232
|
/**
|
|
221
233
|
* Check whether the current user is flagged for account sharing.
|
|
222
234
|
* Direct mode only — proxy-mode apps get verdicts from their own backend.
|
|
223
235
|
*/
|
|
224
236
|
checkUser(opts?: DirectIdentityOptions): Promise<BrowserApiResult<CheckUserData>>;
|
|
237
|
+
private _checkUser;
|
|
225
238
|
/**
|
|
226
239
|
* Send a 6-digit verification code to the user's email.
|
|
227
240
|
* Never retried — each attempt sends a real email. Rate-limited responses
|
|
@@ -233,6 +246,8 @@ export declare class UnsharedBrowser {
|
|
|
233
246
|
* budgeted server-side to block brute force.
|
|
234
247
|
*/
|
|
235
248
|
verify(code: string, opts?: DirectIdentityOptions): Promise<BrowserApiResult<VerifyData>>;
|
|
249
|
+
verifyEmail(request: VerifyEmailRequest): Promise<BrowserApiResult<VerifyEmailResult>>;
|
|
250
|
+
private _verifyEmail;
|
|
236
251
|
/** Report the email-send cooldown state without sending anything. */
|
|
237
252
|
emailVerificationStatus(opts?: DirectIdentityOptions): Promise<BrowserApiResult<VerificationStatusData>>;
|
|
238
253
|
/**
|
|
@@ -268,7 +283,11 @@ export declare class UnsharedBrowser {
|
|
|
268
283
|
private _directApi;
|
|
269
284
|
private _getStoredEmail;
|
|
270
285
|
private _getCachedFingerprint;
|
|
271
|
-
private
|
|
286
|
+
private _getCachedRawFingerprint;
|
|
287
|
+
private _getLifecycleCollection;
|
|
288
|
+
private _getOrCollectFingerprint;
|
|
289
|
+
private _storedVerificationId;
|
|
290
|
+
private _storedVerificationIdentity;
|
|
272
291
|
private _shouldProcessPath;
|
|
273
292
|
private _submitEvent;
|
|
274
293
|
/**
|
|
@@ -278,39 +297,10 @@ export declare class UnsharedBrowser {
|
|
|
278
297
|
private _submitUrl;
|
|
279
298
|
private _attachMpaListener;
|
|
280
299
|
private _sendWithRetry;
|
|
281
|
-
private
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
private
|
|
285
|
-
/**
|
|
286
|
-
* Encrypt a PII field (user_id, email_address) for direct-mode requests:
|
|
287
|
-
* AES-256-GCM keyed by SHA-256(publishable key), emitted as
|
|
288
|
-
* `base64(iv):base64(authTag):base64(ciphertext)` — byte-compatible with the
|
|
289
|
-
* Node SDK's encryptData, so the platform's existing ciphertext detection and
|
|
290
|
-
* decryption handle it unchanged. Returns the plaintext when WebCrypto is
|
|
291
|
-
* unavailable or fails: the ingress accepts both and always (re-)encrypts PII
|
|
292
|
-
* with the company secret key before anything is stored.
|
|
293
|
-
*/
|
|
294
|
-
private _encryptPII;
|
|
295
|
-
private _queueKey;
|
|
296
|
-
private _encryptQueuedDelivery;
|
|
297
|
-
private _decryptQueuedDelivery;
|
|
298
|
-
private _readQueueRecords;
|
|
299
|
-
private _writeQueueRecords;
|
|
300
|
-
private _queueFailedDelivery;
|
|
301
|
-
/**
|
|
302
|
-
* Recover a batch orphaned by a crashed/killed tab mid-flush. A flush claims
|
|
303
|
-
* its batch by leasing it here BEFORE clearing the main queue (see
|
|
304
|
-
* _flushQueuedEvents); if the process dies before the `finally` that removes
|
|
305
|
-
* the lease runs, the claimed records would otherwise be lost forever. A
|
|
306
|
-
* STALE lease (older than INFLIGHT_LEASE_MS) is presumed orphaned and merged
|
|
307
|
-
* back into the main queue. A FRESH lease means another tab is genuinely
|
|
308
|
-
* mid-flight — leave it untouched, or this would recreate the double-send
|
|
309
|
-
* that the claim-then-clear scheme exists to prevent.
|
|
310
|
-
*/
|
|
311
|
-
private _recoverStaleInflightLease;
|
|
312
|
-
private _flushQueuedEvents;
|
|
313
|
-
private _buildBody;
|
|
300
|
+
private _buildIdentifiers;
|
|
301
|
+
private _buildEventPayload;
|
|
302
|
+
private _ensurePermanentDeviceId;
|
|
303
|
+
private _isSameOriginProxy;
|
|
314
304
|
}
|
|
315
305
|
/**
|
|
316
306
|
* Creates an Axios response error interceptor that calls onFlagged
|
|
@@ -338,3 +328,4 @@ export declare function createAxiosInterceptor(opts: FlaggedInterceptorOptions):
|
|
|
338
328
|
* ```
|
|
339
329
|
*/
|
|
340
330
|
export declare function createFetchInterceptor(originalFetch: typeof fetch, opts: FlaggedInterceptorOptions): typeof fetch;
|
|
331
|
+
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { FingerprintConfig
|
|
2
|
-
|
|
1
|
+
import type { FingerprintConfig } from 'unshared-fingerprint-lib';
|
|
2
|
+
import type { JSONObject } from './shared-types';
|
|
3
|
+
export type GetFingerprint = (config?: FingerprintConfig) => Promise<JSONObject>;
|
|
3
4
|
export interface ResolvedAgent {
|
|
4
5
|
getFingerprint: GetFingerprint;
|
|
5
6
|
source: 'remote' | 'bundled';
|