unshared-clientjs-sdk 1.0.13 → 2.0.0-rc.2
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 +138 -68
- package/dist/client.d.ts +136 -185
- package/dist/client.js +1 -1
- package/dist/esm/client.d.mts +158 -0
- package/dist/esm/client.mjs +1 -0
- package/dist/esm/index.d.mts +2 -0
- package/dist/esm/index.mjs +1 -0
- package/dist/esm/middleware.d.mts +48 -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 +2 -0
- package/dist/index.js +1 -0
- package/dist/middleware.d.ts +48 -0
- package/dist/middleware.js +1 -0
- package/dist/util.d.ts +1 -2
- package/dist/util.js +1 -1
- package/package.json +28 -4
package/README.md
CHANGED
|
@@ -1,109 +1,179 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @unshared-labs/sdk
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Server-side Node.js SDK for the Unshared Labs V2 API. Handles authentication, AES-256-GCM field encryption, retry logic, and structured error responses.
|
|
4
|
+
|
|
5
|
+
**Keep the API key server-side — never expose it in browser code.**
|
|
5
6
|
|
|
6
7
|
---
|
|
7
8
|
|
|
8
9
|
## Installation
|
|
9
10
|
|
|
10
11
|
```bash
|
|
11
|
-
npm install unshared-
|
|
12
|
-
# or
|
|
13
|
-
yarn add unshared-clientjs-sdk
|
|
12
|
+
npm install @unshared-labs/sdk
|
|
14
13
|
```
|
|
15
14
|
|
|
15
|
+
Requires Node.js ≥ 18.
|
|
16
|
+
|
|
16
17
|
---
|
|
17
18
|
|
|
18
|
-
## Quick
|
|
19
|
+
## Quick Start
|
|
19
20
|
|
|
20
|
-
```
|
|
21
|
-
import UnsharedLabsClient from
|
|
21
|
+
```typescript
|
|
22
|
+
import { UnsharedLabsClient } from '@unshared-labs/sdk';
|
|
22
23
|
|
|
23
24
|
const client = new UnsharedLabsClient({
|
|
24
|
-
apiKey: process.env.
|
|
25
|
+
apiKey: process.env.UNSHARED_API_KEY!,
|
|
25
26
|
});
|
|
26
27
|
```
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
### CommonJS
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
### `submitEvent(...)`
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
submitEvent(
|
|
36
|
-
eventType: string,
|
|
37
|
-
userId: string,
|
|
38
|
-
ipAddress: string,
|
|
39
|
-
deviceId: string,
|
|
40
|
-
sessionHash: string,
|
|
41
|
-
userAgent: string,
|
|
42
|
-
clientTimestamp: string,
|
|
43
|
-
eventDetails?: map<String,any> | null
|
|
44
|
-
): Promise<any>
|
|
31
|
+
```javascript
|
|
32
|
+
const { UnsharedLabsClient } = require('@unshared-labs/sdk');
|
|
45
33
|
```
|
|
46
34
|
|
|
47
|
-
|
|
35
|
+
---
|
|
48
36
|
|
|
49
|
-
|
|
50
|
-
* `userId` — string, unique user identifier (encrypted before sending)
|
|
51
|
-
* `ipAddress` — string, client IP (encrypted)
|
|
52
|
-
* `deviceId` — string, device identifier (encrypted)
|
|
53
|
-
* `sessionHash` — string, session identifier or fingerprint (encrypted)
|
|
54
|
-
* `userAgent` — string
|
|
55
|
-
* `clientTimestamp` — ISO timestamp string (e.g. `new Date().toISOString()`).
|
|
56
|
-
* `eventDetails` — optional map of objects (String,any) of additional details
|
|
37
|
+
## Configuration
|
|
57
38
|
|
|
58
|
-
|
|
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
|
|
45
|
+
});
|
|
46
|
+
```
|
|
59
47
|
|
|
60
48
|
---
|
|
61
49
|
|
|
62
|
-
##
|
|
50
|
+
## Methods
|
|
63
51
|
|
|
64
|
-
|
|
65
|
-
import express from "express";
|
|
66
|
-
import UnsharedLabsClient from "unshared-clientjs-sdk";
|
|
52
|
+
### `submitFingerprintEvent(fingerprint, opts?)`
|
|
67
53
|
|
|
68
|
-
|
|
69
|
-
app.use(express.json());
|
|
54
|
+
Submit a browser fingerprint collected by `@unshared-labs/frontend-fingerprint`. Returns 202 Accepted; the event is stored asynchronously.
|
|
70
55
|
|
|
71
|
-
|
|
72
|
-
|
|
56
|
+
```typescript
|
|
57
|
+
const result = await client.submitFingerprintEvent(fingerprintData, {
|
|
58
|
+
userId: 'u_123',
|
|
59
|
+
sessionHash: 'sess_abc',
|
|
60
|
+
eventType: 'login',
|
|
73
61
|
});
|
|
62
|
+
// result.data: { hash, stable_hash, collected_at, version }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
An `X-Idempotency-Key` is sent automatically. Retries with the same key are deduplicated at the backend.
|
|
66
|
+
|
|
67
|
+
### `processUserEvent(params)`
|
|
74
68
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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 });
|
|
93
|
-
}
|
|
69
|
+
Record a user event and receive naughty-list analysis. `emailAddress` and `deviceId` are AES-256-GCM encrypted before sending.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
const result = await client.processUserEvent({
|
|
73
|
+
eventType: 'login',
|
|
74
|
+
userId: 'u_123',
|
|
75
|
+
ipAddress: req.ip,
|
|
76
|
+
deviceId: 'device-uuid',
|
|
77
|
+
sessionHash: 'sess_abc',
|
|
78
|
+
userAgent: req.headers['user-agent'] ?? '',
|
|
79
|
+
emailAddress: 'user@example.com',
|
|
80
|
+
subscriptionStatus: 'paid',
|
|
94
81
|
});
|
|
82
|
+
// result.data: { event: { … }, analysis: { status, is_user_flagged } }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
> **Note:** This endpoint has no server-side idempotency. Retries may insert duplicate rows.
|
|
86
|
+
|
|
87
|
+
### `checkUser(emailAddress, deviceId)`
|
|
88
|
+
|
|
89
|
+
Check if a user is flagged in the naughty list. Always returns `{ is_user_flagged: false }` on any failure — a backend outage never blocks a legitimate user.
|
|
95
90
|
|
|
96
|
-
|
|
91
|
+
```typescript
|
|
92
|
+
const result = await client.checkUser('user@example.com', 'device-uuid');
|
|
93
|
+
if (result.data?.is_user_flagged) { /* … */ }
|
|
97
94
|
```
|
|
98
95
|
|
|
96
|
+
### `triggerEmailVerification(emailAddress, deviceId)`
|
|
97
|
+
|
|
98
|
+
Send a 6-digit verification code. Rate-limited to one request per `(email_address, device_id)` pair per 2 minutes.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const result = await client.triggerEmailVerification('user@example.com', 'device-uuid');
|
|
102
|
+
if (!result.success && result.error?.code === 'RATE_LIMIT_EXCEEDED') {
|
|
103
|
+
const waitSeconds = result.error.retryAfter ?? result.error.details?.retry_after_seconds;
|
|
104
|
+
// back off for waitSeconds
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Pre-condition:** sender settings must be configured via `createSender` + `validateSender` before this will succeed.
|
|
109
|
+
|
|
110
|
+
### `verify(emailAddress, deviceId, code)`
|
|
111
|
+
|
|
112
|
+
Verify the 6-digit code entered by the user. `success: true` with `verified: false` means the code was wrong — not a transport error.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
const result = await client.verify('user@example.com', 'device-uuid', '123456');
|
|
116
|
+
if (result.data?.verified) { /* proceed */ }
|
|
117
|
+
else { /* result.data.reason: 'not_found' | 'code_mismatch' | 'code_expired' */ }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Express Middleware (Browser Proxy)
|
|
123
|
+
|
|
124
|
+
Mount this to handle fingerprint events forwarded from `@unshared-labs/frontend-fingerprint`:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { createUnsharedMiddleware } from '@unshared-labs/sdk/middleware';
|
|
128
|
+
|
|
129
|
+
app.use(createUnsharedMiddleware(client, {
|
|
130
|
+
userIdExtractor: (req) => req.user?.id,
|
|
131
|
+
eventTypeExtractor: (req) => req.body.event_type,
|
|
132
|
+
routePrefix: '/unshared', // default
|
|
133
|
+
}));
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The middleware:
|
|
137
|
+
- Handles `POST /unshared/submit-fingerprint-event`
|
|
138
|
+
- Passes `next()` for all other routes
|
|
139
|
+
- Never returns 5xx to the browser — upstream errors become `HTTP 200 { success: false }`
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Error Handling
|
|
144
|
+
|
|
145
|
+
All methods return `ApiResult<T>`:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
interface ApiResult<T> {
|
|
149
|
+
success: boolean;
|
|
150
|
+
data?: T;
|
|
151
|
+
error?: {
|
|
152
|
+
code: string;
|
|
153
|
+
message: string;
|
|
154
|
+
details?: Record<string, unknown>;
|
|
155
|
+
retryAfter?: number; // seconds — present on 429 RATE_LIMIT_EXCEEDED
|
|
156
|
+
};
|
|
157
|
+
status: number; // HTTP status, 0 for network errors
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
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
|
+
|
|
99
167
|
---
|
|
100
168
|
|
|
101
|
-
##
|
|
169
|
+
## Encryption
|
|
102
170
|
|
|
103
|
-
|
|
171
|
+
`emailAddress`, `deviceId`, and `code` are encrypted client-side before transmission using AES-256-GCM with a key derived from `SHA256(apiKey)`. Wire format: `<base64(iv)>:<base64(authTag)>:<base64(ciphertext)>` (colon-separated, standard Base64). The backend decrypts with `DecryptSDKField` in `lib/crypto.go`.
|
|
104
172
|
|
|
105
173
|
---
|
|
106
174
|
|
|
107
|
-
##
|
|
175
|
+
## Environment Variables
|
|
108
176
|
|
|
109
|
-
|
|
177
|
+
```bash
|
|
178
|
+
UNSHARED_API_KEY=sk_live_…
|
|
179
|
+
```
|
package/dist/client.d.ts
CHANGED
|
@@ -1,207 +1,158 @@
|
|
|
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: sk_live_… or sk_test_…
|
|
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
|
+
}
|
|
46
|
+
export interface SubmitFingerprintResult {
|
|
47
|
+
hash: string;
|
|
48
|
+
stable_hash: string;
|
|
49
|
+
collected_at: string;
|
|
50
|
+
/** Always present; set to "unknown" by the server if not provided. */
|
|
51
|
+
version: string;
|
|
52
|
+
}
|
|
53
|
+
export interface ProcessUserEventParams {
|
|
54
|
+
eventType: string;
|
|
55
|
+
userId: string;
|
|
56
|
+
/** Plaintext — SDK does not encrypt this field. */
|
|
57
|
+
ipAddress: string;
|
|
58
|
+
/** SDK encrypts before sending. */
|
|
59
|
+
deviceId: string;
|
|
60
|
+
sessionHash: string;
|
|
61
|
+
userAgent: string;
|
|
62
|
+
/** SDK encrypts before sending. */
|
|
63
|
+
emailAddress: string;
|
|
64
|
+
subscriptionStatus?: string | null;
|
|
65
|
+
eventDetails?: Record<string, unknown> | null;
|
|
66
|
+
}
|
|
67
|
+
export interface ProcessUserEventResult {
|
|
68
|
+
event: {
|
|
69
|
+
event_type: string;
|
|
70
|
+
user_id: string;
|
|
71
|
+
email_address: string;
|
|
72
|
+
ip_address: string;
|
|
73
|
+
device_id: string;
|
|
74
|
+
session_hash: string;
|
|
75
|
+
user_agent: string;
|
|
76
|
+
event_details?: string;
|
|
77
|
+
subscription_status?: string;
|
|
78
|
+
};
|
|
79
|
+
analysis: {
|
|
80
|
+
status: 'success' | 'error';
|
|
81
|
+
is_user_flagged: boolean;
|
|
82
|
+
status_details?: string;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export interface CheckUserResult {
|
|
86
|
+
is_user_flagged: boolean;
|
|
87
|
+
}
|
|
88
|
+
export interface TriggerEmailVerificationResult {
|
|
89
|
+
message: string;
|
|
90
|
+
}
|
|
91
|
+
export interface VerifyResult {
|
|
92
|
+
verified: boolean;
|
|
93
|
+
reason?: 'not_found' | 'code_mismatch' | 'code_expired';
|
|
94
|
+
}
|
|
95
|
+
export declare class UnsharedLabsClient {
|
|
96
|
+
private readonly _apiKey;
|
|
97
|
+
private readonly _baseUrl;
|
|
98
|
+
private readonly _timeout;
|
|
99
|
+
private readonly _maxRetries;
|
|
100
|
+
private readonly _encryptionKey;
|
|
48
101
|
constructor(config: UnsharedLabsClientConfig);
|
|
49
|
-
private
|
|
102
|
+
private _encrypt;
|
|
50
103
|
/**
|
|
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
|
-
* ```
|
|
104
|
+
* Core HTTP method with retry logic.
|
|
105
|
+
* - 2xx → success result
|
|
106
|
+
* - 4xx → non-retryable, error result returned immediately
|
|
107
|
+
* - 5xx → retried up to maxRetries, last error result returned
|
|
108
|
+
* - Network/timeout → retried, NETWORK_ERROR result returned
|
|
81
109
|
*/
|
|
82
|
-
|
|
110
|
+
private _fetch;
|
|
83
111
|
/**
|
|
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
|
-
* ```
|
|
112
|
+
* Submit a browser fingerprint event. Publishes asynchronously via Pub/Sub.
|
|
113
|
+
* Maps to: POST /v2/submit-fingerprint-event
|
|
126
114
|
*/
|
|
127
|
-
|
|
115
|
+
submitFingerprintEvent(fingerprint: FingerprintWireFormat, opts?: SubmitFingerprintOptions): Promise<ApiResult<SubmitFingerprintResult>>;
|
|
128
116
|
/**
|
|
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
|
-
* ```
|
|
117
|
+
* Record a user event and receive naughty-list analysis.
|
|
118
|
+
* Maps to: POST /v2/process-user-event
|
|
157
119
|
*/
|
|
158
|
-
|
|
120
|
+
processUserEvent(params: ProcessUserEventParams): Promise<ApiResult<ProcessUserEventResult>>;
|
|
159
121
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* - The email service request fails or returns a non-OK status
|
|
171
|
-
*
|
|
172
|
-
* @example
|
|
173
|
-
* ```typescript
|
|
174
|
-
* await client.triggerEmailVerification('user@example.com');
|
|
175
|
-
* ```
|
|
122
|
+
* Check if a user is flagged in the naughty list.
|
|
123
|
+
* Maps to: GET /v2/check-user
|
|
124
|
+
*
|
|
125
|
+
* **Defensive default:** On any failure (network error, 4xx, 5xx, timeout) this
|
|
126
|
+
* method returns `{ success: true, data: { is_user_flagged: false } }` rather
|
|
127
|
+
* than surfacing the error. This ensures a backend outage never incorrectly
|
|
128
|
+
* blocks a legitimate user. As a consequence, a complete API outage is
|
|
129
|
+
* **invisible** to the caller — you cannot distinguish "user is clean" from
|
|
130
|
+
* "request failed" by inspecting the return value. Monitor API availability
|
|
131
|
+
* through your infrastructure metrics, not through this method's return value.
|
|
176
132
|
*/
|
|
177
|
-
|
|
133
|
+
checkUser(emailAddress: string, deviceId: string): Promise<ApiResult<CheckUserResult>>;
|
|
178
134
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* @param {string} code - Verification code that was sent to the user's email
|
|
187
|
-
*
|
|
188
|
-
* @returns {Promise<any>} Response object from the verification service containing:
|
|
189
|
-
* - Verification status (success/failure)
|
|
190
|
-
* - Additional verification details
|
|
135
|
+
* Send a 6-digit verification code to the user's email address.
|
|
136
|
+
* Maps to: POST /v2/trigger-email-verification
|
|
137
|
+
*/
|
|
138
|
+
triggerEmailVerification(emailAddress: string, deviceId: string): Promise<ApiResult<TriggerEmailVerificationResult>>;
|
|
139
|
+
/**
|
|
140
|
+
* Verify a 6-digit code submitted by the user.
|
|
141
|
+
* Maps to: POST /v2/verify
|
|
191
142
|
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
143
|
+
* **Important:** `result.success` is `true` even when `result.data.verified`
|
|
144
|
+
* is `false`. A `false` verified result means the code was wrong or expired —
|
|
145
|
+
* not a transport failure. Always check `result.data.verified` explicitly:
|
|
195
146
|
*
|
|
196
|
-
* @example
|
|
197
147
|
* ```typescript
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* console.error('Verification failed:', error.message);
|
|
203
|
-
* }
|
|
148
|
+
* const result = await client.verify(email, deviceId, code);
|
|
149
|
+
* if (!result.success) { /* transport/infra error *\/ }
|
|
150
|
+
* else if (!result.data?.verified) { /* wrong or expired code *\/ }
|
|
151
|
+
* else { /* verified *\/ }
|
|
204
152
|
* ```
|
|
153
|
+
*
|
|
154
|
+
* On network or 5xx errors, returns `{ success: false, error: { code: "DELIVERY_FAILED" } }`.
|
|
155
|
+
* On 4xx (e.g. rate limit), the server error is returned as-is.
|
|
205
156
|
*/
|
|
206
|
-
verify(emailAddress: string, deviceId: string, code: string): Promise<
|
|
157
|
+
verify(emailAddress: string, deviceId: string, code: string): Promise<ApiResult<VerifyResult>>;
|
|
207
158
|
}
|
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,a=setTimeout(()=>t.abort(),this.h);try{const i=await fetch(e,{method:s.method,headers:{"X-API-Key":this.i,...s.headers},body:s.body,signal:t.signal});if(clearTimeout(a),i.ok){const e=await i.text().catch(()=>"{}");let s;try{s=JSON.parse(e)}catch{s={}}const t="data"in s?s.data:s;return{success:!0,status:i.status,data:t}}const n=await parseErrorBody(i);if(i.status>=400&&i.status<500){if(429===i.status){const e=i.headers.get("Retry-After");if(null!=e){const s=parseInt(e,10);isNaN(s)||(n.retryAfter=s)}}return{success:!1,status:i.status,error:n}}r={success:!1,status:i.status,error:n}}catch(e){clearTimeout(a),r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return r}async submitFingerprintEvent(e,s){const t={hash:e.full_hash,stable_hash:e.fingerprint_id,collected_at:e.timestamp,is_incognito:e.isIncognito,components:e.components,version:e.version};return null!=s?.userId&&(t.user_id=this._(s.userId)),null!=s?.sessionHash&&(t.session_hash=s.sessionHash),null!=s?.eventType&&(t.event_type=s.eventType),this.p(`${this.o}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":(0,crypto_1.randomUUID)()},body:JSON.stringify(t)})}async processUserEvent(e){const s={event_type:e.eventType,user_id:e.userId,ip_address:e.ipAddress,device_id:this._(e.deviceId),session_hash:e.sessionHash,user_agent:e.userAgent,email_address:this._(e.emailAddress)};return null!=e.subscriptionStatus&&(s.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(s.event_details=e.eventDetails),this.p(`${this.o}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})}async checkUser(e,s){const t=new URLSearchParams({email_address:this._(e),device_id:this._(s)}),r=await this.p(`${this.o}/v2/check-user?${t}`,{method:"GET"});return r.success?r:{success:!0,status:200,data:{is_user_flagged:!1}}}async triggerEmailVerification(e,s){const t=await this.p(`${this.o}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email_address:this._(e),device_id:this._(s)})});return!t.success&&(0===t.status||t.status>=500)?{success:!1,status:t.status,error:{code:"DELIVERY_FAILED",message:t.error?.message??"Delivery failed"}}:t}async verify(e,s,t){const r=await this.p(`${this.o}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email_address:this._(e),device_id:this._(s),code:this._(t)})});return!r.success&&(0===r.status||r.status>=500)?{success:!1,status:r.status,error:{code:"DELIVERY_FAILED",message:r.error?.message??"Delivery failed"}}:r}}exports.UnsharedLabsClient=UnsharedLabsClient;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { FingerprintWireFormat } from '@unshared-labs/shared-types';
|
|
2
|
+
export interface UnsharedLabsClientConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Secret API key. Must be kept server-side.
|
|
5
|
+
* Format: sk_live_… or sk_test_…
|
|
6
|
+
*/
|
|
7
|
+
apiKey: string;
|
|
8
|
+
/**
|
|
9
|
+
* Base URL of the Unshared Labs V2 ingress.
|
|
10
|
+
* @default "https://api-ingress.unsharedlabs.com"
|
|
11
|
+
*/
|
|
12
|
+
baseUrl?: string;
|
|
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;
|
|
23
|
+
}
|
|
24
|
+
export interface UnsharedLabsError {
|
|
25
|
+
code: string;
|
|
26
|
+
message: string;
|
|
27
|
+
details?: Record<string, unknown>;
|
|
28
|
+
/**
|
|
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.
|
|
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
|
+
}
|
|
46
|
+
export interface SubmitFingerprintResult {
|
|
47
|
+
hash: string;
|
|
48
|
+
stable_hash: string;
|
|
49
|
+
collected_at: string;
|
|
50
|
+
/** Always present; set to "unknown" by the server if not provided. */
|
|
51
|
+
version: string;
|
|
52
|
+
}
|
|
53
|
+
export interface ProcessUserEventParams {
|
|
54
|
+
eventType: string;
|
|
55
|
+
userId: string;
|
|
56
|
+
/** Plaintext — SDK does not encrypt this field. */
|
|
57
|
+
ipAddress: string;
|
|
58
|
+
/** SDK encrypts before sending. */
|
|
59
|
+
deviceId: string;
|
|
60
|
+
sessionHash: string;
|
|
61
|
+
userAgent: string;
|
|
62
|
+
/** SDK encrypts before sending. */
|
|
63
|
+
emailAddress: string;
|
|
64
|
+
subscriptionStatus?: string | null;
|
|
65
|
+
eventDetails?: Record<string, unknown> | null;
|
|
66
|
+
}
|
|
67
|
+
export interface ProcessUserEventResult {
|
|
68
|
+
event: {
|
|
69
|
+
event_type: string;
|
|
70
|
+
user_id: string;
|
|
71
|
+
email_address: string;
|
|
72
|
+
ip_address: string;
|
|
73
|
+
device_id: string;
|
|
74
|
+
session_hash: string;
|
|
75
|
+
user_agent: string;
|
|
76
|
+
event_details?: string;
|
|
77
|
+
subscription_status?: string;
|
|
78
|
+
};
|
|
79
|
+
analysis: {
|
|
80
|
+
status: 'success' | 'error';
|
|
81
|
+
is_user_flagged: boolean;
|
|
82
|
+
status_details?: string;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export interface CheckUserResult {
|
|
86
|
+
is_user_flagged: boolean;
|
|
87
|
+
}
|
|
88
|
+
export interface TriggerEmailVerificationResult {
|
|
89
|
+
message: string;
|
|
90
|
+
}
|
|
91
|
+
export interface VerifyResult {
|
|
92
|
+
verified: boolean;
|
|
93
|
+
reason?: 'not_found' | 'code_mismatch' | 'code_expired';
|
|
94
|
+
}
|
|
95
|
+
export declare class UnsharedLabsClient {
|
|
96
|
+
private readonly _apiKey;
|
|
97
|
+
private readonly _baseUrl;
|
|
98
|
+
private readonly _timeout;
|
|
99
|
+
private readonly _maxRetries;
|
|
100
|
+
private readonly _encryptionKey;
|
|
101
|
+
constructor(config: UnsharedLabsClientConfig);
|
|
102
|
+
private _encrypt;
|
|
103
|
+
/**
|
|
104
|
+
* Core HTTP method with retry logic.
|
|
105
|
+
* - 2xx → success result
|
|
106
|
+
* - 4xx → non-retryable, error result returned immediately
|
|
107
|
+
* - 5xx → retried up to maxRetries, last error result returned
|
|
108
|
+
* - Network/timeout → retried, NETWORK_ERROR result returned
|
|
109
|
+
*/
|
|
110
|
+
private _fetch;
|
|
111
|
+
/**
|
|
112
|
+
* Submit a browser fingerprint event. Publishes asynchronously via Pub/Sub.
|
|
113
|
+
* Maps to: POST /v2/submit-fingerprint-event
|
|
114
|
+
*/
|
|
115
|
+
submitFingerprintEvent(fingerprint: FingerprintWireFormat, opts?: SubmitFingerprintOptions): Promise<ApiResult<SubmitFingerprintResult>>;
|
|
116
|
+
/**
|
|
117
|
+
* Record a user event and receive naughty-list analysis.
|
|
118
|
+
* Maps to: POST /v2/process-user-event
|
|
119
|
+
*/
|
|
120
|
+
processUserEvent(params: ProcessUserEventParams): Promise<ApiResult<ProcessUserEventResult>>;
|
|
121
|
+
/**
|
|
122
|
+
* Check if a user is flagged in the naughty list.
|
|
123
|
+
* Maps to: GET /v2/check-user
|
|
124
|
+
*
|
|
125
|
+
* **Defensive default:** On any failure (network error, 4xx, 5xx, timeout) this
|
|
126
|
+
* method returns `{ success: true, data: { is_user_flagged: false } }` rather
|
|
127
|
+
* than surfacing the error. This ensures a backend outage never incorrectly
|
|
128
|
+
* blocks a legitimate user. As a consequence, a complete API outage is
|
|
129
|
+
* **invisible** to the caller — you cannot distinguish "user is clean" from
|
|
130
|
+
* "request failed" by inspecting the return value. Monitor API availability
|
|
131
|
+
* through your infrastructure metrics, not through this method's return value.
|
|
132
|
+
*/
|
|
133
|
+
checkUser(emailAddress: string, deviceId: string): Promise<ApiResult<CheckUserResult>>;
|
|
134
|
+
/**
|
|
135
|
+
* Send a 6-digit verification code to the user's email address.
|
|
136
|
+
* Maps to: POST /v2/trigger-email-verification
|
|
137
|
+
*/
|
|
138
|
+
triggerEmailVerification(emailAddress: string, deviceId: string): Promise<ApiResult<TriggerEmailVerificationResult>>;
|
|
139
|
+
/**
|
|
140
|
+
* Verify a 6-digit code submitted by the user.
|
|
141
|
+
* Maps to: POST /v2/verify
|
|
142
|
+
*
|
|
143
|
+
* **Important:** `result.success` is `true` even when `result.data.verified`
|
|
144
|
+
* is `false`. A `false` verified result means the code was wrong or expired —
|
|
145
|
+
* not a transport failure. Always check `result.data.verified` explicitly:
|
|
146
|
+
*
|
|
147
|
+
* ```typescript
|
|
148
|
+
* const result = await client.verify(email, deviceId, code);
|
|
149
|
+
* if (!result.success) { /* transport/infra error *\/ }
|
|
150
|
+
* else if (!result.data?.verified) { /* wrong or expired code *\/ }
|
|
151
|
+
* else { /* verified *\/ }
|
|
152
|
+
* ```
|
|
153
|
+
*
|
|
154
|
+
* On network or 5xx errors, returns `{ success: false, error: { code: "DELIVERY_FAILED" } }`.
|
|
155
|
+
* On 4xx (e.g. rate limit), the server error is returned as-is.
|
|
156
|
+
*/
|
|
157
|
+
verify(emailAddress: string, deviceId: string, code: string): Promise<ApiResult<VerifyResult>>;
|
|
158
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createHash,randomUUID}from"crypto";import{encryptData}from"./util";const DEFAULT_BASE_URL="https://api-ingress.unsharedlabs.com",DEFAULT_TIMEOUT_MS=1e4,DEFAULT_MAX_RETRIES=3,MAX_DELAY_MS=3e4,BASE_DELAY_MS=1e3;function sleep(e){return new Promise(s=>setTimeout(s,e))}function retryDelay(e){const s=Math.min(1e3*Math.pow(2,e-1),3e4),t=s*(.5*Math.random()-.25);return Math.max(0,s+t)}async function parseErrorBody(e){const s=await e.text().catch(()=>"");try{const t=JSON.parse(s);return t?.error?.code?{code:t.error.code,message:t.error.message??"Unknown error",details:t.error.details}:{code:"UNKNOWN_ERROR",message:s||e.statusText}}catch{return{code:"UNKNOWN_ERROR",message:s||e.statusText}}}export class UnsharedLabsClient{constructor(e){if(!e.apiKey||""===e.apiKey.trim())throw new Error("apiKey is required");this.t=e.apiKey,this.i=e.baseUrl?e.baseUrl.replace(/\/$/,""):DEFAULT_BASE_URL,this.o=e.timeout??1e4,this.h=e.maxRetries??3,this.u=createHash("sha256").update(e.apiKey).digest()}l(e){return encryptData(e,this.u)}async _(e,s){const t=this.h+1;let r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let a=1;a<=t;a++){a>1&&await sleep(retryDelay(a-1));const t=new AbortController,i=setTimeout(()=>t.abort(),this.o);try{const a=await fetch(e,{method:s.method,headers:{"X-API-Key":this.t,...s.headers},body:s.body,signal:t.signal});if(clearTimeout(i),a.ok){const e=await a.text().catch(()=>"{}");let s;try{s=JSON.parse(e)}catch{s={}}const t="data"in s?s.data:s;return{success:!0,status:a.status,data:t}}const n=await parseErrorBody(a);if(a.status>=400&&a.status<500){if(429===a.status){const e=a.headers.get("Retry-After");if(null!=e){const s=parseInt(e,10);isNaN(s)||(n.retryAfter=s)}}return{success:!1,status:a.status,error:n}}r={success:!1,status:a.status,error:n}}catch(e){clearTimeout(i),r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return r}async submitFingerprintEvent(e,s){const t={hash:e.full_hash,stable_hash:e.fingerprint_id,collected_at:e.timestamp,is_incognito:e.isIncognito,components:e.components,version:e.version};return null!=s?.userId&&(t.user_id=this.l(s.userId)),null!=s?.sessionHash&&(t.session_hash=s.sessionHash),null!=s?.eventType&&(t.event_type=s.eventType),this._(`${this.i}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":randomUUID()},body:JSON.stringify(t)})}async processUserEvent(e){const s={event_type:e.eventType,user_id:e.userId,ip_address:e.ipAddress,device_id:this.l(e.deviceId),session_hash:e.sessionHash,user_agent:e.userAgent,email_address:this.l(e.emailAddress)};return null!=e.subscriptionStatus&&(s.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(s.event_details=e.eventDetails),this._(`${this.i}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})}async checkUser(e,s){const t=new URLSearchParams({email_address:this.l(e),device_id:this.l(s)}),r=await this._(`${this.i}/v2/check-user?${t}`,{method:"GET"});return r.success?r:{success:!0,status:200,data:{is_user_flagged:!1}}}async triggerEmailVerification(e,s){const t=await this._(`${this.i}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email_address:this.l(e),device_id:this.l(s)})});return!t.success&&(0===t.status||t.status>=500)?{success:!1,status:t.status,error:{code:"DELIVERY_FAILED",message:t.error?.message??"Delivery failed"}}:t}async verify(e,s,t){const r=await this._(`${this.i}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email_address:this.l(e),device_id:this.l(s),code:this.l(t)})});return!r.success&&(0===r.status||r.status>=500)?{success:!1,status:r.status,error:{code:"DELIVERY_FAILED",message:r.error?.message??"Delivery failed"}}:r}}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { UnsharedLabsClient } from './client';
|
|
2
|
+
export type { UnsharedLabsClientConfig, ApiResult, UnsharedLabsError, SubmitFingerprintOptions, SubmitFingerprintResult, ProcessUserEventParams, ProcessUserEventResult, CheckUserResult, TriggerEmailVerificationResult, VerifyResult, } from './client';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{UnsharedLabsClient}from"./client";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import type { UnsharedLabsClient } from './client';
|
|
3
|
+
export interface MiddlewareOptions {
|
|
4
|
+
/** Override userId extractor. Falls back to req.body.user_id. */
|
|
5
|
+
userIdExtractor?: (req: Request) => string | undefined;
|
|
6
|
+
/** Override eventType extractor. Falls back to req.body.event_type. */
|
|
7
|
+
eventTypeExtractor?: (req: Request) => string | undefined;
|
|
8
|
+
/** Override sessionId extractor. Falls back to X-Session-Id header, then req.body.session_id. */
|
|
9
|
+
sessionIdExtractor?: (req: Request) => string | undefined;
|
|
10
|
+
/** Default event type when none is extractable. @default "browser_event" */
|
|
11
|
+
defaultEventType?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Route prefix the middleware is mounted under.
|
|
14
|
+
* @default "/unshared"
|
|
15
|
+
*/
|
|
16
|
+
routePrefix?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Creates an Express middleware that proxies browser fingerprint events to
|
|
20
|
+
* Unshared Labs. Mount this to handle the browser fingerprint route contract (§4 of spec).
|
|
21
|
+
*
|
|
22
|
+
* Handles POST <routePrefix>/submit-fingerprint-event.
|
|
23
|
+
* Passes all other requests to next().
|
|
24
|
+
*
|
|
25
|
+
* **Prerequisites:**
|
|
26
|
+
* - Mount `express.json()` (or equivalent body-parser) **before** this middleware,
|
|
27
|
+
* otherwise `req.body` will be undefined and every request will return 400.
|
|
28
|
+
* - Configure CORS on your `/unshared/*` routes separately — this middleware
|
|
29
|
+
* does not set any `Access-Control-*` headers.
|
|
30
|
+
* - Disable request body logging on `/unshared/*` routes. `user_id` arrives in
|
|
31
|
+
* plaintext from the browser and must not be captured by logging middleware.
|
|
32
|
+
*
|
|
33
|
+
* **Error contract:** Never returns 5xx to the browser. Upstream failures are
|
|
34
|
+
* returned as HTTP 200 with { success: false, error: { code: "UPSTREAM_ERROR" } }.
|
|
35
|
+
* Standard HTTP-level monitoring will not surface proxy errors — monitor the
|
|
36
|
+
* rate of `success: false` responses in your application layer instead.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* import { createUnsharedMiddleware } from "@unshared-labs/sdk/middleware";
|
|
41
|
+
*
|
|
42
|
+
* app.use(express.json()); // must come first
|
|
43
|
+
* app.use(createUnsharedMiddleware(client, {
|
|
44
|
+
* userIdExtractor: (req) => req.user?.id,
|
|
45
|
+
* }));
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function createUnsharedMiddleware(client: UnsharedLabsClient, options?: MiddlewareOptions): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function createUnsharedMiddleware(e,r){const{userIdExtractor:s,eventTypeExtractor:t,sessionIdExtractor:n,defaultEventType:c="browser_event",routePrefix:o="/unshared"}=r??{},i=`${o}/submit-fingerprint-event`;return async(r,o,a)=>{if("POST"===r.method&&r.path===i)try{const i=r.body??{};if(!i.hash||!i.stable_hash||!i.collected_at)return void o.status(400).json({success:!1,error:{code:"VALIDATION_ERROR",message:"Missing required fingerprint fields: hash, stable_hash, collected_at"}});const a={full_hash:i.hash,fingerprint_id:i.stable_hash,timestamp:i.collected_at,isIncognito:i.is_incognito??!1,components:i.components??{},version:i.version??"unknown"};let d,u,f;try{d=(s?s(r):void 0)??i.user_id}catch{d=i.user_id}try{u=(t?t(r):void 0)??i.event_type??c}catch{u=i.event_type??c}try{f=(n?n(r):void 0)??r.headers["x-session-id"]?.toString()??i.session_id}catch{f=r.headers["x-session-id"]?.toString()??i.session_id}const h=await e.submitFingerprintEvent(a,{userId:d,sessionHash:f,eventType:u});if(!h.success)return void o.status(200).json({success:!1,error:{code:"UPSTREAM_ERROR",message:h.error?.message??"Upstream request failed"}});o.status(202).json({success:!0,data:h.data})}catch(e){o.status(200).json({success:!1,error:{code:"MIDDLEWARE_ERROR",message:e instanceof Error?e.message:"Middleware error"}})}else a()}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function encryptData(data: string, key: Buffer): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createCipheriv,randomBytes}from"crypto";export function encryptData(e,t){const r=randomBytes(12),a=createCipheriv("aes-256-gcm",t,r);let o=a.update(e,"utf8","base64");o+=a.final("base64");const s=a.getAuthTag();return r.toString("base64")+":"+s.toString("base64")+":"+o}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { UnsharedLabsClient } from './client';
|
|
2
|
+
export type { UnsharedLabsClientConfig, ApiResult, UnsharedLabsError, SubmitFingerprintOptions, SubmitFingerprintResult, ProcessUserEventParams, ProcessUserEventResult, CheckUserResult, TriggerEmailVerificationResult, VerifyResult, } from './client';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"t",{value:!0}),exports.UnsharedLabsClient=void 0;var client_1=require("./client");Object.defineProperty(exports,"UnsharedLabsClient",{enumerable:!0,get:function(){return client_1.UnsharedLabsClient}});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import type { UnsharedLabsClient } from './client';
|
|
3
|
+
export interface MiddlewareOptions {
|
|
4
|
+
/** Override userId extractor. Falls back to req.body.user_id. */
|
|
5
|
+
userIdExtractor?: (req: Request) => string | undefined;
|
|
6
|
+
/** Override eventType extractor. Falls back to req.body.event_type. */
|
|
7
|
+
eventTypeExtractor?: (req: Request) => string | undefined;
|
|
8
|
+
/** Override sessionId extractor. Falls back to X-Session-Id header, then req.body.session_id. */
|
|
9
|
+
sessionIdExtractor?: (req: Request) => string | undefined;
|
|
10
|
+
/** Default event type when none is extractable. @default "browser_event" */
|
|
11
|
+
defaultEventType?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Route prefix the middleware is mounted under.
|
|
14
|
+
* @default "/unshared"
|
|
15
|
+
*/
|
|
16
|
+
routePrefix?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Creates an Express middleware that proxies browser fingerprint events to
|
|
20
|
+
* Unshared Labs. Mount this to handle the browser fingerprint route contract (§4 of spec).
|
|
21
|
+
*
|
|
22
|
+
* Handles POST <routePrefix>/submit-fingerprint-event.
|
|
23
|
+
* Passes all other requests to next().
|
|
24
|
+
*
|
|
25
|
+
* **Prerequisites:**
|
|
26
|
+
* - Mount `express.json()` (or equivalent body-parser) **before** this middleware,
|
|
27
|
+
* otherwise `req.body` will be undefined and every request will return 400.
|
|
28
|
+
* - Configure CORS on your `/unshared/*` routes separately — this middleware
|
|
29
|
+
* does not set any `Access-Control-*` headers.
|
|
30
|
+
* - Disable request body logging on `/unshared/*` routes. `user_id` arrives in
|
|
31
|
+
* plaintext from the browser and must not be captured by logging middleware.
|
|
32
|
+
*
|
|
33
|
+
* **Error contract:** Never returns 5xx to the browser. Upstream failures are
|
|
34
|
+
* returned as HTTP 200 with { success: false, error: { code: "UPSTREAM_ERROR" } }.
|
|
35
|
+
* Standard HTTP-level monitoring will not surface proxy errors — monitor the
|
|
36
|
+
* rate of `success: false` responses in your application layer instead.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* import { createUnsharedMiddleware } from "@unshared-labs/sdk/middleware";
|
|
41
|
+
*
|
|
42
|
+
* app.use(express.json()); // must come first
|
|
43
|
+
* app.use(createUnsharedMiddleware(client, {
|
|
44
|
+
* userIdExtractor: (req) => req.user?.id,
|
|
45
|
+
* }));
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function createUnsharedMiddleware(client: UnsharedLabsClient, options?: MiddlewareOptions): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";function createUnsharedMiddleware(e,r){const{userIdExtractor:s,eventTypeExtractor:t,sessionIdExtractor:c,defaultEventType:n="browser_event",routePrefix:o="/unshared"}=r??{},a=`${o}/submit-fingerprint-event`;return async(r,o,i)=>{if("POST"===r.method&&r.path===a)try{const a=r.body??{};if(!a.hash||!a.stable_hash||!a.collected_at)return void o.status(400).json({success:!1,error:{code:"VALIDATION_ERROR",message:"Missing required fingerprint fields: hash, stable_hash, collected_at"}});const i={full_hash:a.hash,fingerprint_id:a.stable_hash,timestamp:a.collected_at,isIncognito:a.is_incognito??!1,components:a.components??{},version:a.version??"unknown"};let d,u,l;try{d=(s?s(r):void 0)??a.user_id}catch{d=a.user_id}try{u=(t?t(r):void 0)??a.event_type??n}catch{u=a.event_type??n}try{l=(c?c(r):void 0)??r.headers["x-session-id"]?.toString()??a.session_id}catch{l=r.headers["x-session-id"]?.toString()??a.session_id}const h=await e.submitFingerprintEvent(i,{userId:d,sessionHash:l,eventType:u});if(!h.success)return void o.status(200).json({success:!1,error:{code:"UPSTREAM_ERROR",message:h.error?.message??"Upstream request failed"}});o.status(202).json({success:!0,data:h.data})}catch(e){o.status(200).json({success:!1,error:{code:"MIDDLEWARE_ERROR",message:e instanceof Error?e.message:"Middleware error"}})}else i()}}Object.defineProperty(exports,"t",{value:!0}),exports.createUnsharedMiddleware=createUnsharedMiddleware;
|
package/dist/util.d.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export declare function encryptData(data: string
|
|
2
|
-
export declare function decryptData(encryptedData: string, clientCredentials: string): string | null;
|
|
1
|
+
export declare function encryptData(data: string, key: Buffer): string;
|
package/dist/util.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"
|
|
1
|
+
"use strict";Object.defineProperty(exports,"t",{value:!0}),exports.encryptData=encryptData;const crypto_1=require("crypto");function encryptData(t,e){const c=(0,crypto_1.randomBytes)(12),r=(0,crypto_1.createCipheriv)("aes-256-gcm",e,c);let s=r.update(t,"utf8","base64");s+=r.final("base64");const o=r.getAuthTag();return c.toString("base64")+":"+o.toString("base64")+":"+s}
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unshared-clientjs-sdk",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "",
|
|
5
|
-
"main": "dist/
|
|
6
|
-
"
|
|
3
|
+
"version": "2.0.0-rc.2",
|
|
4
|
+
"description": "Server-side Node.js SDK for the Unshared Labs V2 API",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/esm/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
7
8
|
"scripts": {
|
|
8
9
|
"build": "node scripts/build.js",
|
|
9
10
|
"build:tsc": "tsc",
|
|
@@ -17,9 +18,32 @@
|
|
|
17
18
|
"dist",
|
|
18
19
|
"README.md"
|
|
19
20
|
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": "./dist/esm/index.mjs",
|
|
24
|
+
"require": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts"
|
|
26
|
+
},
|
|
27
|
+
"./middleware": {
|
|
28
|
+
"import": "./dist/esm/middleware.mjs",
|
|
29
|
+
"require": "./dist/middleware.js",
|
|
30
|
+
"types": "./dist/middleware.d.ts"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
20
36
|
"keywords": [],
|
|
21
37
|
"author": "",
|
|
22
38
|
"license": "MIT",
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@types/express": ">=4"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"@types/express": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
},
|
|
23
47
|
"devDependencies": {
|
|
24
48
|
"@types/express": "^4.17.21",
|
|
25
49
|
"@types/node": "^24.10.1",
|