unshared-clientjs-sdk 1.0.14 → 2.0.0-rc.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +135 -67
  2. package/dist/client.d.ts +175 -183
  3. package/dist/client.js +1 -1
  4. package/dist/esm/client.d.mts +199 -0
  5. package/dist/esm/client.mjs +1 -0
  6. package/dist/esm/index.d.mts +6 -0
  7. package/dist/esm/index.mjs +1 -0
  8. package/dist/esm/middleware/index.d.mts +50 -0
  9. package/dist/esm/middleware/index.mjs +1 -0
  10. package/dist/esm/middleware/injection/fingerprint-script.d.mts +10 -0
  11. package/dist/esm/middleware/injection/fingerprint-script.mjs +1 -0
  12. package/dist/esm/middleware/rate-limit-backoff.d.mts +14 -0
  13. package/dist/esm/middleware/rate-limit-backoff.mjs +1 -0
  14. package/dist/esm/middleware/response-interceptor.d.mts +13 -0
  15. package/dist/esm/middleware/response-interceptor.mjs +1 -0
  16. package/dist/esm/middleware/routes/submit-fp.d.mts +24 -0
  17. package/dist/esm/middleware/routes/submit-fp.mjs +1 -0
  18. package/dist/esm/middleware/routes/verify.d.mts +28 -0
  19. package/dist/esm/middleware/routes/verify.mjs +1 -0
  20. package/dist/esm/middleware/utils/content-type.d.mts +6 -0
  21. package/dist/esm/middleware/utils/content-type.mjs +1 -0
  22. package/dist/esm/middleware/utils/is-bot.d.mts +5 -0
  23. package/dist/esm/middleware/utils/is-bot.mjs +1 -0
  24. package/dist/esm/middleware/utils/skip-paths.d.mts +5 -0
  25. package/dist/esm/middleware/utils/skip-paths.mjs +1 -0
  26. package/dist/esm/middleware/verdict-cache.d.mts +36 -0
  27. package/dist/esm/middleware/verdict-cache.mjs +1 -0
  28. package/dist/esm/middleware.d.mts +73 -0
  29. package/dist/esm/middleware.mjs +1 -0
  30. package/dist/esm/util.d.mts +1 -0
  31. package/dist/esm/util.mjs +1 -0
  32. package/dist/index.d.ts +6 -0
  33. package/dist/index.js +1 -0
  34. package/dist/middleware/index.d.ts +50 -0
  35. package/dist/middleware/index.js +1 -0
  36. package/dist/middleware/injection/fingerprint-script.d.ts +10 -0
  37. package/dist/middleware/injection/fingerprint-script.js +1 -0
  38. package/dist/middleware/rate-limit-backoff.d.ts +14 -0
  39. package/dist/middleware/rate-limit-backoff.js +1 -0
  40. package/dist/middleware/response-interceptor.d.ts +13 -0
  41. package/dist/middleware/response-interceptor.js +1 -0
  42. package/dist/middleware/routes/submit-fp.d.ts +24 -0
  43. package/dist/middleware/routes/submit-fp.js +1 -0
  44. package/dist/middleware/routes/verify.d.ts +28 -0
  45. package/dist/middleware/routes/verify.js +1 -0
  46. package/dist/middleware/utils/content-type.d.ts +6 -0
  47. package/dist/middleware/utils/content-type.js +1 -0
  48. package/dist/middleware/utils/is-bot.d.ts +5 -0
  49. package/dist/middleware/utils/is-bot.js +1 -0
  50. package/dist/middleware/utils/skip-paths.d.ts +5 -0
  51. package/dist/middleware/utils/skip-paths.js +1 -0
  52. package/dist/middleware/verdict-cache.d.ts +36 -0
  53. package/dist/middleware/verdict-cache.js +1 -0
  54. package/dist/middleware.d.ts +73 -0
  55. package/dist/middleware.js +1 -0
  56. package/dist/util.d.ts +1 -2
  57. package/dist/util.js +1 -1
  58. package/package.json +41 -4
package/README.md CHANGED
@@ -1,109 +1,177 @@
1
- # Unshared Labs SDK
1
+ # unshared-clientjs-sdk
2
2
 
3
- A lightweight, drop-in SDK for sending secure session and event data to the **Unshared Labs** platform.
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
- ## Installation
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 Example
17
+ ## Quick Start
19
18
 
