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

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"})}}
@@ -60,8 +60,30 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
60
60
  *
61
61
  * When `true`, the SDK owns the flagged response and `onFlagged` is ignored. Requires
62
62
  * `unshared-frontend-sdk` to be installed and the default `routePrefix`. @default false
63
+ *
64
+ * `blockFlagged: true` is equivalent to `flaggedMode: 'gate'` (see {@link flaggedMode}).
63
65
  */
64
66
  blockFlagged?: boolean;
67
+ /**
68
+ * How the SDK enforces a flagged + unverified user. Selects the flagged-user
69
+ * experience; setting it enables enforcement on its own (you do NOT also need
70
+ * `blockFlagged`). When both are set, `flaggedMode` wins.
71
+ *
72
+ * - `'gate'` — **hard gate** (the strongest guarantee, == `blockFlagged: true`). HTML
73
+ * navigations get a standalone verification gate page with no app markup; everything
74
+ * else gets `403 { error: 'account_flagged' }`. The protected content is never sent.
75
+ * - `'overlay'` — **modal over blurred live content**. HTML navigations render normally
76
+ * (the page IS delivered) and the SDK auto-renders the (non-dismissible) interstitial
77
+ * modal over the blurred page; non-HTML/data requests get `403 account_flagged`.
78
+ * Implies `autoInterstitial`. Use `includePathPrefix` to scope which data routes are
79
+ * gated. Honest trade-off: the HTML is delivered to the client and the blur is
80
+ * cosmetic — the real protection is the `403` on the gated data, so put sensitive
81
+ * content behind gated endpoints.
82
+ *
83
+ * Both modes ignore `onFlagged` (the SDK owns the response) and require
84
+ * `unshared-frontend-sdk` installed + the default `routePrefix`. @default undefined
85
+ */
86
+ flaggedMode?: 'gate' | 'overlay';
65
87
  /**
66
88
  * Auto-render the interstitial modal on SPA/JSON `403 account_flagged` paths
67
89
  * (and the deferred `/status` poll) with no app code. When `true`, the injected
@@ -93,6 +115,29 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
93
115
  userId?: string;
94
116
  emailAddress?: string;
95
117
  }) => void;
118
+ /**
119
+ * Called whenever a verdict check could NOT be determined and the request
120
+ * therefore passes through (fails open). This is distinct from `onError`: a
121
+ * non-2xx verdict response is masked into a clean "not flagged" verdict and
122
+ * never surfaces as an error, so without this hook a flagged user passing
123
+ * because the API errored is indistinguishable from a genuinely clean user.
124
+ *
125
+ * The request still passes through regardless (the fail-open behaviour is
126
+ * unchanged) — this only makes it observable. Wrap your handler defensively;
127
+ * it is invoked best-effort and never blocks the request.
128
+ *
129
+ * `reason`: 'timeout' (exceeded checkUserTimeoutMs), 'http_error' (non-2xx
130
+ * masked into clean), or 'exception' (checkUser threw). `status` is present
131
+ * for 'http_error' (0 = network/transport).
132
+ */
133
+ onFailOpen?: (context: FailOpenContext) => void;
134
+ }
135
+ export interface FailOpenContext {
136
+ operation: 'checkUser';
137
+ reason: 'timeout' | 'http_error' | 'exception';
138
+ status?: number;
139
+ userId?: string;
140
+ emailAddress?: string;
96
141
  }
97
142
  export type { Verdict };
