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 CHANGED
@@ -1,6 +1,30 @@
1
1
  # unshared-frontend-sdk
2
2
 
3
- Browser SDK for [Unshared Labs](https://unsharedlabs.com) — collects a device fingerprint and sends it through your own backend. Your API key never touches the browser.
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 { UnsharedLabsBrowser } from 'unshared-frontend-sdk';
44
+ import { UnsharedBrowser } from 'unshared-frontend-sdk';
21
45
 
22
- // Same-origin (frontend served by the same backend): omit baseUrl
23
- const client = new UnsharedLabsBrowser({});
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 UnsharedLabsBrowser({ baseUrl: process.env.BACKEND_URL });
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 UnsharedLabsBrowser({
41
- baseUrl: process.env.BACKEND_URL, // optional — omit for same-origin
42
- maxRetries: 3, // optional, default: 3
43
- timeout: 30_000, // optional, default: 30s per attempt
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` | `""` (same-origin) | Base URL of your backend. Omit if your frontend and backend share the same domain. |
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.UnsharedLabsBrowser` namespace object. Destructure the class from it first:
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 { UnsharedLabsBrowser } = window.UnsharedLabsBrowser;
262
+ const { UnsharedBrowser } = window.UnsharedBrowser;
136
263
 
137
- // Omit baseUrl for same-origin. Set it for cross-origin: { baseUrl: 'https://api.example.com' }
138
- const client = new UnsharedLabsBrowser({});
264
+ // Direct mode using the publishable key packaged into the UMD build.
265
+ const client = new UnsharedBrowser({});
139
266
 
140
- client.collect().then(fp => {
141
- client.submitFingerprintEvent(fp);
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
- * Omit when the backend serves the frontend from the same origin.
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 no API key
59
- * is ever present in the browser.
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. Mirrors the same
93
- * dedup behavior in the auto-injected inline script.
173
+ * redundant FP row with an identical stable_hash.
94
174
  *
95
- * Persisted to sessionStorage so that hard reloads, framework double-boots,
96
- * and SDK re-instantiation within the same tab all still dedupe. The backend
97
- * `ON CONFLICT (idempotency_key)` check handles the cross-tab and cross-reload
98
- * cases that sessionStorage can't (sessionStorage is tab-scoped).
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
  /**