unshared-clientjs-sdk 2.0.0-rc.2 → 2.0.0-rc.22
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 +86 -12
- package/dist/client.js +1 -1
- package/dist/esm/client.d.mts +86 -12
- package/dist/esm/client.mjs +1 -1
- package/dist/esm/index.d.mts +7 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/middleware/dispatch-dedupe.d.mts +11 -0
- package/dist/esm/middleware/dispatch-dedupe.mjs +1 -0
- package/dist/esm/middleware/index.d.mts +65 -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 +31 -0
- package/dist/esm/middleware/routes/submit-fp.mjs +1 -0
- package/dist/esm/middleware/routes/verify.d.mts +33 -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 +19 -0
- package/dist/esm/middleware/utils/device-id.mjs +1 -0
- package/dist/esm/middleware/utils/http-helpers.d.mts +21 -0
- package/dist/esm/middleware/utils/http-helpers.mjs +1 -0
- package/dist/esm/middleware/utils/include-path.d.mts +6 -0
- package/dist/esm/middleware/utils/include-path.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/sentinel-user-id.d.mts +10 -0
- package/dist/esm/middleware/utils/sentinel-user-id.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 +38 -10
- package/dist/esm/middleware.mjs +1 -1
- package/dist/esm/types.d.mts +44 -0
- package/dist/esm/types.mjs +1 -0
- package/dist/esm/web/index.d.mts +17 -0
- package/dist/esm/web/index.mjs +1 -0
- package/dist/esm/web/protection-handler.d.mts +28 -0
- package/dist/esm/web/protection-handler.mjs +1 -0
- package/dist/esm/web/submit-handler.d.mts +27 -0
- package/dist/esm/web/submit-handler.mjs +1 -0
- package/dist/esm/web/types.d.mts +110 -0
- package/dist/esm/web/types.mjs +1 -0
- package/dist/esm/web/web-helpers.d.mts +55 -0
- package/dist/esm/web/web-helpers.mjs +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +1 -1
- package/dist/middleware/dispatch-dedupe.d.ts +11 -0
- package/dist/middleware/dispatch-dedupe.js +1 -0
- package/dist/middleware/index.d.ts +65 -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 +31 -0
- package/dist/middleware/routes/submit-fp.js +1 -0
- package/dist/middleware/routes/verify.d.ts +33 -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 +19 -0
- package/dist/middleware/utils/device-id.js +1 -0
- package/dist/middleware/utils/http-helpers.d.ts +21 -0
- package/dist/middleware/utils/http-helpers.js +1 -0
- package/dist/middleware/utils/include-path.d.ts +6 -0
- package/dist/middleware/utils/include-path.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/sentinel-user-id.d.ts +10 -0
- package/dist/middleware/utils/sentinel-user-id.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 +38 -10
- package/dist/middleware.js +1 -1
- package/dist/types.d.ts +44 -0
- package/dist/types.js +1 -0
- package/dist/web/index.d.ts +17 -0
- package/dist/web/index.js +1 -0
- package/dist/web/protection-handler.d.ts +28 -0
- package/dist/web/protection-handler.js +1 -0
- package/dist/web/submit-handler.d.ts +27 -0
- package/dist/web/submit-handler.js +1 -0
- package/dist/web/types.d.ts +110 -0
- package/dist/web/types.js +1 -0
- package/dist/web/web-helpers.d.ts +55 -0
- package/dist/web/web-helpers.js +1 -0
- package/package.json +18 -8
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
|
/**
|
|
@@ -20,6 +20,26 @@ export interface UnsharedLabsClientConfig {
|
|
|
20
20
|
* @default 3
|
|
21
21
|
*/
|
|
22
22
|
maxRetries?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Custom fetch implementation. Use this to configure connection pooling,
|
|
25
|
+
* custom HTTP agents, or proxies. Defaults to the global `fetch`.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* import { Agent, fetch as undiciFetch } from 'undici';
|
|
30
|
+
*
|
|
31
|
+
* const agent = new Agent({
|
|
32
|
+
* keepAliveTimeout: 30_000,
|
|
33
|
+
* connections: 50,
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* const client = new UnsharedLabsClient({
|
|
37
|
+
* apiKey: 'usk_...',
|
|
38
|
+
* fetch: (url, init) => undiciFetch(url, { ...init, dispatcher: agent }),
|
|
39
|
+
* });
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
fetch?: typeof globalThis.fetch;
|
|
23
43
|
}
|
|
24
44
|
export interface UnsharedLabsError {
|
|
25
45
|
code: string;
|
|
@@ -40,8 +60,22 @@ export interface ApiResult<T = unknown> {
|
|
|
40
60
|
}
|
|
41
61
|
export interface SubmitFingerprintOptions {
|
|
42
62
|
userId?: string;
|
|
63
|
+
/** SDK encrypts before sending. */
|
|
64
|
+
emailAddress?: string;
|
|
43
65
|
sessionHash?: string;
|
|
44
66
|
eventType?: string;
|
|
67
|
+
/** SDK encrypts before sending. */
|
|
68
|
+
ipAddress?: string;
|
|
69
|
+
/** SDK encrypts before sending. */
|
|
70
|
+
userAgent?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Client-supplied idempotency key. Forwarded verbatim as X-Idempotency-Key
|
|
73
|
+
* so the backend's ON CONFLICT (idempotency_key) catches duplicates across
|
|
74
|
+
* reloads, tabs, and concurrent SDK instances. When omitted, a fresh UUID
|
|
75
|
+
* is generated (best-effort dedup only within a single submitFingerprintEvent
|
|
76
|
+
* call's retries).
|
|
77
|
+
*/
|
|
78
|
+
idempotencyKey?: string;
|
|
45
79
|
}
|
|
46
80
|
export interface SubmitFingerprintResult {
|
|
47
81
|
hash: string;
|
|
@@ -52,15 +86,19 @@ export interface SubmitFingerprintResult {
|
|
|
52
86
|
}
|
|
53
87
|
export interface ProcessUserEventParams {
|
|
54
88
|
eventType: string;
|
|
89
|
+
/** SDK encrypts before sending. */
|
|
55
90
|
userId: string;
|
|
56
|
-
/**
|
|
91
|
+
/** SDK encrypts before sending. */
|
|
57
92
|
ipAddress: string;
|
|
58
93
|
/** SDK encrypts before sending. */
|
|
59
94
|
deviceId: string;
|
|
60
95
|
sessionHash: string;
|
|
96
|
+
/** SDK encrypts before sending. */
|
|
61
97
|
userAgent: string;
|
|
62
98
|
/** SDK encrypts before sending. */
|
|
63
99
|
emailAddress: string;
|
|
100
|
+
/** SDK encrypts before sending. */
|
|
101
|
+
fingerprintId?: string;
|
|
64
102
|
subscriptionStatus?: string | null;
|
|
65
103
|
eventDetails?: Record<string, unknown> | null;
|
|
66
104
|
}
|
|
@@ -92,12 +130,29 @@ export interface VerifyResult {
|
|
|
92
130
|
verified: boolean;
|
|
93
131
|
reason?: 'not_found' | 'code_mismatch' | 'code_expired';
|
|
94
132
|
}
|
|
133
|
+
export interface VerificationFlowStep {
|
|
134
|
+
type: 'message' | 'email_input' | 'otp_input' | 'support_link';
|
|
135
|
+
title: string;
|
|
136
|
+
body: string;
|
|
137
|
+
buttonText?: string;
|
|
138
|
+
url?: string;
|
|
139
|
+
}
|
|
140
|
+
export interface VerificationFlowConfigResult {
|
|
141
|
+
steps: VerificationFlowStep[];
|
|
142
|
+
branding?: {
|
|
143
|
+
companyName?: string;
|
|
144
|
+
logoUrl?: string;
|
|
145
|
+
primaryColor?: string;
|
|
146
|
+
supportEmail?: string;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
95
149
|
export declare class UnsharedLabsClient {
|
|
96
150
|
private readonly _apiKey;
|
|
97
151
|
private readonly _baseUrl;
|
|
98
152
|
private readonly _timeout;
|
|
99
153
|
private readonly _maxRetries;
|
|
100
154
|
private readonly _encryptionKey;
|
|
155
|
+
private readonly _customFetch;
|
|
101
156
|
constructor(config: UnsharedLabsClientConfig);
|
|
102
157
|
private _encrypt;
|
|
103
158
|
/**
|
|
@@ -131,28 +186,47 @@ export declare class UnsharedLabsClient {
|
|
|
131
186
|
* through your infrastructure metrics, not through this method's return value.
|
|
132
187
|
*/
|
|
133
188
|
checkUser(emailAddress: string, deviceId: string): Promise<ApiResult<CheckUserResult>>;
|
|
189
|
+
checkUser(emailAddress: string, opts: {
|
|
190
|
+
deviceId?: string;
|
|
191
|
+
fingerprintId?: string;
|
|
192
|
+
}): Promise<ApiResult<CheckUserResult>>;
|
|
134
193
|
/**
|
|
135
194
|
* Send a 6-digit verification code to the user's email address.
|
|
136
195
|
* Maps to: POST /v2/trigger-email-verification
|
|
137
196
|
*/
|
|
138
|
-
triggerEmailVerification(emailAddress: string, deviceId: string
|
|
197
|
+
triggerEmailVerification(emailAddress: string, deviceId: string, opts?: {
|
|
198
|
+
fingerprintId?: string;
|
|
199
|
+
}): Promise<ApiResult<TriggerEmailVerificationResult>>;
|
|
139
200
|
/**
|
|
140
201
|
* Verify a 6-digit code submitted by the user.
|
|
141
202
|
* Maps to: POST /v2/verify
|
|
142
203
|
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
204
|
+
* `result.success` reliably indicates whether verification succeeded:
|
|
205
|
+
* - `success: true` → code was correct, user is verified
|
|
206
|
+
* - `success: false, error.code: "VERIFICATION_FAILED"` → wrong or expired code
|
|
207
|
+
* - `success: false, error.code: "DELIVERY_FAILED"` → network/server error
|
|
146
208
|
*
|
|
147
209
|
* ```typescript
|
|
148
210
|
* const result = await client.verify(email, deviceId, code);
|
|
149
|
-
* if (!result.success) {
|
|
150
|
-
*
|
|
151
|
-
*
|
|
211
|
+
* if (!result.success) {
|
|
212
|
+
* if (result.error?.code === 'VERIFICATION_FAILED') { /* bad code *\/ }
|
|
213
|
+
* else { /* transport error *\/ }
|
|
214
|
+
* } else { /* verified *\/ }
|
|
152
215
|
* ```
|
|
216
|
+
*/
|
|
217
|
+
verify(emailAddress: string, deviceId: string, code: string, opts?: {
|
|
218
|
+
fingerprintId?: string;
|
|
219
|
+
}): Promise<ApiResult<VerifyResult>>;
|
|
220
|
+
/**
|
|
221
|
+
* Fetch the verification flow configuration for this company.
|
|
222
|
+
* Maps to: GET /v2/verification-flow-config
|
|
223
|
+
*
|
|
224
|
+
* Returns the flow steps and branding configured by the Unshared Labs
|
|
225
|
+
* team for this company. The middleware uses this to render the
|
|
226
|
+
* verification overlay.
|
|
153
227
|
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
228
|
+
* Returns `null` on any failure (network error, 4xx, 5xx) so the
|
|
229
|
+
* middleware can fall back to the default flow.
|
|
156
230
|
*/
|
|
157
|
-
|
|
231
|
+
getVerificationFlowConfig(): Promise<VerificationFlowConfigResult | null>;
|
|
158
232
|
}
|
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()}
|
|
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(),this._=e.fetch}p(e){return(0,util_1.encryptData)(e,this.l)}async m(e,s){const t=this.u+1;let i={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let r=1;r<=t;r++){r>1&&await sleep(retryDelay(r-1));const t=new AbortController,n=setTimeout(()=>t.abort(),this.h);try{const r=this._??globalThis.fetch,a=await r(e,{method:s.method,headers:{"X-API-Key":this.i,...s.headers},body:s.body,signal:t.signal});if(clearTimeout(n),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 o=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)||(o.retryAfter=s)}}return{success:!1,status:a.status,error:o}}i={success:!1,status:a.status,error:o}}catch(e){clearTimeout(n),i={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return i}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.p(s.userId)),null!=s?.emailAddress&&(t.email_address=this.p(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.p(s.ipAddress)),null!=s?.userAgent&&(t.user_agent=this.p(s.userAgent)),this.m(`${this.o}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":s?.idempotencyKey??(0,crypto_1.randomUUID)()},body:JSON.stringify(t)})}async processUserEvent(e){const s={event_type:e.eventType,user_id:this.p(e.userId),ip_address:this.p(e.ipAddress),device_id:this.p(e.deviceId),session_hash:e.sessionHash,user_agent:this.p(e.userAgent),email_address:this.p(e.emailAddress)};return null!=e.fingerprintId&&(s.fingerprint_id=this.p(e.fingerprintId)),null!=e.subscriptionStatus&&(s.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(s.event_details=e.eventDetails),this.m(`${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 i=new URLSearchParams;i.set("email_address",this.p(e)),t.deviceId&&i.set("device_id",this.p(t.deviceId)),t.fingerprintId&&i.set("fingerprint_id",this.p(t.fingerprintId));const r=await this.m(`${this.o}/v2/check-user?${i}`,{method:"GET"});return r.success?r:{success:!0,status:200,data:{is_user_flagged:!1}}}async triggerEmailVerification(e,s,t){const i={email_address:this.p(e),device_id:this.p(s)};t?.fingerprintId&&(i.fingerprint_id=this.p(t.fingerprintId));const r=await this.m(`${this.o}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});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}async verify(e,s,t,i){const r={email_address:this.p(e),device_id:this.p(s),code:this.p(t)};i?.fingerprintId&&(r.fingerprint_id=this.p(i.fingerprintId));const n=await this.m(`${this.o}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});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.m(`${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
|
/**
|
|
@@ -20,6 +20,26 @@ export interface UnsharedLabsClientConfig {
|
|
|
20
20
|
* @default 3
|
|
21
21
|
*/
|
|
22
22
|
maxRetries?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Custom fetch implementation. Use this to configure connection pooling,
|
|
25
|
+
* custom HTTP agents, or proxies. Defaults to the global `fetch`.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* import { Agent, fetch as undiciFetch } from 'undici';
|
|
30
|
+
*
|
|
31
|
+
* const agent = new Agent({
|
|
32
|
+
* keepAliveTimeout: 30_000,
|
|
33
|
+
* connections: 50,
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* const client = new UnsharedLabsClient({
|
|
37
|
+
* apiKey: 'usk_...',
|
|
38
|
+
* fetch: (url, init) => undiciFetch(url, { ...init, dispatcher: agent }),
|
|
39
|
+
* });
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
fetch?: typeof globalThis.fetch;
|
|
23
43
|
}
|
|
24
44
|
export interface UnsharedLabsError {
|
|
25
45
|
code: string;
|
|
@@ -40,8 +60,22 @@ export interface ApiResult<T = unknown> {
|
|
|
40
60
|
}
|
|
41
61
|
export interface SubmitFingerprintOptions {
|
|
42
62
|
userId?: string;
|
|
63
|
+
/** SDK encrypts before sending. */
|
|
64
|
+
emailAddress?: string;
|
|
43
65
|
sessionHash?: string;
|
|
44
66
|
eventType?: string;
|
|
67
|
+
/** SDK encrypts before sending. */
|
|
68
|
+
ipAddress?: string;
|
|
69
|
+
/** SDK encrypts before sending. */
|
|
70
|
+
userAgent?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Client-supplied idempotency key. Forwarded verbatim as X-Idempotency-Key
|
|
73
|
+
* so the backend's ON CONFLICT (idempotency_key) catches duplicates across
|
|
74
|
+
* reloads, tabs, and concurrent SDK instances. When omitted, a fresh UUID
|
|
75
|
+
* is generated (best-effort dedup only within a single submitFingerprintEvent
|
|
76
|
+
* call's retries).
|
|
77
|
+
*/
|
|
78
|
+
idempotencyKey?: string;
|
|
45
79
|
}
|
|
46
80
|
export interface SubmitFingerprintResult {
|
|
47
81
|
hash: string;
|
|
@@ -52,15 +86,19 @@ export interface SubmitFingerprintResult {
|
|
|
52
86
|
}
|
|
53
87
|
export interface ProcessUserEventParams {
|
|
54
88
|
eventType: string;
|
|
89
|
+
/** SDK encrypts before sending. */
|
|
55
90
|
userId: string;
|
|
56
|
-
/**
|
|
91
|
+
/** SDK encrypts before sending. */
|
|
57
92
|
ipAddress: string;
|
|
58
93
|
/** SDK encrypts before sending. */
|
|
59
94
|
deviceId: string;
|
|
60
95
|
sessionHash: string;
|
|
96
|
+
/** SDK encrypts before sending. */
|
|
61
97
|
userAgent: string;
|
|
62
98
|
/** SDK encrypts before sending. */
|
|
63
99
|
emailAddress: string;
|
|
100
|
+
/** SDK encrypts before sending. */
|
|
101
|
+
fingerprintId?: string;
|
|
64
102
|
subscriptionStatus?: string | null;
|
|
65
103
|
eventDetails?: Record<string, unknown> | null;
|
|
66
104
|
}
|
|
@@ -92,12 +130,29 @@ export interface VerifyResult {
|
|
|
92
130
|
verified: boolean;
|
|
93
131
|
reason?: 'not_found' | 'code_mismatch' | 'code_expired';
|
|
94
132
|
}
|
|
133
|
+
export interface VerificationFlowStep {
|
|
134
|
+
type: 'message' | 'email_input' | 'otp_input' | 'support_link';
|
|
135
|
+
title: string;
|
|
136
|
+
body: string;
|
|
137
|
+
buttonText?: string;
|
|
138
|
+
url?: string;
|
|
139
|
+
}
|
|
140
|
+
export interface VerificationFlowConfigResult {
|
|
141
|
+
steps: VerificationFlowStep[];
|
|
142
|
+
branding?: {
|
|
143
|
+
companyName?: string;
|
|
144
|
+
logoUrl?: string;
|
|
145
|
+
primaryColor?: string;
|
|
146
|
+
supportEmail?: string;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
95
149
|
export declare class UnsharedLabsClient {
|
|
96
150
|
private readonly _apiKey;
|
|
97
151
|
private readonly _baseUrl;
|
|
98
152
|
private readonly _timeout;
|
|
99
153
|
private readonly _maxRetries;
|
|
100
154
|
private readonly _encryptionKey;
|
|
155
|
+
private readonly _customFetch;
|
|
101
156
|
constructor(config: UnsharedLabsClientConfig);
|
|
102
157
|
private _encrypt;
|
|
103
158
|
/**
|
|
@@ -131,28 +186,47 @@ export declare class UnsharedLabsClient {
|
|
|
131
186
|
* through your infrastructure metrics, not through this method's return value.
|
|
132
187
|
*/
|
|
133
188
|
checkUser(emailAddress: string, deviceId: string): Promise<ApiResult<CheckUserResult>>;
|
|
189
|
+
checkUser(emailAddress: string, opts: {
|
|
190
|
+
deviceId?: string;
|
|
191
|
+
fingerprintId?: string;
|
|
192
|
+
}): Promise<ApiResult<CheckUserResult>>;
|
|
134
193
|
/**
|
|
135
194
|
* Send a 6-digit verification code to the user's email address.
|
|
136
195
|
* Maps to: POST /v2/trigger-email-verification
|
|
137
196
|
*/
|
|
138
|
-
triggerEmailVerification(emailAddress: string, deviceId: string
|
|
197
|
+
triggerEmailVerification(emailAddress: string, deviceId: string, opts?: {
|
|
198
|
+
fingerprintId?: string;
|
|
199
|
+
}): Promise<ApiResult<TriggerEmailVerificationResult>>;
|
|
139
200
|
/**
|
|
140
201
|
* Verify a 6-digit code submitted by the user.
|
|
141
202
|
* Maps to: POST /v2/verify
|
|
142
203
|
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
204
|
+
* `result.success` reliably indicates whether verification succeeded:
|
|
205
|
+
* - `success: true` → code was correct, user is verified
|
|
206
|
+
* - `success: false, error.code: "VERIFICATION_FAILED"` → wrong or expired code
|
|
207
|
+
* - `success: false, error.code: "DELIVERY_FAILED"` → network/server error
|
|
146
208
|
*
|
|
147
209
|
* ```typescript
|
|
148
210
|
* const result = await client.verify(email, deviceId, code);
|
|
149
|
-
* if (!result.success) {
|
|
150
|
-
*
|
|
151
|
-
*
|
|
211
|
+
* if (!result.success) {
|
|
212
|
+
* if (result.error?.code === 'VERIFICATION_FAILED') { /* bad code *\/ }
|
|
213
|
+
* else { /* transport error *\/ }
|
|
214
|
+
* } else { /* verified *\/ }
|
|
152
215
|
* ```
|
|
216
|
+
*/
|
|
217
|
+
verify(emailAddress: string, deviceId: string, code: string, opts?: {
|
|
218
|
+
fingerprintId?: string;
|
|
219
|
+
}): Promise<ApiResult<VerifyResult>>;
|
|
220
|
+
/**
|
|
221
|
+
* Fetch the verification flow configuration for this company.
|
|
222
|
+
* Maps to: GET /v2/verification-flow-config
|
|
223
|
+
*
|
|
224
|
+
* Returns the flow steps and branding configured by the Unshared Labs
|
|
225
|
+
* team for this company. The middleware uses this to render the
|
|
226
|
+
* verification overlay.
|
|
153
227
|
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
228
|
+
* Returns `null` on any failure (network error, 4xx, 5xx) so the
|
|
229
|
+
* middleware can fall back to the default flow.
|
|
156
230
|
*/
|
|
157
|
-
|
|
231
|
+
getVerificationFlowConfig(): Promise<VerificationFlowConfigResult | null>;
|
|
158
232
|
}
|