98
143
  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:l=CHECK_USER_TIMEOUT_MS,sessionId:u,deviceId:p,onFlagged:f,onError:m,onFailOpen:h,blockFlagged:g=!1,flaggedMode:I,autoInterstitial:S=!1,interstitialFlowType:v="email_verification"}=t,_=I??(g?"gate":void 0),C=S||"overlay"===_,y=e=>{if(h)try{h(e)}catch{}},k=new VerdictCache(o),A=new RateLimitBackoff,E=new DispatchDedupe,T=Date.now().toString(36),x=generateFingerprintScript(n,T,{autoInterstitial:C,interstitialFlowType:v});let U="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");U=readFileSync(e,"utf8")}catch{}const w=void 0!==_||C,F="gate"===_?"flaggedMode 'gate' (blockFlagged)":"overlay"===_?"flaggedMode 'overlay'":"autoInterstitial";if(w&&!U)throw new Error(`[Unshared] ${F} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(w&&"/__unshared"!==n)throw new Error(`[Unshared] ${F} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const P="gate"===_?generateGatePage(n,T):"",R=handleSubmitFingerprint({client:e,verdictCache:k,rateLimitBackoff:A,dispatchDedupe:E,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:u,resolveDeviceId:p,disableBotFilter:c,onError:m}),b=handleVerifyTrigger({client:e,verdictCache:k,resolveEmailAddress:i,resolveDeviceId:p,onError:m}),O=handleVerify({client:e,verdictCache:k,resolveEmailAddress:i,resolveDeviceId:p,onError:m}),N=handleGetInterstitialFlow({client:e}),D=s?Array.isArray(s)?s:[s]:null,M=`${n}/fp.js`,$=`${n}/submit-fp`,L=`${n}/verify-trigger`,V=`${n}/verify`,j=`${n}/status`,q=`${n}/interstitial-flow`;return function(t,s,o){const h=getRequestPath(t.url),g=t.url||h;if(h.startsWith(n+"/")){if(function(e,t){if(!D)return;const r=e.headers.origin??"",i=D.includes("*");(i||D.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&&h===M)return s.setHeader("Content-Type","application/javascript"),s.setHeader("Cache-Control","public, max-age=3600"),void sendBody(s,200,U);if("POST"===t.method&&(h===$||h===L||h===V))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."}}):h===$?void R(t,s):h===L?void b(t,s):void O(t,s);if("GET"===t.method&&h===q)return void N(t,s);if("GET"===t.method&&h===j){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=k.get(n);if((!r||k.isStale(n))&&o&&!A.isPaused()&&!k.isRefreshing(n)){k.markRefreshing(n);try{const i=extractDeviceIdOrUndefined(t,p),s=extractFingerprintId(t),d=extractSessionId(t,u),a=i??s??"unknown";await fetchAndCacheVerdict(e,k,n,o,a,s,d,l,(e,t)=>y({operation:"checkUser",reason:e,status:t,userId:n,emailAddress:o})),r=k.get(n)}catch(e){m&&m(e,{operation:"checkUser",userId:n,emailAddress:o}),y({operation:"checkUser",reason:"exception",userId:n,emailAddress:o})}finally{k.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(h,d))return void o();if(!shouldIncludePath(h,a))return interceptForInjection(t,s,x),void o();let I;try{I=r(t)}catch{}if(isSentinelUserId(I)){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;I=e&&n?e:void 0}if(!I){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,x),void o()}const S=resolveEmail(t,i);if(setUserIdCookie(t,s,I),S&&setEmailCookie(t,s,S),!S)return interceptForInjection(t,s,x),void o();const v=extractSessionId(t,u),C=extractDeviceIdOrUndefined(t,p),T=extractFingerprintId(t),w=t.headers["user-agent"]??"",F=extractClientIp(t),B=C??T;if(!c&&isBot(w))return void o();const G=k.get(I);function H(){"unknown"!==v&&B&&(A.isPaused()||dispatchUserEvent(e,k,A,E,{userId:I,emailAddress:S,sessionId:v,deviceId:B,fingerprintId:T,userAgent:w,ipAddress:F,eventType:g},m))}G?(k.isStale(I)&&!k.isRefreshing(I)&&(k.markRefreshing(I),fetchAndCacheVerdict(e,k,I,S,B??"unknown",T,v,l,(e,t)=>y({operation:"checkUser",reason:e,status:t,userId:I,emailAddress:S})).catch(()=>y({operation:"checkUser",reason:"exception",userId:I,emailAddress:S})).finally(()=>k.clearRefreshing(I))),G.isFlagged||H(),applyVerdict(G,I,S,t,s,o,x,f,_,P)):fetchAndCacheVerdict(e,k,I,S,B??"unknown",T,v,l,(e,t)=>y({operation:"checkUser",reason:e,status:t,userId:I,emailAddress:S})).then(e=>{e.isFlagged||H(),applyVerdict(e,I,S,t,s,o,x,f,_,P)}).catch(()=>{y({operation:"checkUser",reason:"exception",userId:I,emailAddress:S}),H(),interceptForInjection(t,s,x),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){const l=e.isFlagged&&!e.isVerified;if("gate"===a&&l)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("overlay"===a&&l)isHtmlNavigation(i.method,i.headers.accept)?(interceptForInjection(i,n,o),s()):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 l;n&&"unknown"!==n&&(c.deviceId=n),s&&(c.fingerprintId=s);const u=await Promise.race([e.checkUser(i,c),new Promise(e=>{l=setTimeout(()=>e(null),d)})]);if(clearTimeout(l),!u)return a?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:o,cachedAt:0,ttl:0};u.failedOpen&&a?.("http_error",u.failedOpen.status);const p=u.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,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");const{userId:s,emailAddress:r,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,flaggedMode:R,autoInterstitial:I=!1,interstitialFlowType:v="email_verification"}=t,S=R??(g?"gate":void 0),y=I||"overlay"===S,w=e=>{if(f)try{f(e)}catch{}},C=Date.now().toString(36),A=void 0!==S||y,E="gate"===S?"flaggedMode 'gate' (blockFlagged)":"overlay"===S?"flaggedMode 'overlay'":"autoInterstitial";if(A&&!l)throw new Error(`[Unshared] ${E} requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the interstitial).`);if(A&&"/__unshared"!==n)throw new Error(`[Unshared] ${E} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const k="gate"===S?generateGatePage(n,C):"",F=new VerdictCache(o),T=new RateLimitBackoff,x=new DispatchDedupe,U=generateFingerprintScript(n,C,{autoInterstitial:y,interstitialFlowType:v}),q=`${n}/fp.js`,j=`${n}/submit-fp`,O=`${n}/verify-trigger`,D=`${n}/verify`,b=`${n}/status`,P=i?Array.isArray(i)?i:[i]:null;return async function(t,i){let o,f,g;try{const e=new URL(t.url);o=e.pathname,f=e.search}catch{return i(t)}if(o.startsWith(n+"/")){const n=function(e){if(!P)return{};const t=e.headers.get("origin")??"",s=P.includes("*");return s||P.includes(t)?{"Access-Control-Allow-Origin":s?"*":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===q)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===j||o===O||o===D)){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===j?handleSubmitFp(t,i,{client:e,verdictCache:F,rateLimitBackoff:T,dispatchDedupe:x,resolveUserId:s,resolveEmailAddress:r,resolveSessionId:c,resolveDeviceId:u,disableBotFilter:h,onError:m},n):o===O?handleVerifyTriggerWeb(t,i,{client:e,verdictCache:F,resolveEmailAddress:r,resolveDeviceId:u,onError:m},n):handleVerifyWeb(t,i,{client:e,verdictCache:F,resolveEmailAddress:r,resolveDeviceId:u,onError:m},n)}if("GET"===t.method&&o===b){let i;try{i=s(t)}catch{}if(!i)return jsonResponse(200,{status:"anonymous"},n);const o=resolveEmail(t,r);let a=F.get(i);if((!a||F.isStale(i))&&o&&!T.isPaused()&&!F.isRefreshing(i)){F.markRefreshing(i);try{const s=extractDeviceIdFromRequest(t,u),r=parseCookieFromRequest(t,"__unshared_fingerprint_id")||void 0,n=extractSessionIdFromRequest(t,c),d=s??r??"unknown";await fetchAndCacheVerdict(e,F,i,o,d,r,n,_,(e,t)=>w({operation:"checkUser",reason:e,status:t,userId:i,emailAddress:o})),a=F.get(i)}catch(e){m&&m(e,{operation:"checkUser",userId:i,emailAddress:o}),w({operation:"checkUser",reason:"exception",userId:i,emailAddress:o})}finally{F.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),U);try{g=s(t)}catch{}if(isSentinelUserId(g)){const e=parseCookieFromRequest(t,"__unshared_uid"),s=parseCookieFromRequest(t,"__unshared_uid_at"),r=s?Number(s):NaN,n=Number.isFinite(r)&&Date.now()-r<=SENTINEL_STICKINESS_TTL_MS;g=e&&n?e:void 0}if(!g){const e=isSecureWebRequest(t)?"; Secure":"",s=[`__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),U,s)}const R=resolveEmail(t,r),I=[],v=isSecureWebRequest(t)?"; Secure":"";if(I.push(`__unshared_uid=${encodeURIComponent(g)}; Path=/; SameSite=Lax${v}`),I.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${v}`),R&&I.push(`__unshared_email=${encodeURIComponent(R)}; HttpOnly; Path=/; SameSite=Lax${v}`),!R)return injectIntoHtmlResponse(await i(t),U,I);const y=extractSessionIdFromRequest(t,c),C=extractDeviceIdFromRequest(t,u),A=parseCookieFromRequest(t,"__unshared_fingerprint_id")||void 0,E=t.headers.get("user-agent")??"",$=extractClientIpFromRequest(t),N=C??A;if(!h&&isBot(E))return i(t);let H=F.get(g);if(H)F.isStale(g)&&!F.isRefreshing(g)&&(F.markRefreshing(g),fetchAndCacheVerdict(e,F,g,R,N??"unknown",A,y,_,(e,t)=>w({operation:"checkUser",reason:e,status:t,userId:g,emailAddress:R})).catch(()=>w({operation:"checkUser",reason:"exception",userId:g,emailAddress:R})).finally(()=>F.clearRefreshing(g)));else try{H=await fetchAndCacheVerdict(e,F,g,R,N??"unknown",A,y,_,(e,t)=>w({operation:"checkUser",reason:e,status:t,userId:g,emailAddress:R}))}catch{return w({operation:"checkUser",reason:"exception",userId:g,emailAddress:R}),injectIntoHtmlResponse(await i(t),U,I)}const L=H.isFlagged&&!H.isVerified;if("gate"===S&&L)return isHtmlNavigation(t.method,t.headers.get("accept")??void 0)?bodyResponse(200,k,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}):jsonResponse(403,flaggedResponse(R));if("overlay"===S&&L)return isHtmlNavigation(t.method,t.headers.get("accept")??void 0)?injectIntoHtmlResponse(await i(t),U,I):jsonResponse(403,flaggedResponse(R));if(H.isFlagged&&!H.isVerified&&p)try{const e=await p({userId:g,emailAddress:R,verdict:H,request:t});if(e)return injectIntoHtmlResponse(e,U,I)}catch(e){m&&m(e,{operation:"checkUser",userId:g,emailAddress:R})}return H.isFlagged||"unknown"===y||!N||T.isPaused()||dispatchUserEvent(e,F,T,x,{userId:g,emailAddress:R,sessionId:y,deviceId:N,fingerprintId:A,userAgent:E,ipAddress:$,eventType:o+f},m),injectIntoHtmlResponse(await i(t),U,I)}}async function injectIntoHtmlResponse(e,t,s){const r=e.headers.get("content-type");if(!isHtmlContentType(r??void 0)){if(!s||0===s.length)return e;const t=mergeResponseHeaders(e.headers,void 0,s);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)},s);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 s=t(e);if(s)return s}catch{}const s=parseCookieFromRequest(e,"__unshared_email");if(s)return s}function resolveEmailWithBody(e,t,s){const r=resolveEmail(e,s);if(r)return r;const n=t.email;return"string"==typeof n&&n?n:void 0}function extractSessionIdFromRequest(e,t){if(t)try{const s=t(e);if(s)return s}catch{}return parseCookieFromRequest(e,"__unshared_sid")??"unknown"}function dispatchUserEvent(e,t,s,r,n,i){r.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&&s.pause(1e3*e.error.retryAfter)}).catch(e=>{i&&i(e,{operation:"processUserEvent",userId:n.userId,emailAddress:n.emailAddress})})}async function fetchAndCacheVerdict(e,t,s,r,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(r,c),new Promise(e=>{u=setTimeout(()=>e(null),a)})]);if(clearTimeout(u),!l)return d?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:r,sessionId:o,cachedAt:0,ttl:0};l.failedOpen&&d?.("http_error",l.failedOpen.status);const p=l.data?.is_user_flagged??!1;return t.set(s,{isFlagged:p,isVerified:!1,emailAddress:r,sessionId:o}),t.get(s)}async function handleSubmitFp(e,t,s,r){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=s.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=s.resolveEmailAddress?s.resolveEmailAddress(e):void 0}catch{}o=o??parseCookieFromRequest(e,"__unshared_email")??t.email??void 0;try{a=s.resolveSessionId?s.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(!s.disableBotFilter&&isBot(c))return jsonResponse(200,{success:!0},r);const u=(n.fingerprint_id&&n.fingerprint_id.length>0?n.fingerprint_id:void 0)??extractDeviceIdFromRequestOrUnknown(e,s.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 s="unknown";if(t)try{const e=new URL(t);s=(e.pathname||"/")+(e.search||"")}catch{}h=s}const _=e.headers.get("x-idempotency-key")||void 0,g=Date.now(),R=_?`${_}|${g}`:l&&i?`${sha256Hex(`${l}|${i}|${h}`)}|${g}`:void 0;i&&s.client.submitFingerprintEvent(n,{userId:i,emailAddress:o,sessionHash:a,eventType:h,ipAddress:d,userAgent:c,idempotencyKey:R}).catch(e=>{s.onError&&s.onError(e,{operation:"submitFingerprintEvent",userId:i,emailAddress:o})}),i&&o&&!s.rateLimitBackoff.isPaused()&&!s.dispatchDedupe.wasRecentlyDispatched(i,h)&&s.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&&s.verdictCache.update(i,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&s.rateLimitBackoff.pause(1e3*e.error.retryAfter)}).catch(e=>{s.onError&&s.onError(e,{operation:"processUserEvent",userId:i,emailAddress:o})});const I={...r,"Content-Type":"application/json"},v=new Response(JSON.stringify({success:!0}),{status:200,headers:I});for(const e of f)v.headers.append("Set-Cookie",e);return v}catch{return jsonResponse(200,{success:!0},r)}}async function handleVerifyTriggerWeb(e,t,s,r){try{const n=resolveEmailWithBody(e,t??{},s.resolveEmailAddress);if(!n)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},r);const i=extractDeviceIdFromRequestOrUnknown(e,s.resolveDeviceId),o=parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,a=await s.client.triggerEmailVerification(n,i,{fingerprintId:o});return a.success?jsonResponse(200,{success:!0,data:a.data},r):jsonResponse(200,{success:!1,error:a.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},r)}catch(e){return s.onError&&s.onError(e,{operation:"verifyTrigger"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},r)}}async function handleVerifyWeb(e,t,s,r){try{const n=resolveEmailWithBody(e,t??{},s.resolveEmailAddress),i=t?.code;if(!n||!i)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}},r);const o=extractDeviceIdFromRequestOrUnknown(e,s.resolveDeviceId),a=parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,d=await s.client.verify(n,o,i,{fingerprintId:a});if(d.success){const t=parseCookieFromRequest(e,"__unshared_uid");return t&&s.verdictCache.update(t,{isVerified:!0}),jsonResponse(200,{success:!0,data:{verified:!0}},r)}return jsonResponse(200,{success:!1,error:d.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}},r)}catch(e){return s.onError&&s.onError(e,{operation:"verify"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},r)}}
@@ -93,8 +93,26 @@ export interface WebProtectionConfig {
93
93
  * `403 { error: 'account_flagged' }`. `onFlagged` is ignored in this mode.
94
94
  * Requires `fingerprintSdkBundle` to be provided and the default `routePrefix`.
95
95
  * @default false
96
+ *
97
+ * `blockFlagged: true` is equivalent to `flaggedMode: 'gate'` (see {@link flaggedMode}).
96
98
  */
97
99
  blockFlagged?: boolean;
100
+ /**
101
+ * How the SDK enforces a flagged + unverified user (Web/edge equivalent of the Node
102
+ * middleware's `flaggedMode`). Setting it enables enforcement on its own; when both
103
+ * are set, `flaggedMode` wins.
104
+ *
105
+ * - `'gate'` — hard gate (== `blockFlagged: true`): HTML navigations get the standalone
106
+ * verification gate page; everything else gets `403 account_flagged`. Content never sent.
107
+ * - `'overlay'` — modal over blurred live content: HTML navigations render normally and
108
+ * the SDK auto-renders the modal over the blurred page; non-HTML/data requests get
109
+ * `403 account_flagged`. Implies `autoInterstitial`. The HTML is delivered (blur is
110
+ * cosmetic) — the real protection is the `403` on the gated data.
111
+ *
112
+ * Both modes ignore `onFlagged` and require `fingerprintSdkBundle` + the default
113
+ * `routePrefix`. @default undefined
114
+ */
115
+ flaggedMode?: 'gate' | 'overlay';
98
116
  /**
99
117
  * Auto-render the interstitial modal on SPA/JSON `403 account_flagged` paths
100
118
  * (and the deferred `/status` poll) with no app code. When `true`, the injected
@@ -138,4 +156,19 @@ export interface WebProtectionConfig {
138
156
  userId?: string;
139
157
  emailAddress?: string;
140
158
  }) => void;
159
+ /**
160
+ * Called whenever a verdict check could NOT be determined and the request
161
+ * therefore passes through (fails open) — see the Node middleware's
162
+ * `onFailOpen`. A non-2xx verdict response is masked into a clean "not flagged"
163
+ * verdict and never surfaces via `onError`; this hook makes that observable.
164
+ * The fail-open behaviour is unchanged; invoked best-effort, never blocks.
165
+ */
166
+ onFailOpen?: (context: WebFailOpenContext) => void;
167
+ }
168
+ export interface WebFailOpenContext {
169
+ operation: 'checkUser';
170
+ reason: 'timeout' | 'http_error' | 'exception';
171
+ status?: number;
172
+ userId?: string;
173
+ emailAddress?: string;
141
174
  }
@@ -60,8 +60,30 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
60
60
  *
61
61
  * When `true`, the SDK owns the flagged response and `onFlagged` is ignored. Requires
62
62
  * `unshared-frontend-sdk` to be installed and the default `routePrefix`. @default false
63
+ *
64
+ * `blockFlagged: true` is equivalent to `flaggedMode: 'gate'` (see {@link flaggedMode}).
63
65
  */
64
66
  blockFlagged?: boolean;
67
+ /**
68
+ * How the SDK enforces a flagged + unverified user. Selects the flagged-user
69
+ * experience; setting it enables enforcement on its own (you do NOT also need
70
+ * `blockFlagged`). When both are set, `flaggedMode` wins.
71
+ *
72
+ * - `'gate'` — **hard gate** (the strongest guarantee, == `blockFlagged: true`). HTML
73
+ * navigations get a standalone verification gate page with no app markup; everything
74
+ * else gets `403 { error: 'account_flagged' }`. The protected content is never sent.
75
+ * - `'overlay'` — **modal over blurred live content**. HTML navigations render normally
76
+ * (the page IS delivered) and the SDK auto-renders the (non-dismissible) interstitial
77
+ * modal over the blurred page; non-HTML/data requests get `403 account_flagged`.
78
+ * Implies `autoInterstitial`. Use `includePathPrefix` to scope which data routes are
79
+ * gated. Honest trade-off: the HTML is delivered to the client and the blur is
80
+ * cosmetic — the real protection is the `403` on the gated data, so put sensitive
81
+ * content behind gated endpoints.
82
+ *
83
+ * Both modes ignore `onFlagged` (the SDK owns the response) and require
84
+ * `unshared-frontend-sdk` installed + the default `routePrefix`. @default undefined
85
+ */
86
+ flaggedMode?: 'gate' | 'overlay';
65
87
  /**
66
88
  * Auto-render the interstitial modal on SPA/JSON `403 account_flagged` paths
67
89
  * (and the deferred `/status` poll) with no app code. When `true`, the injected
@@ -93,6 +115,29 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
93
115
  userId?: string;
94
116
  emailAddress?: string;
95
117
  }) => void;
118
+ /**
119
+ * Called whenever a verdict check could NOT be determined and the request
120
+ * therefore passes through (fails open). This is distinct from `onError`: a
121
+ * non-2xx verdict response is masked into a clean "not flagged" verdict and
122
+ * never surfaces as an error, so without this hook a flagged user passing
123
+ * because the API errored is indistinguishable from a genuinely clean user.
124
+ *
125
+ * The request still passes through regardless (the fail-open behaviour is
126
+ * unchanged) — this only makes it observable. Wrap your handler defensively;
127
+ * it is invoked best-effort and never blocks the request.
128
+ *
129
+ * `reason`: 'timeout' (exceeded checkUserTimeoutMs), 'http_error' (non-2xx
130
+ * masked into clean), or 'exception' (checkUser threw). `status` is present
131
+ * for 'http_error' (0 = network/transport).
132
+ */
133
+ onFailOpen?: (context: FailOpenContext) => void;
134
+ }
135
+ export interface FailOpenContext {
136
+ operation: 'checkUser';
137
+ reason: 'timeout' | 'http_error' | 'exception';
138
+ status?: number;
139
+ userId?: string;
140
+ emailAddress?: string;
96
141
  }
97
142
  export type { Verdict };
98
143
  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,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:l,deviceId:_,onFlagged:p,onError:h,onFailOpen:f,blockFlagged:g=!1,flaggedMode:v,autoInterstitial:m=!1,interstitialFlowType:I="email_verification"}=t,k=v??(g?"gate":void 0),y=m||"overlay"===k,S=e=>{if(f)try{f(e)}catch{}},C=new verdict_cache_1.VerdictCache(o),A=new rate_limit_backoff_1.RateLimitBackoff,x=new dispatch_dedupe_1.DispatchDedupe,E=Date.now().toString(36),b=(0,fingerprint_script_1.generateFingerprintScript)(s,E,{autoInterstitial:y,interstitialFlowType:I});let U="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");U=(0,fs_1.readFileSync)(e,"utf8")}catch{}const T=void 0!==k||y,w="gate"===k?"flaggedMode 'gate' (blockFlagged)":"overlay"===k?"flaggedMode 'overlay'":"autoInterstitial";if(T&&!U)throw new Error(`[Unshared] ${w} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(T&&"/__unshared"!==s)throw new Error(`[Unshared] ${w} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const q="gate"===k?(0,gate_page_1.generateGatePage)(s,E):"",F=(0,submit_fp_1.handleSubmitFingerprint)({client:e,verdictCache:C,rateLimitBackoff:A,dispatchDedupe:x,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:l,resolveDeviceId:_,disableBotFilter:c,onError:h}),O=(0,verify_1.handleVerifyTrigger)({client:e,verdictCache:C,resolveEmailAddress:i,resolveDeviceId:_,onError:h}),M=(0,verify_1.handleVerify)({client:e,verdictCache:C,resolveEmailAddress:i,resolveDeviceId:_,onError:h}),P=(0,interstitial_1.handleGetInterstitialFlow)({client:e}),$=n?Array.isArray(n)?n:[n]:null,j=`${s}/fp.js`,D=`${s}/submit-fp`,N=`${s}/verify-trigger`,R=`${s}/verify`,L=`${s}/status`,V=`${s}/interstitial-flow`;return function(t,n,o){const f=(0,http_helpers_1.getRequestPath)(t.url),g=t.url||f;if(f.startsWith(s+"/")){if(function(e,t){if(!$)return;const r=e.headers.origin??"",i=$.includes("*");(i||$.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&&f===j)return n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBody)(n,200,U);if("POST"===t.method&&(f===D||f===N||f===R))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."}}):f===D?void F(t,n):f===N?void O(t,n):void M(t,n);if("GET"===t.method&&f===V)return void P(t,n);if("GET"===t.method&&f===L){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=C.get(s);if((!r||C.isStale(s))&&o&&!A.isPaused()&&!C.isRefreshing(s)){C.markRefreshing(s);try{const i=(0,device_id_1.extractDeviceIdOrUndefined)(t,_),n=extractFingerprintId(t),d=extractSessionId(t,l),a=i??n??"unknown";await fetchAndCacheVerdict(e,C,s,o,a,n,d,u,(e,t)=>S({operation:"checkUser",reason:e,status:t,userId:s,emailAddress:o})),r=C.get(s)}catch(e){h&&h(e,{operation:"checkUser",userId:s,emailAddress:o}),S({operation:"checkUser",reason:"exception",userId:s,emailAddress:o})}finally{C.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)(f,d))return void o();if(!(0,include_path_1.shouldIncludePath)(f,a))return interceptForInjection(t,n,b),void o();let v;try{v=r(t)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(v)){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;v=e&&s?e:void 0}if(!v){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,b),void o()}const m=resolveEmail(t,i);if(setUserIdCookie(t,n,v),m&&setEmailCookie(t,n,m),!m)return interceptForInjection(t,n,b),void o();const I=extractSessionId(t,l),y=(0,device_id_1.extractDeviceIdOrUndefined)(t,_),E=extractFingerprintId(t),T=t.headers["user-agent"]??"",w=(0,client_ip_1.extractClientIp)(t),G=y??E;if(!c&&(0,is_bot_1.isBot)(T))return void o();const B=C.get(v);function H(){"unknown"!==I&&G&&(A.isPaused()||dispatchUserEvent(e,C,A,x,{userId:v,emailAddress:m,sessionId:I,deviceId:G,fingerprintId:E,userAgent:T,ipAddress:w,eventType:g},h))}B?(C.isStale(v)&&!C.isRefreshing(v)&&(C.markRefreshing(v),fetchAndCacheVerdict(e,C,v,m,G??"unknown",E,I,u,(e,t)=>S({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:m})).catch(()=>S({operation:"checkUser",reason:"exception",userId:v,emailAddress:m})).finally(()=>C.clearRefreshing(v))),B.isFlagged||H(),applyVerdict(B,v,m,t,n,o,b,p,k,q)):fetchAndCacheVerdict(e,C,v,m,G??"unknown",E,I,u,(e,t)=>S({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:m})).then(e=>{e.isFlagged||H(),applyVerdict(e,v,m,t,n,o,b,p,k,q)}).catch(()=>{S({operation:"checkUser",reason:"exception",userId:v,emailAddress:m}),H(),interceptForInjection(t,n,b),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){const u=e.isFlagged&&!e.isVerified;if("gate"===a&&u)(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("overlay"===a&&u)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(interceptForInjection(i,s,o),n()):(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,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 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}`)}
@@ -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:c,deviceId:_,fingerprintSdkBundle:l="",onFlagged:u,onError:p,onFailOpen:h,disableBotFilter:f=!1,checkUserTimeoutMs:m=CHECK_USER_TIMEOUT_MS,blockFlagged:w=!1,flaggedMode:g,autoInterstitial:b=!1,interstitialFlowType:v="email_verification"}=r,I=g??(w?"gate":void 0),y=b||"overlay"===I,A=e=>{if(h)try{h(e)}catch{}},S=Date.now().toString(36),E=void 0!==I||y,R="gate"===I?"flaggedMode 'gate' (blockFlagged)":"overlay"===I?"flaggedMode 'overlay'":"autoInterstitial";if(E&&!l)throw new Error(`[Unshared] ${R} requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the interstitial).`);if(E&&"/__unshared"!==i)throw new Error(`[Unshared] ${R} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const T="gate"===I?(0,gate_page_1.generateGatePage)(i,S):"",C=new verdict_cache_1.VerdictCache(a),x=new rate_limit_backoff_1.RateLimitBackoff,k=new dispatch_dedupe_1.DispatchDedupe,U=(0,fingerprint_script_1.generateFingerprintScript)(i,S,{autoInterstitial:y,interstitialFlowType:v}),O=`${i}/fp.js`,$=`${i}/submit-fp`,F=`${i}/verify-trigger`,P=`${i}/verify`,q=`${i}/status`,L=n?Array.isArray(n)?n:[n]:null;return async function(r,n){let a,h,w;try{const e=new URL(r.url);a=e.pathname,h=e.search}catch{return n(r)}if(a.startsWith(i+"/")){const i=function(e){if(!L)return{};const r=e.headers.get("origin")??"",t=L.includes("*");return t||L.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===O)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===$||a===F||a===P)){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===$?handleSubmitFp(r,n,{client:e,verdictCache:C,rateLimitBackoff:x,dispatchDedupe:k,resolveUserId:t,resolveEmailAddress:s,resolveSessionId:c,resolveDeviceId:_,disableBotFilter:f,onError:p},i):a===F?handleVerifyTriggerWeb(r,n,{client:e,verdictCache:C,resolveEmailAddress:s,resolveDeviceId:_,onError:p},i):handleVerifyWeb(r,n,{client:e,verdictCache:C,resolveEmailAddress:s,resolveDeviceId:_,onError:p},i)}if("GET"===r.method&&a===q){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=C.get(n);if((!o||C.isStale(n))&&a&&!x.isPaused()&&!C.isRefreshing(n)){C.markRefreshing(n);try{const t=(0,web_helpers_1.extractDeviceIdFromRequest)(r,_),s=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_fingerprint_id")||void 0,i=extractSessionIdFromRequest(r,c),d=t??s??"unknown";await fetchAndCacheVerdict(e,C,n,a,d,s,i,m,(e,r)=>A({operation:"checkUser",reason:e,status:r,userId:n,emailAddress:a})),o=C.get(n)}catch(e){p&&p(e,{operation:"checkUser",userId:n,emailAddress:a}),A({operation:"checkUser",reason:"exception",userId:n,emailAddress:a})}finally{C.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),U);try{w=t(r)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(w)){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;w=e&&i?e:void 0}if(!w){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),U,t)}const g=resolveEmail(r,s),b=[],v=(0,web_helpers_1.isSecureWebRequest)(r)?"; Secure":"";if(b.push(`__unshared_uid=${encodeURIComponent(w)}; Path=/; SameSite=Lax${v}`),b.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${v}`),g&&b.push(`__unshared_email=${encodeURIComponent(g)}; HttpOnly; Path=/; SameSite=Lax${v}`),!g)return injectIntoHtmlResponse(await n(r),U,b);const y=extractSessionIdFromRequest(r,c),S=(0,web_helpers_1.extractDeviceIdFromRequest)(r,_),E=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_fingerprint_id")||void 0,R=r.headers.get("user-agent")??"",M=(0,web_helpers_1.extractClientIpFromRequest)(r),D=S??E;if(!f&&(0,is_bot_1.isBot)(R))return n(r);let N=C.get(w);if(N)C.isStale(w)&&!C.isRefreshing(w)&&(C.markRefreshing(w),fetchAndCacheVerdict(e,C,w,g,D??"unknown",E,y,m,(e,r)=>A({operation:"checkUser",reason:e,status:r,userId:w,emailAddress:g})).catch(()=>A({operation:"checkUser",reason:"exception",userId:w,emailAddress:g})).finally(()=>C.clearRefreshing(w)));else try{N=await fetchAndCacheVerdict(e,C,w,g,D??"unknown",E,y,m,(e,r)=>A({operation:"checkUser",reason:e,status:r,userId:w,emailAddress:g}))}catch{return A({operation:"checkUser",reason:"exception",userId:w,emailAddress:g}),injectIntoHtmlResponse(await n(r),U,b)}const H=N.isFlagged&&!N.isVerified;if("gate"===I&&H)return(0,content_type_1.isHtmlNavigation)(r.method,r.headers.get("accept")??void 0)?(0,web_helpers_1.bodyResponse)(200,T,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}):(0,web_helpers_1.jsonResponse)(403,(0,flagged_response_1.flaggedResponse)(g));if("overlay"===I&&H)return(0,content_type_1.isHtmlNavigation)(r.method,r.headers.get("accept")??void 0)?injectIntoHtmlResponse(await n(r),U,b):(0,web_helpers_1.jsonResponse)(403,(0,flagged_response_1.flaggedResponse)(g));if(N.isFlagged&&!N.isVerified&&u)try{const e=await u({userId:w,emailAddress:g,verdict:N,request:r});if(e)return injectIntoHtmlResponse(e,U,b)}catch(e){p&&p(e,{operation:"checkUser",userId:w,emailAddress:g})}return N.isFlagged||"unknown"===y||!D||x.isPaused()||dispatchUserEvent(e,C,x,k,{userId:w,emailAddress:g,sessionId:y,deviceId:D,fingerprintId:E,userAgent:R,ipAddress:M,eventType:a+h},p),injectIntoHtmlResponse(await n(r),U,b)}}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 c={};let _;i&&"unknown"!==i&&(c.deviceId=i),n&&(c.fingerprintId=n);const l=await Promise.race([e.checkUser(s,c),new Promise(e=>{_=setTimeout(()=>e(null),o)})]);if(clearTimeout(_),!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),c=e.headers.get("user-agent")??"";if(!t.disableBotFilter&&(0,is_bot_1.isBot)(c))return(0,web_helpers_1.jsonResponse)(200,{success:!0},s);const _=(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,p=(0,web_helpers_1.isSecureWebRequest)(e)?"; Secure":"",h=[];if(u&&!(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")&&h.push(`__unshared_fingerprint_id=${encodeURIComponent(u)}; HttpOnly; Path=/; SameSite=Lax${p}`),l){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fp_id");r&&r===l||h.push(`__unshared_fp_id=${encodeURIComponent(l)}; Path=/; SameSite=Lax; Max-Age=31536000${p}`)}let f;if(a&&!(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")&&h.push(`__unshared_email=${encodeURIComponent(a)}; HttpOnly; Path=/; SameSite=Lax${p}`),"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 m=e.headers.get("x-idempotency-key")||void 0,w=Date.now(),g=m?`${m}|${w}`:l&&n?`${(0,util_1.sha256Hex)(`${l}|${n}|${f}`)}|${w}`:void 0;n&&t.client.submitFingerprintEvent(i,{userId:n,emailAddress:a,sessionHash:o,eventType:f,ipAddress:d,userAgent:c,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:_,fingerprintId:l,sessionHash:o??"unknown",userAgent:c}).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 h)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)}}
@@ -93,8 +93,26 @@ export interface WebProtectionConfig {
93
93
  * `403 { error: 'account_flagged' }`. `onFlagged` is ignored in this mode.
94
94
  * Requires `fingerprintSdkBundle` to be provided and the default `routePrefix`.
95
95
  * @default false
96
+ *
97
+ * `blockFlagged: true` is equivalent to `flaggedMode: 'gate'` (see {@link flaggedMode}).
96
98
  */
97
99
  blockFlagged?: boolean;
100
+ /**
101
+ * How the SDK enforces a flagged + unverified user (Web/edge equivalent of the Node
102
+ * middleware's `flaggedMode`). Setting it enables enforcement on its own; when both
103
+ * are set, `flaggedMode` wins.
104
+ *
105
+ * - `'gate'` — hard gate (== `blockFlagged: true`): HTML navigations get the standalone
106
+ * verification gate page; everything else gets `403 account_flagged`. Content never sent.
107
+ * - `'overlay'` — modal over blurred live content: HTML navigations render normally and
108
+ * the SDK auto-renders the modal over the blurred page; non-HTML/data requests get
109
+ * `403 account_flagged`. Implies `autoInterstitial`. The HTML is delivered (blur is
110
+ * cosmetic) — the real protection is the `403` on the gated data.
111
+ *
112
+ * Both modes ignore `onFlagged` and require `fingerprintSdkBundle` + the default
113
+ * `routePrefix`. @default undefined
114
+ */
115
+ flaggedMode?: 'gate' | 'overlay';
98
116
  /**
99
117
  * Auto-render the interstitial modal on SPA/JSON `403 account_flagged` paths
100
118
  * (and the deferred `/status` poll) with no app code. When `true`, the injected
@@ -138,4 +156,19 @@ export interface WebProtectionConfig {
138
156
  userId?: string;
139
157
  emailAddress?: string;
140
158
  }) => void;
159
+ /**
160
+ * Called whenever a verdict check could NOT be determined and the request
161
+ * therefore passes through (fails open) — see the Node middleware's
162
+ * `onFailOpen`. A non-2xx verdict response is masked into a clean "not flagged"
163
+ * verdict and never surfaces via `onError`; this hook makes that observable.
164
+ * The fail-open behaviour is unchanged; invoked best-effort, never blocks.
165
+ */
166
+ onFailOpen?: (context: WebFailOpenContext) => void;
167
+ }
168
+ export interface WebFailOpenContext {
169
+ operation: 'checkUser';
170
+ reason: 'timeout' | 'http_error' | 'exception';
171
+ status?: number;
172
+ userId?: string;
173
+ emailAddress?: string;
141
174
  }
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.7",
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.7"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@unshared-labs/shared-types": "file:../../../shared/types",