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.
Files changed (72) hide show
  1. package/README.md +349 -152
  2. package/dist/client.d.ts +54 -25
  3. package/dist/client.js +1 -1
  4. package/dist/esm/client.d.mts +54 -25
  5. package/dist/esm/client.mjs +1 -1
  6. package/dist/esm/index.d.mts +8 -8
  7. package/dist/esm/index.mjs +1 -1
  8. package/dist/esm/middleware/dispatch-processing.d.mts +11 -5
  9. package/dist/esm/middleware/dispatch-processing.mjs +1 -1
  10. package/dist/esm/middleware/index.d.mts +10 -6
  11. package/dist/esm/middleware/index.mjs +1 -1
  12. package/dist/esm/middleware/injection/fingerprint-script.d.mts +9 -0
  13. package/dist/esm/middleware/injection/fingerprint-script.mjs +1 -1
  14. package/dist/esm/middleware/injection/fp-bundle-source.d.mts +5 -4
  15. package/dist/esm/middleware/injection/fp-bundle-source.mjs +1 -1
  16. package/dist/esm/middleware/response-interceptor.d.mts +1 -1
  17. package/dist/esm/middleware/routes/interstitial.d.mts +2 -2
  18. package/dist/esm/middleware/routes/interstitial.mjs +1 -1
  19. package/dist/esm/middleware/routes/submit-fp.d.mts +5 -5
  20. package/dist/esm/middleware/routes/submit-fp.mjs +1 -1
  21. package/dist/esm/middleware/routes/verify.d.mts +5 -4
  22. package/dist/esm/middleware/routes/verify.mjs +1 -1
  23. package/dist/esm/middleware/utils/client-ip.d.mts +1 -1
  24. package/dist/esm/middleware/utils/cookies.d.mts +1 -1
  25. package/dist/esm/middleware/utils/device-id.d.mts +5 -3
  26. package/dist/esm/middleware/utils/device-id.mjs +1 -1
  27. package/dist/esm/middleware/utils/flagged-response.mjs +1 -1
  28. package/dist/esm/middleware/utils/http-helpers.d.mts +1 -1
  29. package/dist/esm/middleware/utils/permanent-device-id.d.mts +7 -0
  30. package/dist/esm/middleware/utils/permanent-device-id.mjs +1 -0
  31. package/dist/esm/middleware/utils/read-json-body.d.mts +3 -0
  32. package/dist/esm/middleware/utils/read-json-body.mjs +1 -0
  33. package/dist/esm/middleware/utils/secure.d.mts +1 -1
  34. package/dist/esm/middleware.d.mts +5 -6
  35. package/dist/esm/middleware.mjs +1 -1
  36. package/dist/esm/shared-types.d.mts +148 -0
  37. package/dist/esm/web/index.d.mts +6 -6
  38. package/dist/esm/web/index.mjs +1 -1
  39. package/dist/esm/web/protection-handler.d.mts +5 -5
  40. package/dist/esm/web/protection-handler.mjs +1 -1
  41. package/dist/esm/web/submit-handler.d.mts +2 -2
  42. package/dist/esm/web/submit-handler.mjs +1 -1
  43. package/dist/esm/web/types.d.mts +5 -3
  44. package/dist/esm/web/web-helpers.d.mts +13 -1
  45. package/dist/esm/web/web-helpers.mjs +1 -1
  46. package/dist/middleware/dispatch-processing.d.ts +8 -2
  47. package/dist/middleware/dispatch-processing.js +1 -1
  48. package/dist/middleware/index.d.ts +5 -1
  49. package/dist/middleware/index.js +1 -1
  50. package/dist/middleware/injection/fingerprint-script.d.ts +9 -0
  51. package/dist/middleware/injection/fingerprint-script.js +1 -1
  52. package/dist/middleware/injection/fp-bundle-source.d.ts +5 -4
  53. package/dist/middleware/injection/fp-bundle-source.js +1 -1
  54. package/dist/middleware/routes/submit-fp.js +1 -1
  55. package/dist/middleware/routes/verify.d.ts +2 -1
  56. package/dist/middleware/routes/verify.js +1 -1
  57. package/dist/middleware/utils/device-id.d.ts +4 -2
  58. package/dist/middleware/utils/device-id.js +1 -1
  59. package/dist/middleware/utils/permanent-device-id.d.ts +7 -0
  60. package/dist/middleware/utils/permanent-device-id.js +1 -0
  61. package/dist/middleware/utils/read-json-body.d.ts +3 -0
  62. package/dist/middleware/utils/read-json-body.js +1 -0
  63. package/dist/middleware.d.ts +3 -4
  64. package/dist/middleware.js +1 -1
  65. package/dist/shared-types.d.ts +148 -0
  66. package/dist/web/protection-handler.d.ts +1 -1
  67. package/dist/web/protection-handler.js +1 -1
  68. package/dist/web/submit-handler.js +1 -1
  69. package/dist/web/types.d.ts +4 -2
  70. package/dist/web/web-helpers.d.ts +13 -1
  71. package/dist/web/web-helpers.js +1 -1
  72. package/package.json +4 -4
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { FingerprintWireFormat } from '@unshared-labs/shared-types';
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 V2 ingress.
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. Publishes asynchronously via Pub/Sub.
166
- * Maps to: POST /v2/submit-fingerprint-event
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 naughty-list analysis.
171
- * Maps to: POST /v2/process-user-event
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: GET /v2/check-user
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 /v2/trigger-email-verification
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 /v2/verify
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;
@@ -1,4 +1,4 @@
1
- import type { FingerprintWireFormat } from '@unshared-labs/shared-types';
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 V2 ingress.
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. Publishes asynchronously via Pub/Sub.
166
- * Maps to: POST /v2/submit-fingerprint-event
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 naughty-list analysis.
171
- * Maps to: POST /v2/process-user-event
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: GET /v2/check-user
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 /v2/trigger-email-verification
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 /v2/verify
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.
@@ -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"})}}
@@ -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';
@@ -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. All four dispatch sites (classic/web x dispatch/submit-fp)
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, timeoutMs: number): Promise<void>;
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 runProcessUserEvent(e,t,s){try{const r=await e.client.processUserEvent(t,{timeoutMs:s,maxRetries:0});r.success&&r.data?.analysis&&e.cache.update(t.userId,{isFlagged:r.data.analysis.is_user_flagged}),r.success?e.backoff.noteSuccess():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.userId,emailAddress:t.emailAddress})}catch{}}}
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 → __unshared_fp_id cookie.
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
  };