unshared-clientjs-sdk 2.0.0-rc.2 → 2.0.0-rc.21

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 (70) hide show
  1. package/README.md +100 -102
  2. package/dist/client.d.ts +57 -12
  3. package/dist/client.js +1 -1
  4. package/dist/esm/client.d.mts +57 -12
  5. package/dist/esm/client.mjs +1 -1
  6. package/dist/esm/index.d.mts +5 -1
  7. package/dist/esm/index.mjs +1 -1
  8. package/dist/esm/middleware/index.d.mts +50 -0
  9. package/dist/esm/middleware/index.mjs +1 -0
  10. package/dist/esm/middleware/injection/fingerprint-script.d.mts +16 -0
  11. package/dist/esm/middleware/injection/fingerprint-script.mjs +1 -0
  12. package/dist/esm/middleware/rate-limit-backoff.d.mts +14 -0
  13. package/dist/esm/middleware/rate-limit-backoff.mjs +1 -0
  14. package/dist/esm/middleware/response-interceptor.d.mts +15 -0
  15. package/dist/esm/middleware/response-interceptor.mjs +1 -0
  16. package/dist/esm/middleware/routes/submit-fp.d.mts +24 -0
  17. package/dist/esm/middleware/routes/submit-fp.mjs +1 -0
  18. package/dist/esm/middleware/routes/verify.d.mts +28 -0
  19. package/dist/esm/middleware/routes/verify.mjs +1 -0
  20. package/dist/esm/middleware/utils/client-ip.d.mts +6 -0
  21. package/dist/esm/middleware/utils/client-ip.mjs +1 -0
  22. package/dist/esm/middleware/utils/content-type.d.mts +6 -0
  23. package/dist/esm/middleware/utils/content-type.mjs +1 -0
  24. package/dist/esm/middleware/utils/cookies.d.mts +6 -0
  25. package/dist/esm/middleware/utils/cookies.mjs +1 -0
  26. package/dist/esm/middleware/utils/device-id.d.mts +5 -0
  27. package/dist/esm/middleware/utils/device-id.mjs +1 -0
  28. package/dist/esm/middleware/utils/is-bot.d.mts +5 -0
  29. package/dist/esm/middleware/utils/is-bot.mjs +1 -0
  30. package/dist/esm/middleware/utils/secure.d.mts +3 -0
  31. package/dist/esm/middleware/utils/secure.mjs +1 -0
  32. package/dist/esm/middleware/utils/skip-paths.d.mts +5 -0
  33. package/dist/esm/middleware/utils/skip-paths.mjs +1 -0
  34. package/dist/esm/middleware/verdict-cache.d.mts +47 -0
  35. package/dist/esm/middleware/verdict-cache.mjs +1 -0
  36. package/dist/esm/middleware.d.mts +30 -5
  37. package/dist/esm/middleware.mjs +1 -1
  38. package/dist/index.d.ts +5 -1
  39. package/dist/index.js +1 -1
  40. package/dist/middleware/index.d.ts +50 -0
  41. package/dist/middleware/index.js +1 -0
  42. package/dist/middleware/injection/fingerprint-script.d.ts +16 -0
  43. package/dist/middleware/injection/fingerprint-script.js +1 -0
  44. package/dist/middleware/rate-limit-backoff.d.ts +14 -0
  45. package/dist/middleware/rate-limit-backoff.js +1 -0
  46. package/dist/middleware/response-interceptor.d.ts +15 -0
  47. package/dist/middleware/response-interceptor.js +1 -0
  48. package/dist/middleware/routes/submit-fp.d.ts +24 -0
  49. package/dist/middleware/routes/submit-fp.js +1 -0
  50. package/dist/middleware/routes/verify.d.ts +28 -0
  51. package/dist/middleware/routes/verify.js +1 -0
  52. package/dist/middleware/utils/client-ip.d.ts +6 -0
  53. package/dist/middleware/utils/client-ip.js +1 -0
  54. package/dist/middleware/utils/content-type.d.ts +6 -0
  55. package/dist/middleware/utils/content-type.js +1 -0
  56. package/dist/middleware/utils/cookies.d.ts +6 -0
  57. package/dist/middleware/utils/cookies.js +1 -0
  58. package/dist/middleware/utils/device-id.d.ts +5 -0
  59. package/dist/middleware/utils/device-id.js +1 -0
  60. package/dist/middleware/utils/is-bot.d.ts +5 -0
  61. package/dist/middleware/utils/is-bot.js +1 -0
  62. package/dist/middleware/utils/secure.d.ts +3 -0
  63. package/dist/middleware/utils/secure.js +1 -0
  64. package/dist/middleware/utils/skip-paths.d.ts +5 -0
  65. package/dist/middleware/utils/skip-paths.js +1 -0
  66. package/dist/middleware/verdict-cache.d.ts +47 -0
  67. package/dist/middleware/verdict-cache.js +1 -0
  68. package/dist/middleware.d.ts +30 -5
  69. package/dist/middleware.js +1 -1
  70. package/package.json +14 -1
package/README.md CHANGED
@@ -1,47 +1,26 @@
1
- # @unshared-labs/sdk
1
+ # unshared-clientjs-sdk
2
2
 
