unshared-clientjs-sdk 2.1.0-rc.5 → 2.1.0-rc.6

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/dist/client.d.ts CHANGED
@@ -57,6 +57,15 @@ export interface ApiResult<T = unknown> {
57
57
  data?: T;
58
58
  error?: UnsharedError;
59
59
  status: number;
60
+ /**
61
+ * Set when a defensive default masked a real failure into a "safe" success
62
+ * (e.g. checkUser returning is_user_flagged:false because the request failed).
63
+ * Lets callers observe a silent fail-open they otherwise couldn't distinguish
64
+ * from a genuine result. `status` is the underlying response status (0 = network).
65
+ */
66
+ failedOpen?: {
67
+ status: number;
68
+ };
60
69
  }
61
70
  export interface SubmitFingerprintOptions {
62
71
  userId?: string;
package/dist/client.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.UnsharedClient=void 0;const crypto_1=require("crypto"),util_1=require("./util"),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(t=>setTimeout(t,e))}function retryDelay(e){const t=Math.min(1e3*Math.pow(2,e-1),3e4),s=t*(.5*Math.random()-.25);return Math.max(0,t+s)}async function parseErrorBody(e){const t=await e.text().catch(()=>"");try{const s=JSON.parse(t);return s?.error?.code?{code:s.error.code,message:s.error.message??"Unknown error",details:s.error.details}:{code:"UNKNOWN_ERROR",message:t||e.statusText}}catch{return{code:"UNKNOWN_ERROR",message:t||e.statusText}}}class UnsharedClient{constructor(e){if(!e.apiKey||""===e.apiKey.trim())throw new Error("apiKey is required");this.i=e.apiKey,this.o=e.baseUrl?e.baseUrl.replace(/\/$/,""):DEFAULT_BASE_URL,this.h=e.timeout??1e4,this.u=e.maxRetries??3,this.l=(0,crypto_1.createHash)("sha256").update(e.apiKey).digest(),this._=e.fetch}p(e){return(0,util_1.encryptData)(e,this.l)}async m(e,t){const s=this.u+1;let r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let i=1;i<=s;i++){i>1&&await sleep(retryDelay(i-1));const s=new AbortController,a=setTimeout(()=>s.abort(),this.h);try{const i=this._??globalThis.fetch,n=await i(e,{method:t.method,headers:{"X-API-Key":this.i,...t.headers},body:t.body,signal:s.signal});if(clearTimeout(a),n.ok){const e=await n.text().catch(()=>"{}");let t;try{t=JSON.parse(e)}catch{t={}}const s="data"in t?t.data:t;return{success:!0,status:n.status,data:s}}const o=await parseErrorBody(n);if(n.status>=400&&n.status<500){if(429===n.status){const e=n.headers.get("Retry-After");if(null!=e){const t=parseInt(e,10);isNaN(t)||(o.retryAfter=t)}}return{success:!1,status:n.status,error:o}}r={success:!1,status:n.status,error:o}}catch(e){clearTimeout(a),r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return r}async submitFingerprintEvent(e,t){const s={hash:e.full_hash,stable_hash:e.fingerprint_id,collected_at:e.timestamp,is_incognito:e.isIncognito,components:e.components,version:e.version};return null!=t?.userId&&(s.user_id=this.p(t.userId)),null!=t?.emailAddress&&(s.email_address=this.p(t.emailAddress)),null!=t?.sessionHash&&(s.session_hash=t.sessionHash),null!=t?.eventType&&(s.event_type=t.eventType),null!=t?.ipAddress&&(s.ip_address=this.p(t.ipAddress)),null!=t?.userAgent&&(s.user_agent=this.p(t.userAgent)),this.m(`${this.o}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":t?.idempotencyKey??(0,crypto_1.randomUUID)()},body:JSON.stringify(s)})}async processUserEvent(e){const t={event_type:e.eventType,user_id:this.p(e.userId),ip_address:this.p(e.ipAddress),device_id:this.p(e.deviceId),session_hash:e.sessionHash,user_agent:this.p(e.userAgent),email_address:this.p(e.emailAddress)};return null!=e.fingerprintId&&(t.fingerprint_id=this.p(e.fingerprintId)),null!=e.subscriptionStatus&&(t.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(t.event_details=e.eventDetails),this.m(`${this.o}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}async checkUser(e,t){const s="string"==typeof t?{deviceId:t}:t;if(!s.deviceId&&!s.fingerprintId)return{success:!0,status:200,data:{is_user_flagged:!1}};const r=new URLSearchParams;r.set("email_address",this.p(e)),s.deviceId&&r.set("device_id",this.p(s.deviceId)),s.fingerprintId&&r.set("fingerprint_id",this.p(s.fingerprintId));const i=await this.m(`${this.o}/v2/check-user?${r}`,{method:"GET"});return i.success?i:{success:!0,status:200,data:{is_user_flagged:!1}}}async triggerEmailVerification(e,t,s){const r={email_address:this.p(e),device_id:this.p(t)};s?.fingerprintId&&(r.fingerprint_id=this.p(s.fingerprintId));const i=await this.m(`${this.o}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});return!i.success&&(0===i.status||i.status>=500)?{success:!1,status:i.status,error:{code:"DELIVERY_FAILED",message:i.error?.message??"Delivery failed"}}:i}async verify(e,t,s,r){const i={email_address:this.p(e),device_id:this.p(t),code:this.p(s)};r?.fingerprintId&&(i.fingerprint_id=this.p(r.fingerprintId));const a=await this.m(`${this.o}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});return!a.success&&(0===a.status||a.status>=500)?{success:!1,status:a.status,error:{code:"DELIVERY_FAILED",message:a.error?.message??"Delivery failed"}}:a.success&&!1===a.data?.verified?{success:!1,status:a.status,error:{code:"VERIFICATION_FAILED",message:"Code is incorrect or expired",details:a.data.reason?{reason:a.data.reason}:void 0}}:a}async getInterstitialFlow(e){const t=new URLSearchParams;return t.set("flow_type",e?.flowType||"email_verification"),t.set("platform",e?.platform||"web"),this.m(`${this.o}/v2/interstitial-flow?${t}`,{method:"GET"})}}exports.UnsharedClient=UnsharedClient;
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.UnsharedClient=void 0;const crypto_1=require("crypto"),util_1=require("./util"),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(t=>setTimeout(t,e))}function retryDelay(e){const t=Math.min(1e3*Math.pow(2,e-1),3e4),s=t*(.5*Math.random()-.25);return Math.max(0,t+s)}async function parseErrorBody(e){const t=await e.text().catch(()=>"");try{const s=JSON.parse(t);return s?.error?.code?{code:s.error.code,message:s.error.message??"Unknown error",details:s.error.details}:{code:"UNKNOWN_ERROR",message:t||e.statusText}}catch{return{code:"UNKNOWN_ERROR",message:t||e.statusText}}}class UnsharedClient{constructor(e){if(!e.apiKey||""===e.apiKey.trim())throw new Error("apiKey is required");this.i=e.apiKey,this.o=e.baseUrl?e.baseUrl.replace(/\/$/,""):DEFAULT_BASE_URL,this.h=e.timeout??1e4,this.u=e.maxRetries??3,this.l=(0,crypto_1.createHash)("sha256").update(e.apiKey).digest(),this._=e.fetch}p(e){return(0,util_1.encryptData)(e,this.l)}async m(e,t){const s=this.u+1;let i={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let r=1;r<=s;r++){r>1&&await sleep(retryDelay(r-1));const s=new AbortController,a=setTimeout(()=>s.abort(),this.h);try{const r=this._??globalThis.fetch,n=await r(e,{method:t.method,headers:{"X-API-Key":this.i,...t.headers},body:t.body,signal:s.signal});if(clearTimeout(a),n.ok){const e=await n.text().catch(()=>"{}");let t;try{t=JSON.parse(e)}catch{t={}}const s="data"in t?t.data:t;return{success:!0,status:n.status,data:s}}const o=await parseErrorBody(n);if(n.status>=400&&n.status<500){if(429===n.status){const e=n.headers.get("Retry-After");if(null!=e){const t=parseInt(e,10);isNaN(t)||(o.retryAfter=t)}}return{success:!1,status:n.status,error:o}}i={success:!1,status:n.status,error:o}}catch(e){clearTimeout(a),i={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return i}async submitFingerprintEvent(e,t){const s={hash:e.full_hash,stable_hash:e.fingerprint_id,collected_at:e.timestamp,is_incognito:e.isIncognito,components:e.components,version:e.version};return null!=t?.userId&&(s.user_id=this.p(t.userId)),null!=t?.emailAddress&&(s.email_address=this.p(t.emailAddress)),null!=t?.sessionHash&&(s.session_hash=t.sessionHash),null!=t?.eventType&&(s.event_type=t.eventType),null!=t?.ipAddress&&(s.ip_address=this.p(t.ipAddress)),null!=t?.userAgent&&(s.user_agent=this.p(t.userAgent)),this.m(`${this.o}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":t?.idempotencyKey??(0,crypto_1.randomUUID)()},body:JSON.stringify(s)})}async processUserEvent(e){const t={event_type:e.eventType,user_id:this.p(e.userId),ip_address:this.p(e.ipAddress),device_id:this.p(e.deviceId),session_hash:e.sessionHash,user_agent:this.p(e.userAgent),email_address:this.p(e.emailAddress)};return null!=e.fingerprintId&&(t.fingerprint_id=this.p(e.fingerprintId)),null!=e.subscriptionStatus&&(t.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(t.event_details=e.eventDetails),this.m(`${this.o}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}async checkUser(e,t){const s="string"==typeof t?{deviceId:t}:t;if(!s.deviceId&&!s.fingerprintId)return{success:!0,status:200,data:{is_user_flagged:!1}};const i=new URLSearchParams;i.set("email_address",this.p(e)),s.deviceId&&i.set("device_id",this.p(s.deviceId)),s.fingerprintId&&i.set("fingerprint_id",this.p(s.fingerprintId));const r=await this.m(`${this.o}/v2/check-user?${i}`,{method:"GET"});return r.success?r:{success:!0,status:200,data:{is_user_flagged:!1},failedOpen:{status:r.status}}}async triggerEmailVerification(e,t,s){const i={email_address:this.p(e),device_id:this.p(t)};s?.fingerprintId&&(i.fingerprint_id=this.p(s.fingerprintId));const r=await this.m(`${this.o}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});return!r.success&&(0===r.status||r.status>=500)?{success:!1,status:r.status,error:{code:"DELIVERY_FAILED",message:r.error?.message??"Delivery failed"}}:r}async verify(e,t,s,i){const r={email_address:this.p(e),device_id:this.p(t),code:this.p(s)};i?.fingerprintId&&(r.fingerprint_id=this.p(i.fingerprintId));const a=await this.m(`${this.o}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});return!a.success&&(0===a.status||a.status>=500)?{success:!1,status:a.status,error:{code:"DELIVERY_FAILED",message:a.error?.message??"Delivery failed"}}:a.success&&!1===a.data?.verified?{success:!1,status:a.status,error:{code:"VERIFICATION_FAILED",message:"Code is incorrect or expired",details:a.data.reason?{reason:a.data.reason}:void 0}}:a}async getInterstitialFlow(e){const t=new URLSearchParams;return t.set("flow_type",e?.flowType||"email_verification"),t.set("platform",e?.platform||"web"),this.m(`${this.o}/v2/interstitial-flow?${t}`,{method:"GET"})}}exports.UnsharedClient=UnsharedClient;
@@ -57,6 +57,15 @@ export interface ApiResult<T = unknown> {
57
57
  data?: T;
58
58
  error?: UnsharedError;
59
59
  status: number;
60
+ /**
61
+ * Set when a defensive default masked a real failure into a "safe" success
62
+ * (e.g. checkUser returning is_user_flagged:false because the request failed).
63
+ * Lets callers observe a silent fail-open they otherwise couldn't distinguish
64
+ * from a genuine result. `status` is the underlying response status (0 = network).
65
+ */
66
+ failedOpen?: {
67
+ status: number;
68
+ };
60
69
  }
61
70
  export interface SubmitFingerprintOptions {
62
71
  userId?: string;
@@ -1 +1 @@
1
- import{createHash,randomUUID}from"crypto";import{encryptData}from"./util";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(t=>setTimeout(t,e))}function retryDelay(e){const t=Math.min(1e3*Math.pow(2,e-1),3e4),s=t*(.5*Math.random()-.25);return Math.max(0,t+s)}async function parseErrorBody(e){const t=await e.text().catch(()=>"");try{const s=JSON.parse(t);return s?.error?.code?{code:s.error.code,message:s.error.message??"Unknown error",details:s.error.details}:{code:"UNKNOWN_ERROR",message:t||e.statusText}}catch{return{code:"UNKNOWN_ERROR",message:t||e.statusText}}}export class UnsharedClient{constructor(e){if(!e.apiKey||""===e.apiKey.trim())throw new Error("apiKey is required");this.t=e.apiKey,this.i=e.baseUrl?e.baseUrl.replace(/\/$/,""):DEFAULT_BASE_URL,this.o=e.timeout??1e4,this.h=e.maxRetries??3,this.l=createHash("sha256").update(e.apiKey).digest(),this.u=e.fetch}_(e){return encryptData(e,this.l)}async m(e,t){const s=this.h+1;let r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let i=1;i<=s;i++){i>1&&await sleep(retryDelay(i-1));const s=new AbortController,a=setTimeout(()=>s.abort(),this.o);try{const i=this.u??globalThis.fetch,n=await i(e,{method:t.method,headers:{"X-API-Key":this.t,...t.headers},body:t.body,signal:s.signal});if(clearTimeout(a),n.ok){const e=await n.text().catch(()=>"{}");let t;try{t=JSON.parse(e)}catch{t={}}const s="data"in t?t.data:t;return{success:!0,status:n.status,data:s}}const o=await parseErrorBody(n);if(n.status>=400&&n.status<500){if(429===n.status){const e=n.headers.get("Retry-After");if(null!=e){const t=parseInt(e,10);isNaN(t)||(o.retryAfter=t)}}return{success:!1,status:n.status,error:o}}r={success:!1,status:n.status,error:o}}catch(e){clearTimeout(a),r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return r}async submitFingerprintEvent(e,t){const s={hash:e.full_hash,stable_hash:e.fingerprint_id,collected_at:e.timestamp,is_incognito:e.isIncognito,components:e.components,version:e.version};return null!=t?.userId&&(s.user_id=this._(t.userId)),null!=t?.emailAddress&&(s.email_address=this._(t.emailAddress)),null!=t?.sessionHash&&(s.session_hash=t.sessionHash),null!=t?.eventType&&(s.event_type=t.eventType),null!=t?.ipAddress&&(s.ip_address=this._(t.ipAddress)),null!=t?.userAgent&&(s.user_agent=this._(t.userAgent)),this.m(`${this.i}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":t?.idempotencyKey??randomUUID()},body:JSON.stringify(s)})}async processUserEvent(e){const t={event_type:e.eventType,user_id:this._(e.userId),ip_address:this._(e.ipAddress),device_id:this._(e.deviceId),session_hash:e.sessionHash,user_agent:this._(e.userAgent),email_address:this._(e.emailAddress)};return null!=e.fingerprintId&&(t.fingerprint_id=this._(e.fingerprintId)),null!=e.subscriptionStatus&&(t.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(t.event_details=e.eventDetails),this.m(`${this.i}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}async checkUser(e,t){const s="string"==typeof t?{deviceId:t}:t;if(!s.deviceId&&!s.fingerprintId)return{success:!0,status:200,data:{is_user_flagged:!1}};const r=new URLSearchParams;r.set("email_address",this._(e)),s.deviceId&&r.set("device_id",this._(s.deviceId)),s.fingerprintId&&r.set("fingerprint_id",this._(s.fingerprintId));const i=await this.m(`${this.i}/v2/check-user?${r}`,{method:"GET"});return i.success?i:{success:!0,status:200,data:{is_user_flagged:!1}}}async triggerEmailVerification(e,t,s){const r={email_address:this._(e),device_id:this._(t)};s?.fingerprintId&&(r.fingerprint_id=this._(s.fingerprintId));const i=await this.m(`${this.i}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});return!i.success&&(0===i.status||i.status>=500)?{success:!1,status:i.status,error:{code:"DELIVERY_FAILED",message:i.error?.message??"Delivery failed"}}:i}async verify(e,t,s,r){const i={email_address:this._(e),device_id:this._(t),code:this._(s)};r?.fingerprintId&&(i.fingerprint_id=this._(r.fingerprintId));const a=await this.m(`${this.i}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});return!a.success&&(0===a.status||a.status>=500)?{success:!1,status:a.status,error:{code:"DELIVERY_FAILED",message:a.error?.message??"Delivery failed"}}:a.success&&!1===a.data?.verified?{success:!1,status:a.status,error:{code:"VERIFICATION_FAILED",message:"Code is incorrect or expired",details:a.data.reason?{reason:a.data.reason}:void 0}}:a}async getInterstitialFlow(e){const t=new URLSearchParams;return t.set("flow_type",e?.flowType||"email_verification"),t.set("platform",e?.platform||"web"),this.m(`${this.i}/v2/interstitial-flow?${t}`,{method:"GET"})}}
1
+ import{createHash,randomUUID}from"crypto";import{encryptData}from"./util";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(t=>setTimeout(t,e))}function retryDelay(e){const t=Math.min(1e3*Math.pow(2,e-1),3e4),s=t*(.5*Math.random()-.25);return Math.max(0,t+s)}async function parseErrorBody(e){const t=await e.text().catch(()=>"");try{const s=JSON.parse(t);return s?.error?.code?{code:s.error.code,message:s.error.message??"Unknown error",details:s.error.details}:{code:"UNKNOWN_ERROR",message:t||e.statusText}}catch{return{code:"UNKNOWN_ERROR",message:t||e.statusText}}}export class UnsharedClient{constructor(e){if(!e.apiKey||""===e.apiKey.trim())throw new Error("apiKey is required");this.t=e.apiKey,this.i=e.baseUrl?e.baseUrl.replace(/\/$/,""):DEFAULT_BASE_URL,this.o=e.timeout??1e4,this.h=e.maxRetries??3,this.l=createHash("sha256").update(e.apiKey).digest(),this.u=e.fetch}_(e){return encryptData(e,this.l)}async p(e,t){const s=this.h+1;let r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let i=1;i<=s;i++){i>1&&await sleep(retryDelay(i-1));const s=new AbortController,a=setTimeout(()=>s.abort(),this.o);try{const i=this.u??globalThis.fetch,n=await i(e,{method:t.method,headers:{"X-API-Key":this.t,...t.headers},body:t.body,signal:s.signal});if(clearTimeout(a),n.ok){const e=await n.text().catch(()=>"{}");let t;try{t=JSON.parse(e)}catch{t={}}const s="data"in t?t.data:t;return{success:!0,status:n.status,data:s}}const o=await parseErrorBody(n);if(n.status>=400&&n.status<500){if(429===n.status){const e=n.headers.get("Retry-After");if(null!=e){const t=parseInt(e,10);isNaN(t)||(o.retryAfter=t)}}return{success:!1,status:n.status,error:o}}r={success:!1,status:n.status,error:o}}catch(e){clearTimeout(a),r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return r}async submitFingerprintEvent(e,t){const s={hash:e.full_hash,stable_hash:e.fingerprint_id,collected_at:e.timestamp,is_incognito:e.isIncognito,components:e.components,version:e.version};return null!=t?.userId&&(s.user_id=this._(t.userId)),null!=t?.emailAddress&&(s.email_address=this._(t.emailAddress)),null!=t?.sessionHash&&(s.session_hash=t.sessionHash),null!=t?.eventType&&(s.event_type=t.eventType),null!=t?.ipAddress&&(s.ip_address=this._(t.ipAddress)),null!=t?.userAgent&&(s.user_agent=this._(t.userAgent)),this.p(`${this.i}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":t?.idempotencyKey??randomUUID()},body:JSON.stringify(s)})}async processUserEvent(e){const t={event_type:e.eventType,user_id:this._(e.userId),ip_address:this._(e.ipAddress),device_id:this._(e.deviceId),session_hash:e.sessionHash,user_agent:this._(e.userAgent),email_address:this._(e.emailAddress)};return null!=e.fingerprintId&&(t.fingerprint_id=this._(e.fingerprintId)),null!=e.subscriptionStatus&&(t.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(t.event_details=e.eventDetails),this.p(`${this.i}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}async checkUser(e,t){const s="string"==typeof t?{deviceId:t}:t;if(!s.deviceId&&!s.fingerprintId)return{success:!0,status:200,data:{is_user_flagged:!1}};const r=new URLSearchParams;r.set("email_address",this._(e)),s.deviceId&&r.set("device_id",this._(s.deviceId)),s.fingerprintId&&r.set("fingerprint_id",this._(s.fingerprintId));const i=await this.p(`${this.i}/v2/check-user?${r}`,{method:"GET"});return i.success?i:{success:!0,status:200,data:{is_user_flagged:!1},failedOpen:{status:i.status}}}async triggerEmailVerification(e,t,s){const r={email_address:this._(e),device_id:this._(t)};s?.fingerprintId&&(r.fingerprint_id=this._(s.fingerprintId));const i=await this.p(`${this.i}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});return!i.success&&(0===i.status||i.status>=500)?{success:!1,status:i.status,error:{code:"DELIVERY_FAILED",message:i.error?.message??"Delivery failed"}}:i}async verify(e,t,s,r){const i={email_address:this._(e),device_id:this._(t),code:this._(s)};r?.fingerprintId&&(i.fingerprint_id=this._(r.fingerprintId));const a=await this.p(`${this.i}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});return!a.success&&(0===a.status||a.status>=500)?{success:!1,status:a.status,error:{code:"DELIVERY_FAILED",message:a.error?.message??"Delivery failed"}}:a.success&&!1===a.data?.verified?{success:!1,status:a.status,error:{code:"VERIFICATION_FAILED",message:"Code is incorrect or expired",details:a.data.reason?{reason:a.data.reason}:void 0}}:a}async getInterstitialFlow(e){const t=new URLSearchParams;return t.set("flow_type",e?.flowType||"email_verification"),t.set("platform",e?.platform||"web"),this.p(`${this.i}/v2/interstitial-flow?${t}`,{method:"GET"})}}
@@ -93,6 +93,29 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
93
93
  userId?: string;
94
94
  emailAddress?: string;
95
95
  }) => void;
96
+ /**
97
+ * Called whenever a verdict check could NOT be determined and the request
98
+ * therefore passes through (fails open). This is distinct from `onError`: a
99
+ * non-2xx verdict response is masked into a clean "not flagged" verdict and
100
+ * never surfaces as an error, so without this hook a flagged user passing
101
+ * because the API errored is indistinguishable from a genuinely clean user.
102
+ *
103
+ * The request still passes through regardless (the fail-open behaviour is
104
+ * unchanged) — this only makes it observable. Wrap your handler defensively;
105
+ * it is invoked best-effort and never blocks the request.
106
+ *
107
+ * `reason`: 'timeout' (exceeded checkUserTimeoutMs), 'http_error' (non-2xx
108
+ * masked into clean), or 'exception' (checkUser threw). `status` is present
109
+ * for 'http_error' (0 = network/transport).
110
+ */
111
+ onFailOpen?: (context: FailOpenContext) => void;
112
+ }
113
+ export interface FailOpenContext {
114
+ operation: 'checkUser';
115
+ reason: 'timeout' | 'http_error' | 'exception';
116
+ status?: number;
117
+ userId?: string;
118
+ emailAddress?: string;
96
119
  }
97
120
  export type { Verdict };
98
121
  export { VerdictCache };
@@ -1 +1 @@
1
- import{readFileSync}from"fs";import{VerdictCache}from"./verdict-cache";import{RateLimitBackoff}from"./rate-limit-backoff";import{DispatchDedupe}from"./dispatch-dedupe";import{interceptResponse}from"./response-interceptor";import{generateFingerprintScript}from"./injection/fingerprint-script";import{handleSubmitFingerprint}from"./routes/submit-fp";import{handleVerifyTrigger,handleVerify}from"./routes/verify";import{handleGetInterstitialFlow}from"./routes/interstitial";import{generateGatePage}from"./injection/gate-page";import{sendJson,sendEmpty,sendBody,getRequestPath}from"./utils/http-helpers";import{isHtmlContentType,isHtmlNavigation}from"./utils/content-type";import{flaggedResponse}from"./utils/flagged-response";import{shouldSkipPath}from"./utils/skip-paths";import{shouldIncludePath}from"./utils/include-path";import{isBot}from"./utils/is-bot";import{extractClientIp}from"./utils/client-ip";import{parseCookie}from"./utils/cookies";import{extractDeviceIdOrUndefined}from"./utils/device-id";import{isSecureRequest}from"./utils/secure";import{isSentinelUserId,SENTINEL_STICKINESS_TTL_MS}from"./utils/sentinel-user-id";export{VerdictCache};export{flaggedResponse,ACCOUNT_FLAGGED_ERROR}from"./utils/flagged-response";const CHECK_USER_TIMEOUT_MS=500;export function unsharedBoundToUser(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");if(!t.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:r,emailAddress:i,routePrefix:n="/__unshared",corsOrigins:o,cacheTTL:s=6e4,skipPaths:d,includePathPrefix:a,disableBotFilter:c=!1,checkUserTimeoutMs:l=CHECK_USER_TIMEOUT_MS,sessionId:u,deviceId:p,onFlagged:f,onError:h,blockFlagged:m=!1,autoInterstitial:g=!1,interstitialFlowType:S="email_verification"}=t,_=new VerdictCache(s),I=new RateLimitBackoff,v=new DispatchDedupe,C=Date.now().toString(36),y=generateFingerprintScript(n,C,{autoInterstitial:g,interstitialFlowType:S});let k="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");k=readFileSync(e,"utf8")}catch{}if(m&&!k)throw new Error("[Unshared] blockFlagged requires unshared-frontend-sdk to be installed (its UMD bundle is the gate-page renderer).");if(m&&"/__unshared"!==n)throw new Error('[Unshared] blockFlagged requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');if(g&&!k)throw new Error("[Unshared] autoInterstitial requires unshared-frontend-sdk to be installed (its UMD bundle boots the auto-rendered interstitial).");if(g&&"/__unshared"!==n)throw new Error('[Unshared] autoInterstitial requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');const E=m?generateGatePage(n):"",T=handleSubmitFingerprint({client:e,verdictCache:_,rateLimitBackoff:I,dispatchDedupe:v,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:u,resolveDeviceId:p,disableBotFilter:c,onError:h}),x=handleVerifyTrigger({client:e,verdictCache:_,resolveEmailAddress:i,resolveDeviceId:p,onError:h}),A=handleVerify({client:e,verdictCache:_,resolveEmailAddress:i,resolveDeviceId:p,onError:h}),w=handleGetInterstitialFlow({client:e}),U=o?Array.isArray(o)?o:[o]:null,F=`${n}/fp.js`,P=`${n}/submit-fp`,b=`${n}/verify-trigger`,R=`${n}/verify`,D=`${n}/status`,O=`${n}/interstitial-flow`;return function(t,o,s){const g=getRequestPath(t.url),S=t.url||g;if(g.startsWith(n+"/")){if(function(e,t){if(!U)return;const r=e.headers.origin??"",i=U.includes("*");(i||U.includes(r))&&(t.setHeader("Access-Control-Allow-Origin",i?"*":r),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,o),"OPTIONS"===t.method)return void sendEmpty(o,204);if("GET"===t.method&&g===F)return o.setHeader("Content-Type","application/javascript"),o.setHeader("Cache-Control","public, max-age=3600"),void sendBody(o,200,k);if("POST"===t.method&&(g===P||g===b||g===R))return void 0===t.body?void sendJson(o,400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"req.body is undefined. Mount a JSON body-parsing middleware (e.g., express.json()) before the Unshared middleware."}}):g===P?void T(t,o):g===b?void x(t,o):void A(t,o);if("GET"===t.method&&g===O)return void w(t,o);if("GET"===t.method&&g===D){let n;try{n=r(t)}catch{}if(!n)return void sendJson(o,200,{status:"anonymous"});const s=resolveEmail(t,i);return void(async()=>{let r=_.get(n);if((!r||_.isStale(n))&&s&&!I.isPaused()&&!_.isRefreshing(n)){_.markRefreshing(n);try{const i=extractDeviceIdOrUndefined(t,p),o=extractFingerprintId(t),d=extractSessionId(t,u),a=i??o??"unknown";await fetchAndCacheVerdict(e,_,n,s,a,o,d,l),r=_.get(n)}catch(e){h&&h(e,{operation:"checkUser",userId:n,emailAddress:s})}finally{_.clearRefreshing(n)}}r&&r.isFlagged&&!r.isVerified&&f&&s?sendJson(o,200,{status:"flagged",email:s}):sendJson(o,200,{status:"ok"})})()}return void sendJson(o,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if(shouldSkipPath(g,d))return void s();if(!shouldIncludePath(g,a))return interceptForInjection(t,o,y),void s();let C;try{C=r(t)}catch{}if(isSentinelUserId(C)){const e=parseCookie(t,"__unshared_uid"),r=parseCookie(t,"__unshared_uid_at"),i=r?Number(r):NaN,n=Number.isFinite(i)&&Date.now()-i<=SENTINEL_STICKINESS_TTL_MS;C=e&&n?e:void 0}if(!C){const e=isSecureRequest(t)?"; Secure":"";return appendSetCookie(o,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(o,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(o,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(o,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(t,o,y),void s()}const N=resolveEmail(t,i);if(setUserIdCookie(t,o,C),N&&setEmailCookie(t,o,N),!N)return interceptForInjection(t,o,y),void s();const M=extractSessionId(t,u),L=extractDeviceIdOrUndefined(t,p),V=extractFingerprintId(t),$=t.headers["user-agent"]??"",q=extractClientIp(t),j=L??V;if(!c&&isBot($))return void s();const B=_.get(C);function G(){"unknown"!==M&&j&&(I.isPaused()||dispatchUserEvent(e,_,I,v,{userId:C,emailAddress:N,sessionId:M,deviceId:j,fingerprintId:V,userAgent:$,ipAddress:q,eventType:S},h))}B?(_.isStale(C)&&!_.isRefreshing(C)&&(_.markRefreshing(C),fetchAndCacheVerdict(e,_,C,N,j??"unknown",V,M,l).finally(()=>_.clearRefreshing(C))),B.isFlagged||G(),applyVerdict(B,C,N,t,o,s,y,f,m,E)):fetchAndCacheVerdict(e,_,C,N,j??"unknown",V,M,l).then(e=>{e.isFlagged||G(),applyVerdict(e,C,N,t,o,s,y,f,m,E)}).catch(()=>{G(),interceptForInjection(t,o,y),s()})}}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=parseCookie(e,"__unshared_email");if(r)return r;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,t,r,i,n,o,s,d,a,c){if(a&&e.isFlagged&&!e.isVerified)isHtmlNavigation(i.method,i.headers.accept)?(n.statusCode=200,n.setHeader("Content-Type","text/html; charset=utf-8"),n.setHeader("Cache-Control","no-store"),n.end(c)):sendJson(n,403,flaggedResponse(r));else if(interceptForInjection(i,n,s),e.isFlagged&&!e.isVerified&&d)try{d({userId:t,emailAddress:r,verdict:e,req:i,res:n,next:o})}catch{o()}else o()}function interceptForInjection(e,t,r){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],interceptResponse(t,(e,t)=>{if(!isHtmlContentType(t))return null;const i=e.toString("utf8"),n=i.lastIndexOf("</body>");return-1===n?i+r:i.slice(0,n)+r+i.slice(n)},{preventCaching:!0})}function dispatchUserEvent(e,t,r,i,n,o){i.mark(n.userId,n.eventType),e.processUserEvent({eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,sessionHash:n.sessionId,userAgent:n.userAgent}).then(e=>{e.success&&e.data?.analysis&&t.update(n.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&r.pause(1e3*e.error.retryAfter)}).catch(e=>{o&&o(e,{operation:"processUserEvent",userId:n.userId,emailAddress:n.emailAddress})})}async function fetchAndCacheVerdict(e,t,r,i,n,o,s,d=CHECK_USER_TIMEOUT_MS){const a={};let c;n&&"unknown"!==n&&(a.deviceId=n),o&&(a.fingerprintId=o);const l=await Promise.race([e.checkUser(i,a),new Promise(e=>{c=setTimeout(()=>e(null),d)})]);if(clearTimeout(c),!l)return{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:s,cachedAt:0,ttl:0};const u=l.data?.is_user_flagged??!1;return t.set(r,{isFlagged:u,isVerified:!1,emailAddress:i,sessionId:s}),t.get(r)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return parseCookie(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return parseCookie(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
1
+ import{readFileSync}from"fs";import{VerdictCache}from"./verdict-cache";import{RateLimitBackoff}from"./rate-limit-backoff";import{DispatchDedupe}from"./dispatch-dedupe";import{interceptResponse}from"./response-interceptor";import{generateFingerprintScript}from"./injection/fingerprint-script";import{handleSubmitFingerprint}from"./routes/submit-fp";import{handleVerifyTrigger,handleVerify}from"./routes/verify";import{handleGetInterstitialFlow}from"./routes/interstitial";import{generateGatePage}from"./injection/gate-page";import{sendJson,sendEmpty,sendBody,getRequestPath}from"./utils/http-helpers";import{isHtmlContentType,isHtmlNavigation}from"./utils/content-type";import{flaggedResponse}from"./utils/flagged-response";import{shouldSkipPath}from"./utils/skip-paths";import{shouldIncludePath}from"./utils/include-path";import{isBot}from"./utils/is-bot";import{extractClientIp}from"./utils/client-ip";import{parseCookie}from"./utils/cookies";import{extractDeviceIdOrUndefined}from"./utils/device-id";import{isSecureRequest}from"./utils/secure";import{isSentinelUserId,SENTINEL_STICKINESS_TTL_MS}from"./utils/sentinel-user-id";export{VerdictCache};export{flaggedResponse,ACCOUNT_FLAGGED_ERROR}from"./utils/flagged-response";const CHECK_USER_TIMEOUT_MS=500;export function unsharedBoundToUser(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");if(!t.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:r,emailAddress:i,routePrefix:n="/__unshared",corsOrigins:s,cacheTTL:o=6e4,skipPaths:d,includePathPrefix:a,disableBotFilter:c=!1,checkUserTimeoutMs:u=CHECK_USER_TIMEOUT_MS,sessionId:l,deviceId:p,onFlagged:f,onError:h,onFailOpen:m,blockFlagged:g=!1,autoInterstitial:I=!1,interstitialFlowType:S="email_verification"}=t,_=e=>{if(m)try{m(e)}catch{}},v=new VerdictCache(o),C=new RateLimitBackoff,k=new DispatchDedupe,y=Date.now().toString(36),A=generateFingerprintScript(n,y,{autoInterstitial:I,interstitialFlowType:S});let E="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");E=readFileSync(e,"utf8")}catch{}if(g&&!E)throw new Error("[Unshared] blockFlagged requires unshared-frontend-sdk to be installed (its UMD bundle is the gate-page renderer).");if(g&&"/__unshared"!==n)throw new Error('[Unshared] blockFlagged requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');if(I&&!E)throw new Error("[Unshared] autoInterstitial requires unshared-frontend-sdk to be installed (its UMD bundle boots the auto-rendered interstitial).");if(I&&"/__unshared"!==n)throw new Error('[Unshared] autoInterstitial requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');const x=g?generateGatePage(n,y):"",T=handleSubmitFingerprint({client:e,verdictCache:v,rateLimitBackoff:C,dispatchDedupe:k,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:l,resolveDeviceId:p,disableBotFilter:c,onError:h}),U=handleVerifyTrigger({client:e,verdictCache:v,resolveEmailAddress:i,resolveDeviceId:p,onError:h}),w=handleVerify({client:e,verdictCache:v,resolveEmailAddress:i,resolveDeviceId:p,onError:h}),F=handleGetInterstitialFlow({client:e}),P=s?Array.isArray(s)?s:[s]:null,b=`${n}/fp.js`,R=`${n}/submit-fp`,D=`${n}/verify-trigger`,O=`${n}/verify`,N=`${n}/status`,M=`${n}/interstitial-flow`;return function(t,s,o){const m=getRequestPath(t.url),I=t.url||m;if(m.startsWith(n+"/")){if(function(e,t){if(!P)return;const r=e.headers.origin??"",i=P.includes("*");(i||P.includes(r))&&(t.setHeader("Access-Control-Allow-Origin",i?"*":r),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,s),"OPTIONS"===t.method)return void sendEmpty(s,204);if("GET"===t.method&&m===b)return s.setHeader("Content-Type","application/javascript"),s.setHeader("Cache-Control","public, max-age=3600"),void sendBody(s,200,E);if("POST"===t.method&&(m===R||m===D||m===O))return void 0===t.body?void sendJson(s,400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"req.body is undefined. Mount a JSON body-parsing middleware (e.g., express.json()) before the Unshared middleware."}}):m===R?void T(t,s):m===D?void U(t,s):void w(t,s);if("GET"===t.method&&m===M)return void F(t,s);if("GET"===t.method&&m===N){let n;try{n=r(t)}catch{}if(!n)return void sendJson(s,200,{status:"anonymous"});const o=resolveEmail(t,i);return void(async()=>{let r=v.get(n);if((!r||v.isStale(n))&&o&&!C.isPaused()&&!v.isRefreshing(n)){v.markRefreshing(n);try{const i=extractDeviceIdOrUndefined(t,p),s=extractFingerprintId(t),d=extractSessionId(t,l),a=i??s??"unknown";await fetchAndCacheVerdict(e,v,n,o,a,s,d,u,(e,t)=>_({operation:"checkUser",reason:e,status:t,userId:n,emailAddress:o})),r=v.get(n)}catch(e){h&&h(e,{operation:"checkUser",userId:n,emailAddress:o}),_({operation:"checkUser",reason:"exception",userId:n,emailAddress:o})}finally{v.clearRefreshing(n)}}r&&r.isFlagged&&!r.isVerified&&f&&o?sendJson(s,200,{status:"flagged",email:o}):sendJson(s,200,{status:"ok"})})()}return void sendJson(s,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if(shouldSkipPath(m,d))return void o();if(!shouldIncludePath(m,a))return interceptForInjection(t,s,A),void o();let S;try{S=r(t)}catch{}if(isSentinelUserId(S)){const e=parseCookie(t,"__unshared_uid"),r=parseCookie(t,"__unshared_uid_at"),i=r?Number(r):NaN,n=Number.isFinite(i)&&Date.now()-i<=SENTINEL_STICKINESS_TTL_MS;S=e&&n?e:void 0}if(!S){const e=isSecureRequest(t)?"; Secure":"";return appendSetCookie(s,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(t,s,A),void o()}const y=resolveEmail(t,i);if(setUserIdCookie(t,s,S),y&&setEmailCookie(t,s,y),!y)return interceptForInjection(t,s,A),void o();const L=extractSessionId(t,l),V=extractDeviceIdOrUndefined(t,p),$=extractFingerprintId(t),q=t.headers["user-agent"]??"",j=extractClientIp(t),B=V??$;if(!c&&isBot(q))return void o();const G=v.get(S);function H(){"unknown"!==L&&B&&(C.isPaused()||dispatchUserEvent(e,v,C,k,{userId:S,emailAddress:y,sessionId:L,deviceId:B,fingerprintId:$,userAgent:q,ipAddress:j,eventType:I},h))}G?(v.isStale(S)&&!v.isRefreshing(S)&&(v.markRefreshing(S),fetchAndCacheVerdict(e,v,S,y,B??"unknown",$,L,u,(e,t)=>_({operation:"checkUser",reason:e,status:t,userId:S,emailAddress:y})).catch(()=>_({operation:"checkUser",reason:"exception",userId:S,emailAddress:y})).finally(()=>v.clearRefreshing(S))),G.isFlagged||H(),applyVerdict(G,S,y,t,s,o,A,f,g,x)):fetchAndCacheVerdict(e,v,S,y,B??"unknown",$,L,u,(e,t)=>_({operation:"checkUser",reason:e,status:t,userId:S,emailAddress:y})).then(e=>{e.isFlagged||H(),applyVerdict(e,S,y,t,s,o,A,f,g,x)}).catch(()=>{_({operation:"checkUser",reason:"exception",userId:S,emailAddress:y}),H(),interceptForInjection(t,s,A),o()})}}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=parseCookie(e,"__unshared_email");if(r)return r;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,t,r,i,n,s,o,d,a,c){if(a&&e.isFlagged&&!e.isVerified)isHtmlNavigation(i.method,i.headers.accept)?(n.statusCode=200,n.setHeader("Content-Type","text/html; charset=utf-8"),n.setHeader("Cache-Control","no-store"),n.end(c)):sendJson(n,403,flaggedResponse(r));else if(interceptForInjection(i,n,o),e.isFlagged&&!e.isVerified&&d)try{d({userId:t,emailAddress:r,verdict:e,req:i,res:n,next:s})}catch{s()}else s()}function interceptForInjection(e,t,r){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],interceptResponse(t,(e,t)=>{if(!isHtmlContentType(t))return null;const i=e.toString("utf8"),n=i.lastIndexOf("</body>");return-1===n?i+r:i.slice(0,n)+r+i.slice(n)},{preventCaching:!0})}function dispatchUserEvent(e,t,r,i,n,s){i.mark(n.userId,n.eventType),e.processUserEvent({eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,sessionHash:n.sessionId,userAgent:n.userAgent}).then(e=>{e.success&&e.data?.analysis&&t.update(n.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&r.pause(1e3*e.error.retryAfter)}).catch(e=>{s&&s(e,{operation:"processUserEvent",userId:n.userId,emailAddress:n.emailAddress})})}async function fetchAndCacheVerdict(e,t,r,i,n,s,o,d=CHECK_USER_TIMEOUT_MS,a){const c={};let u;n&&"unknown"!==n&&(c.deviceId=n),s&&(c.fingerprintId=s);const l=await Promise.race([e.checkUser(i,c),new Promise(e=>{u=setTimeout(()=>e(null),d)})]);if(clearTimeout(u),!l)return a?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:o,cachedAt:0,ttl:0};l.failedOpen&&a?.("http_error",l.failedOpen.status);const p=l.data?.is_user_flagged??!1;return t.set(r,{isFlagged:p,isVerified:!1,emailAddress:i,sessionId:o}),t.get(r)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return parseCookie(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return parseCookie(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
@@ -12,4 +12,4 @@
12
12
  * NOTE: the browser SDK's proxy paths are hardcoded to the default `/__unshared` prefix,
13
13
  * so block mode requires the default routePrefix (the middleware validates this).
14
14
  */
15
- export declare function generateGatePage(routePrefix: string): string;
15
+ export declare function generateGatePage(routePrefix: string, version?: string): string;
@@ -1 +1 @@
1
- export function generateGatePage(n){return`<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n<meta name="robots" content="noindex,nofollow">\n<title>Verification required</title>\n<style>\nhtml,body{margin:0;height:100%;background:#0b0b0c;font-family:system-ui,sans-serif}\n#__unshared_gate_fallback{\n position:fixed;inset:0;display:flex;flex-direction:column;\n align-items:center;justify-content:center;gap:16px;padding:24px;\n text-align:center;color:#e5e7eb;\n}\n#__unshared_gate_fallback p{margin:0;font-size:16px;line-height:1.5;max-width:360px}\n#__unshared_gate_fallback button{\n display:none;padding:10px 20px;font-size:15px;font-weight:600;cursor:pointer;\n color:#fff;background:#4f46e5;border:none;border-radius:8px;font-family:inherit;\n}\n</style>\n</head>\n<body>\n<noscript>Verification is required to continue. Please enable JavaScript.</noscript>\n\x3c!-- Always-visible fallback so a slow/failed modal render is never a blank black void.\n The interstitial modal (full-viewport overlay, z-index 2147483647) covers this once\n it mounts; if it never mounts, the user sees a legible message instead of nothing. --\x3e\n<div id="__unshared_gate_fallback" role="status" aria-live="polite">\n <p id="__unshared_gate_msg">Verifying your account&hellip;</p>\n <button id="__unshared_gate_reload" type="button">Refresh to try again</button>\n</div>\n<script src="${n}/fp.js"><\/script>\n<script>\n(function(){\n function showError(){\n try{\n var msg = document.getElementById('__unshared_gate_msg');\n var btn = document.getElementById('__unshared_gate_reload');\n if(msg){ msg.textContent = "We couldn't load verification. Refresh to try again."; }\n if(btn){ btn.style.display = 'inline-block'; btn.onclick = function(){ try{ location.reload(); }catch(e){} }; }\n }catch(e){}\n }\n function boot(){\n try{\n var ns = window.UnsharedBrowser;\n if(!ns || !ns.UnsharedBrowser){ showError(); return; }\n var sdk = new ns.UnsharedBrowser({ baseUrl: '' });\n Promise.resolve(sdk.showInterstitial({ onComplete: function(){ try{ location.reload(); }catch(e){} } }))\n .then(function(res){ if(!res || !res.success){ showError(); } })\n .catch(function(){ showError(); });\n }catch(e){ showError(); }\n }\n if(window.UnsharedBrowser){ boot(); return; }\n var tries = 0;\n var timer = setInterval(function(){\n if(window.UnsharedBrowser || tries++ > 50){ clearInterval(timer); boot(); }\n }, 100);\n})();\n<\/script>\n</body>\n</html>`}
1
+ export function generateGatePage(n,e){return`<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n<meta name="robots" content="noindex,nofollow">\n<title>Verification required</title>\n<style>\nhtml,body{margin:0;height:100%;background:#0b0b0c;font-family:system-ui,sans-serif}\n#__unshared_gate_fallback{\n position:fixed;inset:0;display:flex;flex-direction:column;\n align-items:center;justify-content:center;gap:16px;padding:24px;\n text-align:center;color:#e5e7eb;\n}\n#__unshared_gate_fallback p{margin:0;font-size:16px;line-height:1.5;max-width:360px}\n#__unshared_gate_fallback button{\n display:none;padding:10px 20px;font-size:15px;font-weight:600;cursor:pointer;\n color:#fff;background:#4f46e5;border:none;border-radius:8px;font-family:inherit;\n}\n</style>\n</head>\n<body>\n<noscript>Verification is required to continue. Please enable JavaScript.</noscript>\n\x3c!-- Always-visible fallback so a slow/failed modal render is never a blank black void.\n The interstitial modal (full-viewport overlay, z-index 2147483647) covers this once\n it mounts; if it never mounts, the user sees a legible message instead of nothing. --\x3e\n<div id="__unshared_gate_fallback" role="status" aria-live="polite">\n <p id="__unshared_gate_msg">Verifying your account&hellip;</p>\n <button id="__unshared_gate_reload" type="button">Refresh to try again</button>\n</div>\n<script src="${n}/fp.js${e?`?v=${encodeURIComponent(e)}`:""}"><\/script>\n<script>\n(function(){\n function showError(){\n try{\n var msg = document.getElementById('__unshared_gate_msg');\n var btn = document.getElementById('__unshared_gate_reload');\n if(msg){ msg.textContent = "We couldn't load verification. Refresh to try again."; }\n if(btn){ btn.style.display = 'inline-block'; btn.onclick = function(){ try{ location.reload(); }catch(e){} }; }\n }catch(e){}\n }\n function boot(){\n try{\n var ns = window.UnsharedBrowser;\n if(!ns || !ns.UnsharedBrowser){ showError(); return; }\n var sdk = new ns.UnsharedBrowser({ baseUrl: '' });\n Promise.resolve(sdk.showInterstitial({ onComplete: function(){ try{ location.reload(); }catch(e){} } }))\n .then(function(res){ if(!res || !res.success){ showError(); } })\n .catch(function(){ showError(); });\n }catch(e){ showError(); }\n }\n if(window.UnsharedBrowser){ boot(); return; }\n var tries = 0;\n var timer = setInterval(function(){\n if(window.UnsharedBrowser || tries++ > 50){ clearInterval(timer); boot(); }\n }, 100);\n})();\n<\/script>\n</body>\n</html>`}
@@ -8,5 +8,10 @@ export interface InterstitialDependencies {
8
8
  * Fetches the published interstitial flow via the secret-key client and returns it
9
9
  * to the browser. Carries no user data — only the flow definition. Never 500s
10
10
  * (mirrors the verify routes' always-200 envelope style).
11
+ *
12
+ * When no flow is published (upstream 404, or success with empty `data`) it returns a
13
+ * distinguishable `FLOW_NOT_PUBLISHED` error so an integrator can tell a provisioning
14
+ * gap apart from a transport/auth failure in the network tab. Other upstream errors
15
+ * pass through unchanged.
11
16
  */
12
17
  export declare function handleGetInterstitialFlow(deps: InterstitialDependencies): (req: UnsharedRequest, res: UnsharedResponse) => Promise<void>;
@@ -1 +1 @@
1
- import{sendJson}from"../utils/http-helpers";export function handleGetInterstitialFlow(e){return async(s,t)=>{try{const o=s.url&&s.url.includes("?")?s.url.slice(s.url.indexOf("?")+1):"",a=new URLSearchParams(o),r=a.get("flow_type")||void 0,n=a.get("platform")||void 0,l=await e.client.getInterstitialFlow({flowType:r,platform:n});l.success?sendJson(t,200,{success:!0,data:l.data}):sendJson(t,200,{success:!1,error:l.error??{code:"FLOW_FETCH_FAILED",message:"Failed to load interstitial flow"}})}catch{sendJson(t,200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to load interstitial flow"}})}}}
1
+ import{sendJson}from"../utils/http-helpers";function isPublishedFlow(s){return!!s&&"object"==typeof s&&Object.keys(s).length>0}const FLOW_NOT_PUBLISHED={code:"FLOW_NOT_PUBLISHED",message:"No interstitial flow is published for this company."};export function handleGetInterstitialFlow(s){return async(e,o)=>{try{const t=e.url&&e.url.includes("?")?e.url.slice(e.url.indexOf("?")+1):"",r=new URLSearchParams(t),n=r.get("flow_type")||void 0,i=r.get("platform")||void 0,a=await s.client.getInterstitialFlow({flowType:n,platform:i});a.success?isPublishedFlow(a.data)?sendJson(o,200,{success:!0,data:a.data}):sendJson(o,200,{success:!1,error:{...FLOW_NOT_PUBLISHED}}):404===a.status?sendJson(o,200,{success:!1,error:{...FLOW_NOT_PUBLISHED}}):sendJson(o,200,{success:!1,error:a.error??{code:"FLOW_FETCH_FAILED",message:"Failed to load interstitial flow"}})}catch{sendJson(o,200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to load interstitial flow"}})}}}
@@ -1 +1 @@
1
- import{sha256Hex}from"../util";import{VerdictCache}from"../middleware/verdict-cache";import{RateLimitBackoff}from"../middleware/rate-limit-backoff";import{DispatchDedupe}from"../middleware/dispatch-dedupe";import{generateFingerprintScript}from"../middleware/injection/fingerprint-script";import{generateGatePage}from"../middleware/injection/gate-page";import{flaggedResponse}from"../middleware/utils/flagged-response";import{isHtmlContentType,isHtmlNavigation}from"../middleware/utils/content-type";import{shouldSkipPath}from"../middleware/utils/skip-paths";import{shouldIncludePath}from"../middleware/utils/include-path";import{isBot}from"../middleware/utils/is-bot";import{isSentinelUserId,SENTINEL_STICKINESS_TTL_MS}from"../middleware/utils/sentinel-user-id";import{parseCookieFromRequest,extractClientIpFromRequest,extractDeviceIdFromRequest,extractDeviceIdFromRequestOrUnknown,isSecureWebRequest,jsonResponse,emptyResponse,bodyResponse,mergeResponseHeaders}from"./web-helpers";const CHECK_USER_TIMEOUT_MS=500;export function createWebProtectionMiddleware(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");const{userId:r,emailAddress:s,routePrefix:n="/__unshared",corsOrigins:i,cacheTTL:o=6e4,skipPaths:a,includePathPrefix:d,sessionId:c,deviceId:u,fingerprintSdkBundle:l="",onFlagged:p,onError:m,disableBotFilter:f=!1,checkUserTimeoutMs:h=CHECK_USER_TIMEOUT_MS,blockFlagged:_=!1,autoInterstitial:g=!1,interstitialFlowType:R="email_verification"}=t;if(_&&!l)throw new Error("[Unshared] blockFlagged requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the gate page).");if(_&&"/__unshared"!==n)throw new Error('[Unshared] blockFlagged requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');if(g&&!l)throw new Error("[Unshared] autoInterstitial requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js boots the auto-rendered interstitial).");if(g&&"/__unshared"!==n)throw new Error('[Unshared] autoInterstitial requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');const I=_?generateGatePage(n):"",S=new VerdictCache(o),v=new RateLimitBackoff,y=new DispatchDedupe,w=Date.now().toString(36),C=generateFingerprintScript(n,w,{autoInterstitial:g,interstitialFlowType:R}),E=`${n}/fp.js`,F=`${n}/submit-fp`,A=`${n}/verify-trigger`,T=`${n}/verify`,k=`${n}/status`,x=i?Array.isArray(i)?i:[i]:null;return async function(t,i){let o,g,R;try{const e=new URL(t.url);o=e.pathname,g=e.search}catch{return i(t)}if(o.startsWith(n+"/")){const n=function(e){if(!x)return{};const t=e.headers.get("origin")??"",r=x.includes("*");return r||x.includes(t)?{"Access-Control-Allow-Origin":r?"*":t,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id","Access-Control-Allow-Credentials":"true"}:{}}(t);if("OPTIONS"===t.method)return emptyResponse(204,n);if("GET"===t.method&&o===E)return l?bodyResponse(200,l,{...n,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600"}):jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},n);if("POST"===t.method&&(o===F||o===A||o===T)){let i;try{i=await t.json()}catch{return jsonResponse(400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"Request body is not valid JSON."}},n)}return o===F?handleSubmitFp(t,i,{client:e,verdictCache:S,rateLimitBackoff:v,dispatchDedupe:y,resolveUserId:r,resolveEmailAddress:s,resolveSessionId:c,resolveDeviceId:u,disableBotFilter:f,onError:m},n):o===A?handleVerifyTriggerWeb(t,i,{client:e,verdictCache:S,resolveEmailAddress:s,resolveDeviceId:u,onError:m},n):handleVerifyWeb(t,i,{client:e,verdictCache:S,resolveEmailAddress:s,resolveDeviceId:u,onError:m},n)}if("GET"===t.method&&o===k){let i;try{i=r(t)}catch{}if(!i)return jsonResponse(200,{status:"anonymous"},n);const o=resolveEmail(t,s);let a=S.get(i);if((!a||S.isStale(i))&&o&&!v.isPaused()&&!S.isRefreshing(i)){S.markRefreshing(i);try{const r=extractDeviceIdFromRequest(t,u),s=parseCookieFromRequest(t,"__unshared_fingerprint_id")||void 0,n=extractSessionIdFromRequest(t,c),d=r??s??"unknown";await fetchAndCacheVerdict(e,S,i,o,d,s,n,h),a=S.get(i)}catch(e){m&&m(e,{operation:"checkUser",userId:i,emailAddress:o})}finally{S.clearRefreshing(i)}}return a&&a.isFlagged&&!a.isVerified&&p&&o?jsonResponse(200,{status:"flagged",email:o},n):jsonResponse(200,{status:"ok"},n)}return jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},n)}if(shouldSkipPath(o,a))return i(t);if(!shouldIncludePath(o,d))return injectIntoHtmlResponse(await i(t),C);try{R=r(t)}catch{}if(isSentinelUserId(R)){const e=parseCookieFromRequest(t,"__unshared_uid"),r=parseCookieFromRequest(t,"__unshared_uid_at"),s=r?Number(r):NaN,n=Number.isFinite(s)&&Date.now()-s<=SENTINEL_STICKINESS_TTL_MS;R=e&&n?e:void 0}if(!R){const e=isSecureWebRequest(t)?"; Secure":"",r=[`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`];return injectIntoHtmlResponse(await i(t),C,r)}const w=resolveEmail(t,s),q=[],U=isSecureWebRequest(t)?"; Secure":"";if(q.push(`__unshared_uid=${encodeURIComponent(R)}; Path=/; SameSite=Lax${U}`),q.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${U}`),w&&q.push(`__unshared_email=${encodeURIComponent(w)}; HttpOnly; Path=/; SameSite=Lax${U}`),!w)return injectIntoHtmlResponse(await i(t),C,q);const b=extractSessionIdFromRequest(t,c),D=extractDeviceIdFromRequest(t,u),P=parseCookieFromRequest(t,"__unshared_fingerprint_id")||void 0,j=t.headers.get("user-agent")??"",O=extractClientIpFromRequest(t),L=D??P;if(!f&&isBot(j))return i(t);let N=S.get(R);if(N)S.isStale(R)&&!S.isRefreshing(R)&&(S.markRefreshing(R),fetchAndCacheVerdict(e,S,R,w,L??"unknown",P,b,h).finally(()=>S.clearRefreshing(R)));else try{N=await fetchAndCacheVerdict(e,S,R,w,L??"unknown",P,b,h)}catch{return injectIntoHtmlResponse(await i(t),C,q)}if(_&&N.isFlagged&&!N.isVerified)return isHtmlNavigation(t.method,t.headers.get("accept")??void 0)?bodyResponse(200,I,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}):jsonResponse(403,flaggedResponse(w));if(N.isFlagged&&!N.isVerified&&p)try{const e=await p({userId:R,emailAddress:w,verdict:N,request:t});if(e)return injectIntoHtmlResponse(e,C,q)}catch(e){m&&m(e,{operation:"checkUser",userId:R,emailAddress:w})}return N.isFlagged||"unknown"===b||!L||v.isPaused()||dispatchUserEvent(e,S,v,y,{userId:R,emailAddress:w,sessionId:b,deviceId:L,fingerprintId:P,userAgent:j,ipAddress:O,eventType:o+g},m),injectIntoHtmlResponse(await i(t),C,q)}}async function injectIntoHtmlResponse(e,t,r){const s=e.headers.get("content-type");if(!isHtmlContentType(s??void 0)){if(!r||0===r.length)return e;const t=mergeResponseHeaders(e.headers,void 0,r);return new Response(e.body,{status:e.status,statusText:e.statusText,headers:t})}const n=await e.text(),i=n.lastIndexOf("</body>"),o=-1===i?n+t:n.slice(0,i)+t+n.slice(i),a=mergeResponseHeaders(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(o).length)},r);return a.delete("ETag"),a.delete("Last-Modified"),a.delete("Content-Encoding"),new Response(o,{status:e.status,statusText:e.statusText,headers:a})}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=parseCookieFromRequest(e,"__unshared_email");if(r)return r}function resolveEmailWithBody(e,t,r){const s=resolveEmail(e,r);if(s)return s;const n=t.email;return"string"==typeof n&&n?n:void 0}function extractSessionIdFromRequest(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return parseCookieFromRequest(e,"__unshared_sid")??"unknown"}function dispatchUserEvent(e,t,r,s,n,i){s.mark(n.userId,n.eventType),e.processUserEvent({eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,sessionHash:n.sessionId,userAgent:n.userAgent}).then(e=>{e.success&&e.data?.analysis&&t.update(n.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&r.pause(1e3*e.error.retryAfter)}).catch(e=>{i&&i(e,{operation:"processUserEvent",userId:n.userId,emailAddress:n.emailAddress})})}async function fetchAndCacheVerdict(e,t,r,s,n,i,o,a=CHECK_USER_TIMEOUT_MS){const d={};let c;n&&"unknown"!==n&&(d.deviceId=n),i&&(d.fingerprintId=i);const u=await Promise.race([e.checkUser(s,d),new Promise(e=>{c=setTimeout(()=>e(null),a)})]);if(clearTimeout(c),!u)return{isFlagged:!1,isVerified:!1,emailAddress:s,sessionId:o,cachedAt:0,ttl:0};const l=u.data?.is_user_flagged??!1;return t.set(r,{isFlagged:l,isVerified:!1,emailAddress:s,sessionId:o}),t.get(r)}async function handleSubmitFp(e,t,r,s){try{const n={full_hash:t.hash??"",fingerprint_id:t.stable_hash??"",timestamp:t.collected_at??(new Date).toISOString(),isIncognito:t.is_incognito??!1,components:t.components??{},version:t.version??"inline-1.0.0"};let i,o,a;try{const t=r.resolveUserId(e);t&&!isSentinelUserId(t)&&(i=t)}catch{}if(!i){const e="string"==typeof t.user_id?t.user_id:void 0;e&&!isSentinelUserId(e)&&(i=e)}if(!i){const t=parseCookieFromRequest(e,"__unshared_uid");t&&!isSentinelUserId(t)&&(i=t)}try{o=r.resolveEmailAddress?r.resolveEmailAddress(e):void 0}catch{}o=o??parseCookieFromRequest(e,"__unshared_email")??t.email??void 0;try{a=r.resolveSessionId?r.resolveSessionId(e):void 0}catch{}a=a??t.session_id??parseCookieFromRequest(e,"__unshared_sid");const d=extractClientIpFromRequest(e),c=e.headers.get("user-agent")??"";if(!r.disableBotFilter&&isBot(c))return jsonResponse(200,{success:!0},s);const u=(n.fingerprint_id&&n.fingerprint_id.length>0?n.fingerprint_id:void 0)??extractDeviceIdFromRequestOrUnknown(e,r.resolveDeviceId),l=n.fingerprint_id||void 0,p=n.full_hash||void 0,m=isSecureWebRequest(e)?"; Secure":"",f=[];if(p&&!parseCookieFromRequest(e,"__unshared_fingerprint_id")&&f.push(`__unshared_fingerprint_id=${encodeURIComponent(p)}; HttpOnly; Path=/; SameSite=Lax${m}`),l){const t=parseCookieFromRequest(e,"__unshared_fp_id");t&&t===l||f.push(`__unshared_fp_id=${encodeURIComponent(l)}; Path=/; SameSite=Lax; Max-Age=31536000${m}`)}let h;if(o&&!parseCookieFromRequest(e,"__unshared_email")&&f.push(`__unshared_email=${encodeURIComponent(o)}; HttpOnly; Path=/; SameSite=Lax${m}`),"string"==typeof t.event_type&&t.event_type)h=t.event_type;else{const t=e.headers.get("referer")??e.headers.get("referrer");let r="unknown";if(t)try{const e=new URL(t);r=(e.pathname||"/")+(e.search||"")}catch{}h=r}const _=e.headers.get("x-idempotency-key")||void 0,g=Date.now(),R=_?`${_}|${g}`:l&&i?`${sha256Hex(`${l}|${i}|${h}`)}|${g}`:void 0;i&&r.client.submitFingerprintEvent(n,{userId:i,emailAddress:o,sessionHash:a,eventType:h,ipAddress:d,userAgent:c,idempotencyKey:R}).catch(e=>{r.onError&&r.onError(e,{operation:"submitFingerprintEvent",userId:i,emailAddress:o})}),i&&o&&!r.rateLimitBackoff.isPaused()&&!r.dispatchDedupe.wasRecentlyDispatched(i,h)&&r.client.processUserEvent({eventType:h,userId:i,emailAddress:o,ipAddress:d,deviceId:u,fingerprintId:l,sessionHash:a??"unknown",userAgent:c}).then(e=>{e.success&&e.data?.analysis&&r.verdictCache.update(i,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&r.rateLimitBackoff.pause(1e3*e.error.retryAfter)}).catch(e=>{r.onError&&r.onError(e,{operation:"processUserEvent",userId:i,emailAddress:o})});const I={...s,"Content-Type":"application/json"},S=new Response(JSON.stringify({success:!0}),{status:200,headers:I});for(const e of f)S.headers.append("Set-Cookie",e);return S}catch{return jsonResponse(200,{success:!0},s)}}async function handleVerifyTriggerWeb(e,t,r,s){try{const n=resolveEmailWithBody(e,t??{},r.resolveEmailAddress);if(!n)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},s);const i=extractDeviceIdFromRequestOrUnknown(e,r.resolveDeviceId),o=parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,a=await r.client.triggerEmailVerification(n,i,{fingerprintId:o});return a.success?jsonResponse(200,{success:!0,data:a.data},s):jsonResponse(200,{success:!1,error:a.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},s)}catch(e){return r.onError&&r.onError(e,{operation:"verifyTrigger"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},s)}}async function handleVerifyWeb(e,t,r,s){try{const n=resolveEmailWithBody(e,t??{},r.resolveEmailAddress),i=t?.code;if(!n||!i)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}},s);const o=extractDeviceIdFromRequestOrUnknown(e,r.resolveDeviceId),a=parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,d=await r.client.verify(n,o,i,{fingerprintId:a});if(d.success){const t=parseCookieFromRequest(e,"__unshared_uid");return t&&r.verdictCache.update(t,{isVerified:!0}),jsonResponse(200,{success:!0,data:{verified:!0}},s)}return jsonResponse(200,{success:!1,error:d.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}},s)}catch(e){return r.onError&&r.onError(e,{operation:"verify"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},s)}}
1
+ import{sha256Hex}from"../util";import{VerdictCache}from"../middleware/verdict-cache";import{RateLimitBackoff}from"../middleware/rate-limit-backoff";import{DispatchDedupe}from"../middleware/dispatch-dedupe";import{generateFingerprintScript}from"../middleware/injection/fingerprint-script";import{generateGatePage}from"../middleware/injection/gate-page";import{flaggedResponse}from"../middleware/utils/flagged-response";import{isHtmlContentType,isHtmlNavigation}from"../middleware/utils/content-type";import{shouldSkipPath}from"../middleware/utils/skip-paths";import{shouldIncludePath}from"../middleware/utils/include-path";import{isBot}from"../middleware/utils/is-bot";import{isSentinelUserId,SENTINEL_STICKINESS_TTL_MS}from"../middleware/utils/sentinel-user-id";import{parseCookieFromRequest,extractClientIpFromRequest,extractDeviceIdFromRequest,extractDeviceIdFromRequestOrUnknown,isSecureWebRequest,jsonResponse,emptyResponse,bodyResponse,mergeResponseHeaders}from"./web-helpers";const CHECK_USER_TIMEOUT_MS=500;export function createWebProtectionMiddleware(e,r){if(!r.userId)throw new Error("[Unshared] userId resolver is required");const{userId:t,emailAddress:s,routePrefix:n="/__unshared",corsOrigins:i,cacheTTL:o=6e4,skipPaths:a,includePathPrefix:d,sessionId:c,deviceId:u,fingerprintSdkBundle:l="",onFlagged:p,onError:m,onFailOpen:f,disableBotFilter:h=!1,checkUserTimeoutMs:_=CHECK_USER_TIMEOUT_MS,blockFlagged:g=!1,autoInterstitial:R=!1,interstitialFlowType:I="email_verification"}=r,S=e=>{if(f)try{f(e)}catch{}};if(g&&!l)throw new Error("[Unshared] blockFlagged requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the gate page).");if(g&&"/__unshared"!==n)throw new Error('[Unshared] blockFlagged requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');if(R&&!l)throw new Error("[Unshared] autoInterstitial requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js boots the auto-rendered interstitial).");if(R&&"/__unshared"!==n)throw new Error('[Unshared] autoInterstitial requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');const v=g?generateGatePage(n):"",y=new VerdictCache(o),w=new RateLimitBackoff,C=new DispatchDedupe,A=Date.now().toString(36),E=generateFingerprintScript(n,A,{autoInterstitial:R,interstitialFlowType:I}),k=`${n}/fp.js`,F=`${n}/submit-fp`,x=`${n}/verify-trigger`,T=`${n}/verify`,U=`${n}/status`,q=i?Array.isArray(i)?i:[i]:null;return async function(r,i){let o,f,R;try{const e=new URL(r.url);o=e.pathname,f=e.search}catch{return i(r)}if(o.startsWith(n+"/")){const n=function(e){if(!q)return{};const r=e.headers.get("origin")??"",t=q.includes("*");return t||q.includes(r)?{"Access-Control-Allow-Origin":t?"*":r,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id","Access-Control-Allow-Credentials":"true"}:{}}(r);if("OPTIONS"===r.method)return emptyResponse(204,n);if("GET"===r.method&&o===k)return l?bodyResponse(200,l,{...n,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600"}):jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},n);if("POST"===r.method&&(o===F||o===x||o===T)){let i;try{i=await r.json()}catch{return jsonResponse(400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"Request body is not valid JSON."}},n)}return o===F?handleSubmitFp(r,i,{client:e,verdictCache:y,rateLimitBackoff:w,dispatchDedupe:C,resolveUserId:t,resolveEmailAddress:s,resolveSessionId:c,resolveDeviceId:u,disableBotFilter:h,onError:m},n):o===x?handleVerifyTriggerWeb(r,i,{client:e,verdictCache:y,resolveEmailAddress:s,resolveDeviceId:u,onError:m},n):handleVerifyWeb(r,i,{client:e,verdictCache:y,resolveEmailAddress:s,resolveDeviceId:u,onError:m},n)}if("GET"===r.method&&o===U){let i;try{i=t(r)}catch{}if(!i)return jsonResponse(200,{status:"anonymous"},n);const o=resolveEmail(r,s);let a=y.get(i);if((!a||y.isStale(i))&&o&&!w.isPaused()&&!y.isRefreshing(i)){y.markRefreshing(i);try{const t=extractDeviceIdFromRequest(r,u),s=parseCookieFromRequest(r,"__unshared_fingerprint_id")||void 0,n=extractSessionIdFromRequest(r,c),d=t??s??"unknown";await fetchAndCacheVerdict(e,y,i,o,d,s,n,_,(e,r)=>S({operation:"checkUser",reason:e,status:r,userId:i,emailAddress:o})),a=y.get(i)}catch(e){m&&m(e,{operation:"checkUser",userId:i,emailAddress:o}),S({operation:"checkUser",reason:"exception",userId:i,emailAddress:o})}finally{y.clearRefreshing(i)}}return a&&a.isFlagged&&!a.isVerified&&p&&o?jsonResponse(200,{status:"flagged",email:o},n):jsonResponse(200,{status:"ok"},n)}return jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},n)}if(shouldSkipPath(o,a))return i(r);if(!shouldIncludePath(o,d))return injectIntoHtmlResponse(await i(r),E);try{R=t(r)}catch{}if(isSentinelUserId(R)){const e=parseCookieFromRequest(r,"__unshared_uid"),t=parseCookieFromRequest(r,"__unshared_uid_at"),s=t?Number(t):NaN,n=Number.isFinite(s)&&Date.now()-s<=SENTINEL_STICKINESS_TTL_MS;R=e&&n?e:void 0}if(!R){const e=isSecureWebRequest(r)?"; Secure":"",t=[`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`];return injectIntoHtmlResponse(await i(r),E,t)}const I=resolveEmail(r,s),A=[],b=isSecureWebRequest(r)?"; Secure":"";if(A.push(`__unshared_uid=${encodeURIComponent(R)}; Path=/; SameSite=Lax${b}`),A.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${b}`),I&&A.push(`__unshared_email=${encodeURIComponent(I)}; HttpOnly; Path=/; SameSite=Lax${b}`),!I)return injectIntoHtmlResponse(await i(r),E,A);const D=extractSessionIdFromRequest(r,c),O=extractDeviceIdFromRequest(r,u),P=parseCookieFromRequest(r,"__unshared_fingerprint_id")||void 0,j=r.headers.get("user-agent")??"",L=extractClientIpFromRequest(r),N=O??P;if(!h&&isBot(j))return i(r);let $=y.get(R);if($)y.isStale(R)&&!y.isRefreshing(R)&&(y.markRefreshing(R),fetchAndCacheVerdict(e,y,R,I,N??"unknown",P,D,_,(e,r)=>S({operation:"checkUser",reason:e,status:r,userId:R,emailAddress:I})).catch(()=>S({operation:"checkUser",reason:"exception",userId:R,emailAddress:I})).finally(()=>y.clearRefreshing(R)));else try{$=await fetchAndCacheVerdict(e,y,R,I,N??"unknown",P,D,_,(e,r)=>S({operation:"checkUser",reason:e,status:r,userId:R,emailAddress:I}))}catch{return S({operation:"checkUser",reason:"exception",userId:R,emailAddress:I}),injectIntoHtmlResponse(await i(r),E,A)}if(g&&$.isFlagged&&!$.isVerified)return isHtmlNavigation(r.method,r.headers.get("accept")??void 0)?bodyResponse(200,v,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}):jsonResponse(403,flaggedResponse(I));if($.isFlagged&&!$.isVerified&&p)try{const e=await p({userId:R,emailAddress:I,verdict:$,request:r});if(e)return injectIntoHtmlResponse(e,E,A)}catch(e){m&&m(e,{operation:"checkUser",userId:R,emailAddress:I})}return $.isFlagged||"unknown"===D||!N||w.isPaused()||dispatchUserEvent(e,y,w,C,{userId:R,emailAddress:I,sessionId:D,deviceId:N,fingerprintId:P,userAgent:j,ipAddress:L,eventType:o+f},m),injectIntoHtmlResponse(await i(r),E,A)}}async function injectIntoHtmlResponse(e,r,t){const s=e.headers.get("content-type");if(!isHtmlContentType(s??void 0)){if(!t||0===t.length)return e;const r=mergeResponseHeaders(e.headers,void 0,t);return new Response(e.body,{status:e.status,statusText:e.statusText,headers:r})}const n=await e.text(),i=n.lastIndexOf("</body>"),o=-1===i?n+r:n.slice(0,i)+r+n.slice(i),a=mergeResponseHeaders(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(o).length)},t);return a.delete("ETag"),a.delete("Last-Modified"),a.delete("Content-Encoding"),new Response(o,{status:e.status,statusText:e.statusText,headers:a})}function resolveEmail(e,r){if(r)try{const t=r(e);if(t)return t}catch{}const t=parseCookieFromRequest(e,"__unshared_email");if(t)return t}function resolveEmailWithBody(e,r,t){const s=resolveEmail(e,t);if(s)return s;const n=r.email;return"string"==typeof n&&n?n:void 0}function extractSessionIdFromRequest(e,r){if(r)try{const t=r(e);if(t)return t}catch{}return parseCookieFromRequest(e,"__unshared_sid")??"unknown"}function dispatchUserEvent(e,r,t,s,n,i){s.mark(n.userId,n.eventType),e.processUserEvent({eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,sessionHash:n.sessionId,userAgent:n.userAgent}).then(e=>{e.success&&e.data?.analysis&&r.update(n.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.pause(1e3*e.error.retryAfter)}).catch(e=>{i&&i(e,{operation:"processUserEvent",userId:n.userId,emailAddress:n.emailAddress})})}async function fetchAndCacheVerdict(e,r,t,s,n,i,o,a=CHECK_USER_TIMEOUT_MS,d){const c={};let u;n&&"unknown"!==n&&(c.deviceId=n),i&&(c.fingerprintId=i);const l=await Promise.race([e.checkUser(s,c),new Promise(e=>{u=setTimeout(()=>e(null),a)})]);if(clearTimeout(u),!l)return d?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:s,sessionId:o,cachedAt:0,ttl:0};l.failedOpen&&d?.("http_error",l.failedOpen.status);const p=l.data?.is_user_flagged??!1;return r.set(t,{isFlagged:p,isVerified:!1,emailAddress:s,sessionId:o}),r.get(t)}async function handleSubmitFp(e,r,t,s){try{const n={full_hash:r.hash??"",fingerprint_id:r.stable_hash??"",timestamp:r.collected_at??(new Date).toISOString(),isIncognito:r.is_incognito??!1,components:r.components??{},version:r.version??"inline-1.0.0"};let i,o,a;try{const r=t.resolveUserId(e);r&&!isSentinelUserId(r)&&(i=r)}catch{}if(!i){const e="string"==typeof r.user_id?r.user_id:void 0;e&&!isSentinelUserId(e)&&(i=e)}if(!i){const r=parseCookieFromRequest(e,"__unshared_uid");r&&!isSentinelUserId(r)&&(i=r)}try{o=t.resolveEmailAddress?t.resolveEmailAddress(e):void 0}catch{}o=o??parseCookieFromRequest(e,"__unshared_email")??r.email??void 0;try{a=t.resolveSessionId?t.resolveSessionId(e):void 0}catch{}a=a??r.session_id??parseCookieFromRequest(e,"__unshared_sid");const d=extractClientIpFromRequest(e),c=e.headers.get("user-agent")??"";if(!t.disableBotFilter&&isBot(c))return jsonResponse(200,{success:!0},s);const u=(n.fingerprint_id&&n.fingerprint_id.length>0?n.fingerprint_id:void 0)??extractDeviceIdFromRequestOrUnknown(e,t.resolveDeviceId),l=n.fingerprint_id||void 0,p=n.full_hash||void 0,m=isSecureWebRequest(e)?"; Secure":"",f=[];if(p&&!parseCookieFromRequest(e,"__unshared_fingerprint_id")&&f.push(`__unshared_fingerprint_id=${encodeURIComponent(p)}; HttpOnly; Path=/; SameSite=Lax${m}`),l){const r=parseCookieFromRequest(e,"__unshared_fp_id");r&&r===l||f.push(`__unshared_fp_id=${encodeURIComponent(l)}; Path=/; SameSite=Lax; Max-Age=31536000${m}`)}let h;if(o&&!parseCookieFromRequest(e,"__unshared_email")&&f.push(`__unshared_email=${encodeURIComponent(o)}; HttpOnly; Path=/; SameSite=Lax${m}`),"string"==typeof r.event_type&&r.event_type)h=r.event_type;else{const r=e.headers.get("referer")??e.headers.get("referrer");let t="unknown";if(r)try{const e=new URL(r);t=(e.pathname||"/")+(e.search||"")}catch{}h=t}const _=e.headers.get("x-idempotency-key")||void 0,g=Date.now(),R=_?`${_}|${g}`:l&&i?`${sha256Hex(`${l}|${i}|${h}`)}|${g}`:void 0;i&&t.client.submitFingerprintEvent(n,{userId:i,emailAddress:o,sessionHash:a,eventType:h,ipAddress:d,userAgent:c,idempotencyKey:R}).catch(e=>{t.onError&&t.onError(e,{operation:"submitFingerprintEvent",userId:i,emailAddress:o})}),i&&o&&!t.rateLimitBackoff.isPaused()&&!t.dispatchDedupe.wasRecentlyDispatched(i,h)&&t.client.processUserEvent({eventType:h,userId:i,emailAddress:o,ipAddress:d,deviceId:u,fingerprintId:l,sessionHash:a??"unknown",userAgent:c}).then(e=>{e.success&&e.data?.analysis&&t.verdictCache.update(i,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.rateLimitBackoff.pause(1e3*e.error.retryAfter)}).catch(e=>{t.onError&&t.onError(e,{operation:"processUserEvent",userId:i,emailAddress:o})});const I={...s,"Content-Type":"application/json"},S=new Response(JSON.stringify({success:!0}),{status:200,headers:I});for(const e of f)S.headers.append("Set-Cookie",e);return S}catch{return jsonResponse(200,{success:!0},s)}}async function handleVerifyTriggerWeb(e,r,t,s){try{const n=resolveEmailWithBody(e,r??{},t.resolveEmailAddress);if(!n)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},s);const i=extractDeviceIdFromRequestOrUnknown(e,t.resolveDeviceId),o=parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,a=await t.client.triggerEmailVerification(n,i,{fingerprintId:o});return a.success?jsonResponse(200,{success:!0,data:a.data},s):jsonResponse(200,{success:!1,error:a.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},s)}catch(e){return t.onError&&t.onError(e,{operation:"verifyTrigger"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},s)}}async function handleVerifyWeb(e,r,t,s){try{const n=resolveEmailWithBody(e,r??{},t.resolveEmailAddress),i=r?.code;if(!n||!i)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}},s);const o=extractDeviceIdFromRequestOrUnknown(e,t.resolveDeviceId),a=parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,d=await t.client.verify(n,o,i,{fingerprintId:a});if(d.success){const r=parseCookieFromRequest(e,"__unshared_uid");return r&&t.verdictCache.update(r,{isVerified:!0}),jsonResponse(200,{success:!0,data:{verified:!0}},s)}return jsonResponse(200,{success:!1,error:d.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}},s)}catch(e){return t.onError&&t.onError(e,{operation:"verify"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},s)}}
@@ -138,4 +138,19 @@ export interface WebProtectionConfig {
138
138
  userId?: string;
139
139
  emailAddress?: string;
140
140
  }) => void;
141
+ /**
142
+ * Called whenever a verdict check could NOT be determined and the request
143
+ * therefore passes through (fails open) — see the Node middleware's
144
+ * `onFailOpen`. A non-2xx verdict response is masked into a clean "not flagged"
145
+ * verdict and never surfaces via `onError`; this hook makes that observable.
146
+ * The fail-open behaviour is unchanged; invoked best-effort, never blocks.
147
+ */
148
+ onFailOpen?: (context: WebFailOpenContext) => void;
149
+ }
150
+ export interface WebFailOpenContext {
151
+ operation: 'checkUser';
152
+ reason: 'timeout' | 'http_error' | 'exception';
153
+ status?: number;
154
+ userId?: string;
155
+ emailAddress?: string;
141
156
  }
@@ -93,6 +93,29 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
93
93
  userId?: string;
94
94
  emailAddress?: string;
95
95
  }) => void;
96
+ /**
97
+ * Called whenever a verdict check could NOT be determined and the request
98
+ * therefore passes through (fails open). This is distinct from `onError`: a
99
+ * non-2xx verdict response is masked into a clean "not flagged" verdict and
100
+ * never surfaces as an error, so without this hook a flagged user passing
101
+ * because the API errored is indistinguishable from a genuinely clean user.
102
+ *
103
+ * The request still passes through regardless (the fail-open behaviour is
104
+ * unchanged) — this only makes it observable. Wrap your handler defensively;
105
+ * it is invoked best-effort and never blocks the request.
106
+ *
107
+ * `reason`: 'timeout' (exceeded checkUserTimeoutMs), 'http_error' (non-2xx
108
+ * masked into clean), or 'exception' (checkUser threw). `status` is present
109
+ * for 'http_error' (0 = network/transport).
110
+ */
111
+ onFailOpen?: (context: FailOpenContext) => void;
112
+ }
113
+ export interface FailOpenContext {
114
+ operation: 'checkUser';
115
+ reason: 'timeout' | 'http_error' | 'exception';
116
+ status?: number;
117
+ userId?: string;
118
+ emailAddress?: string;
96
119
  }
97
120
  export type { Verdict };
98
121
  export { VerdictCache };
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.ACCOUNT_FLAGGED_ERROR=exports.flaggedResponse=exports.VerdictCache=void 0,exports.unsharedBoundToUser=unsharedBoundToUser;const fs_1=require("fs"),verdict_cache_1=require("./verdict-cache");Object.defineProperty(exports,"VerdictCache",{enumerable:!0,get:function(){return verdict_cache_1.VerdictCache}});const rate_limit_backoff_1=require("./rate-limit-backoff"),dispatch_dedupe_1=require("./dispatch-dedupe"),response_interceptor_1=require("./response-interceptor"),fingerprint_script_1=require("./injection/fingerprint-script"),submit_fp_1=require("./routes/submit-fp"),verify_1=require("./routes/verify"),interstitial_1=require("./routes/interstitial"),gate_page_1=require("./injection/gate-page"),http_helpers_1=require("./utils/http-helpers"),content_type_1=require("./utils/content-type"),flagged_response_1=require("./utils/flagged-response"),skip_paths_1=require("./utils/skip-paths"),include_path_1=require("./utils/include-path"),is_bot_1=require("./utils/is-bot"),client_ip_1=require("./utils/client-ip"),cookies_1=require("./utils/cookies"),device_id_1=require("./utils/device-id"),secure_1=require("./utils/secure"),sentinel_user_id_1=require("./utils/sentinel-user-id");var flagged_response_2=require("./utils/flagged-response");Object.defineProperty(exports,"flaggedResponse",{enumerable:!0,get:function(){return flagged_response_2.flaggedResponse}}),Object.defineProperty(exports,"ACCOUNT_FLAGGED_ERROR",{enumerable:!0,get:function(){return flagged_response_2.ACCOUNT_FLAGGED_ERROR}});const CHECK_USER_TIMEOUT_MS=500;function unsharedBoundToUser(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");if(!t.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:r,emailAddress:i,routePrefix:s="/__unshared",corsOrigins:n,cacheTTL:o=6e4,skipPaths:d,includePathPrefix:a,disableBotFilter:c=!1,checkUserTimeoutMs:u=CHECK_USER_TIMEOUT_MS,sessionId:_,deviceId:l,onFlagged:p,onError:h,blockFlagged:f=!1,autoInterstitial:g=!1,interstitialFlowType:v="email_verification"}=t,m=new verdict_cache_1.VerdictCache(o),I=new rate_limit_backoff_1.RateLimitBackoff,S=new dispatch_dedupe_1.DispatchDedupe,k=Date.now().toString(36),C=(0,fingerprint_script_1.generateFingerprintScript)(s,k,{autoInterstitial:g,interstitialFlowType:v});let y="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");y=(0,fs_1.readFileSync)(e,"utf8")}catch{}if(f&&!y)throw new Error("[Unshared] blockFlagged requires unshared-frontend-sdk to be installed (its UMD bundle is the gate-page renderer).");if(f&&"/__unshared"!==s)throw new Error('[Unshared] blockFlagged requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');if(g&&!y)throw new Error("[Unshared] autoInterstitial requires unshared-frontend-sdk to be installed (its UMD bundle boots the auto-rendered interstitial).");if(g&&"/__unshared"!==s)throw new Error('[Unshared] autoInterstitial requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');const x=f?(0,gate_page_1.generateGatePage)(s):"",A=(0,submit_fp_1.handleSubmitFingerprint)({client:e,verdictCache:m,rateLimitBackoff:I,dispatchDedupe:S,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:_,resolveDeviceId:l,disableBotFilter:c,onError:h}),b=(0,verify_1.handleVerifyTrigger)({client:e,verdictCache:m,resolveEmailAddress:i,resolveDeviceId:l,onError:h}),E=(0,verify_1.handleVerify)({client:e,verdictCache:m,resolveEmailAddress:i,resolveDeviceId:l,onError:h}),w=(0,interstitial_1.handleGetInterstitialFlow)({client:e}),T=n?Array.isArray(n)?n:[n]:null,q=`${s}/fp.js`,U=`${s}/submit-fp`,F=`${s}/verify-trigger`,O=`${s}/verify`,P=`${s}/status`,M=`${s}/interstitial-flow`;return function(t,n,o){const g=(0,http_helpers_1.getRequestPath)(t.url),v=t.url||g;if(g.startsWith(s+"/")){if(function(e,t){if(!T)return;const r=e.headers.origin??"",i=T.includes("*");(i||T.includes(r))&&(t.setHeader("Access-Control-Allow-Origin",i?"*":r),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,n),"OPTIONS"===t.method)return void(0,http_helpers_1.sendEmpty)(n,204);if("GET"===t.method&&g===q)return n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBody)(n,200,y);if("POST"===t.method&&(g===U||g===F||g===O))return void 0===t.body?void(0,http_helpers_1.sendJson)(n,400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"req.body is undefined. Mount a JSON body-parsing middleware (e.g., express.json()) before the Unshared middleware."}}):g===U?void A(t,n):g===F?void b(t,n):void E(t,n);if("GET"===t.method&&g===M)return void w(t,n);if("GET"===t.method&&g===P){let s;try{s=r(t)}catch{}if(!s)return void(0,http_helpers_1.sendJson)(n,200,{status:"anonymous"});const o=resolveEmail(t,i);return void(async()=>{let r=m.get(s);if((!r||m.isStale(s))&&o&&!I.isPaused()&&!m.isRefreshing(s)){m.markRefreshing(s);try{const i=(0,device_id_1.extractDeviceIdOrUndefined)(t,l),n=extractFingerprintId(t),d=extractSessionId(t,_),a=i??n??"unknown";await fetchAndCacheVerdict(e,m,s,o,a,n,d,u),r=m.get(s)}catch(e){h&&h(e,{operation:"checkUser",userId:s,emailAddress:o})}finally{m.clearRefreshing(s)}}r&&r.isFlagged&&!r.isVerified&&p&&o?(0,http_helpers_1.sendJson)(n,200,{status:"flagged",email:o}):(0,http_helpers_1.sendJson)(n,200,{status:"ok"})})()}return void(0,http_helpers_1.sendJson)(n,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if((0,skip_paths_1.shouldSkipPath)(g,d))return void o();if(!(0,include_path_1.shouldIncludePath)(g,a))return interceptForInjection(t,n,C),void o();let k;try{k=r(t)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(k)){const e=(0,cookies_1.parseCookie)(t,"__unshared_uid"),r=(0,cookies_1.parseCookie)(t,"__unshared_uid_at"),i=r?Number(r):NaN,s=Number.isFinite(i)&&Date.now()-i<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;k=e&&s?e:void 0}if(!k){const e=(0,secure_1.isSecureRequest)(t)?"; Secure":"";return appendSetCookie(n,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(t,n,C),void o()}const j=resolveEmail(t,i);if(setUserIdCookie(t,n,k),j&&setEmailCookie(t,n,j),!j)return interceptForInjection(t,n,C),void o();const $=extractSessionId(t,_),D=(0,device_id_1.extractDeviceIdOrUndefined)(t,l),N=extractFingerprintId(t),R=t.headers["user-agent"]??"",L=(0,client_ip_1.extractClientIp)(t),V=D??N;if(!c&&(0,is_bot_1.isBot)(R))return void o();const G=m.get(k);function B(){"unknown"!==$&&V&&(I.isPaused()||dispatchUserEvent(e,m,I,S,{userId:k,emailAddress:j,sessionId:$,deviceId:V,fingerprintId:N,userAgent:R,ipAddress:L,eventType:v},h))}G?(m.isStale(k)&&!m.isRefreshing(k)&&(m.markRefreshing(k),fetchAndCacheVerdict(e,m,k,j,V??"unknown",N,$,u).finally(()=>m.clearRefreshing(k))),G.isFlagged||B(),applyVerdict(G,k,j,t,n,o,C,p,f,x)):fetchAndCacheVerdict(e,m,k,j,V??"unknown",N,$,u).then(e=>{e.isFlagged||B(),applyVerdict(e,k,j,t,n,o,C,p,f,x)}).catch(()=>{B(),interceptForInjection(t,n,C),o()})}}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=(0,cookies_1.parseCookie)(e,"__unshared_email");if(r)return r;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,t,r,i,s,n,o,d,a,c){if(a&&e.isFlagged&&!e.isVerified)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(s.statusCode=200,s.setHeader("Content-Type","text/html; charset=utf-8"),s.setHeader("Cache-Control","no-store"),s.end(c)):(0,http_helpers_1.sendJson)(s,403,(0,flagged_response_1.flaggedResponse)(r));else if(interceptForInjection(i,s,o),e.isFlagged&&!e.isVerified&&d)try{d({userId:t,emailAddress:r,verdict:e,req:i,res:s,next:n})}catch{n()}else n()}function interceptForInjection(e,t,r){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],(0,response_interceptor_1.interceptResponse)(t,(e,t)=>{if(!(0,content_type_1.isHtmlContentType)(t))return null;const i=e.toString("utf8"),s=i.lastIndexOf("</body>");return-1===s?i+r:i.slice(0,s)+r+i.slice(s)},{preventCaching:!0})}function dispatchUserEvent(e,t,r,i,s,n){i.mark(s.userId,s.eventType),e.processUserEvent({eventType:s.eventType,userId:s.userId,emailAddress:s.emailAddress,ipAddress:s.ipAddress,deviceId:s.deviceId,fingerprintId:s.fingerprintId,sessionHash:s.sessionId,userAgent:s.userAgent}).then(e=>{e.success&&e.data?.analysis&&t.update(s.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&r.pause(1e3*e.error.retryAfter)}).catch(e=>{n&&n(e,{operation:"processUserEvent",userId:s.userId,emailAddress:s.emailAddress})})}async function fetchAndCacheVerdict(e,t,r,i,s,n,o,d=CHECK_USER_TIMEOUT_MS){const a={};let c;s&&"unknown"!==s&&(a.deviceId=s),n&&(a.fingerprintId=n);const u=await Promise.race([e.checkUser(i,a),new Promise(e=>{c=setTimeout(()=>e(null),d)})]);if(clearTimeout(c),!u)return{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:o,cachedAt:0,ttl:0};const _=u.data?.is_user_flagged??!1;return t.set(r,{isFlagged:_,isVerified:!1,emailAddress:i,sessionId:o}),t.get(r)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return(0,cookies_1.parseCookie)(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return(0,cookies_1.parseCookie)(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.ACCOUNT_FLAGGED_ERROR=exports.flaggedResponse=exports.VerdictCache=void 0,exports.unsharedBoundToUser=unsharedBoundToUser;const fs_1=require("fs"),verdict_cache_1=require("./verdict-cache");Object.defineProperty(exports,"VerdictCache",{enumerable:!0,get:function(){return verdict_cache_1.VerdictCache}});const rate_limit_backoff_1=require("./rate-limit-backoff"),dispatch_dedupe_1=require("./dispatch-dedupe"),response_interceptor_1=require("./response-interceptor"),fingerprint_script_1=require("./injection/fingerprint-script"),submit_fp_1=require("./routes/submit-fp"),verify_1=require("./routes/verify"),interstitial_1=require("./routes/interstitial"),gate_page_1=require("./injection/gate-page"),http_helpers_1=require("./utils/http-helpers"),content_type_1=require("./utils/content-type"),flagged_response_1=require("./utils/flagged-response"),skip_paths_1=require("./utils/skip-paths"),include_path_1=require("./utils/include-path"),is_bot_1=require("./utils/is-bot"),client_ip_1=require("./utils/client-ip"),cookies_1=require("./utils/cookies"),device_id_1=require("./utils/device-id"),secure_1=require("./utils/secure"),sentinel_user_id_1=require("./utils/sentinel-user-id");var flagged_response_2=require("./utils/flagged-response");Object.defineProperty(exports,"flaggedResponse",{enumerable:!0,get:function(){return flagged_response_2.flaggedResponse}}),Object.defineProperty(exports,"ACCOUNT_FLAGGED_ERROR",{enumerable:!0,get:function(){return flagged_response_2.ACCOUNT_FLAGGED_ERROR}});const CHECK_USER_TIMEOUT_MS=500;function unsharedBoundToUser(e,r){if(!r.userId)throw new Error("[Unshared] userId resolver is required");if(!r.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:t,emailAddress:i,routePrefix:s="/__unshared",corsOrigins:n,cacheTTL:o=6e4,skipPaths:d,includePathPrefix:a,disableBotFilter:c=!1,checkUserTimeoutMs:u=CHECK_USER_TIMEOUT_MS,sessionId:l,deviceId:_,onFlagged:p,onError:h,onFailOpen:f,blockFlagged:g=!1,autoInterstitial:v=!1,interstitialFlowType:m="email_verification"}=r,I=e=>{if(f)try{f(e)}catch{}},k=new verdict_cache_1.VerdictCache(o),S=new rate_limit_backoff_1.RateLimitBackoff,C=new dispatch_dedupe_1.DispatchDedupe,y=Date.now().toString(36),A=(0,fingerprint_script_1.generateFingerprintScript)(s,y,{autoInterstitial:v,interstitialFlowType:m});let x="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");x=(0,fs_1.readFileSync)(e,"utf8")}catch{}if(g&&!x)throw new Error("[Unshared] blockFlagged requires unshared-frontend-sdk to be installed (its UMD bundle is the gate-page renderer).");if(g&&"/__unshared"!==s)throw new Error('[Unshared] blockFlagged requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');if(v&&!x)throw new Error("[Unshared] autoInterstitial requires unshared-frontend-sdk to be installed (its UMD bundle boots the auto-rendered interstitial).");if(v&&"/__unshared"!==s)throw new Error('[Unshared] autoInterstitial requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');const b=g?(0,gate_page_1.generateGatePage)(s,y):"",E=(0,submit_fp_1.handleSubmitFingerprint)({client:e,verdictCache:k,rateLimitBackoff:S,dispatchDedupe:C,resolveUserId:t,resolveEmailAddress:i,resolveSessionId:l,resolveDeviceId:_,disableBotFilter:c,onError:h}),w=(0,verify_1.handleVerifyTrigger)({client:e,verdictCache:k,resolveEmailAddress:i,resolveDeviceId:_,onError:h}),U=(0,verify_1.handleVerify)({client:e,verdictCache:k,resolveEmailAddress:i,resolveDeviceId:_,onError:h}),T=(0,interstitial_1.handleGetInterstitialFlow)({client:e}),q=n?Array.isArray(n)?n:[n]:null,F=`${s}/fp.js`,O=`${s}/submit-fp`,P=`${s}/verify-trigger`,M=`${s}/verify`,j=`${s}/status`,$=`${s}/interstitial-flow`;return function(r,n,o){const f=(0,http_helpers_1.getRequestPath)(r.url),v=r.url||f;if(f.startsWith(s+"/")){if(function(e,r){if(!q)return;const t=e.headers.origin??"",i=q.includes("*");(i||q.includes(t))&&(r.setHeader("Access-Control-Allow-Origin",i?"*":t),r.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),r.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),r.setHeader("Access-Control-Allow-Credentials","true"))}(r,n),"OPTIONS"===r.method)return void(0,http_helpers_1.sendEmpty)(n,204);if("GET"===r.method&&f===F)return n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBody)(n,200,x);if("POST"===r.method&&(f===O||f===P||f===M))return void 0===r.body?void(0,http_helpers_1.sendJson)(n,400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"req.body is undefined. Mount a JSON body-parsing middleware (e.g., express.json()) before the Unshared middleware."}}):f===O?void E(r,n):f===P?void w(r,n):void U(r,n);if("GET"===r.method&&f===$)return void T(r,n);if("GET"===r.method&&f===j){let s;try{s=t(r)}catch{}if(!s)return void(0,http_helpers_1.sendJson)(n,200,{status:"anonymous"});const o=resolveEmail(r,i);return void(async()=>{let t=k.get(s);if((!t||k.isStale(s))&&o&&!S.isPaused()&&!k.isRefreshing(s)){k.markRefreshing(s);try{const i=(0,device_id_1.extractDeviceIdOrUndefined)(r,_),n=extractFingerprintId(r),d=extractSessionId(r,l),a=i??n??"unknown";await fetchAndCacheVerdict(e,k,s,o,a,n,d,u,(e,r)=>I({operation:"checkUser",reason:e,status:r,userId:s,emailAddress:o})),t=k.get(s)}catch(e){h&&h(e,{operation:"checkUser",userId:s,emailAddress:o}),I({operation:"checkUser",reason:"exception",userId:s,emailAddress:o})}finally{k.clearRefreshing(s)}}t&&t.isFlagged&&!t.isVerified&&p&&o?(0,http_helpers_1.sendJson)(n,200,{status:"flagged",email:o}):(0,http_helpers_1.sendJson)(n,200,{status:"ok"})})()}return void(0,http_helpers_1.sendJson)(n,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if((0,skip_paths_1.shouldSkipPath)(f,d))return void o();if(!(0,include_path_1.shouldIncludePath)(f,a))return interceptForInjection(r,n,A),void o();let m;try{m=t(r)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(m)){const e=(0,cookies_1.parseCookie)(r,"__unshared_uid"),t=(0,cookies_1.parseCookie)(r,"__unshared_uid_at"),i=t?Number(t):NaN,s=Number.isFinite(i)&&Date.now()-i<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;m=e&&s?e:void 0}if(!m){const e=(0,secure_1.isSecureRequest)(r)?"; Secure":"";return appendSetCookie(n,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(r,n,A),void o()}const y=resolveEmail(r,i);if(setUserIdCookie(r,n,m),y&&setEmailCookie(r,n,y),!y)return interceptForInjection(r,n,A),void o();const D=extractSessionId(r,l),N=(0,device_id_1.extractDeviceIdOrUndefined)(r,_),R=extractFingerprintId(r),L=r.headers["user-agent"]??"",V=(0,client_ip_1.extractClientIp)(r),G=N??R;if(!c&&(0,is_bot_1.isBot)(L))return void o();const B=k.get(m);function H(){"unknown"!==D&&G&&(S.isPaused()||dispatchUserEvent(e,k,S,C,{userId:m,emailAddress:y,sessionId:D,deviceId:G,fingerprintId:R,userAgent:L,ipAddress:V,eventType:v},h))}B?(k.isStale(m)&&!k.isRefreshing(m)&&(k.markRefreshing(m),fetchAndCacheVerdict(e,k,m,y,G??"unknown",R,D,u,(e,r)=>I({operation:"checkUser",reason:e,status:r,userId:m,emailAddress:y})).catch(()=>I({operation:"checkUser",reason:"exception",userId:m,emailAddress:y})).finally(()=>k.clearRefreshing(m))),B.isFlagged||H(),applyVerdict(B,m,y,r,n,o,A,p,g,b)):fetchAndCacheVerdict(e,k,m,y,G??"unknown",R,D,u,(e,r)=>I({operation:"checkUser",reason:e,status:r,userId:m,emailAddress:y})).then(e=>{e.isFlagged||H(),applyVerdict(e,m,y,r,n,o,A,p,g,b)}).catch(()=>{I({operation:"checkUser",reason:"exception",userId:m,emailAddress:y}),H(),interceptForInjection(r,n,A),o()})}}function resolveEmail(e,r){if(r)try{const t=r(e);if(t)return t}catch{}const t=(0,cookies_1.parseCookie)(e,"__unshared_email");if(t)return t;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,r,t,i,s,n,o,d,a,c){if(a&&e.isFlagged&&!e.isVerified)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(s.statusCode=200,s.setHeader("Content-Type","text/html; charset=utf-8"),s.setHeader("Cache-Control","no-store"),s.end(c)):(0,http_helpers_1.sendJson)(s,403,(0,flagged_response_1.flaggedResponse)(t));else if(interceptForInjection(i,s,o),e.isFlagged&&!e.isVerified&&d)try{d({userId:r,emailAddress:t,verdict:e,req:i,res:s,next:n})}catch{n()}else n()}function interceptForInjection(e,r,t){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],(0,response_interceptor_1.interceptResponse)(r,(e,r)=>{if(!(0,content_type_1.isHtmlContentType)(r))return null;const i=e.toString("utf8"),s=i.lastIndexOf("</body>");return-1===s?i+t:i.slice(0,s)+t+i.slice(s)},{preventCaching:!0})}function dispatchUserEvent(e,r,t,i,s,n){i.mark(s.userId,s.eventType),e.processUserEvent({eventType:s.eventType,userId:s.userId,emailAddress:s.emailAddress,ipAddress:s.ipAddress,deviceId:s.deviceId,fingerprintId:s.fingerprintId,sessionHash:s.sessionId,userAgent:s.userAgent}).then(e=>{e.success&&e.data?.analysis&&r.update(s.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.pause(1e3*e.error.retryAfter)}).catch(e=>{n&&n(e,{operation:"processUserEvent",userId:s.userId,emailAddress:s.emailAddress})})}async function fetchAndCacheVerdict(e,r,t,i,s,n,o,d=CHECK_USER_TIMEOUT_MS,a){const c={};let u;s&&"unknown"!==s&&(c.deviceId=s),n&&(c.fingerprintId=n);const l=await Promise.race([e.checkUser(i,c),new Promise(e=>{u=setTimeout(()=>e(null),d)})]);if(clearTimeout(u),!l)return a?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:o,cachedAt:0,ttl:0};l.failedOpen&&a?.("http_error",l.failedOpen.status);const _=l.data?.is_user_flagged??!1;return r.set(t,{isFlagged:_,isVerified:!1,emailAddress:i,sessionId:o}),r.get(t)}function extractSessionId(e,r){if(r)try{const t=r(e);if(t)return t}catch{}return(0,cookies_1.parseCookie)(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return(0,cookies_1.parseCookie)(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,r){const t=e.getHeader("Set-Cookie");if(t){const i=Array.isArray(t)?[...t]:[String(t)];i.push(r),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",r)}function setUserIdCookie(e,r,t){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(r,`__unshared_uid=${encodeURIComponent(t)}; Path=/; SameSite=Lax${i}`),appendSetCookie(r,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,r,t){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(r,`__unshared_email=${encodeURIComponent(t)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
@@ -12,4 +12,4 @@
12
12
  * NOTE: the browser SDK's proxy paths are hardcoded to the default `/__unshared` prefix,
13
13
  * so block mode requires the default routePrefix (the middleware validates this).
14
14
  */
15
- export declare function generateGatePage(routePrefix: string): string;
15
+ export declare function generateGatePage(routePrefix: string, version?: string): string;
@@ -1 +1 @@
1
- "use strict";function generateGatePage(n){return`<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n<meta name="robots" content="noindex,nofollow">\n<title>Verification required</title>\n<style>\nhtml,body{margin:0;height:100%;background:#0b0b0c;font-family:system-ui,sans-serif}\n#__unshared_gate_fallback{\n position:fixed;inset:0;display:flex;flex-direction:column;\n align-items:center;justify-content:center;gap:16px;padding:24px;\n text-align:center;color:#e5e7eb;\n}\n#__unshared_gate_fallback p{margin:0;font-size:16px;line-height:1.5;max-width:360px}\n#__unshared_gate_fallback button{\n display:none;padding:10px 20px;font-size:15px;font-weight:600;cursor:pointer;\n color:#fff;background:#4f46e5;border:none;border-radius:8px;font-family:inherit;\n}\n</style>\n</head>\n<body>\n<noscript>Verification is required to continue. Please enable JavaScript.</noscript>\n\x3c!-- Always-visible fallback so a slow/failed modal render is never a blank black void.\n The interstitial modal (full-viewport overlay, z-index 2147483647) covers this once\n it mounts; if it never mounts, the user sees a legible message instead of nothing. --\x3e\n<div id="__unshared_gate_fallback" role="status" aria-live="polite">\n <p id="__unshared_gate_msg">Verifying your account&hellip;</p>\n <button id="__unshared_gate_reload" type="button">Refresh to try again</button>\n</div>\n<script src="${n}/fp.js"><\/script>\n<script>\n(function(){\n function showError(){\n try{\n var msg = document.getElementById('__unshared_gate_msg');\n var btn = document.getElementById('__unshared_gate_reload');\n if(msg){ msg.textContent = "We couldn't load verification. Refresh to try again."; }\n if(btn){ btn.style.display = 'inline-block'; btn.onclick = function(){ try{ location.reload(); }catch(e){} }; }\n }catch(e){}\n }\n function boot(){\n try{\n var ns = window.UnsharedBrowser;\n if(!ns || !ns.UnsharedBrowser){ showError(); return; }\n var sdk = new ns.UnsharedBrowser({ baseUrl: '' });\n Promise.resolve(sdk.showInterstitial({ onComplete: function(){ try{ location.reload(); }catch(e){} } }))\n .then(function(res){ if(!res || !res.success){ showError(); } })\n .catch(function(){ showError(); });\n }catch(e){ showError(); }\n }\n if(window.UnsharedBrowser){ boot(); return; }\n var tries = 0;\n var timer = setInterval(function(){\n if(window.UnsharedBrowser || tries++ > 50){ clearInterval(timer); boot(); }\n }, 100);\n})();\n<\/script>\n</body>\n</html>`}Object.defineProperty(exports,"t",{value:!0}),exports.generateGatePage=generateGatePage;
1
+ "use strict";function generateGatePage(n,e){return`<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n<meta name="robots" content="noindex,nofollow">\n<title>Verification required</title>\n<style>\nhtml,body{margin:0;height:100%;background:#0b0b0c;font-family:system-ui,sans-serif}\n#__unshared_gate_fallback{\n position:fixed;inset:0;display:flex;flex-direction:column;\n align-items:center;justify-content:center;gap:16px;padding:24px;\n text-align:center;color:#e5e7eb;\n}\n#__unshared_gate_fallback p{margin:0;font-size:16px;line-height:1.5;max-width:360px}\n#__unshared_gate_fallback button{\n display:none;padding:10px 20px;font-size:15px;font-weight:600;cursor:pointer;\n color:#fff;background:#4f46e5;border:none;border-radius:8px;font-family:inherit;\n}\n</style>\n</head>\n<body>\n<noscript>Verification is required to continue. Please enable JavaScript.</noscript>\n\x3c!-- Always-visible fallback so a slow/failed modal render is never a blank black void.\n The interstitial modal (full-viewport overlay, z-index 2147483647) covers this once\n it mounts; if it never mounts, the user sees a legible message instead of nothing. --\x3e\n<div id="__unshared_gate_fallback" role="status" aria-live="polite">\n <p id="__unshared_gate_msg">Verifying your account&hellip;</p>\n <button id="__unshared_gate_reload" type="button">Refresh to try again</button>\n</div>\n<script src="${n}/fp.js${e?`?v=${encodeURIComponent(e)}`:""}"><\/script>\n<script>\n(function(){\n function showError(){\n try{\n var msg = document.getElementById('__unshared_gate_msg');\n var btn = document.getElementById('__unshared_gate_reload');\n if(msg){ msg.textContent = "We couldn't load verification. Refresh to try again."; }\n if(btn){ btn.style.display = 'inline-block'; btn.onclick = function(){ try{ location.reload(); }catch(e){} }; }\n }catch(e){}\n }\n function boot(){\n try{\n var ns = window.UnsharedBrowser;\n if(!ns || !ns.UnsharedBrowser){ showError(); return; }\n var sdk = new ns.UnsharedBrowser({ baseUrl: '' });\n Promise.resolve(sdk.showInterstitial({ onComplete: function(){ try{ location.reload(); }catch(e){} } }))\n .then(function(res){ if(!res || !res.success){ showError(); } })\n .catch(function(){ showError(); });\n }catch(e){ showError(); }\n }\n if(window.UnsharedBrowser){ boot(); return; }\n var tries = 0;\n var timer = setInterval(function(){\n if(window.UnsharedBrowser || tries++ > 50){ clearInterval(timer); boot(); }\n }, 100);\n})();\n<\/script>\n</body>\n</html>`}Object.defineProperty(exports,"t",{value:!0}),exports.generateGatePage=generateGatePage;
@@ -8,5 +8,10 @@ export interface InterstitialDependencies {
8
8
  * Fetches the published interstitial flow via the secret-key client and returns it
9
9
  * to the browser. Carries no user data — only the flow definition. Never 500s
10
10
  * (mirrors the verify routes' always-200 envelope style).
11
+ *
12
+ * When no flow is published (upstream 404, or success with empty `data`) it returns a
13
+ * distinguishable `FLOW_NOT_PUBLISHED` error so an integrator can tell a provisioning
14
+ * gap apart from a transport/auth failure in the network tab. Other upstream errors
15
+ * pass through unchanged.
11
16
  */
12
17
  export declare function handleGetInterstitialFlow(deps: InterstitialDependencies): (req: UnsharedRequest, res: UnsharedResponse) => Promise<void>;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.handleGetInterstitialFlow=handleGetInterstitialFlow;const http_helpers_1=require("../utils/http-helpers");function handleGetInterstitialFlow(e){return async(t,s)=>{try{const r=t.url&&t.url.includes("?")?t.url.slice(t.url.indexOf("?")+1):"",l=new URLSearchParams(r),a=l.get("flow_type")||void 0,o=l.get("platform")||void 0,i=await e.client.getInterstitialFlow({flowType:a,platform:o});i.success?(0,http_helpers_1.sendJson)(s,200,{success:!0,data:i.data}):(0,http_helpers_1.sendJson)(s,200,{success:!1,error:i.error??{code:"FLOW_FETCH_FAILED",message:"Failed to load interstitial flow"}})}catch{(0,http_helpers_1.sendJson)(s,200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to load interstitial flow"}})}}}
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.handleGetInterstitialFlow=handleGetInterstitialFlow;const http_helpers_1=require("../utils/http-helpers");function isPublishedFlow(e){return!!e&&"object"==typeof e&&Object.keys(e).length>0}const FLOW_NOT_PUBLISHED={code:"FLOW_NOT_PUBLISHED",message:"No interstitial flow is published for this company."};function handleGetInterstitialFlow(e){return async(t,s)=>{try{const r=t.url&&t.url.includes("?")?t.url.slice(t.url.indexOf("?")+1):"",o=new URLSearchParams(r),l=o.get("flow_type")||void 0,i=o.get("platform")||void 0,a=await e.client.getInterstitialFlow({flowType:l,platform:i});a.success?isPublishedFlow(a.data)?(0,http_helpers_1.sendJson)(s,200,{success:!0,data:a.data}):(0,http_helpers_1.sendJson)(s,200,{success:!1,error:{...FLOW_NOT_PUBLISHED}}):404===a.status?(0,http_helpers_1.sendJson)(s,200,{success:!1,error:{...FLOW_NOT_PUBLISHED}}):(0,http_helpers_1.sendJson)(s,200,{success:!1,error:a.error??{code:"FLOW_FETCH_FAILED",message:"Failed to load interstitial flow"}})}catch{(0,http_helpers_1.sendJson)(s,200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to load interstitial flow"}})}}}
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.createWebProtectionMiddleware=createWebProtectionMiddleware;const util_1=require("../util"),verdict_cache_1=require("../middleware/verdict-cache"),rate_limit_backoff_1=require("../middleware/rate-limit-backoff"),dispatch_dedupe_1=require("../middleware/dispatch-dedupe"),fingerprint_script_1=require("../middleware/injection/fingerprint-script"),gate_page_1=require("../middleware/injection/gate-page"),flagged_response_1=require("../middleware/utils/flagged-response"),content_type_1=require("../middleware/utils/content-type"),skip_paths_1=require("../middleware/utils/skip-paths"),include_path_1=require("../middleware/utils/include-path"),is_bot_1=require("../middleware/utils/is-bot"),sentinel_user_id_1=require("../middleware/utils/sentinel-user-id"),web_helpers_1=require("./web-helpers"),CHECK_USER_TIMEOUT_MS=500;function createWebProtectionMiddleware(e,r){if(!r.userId)throw new Error("[Unshared] userId resolver is required");const{userId:t,emailAddress:s,routePrefix:i="/__unshared",corsOrigins:n,cacheTTL:a=6e4,skipPaths:o,includePathPrefix:d,sessionId:_,deviceId:c,fingerprintSdkBundle:l="",onFlagged:u,onError:h,disableBotFilter:p=!1,checkUserTimeoutMs:f=CHECK_USER_TIMEOUT_MS,blockFlagged:w=!1,autoInterstitial:m=!1,interstitialFlowType:g="email_verification"}=r;if(w&&!l)throw new Error("[Unshared] blockFlagged requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the gate page).");if(w&&"/__unshared"!==i)throw new Error('[Unshared] blockFlagged requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');if(m&&!l)throw new Error("[Unshared] autoInterstitial requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js boots the auto-rendered interstitial).");if(m&&"/__unshared"!==i)throw new Error('[Unshared] autoInterstitial requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');const b=w?(0,gate_page_1.generateGatePage)(i):"",v=new verdict_cache_1.VerdictCache(a),I=new rate_limit_backoff_1.RateLimitBackoff,y=new dispatch_dedupe_1.DispatchDedupe,S=Date.now().toString(36),A=(0,fingerprint_script_1.generateFingerprintScript)(i,S,{autoInterstitial:m,interstitialFlowType:g}),E=`${i}/fp.js`,R=`${i}/submit-fp`,T=`${i}/verify-trigger`,x=`${i}/verify`,C=`${i}/status`,U=n?Array.isArray(n)?n:[n]:null;return async function(r,n){let a,m,g;try{const e=new URL(r.url);a=e.pathname,m=e.search}catch{return n(r)}if(a.startsWith(i+"/")){const i=function(e){if(!U)return{};const r=e.headers.get("origin")??"",t=U.includes("*");return t||U.includes(r)?{"Access-Control-Allow-Origin":t?"*":r,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id","Access-Control-Allow-Credentials":"true"}:{}}(r);if("OPTIONS"===r.method)return(0,web_helpers_1.emptyResponse)(204,i);if("GET"===r.method&&a===E)return l?(0,web_helpers_1.bodyResponse)(200,l,{...i,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600"}):(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},i);if("POST"===r.method&&(a===R||a===T||a===x)){let n;try{n=await r.json()}catch{return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"Request body is not valid JSON."}},i)}return a===R?handleSubmitFp(r,n,{client:e,verdictCache:v,rateLimitBackoff:I,dispatchDedupe:y,resolveUserId:t,resolveEmailAddress:s,resolveSessionId:_,resolveDeviceId:c,disableBotFilter:p,onError:h},i):a===T?handleVerifyTriggerWeb(r,n,{client:e,verdictCache:v,resolveEmailAddress:s,resolveDeviceId:c,onError:h},i):handleVerifyWeb(r,n,{client:e,verdictCache:v,resolveEmailAddress:s,resolveDeviceId:c,onError:h},i)}if("GET"===r.method&&a===C){let n;try{n=t(r)}catch{}if(!n)return(0,web_helpers_1.jsonResponse)(200,{status:"anonymous"},i);const a=resolveEmail(r,s);let o=v.get(n);if((!o||v.isStale(n))&&a&&!I.isPaused()&&!v.isRefreshing(n)){v.markRefreshing(n);try{const t=(0,web_helpers_1.extractDeviceIdFromRequest)(r,c),s=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_fingerprint_id")||void 0,i=extractSessionIdFromRequest(r,_),d=t??s??"unknown";await fetchAndCacheVerdict(e,v,n,a,d,s,i,f),o=v.get(n)}catch(e){h&&h(e,{operation:"checkUser",userId:n,emailAddress:a})}finally{v.clearRefreshing(n)}}return o&&o.isFlagged&&!o.isVerified&&u&&a?(0,web_helpers_1.jsonResponse)(200,{status:"flagged",email:a},i):(0,web_helpers_1.jsonResponse)(200,{status:"ok"},i)}return(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},i)}if((0,skip_paths_1.shouldSkipPath)(a,o))return n(r);if(!(0,include_path_1.shouldIncludePath)(a,d))return injectIntoHtmlResponse(await n(r),A);try{g=t(r)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(g)){const e=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_uid"),t=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_uid_at"),s=t?Number(t):NaN,i=Number.isFinite(s)&&Date.now()-s<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;g=e&&i?e:void 0}if(!g){const e=(0,web_helpers_1.isSecureWebRequest)(r)?"; Secure":"",t=[`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`];return injectIntoHtmlResponse(await n(r),A,t)}const S=resolveEmail(r,s),k=[],O=(0,web_helpers_1.isSecureWebRequest)(r)?"; Secure":"";if(k.push(`__unshared_uid=${encodeURIComponent(g)}; Path=/; SameSite=Lax${O}`),k.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${O}`),S&&k.push(`__unshared_email=${encodeURIComponent(S)}; HttpOnly; Path=/; SameSite=Lax${O}`),!S)return injectIntoHtmlResponse(await n(r),A,k);const P=extractSessionIdFromRequest(r,_),$=(0,web_helpers_1.extractDeviceIdFromRequest)(r,c),F=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_fingerprint_id")||void 0,q=r.headers.get("user-agent")??"",D=(0,web_helpers_1.extractClientIpFromRequest)(r),L=$??F;if(!p&&(0,is_bot_1.isBot)(q))return n(r);let M=v.get(g);if(M)v.isStale(g)&&!v.isRefreshing(g)&&(v.markRefreshing(g),fetchAndCacheVerdict(e,v,g,S,L??"unknown",F,P,f).finally(()=>v.clearRefreshing(g)));else try{M=await fetchAndCacheVerdict(e,v,g,S,L??"unknown",F,P,f)}catch{return injectIntoHtmlResponse(await n(r),A,k)}if(w&&M.isFlagged&&!M.isVerified)return(0,content_type_1.isHtmlNavigation)(r.method,r.headers.get("accept")??void 0)?(0,web_helpers_1.bodyResponse)(200,b,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}):(0,web_helpers_1.jsonResponse)(403,(0,flagged_response_1.flaggedResponse)(S));if(M.isFlagged&&!M.isVerified&&u)try{const e=await u({userId:g,emailAddress:S,verdict:M,request:r});if(e)return injectIntoHtmlResponse(e,A,k)}catch(e){h&&h(e,{operation:"checkUser",userId:g,emailAddress:S})}return M.isFlagged||"unknown"===P||!L||I.isPaused()||dispatchUserEvent(e,v,I,y,{userId:g,emailAddress:S,sessionId:P,deviceId:L,fingerprintId:F,userAgent:q,ipAddress:D,eventType:a+m},h),injectIntoHtmlResponse(await n(r),A,k)}}async function injectIntoHtmlResponse(e,r,t){const s=e.headers.get("content-type");if(!(0,content_type_1.isHtmlContentType)(s??void 0)){if(!t||0===t.length)return e;const r=(0,web_helpers_1.mergeResponseHeaders)(e.headers,void 0,t);return new Response(e.body,{status:e.status,statusText:e.statusText,headers:r})}const i=await e.text(),n=i.lastIndexOf("</body>"),a=-1===n?i+r:i.slice(0,n)+r+i.slice(n),o=(0,web_helpers_1.mergeResponseHeaders)(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(a).length)},t);return o.delete("ETag"),o.delete("Last-Modified"),o.delete("Content-Encoding"),new Response(a,{status:e.status,statusText:e.statusText,headers:o})}function resolveEmail(e,r){if(r)try{const t=r(e);if(t)return t}catch{}const t=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email");if(t)return t}function resolveEmailWithBody(e,r,t){const s=resolveEmail(e,t);if(s)return s;const i=r.email;return"string"==typeof i&&i?i:void 0}function extractSessionIdFromRequest(e,r){if(r)try{const t=r(e);if(t)return t}catch{}return(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid")??"unknown"}function dispatchUserEvent(e,r,t,s,i,n){s.mark(i.userId,i.eventType),e.processUserEvent({eventType:i.eventType,userId:i.userId,emailAddress:i.emailAddress,ipAddress:i.ipAddress,deviceId:i.deviceId,fingerprintId:i.fingerprintId,sessionHash:i.sessionId,userAgent:i.userAgent}).then(e=>{e.success&&e.data?.analysis&&r.update(i.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.pause(1e3*e.error.retryAfter)}).catch(e=>{n&&n(e,{operation:"processUserEvent",userId:i.userId,emailAddress:i.emailAddress})})}async function fetchAndCacheVerdict(e,r,t,s,i,n,a,o=CHECK_USER_TIMEOUT_MS){const d={};let _;i&&"unknown"!==i&&(d.deviceId=i),n&&(d.fingerprintId=n);const c=await Promise.race([e.checkUser(s,d),new Promise(e=>{_=setTimeout(()=>e(null),o)})]);if(clearTimeout(_),!c)return{isFlagged:!1,isVerified:!1,emailAddress:s,sessionId:a,cachedAt:0,ttl:0};const l=c.data?.is_user_flagged??!1;return r.set(t,{isFlagged:l,isVerified:!1,emailAddress:s,sessionId:a}),r.get(t)}async function handleSubmitFp(e,r,t,s){try{const i={full_hash:r.hash??"",fingerprint_id:r.stable_hash??"",timestamp:r.collected_at??(new Date).toISOString(),isIncognito:r.is_incognito??!1,components:r.components??{},version:r.version??"inline-1.0.0"};let n,a,o;try{const r=t.resolveUserId(e);r&&!(0,sentinel_user_id_1.isSentinelUserId)(r)&&(n=r)}catch{}if(!n){const e="string"==typeof r.user_id?r.user_id:void 0;e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(n=e)}if(!n){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_uid");r&&!(0,sentinel_user_id_1.isSentinelUserId)(r)&&(n=r)}try{a=t.resolveEmailAddress?t.resolveEmailAddress(e):void 0}catch{}a=a??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")??r.email??void 0;try{o=t.resolveSessionId?t.resolveSessionId(e):void 0}catch{}o=o??r.session_id??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid");const d=(0,web_helpers_1.extractClientIpFromRequest)(e),_=e.headers.get("user-agent")??"";if(!t.disableBotFilter&&(0,is_bot_1.isBot)(_))return(0,web_helpers_1.jsonResponse)(200,{success:!0},s);const c=(i.fingerprint_id&&i.fingerprint_id.length>0?i.fingerprint_id:void 0)??(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),l=i.fingerprint_id||void 0,u=i.full_hash||void 0,h=(0,web_helpers_1.isSecureWebRequest)(e)?"; Secure":"",p=[];if(u&&!(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")&&p.push(`__unshared_fingerprint_id=${encodeURIComponent(u)}; HttpOnly; Path=/; SameSite=Lax${h}`),l){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fp_id");r&&r===l||p.push(`__unshared_fp_id=${encodeURIComponent(l)}; Path=/; SameSite=Lax; Max-Age=31536000${h}`)}let f;if(a&&!(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")&&p.push(`__unshared_email=${encodeURIComponent(a)}; HttpOnly; Path=/; SameSite=Lax${h}`),"string"==typeof r.event_type&&r.event_type)f=r.event_type;else{const r=e.headers.get("referer")??e.headers.get("referrer");let t="unknown";if(r)try{const e=new URL(r);t=(e.pathname||"/")+(e.search||"")}catch{}f=t}const w=e.headers.get("x-idempotency-key")||void 0,m=Date.now(),g=w?`${w}|${m}`:l&&n?`${(0,util_1.sha256Hex)(`${l}|${n}|${f}`)}|${m}`:void 0;n&&t.client.submitFingerprintEvent(i,{userId:n,emailAddress:a,sessionHash:o,eventType:f,ipAddress:d,userAgent:_,idempotencyKey:g}).catch(e=>{t.onError&&t.onError(e,{operation:"submitFingerprintEvent",userId:n,emailAddress:a})}),n&&a&&!t.rateLimitBackoff.isPaused()&&!t.dispatchDedupe.wasRecentlyDispatched(n,f)&&t.client.processUserEvent({eventType:f,userId:n,emailAddress:a,ipAddress:d,deviceId:c,fingerprintId:l,sessionHash:o??"unknown",userAgent:_}).then(e=>{e.success&&e.data?.analysis&&t.verdictCache.update(n,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.rateLimitBackoff.pause(1e3*e.error.retryAfter)}).catch(e=>{t.onError&&t.onError(e,{operation:"processUserEvent",userId:n,emailAddress:a})});const b={...s,"Content-Type":"application/json"},v=new Response(JSON.stringify({success:!0}),{status:200,headers:b});for(const e of p)v.headers.append("Set-Cookie",e);return v}catch{return(0,web_helpers_1.jsonResponse)(200,{success:!0},s)}}async function handleVerifyTriggerWeb(e,r,t,s){try{const i=resolveEmailWithBody(e,r??{},t.resolveEmailAddress);if(!i)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},s);const n=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),a=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,o=await t.client.triggerEmailVerification(i,n,{fingerprintId:a});return o.success?(0,web_helpers_1.jsonResponse)(200,{success:!0,data:o.data},s):(0,web_helpers_1.jsonResponse)(200,{success:!1,error:o.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},s)}catch(e){return t.onError&&t.onError(e,{operation:"verifyTrigger"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},s)}}async function handleVerifyWeb(e,r,t,s){try{const i=resolveEmailWithBody(e,r??{},t.resolveEmailAddress),n=r?.code;if(!i||!n)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}},s);const a=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),o=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,d=await t.client.verify(i,a,n,{fingerprintId:o});if(d.success){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_uid");return r&&t.verdictCache.update(r,{isVerified:!0}),(0,web_helpers_1.jsonResponse)(200,{success:!0,data:{verified:!0}},s)}return(0,web_helpers_1.jsonResponse)(200,{success:!1,error:d.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}},s)}catch(e){return t.onError&&t.onError(e,{operation:"verify"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},s)}}
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.createWebProtectionMiddleware=createWebProtectionMiddleware;const util_1=require("../util"),verdict_cache_1=require("../middleware/verdict-cache"),rate_limit_backoff_1=require("../middleware/rate-limit-backoff"),dispatch_dedupe_1=require("../middleware/dispatch-dedupe"),fingerprint_script_1=require("../middleware/injection/fingerprint-script"),gate_page_1=require("../middleware/injection/gate-page"),flagged_response_1=require("../middleware/utils/flagged-response"),content_type_1=require("../middleware/utils/content-type"),skip_paths_1=require("../middleware/utils/skip-paths"),include_path_1=require("../middleware/utils/include-path"),is_bot_1=require("../middleware/utils/is-bot"),sentinel_user_id_1=require("../middleware/utils/sentinel-user-id"),web_helpers_1=require("./web-helpers"),CHECK_USER_TIMEOUT_MS=500;function createWebProtectionMiddleware(e,r){if(!r.userId)throw new Error("[Unshared] userId resolver is required");const{userId:t,emailAddress:s,routePrefix:i="/__unshared",corsOrigins:n,cacheTTL:a=6e4,skipPaths:o,includePathPrefix:d,sessionId:_,deviceId:c,fingerprintSdkBundle:l="",onFlagged:u,onError:h,onFailOpen:p,disableBotFilter:f=!1,checkUserTimeoutMs:w=CHECK_USER_TIMEOUT_MS,blockFlagged:m=!1,autoInterstitial:g=!1,interstitialFlowType:b="email_verification"}=r,I=e=>{if(p)try{p(e)}catch{}};if(m&&!l)throw new Error("[Unshared] blockFlagged requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the gate page).");if(m&&"/__unshared"!==i)throw new Error('[Unshared] blockFlagged requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');if(g&&!l)throw new Error("[Unshared] autoInterstitial requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js boots the auto-rendered interstitial).");if(g&&"/__unshared"!==i)throw new Error('[Unshared] autoInterstitial requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');const v=m?(0,gate_page_1.generateGatePage)(i):"",y=new verdict_cache_1.VerdictCache(a),A=new rate_limit_backoff_1.RateLimitBackoff,S=new dispatch_dedupe_1.DispatchDedupe,E=Date.now().toString(36),R=(0,fingerprint_script_1.generateFingerprintScript)(i,E,{autoInterstitial:g,interstitialFlowType:b}),T=`${i}/fp.js`,x=`${i}/submit-fp`,C=`${i}/verify-trigger`,U=`${i}/verify`,k=`${i}/status`,O=n?Array.isArray(n)?n:[n]:null;return async function(r,n){let a,p,g;try{const e=new URL(r.url);a=e.pathname,p=e.search}catch{return n(r)}if(a.startsWith(i+"/")){const i=function(e){if(!O)return{};const r=e.headers.get("origin")??"",t=O.includes("*");return t||O.includes(r)?{"Access-Control-Allow-Origin":t?"*":r,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id","Access-Control-Allow-Credentials":"true"}:{}}(r);if("OPTIONS"===r.method)return(0,web_helpers_1.emptyResponse)(204,i);if("GET"===r.method&&a===T)return l?(0,web_helpers_1.bodyResponse)(200,l,{...i,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600"}):(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},i);if("POST"===r.method&&(a===x||a===C||a===U)){let n;try{n=await r.json()}catch{return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"Request body is not valid JSON."}},i)}return a===x?handleSubmitFp(r,n,{client:e,verdictCache:y,rateLimitBackoff:A,dispatchDedupe:S,resolveUserId:t,resolveEmailAddress:s,resolveSessionId:_,resolveDeviceId:c,disableBotFilter:f,onError:h},i):a===C?handleVerifyTriggerWeb(r,n,{client:e,verdictCache:y,resolveEmailAddress:s,resolveDeviceId:c,onError:h},i):handleVerifyWeb(r,n,{client:e,verdictCache:y,resolveEmailAddress:s,resolveDeviceId:c,onError:h},i)}if("GET"===r.method&&a===k){let n;try{n=t(r)}catch{}if(!n)return(0,web_helpers_1.jsonResponse)(200,{status:"anonymous"},i);const a=resolveEmail(r,s);let o=y.get(n);if((!o||y.isStale(n))&&a&&!A.isPaused()&&!y.isRefreshing(n)){y.markRefreshing(n);try{const t=(0,web_helpers_1.extractDeviceIdFromRequest)(r,c),s=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_fingerprint_id")||void 0,i=extractSessionIdFromRequest(r,_),d=t??s??"unknown";await fetchAndCacheVerdict(e,y,n,a,d,s,i,w,(e,r)=>I({operation:"checkUser",reason:e,status:r,userId:n,emailAddress:a})),o=y.get(n)}catch(e){h&&h(e,{operation:"checkUser",userId:n,emailAddress:a}),I({operation:"checkUser",reason:"exception",userId:n,emailAddress:a})}finally{y.clearRefreshing(n)}}return o&&o.isFlagged&&!o.isVerified&&u&&a?(0,web_helpers_1.jsonResponse)(200,{status:"flagged",email:a},i):(0,web_helpers_1.jsonResponse)(200,{status:"ok"},i)}return(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},i)}if((0,skip_paths_1.shouldSkipPath)(a,o))return n(r);if(!(0,include_path_1.shouldIncludePath)(a,d))return injectIntoHtmlResponse(await n(r),R);try{g=t(r)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(g)){const e=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_uid"),t=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_uid_at"),s=t?Number(t):NaN,i=Number.isFinite(s)&&Date.now()-s<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;g=e&&i?e:void 0}if(!g){const e=(0,web_helpers_1.isSecureWebRequest)(r)?"; Secure":"",t=[`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`];return injectIntoHtmlResponse(await n(r),R,t)}const b=resolveEmail(r,s),E=[],P=(0,web_helpers_1.isSecureWebRequest)(r)?"; Secure":"";if(E.push(`__unshared_uid=${encodeURIComponent(g)}; Path=/; SameSite=Lax${P}`),E.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${P}`),b&&E.push(`__unshared_email=${encodeURIComponent(b)}; HttpOnly; Path=/; SameSite=Lax${P}`),!b)return injectIntoHtmlResponse(await n(r),R,E);const $=extractSessionIdFromRequest(r,_),F=(0,web_helpers_1.extractDeviceIdFromRequest)(r,c),q=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_fingerprint_id")||void 0,D=r.headers.get("user-agent")??"",L=(0,web_helpers_1.extractClientIpFromRequest)(r),M=F??q;if(!f&&(0,is_bot_1.isBot)(D))return n(r);let N=y.get(g);if(N)y.isStale(g)&&!y.isRefreshing(g)&&(y.markRefreshing(g),fetchAndCacheVerdict(e,y,g,b,M??"unknown",q,$,w,(e,r)=>I({operation:"checkUser",reason:e,status:r,userId:g,emailAddress:b})).catch(()=>I({operation:"checkUser",reason:"exception",userId:g,emailAddress:b})).finally(()=>y.clearRefreshing(g)));else try{N=await fetchAndCacheVerdict(e,y,g,b,M??"unknown",q,$,w,(e,r)=>I({operation:"checkUser",reason:e,status:r,userId:g,emailAddress:b}))}catch{return I({operation:"checkUser",reason:"exception",userId:g,emailAddress:b}),injectIntoHtmlResponse(await n(r),R,E)}if(m&&N.isFlagged&&!N.isVerified)return(0,content_type_1.isHtmlNavigation)(r.method,r.headers.get("accept")??void 0)?(0,web_helpers_1.bodyResponse)(200,v,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}):(0,web_helpers_1.jsonResponse)(403,(0,flagged_response_1.flaggedResponse)(b));if(N.isFlagged&&!N.isVerified&&u)try{const e=await u({userId:g,emailAddress:b,verdict:N,request:r});if(e)return injectIntoHtmlResponse(e,R,E)}catch(e){h&&h(e,{operation:"checkUser",userId:g,emailAddress:b})}return N.isFlagged||"unknown"===$||!M||A.isPaused()||dispatchUserEvent(e,y,A,S,{userId:g,emailAddress:b,sessionId:$,deviceId:M,fingerprintId:q,userAgent:D,ipAddress:L,eventType:a+p},h),injectIntoHtmlResponse(await n(r),R,E)}}async function injectIntoHtmlResponse(e,r,t){const s=e.headers.get("content-type");if(!(0,content_type_1.isHtmlContentType)(s??void 0)){if(!t||0===t.length)return e;const r=(0,web_helpers_1.mergeResponseHeaders)(e.headers,void 0,t);return new Response(e.body,{status:e.status,statusText:e.statusText,headers:r})}const i=await e.text(),n=i.lastIndexOf("</body>"),a=-1===n?i+r:i.slice(0,n)+r+i.slice(n),o=(0,web_helpers_1.mergeResponseHeaders)(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(a).length)},t);return o.delete("ETag"),o.delete("Last-Modified"),o.delete("Content-Encoding"),new Response(a,{status:e.status,statusText:e.statusText,headers:o})}function resolveEmail(e,r){if(r)try{const t=r(e);if(t)return t}catch{}const t=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email");if(t)return t}function resolveEmailWithBody(e,r,t){const s=resolveEmail(e,t);if(s)return s;const i=r.email;return"string"==typeof i&&i?i:void 0}function extractSessionIdFromRequest(e,r){if(r)try{const t=r(e);if(t)return t}catch{}return(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid")??"unknown"}function dispatchUserEvent(e,r,t,s,i,n){s.mark(i.userId,i.eventType),e.processUserEvent({eventType:i.eventType,userId:i.userId,emailAddress:i.emailAddress,ipAddress:i.ipAddress,deviceId:i.deviceId,fingerprintId:i.fingerprintId,sessionHash:i.sessionId,userAgent:i.userAgent}).then(e=>{e.success&&e.data?.analysis&&r.update(i.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.pause(1e3*e.error.retryAfter)}).catch(e=>{n&&n(e,{operation:"processUserEvent",userId:i.userId,emailAddress:i.emailAddress})})}async function fetchAndCacheVerdict(e,r,t,s,i,n,a,o=CHECK_USER_TIMEOUT_MS,d){const _={};let c;i&&"unknown"!==i&&(_.deviceId=i),n&&(_.fingerprintId=n);const l=await Promise.race([e.checkUser(s,_),new Promise(e=>{c=setTimeout(()=>e(null),o)})]);if(clearTimeout(c),!l)return d?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:s,sessionId:a,cachedAt:0,ttl:0};l.failedOpen&&d?.("http_error",l.failedOpen.status);const u=l.data?.is_user_flagged??!1;return r.set(t,{isFlagged:u,isVerified:!1,emailAddress:s,sessionId:a}),r.get(t)}async function handleSubmitFp(e,r,t,s){try{const i={full_hash:r.hash??"",fingerprint_id:r.stable_hash??"",timestamp:r.collected_at??(new Date).toISOString(),isIncognito:r.is_incognito??!1,components:r.components??{},version:r.version??"inline-1.0.0"};let n,a,o;try{const r=t.resolveUserId(e);r&&!(0,sentinel_user_id_1.isSentinelUserId)(r)&&(n=r)}catch{}if(!n){const e="string"==typeof r.user_id?r.user_id:void 0;e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(n=e)}if(!n){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_uid");r&&!(0,sentinel_user_id_1.isSentinelUserId)(r)&&(n=r)}try{a=t.resolveEmailAddress?t.resolveEmailAddress(e):void 0}catch{}a=a??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")??r.email??void 0;try{o=t.resolveSessionId?t.resolveSessionId(e):void 0}catch{}o=o??r.session_id??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid");const d=(0,web_helpers_1.extractClientIpFromRequest)(e),_=e.headers.get("user-agent")??"";if(!t.disableBotFilter&&(0,is_bot_1.isBot)(_))return(0,web_helpers_1.jsonResponse)(200,{success:!0},s);const c=(i.fingerprint_id&&i.fingerprint_id.length>0?i.fingerprint_id:void 0)??(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),l=i.fingerprint_id||void 0,u=i.full_hash||void 0,h=(0,web_helpers_1.isSecureWebRequest)(e)?"; Secure":"",p=[];if(u&&!(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")&&p.push(`__unshared_fingerprint_id=${encodeURIComponent(u)}; HttpOnly; Path=/; SameSite=Lax${h}`),l){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fp_id");r&&r===l||p.push(`__unshared_fp_id=${encodeURIComponent(l)}; Path=/; SameSite=Lax; Max-Age=31536000${h}`)}let f;if(a&&!(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")&&p.push(`__unshared_email=${encodeURIComponent(a)}; HttpOnly; Path=/; SameSite=Lax${h}`),"string"==typeof r.event_type&&r.event_type)f=r.event_type;else{const r=e.headers.get("referer")??e.headers.get("referrer");let t="unknown";if(r)try{const e=new URL(r);t=(e.pathname||"/")+(e.search||"")}catch{}f=t}const w=e.headers.get("x-idempotency-key")||void 0,m=Date.now(),g=w?`${w}|${m}`:l&&n?`${(0,util_1.sha256Hex)(`${l}|${n}|${f}`)}|${m}`:void 0;n&&t.client.submitFingerprintEvent(i,{userId:n,emailAddress:a,sessionHash:o,eventType:f,ipAddress:d,userAgent:_,idempotencyKey:g}).catch(e=>{t.onError&&t.onError(e,{operation:"submitFingerprintEvent",userId:n,emailAddress:a})}),n&&a&&!t.rateLimitBackoff.isPaused()&&!t.dispatchDedupe.wasRecentlyDispatched(n,f)&&t.client.processUserEvent({eventType:f,userId:n,emailAddress:a,ipAddress:d,deviceId:c,fingerprintId:l,sessionHash:o??"unknown",userAgent:_}).then(e=>{e.success&&e.data?.analysis&&t.verdictCache.update(n,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.rateLimitBackoff.pause(1e3*e.error.retryAfter)}).catch(e=>{t.onError&&t.onError(e,{operation:"processUserEvent",userId:n,emailAddress:a})});const b={...s,"Content-Type":"application/json"},I=new Response(JSON.stringify({success:!0}),{status:200,headers:b});for(const e of p)I.headers.append("Set-Cookie",e);return I}catch{return(0,web_helpers_1.jsonResponse)(200,{success:!0},s)}}async function handleVerifyTriggerWeb(e,r,t,s){try{const i=resolveEmailWithBody(e,r??{},t.resolveEmailAddress);if(!i)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},s);const n=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),a=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,o=await t.client.triggerEmailVerification(i,n,{fingerprintId:a});return o.success?(0,web_helpers_1.jsonResponse)(200,{success:!0,data:o.data},s):(0,web_helpers_1.jsonResponse)(200,{success:!1,error:o.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},s)}catch(e){return t.onError&&t.onError(e,{operation:"verifyTrigger"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},s)}}async function handleVerifyWeb(e,r,t,s){try{const i=resolveEmailWithBody(e,r??{},t.resolveEmailAddress),n=r?.code;if(!i||!n)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}},s);const a=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),o=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,d=await t.client.verify(i,a,n,{fingerprintId:o});if(d.success){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_uid");return r&&t.verdictCache.update(r,{isVerified:!0}),(0,web_helpers_1.jsonResponse)(200,{success:!0,data:{verified:!0}},s)}return(0,web_helpers_1.jsonResponse)(200,{success:!1,error:d.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}},s)}catch(e){return t.onError&&t.onError(e,{operation:"verify"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},s)}}
@@ -138,4 +138,19 @@ export interface WebProtectionConfig {
138
138
  userId?: string;
139
139
  emailAddress?: string;
140
140
  }) => void;
