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.
- package/README.md +100 -102
- package/dist/client.d.ts +57 -12
- package/dist/client.js +1 -1
- package/dist/esm/client.d.mts +57 -12
- package/dist/esm/client.mjs +1 -1
- package/dist/esm/index.d.mts +5 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/middleware/index.d.mts +50 -0
- package/dist/esm/middleware/index.mjs +1 -0
- package/dist/esm/middleware/injection/fingerprint-script.d.mts +16 -0
- package/dist/esm/middleware/injection/fingerprint-script.mjs +1 -0
- package/dist/esm/middleware/rate-limit-backoff.d.mts +14 -0
- package/dist/esm/middleware/rate-limit-backoff.mjs +1 -0
- package/dist/esm/middleware/response-interceptor.d.mts +15 -0
- package/dist/esm/middleware/response-interceptor.mjs +1 -0
- package/dist/esm/middleware/routes/submit-fp.d.mts +24 -0
- package/dist/esm/middleware/routes/submit-fp.mjs +1 -0
- package/dist/esm/middleware/routes/verify.d.mts +28 -0
- package/dist/esm/middleware/routes/verify.mjs +1 -0
- package/dist/esm/middleware/utils/client-ip.d.mts +6 -0
- package/dist/esm/middleware/utils/client-ip.mjs +1 -0
- package/dist/esm/middleware/utils/content-type.d.mts +6 -0
- package/dist/esm/middleware/utils/content-type.mjs +1 -0
- package/dist/esm/middleware/utils/cookies.d.mts +6 -0
- package/dist/esm/middleware/utils/cookies.mjs +1 -0
- package/dist/esm/middleware/utils/device-id.d.mts +5 -0
- package/dist/esm/middleware/utils/device-id.mjs +1 -0
- package/dist/esm/middleware/utils/is-bot.d.mts +5 -0
- package/dist/esm/middleware/utils/is-bot.mjs +1 -0
- package/dist/esm/middleware/utils/secure.d.mts +3 -0
- package/dist/esm/middleware/utils/secure.mjs +1 -0
- package/dist/esm/middleware/utils/skip-paths.d.mts +5 -0
- package/dist/esm/middleware/utils/skip-paths.mjs +1 -0
- package/dist/esm/middleware/verdict-cache.d.mts +47 -0
- package/dist/esm/middleware/verdict-cache.mjs +1 -0
- package/dist/esm/middleware.d.mts +30 -5
- package/dist/esm/middleware.mjs +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +1 -1
- package/dist/middleware/index.d.ts +50 -0
- package/dist/middleware/index.js +1 -0
- package/dist/middleware/injection/fingerprint-script.d.ts +16 -0
- package/dist/middleware/injection/fingerprint-script.js +1 -0
- package/dist/middleware/rate-limit-backoff.d.ts +14 -0
- package/dist/middleware/rate-limit-backoff.js +1 -0
- package/dist/middleware/response-interceptor.d.ts +15 -0
- package/dist/middleware/response-interceptor.js +1 -0
- package/dist/middleware/routes/submit-fp.d.ts +24 -0
- package/dist/middleware/routes/submit-fp.js +1 -0
- package/dist/middleware/routes/verify.d.ts +28 -0
- package/dist/middleware/routes/verify.js +1 -0
- package/dist/middleware/utils/client-ip.d.ts +6 -0
- package/dist/middleware/utils/client-ip.js +1 -0
- package/dist/middleware/utils/content-type.d.ts +6 -0
- package/dist/middleware/utils/content-type.js +1 -0
- package/dist/middleware/utils/cookies.d.ts +6 -0
- package/dist/middleware/utils/cookies.js +1 -0
- package/dist/middleware/utils/device-id.d.ts +5 -0
- package/dist/middleware/utils/device-id.js +1 -0
- package/dist/middleware/utils/is-bot.d.ts +5 -0
- package/dist/middleware/utils/is-bot.js +1 -0
- package/dist/middleware/utils/secure.d.ts +3 -0
- package/dist/middleware/utils/secure.js +1 -0
- package/dist/middleware/utils/skip-paths.d.ts +5 -0
- package/dist/middleware/utils/skip-paths.js +1 -0
- package/dist/middleware/verdict-cache.d.ts +47 -0
- package/dist/middleware/verdict-cache.js +1 -0
- package/dist/middleware.d.ts +30 -5
- package/dist/middleware.js +1 -1
- package/package.json +14 -1
package/README.md
CHANGED
|
@@ -1,47 +1,26 @@
|
|
|
1
|
-
#
|
|
1
|
+
# unshared-clientjs-sdk
|
|
2
2
|
|
|
3
|
-
Server-side Node.js SDK for
|
|
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
|
-
##
|
|
7
|
+
## Install
|
|
10
8
|
|
|
11
9
|
```bash
|
|
12
|
-
npm install
|
|
10
|
+
npm install unshared-clientjs-sdk
|
|
13
11
|
```
|
|
14
12
|
|
|
15
|
-
Requires Node.js
|
|
13
|
+
**Requires Node.js 18+**
|
|
16
14
|
|
|
17
15
|
---
|
|
18
16
|
|
|
19
17
|
## Quick Start
|
|
20
18
|
|
|
21
19
|
```typescript
|
|
22
|
-
import { UnsharedLabsClient } from '
|
|
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
|
-
### `
|
|
31
|
+
### `processUserEvent(params)`
|
|
53
32
|
|
|
54
|
-
|
|
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.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
45
|
+
|
|
46
|
+
if (result.success && result.data?.analysis.is_user_flagged) {
|
|
47
|
+
// Block or challenge the user
|
|
48
|
+
}
|
|
63
49
|
```
|
|
64
50
|
|
|
65
|
-
|
|
51
|
+
**Fields encrypted before sending:** `emailAddress`, `deviceId`
|
|
66
52
|
|
|
67
|
-
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### `checkUser(emailAddress, deviceId)`
|
|
68
56
|
|
|
69
|
-
|
|
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.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
> **
|
|
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
|
-
|
|
69
|
+
---
|
|
88
70
|
|
|
89
|
-
|
|
71
|
+
### `triggerEmailVerification(emailAddress, deviceId)`
|
|
72
|
+
|
|
73
|
+
Send a 6-digit verification code to the user's email.
|
|
90
74
|
|
|
91
75
|
```typescript
|
|
92
|
-
|
|
93
|
-
if (result.data?.is_user_flagged) { /* … */ }
|
|
76
|
+
await client.triggerEmailVerification('user@example.com', 'device_abc');
|
|
94
77
|
```
|
|
95
78
|
|
|
96
|
-
|
|
79
|
+
---
|
|
97
80
|
|
|
98
|
-
|
|
81
|
+
### `verify(emailAddress, deviceId, code)`
|
|
82
|
+
|
|
83
|
+
Validate the code the user submitted.
|
|
99
84
|
|
|
100
85
|
```typescript
|
|
101
|
-
const result = await client.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
99
|
+
---
|
|
109
100
|
|
|
110
|
-
### `
|
|
101
|
+
### `submitFingerprintEvent(fingerprint, opts?)`
|
|
111
102
|
|
|
112
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
115
|
+
## Express Middleware
|
|
123
116
|
|
|
124
|
-
|
|
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 '
|
|
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
|
-
|
|
137
|
-
-
|
|
138
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Configuration
|
|
146
148
|
|
|
147
149
|
```typescript
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
##
|
|
160
|
+
## Response shape
|
|
161
|
+
|
|
162
|
+
All methods return `ApiResult<T>`:
|
|
170
163
|
|
|
171
|
-
|
|
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
|
-
##
|
|
175
|
+
## Security
|
|
176
176
|
|
|
177
|
-
|
|
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:
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
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) {
|
|
150
|
-
*
|
|
151
|
-
*
|
|
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
|
-
*
|
|
155
|
-
*
|
|
199
|
+
* Returns `null` on any failure (network error, 4xx, 5xx) so the
|
|
200
|
+
* middleware can fall back to the default flow.
|
|
156
201
|
*/
|
|
157
|
-
|
|
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,
|
|
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;
|
package/dist/esm/client.d.mts
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:
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
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) {
|
|
150
|
-
*
|
|
151
|
-
*
|
|
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
|
-
*
|
|
155
|
-
*
|
|
199
|
+
* Returns `null` on any failure (network error, 4xx, 5xx) so the
|
|
200
|
+
* middleware can fall back to the default flow.
|
|
156
201
|
*/
|
|
157
|
-
|
|
202
|
+
getVerificationFlowConfig(): Promise<VerificationFlowConfigResult | null>;
|
|
158
203
|
}
|
package/dist/esm/client.mjs
CHANGED
|
@@ -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
|
|
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}}
|
package/dist/esm/index.d.mts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
export { UnsharedLabsClient } from './client';
|
|
2
|
-
export
|
|
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';
|
package/dist/esm/index.mjs
CHANGED
|
@@ -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;
|