3
- Server-side Node.js SDK for the Unshared Labs V2 API. Handles authentication, AES-256-GCM field encryption, retry logic, and structured error responses.
4
-
5
- **Keep the API key server-side — never expose it in browser code.**
3
+ Server-side Node.js SDK for [Unshared Labs](https://unsharedlabs.com) detect account sharing, analyze user events for fraud, and run email verification flows.
6
4
 
7
5
  ---
8
6
 
9
- ## Installation
7
+ ## Install
10
8
 
11
9
  ```bash
12
- npm install @unshared-labs/sdk
10
+ npm install unshared-clientjs-sdk
13
11
  ```
14
12
 
15
- Requires Node.js 18.
13
+ **Requires Node.js 18+**
16
14
 
17
15
  ---
18
16
 
19
17
  ## Quick Start
20
18
 
21
19
  ```typescript
22
- import { UnsharedLabsClient } from '@unshared-labs/sdk';
20
+ import { UnsharedLabsClient } from 'unshared-clientjs-sdk';
23
21
 
24
22
  const client = new UnsharedLabsClient({
25
- apiKey: process.env.UNSHARED_API_KEY!,
26
- });
27
- ```
28
-
29
- ### CommonJS
30
-
31
- ```javascript
32
- const { UnsharedLabsClient } = require('@unshared-labs/sdk');
33
- ```
34
-
35
- ---
36
-
37
- ## Configuration
38
-
39
- ```typescript
40
- const client = new UnsharedLabsClient({
41
- apiKey: 'sk_live_…', // required — keep server-side
42
- baseUrl: 'https://…', // optional, default: https://api-ingress.unsharedlabs.com
43
- timeout: 10_000, // optional, ms — default: 10 s
44
- maxRetries: 3, // optional — retries on 5xx / network errors
23
+ apiKey: process.env.UNSHARED_API_KEY, // usk_…
45
24
  });
46
25
  ```
47
26
 
@@ -49,131 +28,150 @@ const client = new UnsharedLabsClient({
49
28
 
50
29
  ## Methods
51
30
 
52
- ### `submitFingerprintEvent(fingerprint, opts?)`
31
+ ### `processUserEvent(params)`
53
32
 
54
- Submit a browser fingerprint collected by `@unshared-labs/frontend-fingerprint`. Returns 202 Accepted; the event is stored asynchronously.
33
+ Record a user event and get a fraud signal back. Call this on login, signup, or any high-value action.
55
34
 
56
35
  ```typescript
57
- const result = await client.submitFingerprintEvent(fingerprintData, {
58
- userId: 'u_123',
59
- sessionHash: 'sess_abc',
60
- eventType: 'login',
36
+ 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'],
61
44
  });
62
- // result.data: { hash, stable_hash, collected_at, version }
45
+
46
+ if (result.success && result.data?.analysis.is_user_flagged) {
47
+ // Block or challenge the user
48
+ }
63
49
  ```
64
50
 
65
- An `X-Idempotency-Key` is sent automatically. Retries with the same key are deduplicated at the backend.
51
+ **Fields encrypted before sending:** `emailAddress`, `deviceId`
66
52
 
67
- ### `processUserEvent(params)`
53
+ ---
54
+
55
+ ### `checkUser(emailAddress, deviceId)`
68
56
 
69
- Record a user event and receive naughty-list analysis. `emailAddress` and `deviceId` are AES-256-GCM encrypted before sending.
57
+ Quick check to see if a user is flagged. Useful in middleware or route guards.
70
58
 
71
59
  ```typescript
72
- const result = await client.processUserEvent({
73
- eventType: 'login',
74
- userId: 'u_123',
75
- ipAddress: req.ip,
76
- deviceId: 'device-uuid',
77
- sessionHash: 'sess_abc',
78
- userAgent: req.headers['user-agent'] ?? '',
79
- emailAddress: 'user@example.com',
80
- subscriptionStatus: 'paid',
81
- });
82
- // result.data: { event: { … }, analysis: { status, is_user_flagged } }
60
+ const result = await client.checkUser('user@example.com', 'device_abc');
61
+
62
+ if (result.data?.is_user_flagged) {
63
+ // Deny access
64
+ }
83
65
  ```
84
66
 
85
- > **Note:** This endpoint has no server-side idempotency. Retries may insert duplicate rows.
67
+ > **Safe default:** Returns `{ is_user_flagged: false }` on any failure (network error, outage). A backend outage will never accidentally block a legitimate user.
86
68
 
87
- ### `checkUser(emailAddress, deviceId)`
69
+ ---
88
70
 
89
- Check if a user is flagged in the naughty list. Always returns `{ is_user_flagged: false }` on any failure — a backend outage never blocks a legitimate user.
71
+ ### `triggerEmailVerification(emailAddress, deviceId)`
72
+
73
+ Send a 6-digit verification code to the user's email.
90
74
 
91
75
  ```typescript
92
- const result = await client.checkUser('user@example.com', 'device-uuid');
93
- if (result.data?.is_user_flagged) { /* … */ }
76
+ await client.triggerEmailVerification('user@example.com', 'device_abc');
94
77
  ```
95
78
 
96
- ### `triggerEmailVerification(emailAddress, deviceId)`
79
+ ---
97
80
 
98
- Send a 6-digit verification code. Rate-limited to one request per `(email_address, device_id)` pair per 2 minutes.
81
+ ### `verify(emailAddress, deviceId, code)`
82
+
83
+ Validate the code the user submitted.
99
84
 
100
85
  ```typescript
101
- const result = await client.triggerEmailVerification('user@example.com', 'device-uuid');
102
- if (!result.success && result.error?.code === 'RATE_LIMIT_EXCEEDED') {
103
- const waitSeconds = result.error.retryAfter ?? result.error.details?.retry_after_seconds;
104
- // back off for waitSeconds
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
105
96
  }
106
97
  ```
107
98
 
108
- **Pre-condition:** sender settings must be configured via `createSender` + `validateSender` before this will succeed.
99
+ ---
109
100
 
110
- ### `verify(emailAddress, deviceId, code)`
101
+ ### `submitFingerprintEvent(fingerprint, opts?)`
111
102
 
112
- Verify the 6-digit code entered by the user. `success: true` with `verified: false` means the code was wrong not a transport error.
103
+ Submit a browser fingerprint collected by `unshared-frontend-sdk`. Typically called by the middleware you usually won't call this directly.
113
104
 
114
105
  ```typescript
115
- const result = await client.verify('user@example.com', 'device-uuid', '123456');
116
- if (result.data?.verified) { /* proceed */ }
117
- else { /* result.data.reason: 'not_found' | 'code_mismatch' | 'code_expired' */ }
106
+ await client.submitFingerprintEvent(fingerprint, {
107
+ userId: 'user_123',
108
+ sessionHash: 'session_xyz',
109
+ eventType: 'page_view',
110
+ });
118
111
  ```
119
112
 
120
113
  ---
121
114
 
122
- ## Express Middleware (Browser Proxy)
115
+ ## Express Middleware
123
116
 
124
- Mount this to handle fingerprint events forwarded from `@unshared-labs/frontend-fingerprint`:
117
+ The middleware adds a proxy route (`POST /unshared/submit-fingerprint-event`) that the browser SDK calls. It handles forwarding fingerprints to Unshared Labs and attaching your API key.
125
118
 
126
119
  ```typescript
127
- import { createUnsharedMiddleware } from '@unshared-labs/sdk/middleware';
120
+ import { createUnsharedMiddleware } from 'unshared-clientjs-sdk/middleware';
121
+
122
+ // express.json() must come before this middleware
123
+ app.use(express.json());
128
124
 
129
125
  app.use(createUnsharedMiddleware(client, {
130
- userIdExtractor: (req) => req.user?.id,
131
- eventTypeExtractor: (req) => req.body.event_type,
132
- routePrefix: '/unshared', // default
126
+ userIdExtractor: (req) => req.user?.id, // attach logged-in user
133
127
  }));
134
128
  ```
135
129
 
136
- The middleware:
137
- - Handles `POST /unshared/submit-fingerprint-event`
138
- - Passes `next()` for all other routes
139
- - Never returns 5xx to the browser — upstream errors become `HTTP 200 { success: false }`
130
+ **Prerequisites:**
131
+ - Mount `express.json()` before the middleware
132
+ - `user_id` is automatically removed from `req.body` after being read — downstream logging won't capture it
140
133
 
141
- ---
134
+ **Options:**
142
135
 
143
- ## Error Handling
136
+ | Option | Type | Default | Description |
137
+ |--------|------|---------|-------------|
138
+ | `userIdExtractor` | `(req) => string \| undefined` | — | Pull user ID from your auth session |
139
+ | `eventTypeExtractor` | `(req) => string \| undefined` | — | Override event type |
140
+ | `sessionIdExtractor` | `(req) => string \| undefined` | — | Override session ID |
141
+ | `defaultEventType` | `string` | `"browser_event"` | Fallback event type |
142
+ | `routePrefix` | `string` | `"/unshared"` | Route mount prefix |
143
+ | `corsOrigins` | `string \| string[]` | — | Allowed CORS origins; handles OPTIONS preflight automatically |
144
144
 
145
- All methods return `ApiResult<T>`:
145
+ ---
146
+
147
+ ## Configuration
146
148
 
147
149
  ```typescript
148
- interface ApiResult<T> {
149
- success: boolean;
150
- data?: T;
151
- error?: {
152
- code: string;
153
- message: string;
154
- details?: Record<string, unknown>;
155
- retryAfter?: number; // seconds — present on 429 RATE_LIMIT_EXCEEDED
156
- };
157
- status: number; // HTTP status, 0 for network errors
158
- }
150
+ new UnsharedLabsClient({
151
+ apiKey: 'usk_…', // required
152
+ baseUrl: 'https://api-ingress.unsharedlabs.com', // optional
153
+ timeout: 10_000, // optional, ms
154
+ maxRetries: 3, // optional
155
+ });
159
156
  ```
160
157
 
161
- - **4xx** — returned immediately, not retried
162
- - **5xx / network** — retried up to `maxRetries` with exponential backoff (base 1 s, ±25% jitter)
163
- - `triggerEmailVerification` and `verify` map 5xx / network errors to `{ code: 'DELIVERY_FAILED' }`
164
-
165
- See [Appendix A of the spec](../V2_CLIENT_SDK_SPEC.md) for the full error code list.
166
-
167
158
  ---
168
159
 
169
- ## Encryption
160
+ ## Response shape
161
+
162
+ All methods return `ApiResult<T>`:
170
163
 
171
- `emailAddress`, `deviceId`, and `code` are encrypted client-side before transmission using AES-256-GCM with a key derived from `SHA256(apiKey)`. Wire format: `<base64(iv)>:<base64(authTag)>:<base64(ciphertext)>` (colon-separated, standard Base64). The backend decrypts with `DecryptSDKField` in `lib/crypto.go`.
164
+ ```typescript
165
+ {
166
+ success: boolean;
167
+ data?: T;
168
+ error?: { code: string; message: string; retryAfter?: number };
169
+ status: number; // HTTP status code
170
+ }
171
+ ```
172
172
 
173
173
  ---
174
174
 
175
- ## Environment Variables
175
+ ## Security
176
176
 
177
- ```bash
178
- UNSHARED_API_KEY=sk_live_…
179
- ```
177
+ 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.
package/dist/client.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { FingerprintWireFormat } from '@unshared-labs/shared-types';
2
2
  export interface UnsharedLabsClientConfig {
3
3
  /**
4
4
  * Secret API key. Must be kept server-side.
5
- * Format: sk_live_ or sk_test_…
5
+ * Format: usk_
6
6
  */
7
7
  apiKey: string;
8
8
  /**
@@ -40,8 +40,14 @@ export interface ApiResult<T = unknown> {
40
40
  }
41
41
  export interface SubmitFingerprintOptions {
42
42
  userId?: string;
43
+ /** SDK encrypts before sending. */
44
+ emailAddress?: string;
43
45
  sessionHash?: string;
44
46
  eventType?: string;
47
+ /** SDK encrypts before sending. */
48
+ ipAddress?: string;
49
+ /** SDK encrypts before sending. */
50
+ userAgent?: string;
45
51
  }
46
52
  export interface SubmitFingerprintResult {
47
53
  hash: string;
@@ -52,15 +58,19 @@ export interface SubmitFingerprintResult {
52
58
  }
53
59
  export interface ProcessUserEventParams {
54
60
  eventType: string;
61
+ /** SDK encrypts before sending. */
55
62
  userId: string;
56
- /** Plaintext — SDK does not encrypt this field. */
63
+ /** SDK encrypts before sending. */
57
64
  ipAddress: string;
58
65
  /** SDK encrypts before sending. */
59
66
  deviceId: string;
60
67
  sessionHash: string;
68
+ /** SDK encrypts before sending. */
61
69
  userAgent: string;
62
70
  /** SDK encrypts before sending. */
63
71
  emailAddress: string;
72
+ /** SDK encrypts before sending. */
73
+ fingerprintId?: string;
64
74
  subscriptionStatus?: string | null;
65
75
  eventDetails?: Record<string, unknown> | null;
66
76
  }
@@ -92,6 +102,22 @@ export interface VerifyResult {
92
102
  verified: boolean;
93
103
  reason?: 'not_found' | 'code_mismatch' | 'code_expired';
94
104
  }
105
+ export interface VerificationFlowStep {
106
+ type: 'message' | 'email_input' | 'otp_input' | 'support_link';
107
+ title: string;
108
+ body: string;
109
+ buttonText?: string;
110
+ url?: string;
111
+ }
112
+ export interface VerificationFlowConfigResult {
113
+ steps: VerificationFlowStep[];
114
+ branding?: {
115
+ companyName?: string;
116
+ logoUrl?: string;
117
+ primaryColor?: string;
118
+ supportEmail?: string;
119
+ };
120
+ }
95
121
  export declare class UnsharedLabsClient {
96
122
  private readonly _apiKey;
97
123
  private readonly _baseUrl;
@@ -131,28 +157,47 @@ export declare class UnsharedLabsClient {
131
157
  * through your infrastructure metrics, not through this method's return value.
132
158
  */
133
159
  checkUser(emailAddress: string, deviceId: string): Promise<ApiResult<CheckUserResult>>;
160
+ checkUser(emailAddress: string, opts: {
161
+ deviceId?: string;
162
+ fingerprintId?: string;
163
+ }): Promise<ApiResult<CheckUserResult>>;
134
164
  /**
135
165
  * Send a 6-digit verification code to the user's email address.
136
166
  * Maps to: POST /v2/trigger-email-verification
137
167
  */
138
- triggerEmailVerification(emailAddress: string, deviceId: string): Promise<ApiResult<TriggerEmailVerificationResult>>;
168
+ triggerEmailVerification(emailAddress: string, deviceId: string, opts?: {
169
+ fingerprintId?: string;
170
+ }): Promise<ApiResult<TriggerEmailVerificationResult>>;
139
171
  /**
140
172
  * Verify a 6-digit code submitted by the user.
141
173
  * Maps to: POST /v2/verify
142
174
  *
143
- * **Important:** `result.success` is `true` even when `result.data.verified`
144
- * is `false`. A `false` verified result means the code was wrong or expired
145
- * not a transport failure. Always check `result.data.verified` explicitly:
175
+ * `result.success` reliably indicates whether verification succeeded:
176
+ * - `success: true` code was correct, user is verified
177
+ * - `success: false, error.code: "VERIFICATION_FAILED"` wrong or expired code
178
+ * - `success: false, error.code: "DELIVERY_FAILED"` → network/server error
146
179
  *
147
180
  * ```typescript
148
181
  * const result = await client.verify(email, deviceId, code);
149
- * if (!result.success) { /* transport/infra error *\/ }
150
- * else if (!result.data?.verified) { /* wrong or expired code *\/ }
151
- * else { /* verified *\/ }
182
+ * if (!result.success) {
183
+ * if (result.error?.code === 'VERIFICATION_FAILED') { /* bad code *\/ }
184
+ * else { /* transport error *\/ }
185
+ * } else { /* verified *\/ }
152
186
  * ```
187
+ */
188
+ verify(emailAddress: string, deviceId: string, code: string, opts?: {
189
+ fingerprintId?: string;
190
+ }): Promise<ApiResult<VerifyResult>>;
191
+ /**
192
+ * Fetch the verification flow configuration for this company.
193
+ * Maps to: GET /v2/verification-flow-config
194
+ *
195
+ * Returns the flow steps and branding configured by the Unshared Labs
196
+ * team for this company. The middleware uses this to render the
197
+ * verification overlay.
153
198
  *
154
- * On network or 5xx errors, returns `{ success: false, error: { code: "DELIVERY_FAILED" } }`.
155
- * On 4xx (e.g. rate limit), the server error is returned as-is.
199
+ * Returns `null` on any failure (network error, 4xx, 5xx) so the
200
+ * middleware can fall back to the default flow.
156
201
  */
157
- verify(emailAddress: string, deviceId: string, code: string): Promise<ApiResult<VerifyResult>>;
202
+ getVerificationFlowConfig(): Promise<VerificationFlowConfigResult | null>;
158
203
  }
package/dist/client.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.UnsharedLabsClient=void 0;const crypto_1=require("crypto"),util_1=require("./util"),DEFAULT_BASE_URL="https://api-ingress.unsharedlabs.com",DEFAULT_TIMEOUT_MS=1e4,DEFAULT_MAX_RETRIES=3,MAX_DELAY_MS=3e4,BASE_DELAY_MS=1e3;function sleep(e){return new Promise(s=>setTimeout(s,e))}function retryDelay(e){const s=Math.min(1e3*Math.pow(2,e-1),3e4),t=s*(.5*Math.random()-.25);return Math.max(0,s+t)}async function parseErrorBody(e){const s=await e.text().catch(()=>"");try{const t=JSON.parse(s);return t?.error?.code?{code:t.error.code,message:t.error.message??"Unknown error",details:t.error.details}:{code:"UNKNOWN_ERROR",message:s||e.statusText}}catch{return{code:"UNKNOWN_ERROR",message:s||e.statusText}}}class UnsharedLabsClient{constructor(e){if(!e.apiKey||""===e.apiKey.trim())throw new Error("apiKey is required");this.i=e.apiKey,this.o=e.baseUrl?e.baseUrl.replace(/\/$/,""):DEFAULT_BASE_URL,this.h=e.timeout??1e4,this.u=e.maxRetries??3,this.l=(0,crypto_1.createHash)("sha256").update(e.apiKey).digest()}_(e){return(0,util_1.encryptData)(e,this.l)}async p(e,s){const t=this.u+1;let r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let i=1;i<=t;i++){i>1&&await sleep(retryDelay(i-1));const t=new AbortController,a=setTimeout(()=>t.abort(),this.h);try{const i=await fetch(e,{method:s.method,headers:{"X-API-Key":this.i,...s.headers},body:s.body,signal:t.signal});if(clearTimeout(a),i.ok){const e=await i.text().catch(()=>"{}");let s;try{s=JSON.parse(e)}catch{s={}}const t="data"in s?s.data:s;return{success:!0,status:i.status,data:t}}const n=await parseErrorBody(i);if(i.status>=400&&i.status<500){if(429===i.status){const e=i.headers.get("Retry-After");if(null!=e){const s=parseInt(e,10);isNaN(s)||(n.retryAfter=s)}}return{success:!1,status:i.status,error:n}}r={success:!1,status:i.status,error:n}}catch(e){clearTimeout(a),r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return r}async submitFingerprintEvent(e,s){const t={hash:e.full_hash,stable_hash:e.fingerprint_id,collected_at:e.timestamp,is_incognito:e.isIncognito,components:e.components,version:e.version};return null!=s?.userId&&(t.user_id=this._(s.userId)),null!=s?.sessionHash&&(t.session_hash=s.sessionHash),null!=s?.eventType&&(t.event_type=s.eventType),this.p(`${this.o}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":(0,crypto_1.randomUUID)()},body:JSON.stringify(t)})}async processUserEvent(e){const s={event_type:e.eventType,user_id:e.userId,ip_address:e.ipAddress,device_id:this._(e.deviceId),session_hash:e.sessionHash,user_agent:e.userAgent,email_address:this._(e.emailAddress)};return null!=e.subscriptionStatus&&(s.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(s.event_details=e.eventDetails),this.p(`${this.o}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})}async checkUser(e,s){const t=new URLSearchParams({email_address:this._(e),device_id:this._(s)}),r=await this.p(`${this.o}/v2/check-user?${t}`,{method:"GET"});return r.success?r:{success:!0,status:200,data:{is_user_flagged:!1}}}async triggerEmailVerification(e,s){const t=await this.p(`${this.o}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email_address:this._(e),device_id:this._(s)})});return!t.success&&(0===t.status||t.status>=500)?{success:!1,status:t.status,error:{code:"DELIVERY_FAILED",message:t.error?.message??"Delivery failed"}}:t}async verify(e,s,t){const r=await this.p(`${this.o}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email_address:this._(e),device_id:this._(s),code:this._(t)})});return!r.success&&(0===r.status||r.status>=500)?{success:!1,status:r.status,error:{code:"DELIVERY_FAILED",message:r.error?.message??"Delivery failed"}}:r}}exports.UnsharedLabsClient=UnsharedLabsClient;
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.UnsharedLabsClient=void 0;const crypto_1=require("crypto"),util_1=require("./util"),DEFAULT_BASE_URL="https://api-ingress.unsharedlabs.com",DEFAULT_TIMEOUT_MS=1e4,DEFAULT_MAX_RETRIES=3,MAX_DELAY_MS=3e4,BASE_DELAY_MS=1e3;function sleep(e){return new Promise(s=>setTimeout(s,e))}function retryDelay(e){const s=Math.min(1e3*Math.pow(2,e-1),3e4),t=s*(.5*Math.random()-.25);return Math.max(0,s+t)}async function parseErrorBody(e){const s=await e.text().catch(()=>"");try{const t=JSON.parse(s);return t?.error?.code?{code:t.error.code,message:t.error.message??"Unknown error",details:t.error.details}:{code:"UNKNOWN_ERROR",message:s||e.statusText}}catch{return{code:"UNKNOWN_ERROR",message:s||e.statusText}}}class UnsharedLabsClient{constructor(e){if(!e.apiKey||""===e.apiKey.trim())throw new Error("apiKey is required");this.i=e.apiKey,this.o=e.baseUrl?e.baseUrl.replace(/\/$/,""):DEFAULT_BASE_URL,this.h=e.timeout??1e4,this.u=e.maxRetries??3,this.l=(0,crypto_1.createHash)("sha256").update(e.apiKey).digest()}_(e){return(0,util_1.encryptData)(e,this.l)}async p(e,s){const t=this.u+1;let r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let i=1;i<=t;i++){i>1&&await sleep(retryDelay(i-1));const t=new AbortController,n=setTimeout(()=>t.abort(),this.h);try{const i=await fetch(e,{method:s.method,headers:{"X-API-Key":this.i,...s.headers},body:s.body,signal:t.signal});if(clearTimeout(n),i.ok){const e=await i.text().catch(()=>"{}");let s;try{s=JSON.parse(e)}catch{s={}}const t="data"in s?s.data:s;return{success:!0,status:i.status,data:t}}const a=await parseErrorBody(i);if(i.status>=400&&i.status<500){if(429===i.status){const e=i.headers.get("Retry-After");if(null!=e){const s=parseInt(e,10);isNaN(s)||(a.retryAfter=s)}}return{success:!1,status:i.status,error:a}}r={success:!1,status:i.status,error:a}}catch(e){clearTimeout(n),r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return r}async submitFingerprintEvent(e,s){const t={hash:e.full_hash,stable_hash:e.fingerprint_id,collected_at:e.timestamp,is_incognito:e.isIncognito,components:e.components,version:e.version};return null!=s?.userId&&(t.user_id=this._(s.userId)),null!=s?.emailAddress&&(t.email_address=this._(s.emailAddress)),null!=s?.sessionHash&&(t.session_hash=s.sessionHash),null!=s?.eventType&&(t.event_type=s.eventType),null!=s?.ipAddress&&(t.ip_address=this._(s.ipAddress)),null!=s?.userAgent&&(t.user_agent=this._(s.userAgent)),this.p(`${this.o}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":(0,crypto_1.randomUUID)()},body:JSON.stringify(t)})}async processUserEvent(e){const s={event_type:e.eventType,user_id:this._(e.userId),ip_address:this._(e.ipAddress),device_id:this._(e.deviceId),session_hash:e.sessionHash,user_agent:this._(e.userAgent),email_address:this._(e.emailAddress)};return null!=e.fingerprintId&&(s.fingerprint_id=this._(e.fingerprintId)),null!=e.subscriptionStatus&&(s.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(s.event_details=e.eventDetails),this.p(`${this.o}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})}async checkUser(e,s){const t="string"==typeof s?{deviceId:s}:s;if(!t.deviceId&&!t.fingerprintId)return{success:!0,status:200,data:{is_user_flagged:!1}};const r=new URLSearchParams;r.set("email_address",this._(e)),t.deviceId&&r.set("device_id",this._(t.deviceId)),t.fingerprintId&&r.set("fingerprint_id",this._(t.fingerprintId));const i=await this.p(`${this.o}/v2/check-user?${r}`,{method:"GET"});return i.success?i:{success:!0,status:200,data:{is_user_flagged:!1}}}async triggerEmailVerification(e,s,t){const r={email_address:this._(e),device_id:this._(s)};t?.fingerprintId&&(r.fingerprint_id=this._(t.fingerprintId));const i=await this.p(`${this.o}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});return!i.success&&(0===i.status||i.status>=500)?{success:!1,status:i.status,error:{code:"DELIVERY_FAILED",message:i.error?.message??"Delivery failed"}}:i}async verify(e,s,t,r){const i={email_address:this._(e),device_id:this._(s),code:this._(t)};r?.fingerprintId&&(i.fingerprint_id=this._(r.fingerprintId));const n=await this.p(`${this.o}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});return!n.success&&(0===n.status||n.status>=500)?{success:!1,status:n.status,error:{code:"DELIVERY_FAILED",message:n.error?.message??"Delivery failed"}}:n.success&&!1===n.data?.verified?{success:!1,status:n.status,error:{code:"VERIFICATION_FAILED",message:"Code is incorrect or expired",details:n.data.reason?{reason:n.data.reason}:void 0}}:n}async getVerificationFlowConfig(){const e=await this.p(`${this.o}/v2/verification-flow-config`,{method:"GET"});return e.success&&e.data?e.data:null}}exports.UnsharedLabsClient=UnsharedLabsClient;
@@ -2,7 +2,7 @@ import type { FingerprintWireFormat } from '@unshared-labs/shared-types';
2
2
  export interface UnsharedLabsClientConfig {
3
3
  /**
4
4
  * Secret API key. Must be kept server-side.
5
- * Format: sk_live_ or sk_test_…
5
+ * Format: usk_
6
6
  */
7
7
  apiKey: string;
8
8
  /**
@@ -40,8 +40,14 @@ export interface ApiResult<T = unknown> {
40
40
  }
41
41
  export interface SubmitFingerprintOptions {
42
42
  userId?: string;
43
+ /** SDK encrypts before sending. */
44
+ emailAddress?: string;
43
45
  sessionHash?: string;
44
46
  eventType?: string;
47
+ /** SDK encrypts before sending. */
48
+ ipAddress?: string;
49
+ /** SDK encrypts before sending. */
50
+ userAgent?: string;
45
51
  }
46
52
  export interface SubmitFingerprintResult {
47
53
  hash: string;
@@ -52,15 +58,19 @@ export interface SubmitFingerprintResult {
52
58
  }
53
59
  export interface ProcessUserEventParams {
54
60
  eventType: string;
61
+ /** SDK encrypts before sending. */
55
62
  userId: string;
56
- /** Plaintext — SDK does not encrypt this field. */
63
+ /** SDK encrypts before sending. */
57
64
  ipAddress: string;
58
65
  /** SDK encrypts before sending. */
59
66
  deviceId: string;
60
67
  sessionHash: string;
68
+ /** SDK encrypts before sending. */
61
69
  userAgent: string;
62
70
  /** SDK encrypts before sending. */
63
71
  emailAddress: string;
72
+ /** SDK encrypts before sending. */
73
+ fingerprintId?: string;
64
74
  subscriptionStatus?: string | null;
65
75
  eventDetails?: Record<string, unknown> | null;
66
76
  }
@@ -92,6 +102,22 @@ export interface VerifyResult {
92
102
  verified: boolean;
93
103
  reason?: 'not_found' | 'code_mismatch' | 'code_expired';
94
104
  }
105
+ export interface VerificationFlowStep {
106
+ type: 'message' | 'email_input' | 'otp_input' | 'support_link';
107
+ title: string;
108
+ body: string;
109
+ buttonText?: string;
110
+ url?: string;
111
+ }
112
+ export interface VerificationFlowConfigResult {
113
+ steps: VerificationFlowStep[];
114
+ branding?: {
115
+ companyName?: string;
116
+ logoUrl?: string;
117
+ primaryColor?: string;
118
+ supportEmail?: string;
119
+ };
120
+ }
95
121
  export declare class UnsharedLabsClient {
96
122
  private readonly _apiKey;
97
123
  private readonly _baseUrl;
@@ -131,28 +157,47 @@ export declare class UnsharedLabsClient {
131
157
  * through your infrastructure metrics, not through this method's return value.
132
158
  */
133
159
  checkUser(emailAddress: string, deviceId: string): Promise<ApiResult<CheckUserResult>>;
160
+ checkUser(emailAddress: string, opts: {
161
+ deviceId?: string;
162
+ fingerprintId?: string;
163
+ }): Promise<ApiResult<CheckUserResult>>;
134
164
  /**
135
165
  * Send a 6-digit verification code to the user's email address.
136
166
  * Maps to: POST /v2/trigger-email-verification
137
167
  */
138
- triggerEmailVerification(emailAddress: string, deviceId: string): Promise<ApiResult<TriggerEmailVerificationResult>>;
168
+ triggerEmailVerification(emailAddress: string, deviceId: string, opts?: {
169
+ fingerprintId?: string;
170
+ }): Promise<ApiResult<TriggerEmailVerificationResult>>;
139
171
  /**
140
172
  * Verify a 6-digit code submitted by the user.
141
173
  * Maps to: POST /v2/verify
142
174
  *
143
- * **Important:** `result.success` is `true` even when `result.data.verified`
144
- * is `false`. A `false` verified result means the code was wrong or expired
145
- * not a transport failure. Always check `result.data.verified` explicitly:
175
+ * `result.success` reliably indicates whether verification succeeded:
176
+ * - `success: true` code was correct, user is verified
177
+ * - `success: false, error.code: "VERIFICATION_FAILED"` wrong or expired code
178
+ * - `success: false, error.code: "DELIVERY_FAILED"` → network/server error
146
179
  *
147
180
  * ```typescript
148
181
  * const result = await client.verify(email, deviceId, code);
149
- * if (!result.success) { /* transport/infra error *\/ }
150
- * else if (!result.data?.verified) { /* wrong or expired code *\/ }
151
- * else { /* verified *\/ }
182
+ * if (!result.success) {
183
+ * if (result.error?.code === 'VERIFICATION_FAILED') { /* bad code *\/ }
184
+ * else { /* transport error *\/ }
185
+ * } else { /* verified *\/ }
152
186
  * ```
187
+ */
188
+ verify(emailAddress: string, deviceId: string, code: string, opts?: {
189
+ fingerprintId?: string;
190
+ }): Promise<ApiResult<VerifyResult>>;
191
+ /**
192
+ * Fetch the verification flow configuration for this company.
193
+ * Maps to: GET /v2/verification-flow-config
194
+ *
195
+ * Returns the flow steps and branding configured by the Unshared Labs
196
+ * team for this company. The middleware uses this to render the
197
+ * verification overlay.
153
198
  *
154
- * On network or 5xx errors, returns `{ success: false, error: { code: "DELIVERY_FAILED" } }`.
155
- * On 4xx (e.g. rate limit), the server error is returned as-is.
199
+ * Returns `null` on any failure (network error, 4xx, 5xx) so the
200
+ * middleware can fall back to the default flow.
156
201
  */
157
- verify(emailAddress: string, deviceId: string, code: string): Promise<ApiResult<VerifyResult>>;
202
+ getVerificationFlowConfig(): Promise<VerificationFlowConfigResult | null>;
158
203
  }
@@ -1 +1 @@
1
- import{createHash,randomUUID}from"crypto";import{encryptData}from"./util";const DEFAULT_BASE_URL="https://api-ingress.unsharedlabs.com",DEFAULT_TIMEOUT_MS=1e4,DEFAULT_MAX_RETRIES=3,MAX_DELAY_MS=3e4,BASE_DELAY_MS=1e3;function sleep(e){return new Promise(s=>setTimeout(s,e))}function retryDelay(e){const s=Math.min(1e3*Math.pow(2,e-1),3e4),t=s*(.5*Math.random()-.25);return Math.max(0,s+t)}async function parseErrorBody(e){const s=await e.text().catch(()=>"");try{const t=JSON.parse(s);return t?.error?.code?{code:t.error.code,message:t.error.message??"Unknown error",details:t.error.details}:{code:"UNKNOWN_ERROR",message:s||e.statusText}}catch{return{code:"UNKNOWN_ERROR",message:s||e.statusText}}}export class UnsharedLabsClient{constructor(e){if(!e.apiKey||""===e.apiKey.trim())throw new Error("apiKey is required");this.t=e.apiKey,this.i=e.baseUrl?e.baseUrl.replace(/\/$/,""):DEFAULT_BASE_URL,this.o=e.timeout??1e4,this.h=e.maxRetries??3,this.u=createHash("sha256").update(e.apiKey).digest()}l(e){return encryptData(e,this.u)}async _(e,s){const t=this.h+1;let r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let a=1;a<=t;a++){a>1&&await sleep(retryDelay(a-1));const t=new AbortController,i=setTimeout(()=>t.abort(),this.o);try{const a=await fetch(e,{method:s.method,headers:{"X-API-Key":this.t,...s.headers},body:s.body,signal:t.signal});if(clearTimeout(i),a.ok){const e=await a.text().catch(()=>"{}");let s;try{s=JSON.parse(e)}catch{s={}}const t="data"in s?s.data:s;return{success:!0,status:a.status,data:t}}const n=await parseErrorBody(a);if(a.status>=400&&a.status<500){if(429===a.status){const e=a.headers.get("Retry-After");if(null!=e){const s=parseInt(e,10);isNaN(s)||(n.retryAfter=s)}}return{success:!1,status:a.status,error:n}}r={success:!1,status:a.status,error:n}}catch(e){clearTimeout(i),r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return r}async submitFingerprintEvent(e,s){const t={hash:e.full_hash,stable_hash:e.fingerprint_id,collected_at:e.timestamp,is_incognito:e.isIncognito,components:e.components,version:e.version};return null!=s?.userId&&(t.user_id=this.l(s.userId)),null!=s?.sessionHash&&(t.session_hash=s.sessionHash),null!=s?.eventType&&(t.event_type=s.eventType),this._(`${this.i}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":randomUUID()},body:JSON.stringify(t)})}async processUserEvent(e){const s={event_type:e.eventType,user_id:e.userId,ip_address:e.ipAddress,device_id:this.l(e.deviceId),session_hash:e.sessionHash,user_agent:e.userAgent,email_address:this.l(e.emailAddress)};return null!=e.subscriptionStatus&&(s.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(s.event_details=e.eventDetails),this._(`${this.i}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})}async checkUser(e,s){const t=new URLSearchParams({email_address:this.l(e),device_id:this.l(s)}),r=await this._(`${this.i}/v2/check-user?${t}`,{method:"GET"});return r.success?r:{success:!0,status:200,data:{is_user_flagged:!1}}}async triggerEmailVerification(e,s){const t=await this._(`${this.i}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email_address:this.l(e),device_id:this.l(s)})});return!t.success&&(0===t.status||t.status>=500)?{success:!1,status:t.status,error:{code:"DELIVERY_FAILED",message:t.error?.message??"Delivery failed"}}:t}async verify(e,s,t){const r=await this._(`${this.i}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email_address:this.l(e),device_id:this.l(s),code:this.l(t)})});return!r.success&&(0===r.status||r.status>=500)?{success:!1,status:r.status,error:{code:"DELIVERY_FAILED",message:r.error?.message??"Delivery failed"}}:r}}
1
+ import{createHash,randomUUID}from"crypto";import{encryptData}from"./util";const DEFAULT_BASE_URL="https://api-ingress.unsharedlabs.com",DEFAULT_TIMEOUT_MS=1e4,DEFAULT_MAX_RETRIES=3,MAX_DELAY_MS=3e4,BASE_DELAY_MS=1e3;function sleep(e){return new Promise(s=>setTimeout(s,e))}function retryDelay(e){const s=Math.min(1e3*Math.pow(2,e-1),3e4),t=s*(.5*Math.random()-.25);return Math.max(0,s+t)}async function parseErrorBody(e){const s=await e.text().catch(()=>"");try{const t=JSON.parse(s);return t?.error?.code?{code:t.error.code,message:t.error.message??"Unknown error",details:t.error.details}:{code:"UNKNOWN_ERROR",message:s||e.statusText}}catch{return{code:"UNKNOWN_ERROR",message:s||e.statusText}}}export class UnsharedLabsClient{constructor(e){if(!e.apiKey||""===e.apiKey.trim())throw new Error("apiKey is required");this.t=e.apiKey,this.i=e.baseUrl?e.baseUrl.replace(/\/$/,""):DEFAULT_BASE_URL,this.o=e.timeout??1e4,this.h=e.maxRetries??3,this.u=createHash("sha256").update(e.apiKey).digest()}l(e){return encryptData(e,this.u)}async _(e,s){const t=this.h+1;let r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let i=1;i<=t;i++){i>1&&await sleep(retryDelay(i-1));const t=new AbortController,a=setTimeout(()=>t.abort(),this.o);try{const i=await fetch(e,{method:s.method,headers:{"X-API-Key":this.t,...s.headers},body:s.body,signal:t.signal});if(clearTimeout(a),i.ok){const e=await i.text().catch(()=>"{}");let s;try{s=JSON.parse(e)}catch{s={}}const t="data"in s?s.data:s;return{success:!0,status:i.status,data:t}}const n=await parseErrorBody(i);if(i.status>=400&&i.status<500){if(429===i.status){const e=i.headers.get("Retry-After");if(null!=e){const s=parseInt(e,10);isNaN(s)||(n.retryAfter=s)}}return{success:!1,status:i.status,error:n}}r={success:!1,status:i.status,error:n}}catch(e){clearTimeout(a),r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return r}async submitFingerprintEvent(e,s){const t={hash:e.full_hash,stable_hash:e.fingerprint_id,collected_at:e.timestamp,is_incognito:e.isIncognito,components:e.components,version:e.version};return null!=s?.userId&&(t.user_id=this.l(s.userId)),null!=s?.emailAddress&&(t.email_address=this.l(s.emailAddress)),null!=s?.sessionHash&&(t.session_hash=s.sessionHash),null!=s?.eventType&&(t.event_type=s.eventType),null!=s?.ipAddress&&(t.ip_address=this.l(s.ipAddress)),null!=s?.userAgent&&(t.user_agent=this.l(s.userAgent)),this._(`${this.i}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":randomUUID()},body:JSON.stringify(t)})}async processUserEvent(e){const s={event_type:e.eventType,user_id:this.l(e.userId),ip_address:this.l(e.ipAddress),device_id:this.l(e.deviceId),session_hash:e.sessionHash,user_agent:this.l(e.userAgent),email_address:this.l(e.emailAddress)};return null!=e.fingerprintId&&(s.fingerprint_id=this.l(e.fingerprintId)),null!=e.subscriptionStatus&&(s.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(s.event_details=e.eventDetails),this._(`${this.i}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})}async checkUser(e,s){const t="string"==typeof s?{deviceId:s}:s;if(!t.deviceId&&!t.fingerprintId)return{success:!0,status:200,data:{is_user_flagged:!1}};const r=new URLSearchParams;r.set("email_address",this.l(e)),t.deviceId&&r.set("device_id",this.l(t.deviceId)),t.fingerprintId&&r.set("fingerprint_id",this.l(t.fingerprintId));const i=await this._(`${this.i}/v2/check-user?${r}`,{method:"GET"});return i.success?i:{success:!0,status:200,data:{is_user_flagged:!1}}}async triggerEmailVerification(e,s,t){const r={email_address:this.l(e),device_id:this.l(s)};t?.fingerprintId&&(r.fingerprint_id=this.l(t.fingerprintId));const i=await this._(`${this.i}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});return!i.success&&(0===i.status||i.status>=500)?{success:!1,status:i.status,error:{code:"DELIVERY_FAILED",message:i.error?.message??"Delivery failed"}}:i}async verify(e,s,t,r){const i={email_address:this.l(e),device_id:this.l(s),code:this.l(t)};r?.fingerprintId&&(i.fingerprint_id=this.l(r.fingerprintId));const a=await this._(`${this.i}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});return!a.success&&(0===a.status||a.status>=500)?{success:!1,status:a.status,error:{code:"DELIVERY_FAILED",message:a.error?.message??"Delivery failed"}}:a.success&&!1===a.data?.verified?{success:!1,status:a.status,error:{code:"VERIFICATION_FAILED",message:"Code is incorrect or expired",details:a.data.reason?{reason:a.data.reason}:void 0}}:a}async getVerificationFlowConfig(){const e=await this._(`${this.i}/v2/verification-flow-config`,{method:"GET"});return e.success&&e.data?e.data:null}}
@@ -1,2 +1,6 @@
1
1
  export { UnsharedLabsClient } from './client';