20
- ```ts
21
- import UnsharedLabsClient from "unshared-clientjs-sdk";
19
+ ```typescript
20
+ import { UnsharedLabsClient } from 'unshared-clientjs-sdk';
22
21
 
23
22
  const client = new UnsharedLabsClient({
24
- apiKey: process.env.UNSHARED_LABS_API_KEY!,
23
+ apiKey: process.env.UNSHARED_API_KEY, // usk_…
25
24
  });
26
25
  ```
27
26
 
28
27
  ---
29
28
 
30
- ## API
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>
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
- **Parameters**
51
+ **Fields encrypted before sending:** `emailAddress`, `deviceId`
48
52
 
49
- * `eventType` — string, e.g. `"login"`, `"heartbeat"`, `"logout"`
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
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**: A promise resolving Unshared Lab's DB insert result or rejecting with an error.
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
- ## Example (Express)
71
+ ### `triggerEmailVerification(emailAddress, deviceId)`
63
72
 
64
- ```ts
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
- const app = express();
69
- app.use(express.json());
75
+ ```typescript
76
+ await client.triggerEmailVerification('user@example.com', 'device_abc');
77
+ ```
70
78
 
71
- const client = new UnsharedLabsClient({
72
- apiKey: process.env.UNSHARED_LABS_API_KEY!,
73
- });
79
+ ---
80
+
81
+ ### `verify(emailAddress, deviceId, code)`
74
82
 
