unshared-clientjs-sdk 2.3.0 → 3.0.0-rc.14

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.
Files changed (68) hide show
  1. package/README.md +176 -151
  2. package/dist/client.d.ts +54 -25
  3. package/dist/client.js +1 -1
  4. package/dist/esm/client.d.mts +54 -25
  5. package/dist/esm/client.mjs +1 -1
  6. package/dist/esm/index.d.mts +8 -8
  7. package/dist/esm/index.mjs +1 -1
  8. package/dist/esm/middleware/dispatch-processing.d.mts +6 -3
  9. package/dist/esm/middleware/dispatch-processing.mjs +1 -1
  10. package/dist/esm/middleware/index.d.mts +7 -5
  11. package/dist/esm/middleware/index.mjs +1 -1
  12. package/dist/esm/middleware/injection/fingerprint-script.d.mts +7 -0
  13. package/dist/esm/middleware/injection/fingerprint-script.mjs +1 -1
  14. package/dist/esm/middleware/injection/fp-bundle-source.d.mts +5 -4
  15. package/dist/esm/middleware/injection/fp-bundle-source.mjs +1 -1
  16. package/dist/esm/middleware/response-interceptor.d.mts +1 -1
  17. package/dist/esm/middleware/routes/interstitial.d.mts +2 -2
  18. package/dist/esm/middleware/routes/interstitial.mjs +1 -1
  19. package/dist/esm/middleware/routes/submit-fp.d.mts +5 -5
  20. package/dist/esm/middleware/routes/submit-fp.mjs +1 -1
  21. package/dist/esm/middleware/routes/verify.d.mts +3 -3
  22. package/dist/esm/middleware/routes/verify.mjs +1 -1
  23. package/dist/esm/middleware/utils/client-ip.d.mts +1 -1
  24. package/dist/esm/middleware/utils/cookies.d.mts +1 -1
  25. package/dist/esm/middleware/utils/device-id.d.mts +1 -1
  26. package/dist/esm/middleware/utils/device-id.mjs +1 -1
  27. package/dist/esm/middleware/utils/flagged-response.mjs +1 -1
  28. package/dist/esm/middleware/utils/http-helpers.d.mts +1 -1
  29. package/dist/esm/middleware/utils/permanent-device-id.d.mts +7 -0
  30. package/dist/esm/middleware/utils/permanent-device-id.mjs +1 -0
  31. package/dist/esm/middleware/utils/read-json-body.d.mts +3 -0
  32. package/dist/esm/middleware/utils/read-json-body.mjs +1 -0
  33. package/dist/esm/middleware/utils/secure.d.mts +1 -1
  34. package/dist/esm/middleware.d.mts +5 -6
  35. package/dist/esm/middleware.mjs +1 -1
  36. package/dist/esm/shared-types.d.mts +148 -0
  37. package/dist/esm/web/index.d.mts +6 -6
  38. package/dist/esm/web/index.mjs +1 -1
  39. package/dist/esm/web/protection-handler.d.mts +5 -5
  40. package/dist/esm/web/protection-handler.mjs +1 -1
  41. package/dist/esm/web/submit-handler.d.mts +2 -2
  42. package/dist/esm/web/submit-handler.mjs +1 -1
  43. package/dist/esm/web/types.d.mts +1 -1
  44. package/dist/esm/web/web-helpers.d.mts +11 -0
  45. package/dist/esm/web/web-helpers.mjs +1 -1
  46. package/dist/middleware/dispatch-processing.d.ts +3 -0
  47. package/dist/middleware/dispatch-processing.js +1 -1
  48. package/dist/middleware/index.d.ts +2 -0
  49. package/dist/middleware/index.js +1 -1
  50. package/dist/middleware/injection/fingerprint-script.d.ts +7 -0
  51. package/dist/middleware/injection/fingerprint-script.js +1 -1
  52. package/dist/middleware/injection/fp-bundle-source.d.ts +5 -4
  53. package/dist/middleware/injection/fp-bundle-source.js +1 -1
  54. package/dist/middleware/routes/submit-fp.js +1 -1
  55. package/dist/middleware/routes/verify.js +1 -1
  56. package/dist/middleware/utils/permanent-device-id.d.ts +7 -0
  57. package/dist/middleware/utils/permanent-device-id.js +1 -0
  58. package/dist/middleware/utils/read-json-body.d.ts +3 -0
  59. package/dist/middleware/utils/read-json-body.js +1 -0
  60. package/dist/middleware.d.ts +3 -4
  61. package/dist/middleware.js +1 -1
  62. package/dist/shared-types.d.ts +148 -0
  63. package/dist/web/protection-handler.d.ts +1 -1
  64. package/dist/web/protection-handler.js +1 -1
  65. package/dist/web/submit-handler.js +1 -1
  66. package/dist/web/web-helpers.d.ts +11 -0
  67. package/dist/web/web-helpers.js +1 -1
  68. package/package.json +4 -4