2
- export type { UnsharedLabsClientConfig, ApiResult, UnsharedLabsError, SubmitFingerprintOptions, SubmitFingerprintResult, ProcessUserEventParams, ProcessUserEventResult, CheckUserResult, TriggerEmailVerificationResult, VerifyResult, } from './client';
2
+ export { createUnsharedMiddleware, assertTrustProxy } from './middleware';
3
+ export type { MiddlewareOptions } from './middleware';
4
+ export { unsharedBoundToUser, VerdictCache, } from './middleware/index';
5
+ export type { ProtectionConfig, Verdict } from './middleware/index';
6
+ export type { UnsharedLabsClientConfig, ApiResult, UnsharedLabsError, SubmitFingerprintOptions, SubmitFingerprintResult, ProcessUserEventParams, ProcessUserEventResult, CheckUserResult, TriggerEmailVerificationResult, VerifyResult, VerificationFlowStep, VerificationFlowConfigResult, } from './client';
@@ -1 +1 @@
1
- export{UnsharedLabsClient}from"./client";
1
+ export{UnsharedLabsClient}from"./client";export{createUnsharedMiddleware,assertTrustProxy}from"./middleware";export{unsharedBoundToUser,VerdictCache}from"./middleware/index";
@@ -0,0 +1,50 @@
1
+ import type { Request, Response, NextFunction } from 'express';
2
+ import type { UnsharedLabsClient } from '../client';
3
+ import { VerdictCache } from './verdict-cache';
4
+ import type { Verdict } from './verdict-cache';
5
+ export interface ProtectionConfig {
6
+ /**
7
+ * Required. Resolves the current user's ID from the request.
8
+ * Return undefined for anonymous/logged-out visitors.
9
+ */
10
+ userId: (req: Request) => string | undefined;
11
+ /**
12
+ * Resolves the current user's email address from the request.
13
+ * Required in Tier 2 (backend-only). Recommended in Tier 1.
14
+ * Falls back to HttpOnly cookie → req.body.email when not configured.
15
+ */
16
+ emailAddress?: (req: Request) => string | undefined;
17
+ /** Route prefix for internal routes. @default "/__unshared" */
18
+ routePrefix?: string;
19
+ /** Allowed CORS origins for /__unshared/* routes. */
20
+ corsOrigins?: string | string[];
21
+ /** Verdict cache TTL in ms. @default 60000 */
22
+ cacheTTL?: number;
23
+ /** Paths to skip entirely (static assets, health checks). */
24
+ skipPaths?: string[];
25
+ /** Resolves a custom session ID. Falls back to __unshared_sid cookie. */
26
+ sessionId?: (req: Request) => string | undefined;
27
+ /**
28
+ * Resolves a device ID from the request.
29
+ * Falls back to __unshared_fp_id cookie → X-Device-Id header.
30
+ */
31
+ deviceId?: (req: Request) => string | undefined;
32
+ /**
33
+ * Called when a flagged, unverified user makes a request.
34
+ * You own the response — block, redirect, or call next() to let it through.
35
+ *
36
+ * If not provided, flagged requests pass through (data collection only).
37
+ * Exceptions are caught and swallowed — the request passes through on error.
38
+ */
39
+ onFlagged?: (context: {
40
+ userId: string;
41
+ emailAddress: string;
42
+ verdict: Verdict;
43
+ req: Request;
44
+ res: Response;
45
+ next: NextFunction;
46
+ }) => void;
47
+ }
48
+ export type { Verdict };
49
+ export { VerdictCache };
50
+ export declare function unsharedBoundToUser(client: UnsharedLabsClient, config: ProtectionConfig): (req: Request, res: Response, next: NextFunction) => void;