unshared-frontend-sdk 3.0.0-rc.14 → 3.0.0
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 +86 -4
- package/dist/browser.d.ts +3 -18
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.umd.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,6 +80,8 @@ new UnsharedBrowser({
|
|
|
80
80
|
});
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
### BrowserConfig
|
|
84
|
+
|
|
83
85
|
| Option | Type | Default | Description |
|
|
84
86
|
|--------|------|---------|-------------|
|
|
85
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). |
|
|
@@ -87,6 +89,10 @@ new UnsharedBrowser({
|
|
|
87
89
|
| `apiUrl` | `string` | `https://api.unshared.ai` | Direct mode: Unshared Labs platform origin. |
|
|
88
90
|
| `maxRetries` | `number` | `3` | Retry budget for eligible operations |
|
|
89
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. |
|
|
90
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). |
|
|
91
97
|
| `interstitialFlowType` | `string` | `email_verification` | Flow type requested for the auto-shown interstitial. |
|
|
92
98
|
|
|
@@ -104,7 +110,14 @@ Build a client-specific distribution with:
|
|
|
104
110
|
UNSHARED_PUBLISHABLE_API_KEY=upk_test_or_client_key npm --prefix sdks/javascript/browser run build
|
|
105
111
|
```
|
|
106
112
|
|
|
107
|
-
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.
|
|
108
121
|
|
|
109
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.
|
|
110
123
|
|
|
@@ -123,6 +136,44 @@ Use the confidential-docs override only for local investigation; do not commit m
|
|
|
123
136
|
|
|
124
137
|
## Methods
|
|
125
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
|
+
|
|
126
177
|
### `collect()`
|
|
127
178
|
|
|
128
179
|
Collect a browser fingerprint. Returns a `FingerprintWireFormat` object ready to pass to `submitFingerprintEvent`.
|
|
@@ -222,9 +273,15 @@ flow is authored in the Unshared dashboard and stored per company; the SDK fetch
|
|
|
222
273
|
published definition from `GET /v2/browser/interstitial-flow`, renders it in a
|
|
223
274
|
Shadow-DOM-isolated modal, and routes the flow's actions back through
|
|
224
275
|
`triggerEmailVerification()` / `verify()` above — so all verification logic stays
|
|
225
|
-
server-side. Direct mode
|
|
276
|
+
server-side. Direct mode uses `publishableKey`; proxy mode is described below.
|
|
226
277
|
|
|
227
|
-
**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.
|
|
228
285
|
|
|
229
286
|
### `showInterstitial(opts?)`
|
|
230
287
|
|
|
@@ -279,9 +336,34 @@ const app = express();
|
|
|
279
336
|
const client = new UnsharedClient({ apiKey: process.env.UNSHARED_API_KEY });
|
|
280
337
|
|
|
281
338
|
app.use(express.json());
|
|
282
|
-
|
|
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.
|
|
283
346
|
```
|
|
284
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
|
+
|
|
285
367
|
---
|
|
286
368
|
|
|
287
369
|
## Error handling
|
package/dist/browser.d.ts
CHANGED
|
@@ -172,6 +172,7 @@ export declare class UnsharedBrowser {
|
|
|
172
172
|
/** True after init() with isPaidSubscriber:false — blocks every submission path. */
|
|
173
173
|
private _doNotCollect;
|
|
174
174
|
private _mpaHandler;
|
|
175
|
+
private readonly _lifecycleState;
|
|
175
176
|
/** Interstitial auto-show config + live state. */
|
|
176
177
|
private readonly _enableInterstitial;
|
|
177
178
|
private readonly _interstitialFlowType;
|
|
@@ -179,22 +180,6 @@ export declare class UnsharedBrowser {
|
|
|
179
180
|
private _interstitialHandle;
|
|
180
181
|
/** Guards against rendering the modal more than once per flagged signal. */
|
|
181
182
|
private _interstitialOpen;
|
|
182
|
-
/**
|
|
183
|
-
* Dedup key for the last fingerprint submission: `${userId}|${route}`.
|
|
184
|
-
* Modern SPAs fire pushState/replaceState multiple times during hydration
|
|
185
|
-
* with the same URL — without this guard each call would generate a
|
|
186
|
-
* redundant FP row with an identical stable_hash.
|
|
187
|
-
*
|
|
188
|
-
* This is the IN-MEMORY layer (collapses bursts from this one instance). It
|
|
189
|
-
* works alongside two other client-side layers keyed on the same identity:
|
|
190
|
-
* the sessionStorage `__unshared_last_submit` mirror (survives hard reloads
|
|
191
|
-
* and SDK re-instantiation within the tab) and the page-scoped
|
|
192
|
-
* `window.__unshared.lastKey` guard shared with the auto-injected inline
|
|
193
|
-
* script (see getSharedDedup). Dedup is CLIENT-SIDE ONLY: the middleware
|
|
194
|
-
* appends a timestamp to X-Idempotency-Key (submit-fp.ts), so the backend
|
|
195
|
-
* drops only PubSub redeliveries, never two distinct submissions.
|
|
196
|
-
*/
|
|
197
|
-
private _lastSubmitKey;
|
|
198
183
|
constructor(config?: BrowserConfig);
|
|
199
184
|
/**
|
|
200
185
|
* Initialize the SDK after user login.
|
|
@@ -299,8 +284,8 @@ export declare class UnsharedBrowser {
|
|
|
299
284
|
private _getStoredEmail;
|
|
300
285
|
private _getCachedFingerprint;
|
|
301
286
|
private _getCachedRawFingerprint;
|
|
302
|
-
private
|
|
303
|
-
private
|
|
287
|
+
private _getLifecycleCollection;
|
|
288
|
+
private _getOrCollectFingerprint;
|
|
304
289
|
private _storedVerificationId;
|
|
305
290
|
private _storedVerificationIdentity;
|
|
306
291
|
private _shouldProcessPath;
|