package/README.md CHANGED
@@ -1,240 +1,265 @@
1
1
  # unshared-clientjs-sdk
2
2
 
3
- Server-side Node.js SDK for [Unshared](https://unshared.ai) detect account sharing, analyze user events for fraud, and run email verification flows.
4
-
5
- ---
3
+ Node.js SDK for Unshared v3 ingestion, risk checks, email verification, and application protection middleware.
6
4
 
7
5
  ## Install
8
6
 
9
7
  ```bash
10
- npm install unshared-clientjs-sdk
8
+ npm install unshared-clientjs-sdk@^3
11
9
  ```
12
10
 
13
- **Requires Node.js 18+**
14
-
15
- ---
16
-
17
- ## Quick Start
11
+ Requires Node.js 20 or newer.
18
12
 
19
13
  ```typescript
20
14
  import { UnsharedClient } from 'unshared-clientjs-sdk';
21
15
 
22
16
  const client = new UnsharedClient({
23
- apiKey: process.env.UNSHARED_API_KEY, // usk_
17
+ apiKey: process.env.UNSHARED_API_KEY!, // usk_...
24
18
  });
25
19
  ```
26
20
 
27
- ---
21
+ V3 fingerprint and user-event ingestion encrypts the complete JSON object with AES-256-GCM before sending it to Unshared. Check, Trigger, and Verify remain structured plaintext JSON over HTTPS.
22
+
23
+ ## V3 Methods
28
24
 
29
- ## Methods
25
+ ### `submitFingerprint(payload)`
26
+
27
+ Sends any JSON object to `POST /v3/submit-fingerprint-event` without reshaping it.
28
+
29
+ ```typescript
30
+ const result = await client.submitFingerprint({
31
+ hash: 'full-456',
32
+ stable_hash: 'stable-123',
33
+ components: {},
34
+ custom_data: { nested: ['preserved', 1, false] },
35
+ });
36
+ ```
30
37
 
31
- ### `processUserEvent(params)`
38
+ ### `processUserEvent(payload)`
32
39
 
33
- Record a user event and get a fraud signal back. Call this on login, signup, or any high-value action.
40
+ Sends any JSON object to `POST /v3/process-user-event` without reshaping it.
34
41
 
35
42
  ```typescript
36
43
  const result = await client.processUserEvent({
37
- eventType: 'login',
38
- userId: 'user_123',
39
- emailAddress: 'user@example.com',
40
- deviceId: 'device_abc',
41
- sessionHash: 'session_xyz',
42
- ipAddress: '1.2.3.4', // plaintext — not encrypted
43
- userAgent: req.headers['user-agent'],
44
+ event_type: 'LOGIN',
45
+ identity: { user_id: user.id, email_address: user.email },
46
+ identifiers: {
47
+ unshared_device_id: permanentDeviceId,
48
+ device_id: deviceId,
49
+ session_hash: sessionId,
50
+ },
51
+ custom_data: { plan: 'pro' },
44
52
  });
53
+ ```
45
54
 
46
- if (result.success && result.data?.analysis.is_user_flagged) {
47
- // Block or challenge the user
55
+ Both ingestion calls accept objects up to 1 MiB and return:
56
+
57
+ ```typescript
58
+ {
59
+ event_id: string;
60
+ collected_at: string;
61
+ endpoint_version: 'v3';
48
62
  }
49
63
  ```
50
64
 
51
- **Fields encrypted before sending:** `emailAddress`, `deviceId`
52
-
53
- ---
65
+ Ingestion is not automatically retried because an ambiguous failure may already have created an event.
54
66
 
55
- ### `checkUser(emailAddress, deviceId)`
67
+ ### `checkUser(...)`
56
68
 
57
- Quick check to see if a user is flagged. Useful in middleware or route guards.
69
+ Checks existing risk state through `POST /v3/check-user`.
58
70
 
59
71
  ```typescript
60
- const result = await client.checkUser('user@example.com', 'device_abc');
72
+ const result = await client.checkUser('user@example.com', {
73
+ permanentDeviceId,
74
+ deviceId,
75
+ fingerprintId: stableHash,
76
+ fullHash,
77
+ sessionHash,
78
+ });
61
79
 
62
80
  if (result.data?.is_user_flagged) {
63
- // Deny access
81
+ // Block or challenge.
64
82
  }
65
83
  ```
66
84
 
67
- > **Safe default:** Returns `{ is_user_flagged: false }` on any failure (network error, outage). A backend outage will never accidentally block a legitimate user.
85
+ You can also pass the v3 request object directly:
86
+
87
+ ```typescript
88
+ await client.checkUser({
89
+ user_id: user.id,
90
+ email_address: user.email,
91
+ identifiers: { device_id: deviceId },
92
+ });
93
+ ```
68
94
 
69
- ---
95
+ `checkUser` is read-only and can retry. On transport or HTTP failure it deliberately returns a clean verdict with `failedOpen` metadata so an outage does not accidentally block users.
70
96
 
71
- ### `triggerEmailVerification(emailAddress, deviceId)`
97
+ ### Email Verification
72
98
 
73
- Send a 6-digit verification code to the user's email.
99
+ Trigger returns a challenge ID that Verify must send back:
74
100
 
75
101
  ```typescript
76
- await client.triggerEmailVerification('user@example.com', 'device_abc');
77
- ```
78
-
79
- ---
102
+ const trigger = await client.triggerEmailVerification(user.email, deviceId, {
103
+ permanentDeviceId,
104
+ fingerprintId: stableHash,
105
+ });
80
106
 
81
- ### `verify(emailAddress, deviceId, code)`
107
+ const verify = await client.verifyEmail({
108
+ verification_id: trigger.data!.verification_id!,
109
+ code: submittedCode, // string preserves leading zeroes
110
+ });
111
+ ```
82
112
 
83
- Validate the code the user submitted.
113
+ `verifyEmail(...)` is stateless and recommended for distributed deployments. The source-compatible methods retain the challenge in the current `UnsharedClient` instance:
84
114
 
85
115
  ```typescript
86
- const result = await client.verify('user@example.com', 'device_abc', '123456');
87
-
88
- if (!result.success) {
89
- if (result.error?.code === 'VERIFICATION_FAILED') {
90
- // Wrong or expired code — ask user to retry
91
- } else {
92
- // Transport error (DELIVERY_FAILED) — retry or show generic error
93
- }
94
- } else {
95
- // Verified — success: true means the code was correct
96
- }
116
+ await client.triggerEmailVerification(user.email, deviceId);
117
+ const verify = await client.verify(user.email, deviceId, submittedCode);
97
118
  ```
98
119
 
99
- ---
120
+ Trigger and Verify are never automatically retried because the first request may have committed before an ambiguous response failure.
100
121
 
101
- ### `submitFingerprintEvent(fingerprint, opts?)`
122
+ ## Compatibility Methods
102
123
 
103
- Submit a browser fingerprint collected by `unshared-frontend-sdk`. Typically called by the middleware you usually won't call this directly.
124
+ Existing v2 package call sites remain valid while using v3 routes and encrypted ingestion envelopes.
104
125
 
105
126
  ```typescript
106
- await client.submitFingerprintEvent(fingerprint, {
107
- userId: 'user_123',
108
- sessionHash: 'session_xyz',
109
- eventType: 'page_view',
127
+ const result = await client.processUserEvent({
128
+ eventType: 'login',
129
+ userId: user.id,
130
+ emailAddress: user.email,
131
+ deviceId,
132
+ sessionHash: sessionId,
133
+ ipAddress: req.ip,
134
+ userAgent: req.headers['user-agent'] ?? '',
110
135
  });
136
+
137
+ if (result.success && result.data?.analysis.is_user_flagged) {
138
+ // Block or challenge.
139
+ }
111
140
  ```
112
141
 
113
- ---
142
+ `submitFingerprintEvent(fingerprint, opts)` is also retained. New proxy implementations should prefer `submitFingerprint(body)` so unknown future fields cannot be lost.
114
143
 
115
- ## Protection Middleware (Recommended)
144
+ ## Protection Middleware
116
145
 
117
- `unsharedBoundToUser` is the full-featured middleware: auto-injects the fingerprint script, enforces verdicts, handles email verification flows, and dispatches events.
146
+ `unsharedBoundToUser` injects browser fingerprint collection, protects routes, caches verdicts, and hosts the proxy verification flow.
118
147
 
119
148
  ```typescript
120
- import { UnsharedClient, unsharedBoundToUser, flaggedResponse } from 'unshared-clientjs-sdk';
121
-
149
+ import express from 'express';
150
+ import {
151
+ UnsharedClient,
152
+ unsharedBoundToUser,
153
+ flaggedResponse,
154
+ } from 'unshared-clientjs-sdk';
155
+
156
+ const app = express();
122
157
  app.set('trust proxy', 1);
123
- app.use(express.json());
158
+ app.use(express.json()); // the default 100 KiB limit is supported
124
159
 
125
160
  const client = new UnsharedClient({ apiKey: process.env.UNSHARED_API_KEY! });
126
-
127
161
  app.use(unsharedBoundToUser(client, {
128
- userId: (req) => req.cookies?.userId,
129
- emailAddress: (req) => req.cookies?.email,
130
- includePathPrefix: ['/api/'],
162
+ userId: (req) => req.user?.id,
163
+ emailAddress: (req) => req.user?.email,
131
164
  onFlagged: ({ emailAddress, res }) => {
132
165
  res.status(403).json(flaggedResponse(emailAddress));
133
166
  },
134
167
  }));
135
168
  ```
136
169
 
137
- **Smoke test:** `curl http://localhost:3000/__unshared/status` returns `{ "status": "anonymous" | "ok" | "flagged" }`.
138
-
139
- **Key options:**
140
-
141
- | Option | Type | Default | Description |
142
- |--------|------|---------|-------------|
143
- | `userId` | `(req) => string \| undefined` | — | **Required.** Resolve the current user's ID |
144
- | `emailAddress` | `(req) => string \| undefined` | | Resolve the current user's email |
145
- | `routePrefix` | `string` | `"/__unshared"` | Route mount prefix |
146
- | `includePathPrefix` | `string[]` | | Only these path prefixes trigger verdicts and events |
147
- | `onFlagged` | `(ctx) => void` | | Called when a flagged user makes a request |
148
- | `disableBotFilter` | `boolean` | `false` | Skip bot UA filter (enable for E2E testing) |
149
- | `checkUserTimeoutMs` | `number` | `500` | Timeout (ms) for checkUser API calls; fails open on timeout |
150
- | `skipPaths` | `string[]` | — | Paths to skip entirely (static assets, health checks) |
151
- | `corsOrigins` | `string \| string[]` | | Allowed CORS origins; handles OPTIONS preflight |
152
- | `onError` | `(error, ctx) => void` | | Called on background SDK errors for observability |
153
-
154
- `flaggedResponse(email)` formats the 403 body the inline script expects to trigger the `unshared:flagged` browser event. `ACCOUNT_FLAGGED_ERROR` is the error code string it uses, also exported for server-side checks.
155
-
156
- ### Interstitial (proxy mode)
157
-
158
- `unsharedBoundToUser` mounts a `GET /__unshared/interstitial-flow` route that returns the company's published interstitial flow. It calls `client.getInterstitialFlow()` with the secret key and carries no user data. The browser SDK's `showInterstitial()` calls this route automatically when constructed with `baseUrl` (proxy mode), then runs the modal's OTP actions through the existing `/__unshared/verify-trigger` and `/__unshared/verify` routes. Identity is resolved server-side via your `emailAddress` resolver or the `__unshared_email` cookie — the browser sends only the OTP.
159
-
160
- ---
161
-
162
- ## Simple Fingerprint Middleware
163
-
164
- `createUnsharedMiddleware` is a lightweight alternative that only proxies fingerprint events — no verdicts, no script injection, no verification flows. Use `unsharedBoundToUser` unless you have a specific reason not to.
170
+ Keep your existing parser order and limits. Mount authentication/session middleware
171
+ needed by your resolvers before Unshared. The SDK also reads its own unparsed POST
172
+ routes automatically, up to **1 MiB (1,048,576 UTF-8 bytes)**, without a configuration
173
+ option or an Express dependency. Ordinary application routes are not parsed by the
174
+ SDK. Existing parsed bodies are respected and checked against the same limit.
175
+ Structured browser-to-proxy submissions (including the inline script) fit Express's
176
+ default **100 KiB (102,400 serialized UTF-8 bytes)** without public configuration:
177
+ send the full payload if it fits; otherwise empty both local/session storage lists
178
+ while keeping every cookie; if still too large, omit `context.browser_storage`.
179
+ The complete raw fingerprint, including future fields, and all core identity,
180
+ device, session, SDK and event context are never trimmed. If those alone exceed
181
+ 100 KiB, the browser returns `REQUEST_TOO_LARGE`; the inline script logs that code
182
+ and sends nothing. Cookies are never partially selected or truncated.
183
+
184
+ Low-level browser `submitFingerprint(payload)` preserves arbitrary objects exactly
185
+ and rejects proxy payloads above 100 KiB rather than reducing them. Direct browser
186
+ submission retains full snapshots and its 1 MiB logical limit. An earlier parser
187
+ or reverse proxy with a smaller limit can still reject a request before Unshared.
188
+
189
+ Oversized SDK request bodies return 413, malformed JSON returns 400, and unfinished
190
+ reads time out after 30 seconds with 408. Raw bodies must use identity encoding
191
+ (compressed bodies return 415). Fingerprints and retained cookies are not truncated;
192
+ duplicate SDK fingerprint caches are excluded from storage snapshots. The encrypted
193
+ platform request retains its existing 1.5 MiB envelope limit; the full logical payload,
194
+ including server-added context, must still fit within 1 MiB.
195
+
196
+ | Option | Description |
197
+ |---|---|
198
+ | `userId` | Required server-side user resolver |
199
+ | `emailAddress` | Server-side email resolver used for checks and verification |
200
+ | `routePrefix` | Internal route prefix; default `/__unshared` |
201
+ | `includePathPrefix` | Paths that trigger verdict checks and events |
202
+ | `onFlagged` | Custom blocked-user response |
203
+ | `disableBotFilter` | Include known bot user agents, useful in E2E |
204
+ | `checkUserTimeoutMs` | Bounded verdict-check timeout |
205
+ | `corsOrigins` | Allowed cross-origin frontend origins |
206
+ | `onError` | Background SDK error observer |
207
+
208
+ The middleware exposes:
209
+
210
+ - `POST /__unshared/submit-fp`
211
+ - `GET /__unshared/status`
212
+ - `POST /__unshared/verify-trigger`
213
+ - `POST /__unshared/verify`
214
+ - `GET /__unshared/fp.js`
215
+ - `GET /__unshared/interstitial-flow`
216
+
217
+ The verification trigger stores `verification_id` in an HttpOnly, ten-minute `__unshared_verification_id` cookie. The permanent readable `__unshared_device_id=upid_<uuid-v4>` cookie rolls for 400 days and is not tied to mutable fingerprint hashes.
218
+
219
+ ## Simple Proxy Middleware
220
+
221
+ `createUnsharedMiddleware` only proxies fingerprint events. It preserves arbitrary fields and augments trusted request context.
165
222
 
166
223
  ```typescript
167
- import { createUnsharedMiddleware } from 'unshared-clientjs-sdk';
224
+ import { createUnsharedMiddleware } from 'unshared-clientjs-sdk/middleware';
168
225
 
169
- app.use(express.json());
170
226
  app.use(createUnsharedMiddleware(client, {
171
227
  userIdExtractor: (req) => req.user?.id,
228
+ corsOrigins: 'https://app.example.com',
172
229
  }));
230
+ app.use(express.json());
173
231
  ```
174
232
 
175
- **Options:**
176
-
177
- | Option | Type | Default | Description |
178
- |--------|------|---------|-------------|
179
- | `userIdExtractor` | `(req) => string \| undefined` | — | Pull user ID from your auth session |
180
- | `eventTypeExtractor` | `(req) => string \| undefined` | — | Override event type |
181
- | `sessionIdExtractor` | `(req) => string \| undefined` | — | Override session ID |
182
- | `ipAddressExtractor` | `(req) => string \| undefined` | — | Override IP address |
183
- | `defaultEventType` | `string` | `"browser_event"` | Fallback event type |
184
- | `routePrefix` | `string` | `"/unshared"` | Route mount prefix |
185
- | `corsOrigins` | `string \| string[]` | — | Allowed CORS origins; handles OPTIONS preflight automatically |
186
-
187
- > **Note:** This middleware uses a different default prefix (`/unshared`) and route (`submit-fingerprint-event`) than `unsharedBoundToUser` (`/__unshared`, `submit-fp`).
188
-
189
- ---
233
+ Its default route is `POST /unshared/submit-fingerprint-event`.
190
234
 
191
- ## Web / Edge Handler
235
+ ## Web Standard Handler
192
236
 
193
- For serverless and edge environments (Next.js App Router, Vercel Edge Functions, Cloudflare Workers), import from the `unshared-clientjs-sdk/web` entry point:
237
+ Use the edge/serverless entry point with Next.js, Cloudflare Workers, Vercel Edge, or other Web Standard runtimes:
194
238
 
195
239
  ```typescript
196
240
  import { createWebProtectionMiddleware } from 'unshared-clientjs-sdk/web';
197
241
  ```
198
242
 
199
- `createWebProtectionMiddleware` accepts the same `disableBotFilter` and `checkUserTimeoutMs` options as `unsharedBoundToUser`, with Web Standard `Request`/`Response` types instead of Express ones.
243
+ It mirrors the protection and verification behavior using `Request` and `Response`.
200
244
 
201
- ---
202
-
203
- ## Configuration
204
-
205
- ```typescript
206
- new UnsharedClient({
207
- apiKey: 'usk_…', // required
208
- baseUrl: 'https://api.unshared.ai', // optional
209
- timeout: 10_000, // optional, ms
210
- maxRetries: 3, // optional
211
- });
212
- ```
213
-
214
- ---
215
-
216
- ## Response shape
217
-
218
- All methods return `ApiResult<T>`:
245
+ ## Result and Error Handling
219
246
 
220
247
  ```typescript
221
- {
248
+ type ApiResult<T> = {
222
249
  success: boolean;
223
- data?: T;
224
- error?: { code: string; message: string; retryAfter?: number };
225
- status: number; // HTTP status code
226
- }
250
+ status: number; // 0 for a network failure
251
+ data?: T;
252
+ error?: {
253
+ code: string;
254
+ message: string;
255
+ retryAfter?: number;
256
+ };
257
+ failedOpen?: { status: number; reason?: 'no_device_id' };
258
+ };
227
259
  ```
228
260
 
229
- ---
230
-
231
- ## Security
232
-
233
- All PII is encrypted with **AES-256-GCM** before leaving your server. The encryption key is derived from your API key with SHA-256. Your API key is sent as the `X-API-Key` header never in a URL or browser context.
234
-
235
- ---
236
-
237
- ## See Also
238
-
239
- - [Quickstart](./docs/quickstart.md) — step-by-step Express setup from zero
240
- - [Flag semantics and testing](./docs/flag-semantics.md) — how flags work, E2E testing tips, timeout behavior
261
+ - Success requires a valid `{ success: true, data }` response envelope.
262
+ - Bodies larger than 1 MiB fail locally with `REQUEST_TOO_LARGE`.
263
+ - `Retry-After` is exposed as `error.retryAfter` on 429 responses.
264
+ - Protected same-origin submission routes never return 5xx to browser code.
265
+ - Keep the secret API key server-side and exclude logical identity/event bodies from request logs before SDK encryption.
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { FingerprintWireFormat } from '@unshared-labs/shared-types';
1
+ import type { CheckUserRequest, CheckUserResult as V3CheckUserResult, FingerprintWireFormat, IngestResult, JSONObject, VerifyEmailRequest, VerifyEmailResult } from './shared-types';
2
2
  export interface UnsharedClientConfig {
3
3
  /**
4
4
  * Secret API key. Must be kept server-side.
@@ -6,7 +6,7 @@ export interface UnsharedClientConfig {
6
6
  */
7
7
  apiKey: string;
8
8
  /**
9
- * Base URL of the Unshared Labs V2 ingress.
9
+ * Base URL of the Unshared Labs ingress.
10
10
  * @default "https://api.unshared.ai"
11
11
  */
12
12
  baseUrl?: string;
@@ -70,14 +70,15 @@ export interface ApiResult<T = unknown> {
70
70
  }
71
71
  export interface SubmitFingerprintOptions {
72
72
  userId?: string;
73
- /** SDK encrypts before sending. */
74
73
  emailAddress?: string;
75
74
  sessionHash?: string;
76
75
  eventType?: string;
77
- /** SDK encrypts before sending. */
78
76
  ipAddress?: string;
79
- /** SDK encrypts before sending. */
80
77
  userAgent?: string;
78
+ permanentDeviceId?: string;
79
+ deviceId?: string;
80
+ stableHash?: string;
81
+ fullHash?: string;
81
82
  /**
82
83
  * Client-supplied idempotency key, forwarded verbatim as X-Idempotency-Key.
83
84
  * NOTE: the middleware that calls this (submit-fp.ts / protection-handler.ts)
@@ -92,25 +93,21 @@ export interface SubmitFingerprintOptions {
92
93
  export interface SubmitFingerprintResult {
93
94
  hash: string;
94
95
  stable_hash: string;
95
- collected_at: string;
96
- /** Always present; set to "unknown" by the server if not provided. */
97
96
  version: string;
97
+ event_id: string;
98
+ collected_at: string;
99
+ endpoint_version: 'v3';
98
100
  }
99
101
  export interface ProcessUserEventParams {
100
102
  eventType: string;
101
- /** SDK encrypts before sending. */
102
103
  userId: string;
103
- /** SDK encrypts before sending. */
104
104
  ipAddress: string;
105
- /** SDK encrypts before sending. */
106
105
  deviceId: string;
107
106
  sessionHash: string;
108
- /** SDK encrypts before sending. */
109
107
  userAgent: string;
110
- /** SDK encrypts before sending. */
111
108
  emailAddress: string;
112
- /** SDK encrypts before sending. */
113
109
  fingerprintId?: string;
110
+ permanentDeviceId?: string;
114
111
  subscriptionStatus?: string | null;
115
112
  eventDetails?: Record<string, unknown> | null;
116
113
  }
@@ -121,6 +118,7 @@ export interface ProcessUserEventResult {
121
118
  email_address: string;
122
119
  ip_address: string;
123
120
  device_id: string;
121
+ permanent_device_id?: string;
124
122
  session_hash: string;
125
123
  user_agent: string;
126
124
  event_details?: string;
@@ -134,25 +132,31 @@ export interface ProcessUserEventResult {
134
132
  }
135
133
  export interface CheckUserResult {
136
134
  is_user_flagged: boolean;
135
+ /** Present on authoritative v3 responses; omitted by local fail-open defaults. */
136
+ decision_id?: string;
137
+ /** Present on authoritative v3 responses; omitted by local fail-open defaults. */
138
+ decision_code?: V3CheckUserResult['decision_code'];
137
139
  }
138
140
  export interface TriggerEmailVerificationResult {
139
141
  message: string;
142
+ verification_id?: string;
143
+ next_allowed_at?: string;
144
+ retry_after_seconds?: number;
140
145
  }
141
146
  export interface VerifyResult {
142
147
  verified: boolean;
143
- reason?: 'not_found' | 'code_mismatch' | 'code_expired';
148
+ reason?: 'not_found' | 'code_mismatch' | 'code_expired' | 'invalid_code';
149
+ verification_scope?: 'email';
144
150
  }
145
151
  export declare class UnsharedClient {
146
152
  private readonly _apiKey;
147
153
  private readonly _baseUrl;
148
154
  private readonly _timeout;
149
155
  private readonly _maxRetries;
150
- private _encryptionKey;
151
156
  private readonly _customFetch;
157
+ private _ingestionKey?;
158
+ private readonly _verificationIds;
152
159
  constructor(config: UnsharedClientConfig);
153
- /** Lazily derive + memoize the imported AES-GCM CryptoKey (async via WebCrypto). */
154
- private _getKey;
155
- private _encrypt;
156
160
  /**
157
161
  * Core HTTP method with retry logic.
158
162
  * - 2xx → success result
@@ -161,22 +165,33 @@ export declare class UnsharedClient {
161
165
  * - Network/timeout → retried, NETWORK_ERROR result returned
162
166
  */
163
167
  private _fetch;
168
+ private _postIngestion;
169
+ /** Submit an arbitrary v3 fingerprint payload without reshaping it. */
170
+ submitFingerprint(payload: JSONObject, opts?: {
171
+ idempotencyKey?: string;
172
+ }): Promise<ApiResult<IngestResult>>;
164
173
  /**
165
- * Submit a browser fingerprint event. Publishes asynchronously via Pub/Sub.
166
- * Maps to: POST /v2/submit-fingerprint-event
174
+ * Submit a browser fingerprint event using the source-compatible v2 method.
175
+ * The wire payload and endpoint use the v3 contract.
167
176
  */
168
177
  submitFingerprintEvent(fingerprint: FingerprintWireFormat, opts?: SubmitFingerprintOptions): Promise<ApiResult<SubmitFingerprintResult>>;
169
178
  /**
170
- * Record a user event and receive naughty-list analysis.
171
- * Maps to: POST /v2/process-user-event
179
+ * Record a user event and receive the current risk decision.
180
+ * Maps to: POST /v3/process-user-event followed by POST /v3/check-user.
172
181
  */
173
182
  processUserEvent(params: ProcessUserEventParams, overrides?: {
174
183
  timeoutMs?: number;
175
184
  maxRetries?: number;
176
185
  }): Promise<ApiResult<ProcessUserEventResult>>;
186
+ processUserEvent(payload: JSONObject, overrides?: {
187
+ timeoutMs?: number;
188
+ maxRetries?: number;
189
+ }): Promise<ApiResult<IngestResult>>;
177
190
  /**
178
191
  * Check if a user is flagged in the naughty list.
179
- * Maps to: GET /v2/check-user
192
+ * Maps to: POST /v3/check-user
193
+ * Requires a nonblank email_address; user_id cannot replace it. Invalid email
194
+ * returns a local VALIDATION_ERROR without making a request.
180
195
  *
181
196
  * **Defensive default:** On any failure (network error, 4xx, 5xx, timeout) this
182
197
  * method returns `{ success: true, data: { is_user_flagged: false } }` rather
@@ -190,19 +205,29 @@ export declare class UnsharedClient {
190
205
  checkUser(emailAddress: string, opts: {
191
206
  deviceId?: string;
192
207
  fingerprintId?: string;
208
+ permanentDeviceId?: string;
209
+ fullHash?: string;
210
+ sessionHash?: string;
211
+ timeoutMs?: number;
212
+ maxRetries?: number;
213
+ }): Promise<ApiResult<CheckUserResult>>;
214
+ checkUser(request: CheckUserRequest, opts?: {
193
215
  timeoutMs?: number;
194
216
  maxRetries?: number;
195
217
  }): Promise<ApiResult<CheckUserResult>>;
196
218
  /**
197
219
  * Send a 6-digit verification code to the user's email address.
198
- * Maps to: POST /v2/trigger-email-verification
220
+ * Maps to: POST /v3/trigger-email-verification
199
221
  */
200
222
  triggerEmailVerification(emailAddress: string, deviceId: string, opts?: {
201
223
  fingerprintId?: string;
224
+ permanentDeviceId?: string;
225
+ fullHash?: string;
226
+ sessionHash?: string;
202
227
  }): Promise<ApiResult<TriggerEmailVerificationResult>>;
203
228
  /**
204
229
  * Verify a 6-digit code submitted by the user.
205
- * Maps to: POST /v2/verify
230
+ * Maps to: POST /v3/verify
206
231
  *
207
232
  * `result.success` reliably indicates whether verification succeeded:
208
233
  * - `success: true` → code was correct, user is verified
@@ -219,7 +244,11 @@ export declare class UnsharedClient {
219
244
  */
220
245
  verify(emailAddress: string, deviceId: string, code: string, opts?: {
221
246
  fingerprintId?: string;
247
+ permanentDeviceId?: string;
248
+ verificationId?: string;
222
249
  }): Promise<ApiResult<VerifyResult>>;
250
+ /** Verify a v3 email challenge without relying on process-local state. */
251
+ verifyEmail(request: VerifyEmailRequest): Promise<ApiResult<VerifyEmailResult>>;
223
252
  /**
224
253
  * Fetch the published interstitial flow for this company (secret key, server-side).
225
254
  * Returns the projected flow definition to hand to the browser for rendering.