75
- app.post("/login", async (req, res) => {
76
- const { userId } = req.body;
77
-
78
- try {
79
- await client.submitEvent(
80
- "login",
81
- userId,
82
- req.ip,
83
- req.headers["x-device-id"]?.toString() || "unknown-device",
84
- req.headers["x-session-hash"]?.toString() || "unknown-session",
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.listen(3000, () => console.log("Server running on port 3000"));
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
- ## License
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
- MIT © Unshared Labs
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
- ## Support
175
+ ## Security
108
176
 
109
- For issues or onboarding help, contact Unshared Labs support.
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
- clientId: string;
8
+ /**
9
+ * Base URL of the Unshared Labs V2 ingress.
10
+ * @default "https://api-ingress.unsharedlabs.com"
11
+ */
13
12
  baseUrl?: string;
14
- emailServiceUrl?: 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;
15
23
  }
16
- /**
17
- * Client for interacting with UnsharedLabs API services.
18
- *
19
- * This client provides methods to:
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
- * Creates a new instance of UnsharedLabsClient.
44
- *
45
- * @param {UnsharedLabsClientConfig} config - Configuration object containing API credentials and optional service URLs
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 _getBasicAuthHeader;
124
+ private _encrypt;
50
125
  /**
51
- * Submits a user event to the UnsharedLabs API for processing.
52
- *
53
- * @deprecated This method is deprecated and will be removed in a future version. Use `processUserEvent` instead.
54
- *
55
- * @param {string} eventType - Type of event being submitted (e.g., 'signup', 'login', 'purchase')
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
- submitEvent(eventType: string, userId: string, ipAddress: string, deviceId: string, sessionHash: string, userAgent: string, clientTimestamp: string, eventDetails?: Map<string, any> | null): Promise<any>;
132
+ private _fetch;
83
133
  /**
84
- * Processes a user event through the UnsharedLabs API, including fraud detection and analysis.
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
- processUserEvent(eventType: string, userId: string, ipAddress: string, deviceId: string, sessionHash: string, userAgent: string, emailAddress: string, subscriptionStatus?: string | null, eventDetails?: Map<string, any> | null): Promise<any>;
137
+ submitFingerprintEvent(fingerprint: FingerprintWireFormat, opts?: SubmitFingerprintOptions): Promise<ApiResult<SubmitFingerprintResult>>;
128
138
  /**
129
- * Checks if a user is flagged as an account sharer based on their email address and device ID.
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
- checkUser(emailAddress: string, deviceId: string): Promise<any>;
142
+ processUserEvent(params: ProcessUserEventParams): Promise<ApiResult<ProcessUserEventResult>>;
159
143
  /**
160
- * Triggers an email verification to be sent to the user.
161
- *
162
- * This method sends a verification email to the specified email address.
163
- *
164
- * @param {string} emailAddress - Email address to send the verification to
165
- * @param {string} deviceId - device id for whom the code is being verified.
166
- * @returns {Promise<any>} Response object from the email service containing verification details
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
- * @throws {Error} Throws an error if:
169
- * - The emailAddress is not a valid email address
170
- * - The email service request fails or returns a non-OK status
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.triggerEmailVerification('user@example.com');
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
- triggerEmailVerification(emailAddress: string, deviceId: string): Promise<any>;
184
+ verify(emailAddress: string, deviceId: string, code: string, opts?: {
185
+ fingerprintId?: string;
186
+ }): Promise<ApiResult<VerifyResult>>;
178
187
  /**
179
- * Verifies an email verification code for a user.
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
- * @returns {Promise<any>} Response object from the verification service containing:
189
- * - Verification status (success/failure)
190
- * - Additional verification details
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
- * @throws {Error} Throws an error if:
193
- * - The verification service request fails
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
- verify(emailAddress: string, deviceId: string, code: string): Promise<any>;
198
+ getVerificationFlowConfig(): Promise<VerificationFlowConfigResult | null>;
207
199
  }
package/dist/client.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0});const util_1=require("./util");class UnsharedLabsClient{constructor(t){this._apiBaseUrl="https://api.unsharedlabs.com",this._emailServiceUrl="https://emailservice.unsharedlabs.com",this._apiKey=t.apiKey,this._clientId=atob(t.clientId).trim(),t.baseUrl&&(this._apiBaseUrl=t.baseUrl),t.emailServiceUrl&&(this._emailServiceUrl=t.emailServiceUrl)}_getBasicAuthHeader(){const t=`${this._clientId}:${this._apiKey}`;return`Basic ${Buffer.from(t).toString("base64")}`}async submitEvent(t,e,i,a,r,s,n,c){try{const o=(0,util_1.encryptData)(e,this._apiKey),h=(0,util_1.encryptData)(i,this._apiKey),_=(0,util_1.encryptData)(a,this._apiKey),l=(0,util_1.encryptData)(r,this._apiKey),p=(0,util_1.encryptData)(s,this._apiKey),y={user_id:o,event_type:t,ip_address:h,device_id:_,session_hash:l,user_agent:p,client_timestamp:n,event_details:(0,util_1.encryptData)(c?JSON.stringify([...c.entries()]):null,this._apiKey)},u=await fetch(`${this._apiBaseUrl}/api/v1/submitEvent`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:this._getBasicAuthHeader()},body:JSON.stringify(y)});if(!u.ok){const t=await u.json().catch(()=>({error:u.statusText}));throw new Error(`API returned error: ${JSON.stringify(t)}`)}const d=await u.json();return{status:u.status,statusText:u.statusText,data:d}}catch(t){throw new Error(`Failed to call UnsharedLabs API: ${t instanceof Error?t.message:JSON.stringify(t)}`)}}async processUserEvent(t,e,i,a,r,s,n,c,o){try{const h=(0,util_1.encryptData)(e,this._apiKey),_=(0,util_1.encryptData)(i,this._apiKey),l=(0,util_1.encryptData)(a,this._apiKey),p=(0,util_1.encryptData)(r,this._apiKey),y=(0,util_1.encryptData)(s,this._apiKey),u=(0,util_1.encryptData)(n,this._apiKey),d={user_id:h,event_type:t,ip_address:_,device_id:l,session_hash:p,user_agent:y,email_address:u,event_details:(0,util_1.encryptData)(o?JSON.stringify([...o.entries()]):null,this._apiKey)};null!=c&&"string"==typeof c&&(d.subscription_status=c.trim());const f=await fetch(`${this._apiBaseUrl}/api/v1/processUserEvent`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:this._getBasicAuthHeader()},body:JSON.stringify(d)});return await f.json()}catch(t){return{success:!1,error:`Failed to process user event: ${t instanceof Error?t.message:JSON.stringify(t)}`}}}async checkUser(t,e){try{const i=(0,util_1.encryptData)(t,this._apiKey),a=(0,util_1.encryptData)(e,this._apiKey),r=new URLSearchParams({email_address:i||"",device_id:a||""}),s=await fetch(`${this._apiBaseUrl}/api/v1/checkUser?${r.toString()}`,{method:"GET",headers:{Authorization:this._getBasicAuthHeader()}});return await s.json()}catch(t){return{success:!1,error:`Failed to check user: ${t instanceof Error?t.message:JSON.stringify(t)}`}}}async triggerEmailVerification(t,e){try{if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t))throw new Error("Invalid email address provided");const i=(0,util_1.encryptData)(t,this._apiKey),a=(0,util_1.encryptData)(e,this._apiKey),r=await fetch(`${this._emailServiceUrl}/api/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:this._getBasicAuthHeader()},body:JSON.stringify({email_address:i,device_id:a})});if(!r.ok){const t=await r.json().catch(()=>({error:r.statusText}));throw new Error(`Email service returned error: ${JSON.stringify(t)}`)}return await r.json()}catch(t){throw new Error(`Failed to trigger email verification: ${t instanceof Error?t.message:JSON.stringify(t)}`)}}async verify(t,e,i){try{const a=(0,util_1.encryptData)(t,this._apiKey),r=(0,util_1.encryptData)(e,this._apiKey),s=(0,util_1.encryptData)(i,this._apiKey),n=await fetch(`${this._emailServiceUrl}/api/verify`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:this._getBasicAuthHeader()},body:JSON.stringify({email_address:a,device_id:r,code:s})});if(!n.ok){const t=await n.json().catch(()=>({error:n.statusText}));throw new Error(`Verification service returned error: ${JSON.stringify(t)}`)}return await n.json()}catch(t){throw new Error(`Failed to verify code: ${t instanceof Error?t.message:JSON.stringify(t)}`)}}}exports.default=UnsharedLabsClient;
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.UnsharedLabsClient=void 0;const crypto_1=require("crypto"),util_1=require("./util"),DEFAULT_BASE_URL="https://api-ingress.unsharedlabs.com",DEFAULT_TIMEOUT_MS=1e4,DEFAULT_MAX_RETRIES=3,MAX_DELAY_MS=3e4,BASE_DELAY_MS=1e3;function sleep(e){return new Promise(s=>setTimeout(s,e))}function retryDelay(e){const s=Math.min(1e3*Math.pow(2,e-1),3e4),t=s*(.5*Math.random()-.25);return Math.max(0,s+t)}async function parseErrorBody(e){const s=await e.text().catch(()=>"");try{const t=JSON.parse(s);return t?.error?.code?{code:t.error.code,message:t.error.message??"Unknown error",details:t.error.details}:{code:"UNKNOWN_ERROR",message:s||e.statusText}}catch{return{code:"UNKNOWN_ERROR",message:s||e.statusText}}}class UnsharedLabsClient{constructor(e){if(!e.apiKey||""===e.apiKey.trim())throw new Error("apiKey is required");this.i=e.apiKey,this.o=e.baseUrl?e.baseUrl.replace(/\/$/,""):DEFAULT_BASE_URL,this.h=e.timeout??1e4,this.u=e.maxRetries??3,this.l=(0,crypto_1.createHash)("sha256").update(e.apiKey).digest()}_(e){return(0,util_1.encryptData)(e,this.l)}async p(e,s){const t=this.u+1;let r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let i=1;i<=t;i++){i>1&&await sleep(retryDelay(i-1));const t=new AbortController,n=setTimeout(()=>t.abort(),this.h);try{const i=await fetch(e,{method:s.method,headers:{"X-API-Key":this.i,...s.headers},body:s.body,signal:t.signal});if(clearTimeout(n),i.ok){const e=await i.text().catch(()=>"{}");let s;try{s=JSON.parse(e)}catch{s={}}const t="data"in s?s.data:s;return{success:!0,status:i.status,data:t}}const a=await parseErrorBody(i);if(i.status>=400&&i.status<500){if(429===i.status){const e=i.headers.get("Retry-After");if(null!=e){const s=parseInt(e,10);isNaN(s)||(a.retryAfter=s)}}return{success:!1,status:i.status,error:a}}r={success:!1,status:i.status,error:a}}catch(e){clearTimeout(n),r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return r}async submitFingerprintEvent(e,s){const t={hash:e.full_hash,stable_hash:e.fingerprint_id,collected_at:e.timestamp,is_incognito:e.isIncognito,components:e.components,version:e.version};return null!=s?.userId&&(t.user_id=this._(s.userId)),null!=s?.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;