unshared-frontend-sdk 2.0.1 → 2.1.0-rc.3
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 +191 -16
- package/dist/__tests__/setup.d.ts +1 -0
- package/dist/browser.d.ts +168 -9
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.umd.js +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
# unshared-frontend-sdk
|
|
2
2
|
|
|
3
|
-
Browser SDK for [Unshared Labs](https://
|
|
3
|
+
Browser SDK for [Unshared Labs](https://unshared.ai) — collects a device fingerprint and delivers it in one of two modes:
|
|
4
|
+
|
|
5
|
+
- **Proxy mode**: events go through your own backend (`baseUrl`). Your secret API key never touches the browser.
|
|
6
|
+
- **Direct mode**: events go straight to the Unshared Labs platform, authenticated by a **publishable key** (`upk_…`). No customer backend required — the tag-manager-style integration (à la Adobe Launch / BlueConic). The publishable key is public by design and only grants event submission.
|
|
7
|
+
|
|
8
|
+
> Client-specific script-tag builds can package `UNSHARED_PUBLISHABLE_API_KEY` into the UMD bundle. When the key is packaged, `new UnsharedBrowser({})` runs in direct mode. Use `baseUrl: ""` for same-origin proxy mode.
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
// Direct mode — no backend integration needed
|
|
12
|
+
const client = new UnsharedBrowser({ publishableKey: 'upk_your_key' });
|
|
13
|
+
await client.init({
|
|
14
|
+
userId: currentUser.id,
|
|
15
|
+
emailAddress: currentUser.email,
|
|
16
|
+
isPaidSubscriber: currentUser.hasSubscription, // false → nothing is ever collected or sent
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Direct mode also exposes the read/verification API:
|
|
20
|
+
const { data } = await client.checkUser();
|
|
21
|
+
if (data?.is_user_flagged) {
|
|
22
|
+
await client.triggerEmailVerification(); // emails a 6-digit code
|
|
23
|
+
const result = await client.verify(codeFromUser); // → { verified: true }
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Direct-mode reads are protected server-side: the publishable key only answers for the identity+device the browser presents, origins are allowlisted, and every endpoint is rate-limited per IP (plus per-email cooldowns and code-attempt budgets).
|
|
4
28
|
|
|
5
29
|
---
|
|
6
30
|
|
|
@@ -17,13 +41,13 @@ Or via CDN (no build step) — see [CDN / UMD usage](#cdn--umd-usage) below for
|
|
|
17
41
|
## Quick Start
|
|
18
42
|
|
|
19
43
|
```typescript
|
|
20
|
-
import {
|
|
44
|
+
import { UnsharedBrowser } from 'unshared-frontend-sdk';
|
|
21
45
|
|
|
22
|
-
// Same-origin (frontend served by the same backend)
|
|
23
|
-
const client = new
|
|
46
|
+
// Same-origin proxy mode (frontend served by the same backend)
|
|
47
|
+
const client = new UnsharedBrowser({ baseUrl: '' });
|
|
24
48
|
|
|
25
49
|
// Cross-origin (separate domains): set baseUrl via env var
|
|
26
|
-
// const client = new
|
|
50
|
+
// const client = new UnsharedBrowser({ baseUrl: process.env.BACKEND_URL });
|
|
27
51
|
|
|
28
52
|
// On page load — fire and forget
|
|
29
53
|
const fingerprint = await client.collect();
|
|
@@ -32,23 +56,62 @@ client.submitFingerprintEvent(fingerprint, { userId: currentUser?.id });
|
|
|
32
56
|
|
|
33
57
|
That's it. The SDK handles retries, timeouts, and errors silently.
|
|
34
58
|
|
|
59
|
+
> **Using this SDK alongside the Node `unsharedBoundToUser` middleware (Tier 1)?** The middleware
|
|
60
|
+
> also auto-injects an inline fingerprint script into your HTML. That is expected — the two
|
|
61
|
+
> submitters share a client-side dedup guard (`window.__unshared`) and emit **one** event per
|
|
62
|
+
> `(user, route)`. You do **not** need to turn injection off. See
|
|
63
|
+
> [_Fingerprint Collection: what loads what, who submits when_](../INTEGRATION_GUIDE.md#fingerprint-collection-what-loads-what-who-submits-when).
|
|
64
|
+
|
|
35
65
|
---
|
|
36
66
|
|
|
37
67
|
## Configuration
|
|
38
68
|
|
|
39
69
|
```typescript
|
|
40
|
-
new
|
|
41
|
-
baseUrl:
|
|
42
|
-
maxRetries: 3,
|
|
43
|
-
timeout: 30_000,
|
|
70
|
+
new UnsharedBrowser({
|
|
71
|
+
baseUrl: '', // same-origin proxy mode
|
|
72
|
+
maxRetries: 3, // optional, default: 3
|
|
73
|
+
timeout: 30_000, // optional, default: 30s per attempt
|
|
44
74
|
});
|
|
45
75
|
```
|
|
46
76
|
|
|
47
77
|
| Option | Type | Default | Description |
|
|
48
78
|
|--------|------|---------|-------------|
|
|
49
|
-
| `baseUrl` | `string` | `
|
|
79
|
+
| `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
|
+
| `publishableKey` | `string` | — | Direct mode: publishable key (`upk_…`) issued by Unshared Labs. When set, events bypass your backend and go straight to the platform. |
|
|
81
|
+
| `apiUrl` | `string` | `https://api.unshared.ai` | Direct mode: Unshared Labs platform origin. |
|
|
50
82
|
| `maxRetries` | `number` | `3` | How many times to retry on failure |
|
|
51
83
|
| `timeout` | `number` | `30000` | Per-attempt timeout in milliseconds |
|
|
84
|
+
| `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
|
+
| `interstitialFlowType` | `string` | `email_verification` | Flow type requested for the auto-shown interstitial. |
|
|
86
|
+
|
|
87
|
+
### Packaged publishable key
|
|
88
|
+
|
|
89
|
+
For a client-specific script-tag build, package the publishable key into the UMD bundle so the client does not pass it in page code. When `baseUrl` and `publishableKey` are omitted, the SDK resolves:
|
|
90
|
+
|
|
91
|
+
1. Explicit `publishableKey`.
|
|
92
|
+
2. Build-time `UNSHARED_PUBLISHABLE_API_KEY` baked in by `scripts/build.js` (client-specific builds only, e.g. the S3 flow).
|
|
93
|
+
3. Otherwise **no key** — the SDK stays in same-origin proxy mode. There is no hardcoded fallback; builds made without the env var (including every npm release) never carry a usable key.
|
|
94
|
+
|
|
95
|
+
Build a client-specific distribution with:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
UNSHARED_PUBLISHABLE_API_KEY=upk_test_or_client_key npm --prefix sdks/javascript/browser run build
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Passing `baseUrl`, including `baseUrl: ""`, keeps proxy mode and suppresses the packaged-key fallback.
|
|
102
|
+
|
|
103
|
+
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
|
+
|
|
105
|
+
The browser build also runs a markdown confidentiality guard before producing dist files. It removes obvious generated metadata sections and fails if markdown contains confidential metadata markers. Mentions of known key names print warnings by default because those names are sometimes documented intentionally.
|
|
106
|
+
|
|
107
|
+
Local overrides:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npm --prefix sdks/javascript/browser run build -- --allow-key-mentions
|
|
111
|
+
npm --prefix sdks/javascript/browser run build -- --allow-confidential-docs
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Use the confidential-docs override only for local investigation; do not commit markdown containing generated system metadata or real key values.
|
|
52
115
|
|
|
53
116
|
---
|
|
54
117
|
|
|
@@ -91,6 +154,70 @@ if (!result.success) {
|
|
|
91
154
|
|
|
92
155
|
---
|
|
93
156
|
|
|
157
|
+
### Direct-mode API (requires `publishableKey`)
|
|
158
|
+
|
|
159
|
+
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
|
+
|
|
161
|
+
| Method | Retries | Returns |
|
|
162
|
+
|--------|---------|---------|
|
|
163
|
+
| `checkUser(opts?)` | yes | `{ is_user_flagged: boolean }` |
|
|
164
|
+
| `triggerEmailVerification(opts?)` | **never** (each call emails a code) | `{ next_allowed_at, retry_after_seconds }`; rate-limited calls fail with `error.retryAfter` / `error.nextAllowedAt` for countdown UIs |
|
|
165
|
+
| `verify(code, opts?)` | **never** (attempts are budgeted) | `{ verified: boolean, reason?: 'invalid_code' }` |
|
|
166
|
+
| `emailVerificationStatus(opts?)` | yes | `{ can_send: boolean, next_allowed_at }` |
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Interstitial modal
|
|
171
|
+
|
|
172
|
+
Instead of wiring your own remediation UI, the SDK can render a **server-driven
|
|
173
|
+
interstitial modal** (e.g. an email-OTP verification flow) when a user is flagged. The
|
|
174
|
+
flow is authored in the Unshared dashboard and stored per company; the SDK fetches the
|
|
175
|
+
published definition from `GET /v2/browser/interstitial-flow`, renders it in a
|
|
176
|
+
Shadow-DOM-isolated modal, and routes the flow's actions back through
|
|
177
|
+
`triggerEmailVerification()` / `verify()` above — so all verification logic stays
|
|
178
|
+
server-side. Direct mode only (requires `publishableKey`).
|
|
179
|
+
|
|
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 — the browser sends only the OTP. This requires your middleware to resolve the email server-side (a `resolveEmailAddress` resolver or the `__unshared_email` cookie), the same requirement as the verify routes.
|
|
181
|
+
|
|
182
|
+
### `showInterstitial(opts?)`
|
|
183
|
+
|
|
184
|
+
Fetch and render the published flow on demand. No-op if a modal is already open.
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
const sdk = new UnsharedBrowser({ publishableKey: 'upk_…' });
|
|
188
|
+
await sdk.init({ userId, emailAddress });
|
|
189
|
+
|
|
190
|
+
// e.g. from your own flagged handler:
|
|
191
|
+
await sdk.showInterstitial(); // default flow_type, web variant
|
|
192
|
+
await sdk.showInterstitial({ flowType: 'email_verification', container: myEl });
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
| Option | Type | Default | Description |
|
|
196
|
+
|--------|------|---------|-------------|
|
|
197
|
+
| `flowType` | `string` | constructor's `interstitialFlowType` or `email_verification` | Which published flow to fetch |
|
|
198
|
+
| `container` | `HTMLElement` | `document.body` | Where to mount the modal |
|
|
199
|
+
|
|
200
|
+
Returns `{ success, data: { shown } }`; rendering errors are swallowed (never-throw).
|
|
201
|
+
`shown` is `false` when a modal was already open.
|
|
202
|
+
|
|
203
|
+
### Auto-show
|
|
204
|
+
|
|
205
|
+
Set `enableInterstitial: true` to render the modal automatically the first time the
|
|
206
|
+
`unshared:flagged` event fires (dispatched by the proxy-mode injected script, or by your
|
|
207
|
+
own code). If you detect flagging via [`createFetchInterceptor`](#error-handling) /
|
|
208
|
+
`createAxiosInterceptor` instead, call `sdk.showInterstitial()` from your `onFlagged`
|
|
209
|
+
handler.
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
const sdk = new UnsharedBrowser({ publishableKey: 'upk_…', enableInterstitial: true });
|
|
213
|
+
await sdk.init({ userId, emailAddress });
|
|
214
|
+
// On the next `unshared:flagged` event, the modal renders itself.
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`destroy()` removes any open modal and detaches the listener.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
94
221
|
## Backend requirement
|
|
95
222
|
|
|
96
223
|
The SDK sends fingerprints to `{baseUrl}/unshared/submit-fingerprint-event`. Your backend must handle this route. If you're using `unshared-clientjs-sdk`, the `createUnsharedMiddleware` sets this up automatically:
|
|
@@ -127,18 +254,66 @@ Retries happen automatically on network errors, timeouts, and server errors (5xx
|
|
|
127
254
|
|
|
128
255
|
## CDN / UMD usage
|
|
129
256
|
|
|
130
|
-
The UMD bundle exposes a `window.
|
|
257
|
+
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:
|
|
131
258
|
|
|
132
259
|
```html
|
|
133
260
|
<script src="https://unpkg.com/unshared-frontend-sdk@2.0.0-rc.4/dist/index.umd.js"></script>
|
|
134
261
|
<script>
|
|
135
|
-
const {
|
|
262
|
+
const { UnsharedBrowser } = window.UnsharedBrowser;
|
|
136
263
|
|
|
137
|
-
//
|
|
138
|
-
const client = new
|
|
264
|
+
// Direct mode using the publishable key packaged into the UMD build.
|
|
265
|
+
const client = new UnsharedBrowser({});
|
|
139
266
|
|
|
140
|
-
client.
|
|
141
|
-
|
|
267
|
+
client.init({
|
|
268
|
+
userId: currentUser.id,
|
|
269
|
+
emailAddress: currentUser.email,
|
|
270
|
+
isPaidSubscriber: currentUser.hasSubscription,
|
|
142
271
|
});
|
|
143
272
|
</script>
|
|
144
273
|
```
|
|
274
|
+
|
|
275
|
+
For proxy mode instead, pass `baseUrl` explicitly.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
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
|
+
## Real-browser acceptance coverage
|
|
303
|
+
|
|
304
|
+
For Boston Globe style script-tag integration, split client-side browser tests into host-site flows and SDK-owned delivery behavior.
|
|
305
|
+
|
|
306
|
+
| Area | Cases | Implementation status |
|
|
307
|
+
|------|-------|-----------------------|
|
|
308
|
+
| Signup | Successful signup initiation; duplicate email; missing or invalid password rules. | Host-site auth behavior. Test in the client's browser suite; no SDK code required beyond confirming the script tag remains loaded and does not block the flow. |
|
|
309
|
+
| Identity verification | Correct 6-digit code; wrong code; timeout/expired code. | Host-site or Unshared direct verification UI behavior, depending on who owns the screen. SDK exposes `triggerEmailVerification`, `verify`, and `emailVerificationStatus`; page-level form states must be tested in the client's browser suite. |
|
|
310
|
+
| Signup completion | Success page/message; failure page/message with reason. | Host-site auth behavior. Test in the client's browser suite. |
|
|
311
|
+
| Login | Valid login; wrong email; wrong password; unregistered email; missing password; missing email. | Host-site auth behavior. Test in the client's browser suite while verifying the SDK initializes only after a real identified user is available. |
|
|
312
|
+
| Password reset | Successful reset initiation. | Host-site auth behavior. Test in the client's browser suite. |
|
|
313
|
+
| 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
|
+
| 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
|
+
| 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 | Failed event sends retry until `maxRetries` is exhausted. | Implemented and unit-tested. |
|
|
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. |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/browser.d.ts
CHANGED
|
@@ -2,13 +2,26 @@ import type { FingerprintWireFormat } from '@unshared-labs/shared-types';
|
|
|
2
2
|
export interface BrowserConfig {
|
|
3
3
|
/**
|
|
4
4
|
* Base URL of the customer's own backend.
|
|
5
|
-
*
|
|
5
|
+
* Set to "" when the backend serves the frontend from the same origin.
|
|
6
|
+
* Omit only for direct mode.
|
|
6
7
|
*/
|
|
7
8
|
baseUrl?: string;
|
|
8
9
|
/** Max number of delivery retries per request. @default 3 */
|
|
9
10
|
maxRetries?: number;
|
|
10
11
|
/** Per-request fetch timeout in milliseconds. @default 30_000 */
|
|
11
12
|
timeout?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Publishable key (`upk_…`) issued by Unshared Labs. When set, the SDK runs in
|
|
15
|
+
* **direct mode**: events are sent straight to the Unshared Labs platform
|
|
16
|
+
* (`apiUrl`) instead of the customer's own backend (`baseUrl`). The key is
|
|
17
|
+
* public by design — it only grants event submission, never reads.
|
|
18
|
+
*/
|
|
19
|
+
publishableKey?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Unshared Labs platform origin used in direct mode.
|
|
22
|
+
* @default "https://api.unshared.ai"
|
|
23
|
+
*/
|
|
24
|
+
apiUrl?: string;
|
|
12
25
|
/** When set, events are only submitted for paths matching one of these prefixes. */
|
|
13
26
|
includePathPrefix?: string[];
|
|
14
27
|
/** Paths to skip entirely — prefix match against location.pathname. */
|
|
@@ -27,10 +40,40 @@ export interface BrowserConfig {
|
|
|
27
40
|
* Falls back to existing localStorage/fingerprint behavior when undefined is returned.
|
|
28
41
|
*/
|
|
29
42
|
deviceId?: () => string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* When `true`, the SDK auto-renders an interstitial modal (direct mode only)
|
|
45
|
+
* the first time the user is flagged — detected via the `unshared:flagged`
|
|
46
|
+
* window event. Default `false` (fully backward compatible). Apps that detect
|
|
47
|
+
* flagging themselves (e.g. via {@link createFetchInterceptor}) can instead
|
|
48
|
+
* call {@link UnsharedBrowser.showInterstitial} from their own handler.
|
|
49
|
+
*/
|
|
50
|
+
enableInterstitial?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Flow type to request for the auto-shown interstitial.
|
|
53
|
+
* @default "email_verification"
|
|
54
|
+
*/
|
|
55
|
+
interstitialFlowType?: string;
|
|
56
|
+
}
|
|
57
|
+
/** Options for {@link UnsharedBrowser.showInterstitial}. */
|
|
58
|
+
export interface ShowInterstitialOptions {
|
|
59
|
+
/** Flow type to fetch. @default the constructor's interstitialFlowType or "email_verification". */
|
|
60
|
+
flowType?: string;
|
|
61
|
+
/** Where to mount the modal. @default document.body. */
|
|
62
|
+
container?: HTMLElement;
|
|
63
|
+
/** Called when the flow reaches its terminal (success) screen. */
|
|
64
|
+
onComplete?: () => void;
|
|
65
|
+
/** Called when the modal is dismissed (programmatically / on success-dismiss). */
|
|
66
|
+
onDismiss?: () => void;
|
|
30
67
|
}
|
|
31
68
|
export interface InitOptions {
|
|
32
69
|
userId: string;
|
|
33
70
|
emailAddress: string;
|
|
71
|
+
/**
|
|
72
|
+
* Whether this user is a paying subscriber. When `false`, the SDK collects
|
|
73
|
+
* and submits **nothing** for this user — non-subscriber data must not be
|
|
74
|
+
* received. Omit (or pass `true`) for users whose data should flow.
|
|
75
|
+
*/
|
|
76
|
+
isPaidSubscriber?: boolean;
|
|
34
77
|
}
|
|
35
78
|
export interface SubmitFingerprintOptions {
|
|
36
79
|
userId: string;
|
|
@@ -47,16 +90,42 @@ export interface BrowserApiResult<T = unknown> {
|
|
|
47
90
|
error?: {
|
|
48
91
|
code: string;
|
|
49
92
|
message: string;
|
|
93
|
+
/** Seconds until the call may be retried (rate-limited responses). */
|
|
94
|
+
retryAfter?: number;
|
|
95
|
+
/** RFC3339 timestamp of when the call may be retried (rate-limited responses). */
|
|
96
|
+
nextAllowedAt?: string;
|
|
50
97
|
};
|
|
51
98
|
}
|
|
99
|
+
/** Identity override for the direct-mode API methods. Defaults to the identity
|
|
100
|
+
* captured by init() (email) and the stable fingerprint hash (deviceId). */
|
|
101
|
+
export interface DirectIdentityOptions {
|
|
102
|
+
email?: string;
|
|
103
|
+
deviceId?: string;
|
|
104
|
+
}
|
|
105
|
+
export interface CheckUserData {
|
|
106
|
+
is_user_flagged: boolean;
|
|
107
|
+
}
|
|
108
|
+
export interface TriggerVerificationData {
|
|
109
|
+
message?: string;
|
|
110
|
+
next_allowed_at?: string;
|
|
111
|
+
retry_after_seconds?: number;
|
|
112
|
+
}
|
|
113
|
+
export interface VerifyData {
|
|
114
|
+
verified: boolean;
|
|
115
|
+
reason?: string;
|
|
116
|
+
}
|
|
117
|
+
export interface VerificationStatusData {
|
|
118
|
+
can_send: boolean;
|
|
119
|
+
next_allowed_at?: string | null;
|
|
120
|
+
}
|
|
52
121
|
export interface FlaggedInterceptorOptions {
|
|
53
122
|
onFlagged: () => void;
|
|
54
123
|
}
|
|
55
124
|
/**
|
|
56
125
|
* Browser SDK for Unshared Labs.
|
|
57
126
|
*
|
|
58
|
-
* Routes fingerprint events through the customer's own backend
|
|
59
|
-
*
|
|
127
|
+
* Routes fingerprint events through the customer's own backend in proxy mode,
|
|
128
|
+
* or through the Unshared Labs platform in publishable-key direct mode.
|
|
60
129
|
*
|
|
61
130
|
* Never-throw contract: all public methods catch all errors and never reject.
|
|
62
131
|
*
|
|
@@ -74,6 +143,8 @@ export interface FlaggedInterceptorOptions {
|
|
|
74
143
|
*/
|
|
75
144
|
export declare class UnsharedBrowser {
|
|
76
145
|
private readonly _baseUrl;
|
|
146
|
+
private readonly _publishableKey;
|
|
147
|
+
private readonly _apiUrl;
|
|
77
148
|
private readonly _maxRetries;
|
|
78
149
|
private readonly _timeout;
|
|
79
150
|
private readonly _includePathPrefix;
|
|
@@ -84,18 +155,31 @@ export declare class UnsharedBrowser {
|
|
|
84
155
|
private _deviceId;
|
|
85
156
|
private _userId;
|
|
86
157
|
private _emailAddress;
|
|
158
|
+
/** True after init() with isPaidSubscriber:false — blocks every submission path. */
|
|
159
|
+
private _doNotCollect;
|
|
87
160
|
private _mpaHandler;
|
|
161
|
+
private _flushingQueue;
|
|
162
|
+
/** Interstitial auto-show config + live state. */
|
|
163
|
+
private readonly _enableInterstitial;
|
|
164
|
+
private readonly _interstitialFlowType;
|
|
165
|
+
private _interstitialListener;
|
|
166
|
+
private _interstitialHandle;
|
|
167
|
+
/** Guards against rendering the modal more than once per flagged signal. */
|
|
168
|
+
private _interstitialOpen;
|
|
88
169
|
/**
|
|
89
170
|
* Dedup key for the last fingerprint submission: `${userId}|${route}`.
|
|
90
171
|
* Modern SPAs fire pushState/replaceState multiple times during hydration
|
|
91
172
|
* with the same URL — without this guard each call would generate a
|
|
92
|
-
* redundant FP row with an identical stable_hash.
|
|
93
|
-
* dedup behavior in the auto-injected inline script.
|
|
173
|
+
* redundant FP row with an identical stable_hash.
|
|
94
174
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
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.
|
|
99
183
|
*/
|
|
100
184
|
private _lastSubmitKey;
|
|
101
185
|
constructor(config?: BrowserConfig);
|
|
@@ -133,13 +217,88 @@ export declare class UnsharedBrowser {
|
|
|
133
217
|
* @deprecated Prefer sdk.init() and sdk.onRouteChange().
|
|
134
218
|
*/
|
|
135
219
|
submitFingerprintEvent(fingerprint: FingerprintWireFormat, opts: SubmitFingerprintOptions): Promise<BrowserApiResult<SubmitFingerprintResult>>;
|
|
220
|
+
/**
|
|
221
|
+
* Check whether the current user is flagged for account sharing.
|
|
222
|
+
* Direct mode only — proxy-mode apps get verdicts from their own backend.
|
|
223
|
+
*/
|
|
224
|
+
checkUser(opts?: DirectIdentityOptions): Promise<BrowserApiResult<CheckUserData>>;
|
|
225
|
+
/**
|
|
226
|
+
* Send a 6-digit verification code to the user's email.
|
|
227
|
+
* Never retried — each attempt sends a real email. Rate-limited responses
|
|
228
|
+
* carry `error.retryAfter` / `error.nextAllowedAt` for countdown UIs.
|
|
229
|
+
*/
|
|
230
|
+
triggerEmailVerification(opts?: DirectIdentityOptions): Promise<BrowserApiResult<TriggerVerificationData>>;
|
|
231
|
+
/**
|
|
232
|
+
* Verify a code from triggerEmailVerification. Never retried — attempts are
|
|
233
|
+
* budgeted server-side to block brute force.
|
|
234
|
+
*/
|
|
235
|
+
verify(code: string, opts?: DirectIdentityOptions): Promise<BrowserApiResult<VerifyData>>;
|
|
236
|
+
/** Report the email-send cooldown state without sending anything. */
|
|
237
|
+
emailVerificationStatus(opts?: DirectIdentityOptions): Promise<BrowserApiResult<VerificationStatusData>>;
|
|
238
|
+
/**
|
|
239
|
+
* Fetch the published interstitial flow and render it as a Shadow-DOM modal.
|
|
240
|
+
* Works in direct mode (publishableKey) AND proxy mode (baseUrl). In proxy mode
|
|
241
|
+
* the flow is fetched through the customer backend and the modal's actions run
|
|
242
|
+
* through the middleware verify routes — identity is resolved server-side, so the
|
|
243
|
+
* browser sends only the OTP. Returns a BrowserApiResult describing the fetch
|
|
244
|
+
* outcome; rendering errors are swallowed (never-throw). No-op if already open.
|
|
245
|
+
*/
|
|
246
|
+
showInterstitial(opts?: ShowInterstitialOptions): Promise<BrowserApiResult<{
|
|
247
|
+
shown: boolean;
|
|
248
|
+
}>>;
|
|
249
|
+
/** Direct-mode flow fetch (publishable key → platform API). */
|
|
250
|
+
private _fetchDirectFlow;
|
|
251
|
+
/** Proxy-mode flow fetch (customer backend → secret key). */
|
|
252
|
+
private _fetchProxyFlow;
|
|
253
|
+
/**
|
|
254
|
+
* Proxy-mode actions adapter: routes the flow's operations to the middleware verify
|
|
255
|
+
* routes. Identity is resolved server-side, so only the OTP is sent. Throws on
|
|
256
|
+
* failure so the renderer shows an inline error.
|
|
257
|
+
*/
|
|
258
|
+
private _buildProxyActionsAdapter;
|
|
259
|
+
/**
|
|
260
|
+
* Maps the flow's logical action operations to this SDK's direct-mode methods
|
|
261
|
+
* (see shared/specs/interstitial-actions.md). Resolves on success; throws on
|
|
262
|
+
* failure so the renderer can show an inline error.
|
|
263
|
+
*/
|
|
264
|
+
private _buildActionsAdapter;
|
|
265
|
+
/** Attach the one-shot `unshared:flagged` listener that auto-shows the modal. */
|
|
266
|
+
private _attachInterstitialListener;
|
|
267
|
+
private _resolveDirectIdentity;
|
|
268
|
+
private _directApi;
|
|
136
269
|
private _getStoredEmail;
|
|
137
270
|
private _getCachedFingerprint;
|
|
138
271
|
private _cacheFingerprint;
|
|
139
272
|
private _shouldProcessPath;
|
|
140
273
|
private _submitEvent;
|
|
274
|
+
/**
|
|
275
|
+
* Direct mode (publishableKey set) targets the Unshared Labs platform;
|
|
276
|
+
* proxy mode targets the customer's own backend middleware.
|
|
277
|
+
*/
|
|
278
|
+
private _submitUrl;
|
|
141
279
|
private _attachMpaListener;
|
|
142
280
|
private _sendWithRetry;
|
|
281
|
+
private _piiKeyPromise;
|
|
282
|
+
/** AES-GCM key derived as SHA-256(publishable key) — the same derivation the
|
|
283
|
+
* platform applies to SDK ciphertext, so it can decrypt at ingress. */
|
|
284
|
+
private _piiKey;
|
|
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
|
+
private _flushQueuedEvents;
|
|
143
302
|
private _buildBody;
|
|
144
303
|
}
|
|
145
304
|
/**
|