unshared-clientjs-sdk 1.0.13 → 2.0.0-rc.10
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 +135 -67
- package/dist/client.d.ts +175 -183
- package/dist/client.js +1 -1
- package/dist/esm/client.d.mts +199 -0
- package/dist/esm/client.mjs +1 -0
- package/dist/esm/index.d.mts +6 -0
- package/dist/esm/index.mjs +1 -0
- 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 +10 -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 +13 -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/content-type.d.mts +6 -0
- package/dist/esm/middleware/utils/content-type.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/skip-paths.d.mts +5 -0
- package/dist/esm/middleware/utils/skip-paths.mjs +1 -0
- package/dist/esm/middleware/verdict-cache.d.mts +36 -0
- package/dist/esm/middleware/verdict-cache.mjs +1 -0
- package/dist/esm/middleware.d.mts +73 -0
- package/dist/esm/middleware.mjs +1 -0
- package/dist/esm/util.d.mts +1 -0
- package/dist/esm/util.mjs +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1 -0
- package/dist/middleware/index.d.ts +50 -0
- package/dist/middleware/index.js +1 -0
- package/dist/middleware/injection/fingerprint-script.d.ts +10 -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 +13 -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/content-type.d.ts +6 -0
- package/dist/middleware/utils/content-type.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/skip-paths.d.ts +5 -0
- package/dist/middleware/utils/skip-paths.js +1 -0
- package/dist/middleware/verdict-cache.d.ts +36 -0
- package/dist/middleware/verdict-cache.js +1 -0
- package/dist/middleware.d.ts +73 -0
- package/dist/middleware.js +1 -0
- package/dist/util.d.ts +1 -2
- package/dist/util.js +1 -1
- package/package.json +41 -4
package/README.md
CHANGED
|
@@ -1,109 +1,177 @@
|
|
|
1
|
-
#
|
|
1
|
+
# unshared-clientjs-sdk
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
This package is designed for server environments (e.g., Express, Fastify, Next.js API routes) that need to track account sharing activity, session behavior, or suspicious usage patterns.
|
|
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.
|
|
5
4
|
|
|
6
5
|
---
|
|
7
6
|
|
|
8
|
-
##
|
|
7
|
+
## Install
|
|
9
8
|
|
|
10
9
|
```bash
|
|
11
10
|
npm install unshared-clientjs-sdk
|
|
12
|
-
# or
|
|
13
|
-
yarn add unshared-clientjs-sdk
|
|
14
11
|
```
|
|
15
12
|
|
|
13
|
+
**Requires Node.js 18+**
|
|
14
|
+
|
|
16
15
|
---
|
|
17
16
|
|
|
18
|
-
## Quick
|
|
17
|
+
## Quick Start
|
|
19
18
|
|
|
20
|
-
```
|
|
21
|
-
import UnsharedLabsClient from
|
|
19
|
+
```typescript
|
|
20
|
+
import { UnsharedLabsClient } from 'unshared-clientjs-sdk';
|
|
22
21
|
|
|
23
22
|
const client = new UnsharedLabsClient({
|
|
24
|
-
apiKey: process.env.
|
|
23
|
+
apiKey: process.env.UNSHARED_API_KEY, // usk_…
|
|
25
24
|
});
|
|
26
25
|
```
|
|
27
26
|
|
|
28
27
|
---
|
|
29
28
|
|
|
30
|
-
##
|
|
31
|
-
|
|
32
|
-
### `
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
29
|
+
## Methods
|
|
30
|
+
|
|
31
|
+
### `processUserEvent(params)`
|
|
32
|
+
|
|
33
|
+
Record a user event and get a fraud signal back. Call this on login, signup, or any high-value action.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
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'],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (result.success && result.data?.analysis.is_user_flagged) {
|
|
47
|
+
// Block or challenge the user
|
|
48
|
+
}
|
|
45
49
|
```
|
|
46
50
|
|
|
47
|
-
**
|
|
51
|
+
**Fields encrypted before sending:** `emailAddress`, `deviceId`
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### `checkUser(emailAddress, deviceId)`
|
|
56
|
+
|
|
57
|
+
Quick check to see if a user is flagged. Useful in middleware or route guards.
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
const result = await client.checkUser('user@example.com', 'device_abc');
|
|
61
|
+
|
|
62
|
+
if (result.data?.is_user_flagged) {
|
|
63
|
+
// Deny access
|
|
64
|
+
}
|
|
65
|
+
```
|
|
57
66
|
|
|
58
|
-
**Returns
|
|
67
|
+
> **Safe default:** Returns `{ is_user_flagged: false }` on any failure (network error, outage). A backend outage will never accidentally block a legitimate user.
|
|
59
68
|
|
|
60
69
|
---
|
|
61
70
|
|
|
62
|
-
|
|
71
|
+
### `triggerEmailVerification(emailAddress, deviceId)`
|
|
63
72
|
|
|
64
|
-
|
|
65
|
-
import express from "express";
|
|
66
|
-
import UnsharedLabsClient from "unshared-clientjs-sdk";
|
|
73
|
+
Send a 6-digit verification code to the user's email.
|
|
67
74
|
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
```typescript
|
|
76
|
+
await client.triggerEmailVerification('user@example.com', 'device_abc');
|
|
77
|
+
```
|
|
70
78
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
### `verify(emailAddress, deviceId, code)`
|
|
74
82
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
req.headers["user-agent"] || "",
|
|
86
|
-
new Date().toISOString(),
|
|
87
|
-
new Map(Object.entries({"example": true, "source": 'test-server' }))
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
res.status(200).json({ message: "Login tracked" });
|
|
91
|
-
} catch (err) {
|
|
92
|
-
res.status(500).json({ error: err instanceof Error ? err.message : err });
|
|
83
|
+
Validate the code the user submitted.
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const result = await client.verify('user@example.com', 'device_abc', '123456');
|
|
87
|
+
|
|
88
|
+
if (!result.success) {
|
|
89
|
+
if (result.error?.code === 'VERIFICATION_FAILED') {
|
|
90
|
+
// Wrong or expired code — ask user to retry
|
|
91
|
+
} else {
|
|
92
|
+
// Transport error (DELIVERY_FAILED) — retry or show generic error
|
|
93
93
|
}
|
|
94
|
+
} else {
|
|
95
|
+
// Verified — success: true means the code was correct
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
### `submitFingerprintEvent(fingerprint, opts?)`
|
|
102
|
+
|
|
103
|
+
Submit a browser fingerprint collected by `unshared-frontend-sdk`. Typically called by the middleware — you usually won't call this directly.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
await client.submitFingerprintEvent(fingerprint, {
|
|
107
|
+
userId: 'user_123',
|
|
108
|
+
sessionHash: 'session_xyz',
|
|
109
|
+
eventType: 'page_view',
|
|
94
110
|
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Express Middleware
|
|
116
|
+
|
|
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.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { createUnsharedMiddleware } from 'unshared-clientjs-sdk/middleware';
|
|
121
|
+
|
|
122
|
+
// express.json() must come before this middleware
|
|
123
|
+
app.use(express.json());
|
|
95
124
|
|
|
96
|
-
app.
|
|
125
|
+
app.use(createUnsharedMiddleware(client, {
|
|
126
|
+
userIdExtractor: (req) => req.user?.id, // attach logged-in user
|
|
127
|
+
}));
|
|
97
128
|
```
|
|
98
129
|
|
|
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
|
|
133
|
+
|
|
134
|
+
**Options:**
|
|
135
|
+
|
|
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
|
+
|
|
99
145
|
---
|
|
100
146
|
|
|
101
|
-
##
|
|
147
|
+
## Configuration
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
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
|
+
});
|
|
156
|
+
```
|
|
102
157
|
|
|
103
|
-
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Response shape
|
|
161
|
+
|
|
162
|
+
All methods return `ApiResult<T>`:
|
|
163
|
+
|
|
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
|
+
```
|
|
104
172
|
|
|
105
173
|
---
|
|
106
174
|
|
|
107
|
-
##
|
|
175
|
+
## Security
|
|
108
176
|
|
|
109
|
-
|
|
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
|
@@ -1,207 +1,199 @@
|
|
|
1
|
-
|
|
2
|
-
* Configuration object for initializing the UnsharedLabsClient.
|
|
3
|
-
*
|
|
4
|
-
* @interface UnsharedLabsClientConfig
|
|
5
|
-
* @property {string} apiKey - Your API key for authenticating with UnsharedLabs services
|
|
6
|
-
* @property {string} clientId - Encrypted client ID for authentication
|
|
7
|
-
* @property {string} [baseUrl] - Optional custom base URL for the API service
|
|
8
|
-
* @property {string} [emailServiceUrl] - Optional custom URL for the email service
|
|
9
|
-
*/
|
|
1
|
+
import type { FingerprintWireFormat } from '@unshared-labs/shared-types';
|
|
10
2
|
export interface UnsharedLabsClientConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Secret API key. Must be kept server-side.
|
|
5
|
+
* Format: usk_…
|
|
6
|
+
*/
|
|
11
7
|
apiKey: string;
|
|
12
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Base URL of the Unshared Labs V2 ingress.
|
|
10
|
+
* @default "https://api-ingress.unsharedlabs.com"
|
|
11
|
+
*/
|
|
13
12
|
baseUrl?: string;
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Per-request timeout in milliseconds.
|
|
15
|
+
* @default 10_000
|
|
16
|
+
*/
|
|
17
|
+
timeout?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Max retries for 5xx / network failures after the first attempt.
|
|
20
|
+
* @default 3
|
|
21
|
+
*/
|
|
22
|
+
maxRetries?: number;
|
|
15
23
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* - Process user events for fraud detection and analysis
|
|
21
|
-
* - Check if a user is flagged as an account sharer
|
|
22
|
-
* - Trigger email verification
|
|
23
|
-
* - Verify email verification codes
|
|
24
|
-
*
|
|
25
|
-
* All sensitive data (user IDs, IP addresses, etc.) is automatically encrypted
|
|
26
|
-
* before being sent to the API using the provided API key.
|
|
27
|
-
*
|
|
28
|
-
* @class UnsharedLabsClient
|
|
29
|
-
* @example
|
|
30
|
-
* ```typescript
|
|
31
|
-
* const client = new UnsharedLabsClient({
|
|
32
|
-
* apiKey: 'your-api-key',
|
|
33
|
-
* clientId: 'base64-encoded-client-id'
|
|
34
|
-
* });
|
|
35
|
-
* ```
|
|
36
|
-
*/
|
|
37
|
-
export default class UnsharedLabsClient {
|
|
38
|
-
private _apiKey;
|
|
39
|
-
private _clientId;
|
|
40
|
-
private _apiBaseUrl;
|
|
41
|
-
private _emailServiceUrl;
|
|
24
|
+
export interface UnsharedLabsError {
|
|
25
|
+
code: string;
|
|
26
|
+
message: string;
|
|
27
|
+
details?: Record<string, unknown>;
|
|
42
28
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* @constructor
|
|
29
|
+
* Seconds to wait before retrying. Populated on 429 RATE_LIMIT_EXCEEDED
|
|
30
|
+
* responses by reading the `Retry-After` HTTP response header.
|
|
31
|
+
* Falls back to `details.retry_after_seconds` if the header is absent.
|
|
47
32
|
*/
|
|
33
|
+
retryAfter?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface ApiResult<T = unknown> {
|
|
36
|
+
success: boolean;
|
|
37
|
+
data?: T;
|
|
38
|
+
error?: UnsharedLabsError;
|
|
39
|
+
status: number;
|
|
40
|
+
}
|
|
41
|
+
export interface SubmitFingerprintOptions {
|
|
42
|
+
userId?: string;
|
|
43
|
+
sessionHash?: string;
|
|
44
|
+
eventType?: string;
|
|
45
|
+
/** SDK encrypts before sending. */
|
|
46
|
+
ipAddress?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface SubmitFingerprintResult {
|
|
49
|
+
hash: string;
|
|
50
|
+
stable_hash: string;
|
|
51
|
+
collected_at: string;
|
|
52
|
+
/** Always present; set to "unknown" by the server if not provided. */
|
|
53
|
+
version: string;
|
|
54
|
+
}
|
|
55
|
+
export interface ProcessUserEventParams {
|
|
56
|
+
eventType: string;
|
|
57
|
+
/** SDK encrypts before sending. */
|
|
58
|
+
userId: string;
|
|
59
|
+
/** SDK encrypts before sending. */
|
|
60
|
+
ipAddress: string;
|
|
61
|
+
/** SDK encrypts before sending. */
|
|
62
|
+
deviceId: string;
|
|
63
|
+
sessionHash: string;
|
|
64
|
+
/** SDK encrypts before sending. */
|
|
65
|
+
userAgent: string;
|
|
66
|
+
/** SDK encrypts before sending. */
|
|
67
|
+
emailAddress: string;
|
|
68
|
+
/** SDK encrypts before sending. */
|
|
69
|
+
fingerprintId?: string;
|
|
70
|
+
subscriptionStatus?: string | null;
|
|
71
|
+
eventDetails?: Record<string, unknown> | null;
|
|
72
|
+
}
|
|
73
|
+
export interface ProcessUserEventResult {
|
|
74
|
+
event: {
|
|
75
|
+
event_type: string;
|
|
76
|
+
user_id: string;
|
|
77
|
+
email_address: string;
|
|
78
|
+
ip_address: string;
|
|
79
|
+
device_id: string;
|
|
80
|
+
session_hash: string;
|
|
81
|
+
user_agent: string;
|
|
82
|
+
event_details?: string;
|
|
83
|
+
subscription_status?: string;
|
|
84
|
+
};
|
|
85
|
+
analysis: {
|
|
86
|
+
status: 'success' | 'error';
|
|
87
|
+
is_user_flagged: boolean;
|
|
88
|
+
status_details?: string;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export interface CheckUserResult {
|
|
92
|
+
is_user_flagged: boolean;
|
|
93
|
+
}
|
|
94
|
+
export interface TriggerEmailVerificationResult {
|
|
95
|
+
message: string;
|
|
96
|
+
}
|
|
97
|
+
export interface VerifyResult {
|
|
98
|
+
verified: boolean;
|
|
99
|
+
reason?: 'not_found' | 'code_mismatch' | 'code_expired';
|
|
100
|
+
}
|
|
101
|
+
export interface VerificationFlowStep {
|
|
102
|
+
type: 'message' | 'email_input' | 'otp_input' | 'support_link';
|
|
103
|
+
title: string;
|
|
104
|
+
body: string;
|
|
105
|
+
buttonText?: string;
|
|
106
|
+
url?: string;
|
|
107
|
+
}
|
|
108
|
+
export interface VerificationFlowConfigResult {
|
|
109
|
+
steps: VerificationFlowStep[];
|
|
110
|
+
branding?: {
|
|
111
|
+
companyName?: string;
|
|
112
|
+
logoUrl?: string;
|
|
113
|
+
primaryColor?: string;
|
|
114
|
+
supportEmail?: string;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export declare class UnsharedLabsClient {
|
|
118
|
+
private readonly _apiKey;
|
|
119
|
+
private readonly _baseUrl;
|
|
120
|
+
private readonly _timeout;
|
|
121
|
+
private readonly _maxRetries;
|
|
122
|
+
private readonly _encryptionKey;
|
|
48
123
|
constructor(config: UnsharedLabsClientConfig);
|
|
49
|
-
private
|
|
124
|
+
private _encrypt;
|
|
50
125
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* @param {string} userId - Unique identifier for the user. Use email address if available.
|
|
57
|
-
* @param {string} ipAddress - IP address of the user
|
|
58
|
-
* @param {string} deviceId - Unique identifier for the device
|
|
59
|
-
* @param {string} sessionHash - Hash of the user's session
|
|
60
|
-
* @param {string} userAgent - User agent string from the browser/client
|
|
61
|
-
* @param {string} clientTimestamp - ISO 8601 timestamp when the event occurred on the client
|
|
62
|
-
* @param {Map<string, any> | null} [eventDetails] - Optional map of additional event details to include
|
|
63
|
-
*
|
|
64
|
-
* @returns {Promise<{status: number, statusText: string, data: any}>} Response object containing HTTP status, status text, and response data
|
|
65
|
-
*
|
|
66
|
-
* @throws {Error} Throws an error if the API request fails or returns a non-OK status
|
|
67
|
-
*
|
|
68
|
-
* @example
|
|
69
|
-
* ```typescript
|
|
70
|
-
* const result = await client.submitEvent(
|
|
71
|
-
* 'signup',
|
|
72
|
-
* 'user@example.com',
|
|
73
|
-
* '192.168.1.1',
|
|
74
|
-
* 'device456',
|
|
75
|
-
* 'session789',
|
|
76
|
-
* 'Mozilla/5.0...',
|
|
77
|
-
* '2024-01-01T00:00:00Z',
|
|
78
|
-
* new Map([['source', 'web']])
|
|
79
|
-
* );
|
|
80
|
-
* ```
|
|
126
|
+
* Core HTTP method with retry logic.
|
|
127
|
+
* - 2xx → success result
|
|
128
|
+
* - 4xx → non-retryable, error result returned immediately
|
|
129
|
+
* - 5xx → retried up to maxRetries, last error result returned
|
|
130
|
+
* - Network/timeout → retried, NETWORK_ERROR result returned
|
|
81
131
|
*/
|
|
82
|
-
|
|
132
|
+
private _fetch;
|
|
83
133
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* This method encrypts all sensitive user data (user ID, IP address, device ID, etc.) before
|
|
87
|
-
* sending it to the API. The response includes both event processing results and analysis
|
|
88
|
-
* results that indicate if the user has been flagged for suspicious activity.
|
|
89
|
-
*
|
|
90
|
-
* @param {string} eventType - Type of event being processed (e.g., 'signup', 'login', 'purchase')
|
|
91
|
-
* @param {string} userId - Unique identifier for the user. Use email address if available.
|
|
92
|
-
* @param {string} ipAddress - IP address of the user
|
|
93
|
-
* @param {string} deviceId - Unique identifier for the device
|
|
94
|
-
* @param {string} sessionHash - Hash of the user's session
|
|
95
|
-
* @param {string} userAgent - User agent string from the browser/client
|
|
96
|
-
* @param {string} emailAddress - Email address of the user
|
|
97
|
-
* @param {string | null} [subscriptionStatus] - Optional subscription status string, (e.g. 'paid', 'free', 'trial', 'discounted')
|
|
98
|
-
* @param {Map<string, any> | null} [eventDetails] - Optional map of additional event details to include
|
|
99
|
-
*
|
|
100
|
-
* @returns {Promise<any>} Response object containing:
|
|
101
|
-
* - success: Boolean indicating if the API call was successful
|
|
102
|
-
* - event: Event processing result with status and event data (on success)
|
|
103
|
-
* - analysis: Analysis result with flagging information (on success)
|
|
104
|
-
* - error: Error message (on failure)
|
|
105
|
-
*
|
|
106
|
-
* @example
|
|
107
|
-
* ```typescript
|
|
108
|
-
* const response = await client.processUserEvent(
|
|
109
|
-
* 'signup',
|
|
110
|
-
* 'user@example.com',
|
|
111
|
-
* '192.168.1.1',
|
|
112
|
-
* 'device456',
|
|
113
|
-
* 'session789',
|
|
114
|
-
* 'Mozilla/5.0...',
|
|
115
|
-
* 'user@example.com',
|
|
116
|
-
* 'paid',
|
|
117
|
-
* new Map([['source', 'web']])
|
|
118
|
-
* );
|
|
119
|
-
*
|
|
120
|
-
* if (!response.success) {
|
|
121
|
-
* console.error('Error:', response.error);
|
|
122
|
-
* } else if (response.analysis?.is_user_flagged) {
|
|
123
|
-
* console.log('User flagged for suspicious activity');
|
|
124
|
-
* }
|
|
125
|
-
* ```
|
|
134
|
+
* Submit a browser fingerprint event. Publishes asynchronously via Pub/Sub.
|
|
135
|
+
* Maps to: POST /v2/submit-fingerprint-event
|
|
126
136
|
*/
|
|
127
|
-
|
|
137
|
+
submitFingerprintEvent(fingerprint: FingerprintWireFormat, opts?: SubmitFingerprintOptions): Promise<ApiResult<SubmitFingerprintResult>>;
|
|
128
138
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* This method encrypts the email address and device ID before sending them to the API.
|
|
132
|
-
* The response indicates whether the user has been flagged for suspicious activity.
|
|
133
|
-
*
|
|
134
|
-
* @param {string} emailAddress - Email address of the user to check
|
|
135
|
-
* @param {string} deviceId - Unique identifier for the device
|
|
136
|
-
*
|
|
137
|
-
* @returns {Promise<any>} Response object containing:
|
|
138
|
-
* - success: Boolean indicating if the API call was successful
|
|
139
|
-
* - status: String indicating the status ("success" or "error")
|
|
140
|
-
* - is_user_flagged: Boolean indicating if the user was flagged as an account sharer
|
|
141
|
-
* - status_details: String with error details (only present on error)
|
|
142
|
-
* - error: Error message (only present on failure)
|
|
143
|
-
*
|
|
144
|
-
* @example
|
|
145
|
-
* ```typescript
|
|
146
|
-
* const response = await client.checkUser(
|
|
147
|
-
* 'user@example.com',
|
|
148
|
-
* 'device456'
|
|
149
|
-
* );
|
|
150
|
-
*
|
|
151
|
-
* if (!response.success) {
|
|
152
|
-
* console.error('Error:', response.error || response.status_details);
|
|
153
|
-
* } else if (response.is_user_flagged) {
|
|
154
|
-
* console.log('User is flagged for suspicious activity');
|
|
155
|
-
* }
|
|
156
|
-
* ```
|
|
139
|
+
* Record a user event and receive naughty-list analysis.
|
|
140
|
+
* Maps to: POST /v2/process-user-event
|
|
157
141
|
*/
|
|
158
|
-
|
|
142
|
+
processUserEvent(params: ProcessUserEventParams): Promise<ApiResult<ProcessUserEventResult>>;
|
|
159
143
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
144
|
+
* Check if a user is flagged in the naughty list.
|
|
145
|
+
* Maps to: GET /v2/check-user
|
|
146
|
+
*
|
|
147
|
+
* **Defensive default:** On any failure (network error, 4xx, 5xx, timeout) this
|
|
148
|
+
* method returns `{ success: true, data: { is_user_flagged: false } }` rather
|
|
149
|
+
* than surfacing the error. This ensures a backend outage never incorrectly
|
|
150
|
+
* blocks a legitimate user. As a consequence, a complete API outage is
|
|
151
|
+
* **invisible** to the caller — you cannot distinguish "user is clean" from
|
|
152
|
+
* "request failed" by inspecting the return value. Monitor API availability
|
|
153
|
+
* through your infrastructure metrics, not through this method's return value.
|
|
154
|
+
*/
|
|
155
|
+
checkUser(emailAddress: string, deviceId: string): Promise<ApiResult<CheckUserResult>>;
|
|
156
|
+
checkUser(emailAddress: string, opts: {
|
|
157
|
+
deviceId?: string;
|
|
158
|
+
fingerprintId?: string;
|
|
159
|
+
}): Promise<ApiResult<CheckUserResult>>;
|
|
160
|
+
/**
|
|
161
|
+
* Send a 6-digit verification code to the user's email address.
|
|
162
|
+
* Maps to: POST /v2/trigger-email-verification
|
|
163
|
+
*/
|
|
164
|
+
triggerEmailVerification(emailAddress: string, deviceId: string, opts?: {
|
|
165
|
+
fingerprintId?: string;
|
|
166
|
+
}): Promise<ApiResult<TriggerEmailVerificationResult>>;
|
|
167
|
+
/**
|
|
168
|
+
* Verify a 6-digit code submitted by the user.
|
|
169
|
+
* Maps to: POST /v2/verify
|
|
167
170
|
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
+
* `result.success` reliably indicates whether verification succeeded:
|
|
172
|
+
* - `success: true` → code was correct, user is verified
|
|
173
|
+
* - `success: false, error.code: "VERIFICATION_FAILED"` → wrong or expired code
|
|
174
|
+
* - `success: false, error.code: "DELIVERY_FAILED"` → network/server error
|
|
171
175
|
*
|
|
172
|
-
* @example
|
|
173
176
|
* ```typescript
|
|
174
|
-
* await client.
|
|
177
|
+
* const result = await client.verify(email, deviceId, code);
|
|
178
|
+
* if (!result.success) {
|
|
179
|
+
* if (result.error?.code === 'VERIFICATION_FAILED') { /* bad code *\/ }
|
|
180
|
+
* else { /* transport error *\/ }
|
|
181
|
+
* } else { /* verified *\/ }
|
|
175
182
|
* ```
|
|
176
183
|
*/
|
|
177
|
-
|
|
184
|
+
verify(emailAddress: string, deviceId: string, code: string, opts?: {
|
|
185
|
+
fingerprintId?: string;
|
|
186
|
+
}): Promise<ApiResult<VerifyResult>>;
|
|
178
187
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* This method validates a verification code that was sent to the user via email.
|
|
182
|
-
* The code is typically obtained from the user after they receive the verification email.
|
|
183
|
-
*
|
|
184
|
-
* @param {string} emailAddress - Email address for whom the code is being verified.
|
|
185
|
-
* @param {string} deviceId - device id for whom the code is being verified.
|
|
186
|
-
* @param {string} code - Verification code that was sent to the user's email
|
|
188
|
+
* Fetch the verification flow configuration for this company.
|
|
189
|
+
* Maps to: GET /v2/verification-flow-config
|
|
187
190
|
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
+
* Returns the flow steps and branding configured by the Unshared Labs
|
|
192
|
+
* team for this company. The middleware uses this to render the
|
|
193
|
+
* verification overlay.
|
|
191
194
|
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* - The service returns a non-OK status (e.g., invalid code, expired code)
|
|
195
|
-
*
|
|
196
|
-
* @example
|
|
197
|
-
* ```typescript
|
|
198
|
-
* try {
|
|
199
|
-
* const result = await client.verify('user@example.com', '123456');
|
|
200
|
-
* console.log('Verification successful:', result);
|
|
201
|
-
* } catch (error) {
|
|
202
|
-
* console.error('Verification failed:', error.message);
|
|
203
|
-
* }
|
|
204
|
-
* ```
|
|
195
|
+
* Returns `null` on any failure (network error, 4xx, 5xx) so the
|
|
196
|
+
* middleware can fall back to the default flow.
|
|
205
197
|
*/
|
|
206
|
-
|
|
198
|
+
getVerificationFlowConfig(): Promise<VerificationFlowConfigResult | null>;
|
|
207
199
|
}
|
package/dist/client.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"
|
|
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?.sessionHash&&(t.session_hash=s.sessionHash),null!=s?.eventType&&(t.event_type=s.eventType),null!=s?.ipAddress&&(t.ip_address=this._(s.ipAddress)),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;
|