unshared-clientjs-sdk 2.3.0 → 3.0.0-rc.15
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 +349 -152
- package/dist/client.d.ts +54 -25
- package/dist/client.js +1 -1
- package/dist/esm/client.d.mts +54 -25
- package/dist/esm/client.mjs +1 -1
- package/dist/esm/index.d.mts +8 -8
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/middleware/dispatch-processing.d.mts +11 -5
- package/dist/esm/middleware/dispatch-processing.mjs +1 -1
- package/dist/esm/middleware/index.d.mts +10 -6
- package/dist/esm/middleware/index.mjs +1 -1
- package/dist/esm/middleware/injection/fingerprint-script.d.mts +9 -0
- package/dist/esm/middleware/injection/fingerprint-script.mjs +1 -1
- package/dist/esm/middleware/injection/fp-bundle-source.d.mts +5 -4
- package/dist/esm/middleware/injection/fp-bundle-source.mjs +1 -1
- package/dist/esm/middleware/response-interceptor.d.mts +1 -1
- package/dist/esm/middleware/routes/interstitial.d.mts +2 -2
- package/dist/esm/middleware/routes/interstitial.mjs +1 -1
- package/dist/esm/middleware/routes/submit-fp.d.mts +5 -5
- package/dist/esm/middleware/routes/submit-fp.mjs +1 -1
- package/dist/esm/middleware/routes/verify.d.mts +5 -4
- package/dist/esm/middleware/routes/verify.mjs +1 -1
- package/dist/esm/middleware/utils/client-ip.d.mts +1 -1
- package/dist/esm/middleware/utils/cookies.d.mts +1 -1
- package/dist/esm/middleware/utils/device-id.d.mts +5 -3
- package/dist/esm/middleware/utils/device-id.mjs +1 -1
- package/dist/esm/middleware/utils/flagged-response.mjs +1 -1
- package/dist/esm/middleware/utils/http-helpers.d.mts +1 -1
- package/dist/esm/middleware/utils/permanent-device-id.d.mts +7 -0
- package/dist/esm/middleware/utils/permanent-device-id.mjs +1 -0
- package/dist/esm/middleware/utils/read-json-body.d.mts +3 -0
- package/dist/esm/middleware/utils/read-json-body.mjs +1 -0
- package/dist/esm/middleware/utils/secure.d.mts +1 -1
- package/dist/esm/middleware.d.mts +5 -6
- package/dist/esm/middleware.mjs +1 -1
- package/dist/esm/shared-types.d.mts +148 -0
- package/dist/esm/web/index.d.mts +6 -6
- package/dist/esm/web/index.mjs +1 -1
- package/dist/esm/web/protection-handler.d.mts +5 -5
- package/dist/esm/web/protection-handler.mjs +1 -1
- package/dist/esm/web/submit-handler.d.mts +2 -2
- package/dist/esm/web/submit-handler.mjs +1 -1
- package/dist/esm/web/types.d.mts +5 -3
- package/dist/esm/web/web-helpers.d.mts +13 -1
- package/dist/esm/web/web-helpers.mjs +1 -1
- package/dist/middleware/dispatch-processing.d.ts +8 -2
- package/dist/middleware/dispatch-processing.js +1 -1
- package/dist/middleware/index.d.ts +5 -1
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/injection/fingerprint-script.d.ts +9 -0
- package/dist/middleware/injection/fingerprint-script.js +1 -1
- package/dist/middleware/injection/fp-bundle-source.d.ts +5 -4
- package/dist/middleware/injection/fp-bundle-source.js +1 -1
- package/dist/middleware/routes/submit-fp.js +1 -1
- package/dist/middleware/routes/verify.d.ts +2 -1
- package/dist/middleware/routes/verify.js +1 -1
- package/dist/middleware/utils/device-id.d.ts +4 -2
- package/dist/middleware/utils/device-id.js +1 -1
- package/dist/middleware/utils/permanent-device-id.d.ts +7 -0
- package/dist/middleware/utils/permanent-device-id.js +1 -0
- package/dist/middleware/utils/read-json-body.d.ts +3 -0
- package/dist/middleware/utils/read-json-body.js +1 -0
- package/dist/middleware.d.ts +3 -4
- package/dist/middleware.js +1 -1
- package/dist/shared-types.d.ts +148 -0
- package/dist/web/protection-handler.d.ts +1 -1
- package/dist/web/protection-handler.js +1 -1
- package/dist/web/submit-handler.js +1 -1
- package/dist/web/types.d.ts +4 -2
- package/dist/web/web-helpers.d.ts +13 -1
- package/dist/web/web-helpers.js +1 -1
- package/package.json +4 -4
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FingerprintWireFormat } from '
|
|
1
|
+
import type { CheckUserRequest, CheckUserResult as V3CheckUserResult, FingerprintWireFormat, IngestResult, JSONObject, VerifyEmailRequest, VerifyEmailResult } from './shared-types';
|
|
2
2
|
export interface UnsharedClientConfig {
|
|
3
3
|
/**
|
|
4
4
|
* Secret API key. Must be kept server-side.
|
|
@@ -6,7 +6,7 @@ export interface UnsharedClientConfig {
|
|
|
6
6
|
*/
|
|
7
7
|
apiKey: string;
|
|
8
8
|
/**
|
|
9
|
-
* Base URL of the Unshared Labs
|
|
9
|
+
* Base URL of the Unshared Labs ingress.
|
|
10
10
|
* @default "https://api.unshared.ai"
|
|
11
11
|
*/
|
|
12
12
|
baseUrl?: string;
|
|
@@ -70,14 +70,15 @@ export interface ApiResult<T = unknown> {
|
|
|
70
70
|
}
|
|
71
71
|
export interface SubmitFingerprintOptions {
|
|
72
72
|
userId?: string;
|
|
73
|
-
/** SDK encrypts before sending. */
|
|
74
73
|
emailAddress?: string;
|
|
75
74
|
sessionHash?: string;
|
|
76
75
|
eventType?: string;
|
|
77
|
-
/** SDK encrypts before sending. */
|
|
78
76
|
ipAddress?: string;
|
|
79
|
-
/** SDK encrypts before sending. */
|
|
80
77
|
userAgent?: string;
|
|
78
|
+
permanentDeviceId?: string;
|
|
79
|
+
deviceId?: string;
|
|
80
|
+
stableHash?: string;
|
|
81
|
+
fullHash?: string;
|
|
81
82
|
/**
|
|
82
83
|
* Client-supplied idempotency key, forwarded verbatim as X-Idempotency-Key.
|
|
83
84
|
* NOTE: the middleware that calls this (submit-fp.ts / protection-handler.ts)
|
|
@@ -92,25 +93,21 @@ export interface SubmitFingerprintOptions {
|
|
|
92
93
|
export interface SubmitFingerprintResult {
|
|
93
94
|
hash: string;
|
|
94
95
|
stable_hash: string;
|
|
95
|
-
collected_at: string;
|
|
96
|
-
/** Always present; set to "unknown" by the server if not provided. */
|
|
97
96
|
version: string;
|
|
97
|
+
event_id: string;
|
|
98
|
+
collected_at: string;
|
|
99
|
+
endpoint_version: 'v3';
|
|
98
100
|
}
|
|
99
101
|
export interface ProcessUserEventParams {
|
|
100
102
|
eventType: string;
|
|
101
|
-
/** SDK encrypts before sending. */
|
|
102
103
|
userId: string;
|
|
103
|
-
/** SDK encrypts before sending. */
|
|
104
104
|
ipAddress: string;
|
|
105
|
-
/** SDK encrypts before sending. */
|
|
106
105
|
deviceId: string;
|
|
107
106
|
sessionHash: string;
|
|
108
|
-
/** SDK encrypts before sending. */
|
|
109
107
|
userAgent: string;
|
|
110
|
-
/** SDK encrypts before sending. */
|
|
111
108
|
emailAddress: string;
|
|
112
|
-
/** SDK encrypts before sending. */
|
|
113
109
|
fingerprintId?: string;
|
|
110
|
+
permanentDeviceId?: string;
|
|
114
111
|
subscriptionStatus?: string | null;
|
|
115
112
|
eventDetails?: Record<string, unknown> | null;
|
|
116
113
|
}
|
|
@@ -121,6 +118,7 @@ export interface ProcessUserEventResult {
|
|
|
121
118
|
email_address: string;
|
|
122
119
|
ip_address: string;
|
|
123
120
|
device_id: string;
|
|
121
|
+
permanent_device_id?: string;
|
|
124
122
|
session_hash: string;
|
|
125
123
|
user_agent: string;
|
|
126
124
|
event_details?: string;
|
|
@@ -134,25 +132,31 @@ export interface ProcessUserEventResult {
|
|
|
134
132
|
}
|
|
135
133
|
export interface CheckUserResult {
|
|
136
134
|
is_user_flagged: boolean;
|
|
135
|
+
/** Present on authoritative v3 responses; omitted by local fail-open defaults. */
|
|
136
|
+
decision_id?: string;
|
|
137
|
+
/** Present on authoritative v3 responses; omitted by local fail-open defaults. */
|
|
138
|
+
decision_code?: V3CheckUserResult['decision_code'];
|
|
137
139
|
}
|
|
138
140
|
export interface TriggerEmailVerificationResult {
|
|
139
141
|
message: string;
|
|
142
|
+
verification_id?: string;
|
|
143
|
+
next_allowed_at?: string;
|
|
144
|
+
retry_after_seconds?: number;
|
|
140
145
|
}
|
|
141
146
|
export interface VerifyResult {
|
|
142
147
|
verified: boolean;
|
|
143
|
-
reason?: 'not_found' | 'code_mismatch' | 'code_expired';
|
|
148
|
+
reason?: 'not_found' | 'code_mismatch' | 'code_expired' | 'invalid_code';
|
|
149
|
+
verification_scope?: 'email';
|
|
144
150
|
}
|
|
145
151
|
export declare class UnsharedClient {
|
|
146
152
|
private readonly _apiKey;
|
|
147
153
|
private readonly _baseUrl;
|
|
148
154
|
private readonly _timeout;
|
|
149
155
|
private readonly _maxRetries;
|
|
150
|
-
private _encryptionKey;
|
|
151
156
|
private readonly _customFetch;
|
|
157
|
+
private _ingestionKey?;
|
|
158
|
+
private readonly _verificationIds;
|
|
152
159
|
constructor(config: UnsharedClientConfig);
|
|
153
|
-
/** Lazily derive + memoize the imported AES-GCM CryptoKey (async via WebCrypto). */
|
|
154
|
-
private _getKey;
|
|
155
|
-
private _encrypt;
|
|
156
160
|
/**
|
|
157
161
|
* Core HTTP method with retry logic.
|
|
158
162
|
* - 2xx → success result
|
|
@@ -161,22 +165,33 @@ export declare class UnsharedClient {
|
|
|
161
165
|
* - Network/timeout → retried, NETWORK_ERROR result returned
|
|
162
166
|
*/
|
|
163
167
|
private _fetch;
|
|
168
|
+
private _postIngestion;
|
|
169
|
+
/** Submit an arbitrary v3 fingerprint payload without reshaping it. */
|
|
170
|
+
submitFingerprint(payload: JSONObject, opts?: {
|
|
171
|
+
idempotencyKey?: string;
|
|
172
|
+
}): Promise<ApiResult<IngestResult>>;
|
|
164
173
|
/**
|
|
165
|
-
* Submit a browser fingerprint event
|
|
166
|
-
*
|
|
174
|
+
* Submit a browser fingerprint event using the source-compatible v2 method.
|
|
175
|
+
* The wire payload and endpoint use the v3 contract.
|
|
167
176
|
*/
|
|
168
177
|
submitFingerprintEvent(fingerprint: FingerprintWireFormat, opts?: SubmitFingerprintOptions): Promise<ApiResult<SubmitFingerprintResult>>;
|
|
169
178
|
/**
|
|
170
|
-
* Record a user event and receive
|
|
171
|
-
* Maps to: POST /
|
|
179
|
+
* Record a user event and receive the current risk decision.
|
|
180
|
+
* Maps to: POST /v3/process-user-event followed by POST /v3/check-user.
|
|
172
181
|
*/
|
|
173
182
|
processUserEvent(params: ProcessUserEventParams, overrides?: {
|
|
174
183
|
timeoutMs?: number;
|
|
175
184
|
maxRetries?: number;
|
|
176
185
|
}): Promise<ApiResult<ProcessUserEventResult>>;
|
|
186
|
+
processUserEvent(payload: JSONObject, overrides?: {
|
|
187
|
+
timeoutMs?: number;
|
|
188
|
+
maxRetries?: number;
|
|
189
|
+
}): Promise<ApiResult<IngestResult>>;
|
|
177
190
|
/**
|
|
178
191
|
* Check if a user is flagged in the naughty list.
|
|
179
|
-
* Maps to:
|
|
192
|
+
* Maps to: POST /v3/check-user
|
|
193
|
+
* Requires a nonblank email_address; user_id cannot replace it. Invalid email
|
|
194
|
+
* returns a local VALIDATION_ERROR without making a request.
|
|
180
195
|
*
|
|
181
196
|
* **Defensive default:** On any failure (network error, 4xx, 5xx, timeout) this
|
|
182
197
|
* method returns `{ success: true, data: { is_user_flagged: false } }` rather
|
|
@@ -190,19 +205,29 @@ export declare class UnsharedClient {
|
|
|
190
205
|
checkUser(emailAddress: string, opts: {
|
|
191
206
|
deviceId?: string;
|
|
192
207
|
fingerprintId?: string;
|
|
208
|
+
permanentDeviceId?: string;
|
|
209
|
+
fullHash?: string;
|
|
210
|
+
sessionHash?: string;
|
|
211
|
+
timeoutMs?: number;
|
|
212
|
+
maxRetries?: number;
|
|
213
|
+
}): Promise<ApiResult<CheckUserResult>>;
|
|
214
|
+
checkUser(request: CheckUserRequest, opts?: {
|
|
193
215
|
timeoutMs?: number;
|
|
194
216
|
maxRetries?: number;
|
|
195
217
|
}): Promise<ApiResult<CheckUserResult>>;
|
|
196
218
|
/**
|
|
197
219
|
* Send a 6-digit verification code to the user's email address.
|
|
198
|
-
* Maps to: POST /
|
|
220
|
+
* Maps to: POST /v3/trigger-email-verification
|
|
199
221
|
*/
|
|
200
222
|
triggerEmailVerification(emailAddress: string, deviceId: string, opts?: {
|
|
201
223
|
fingerprintId?: string;
|
|
224
|
+
permanentDeviceId?: string;
|
|
225
|
+
fullHash?: string;
|
|
226
|
+
sessionHash?: string;
|
|
202
227
|
}): Promise<ApiResult<TriggerEmailVerificationResult>>;
|
|
203
228
|
/**
|
|
204
229
|
* Verify a 6-digit code submitted by the user.
|
|
205
|
-
* Maps to: POST /
|
|
230
|
+
* Maps to: POST /v3/verify
|
|
206
231
|
*
|
|
207
232
|
* `result.success` reliably indicates whether verification succeeded:
|
|
208
233
|
* - `success: true` → code was correct, user is verified
|
|
@@ -219,7 +244,11 @@ export declare class UnsharedClient {
|
|
|
219
244
|
*/
|
|
220
245
|
verify(emailAddress: string, deviceId: string, code: string, opts?: {
|
|
221
246
|
fingerprintId?: string;
|
|
247
|
+
permanentDeviceId?: string;
|
|
248
|
+
verificationId?: string;
|
|
222
249
|
}): Promise<ApiResult<VerifyResult>>;
|
|
250
|
+
/** Verify a v3 email challenge without relying on process-local state. */
|
|
251
|
+
verifyEmail(request: VerifyEmailRequest): Promise<ApiResult<VerifyEmailResult>>;
|
|
223
252
|
/**
|
|
224
253
|
* Fetch the published interstitial flow for this company (secret key, server-side).
|
|
225
254
|
* Returns the projected flow definition to hand to the browser for rendering.
|
package/dist/client.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"t",{value:!0}),exports.UnsharedClient=void 0;const webcrypto_1=require("./webcrypto"),DEFAULT_BASE_URL="https://api.unshared.ai",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 UnsharedClient{constructor(e){if(this.i=null,!e.apiKey||""===e.apiKey.trim())throw new Error("apiKey is required");this.o=e.apiKey,this.l=e.baseUrl?e.baseUrl.replace(/\/$/,""):DEFAULT_BASE_URL,this.u=e.timeout??1e4,this.h=e.maxRetries??3,this.m=e.fetch}_(){return this.i||(this.i=(0,webcrypto_1.sha256Bytes)(this.o).then(webcrypto_1.importAesGcmKey)),this.i}async p(e){return(0,webcrypto_1.encryptDataWeb)(e,await this._())}async R(e,s,t){const i=(t?.maxRetries??this.h)+1,r=t?.timeoutMs??this.u;let a={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let t=1;t<=i;t++){t>1&&await sleep(retryDelay(t-1));const i=new AbortController,o=setTimeout(()=>i.abort(),r);try{const t=this.m??globalThis.fetch,r=await t(e,{method:s.method,headers:{"X-API-Key":this.o,...s.headers},body:s.body,signal:i.signal});if(r.ok){let e,s;try{e=await r.text()}catch(e){a={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}};continue}if(""===e.trim())return{success:!0,status:r.status,data:{}};try{s=JSON.parse(e)}catch{return{success:!1,status:r.status,error:{code:"PARSE_ERROR",message:"Unparseable 2xx response body"}}}const t="data"in s?s.data:s;return{success:!0,status:r.status,data:t}}const o=await parseErrorBody(r);if(r.status>=400&&r.status<500){if(429===r.status){const e=r.headers.get("Retry-After");if(null!=e){const s=parseInt(e,10);isNaN(s)||(o.retryAfter=s)}if(null==o.retryAfter){const e=o.details?.retry_after_seconds;"number"!=typeof e||isNaN(e)||(o.retryAfter=e)}}return{success:!1,status:r.status,error:o}}a={success:!1,status:r.status,error:o}}catch(e){a={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}finally{clearTimeout(o)}}return a}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};null!=e.source&&(t.source=e.source);const[i,r,a,o]=await Promise.all([null!=s?.userId?this.p(s.userId):Promise.resolve(void 0),null!=s?.emailAddress?this.p(s.emailAddress):Promise.resolve(void 0),null!=s?.ipAddress?this.p(s.ipAddress):Promise.resolve(void 0),null!=s?.userAgent?this.p(s.userAgent):Promise.resolve(void 0)]);return null!=i&&(t.user_id=i),null!=r&&(t.email_address=r),null!=s?.sessionHash&&(t.session_hash=s.sessionHash),null!=s?.eventType&&(t.event_type=s.eventType),null!=a&&(t.ip_address=a),null!=o&&(t.user_agent=o),this.R(`${this.l}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":s?.idempotencyKey??globalThis.crypto.randomUUID()},body:JSON.stringify(t)})}async processUserEvent(e,s){const[t,i,r,a,o,n]=await Promise.all([this.p(e.userId),this.p(e.ipAddress),this.p(e.deviceId),this.p(e.userAgent),this.p(e.emailAddress),null!=e.fingerprintId?this.p(e.fingerprintId):Promise.resolve(void 0)]),c={event_type:e.eventType,user_id:t,ip_address:i,device_id:r,session_hash:e.sessionHash,user_agent:a,email_address:o};return null!=n&&(c.fingerprint_id=n),null!=e.subscriptionStatus&&(c.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(c.event_details=e.eventDetails),this.R(`${this.l}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c)},{timeoutMs:s?.timeoutMs,maxRetries:s?.maxRetries??0})}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},failedOpen:{status:0,reason:"no_device_id"}};const[i,r,a]=await Promise.all([this.p(e),t.deviceId?this.p(t.deviceId):Promise.resolve(void 0),t.fingerprintId?this.p(t.fingerprintId):Promise.resolve(void 0)]),o=new URLSearchParams;o.set("email_address",i),t.deviceId&&o.set("device_id",r),t.fingerprintId&&o.set("fingerprint_id",a);const n=await this.R(`${this.l}/v2/check-user?${o}`,{method:"GET"},{timeoutMs:t.timeoutMs,maxRetries:t.maxRetries});return n.success?n:{success:!0,status:200,data:{is_user_flagged:!1},failedOpen:{status:n.status}}}async triggerEmailVerification(e,s,t){const[i,r,a]=await Promise.all([this.p(e),this.p(s),t?.fingerprintId?this.p(t.fingerprintId):Promise.resolve(void 0)]),o={email_address:i,device_id:r};t?.fingerprintId&&(o.fingerprint_id=a);const n=await this.R(`${this.l}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)},{maxRetries:0});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}async verify(e,s,t,i){const[r,a,o,n]=await Promise.all([this.p(e),this.p(s),this.p(t),i?.fingerprintId?this.p(i.fingerprintId):Promise.resolve(void 0)]),c={email_address:r,device_id:a,code:o};i?.fingerprintId&&(c.fingerprint_id=n);const l=await this.R(`${this.l}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c)},{maxRetries:0});return!l.success&&(0===l.status||l.status>=500)?{success:!1,status:l.status,error:{code:"DELIVERY_FAILED",message:l.error?.message??"Delivery failed"}}:l.success&&!0!==l.data?.verified?{success:!1,status:l.status,error:{code:"VERIFICATION_FAILED",message:"Code is incorrect or expired",details:l.data?.reason?{reason:l.data.reason}:void 0}}:l}async getInterstitialFlow(e){const s=new URLSearchParams;return s.set("flow_type",e?.flowType||"email_verification"),s.set("platform",e?.platform||"web"),this.R(`${this.l}/v2/interstitial-flow?${s}`,{method:"GET"})}}exports.UnsharedClient=UnsharedClient;
|
|
1
|
+
"use strict";Object.defineProperty(exports,"t",{value:!0}),exports.UnsharedClient=void 0;const webcrypto_1=require("./webcrypto"),DEFAULT_BASE_URL="https://api.unshared.ai",DEFAULT_TIMEOUT_MS=1e4,DEFAULT_MAX_RETRIES=3,MAX_DELAY_MS=3e4,BASE_DELAY_MS=1e3,MAX_REQUEST_BYTES=1<<20,MAX_ENCRYPTED_REQUEST_BYTES=Math.floor(1572864),SDK_VERSION="3.0.0",UUID_V4_PATTERN=/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;function isSecureOrLoopbackUrl(e){try{const s=new URL(e);return"https:"===s.protocol||"localhost"===s.hostname||"[::1]"===s.hostname||/^127\./.test(s.hostname)}catch{return!1}}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)}function asJSONObject(e){return null===e||"object"!=typeof e||Array.isArray(e)?{}:e}function isJSONObject(e,s=new WeakSet){if(null===e||"object"!=typeof e||Array.isArray(e)||s.has(e))return!1;const t=Object.getPrototypeOf(e);if(t!==Object.prototype&&null!==t)return!1;s.add(e);for(const t of Object.values(e))if(null!==t&&"string"!=typeof t&&"boolean"!=typeof t&&!("number"==typeof t&&Number.isFinite(t)||Array.isArray(t)&&isJSONValue(t,s)||isJSONObject(t,s)))return!1;return s.delete(e),!0}function isJSONValue(e,s){if(null===e||"string"==typeof e||"boolean"==typeof e)return!0;if("number"==typeof e)return Number.isFinite(e);if(Array.isArray(e)){if(s.has(e))return!1;s.add(e);let t=Object.keys(e).length===e.length;for(let r=0;t&&r<e.length;r++)t=Object.prototype.hasOwnProperty.call(e,r)&&isJSONValue(e[r],s);return s.delete(e),t}return isJSONObject(e,s)}function hasExactKeys(e,s){return null!==e&&"object"==typeof e&&!Array.isArray(e)&&(Object.keys(e).length===s.length&&s.every(s=>Object.prototype.hasOwnProperty.call(e,s)))}function isValidDateTime(e){if("string"!=typeof e)return!1;const s=/^(\d{4})-(\d{2})-(\d{2})T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.exec(e);if(!s||Number.isNaN(Date.parse(e)))return!1;const t=Number(s[1]),r=Number(s[2]),i=Number(s[3]);return r>=1&&r<=12&&i>=1&&i<=[31,t%4!=0||t%100==0&&t%400!=0?28:29,31,30,31,30,31,31,30,31,30,31][r-1]}function isIngestResult(e){return hasExactKeys(e,["event_id","collected_at","endpoint_version"])&&"string"==typeof e.event_id&&UUID_V4_PATTERN.test(e.event_id)&&isValidDateTime(e.collected_at)&&"v3"===e.endpoint_version}function isCheckUserResult(e){if(!hasExactKeys(e,["is_user_flagged","decision_id","decision_code"]))return!1;const s=new Set(["FLAGGED_RISK_MATCH","NO_ENFORCING_RISK","NO_IDENTITY_MATCH","IDENTITY_UNCERTAIN","CHECK_UNAVAILABLE"]);return"boolean"==typeof e.is_user_flagged&&"string"==typeof e.decision_id&&UUID_V4_PATTERN.test(e.decision_id)&&"string"==typeof e.decision_code&&s.has(e.decision_code)&&e.is_user_flagged===("FLAGGED_RISK_MATCH"===e.decision_code)}function isTriggerEmailResult(e){return hasExactKeys(e,["verification_id","next_allowed_at","retry_after_seconds"])&&"string"==typeof e.verification_id&&UUID_V4_PATTERN.test(e.verification_id)&&isValidDateTime(e.next_allowed_at)&&Number.isInteger(e.retry_after_seconds)&&e.retry_after_seconds>=0}function isVerifyEmailResult(e){return hasExactKeys(e,["verified","verification_scope"])?!0===e.verified&&"email"===e.verification_scope:hasExactKeys(e,["verified","verification_scope","reason"])&&!1===e.verified&&"email"===e.verification_scope&&"invalid_code"===e.reason}function compactIdentifiers(e){const s={};for(const[t,r]of Object.entries(e))"string"==typeof r&&""!==r.trim()&&(s[t]=r.trim());return s}async function parseErrorBody(e,s=!1){const t=await e.text();try{const r=JSON.parse(t);if(s)return hasExactKeys(r,["success","error"])&&!1===r.success&&hasExactKeys(r.error,["code","message"])&&"string"==typeof r.error.code&&"string"==typeof r.error.message?{code:r.error.code,message:r.error.message}:{code:"PARSE_ERROR",message:"Invalid v3 error envelope"};const i=r;return"string"==typeof i?.error?.code?{code:i.error.code,message:"string"==typeof i.error.message?i.error.message:"Unknown error",details:i.error.details}:{code:"UNKNOWN_ERROR",message:t||e.statusText}}catch{return{code:"UNKNOWN_ERROR",message:t||e.statusText}}}class UnsharedClient{constructor(e){if(this.i=new Map,!e.apiKey||""===e.apiKey.trim())throw new Error("apiKey is required");if(e.baseUrl&&!isSecureOrLoopbackUrl(e.baseUrl))throw new Error("baseUrl must be a valid HTTPS URL except for loopback development URLs");this.o=e.apiKey,this.u=e.baseUrl?e.baseUrl.replace(/\/$/,""):DEFAULT_BASE_URL,this._=e.timeout??1e4,this.l=e.maxRetries??3,this.p=e.fetch}async R(e,s,t){const r=t?.maxRequestBytes??1048576;if(s.body&&(new TextEncoder).encode(s.body).byteLength>r)return{success:!1,status:413,error:{code:"REQUEST_TOO_LARGE",message:"Request body exceeds "+(1048576===r?"1 MiB":"1.5 MiB")}};const i=(t?.maxRetries??this.l)+1,a=t?.timeoutMs??this._;let n={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let r=1;r<=i;r++){r>1&&await sleep(retryDelay(r-1));const i=new AbortController,o=setTimeout(()=>i.abort(),a);try{const r=this.p??globalThis.fetch,a=await r(e,{method:s.method,headers:{"X-API-Key":this.o,...s.headers},body:s.body,signal:i.signal});if(a.ok){let e,s;try{e=await a.text()}catch(e){n={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}};continue}if(""===e.trim())return{success:!1,status:a.status,error:{code:"PARSE_ERROR",message:"Empty v3 response body"}};try{s=JSON.parse(e)}catch{return{success:!1,status:a.status,error:{code:"PARSE_ERROR",message:"Unparseable 2xx response body"}}}return(t?.strictEnvelope?hasExactKeys(s,["success","data"])&&!0===s.success:!0===s?.success&&Object.prototype.hasOwnProperty.call(s,"data"))?void 0!==t?.expectedStatus&&a.status!==t.expectedStatus?{success:!1,status:a.status,error:{code:"PARSE_ERROR",message:`Unexpected success status ${a.status}`}}:t?.validateData&&!t.validateData(s.data)?{success:!1,status:a.status,error:{code:"PARSE_ERROR",message:"Invalid v3 response data"}}:{success:!0,status:a.status,data:s.data}:{success:!1,status:a.status,error:{code:"PARSE_ERROR",message:"Invalid v3 response envelope"}}}const o=await parseErrorBody(a,t?.strictEnvelope);if(a.status>=400&&a.status<500){if(429===a.status){const e=a.headers.get("Retry-After");if(null!=e){const s=parseInt(e,10);isNaN(s)||(o.retryAfter=s)}if(null==o.retryAfter){const e=o.details?.retry_after_seconds;"number"!=typeof e||isNaN(e)||(o.retryAfter=e)}}return{success:!1,status:a.status,error:o}}if(n={success:!1,status:a.status,error:o},t?.retryHttpError&&!t.retryHttpError(a.status,o))return n}catch(e){n={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}finally{clearTimeout(o)}}return n}async m(e,s,t){if(!isJSONObject(s))return{success:!1,status:400,error:{code:"INVALID_JSON",message:"Request body must be a JSON object"}};let r,i;try{r=JSON.stringify(s)}catch(e){return{success:!1,status:400,error:{code:"INVALID_JSON",message:e instanceof Error?e.message:String(e)}}}if((new TextEncoder).encode(r).byteLength>1048576)return{success:!1,status:413,error:{code:"REQUEST_TOO_LARGE",message:"Request body exceeds 1 MiB"}};try{this.h??(this.h=(0,webcrypto_1.sha256Bytes)(this.o).then(webcrypto_1.importAesGcmKey)),i=JSON.stringify({encrypted_data:await(0,webcrypto_1.encryptDataWeb)(r,await this.h)})}catch(e){return{success:!1,status:0,error:{code:"CLIENT_ENCRYPTION_ERROR",message:e instanceof Error?e.message:String(e)}}}return this.R(`${this.u}${e}`,{method:"POST",headers:{"Content-Type":"application/json",...t?.idempotencyKey?{"X-Idempotency-Key":t.idempotencyKey}:{}},body:i},{timeoutMs:t?.timeoutMs,maxRetries:0,maxRequestBytes:MAX_ENCRYPTED_REQUEST_BYTES,expectedStatus:202,strictEnvelope:!0,validateData:e=>isIngestResult(e)})}async submitFingerprint(e,s){return this.m("/v3/submit-fingerprint-event",e,{idempotencyKey:s?.idempotencyKey??globalThis.crypto.randomUUID()})}async submitFingerprintEvent(e,s){const t=compactIdentifiers({unshared_device_id:s?.permanentDeviceId,device_id:s?.deviceId,stable_hash:s?.stableHash??e.fingerprint_id,full_hash:s?.fullHash??e.full_hash,session_hash:s?.sessionHash}),r={fingerprint:asJSONObject(e),context:{sdk:{name:"unshared-clientjs-sdk",version:"3.0.0",runtime:"node"},identity:{...s?.userId?{user_id:s.userId}:{},...s?.emailAddress?{email_address:s.emailAddress}:{}},identifiers:t,event:{...s?.eventType?{event_type:s.eventType}:{},...s?.ipAddress?{ip_address:s.ipAddress}:{},...s?.userAgent?{user_agent:s.userAgent}:{}}}},i=await this.m("/v3/submit-fingerprint-event",r,{idempotencyKey:s?.idempotencyKey??globalThis.crypto.randomUUID()});return i.success?i.data?{success:!0,status:i.status,data:{...i.data,hash:e.full_hash,stable_hash:e.fingerprint_id,version:e.version??"unknown"}}:{success:!1,status:i.status,error:{code:"PARSE_ERROR",message:"Invalid v3 ingestion result"}}:{success:!1,status:i.status,error:i.error}}async processUserEvent(e,s){if(null===e||"object"!=typeof e||Array.isArray(e))return{success:!1,status:400,error:{code:"INVALID_JSON",message:"Request body must be a JSON object"}};const t=new Set(["eventType","userId","ipAddress","deviceId","sessionHash","userAgent","emailAddress","fingerprintId","permanentDeviceId","subscriptionStatus","eventDetails"]);if(!Object.keys(e).every(e=>t.has(e))||"string"!=typeof e.eventType||"string"!=typeof e.userId||"string"!=typeof e.emailAddress||"string"!=typeof e.deviceId||"string"!=typeof e.sessionHash)return this.m("/v3/process-user-event",e,{timeoutMs:s?.timeoutMs});const r=e,i=compactIdentifiers({unshared_device_id:r.permanentDeviceId,device_id:r.deviceId,stable_hash:r.fingerprintId,session_hash:r.sessionHash}),a={event_type:r.eventType,identity:{user_id:r.userId,email_address:r.emailAddress},identifiers:i,context:{ip_address:r.ipAddress,user_agent:r.userAgent,...r.subscriptionStatus?{subscription_status:r.subscriptionStatus}:{}},...r.eventDetails?{event_details:asJSONObject(r.eventDetails)}:{}},n=await this.m("/v3/process-user-event",a,{timeoutMs:s?.timeoutMs});if(!n.success)return{success:!1,status:n.status,error:n.error};const o=await this.checkUser({user_id:r.userId,email_address:r.emailAddress,identifiers:i},{timeoutMs:s?.timeoutMs,maxRetries:s?.maxRetries});return o.success?{success:!0,status:o.status,data:{event:{event_type:r.eventType,user_id:r.userId,email_address:r.emailAddress,ip_address:r.ipAddress,device_id:r.deviceId,...r.permanentDeviceId?{permanent_device_id:r.permanentDeviceId}:{},session_hash:r.sessionHash,user_agent:r.userAgent,...r.subscriptionStatus?{subscription_status:r.subscriptionStatus}:{},...r.eventDetails?{event_details:JSON.stringify(r.eventDetails)}:{}},analysis:{status:o.failedOpen?"error":"success",is_user_flagged:o.data.is_user_flagged,...o.data.decision_code?{status_details:o.data.decision_code}:{}}},...o.failedOpen?{failedOpen:o.failedOpen}:{}}:{success:!1,status:o.status,error:o.error}}async checkUser(e,s={}){const t="string"==typeof s?{deviceId:s}:s,r="string"==typeof e?{email_address:e,identifiers:compactIdentifiers({unshared_device_id:t.permanentDeviceId,device_id:t.deviceId,stable_hash:t.fingerprintId,full_hash:t.fullHash,session_hash:t.sessionHash})}:e;if("string"!=typeof r.email_address||""===r.email_address.trim())return{success:!1,status:400,error:{code:"VALIDATION_ERROR",message:"email_address must be a non-empty string"}};if(0===Object.keys(r.identifiers).length)return{success:!0,status:200,data:{is_user_flagged:!1},failedOpen:{status:0,reason:"no_device_id"}};const i=await this.R(`${this.u}/v3/check-user`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)},{timeoutMs:t.timeoutMs,maxRetries:t.maxRetries,expectedStatus:200,strictEnvelope:!0,validateData:isCheckUserResult,retryHttpError:(e,s)=>"RATE_LIMIT_UNAVAILABLE"===s.code||"AUDIT_UNAVAILABLE"===s.code});return i.success?"CHECK_UNAVAILABLE"===i.data?.decision_code?{...i,failedOpen:{status:i.status}}:i:{success:!0,status:200,data:{is_user_flagged:!1},failedOpen:{status:i.status}}}async triggerEmailVerification(e,s,t){if("string"!=typeof e||""===e.trim())return{success:!1,status:400,error:{code:"VALIDATION_ERROR",message:"emailAddress must be a non-empty string"}};const r={email_address:e,identifiers:compactIdentifiers({unshared_device_id:t?.permanentDeviceId,device_id:s,stable_hash:t?.fingerprintId,full_hash:t?.fullHash,session_hash:t?.sessionHash})};if(0===Object.keys(r.identifiers).length)return{success:!1,status:400,error:{code:"VALIDATION_ERROR",message:"At least one non-empty identifier is required"}};const i=await this.R(`${this.u}/v3/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)},{maxRetries:0,expectedStatus:200,strictEnvelope:!0,validateData:isTriggerEmailResult});return i.success?(this.i.set(e.trim().toLowerCase(),i.data.verification_id),{...i,data:{...i.data,message:"Verification code sent"}}):!i.success&&(0===i.status||i.status>=500)?{success:!1,status:i.status,error:{code:"DELIVERY_FAILED",message:i.error?.message??"Delivery failed"}}:{success:!1,status:i.status,error:i.error??{code:"PARSE_ERROR",message:"Missing v3 verification response"}}}async verify(e,s,t,r){const i=r?.verificationId??this.i.get(e.trim().toLowerCase());if(!i)return{success:!1,status:400,error:{code:"MISSING_VERIFICATION_ID",message:"Trigger email verification before verifying the code"}};if(!UUID_V4_PATTERN.test(i)||!/^\d{6}$/.test(t))return{success:!1,status:400,error:{code:"VALIDATION_ERROR",message:"verificationId must be a UUIDv4 and code must be exactly 6 digits"}};const a={verification_id:i,code:t},n=await this.R(`${this.u}/v3/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)},{maxRetries:0,expectedStatus:200,strictEnvelope:!0,validateData:isVerifyEmailResult});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?!0!==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}}:(this.i.delete(e.trim().toLowerCase()),n):n}async verifyEmail(e){return e&&UUID_V4_PATTERN.test(e.verification_id)&&/^\d{6}$/.test(e.code)?this.R(`${this.u}/v3/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)},{maxRetries:0,expectedStatus:200,strictEnvelope:!0,validateData:isVerifyEmailResult}):{success:!1,status:400,error:{code:"VALIDATION_ERROR",message:"verification_id must be a UUIDv4 and code must be exactly 6 digits"}}}async getInterstitialFlow(e){const s=new URLSearchParams;return s.set("flow_type",e?.flowType||"email_verification"),s.set("platform",e?.platform||"web"),this.R(`${this.u}/v2/interstitial-flow?${s}`,{method:"GET"})}}exports.UnsharedClient=UnsharedClient;
|
package/dist/esm/client.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FingerprintWireFormat } from '
|
|
1
|
+
import type { CheckUserRequest, CheckUserResult as V3CheckUserResult, FingerprintWireFormat, IngestResult, JSONObject, VerifyEmailRequest, VerifyEmailResult } from './shared-types.mjs';
|
|
2
2
|
export interface UnsharedClientConfig {
|
|
3
3
|
/**
|
|
4
4
|
* Secret API key. Must be kept server-side.
|
|
@@ -6,7 +6,7 @@ export interface UnsharedClientConfig {
|
|
|
6
6
|
*/
|
|
7
7
|
apiKey: string;
|
|
8
8
|
/**
|
|
9
|
-
* Base URL of the Unshared Labs
|
|
9
|
+
* Base URL of the Unshared Labs ingress.
|
|
10
10
|
* @default "https://api.unshared.ai"
|
|
11
11
|
*/
|
|
12
12
|
baseUrl?: string;
|
|
@@ -70,14 +70,15 @@ export interface ApiResult<T = unknown> {
|
|
|
70
70
|
}
|
|
71
71
|
export interface SubmitFingerprintOptions {
|
|
72
72
|
userId?: string;
|
|
73
|
-
/** SDK encrypts before sending. */
|
|
74
73
|
emailAddress?: string;
|
|
75
74
|
sessionHash?: string;
|
|
76
75
|
eventType?: string;
|
|
77
|
-
/** SDK encrypts before sending. */
|
|
78
76
|
ipAddress?: string;
|
|
79
|
-
/** SDK encrypts before sending. */
|
|
80
77
|
userAgent?: string;
|
|
78
|
+
permanentDeviceId?: string;
|
|
79
|
+
deviceId?: string;
|
|
80
|
+
stableHash?: string;
|
|
81
|
+
fullHash?: string;
|
|
81
82
|
/**
|
|
82
83
|
* Client-supplied idempotency key, forwarded verbatim as X-Idempotency-Key.
|
|
83
84
|
* NOTE: the middleware that calls this (submit-fp.ts / protection-handler.ts)
|
|
@@ -92,25 +93,21 @@ export interface SubmitFingerprintOptions {
|
|
|
92
93
|
export interface SubmitFingerprintResult {
|
|
93
94
|
hash: string;
|
|
94
95
|
stable_hash: string;
|
|
95
|
-
collected_at: string;
|
|
96
|
-
/** Always present; set to "unknown" by the server if not provided. */
|
|
97
96
|
version: string;
|
|
97
|
+
event_id: string;
|
|
98
|
+
collected_at: string;
|
|
99
|
+
endpoint_version: 'v3';
|
|
98
100
|
}
|
|
99
101
|
export interface ProcessUserEventParams {
|
|
100
102
|
eventType: string;
|
|
101
|
-
/** SDK encrypts before sending. */
|
|
102
103
|
userId: string;
|
|
103
|
-
/** SDK encrypts before sending. */
|
|
104
104
|
ipAddress: string;
|
|
105
|
-
/** SDK encrypts before sending. */
|
|
106
105
|
deviceId: string;
|
|
107
106
|
sessionHash: string;
|
|
108
|
-
/** SDK encrypts before sending. */
|
|
109
107
|
userAgent: string;
|
|
110
|
-
/** SDK encrypts before sending. */
|
|
111
108
|
emailAddress: string;
|
|
112
|
-
/** SDK encrypts before sending. */
|
|
113
109
|
fingerprintId?: string;
|
|
110
|
+
permanentDeviceId?: string;
|
|
114
111
|
subscriptionStatus?: string | null;
|
|
115
112
|
eventDetails?: Record<string, unknown> | null;
|
|
116
113
|
}
|
|
@@ -121,6 +118,7 @@ export interface ProcessUserEventResult {
|
|
|
121
118
|
email_address: string;
|
|
122
119
|
ip_address: string;
|
|
123
120
|
device_id: string;
|
|
121
|
+
permanent_device_id?: string;
|
|
124
122
|
session_hash: string;
|
|
125
123
|
user_agent: string;
|
|
126
124
|
event_details?: string;
|
|
@@ -134,25 +132,31 @@ export interface ProcessUserEventResult {
|
|
|
134
132
|
}
|
|
135
133
|
export interface CheckUserResult {
|
|
136
134
|
is_user_flagged: boolean;
|
|
135
|
+
/** Present on authoritative v3 responses; omitted by local fail-open defaults. */
|
|
136
|
+
decision_id?: string;
|
|
137
|
+
/** Present on authoritative v3 responses; omitted by local fail-open defaults. */
|
|
138
|
+
decision_code?: V3CheckUserResult['decision_code'];
|
|
137
139
|
}
|
|
138
140
|
export interface TriggerEmailVerificationResult {
|
|
139
141
|
message: string;
|
|
142
|
+
verification_id?: string;
|
|
143
|
+
next_allowed_at?: string;
|
|
144
|
+
retry_after_seconds?: number;
|
|
140
145
|
}
|
|
141
146
|
export interface VerifyResult {
|
|
142
147
|
verified: boolean;
|
|
143
|
-
reason?: 'not_found' | 'code_mismatch' | 'code_expired';
|
|
148
|
+
reason?: 'not_found' | 'code_mismatch' | 'code_expired' | 'invalid_code';
|
|
149
|
+
verification_scope?: 'email';
|
|
144
150
|
}
|
|
145
151
|
export declare class UnsharedClient {
|
|
146
152
|
private readonly _apiKey;
|
|
147
153
|
private readonly _baseUrl;
|
|
148
154
|
private readonly _timeout;
|
|
149
155
|
private readonly _maxRetries;
|
|
150
|
-
private _encryptionKey;
|
|
151
156
|
private readonly _customFetch;
|
|
157
|
+
private _ingestionKey?;
|
|
158
|
+
private readonly _verificationIds;
|
|
152
159
|
constructor(config: UnsharedClientConfig);
|
|
153
|
-
/** Lazily derive + memoize the imported AES-GCM CryptoKey (async via WebCrypto). */
|
|
154
|
-
private _getKey;
|
|
155
|
-
private _encrypt;
|
|
156
160
|
/**
|
|
157
161
|
* Core HTTP method with retry logic.
|
|
158
162
|
* - 2xx → success result
|
|
@@ -161,22 +165,33 @@ export declare class UnsharedClient {
|
|
|
161
165
|
* - Network/timeout → retried, NETWORK_ERROR result returned
|
|
162
166
|
*/
|
|
163
167
|
private _fetch;
|
|
168
|
+
private _postIngestion;
|
|
169
|
+
/** Submit an arbitrary v3 fingerprint payload without reshaping it. */
|
|
170
|
+
submitFingerprint(payload: JSONObject, opts?: {
|
|
171
|
+
idempotencyKey?: string;
|
|
172
|
+
}): Promise<ApiResult<IngestResult>>;
|
|
164
173
|
/**
|
|
165
|
-
* Submit a browser fingerprint event
|
|
166
|
-
*
|
|
174
|
+
* Submit a browser fingerprint event using the source-compatible v2 method.
|
|
175
|
+
* The wire payload and endpoint use the v3 contract.
|
|
167
176
|
*/
|
|
168
177
|
submitFingerprintEvent(fingerprint: FingerprintWireFormat, opts?: SubmitFingerprintOptions): Promise<ApiResult<SubmitFingerprintResult>>;
|
|
169
178
|
/**
|
|
170
|
-
* Record a user event and receive
|
|
171
|
-
* Maps to: POST /
|
|
179
|
+
* Record a user event and receive the current risk decision.
|
|
180
|
+
* Maps to: POST /v3/process-user-event followed by POST /v3/check-user.
|
|
172
181
|
*/
|
|
173
182
|
processUserEvent(params: ProcessUserEventParams, overrides?: {
|
|
174
183
|
timeoutMs?: number;
|
|
175
184
|
maxRetries?: number;
|
|
176
185
|
}): Promise<ApiResult<ProcessUserEventResult>>;
|
|
186
|
+
processUserEvent(payload: JSONObject, overrides?: {
|
|
187
|
+
timeoutMs?: number;
|
|
188
|
+
maxRetries?: number;
|
|
189
|
+
}): Promise<ApiResult<IngestResult>>;
|
|
177
190
|
/**
|
|
178
191
|
* Check if a user is flagged in the naughty list.
|
|
179
|
-
* Maps to:
|
|
192
|
+
* Maps to: POST /v3/check-user
|
|
193
|
+
* Requires a nonblank email_address; user_id cannot replace it. Invalid email
|
|
194
|
+
* returns a local VALIDATION_ERROR without making a request.
|
|
180
195
|
*
|
|
181
196
|
* **Defensive default:** On any failure (network error, 4xx, 5xx, timeout) this
|
|
182
197
|
* method returns `{ success: true, data: { is_user_flagged: false } }` rather
|
|
@@ -190,19 +205,29 @@ export declare class UnsharedClient {
|
|
|
190
205
|
checkUser(emailAddress: string, opts: {
|
|
191
206
|
deviceId?: string;
|
|
192
207
|
fingerprintId?: string;
|
|
208
|
+
permanentDeviceId?: string;
|
|
209
|
+
fullHash?: string;
|
|
210
|
+
sessionHash?: string;
|
|
211
|
+
timeoutMs?: number;
|
|
212
|
+
maxRetries?: number;
|
|
213
|
+
}): Promise<ApiResult<CheckUserResult>>;
|
|
214
|
+
checkUser(request: CheckUserRequest, opts?: {
|
|
193
215
|
timeoutMs?: number;
|
|
194
216
|
maxRetries?: number;
|
|
195
217
|
}): Promise<ApiResult<CheckUserResult>>;
|
|
196
218
|
/**
|
|
197
219
|
* Send a 6-digit verification code to the user's email address.
|
|
198
|
-
* Maps to: POST /
|
|
220
|
+
* Maps to: POST /v3/trigger-email-verification
|
|
199
221
|
*/
|
|
200
222
|
triggerEmailVerification(emailAddress: string, deviceId: string, opts?: {
|
|
201
223
|
fingerprintId?: string;
|
|
224
|
+
permanentDeviceId?: string;
|
|
225
|
+
fullHash?: string;
|
|
226
|
+
sessionHash?: string;
|
|
202
227
|
}): Promise<ApiResult<TriggerEmailVerificationResult>>;
|
|
203
228
|
/**
|
|
204
229
|
* Verify a 6-digit code submitted by the user.
|
|
205
|
-
* Maps to: POST /
|
|
230
|
+
* Maps to: POST /v3/verify
|
|
206
231
|
*
|
|
207
232
|
* `result.success` reliably indicates whether verification succeeded:
|
|
208
233
|
* - `success: true` → code was correct, user is verified
|
|
@@ -219,7 +244,11 @@ export declare class UnsharedClient {
|
|
|
219
244
|
*/
|
|
220
245
|
verify(emailAddress: string, deviceId: string, code: string, opts?: {
|
|
221
246
|
fingerprintId?: string;
|
|
247
|
+
permanentDeviceId?: string;
|
|
248
|
+
verificationId?: string;
|
|
222
249
|
}): Promise<ApiResult<VerifyResult>>;
|
|
250
|
+
/** Verify a v3 email challenge without relying on process-local state. */
|
|
251
|
+
verifyEmail(request: VerifyEmailRequest): Promise<ApiResult<VerifyEmailResult>>;
|
|
223
252
|
/**
|
|
224
253
|
* Fetch the published interstitial flow for this company (secret key, server-side).
|
|
225
254
|
* Returns the projected flow definition to hand to the browser for rendering.
|
package/dist/esm/client.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{encryptDataWeb,sha256Bytes,importAesGcmKey}from"./webcrypto";const DEFAULT_BASE_URL="https://api.unshared.ai",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 UnsharedClient{constructor(e){if(this.t=null,!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.l=e.timeout??1e4,this.h=e.maxRetries??3,this.u=e.fetch}m(){return this.t||(this.t=sha256Bytes(this.i).then(importAesGcmKey)),this.t}async _(e){return encryptDataWeb(e,await this.m())}async p(e,s,t){const i=(t?.maxRetries??this.h)+1,r=t?.timeoutMs??this.l;let a={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let t=1;t<=i;t++){t>1&&await sleep(retryDelay(t-1));const i=new AbortController,o=setTimeout(()=>i.abort(),r);try{const t=this.u??globalThis.fetch,r=await t(e,{method:s.method,headers:{"X-API-Key":this.i,...s.headers},body:s.body,signal:i.signal});if(r.ok){let e,s;try{e=await r.text()}catch(e){a={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}};continue}if(""===e.trim())return{success:!0,status:r.status,data:{}};try{s=JSON.parse(e)}catch{return{success:!1,status:r.status,error:{code:"PARSE_ERROR",message:"Unparseable 2xx response body"}}}const t="data"in s?s.data:s;return{success:!0,status:r.status,data:t}}const o=await parseErrorBody(r);if(r.status>=400&&r.status<500){if(429===r.status){const e=r.headers.get("Retry-After");if(null!=e){const s=parseInt(e,10);isNaN(s)||(o.retryAfter=s)}if(null==o.retryAfter){const e=o.details?.retry_after_seconds;"number"!=typeof e||isNaN(e)||(o.retryAfter=e)}}return{success:!1,status:r.status,error:o}}a={success:!1,status:r.status,error:o}}catch(e){a={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}finally{clearTimeout(o)}}return a}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};null!=e.source&&(t.source=e.source);const[i,r,a,o]=await Promise.all([null!=s?.userId?this._(s.userId):Promise.resolve(void 0),null!=s?.emailAddress?this._(s.emailAddress):Promise.resolve(void 0),null!=s?.ipAddress?this._(s.ipAddress):Promise.resolve(void 0),null!=s?.userAgent?this._(s.userAgent):Promise.resolve(void 0)]);return null!=i&&(t.user_id=i),null!=r&&(t.email_address=r),null!=s?.sessionHash&&(t.session_hash=s.sessionHash),null!=s?.eventType&&(t.event_type=s.eventType),null!=a&&(t.ip_address=a),null!=o&&(t.user_agent=o),this.p(`${this.o}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":s?.idempotencyKey??globalThis.crypto.randomUUID()},body:JSON.stringify(t)})}async processUserEvent(e,s){const[t,i,r,a,o,n]=await Promise.all([this._(e.userId),this._(e.ipAddress),this._(e.deviceId),this._(e.userAgent),this._(e.emailAddress),null!=e.fingerprintId?this._(e.fingerprintId):Promise.resolve(void 0)]),c={event_type:e.eventType,user_id:t,ip_address:i,device_id:r,session_hash:e.sessionHash,user_agent:a,email_address:o};return null!=n&&(c.fingerprint_id=n),null!=e.subscriptionStatus&&(c.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(c.event_details=e.eventDetails),this.p(`${this.o}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c)},{timeoutMs:s?.timeoutMs,maxRetries:s?.maxRetries??0})}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},failedOpen:{status:0,reason:"no_device_id"}};const[i,r,a]=await Promise.all([this._(e),t.deviceId?this._(t.deviceId):Promise.resolve(void 0),t.fingerprintId?this._(t.fingerprintId):Promise.resolve(void 0)]),o=new URLSearchParams;o.set("email_address",i),t.deviceId&&o.set("device_id",r),t.fingerprintId&&o.set("fingerprint_id",a);const n=await this.p(`${this.o}/v2/check-user?${o}`,{method:"GET"},{timeoutMs:t.timeoutMs,maxRetries:t.maxRetries});return n.success?n:{success:!0,status:200,data:{is_user_flagged:!1},failedOpen:{status:n.status}}}async triggerEmailVerification(e,s,t){const[i,r,a]=await Promise.all([this._(e),this._(s),t?.fingerprintId?this._(t.fingerprintId):Promise.resolve(void 0)]),o={email_address:i,device_id:r};t?.fingerprintId&&(o.fingerprint_id=a);const n=await this.p(`${this.o}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)},{maxRetries:0});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}async verify(e,s,t,i){const[r,a,o,n]=await Promise.all([this._(e),this._(s),this._(t),i?.fingerprintId?this._(i.fingerprintId):Promise.resolve(void 0)]),c={email_address:r,device_id:a,code:o};i?.fingerprintId&&(c.fingerprint_id=n);const l=await this.p(`${this.o}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c)},{maxRetries:0});return!l.success&&(0===l.status||l.status>=500)?{success:!1,status:l.status,error:{code:"DELIVERY_FAILED",message:l.error?.message??"Delivery failed"}}:l.success&&!0!==l.data?.verified?{success:!1,status:l.status,error:{code:"VERIFICATION_FAILED",message:"Code is incorrect or expired",details:l.data?.reason?{reason:l.data.reason}:void 0}}:l}async getInterstitialFlow(e){const s=new URLSearchParams;return s.set("flow_type",e?.flowType||"email_verification"),s.set("platform",e?.platform||"web"),this.p(`${this.o}/v2/interstitial-flow?${s}`,{method:"GET"})}}
|
|
1
|
+
import{encryptDataWeb,importAesGcmKey,sha256Bytes}from"./webcrypto.mjs";const DEFAULT_BASE_URL="https://api.unshared.ai",DEFAULT_TIMEOUT_MS=1e4,DEFAULT_MAX_RETRIES=3,MAX_DELAY_MS=3e4,BASE_DELAY_MS=1e3,MAX_REQUEST_BYTES=1<<20,MAX_ENCRYPTED_REQUEST_BYTES=Math.floor(1572864),SDK_VERSION="3.0.0",UUID_V4_PATTERN=/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;function isSecureOrLoopbackUrl(e){try{const s=new URL(e);return"https:"===s.protocol||"localhost"===s.hostname||"[::1]"===s.hostname||/^127\./.test(s.hostname)}catch{return!1}}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)}function asJSONObject(e){return null===e||"object"!=typeof e||Array.isArray(e)?{}:e}function isJSONObject(e,s=new WeakSet){if(null===e||"object"!=typeof e||Array.isArray(e)||s.has(e))return!1;const t=Object.getPrototypeOf(e);if(t!==Object.prototype&&null!==t)return!1;s.add(e);for(const t of Object.values(e))if(null!==t&&"string"!=typeof t&&"boolean"!=typeof t&&!("number"==typeof t&&Number.isFinite(t)||Array.isArray(t)&&isJSONValue(t,s)||isJSONObject(t,s)))return!1;return s.delete(e),!0}function isJSONValue(e,s){if(null===e||"string"==typeof e||"boolean"==typeof e)return!0;if("number"==typeof e)return Number.isFinite(e);if(Array.isArray(e)){if(s.has(e))return!1;s.add(e);let t=Object.keys(e).length===e.length;for(let r=0;t&&r<e.length;r++)t=Object.prototype.hasOwnProperty.call(e,r)&&isJSONValue(e[r],s);return s.delete(e),t}return isJSONObject(e,s)}function hasExactKeys(e,s){return null!==e&&"object"==typeof e&&!Array.isArray(e)&&(Object.keys(e).length===s.length&&s.every(s=>Object.prototype.hasOwnProperty.call(e,s)))}function isValidDateTime(e){if("string"!=typeof e)return!1;const s=/^(\d{4})-(\d{2})-(\d{2})T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.exec(e);if(!s||Number.isNaN(Date.parse(e)))return!1;const t=Number(s[1]),r=Number(s[2]),i=Number(s[3]);return r>=1&&r<=12&&i>=1&&i<=[31,t%4!=0||t%100==0&&t%400!=0?28:29,31,30,31,30,31,31,30,31,30,31][r-1]}function isIngestResult(e){return hasExactKeys(e,["event_id","collected_at","endpoint_version"])&&"string"==typeof e.event_id&&UUID_V4_PATTERN.test(e.event_id)&&isValidDateTime(e.collected_at)&&"v3"===e.endpoint_version}function isCheckUserResult(e){if(!hasExactKeys(e,["is_user_flagged","decision_id","decision_code"]))return!1;const s=new Set(["FLAGGED_RISK_MATCH","NO_ENFORCING_RISK","NO_IDENTITY_MATCH","IDENTITY_UNCERTAIN","CHECK_UNAVAILABLE"]);return"boolean"==typeof e.is_user_flagged&&"string"==typeof e.decision_id&&UUID_V4_PATTERN.test(e.decision_id)&&"string"==typeof e.decision_code&&s.has(e.decision_code)&&e.is_user_flagged===("FLAGGED_RISK_MATCH"===e.decision_code)}function isTriggerEmailResult(e){return hasExactKeys(e,["verification_id","next_allowed_at","retry_after_seconds"])&&"string"==typeof e.verification_id&&UUID_V4_PATTERN.test(e.verification_id)&&isValidDateTime(e.next_allowed_at)&&Number.isInteger(e.retry_after_seconds)&&e.retry_after_seconds>=0}function isVerifyEmailResult(e){return hasExactKeys(e,["verified","verification_scope"])?!0===e.verified&&"email"===e.verification_scope:hasExactKeys(e,["verified","verification_scope","reason"])&&!1===e.verified&&"email"===e.verification_scope&&"invalid_code"===e.reason}function compactIdentifiers(e){const s={};for(const[t,r]of Object.entries(e))"string"==typeof r&&""!==r.trim()&&(s[t]=r.trim());return s}async function parseErrorBody(e,s=!1){const t=await e.text();try{const r=JSON.parse(t);if(s)return hasExactKeys(r,["success","error"])&&!1===r.success&&hasExactKeys(r.error,["code","message"])&&"string"==typeof r.error.code&&"string"==typeof r.error.message?{code:r.error.code,message:r.error.message}:{code:"PARSE_ERROR",message:"Invalid v3 error envelope"};const i=r;return"string"==typeof i?.error?.code?{code:i.error.code,message:"string"==typeof i.error.message?i.error.message:"Unknown error",details:i.error.details}:{code:"UNKNOWN_ERROR",message:t||e.statusText}}catch{return{code:"UNKNOWN_ERROR",message:t||e.statusText}}}export class UnsharedClient{constructor(e){if(this.t=new Map,!e.apiKey||""===e.apiKey.trim())throw new Error("apiKey is required");if(e.baseUrl&&!isSecureOrLoopbackUrl(e.baseUrl))throw new Error("baseUrl must be a valid HTTPS URL except for loopback development URLs");this.i=e.apiKey,this.o=e.baseUrl?e.baseUrl.replace(/\/$/,""):DEFAULT_BASE_URL,this.u=e.timeout??1e4,this._=e.maxRetries??3,this.l=e.fetch}async p(e,s,t){const r=t?.maxRequestBytes??1048576;if(s.body&&(new TextEncoder).encode(s.body).byteLength>r)return{success:!1,status:413,error:{code:"REQUEST_TOO_LARGE",message:"Request body exceeds "+(1048576===r?"1 MiB":"1.5 MiB")}};const i=(t?.maxRetries??this._)+1,a=t?.timeoutMs??this.u;let n={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let r=1;r<=i;r++){r>1&&await sleep(retryDelay(r-1));const i=new AbortController,o=setTimeout(()=>i.abort(),a);try{const r=this.l??globalThis.fetch,a=await r(e,{method:s.method,headers:{"X-API-Key":this.i,...s.headers},body:s.body,signal:i.signal});if(a.ok){let e,s;try{e=await a.text()}catch(e){n={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}};continue}if(""===e.trim())return{success:!1,status:a.status,error:{code:"PARSE_ERROR",message:"Empty v3 response body"}};try{s=JSON.parse(e)}catch{return{success:!1,status:a.status,error:{code:"PARSE_ERROR",message:"Unparseable 2xx response body"}}}return(t?.strictEnvelope?hasExactKeys(s,["success","data"])&&!0===s.success:!0===s?.success&&Object.prototype.hasOwnProperty.call(s,"data"))?void 0!==t?.expectedStatus&&a.status!==t.expectedStatus?{success:!1,status:a.status,error:{code:"PARSE_ERROR",message:`Unexpected success status ${a.status}`}}:t?.validateData&&!t.validateData(s.data)?{success:!1,status:a.status,error:{code:"PARSE_ERROR",message:"Invalid v3 response data"}}:{success:!0,status:a.status,data:s.data}:{success:!1,status:a.status,error:{code:"PARSE_ERROR",message:"Invalid v3 response envelope"}}}const o=await parseErrorBody(a,t?.strictEnvelope);if(a.status>=400&&a.status<500){if(429===a.status){const e=a.headers.get("Retry-After");if(null!=e){const s=parseInt(e,10);isNaN(s)||(o.retryAfter=s)}if(null==o.retryAfter){const e=o.details?.retry_after_seconds;"number"!=typeof e||isNaN(e)||(o.retryAfter=e)}}return{success:!1,status:a.status,error:o}}if(n={success:!1,status:a.status,error:o},t?.retryHttpError&&!t.retryHttpError(a.status,o))return n}catch(e){n={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}finally{clearTimeout(o)}}return n}async m(e,s,t){if(!isJSONObject(s))return{success:!1,status:400,error:{code:"INVALID_JSON",message:"Request body must be a JSON object"}};let r,i;try{r=JSON.stringify(s)}catch(e){return{success:!1,status:400,error:{code:"INVALID_JSON",message:e instanceof Error?e.message:String(e)}}}if((new TextEncoder).encode(r).byteLength>1048576)return{success:!1,status:413,error:{code:"REQUEST_TOO_LARGE",message:"Request body exceeds 1 MiB"}};try{this.R??(this.R=sha256Bytes(this.i).then(importAesGcmKey)),i=JSON.stringify({encrypted_data:await encryptDataWeb(r,await this.R)})}catch(e){return{success:!1,status:0,error:{code:"CLIENT_ENCRYPTION_ERROR",message:e instanceof Error?e.message:String(e)}}}return this.p(`${this.o}${e}`,{method:"POST",headers:{"Content-Type":"application/json",...t?.idempotencyKey?{"X-Idempotency-Key":t.idempotencyKey}:{}},body:i},{timeoutMs:t?.timeoutMs,maxRetries:0,maxRequestBytes:MAX_ENCRYPTED_REQUEST_BYTES,expectedStatus:202,strictEnvelope:!0,validateData:e=>isIngestResult(e)})}async submitFingerprint(e,s){return this.m("/v3/submit-fingerprint-event",e,{idempotencyKey:s?.idempotencyKey??globalThis.crypto.randomUUID()})}async submitFingerprintEvent(e,s){const t=compactIdentifiers({unshared_device_id:s?.permanentDeviceId,device_id:s?.deviceId,stable_hash:s?.stableHash??e.fingerprint_id,full_hash:s?.fullHash??e.full_hash,session_hash:s?.sessionHash}),r={fingerprint:asJSONObject(e),context:{sdk:{name:"unshared-clientjs-sdk",version:"3.0.0",runtime:"node"},identity:{...s?.userId?{user_id:s.userId}:{},...s?.emailAddress?{email_address:s.emailAddress}:{}},identifiers:t,event:{...s?.eventType?{event_type:s.eventType}:{},...s?.ipAddress?{ip_address:s.ipAddress}:{},...s?.userAgent?{user_agent:s.userAgent}:{}}}},i=await this.m("/v3/submit-fingerprint-event",r,{idempotencyKey:s?.idempotencyKey??globalThis.crypto.randomUUID()});return i.success?i.data?{success:!0,status:i.status,data:{...i.data,hash:e.full_hash,stable_hash:e.fingerprint_id,version:e.version??"unknown"}}:{success:!1,status:i.status,error:{code:"PARSE_ERROR",message:"Invalid v3 ingestion result"}}:{success:!1,status:i.status,error:i.error}}async processUserEvent(e,s){if(null===e||"object"!=typeof e||Array.isArray(e))return{success:!1,status:400,error:{code:"INVALID_JSON",message:"Request body must be a JSON object"}};const t=new Set(["eventType","userId","ipAddress","deviceId","sessionHash","userAgent","emailAddress","fingerprintId","permanentDeviceId","subscriptionStatus","eventDetails"]);if(!Object.keys(e).every(e=>t.has(e))||"string"!=typeof e.eventType||"string"!=typeof e.userId||"string"!=typeof e.emailAddress||"string"!=typeof e.deviceId||"string"!=typeof e.sessionHash)return this.m("/v3/process-user-event",e,{timeoutMs:s?.timeoutMs});const r=e,i=compactIdentifiers({unshared_device_id:r.permanentDeviceId,device_id:r.deviceId,stable_hash:r.fingerprintId,session_hash:r.sessionHash}),a={event_type:r.eventType,identity:{user_id:r.userId,email_address:r.emailAddress},identifiers:i,context:{ip_address:r.ipAddress,user_agent:r.userAgent,...r.subscriptionStatus?{subscription_status:r.subscriptionStatus}:{}},...r.eventDetails?{event_details:asJSONObject(r.eventDetails)}:{}},n=await this.m("/v3/process-user-event",a,{timeoutMs:s?.timeoutMs});if(!n.success)return{success:!1,status:n.status,error:n.error};const o=await this.checkUser({user_id:r.userId,email_address:r.emailAddress,identifiers:i},{timeoutMs:s?.timeoutMs,maxRetries:s?.maxRetries});return o.success?{success:!0,status:o.status,data:{event:{event_type:r.eventType,user_id:r.userId,email_address:r.emailAddress,ip_address:r.ipAddress,device_id:r.deviceId,...r.permanentDeviceId?{permanent_device_id:r.permanentDeviceId}:{},session_hash:r.sessionHash,user_agent:r.userAgent,...r.subscriptionStatus?{subscription_status:r.subscriptionStatus}:{},...r.eventDetails?{event_details:JSON.stringify(r.eventDetails)}:{}},analysis:{status:o.failedOpen?"error":"success",is_user_flagged:o.data.is_user_flagged,...o.data.decision_code?{status_details:o.data.decision_code}:{}}},...o.failedOpen?{failedOpen:o.failedOpen}:{}}:{success:!1,status:o.status,error:o.error}}async checkUser(e,s={}){const t="string"==typeof s?{deviceId:s}:s,r="string"==typeof e?{email_address:e,identifiers:compactIdentifiers({unshared_device_id:t.permanentDeviceId,device_id:t.deviceId,stable_hash:t.fingerprintId,full_hash:t.fullHash,session_hash:t.sessionHash})}:e;if("string"!=typeof r.email_address||""===r.email_address.trim())return{success:!1,status:400,error:{code:"VALIDATION_ERROR",message:"email_address must be a non-empty string"}};if(0===Object.keys(r.identifiers).length)return{success:!0,status:200,data:{is_user_flagged:!1},failedOpen:{status:0,reason:"no_device_id"}};const i=await this.p(`${this.o}/v3/check-user`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)},{timeoutMs:t.timeoutMs,maxRetries:t.maxRetries,expectedStatus:200,strictEnvelope:!0,validateData:isCheckUserResult,retryHttpError:(e,s)=>"RATE_LIMIT_UNAVAILABLE"===s.code||"AUDIT_UNAVAILABLE"===s.code});return i.success?"CHECK_UNAVAILABLE"===i.data?.decision_code?{...i,failedOpen:{status:i.status}}:i:{success:!0,status:200,data:{is_user_flagged:!1},failedOpen:{status:i.status}}}async triggerEmailVerification(e,s,t){if("string"!=typeof e||""===e.trim())return{success:!1,status:400,error:{code:"VALIDATION_ERROR",message:"emailAddress must be a non-empty string"}};const r={email_address:e,identifiers:compactIdentifiers({unshared_device_id:t?.permanentDeviceId,device_id:s,stable_hash:t?.fingerprintId,full_hash:t?.fullHash,session_hash:t?.sessionHash})};if(0===Object.keys(r.identifiers).length)return{success:!1,status:400,error:{code:"VALIDATION_ERROR",message:"At least one non-empty identifier is required"}};const i=await this.p(`${this.o}/v3/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)},{maxRetries:0,expectedStatus:200,strictEnvelope:!0,validateData:isTriggerEmailResult});return i.success?(this.t.set(e.trim().toLowerCase(),i.data.verification_id),{...i,data:{...i.data,message:"Verification code sent"}}):!i.success&&(0===i.status||i.status>=500)?{success:!1,status:i.status,error:{code:"DELIVERY_FAILED",message:i.error?.message??"Delivery failed"}}:{success:!1,status:i.status,error:i.error??{code:"PARSE_ERROR",message:"Missing v3 verification response"}}}async verify(e,s,t,r){const i=r?.verificationId??this.t.get(e.trim().toLowerCase());if(!i)return{success:!1,status:400,error:{code:"MISSING_VERIFICATION_ID",message:"Trigger email verification before verifying the code"}};if(!UUID_V4_PATTERN.test(i)||!/^\d{6}$/.test(t))return{success:!1,status:400,error:{code:"VALIDATION_ERROR",message:"verificationId must be a UUIDv4 and code must be exactly 6 digits"}};const a={verification_id:i,code:t},n=await this.p(`${this.o}/v3/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)},{maxRetries:0,expectedStatus:200,strictEnvelope:!0,validateData:isVerifyEmailResult});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?!0!==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}}:(this.t.delete(e.trim().toLowerCase()),n):n}async verifyEmail(e){return e&&UUID_V4_PATTERN.test(e.verification_id)&&/^\d{6}$/.test(e.code)?this.p(`${this.o}/v3/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)},{maxRetries:0,expectedStatus:200,strictEnvelope:!0,validateData:isVerifyEmailResult}):{success:!1,status:400,error:{code:"VALIDATION_ERROR",message:"verification_id must be a UUIDv4 and code must be exactly 6 digits"}}}async getInterstitialFlow(e){const s=new URLSearchParams;return s.set("flow_type",e?.flowType||"email_verification"),s.set("platform",e?.platform||"web"),this.p(`${this.o}/v2/interstitial-flow?${s}`,{method:"GET"})}}
|
package/dist/esm/index.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export { UnsharedClient } from './client';
|
|
2
|
-
export { createUnsharedMiddleware, assertTrustProxy } from './middleware';
|
|
3
|
-
export type { MiddlewareOptions } from './middleware';
|
|
4
|
-
export { unsharedBoundToUser, VerdictCache, flaggedResponse, ACCOUNT_FLAGGED_ERROR, } from './middleware/index';
|
|
5
|
-
export type { ProtectionConfig, Verdict } from './middleware/index';
|
|
6
|
-
export type { UnsharedClientConfig, ApiResult, UnsharedError, SubmitFingerprintOptions, SubmitFingerprintResult, ProcessUserEventParams, ProcessUserEventResult, CheckUserResult, TriggerEmailVerificationResult, VerifyResult, } from './client';
|
|
7
|
-
export type { UnsharedRequest, UnsharedResponse, UnsharedNextFunction } from './types';
|
|
8
|
-
export { sendJson } from './middleware/utils/http-helpers';
|
|
1
|
+
export { UnsharedClient } from './client.mjs';
|
|
2
|
+
export { createUnsharedMiddleware, assertTrustProxy } from './middleware.mjs';
|
|
3
|
+
export type { MiddlewareOptions } from './middleware.mjs';
|
|
4
|
+
export { unsharedBoundToUser, VerdictCache, flaggedResponse, ACCOUNT_FLAGGED_ERROR, } from './middleware/index.mjs';
|
|
5
|
+
export type { ProtectionConfig, Verdict } from './middleware/index.mjs';
|
|
6
|
+
export type { UnsharedClientConfig, ApiResult, UnsharedError, SubmitFingerprintOptions, SubmitFingerprintResult, ProcessUserEventParams, ProcessUserEventResult, CheckUserResult, TriggerEmailVerificationResult, VerifyResult, } from './client.mjs';
|
|
7
|
+
export type { UnsharedRequest, UnsharedResponse, UnsharedNextFunction } from './types.mjs';
|
|
8
|
+
export { sendJson } from './middleware/utils/http-helpers.mjs';
|
package/dist/esm/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{UnsharedClient}from"./client";export{createUnsharedMiddleware,assertTrustProxy}from"./middleware";export{unsharedBoundToUser,VerdictCache,flaggedResponse,ACCOUNT_FLAGGED_ERROR}from"./middleware/index";export{sendJson}from"./middleware/utils/http-helpers";
|
|
1
|
+
export{UnsharedClient}from"./client.mjs";export{createUnsharedMiddleware,assertTrustProxy}from"./middleware.mjs";export{unsharedBoundToUser,VerdictCache,flaggedResponse,ACCOUNT_FLAGGED_ERROR}from"./middleware/index.mjs";export{sendJson}from"./middleware/utils/http-helpers.mjs";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { UnsharedClient, ProcessUserEventParams } from '../client';
|
|
2
|
-
import type { VerdictCache } from './verdict-cache';
|
|
3
|
-
import type { RateLimitBackoff } from './rate-limit-backoff';
|
|
1
|
+
import type { UnsharedClient, ProcessUserEventParams } from '../client.mjs';
|
|
2
|
+
import type { VerdictCache } from './verdict-cache.mjs';
|
|
3
|
+
import type { RateLimitBackoff } from './rate-limit-backoff.mjs';
|
|
4
|
+
import type { CheckUserRequest } from '../shared-types.mjs';
|
|
4
5
|
/** Single policy point for background processUserEvent budgets. */
|
|
5
6
|
export declare const DISPATCH_TIMEOUT_MS = 2000;
|
|
6
7
|
export declare const SUBMIT_FP_TIMEOUT_MS = 3000;
|
|
@@ -14,9 +15,14 @@ export interface DispatchIo {
|
|
|
14
15
|
emailAddress: string;
|
|
15
16
|
}) => void;
|
|
16
17
|
}
|
|
18
|
+
/** Warm the verdict cache after fingerprint ingestion without creating a duplicate event. */
|
|
19
|
+
export declare function runCheckUser(io: DispatchIo, request: CheckUserRequest, userId: string, emailAddress: string, timeoutMs: number): Promise<void>;
|
|
17
20
|
/**
|
|
18
21
|
* Run processUserEvent with the standard bounded budget and backoff wiring.
|
|
19
|
-
* Never rejects.
|
|
22
|
+
* Never rejects. Both ordinary dispatch sites (classic/web)
|
|
20
23
|
* MUST go through this helper so the cap and backoff can't drift per-site.
|
|
21
24
|
*/
|
|
22
|
-
export declare function runProcessUserEvent(io: DispatchIo, params: ProcessUserEventParams,
|
|
25
|
+
export declare function runProcessUserEvent(io: DispatchIo, params: Omit<ProcessUserEventParams, 'deviceId'> & {
|
|
26
|
+
deviceId?: string;
|
|
27
|
+
fullHash?: string;
|
|
28
|
+
}, timeoutMs: number): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const DISPATCH_TIMEOUT_MS=2e3;export const SUBMIT_FP_TIMEOUT_MS=3e3;export async function
|
|
1
|
+
export const DISPATCH_TIMEOUT_MS=2e3;export const SUBMIT_FP_TIMEOUT_MS=3e3;export async function runCheckUser(e,s,t,r,i){try{const r=await e.client.checkUser(s,{timeoutMs:i,maxRetries:0});if(r.success&&r.data&&!r.failedOpen)return e.cache.update(t,{isFlagged:r.data.is_user_flagged}),void e.backoff.noteSuccess();if(r.failedOpen)return void e.backoff.noteFailure();r.error?.retryAfter?e.backoff.pause(1e3*r.error.retryAfter):e.backoff.noteFailure()}catch(s){e.backoff.noteFailure();try{e.onError&&e.onError(s,{operation:"processUserEvent",userId:t,emailAddress:r})}catch{}}}export async function runProcessUserEvent(e,s,t){try{const r=Object.fromEntries(Object.entries({device_id:s.deviceId,stable_hash:s.fingerprintId,full_hash:s.fullHash,unshared_device_id:s.permanentDeviceId,session_hash:s.sessionHash}).filter(e=>!!e[1]&&"unknown"!==e[1])),i=await e.client.processUserEvent({event_type:s.eventType,identity:{user_id:s.userId,email_address:s.emailAddress},identifiers:r,context:{ip_address:s.ipAddress,user_agent:s.userAgent}},{timeoutMs:t,maxRetries:0});if(i.failedOpen)return void e.backoff.noteFailure();i.success?await runCheckUser(e,{user_id:s.userId,email_address:s.emailAddress,identifiers:r},s.userId,s.emailAddress,t):i.error?.retryAfter?e.backoff.pause(1e3*i.error.retryAfter):e.backoff.noteFailure()}catch(t){e.backoff.noteFailure();try{e.onError&&e.onError(t,{operation:"processUserEvent",userId:s.userId,emailAddress:s.emailAddress})}catch{}}}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { UnsharedRequest, UnsharedResponse, UnsharedNextFunction } from '../types';
|
|
2
|
-
import type { UnsharedClient } from '../client';
|
|
3
|
-
import { VerdictCache } from './verdict-cache';
|
|
4
|
-
import type { Verdict } from './verdict-cache';
|
|
1
|
+
import type { UnsharedRequest, UnsharedResponse, UnsharedNextFunction } from '../types.mjs';
|
|
2
|
+
import type { UnsharedClient } from '../client.mjs';
|
|
3
|
+
import { VerdictCache } from './verdict-cache.mjs';
|
|
4
|
+
import type { Verdict } from './verdict-cache.mjs';
|
|
5
5
|
export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest> {
|
|
6
6
|
/**
|
|
7
7
|
* Required. Resolves the current user's ID from the request.
|
|
@@ -38,6 +38,10 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
|
|
|
38
38
|
includePathPrefix?: string[];
|
|
39
39
|
/** Skip the bot/crawler UA filter. Set to true in test environments so automated browsers (Playwright, Puppeteer, etc.) can observe verdicts. @default false */
|
|
40
40
|
disableBotFilter?: boolean;
|
|
41
|
+
/** Enable diagnostic logging in the injected fingerprint script. @default false */
|
|
42
|
+
debug?: boolean;
|
|
43
|
+
/** @internal Serve only the bundled fingerprint library, without S3 fetches. @default false */
|
|
44
|
+
disableS3Fingerprint?: boolean;
|
|
41
45
|
/** Hard timeout (ms) for checkUser on cache miss. Fails open on timeout. @default 1500 */
|
|
42
46
|
checkUserTimeoutMs?: number;
|
|
43
47
|
/**
|
|
@@ -56,7 +60,7 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
|
|
|
56
60
|
sessionId?: (req: TReq) => string | undefined;
|
|
57
61
|
/**
|
|
58
62
|
* Resolves a device ID from the request.
|
|
59
|
-
* Falls back to X-Device-Id header
|
|
63
|
+
* Falls back to X-Device-Id header, then the canonical or legacy stable-hash cookie.
|
|
60
64
|
*/
|
|
61
65
|
deviceId?: (req: TReq) => string | undefined;
|
|
62
66
|
/**
|
|
@@ -168,7 +172,7 @@ export interface FailOpenContext {
|
|
|
168
172
|
}
|
|
169
173
|
export type { Verdict };
|
|
170
174
|
export { VerdictCache };
|
|
171
|
-
export { flaggedResponse, ACCOUNT_FLAGGED_ERROR } from './utils/flagged-response';
|
|
175
|
+
export { flaggedResponse, ACCOUNT_FLAGGED_ERROR } from './utils/flagged-response.mjs';
|
|
172
176
|
export declare function unsharedBoundToUser<TReq extends UnsharedRequest = UnsharedRequest>(client: UnsharedClient, config: ProtectionConfig<TReq>): ((req: TReq, res: UnsharedResponse, next: UnsharedNextFunction) => void) & {
|
|
173
177
|
destroy: () => void;
|
|
174
178
|
};
|