141
+ /**
142
+ * Called whenever a verdict check could NOT be determined and the request
143
+ * therefore passes through (fails open) — see the Node middleware's
144
+ * `onFailOpen`. A non-2xx verdict response is masked into a clean "not flagged"
145
+ * verdict and never surfaces via `onError`; this hook makes that observable.
146
+ * The fail-open behaviour is unchanged; invoked best-effort, never blocks.
147
+ */
148
+ onFailOpen?: (context: WebFailOpenContext) => void;
149
+ }
150
+ export interface WebFailOpenContext {
151
+ operation: 'checkUser';
152
+ reason: 'timeout' | 'http_error' | 'exception';
153
+ status?: number;
154
+ userId?: string;
155
+ emailAddress?: string;
141
156
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unshared-clientjs-sdk",
3
- "version": "2.1.0-rc.5",
3
+ "version": "2.1.0-rc.6",
4
4
  "description": "Server-side Node.js SDK for the Unshared Labs V2 API",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/esm/index.mjs",
@@ -52,7 +52,7 @@
52
52
  "author": "",
53
53
  "license": "MIT",
54
54
  "dependencies": {
55
- "unshared-frontend-sdk": "2.1.0-rc.5"
55
+ "unshared-frontend-sdk": "2.1.0-rc.6"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@unshared-labs/shared-types": "file:../../../shared/types",