unshared-clientjs-sdk 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/client.d.ts +7 -2
  2. package/dist/client.js +1 -1
  3. package/dist/esm/client.d.mts +7 -2
  4. package/dist/esm/client.mjs +1 -1
  5. package/dist/esm/middleware/dispatch-dedupe.d.mts +1 -0
  6. package/dist/esm/middleware/dispatch-dedupe.mjs +1 -1
  7. package/dist/esm/middleware/dispatch-processing.d.mts +22 -0
  8. package/dist/esm/middleware/dispatch-processing.mjs +1 -0
  9. package/dist/esm/middleware/index.d.mts +12 -0
  10. package/dist/esm/middleware/index.mjs +1 -1
  11. package/dist/esm/middleware/rate-limit-backoff.d.mts +11 -8
  12. package/dist/esm/middleware/rate-limit-backoff.mjs +1 -1
  13. package/dist/esm/middleware/response-interceptor.d.mts +1 -0
  14. package/dist/esm/middleware/response-interceptor.mjs +1 -1
  15. package/dist/esm/middleware/routes/submit-fp.mjs +1 -1
  16. package/dist/esm/middleware/utils/skip-paths.d.mts +5 -1
  17. package/dist/esm/middleware/utils/skip-paths.mjs +1 -1
  18. package/dist/esm/middleware/verdict-cache.d.mts +8 -1
  19. package/dist/esm/middleware/verdict-cache.mjs +1 -1
  20. package/dist/esm/web/protection-handler.d.mts +29 -1
  21. package/dist/esm/web/protection-handler.mjs +1 -1
  22. package/dist/esm/web/types.d.mts +11 -1
  23. package/dist/esm/web/web-helpers.mjs +1 -1
  24. package/dist/esm/webcrypto.d.mts +8 -0
  25. package/dist/esm/webcrypto.mjs +1 -0
  26. package/dist/middleware/dispatch-dedupe.d.ts +1 -0
  27. package/dist/middleware/dispatch-dedupe.js +1 -1
  28. package/dist/middleware/dispatch-processing.d.ts +22 -0
  29. package/dist/middleware/dispatch-processing.js +1 -0
  30. package/dist/middleware/index.d.ts +12 -0
  31. package/dist/middleware/index.js +1 -1
  32. package/dist/middleware/rate-limit-backoff.d.ts +11 -8
  33. package/dist/middleware/rate-limit-backoff.js +1 -1
  34. package/dist/middleware/response-interceptor.d.ts +1 -0
  35. package/dist/middleware/response-interceptor.js +1 -1
  36. package/dist/middleware/routes/submit-fp.js +1 -1
  37. package/dist/middleware/utils/skip-paths.d.ts +5 -1
  38. package/dist/middleware/utils/skip-paths.js +1 -1
  39. package/dist/middleware/verdict-cache.d.ts +8 -1
  40. package/dist/middleware/verdict-cache.js +1 -1
  41. package/dist/web/protection-handler.d.ts +29 -1
  42. package/dist/web/protection-handler.js +1 -1
  43. package/dist/web/types.d.ts +11 -1
  44. package/dist/web/web-helpers.js +1 -1
  45. package/dist/webcrypto.d.ts +8 -0
  46. package/dist/webcrypto.js +1 -0
  47. package/package.json +3 -3
package/dist/client.d.ts CHANGED
@@ -147,9 +147,11 @@ export declare class UnsharedClient {
147
147
  private readonly _baseUrl;
148
148
  private readonly _timeout;
149
149
  private readonly _maxRetries;
150
- private readonly _encryptionKey;
150
+ private _encryptionKey;
151
151
  private readonly _customFetch;
152
152
  constructor(config: UnsharedClientConfig);
153
+ /** Lazily derive + memoize the imported AES-GCM CryptoKey (async via WebCrypto). */
154
+ private _getKey;
153
155
  private _encrypt;
154
156
  /**
155
157
  * Core HTTP method with retry logic.
@@ -168,7 +170,10 @@ export declare class UnsharedClient {
168
170
  * Record a user event and receive naughty-list analysis.
169
171
  * Maps to: POST /v2/process-user-event
170
172
  */
171
- processUserEvent(params: ProcessUserEventParams): Promise<ApiResult<ProcessUserEventResult>>;
173
+ processUserEvent(params: ProcessUserEventParams, overrides?: {
174
+ timeoutMs?: number;
175
+ maxRetries?: number;
176
+ }): Promise<ApiResult<ProcessUserEventResult>>;
172
177
  /**
173
178
  * Check if a user is flagged in the naughty list.
174
179
  * Maps to: GET /v2/check-user
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,s){const i=(s?.maxRetries??this.u)+1,r=s?.timeoutMs??this.h;let a={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let s=1;s<=i;s++){s>1&&await sleep(retryDelay(s-1));const i=new AbortController,n=setTimeout(()=>i.abort(),r);try{const s=this._??globalThis.fetch,r=await s(e,{method:t.method,headers:{"X-API-Key":this.i,...t.headers},body:t.body,signal:i.signal});if(clearTimeout(n),r.ok){const e=await r.text().catch(()=>"{}");let t;try{t=JSON.parse(e)}catch{t={}}const s="data"in t?t.data:t;return{success:!0,status:r.status,data:s}}const o=await parseErrorBody(r);if(r.status>=400&&r.status<500){if(429===r.status){const e=r.headers.get("Retry-After");if(null!=e){const t=parseInt(e,10);isNaN(t)||(o.retryAfter=t)}}return{success:!1,status:r.status,error:o}}a={success:!1,status:r.status,error:o}}catch(e){clearTimeout(n),a={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return a}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!=e.source&&(s.source=e.source),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},failedOpen:{status:0,reason:"no_device_id"}};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"},{timeoutMs:s.timeoutMs,maxRetries:s.maxRetries});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;
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.UnsharedClient=void 0;const webcrypto_1=require("./webcrypto"),DEFAULT_BASE_URL="https://api.unshared.ai",DEFAULT_TIMEOUT_MS=1e4,DEFAULT_MAX_RETRIES=3,MAX_DELAY_MS=3e4,BASE_DELAY_MS=1e3;function sleep(e){return new Promise(s=>setTimeout(s,e))}function retryDelay(e){const s=Math.min(1e3*Math.pow(2,e-1),3e4),t=s*(.5*Math.random()-.25);return Math.max(0,s+t)}async function parseErrorBody(e){const s=await e.text().catch(()=>"");try{const t=JSON.parse(s);return t?.error?.code?{code:t.error.code,message:t.error.message??"Unknown error",details:t.error.details}:{code:"UNKNOWN_ERROR",message:s||e.statusText}}catch{return{code:"UNKNOWN_ERROR",message:s||e.statusText}}}class UnsharedClient{constructor(e){if(this.i=null,!e.apiKey||""===e.apiKey.trim())throw new Error("apiKey is required");this.o=e.apiKey,this.l=e.baseUrl?e.baseUrl.replace(/\/$/,""):DEFAULT_BASE_URL,this.u=e.timeout??1e4,this.h=e.maxRetries??3,this.m=e.fetch}_(){return this.i||(this.i=(0,webcrypto_1.sha256Bytes)(this.o).then(webcrypto_1.importAesGcmKey)),this.i}async p(e){return(0,webcrypto_1.encryptDataWeb)(e,await this._())}async R(e,s,t){const i=(t?.maxRetries??this.h)+1,r=t?.timeoutMs??this.u;let a={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let t=1;t<=i;t++){t>1&&await sleep(retryDelay(t-1));const i=new AbortController,o=setTimeout(()=>i.abort(),r);try{const t=this.m??globalThis.fetch,r=await t(e,{method:s.method,headers:{"X-API-Key":this.o,...s.headers},body:s.body,signal:i.signal});if(r.ok){let e,s;try{e=await r.text()}catch(e){a={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}};continue}if(""===e.trim())return{success:!0,status:r.status,data:{}};try{s=JSON.parse(e)}catch{return{success:!1,status:r.status,error:{code:"PARSE_ERROR",message:"Unparseable 2xx response body"}}}const t="data"in s?s.data:s;return{success:!0,status:r.status,data:t}}const o=await parseErrorBody(r);if(r.status>=400&&r.status<500){if(429===r.status){const e=r.headers.get("Retry-After");if(null!=e){const s=parseInt(e,10);isNaN(s)||(o.retryAfter=s)}if(null==o.retryAfter){const e=o.details?.retry_after_seconds;"number"!=typeof e||isNaN(e)||(o.retryAfter=e)}}return{success:!1,status:r.status,error:o}}a={success:!1,status:r.status,error:o}}catch(e){a={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}finally{clearTimeout(o)}}return a}async submitFingerprintEvent(e,s){const t={hash:e.full_hash,stable_hash:e.fingerprint_id,collected_at:e.timestamp,is_incognito:e.isIncognito,components:e.components,version:e.version};null!=e.source&&(t.source=e.source);const[i,r,a,o]=await Promise.all([null!=s?.userId?this.p(s.userId):Promise.resolve(void 0),null!=s?.emailAddress?this.p(s.emailAddress):Promise.resolve(void 0),null!=s?.ipAddress?this.p(s.ipAddress):Promise.resolve(void 0),null!=s?.userAgent?this.p(s.userAgent):Promise.resolve(void 0)]);return null!=i&&(t.user_id=i),null!=r&&(t.email_address=r),null!=s?.sessionHash&&(t.session_hash=s.sessionHash),null!=s?.eventType&&(t.event_type=s.eventType),null!=a&&(t.ip_address=a),null!=o&&(t.user_agent=o),this.R(`${this.l}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":s?.idempotencyKey??globalThis.crypto.randomUUID()},body:JSON.stringify(t)})}async processUserEvent(e,s){const[t,i,r,a,o,n]=await Promise.all([this.p(e.userId),this.p(e.ipAddress),this.p(e.deviceId),this.p(e.userAgent),this.p(e.emailAddress),null!=e.fingerprintId?this.p(e.fingerprintId):Promise.resolve(void 0)]),c={event_type:e.eventType,user_id:t,ip_address:i,device_id:r,session_hash:e.sessionHash,user_agent:a,email_address:o};return null!=n&&(c.fingerprint_id=n),null!=e.subscriptionStatus&&(c.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(c.event_details=e.eventDetails),this.R(`${this.l}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c)},{timeoutMs:s?.timeoutMs,maxRetries:s?.maxRetries??0})}async checkUser(e,s){const t="string"==typeof s?{deviceId:s}:s;if(!t.deviceId&&!t.fingerprintId)return{success:!0,status:200,data:{is_user_flagged:!1},failedOpen:{status:0,reason:"no_device_id"}};const[i,r,a]=await Promise.all([this.p(e),t.deviceId?this.p(t.deviceId):Promise.resolve(void 0),t.fingerprintId?this.p(t.fingerprintId):Promise.resolve(void 0)]),o=new URLSearchParams;o.set("email_address",i),t.deviceId&&o.set("device_id",r),t.fingerprintId&&o.set("fingerprint_id",a);const n=await this.R(`${this.l}/v2/check-user?${o}`,{method:"GET"},{timeoutMs:t.timeoutMs,maxRetries:t.maxRetries});return n.success?n:{success:!0,status:200,data:{is_user_flagged:!1},failedOpen:{status:n.status}}}async triggerEmailVerification(e,s,t){const[i,r,a]=await Promise.all([this.p(e),this.p(s),t?.fingerprintId?this.p(t.fingerprintId):Promise.resolve(void 0)]),o={email_address:i,device_id:r};t?.fingerprintId&&(o.fingerprint_id=a);const n=await this.R(`${this.l}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)},{maxRetries:0});return!n.success&&(0===n.status||n.status>=500)?{success:!1,status:n.status,error:{code:"DELIVERY_FAILED",message:n.error?.message??"Delivery failed"}}:n}async verify(e,s,t,i){const[r,a,o,n]=await Promise.all([this.p(e),this.p(s),this.p(t),i?.fingerprintId?this.p(i.fingerprintId):Promise.resolve(void 0)]),c={email_address:r,device_id:a,code:o};i?.fingerprintId&&(c.fingerprint_id=n);const l=await this.R(`${this.l}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c)},{maxRetries:0});return!l.success&&(0===l.status||l.status>=500)?{success:!1,status:l.status,error:{code:"DELIVERY_FAILED",message:l.error?.message??"Delivery failed"}}:l.success&&!0!==l.data?.verified?{success:!1,status:l.status,error:{code:"VERIFICATION_FAILED",message:"Code is incorrect or expired",details:l.data?.reason?{reason:l.data.reason}:void 0}}:l}async getInterstitialFlow(e){const s=new URLSearchParams;return s.set("flow_type",e?.flowType||"email_verification"),s.set("platform",e?.platform||"web"),this.R(`${this.l}/v2/interstitial-flow?${s}`,{method:"GET"})}}exports.UnsharedClient=UnsharedClient;
@@ -147,9 +147,11 @@ export declare class UnsharedClient {
147
147
  private readonly _baseUrl;
148
148
  private readonly _timeout;
149
149
  private readonly _maxRetries;
150
- private readonly _encryptionKey;
150
+ private _encryptionKey;
151
151
  private readonly _customFetch;
152
152
  constructor(config: UnsharedClientConfig);
153
+ /** Lazily derive + memoize the imported AES-GCM CryptoKey (async via WebCrypto). */
154
+ private _getKey;
153
155
  private _encrypt;
154
156
  /**
155
157
  * Core HTTP method with retry logic.
@@ -168,7 +170,10 @@ export declare class UnsharedClient {
168
170
  * Record a user event and receive naughty-list analysis.
169
171
  * Maps to: POST /v2/process-user-event
170
172
  */
171
- processUserEvent(params: ProcessUserEventParams): Promise<ApiResult<ProcessUserEventResult>>;
173
+ processUserEvent(params: ProcessUserEventParams, overrides?: {
174
+ timeoutMs?: number;
175
+ maxRetries?: number;
176
+ }): Promise<ApiResult<ProcessUserEventResult>>;
172
177
  /**
173
178
  * Check if a user is flagged in the naughty list.
174
179
  * Maps to: GET /v2/check-user
@@ -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,s){const r=(s?.maxRetries??this.h)+1,i=s?.timeoutMs??this.o;let a={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let s=1;s<=r;s++){s>1&&await sleep(retryDelay(s-1));const r=new AbortController,n=setTimeout(()=>r.abort(),i);try{const s=this.u??globalThis.fetch,i=await s(e,{method:t.method,headers:{"X-API-Key":this.t,...t.headers},body:t.body,signal:r.signal});if(clearTimeout(n),i.ok){const e=await i.text().catch(()=>"{}");let t;try{t=JSON.parse(e)}catch{t={}}const s="data"in t?t.data:t;return{success:!0,status:i.status,data:s}}const o=await parseErrorBody(i);if(i.status>=400&&i.status<500){if(429===i.status){const e=i.headers.get("Retry-After");if(null!=e){const t=parseInt(e,10);isNaN(t)||(o.retryAfter=t)}}return{success:!1,status:i.status,error:o}}a={success:!1,status:i.status,error:o}}catch(e){clearTimeout(n),a={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return a}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!=e.source&&(s.source=e.source),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},failedOpen:{status:0,reason:"no_device_id"}};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"},{timeoutMs:s.timeoutMs,maxRetries:s.maxRetries});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.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{encryptDataWeb,sha256Bytes,importAesGcmKey}from"./webcrypto";const DEFAULT_BASE_URL="https://api.unshared.ai",DEFAULT_TIMEOUT_MS=1e4,DEFAULT_MAX_RETRIES=3,MAX_DELAY_MS=3e4,BASE_DELAY_MS=1e3;function sleep(e){return new Promise(s=>setTimeout(s,e))}function retryDelay(e){const s=Math.min(1e3*Math.pow(2,e-1),3e4),t=s*(.5*Math.random()-.25);return Math.max(0,s+t)}async function parseErrorBody(e){const s=await e.text().catch(()=>"");try{const t=JSON.parse(s);return t?.error?.code?{code:t.error.code,message:t.error.message??"Unknown error",details:t.error.details}:{code:"UNKNOWN_ERROR",message:s||e.statusText}}catch{return{code:"UNKNOWN_ERROR",message:s||e.statusText}}}export class UnsharedClient{constructor(e){if(this.t=null,!e.apiKey||""===e.apiKey.trim())throw new Error("apiKey is required");this.i=e.apiKey,this.o=e.baseUrl?e.baseUrl.replace(/\/$/,""):DEFAULT_BASE_URL,this.l=e.timeout??1e4,this.h=e.maxRetries??3,this.u=e.fetch}m(){return this.t||(this.t=sha256Bytes(this.i).then(importAesGcmKey)),this.t}async _(e){return encryptDataWeb(e,await this.m())}async p(e,s,t){const i=(t?.maxRetries??this.h)+1,r=t?.timeoutMs??this.l;let a={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let t=1;t<=i;t++){t>1&&await sleep(retryDelay(t-1));const i=new AbortController,o=setTimeout(()=>i.abort(),r);try{const t=this.u??globalThis.fetch,r=await t(e,{method:s.method,headers:{"X-API-Key":this.i,...s.headers},body:s.body,signal:i.signal});if(r.ok){let e,s;try{e=await r.text()}catch(e){a={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}};continue}if(""===e.trim())return{success:!0,status:r.status,data:{}};try{s=JSON.parse(e)}catch{return{success:!1,status:r.status,error:{code:"PARSE_ERROR",message:"Unparseable 2xx response body"}}}const t="data"in s?s.data:s;return{success:!0,status:r.status,data:t}}const o=await parseErrorBody(r);if(r.status>=400&&r.status<500){if(429===r.status){const e=r.headers.get("Retry-After");if(null!=e){const s=parseInt(e,10);isNaN(s)||(o.retryAfter=s)}if(null==o.retryAfter){const e=o.details?.retry_after_seconds;"number"!=typeof e||isNaN(e)||(o.retryAfter=e)}}return{success:!1,status:r.status,error:o}}a={success:!1,status:r.status,error:o}}catch(e){a={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}finally{clearTimeout(o)}}return a}async submitFingerprintEvent(e,s){const t={hash:e.full_hash,stable_hash:e.fingerprint_id,collected_at:e.timestamp,is_incognito:e.isIncognito,components:e.components,version:e.version};null!=e.source&&(t.source=e.source);const[i,r,a,o]=await Promise.all([null!=s?.userId?this._(s.userId):Promise.resolve(void 0),null!=s?.emailAddress?this._(s.emailAddress):Promise.resolve(void 0),null!=s?.ipAddress?this._(s.ipAddress):Promise.resolve(void 0),null!=s?.userAgent?this._(s.userAgent):Promise.resolve(void 0)]);return null!=i&&(t.user_id=i),null!=r&&(t.email_address=r),null!=s?.sessionHash&&(t.session_hash=s.sessionHash),null!=s?.eventType&&(t.event_type=s.eventType),null!=a&&(t.ip_address=a),null!=o&&(t.user_agent=o),this.p(`${this.o}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":s?.idempotencyKey??globalThis.crypto.randomUUID()},body:JSON.stringify(t)})}async processUserEvent(e,s){const[t,i,r,a,o,n]=await Promise.all([this._(e.userId),this._(e.ipAddress),this._(e.deviceId),this._(e.userAgent),this._(e.emailAddress),null!=e.fingerprintId?this._(e.fingerprintId):Promise.resolve(void 0)]),c={event_type:e.eventType,user_id:t,ip_address:i,device_id:r,session_hash:e.sessionHash,user_agent:a,email_address:o};return null!=n&&(c.fingerprint_id=n),null!=e.subscriptionStatus&&(c.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(c.event_details=e.eventDetails),this.p(`${this.o}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c)},{timeoutMs:s?.timeoutMs,maxRetries:s?.maxRetries??0})}async checkUser(e,s){const t="string"==typeof s?{deviceId:s}:s;if(!t.deviceId&&!t.fingerprintId)return{success:!0,status:200,data:{is_user_flagged:!1},failedOpen:{status:0,reason:"no_device_id"}};const[i,r,a]=await Promise.all([this._(e),t.deviceId?this._(t.deviceId):Promise.resolve(void 0),t.fingerprintId?this._(t.fingerprintId):Promise.resolve(void 0)]),o=new URLSearchParams;o.set("email_address",i),t.deviceId&&o.set("device_id",r),t.fingerprintId&&o.set("fingerprint_id",a);const n=await this.p(`${this.o}/v2/check-user?${o}`,{method:"GET"},{timeoutMs:t.timeoutMs,maxRetries:t.maxRetries});return n.success?n:{success:!0,status:200,data:{is_user_flagged:!1},failedOpen:{status:n.status}}}async triggerEmailVerification(e,s,t){const[i,r,a]=await Promise.all([this._(e),this._(s),t?.fingerprintId?this._(t.fingerprintId):Promise.resolve(void 0)]),o={email_address:i,device_id:r};t?.fingerprintId&&(o.fingerprint_id=a);const n=await this.p(`${this.o}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)},{maxRetries:0});return!n.success&&(0===n.status||n.status>=500)?{success:!1,status:n.status,error:{code:"DELIVERY_FAILED",message:n.error?.message??"Delivery failed"}}:n}async verify(e,s,t,i){const[r,a,o,n]=await Promise.all([this._(e),this._(s),this._(t),i?.fingerprintId?this._(i.fingerprintId):Promise.resolve(void 0)]),c={email_address:r,device_id:a,code:o};i?.fingerprintId&&(c.fingerprint_id=n);const l=await this.p(`${this.o}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c)},{maxRetries:0});return!l.success&&(0===l.status||l.status>=500)?{success:!1,status:l.status,error:{code:"DELIVERY_FAILED",message:l.error?.message??"Delivery failed"}}:l.success&&!0!==l.data?.verified?{success:!1,status:l.status,error:{code:"VERIFICATION_FAILED",message:"Code is incorrect or expired",details:l.data?.reason?{reason:l.data.reason}:void 0}}:l}async getInterstitialFlow(e){const s=new URLSearchParams;return s.set("flow_type",e?.flowType||"email_verification"),s.set("platform",e?.platform||"web"),this.p(`${this.o}/v2/interstitial-flow?${s}`,{method:"GET"})}}
@@ -1,6 +1,7 @@
1
1
  export declare class DispatchDedupe {
2
2
  private readonly _entries;
3
3
  private readonly _ttlMs;
4
+ private _lastSweepAt;
4
5
  constructor(ttlMs?: number);
5
6
  private _key;
6
7
  mark(userId: string, eventType: string): void;
@@ -1 +1 @@
1
- const DEFAULT_TTL_MS=1e4,SWEEP_THRESHOLD=1e3;export class DispatchDedupe{constructor(t=1e4){this.t=new Map,this.i=t}h(t,s){return`${t}|${s}`}mark(t,s){this.t.set(this.h(t,s),Date.now()),this.t.size>1e3&&this.o()}wasRecentlyDispatched(t,s){const e=this.h(t,s),i=this.t.get(e);return!(void 0===i||Date.now()-i>this.i&&(this.t.delete(e),1))}clear(){this.t.clear()}get size(){return this.t.size}o(){const t=Date.now()-this.i;for(const[s,e]of this.t)e<t&&this.t.delete(s)}}
1
+ const DEFAULT_TTL_MS=1e4,SWEEP_THRESHOLD=1e3,SWEEP_MIN_INTERVAL_MS=1e3;export class DispatchDedupe{constructor(t=1e4){this.t=new Map,this.i=0,this.h=t}o(t,s){return`${t}|${s}`}mark(t,s){if(this.t.set(this.o(t,s),Date.now()),this.t.size>1e3){const t=Date.now();t-this.i>=1e3&&(this.i=t,this.D())}}wasRecentlyDispatched(t,s){const e=this.o(t,s),i=this.t.get(e);return!(void 0===i||Date.now()-i>this.h&&(this.t.delete(e),1))}clear(){this.t.clear()}get size(){return this.t.size}D(){const t=Date.now()-this.h;for(const[s,e]of this.t)e<t&&this.t.delete(s)}}
@@ -0,0 +1,22 @@
1
+ import type { UnsharedClient, ProcessUserEventParams } from '../client';
2
+ import type { VerdictCache } from './verdict-cache';
3
+ import type { RateLimitBackoff } from './rate-limit-backoff';
4
+ /** Single policy point for background processUserEvent budgets. */
5
+ export declare const DISPATCH_TIMEOUT_MS = 2000;
6
+ export declare const SUBMIT_FP_TIMEOUT_MS = 3000;
7
+ export interface DispatchIo {
8
+ client: UnsharedClient;
9
+ cache: VerdictCache;
10
+ backoff: RateLimitBackoff;
11
+ onError?: (err: unknown, ctx: {
12
+ operation: 'processUserEvent';
13
+ userId: string;
14
+ emailAddress: string;
15
+ }) => void;
16
+ }
17
+ /**
18
+ * Run processUserEvent with the standard bounded budget and backoff wiring.
19
+ * Never rejects. All four dispatch sites (classic/web x dispatch/submit-fp)
20
+ * MUST go through this helper so the cap and backoff can't drift per-site.
21
+ */
22
+ export declare function runProcessUserEvent(io: DispatchIo, params: ProcessUserEventParams, timeoutMs: number): Promise<void>;
@@ -0,0 +1 @@
1
+ export const DISPATCH_TIMEOUT_MS=2e3;export const SUBMIT_FP_TIMEOUT_MS=3e3;export async function runProcessUserEvent(e,t,s){try{const r=await e.client.processUserEvent(t,{timeoutMs:s,maxRetries:0});r.success&&r.data?.analysis&&e.cache.update(t.userId,{isFlagged:r.data.analysis.is_user_flagged}),r.success?e.backoff.noteSuccess():r.error?.retryAfter?e.backoff.pause(1e3*r.error.retryAfter):e.backoff.noteFailure()}catch(s){e.backoff.noteFailure();try{e.onError&&e.onError(s,{operation:"processUserEvent",userId:t.userId,emailAddress:t.emailAddress})}catch{}}}
@@ -20,6 +20,18 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
20
20
  corsOrigins?: string | string[];
21
21
  /** Verdict cache TTL in ms. @default 60000 */
22
22
  cacheTTL?: number;
23
+ /** Max number of entries in the verdict cache before oldest-inserted entries are evicted. @default 10000 */
24
+ maxCacheSize?: number;
25
+ /**
26
+ * Budget in ms from the first buffered HTML write until res.end() before the
27
+ * response is treated as streaming SSR and fingerprint injection is skipped
28
+ * (the response itself is never delayed or altered past this point).
29
+ * Raise this if your handlers flush partial HTML early and finish rendering
30
+ * later (e.g. res.write(head) → await db → res.end(rest)) and you see the
31
+ * injection being skipped.
32
+ * @default 50
33
+ */
34
+ streamingIdleMs?: number;
23
35
  /** Paths to skip entirely (static assets, health checks). */
24
36
  skipPaths?: string[];
25
37
  /** When set, only paths matching one of these prefixes get events dispatched and checkUser called. */
@@ -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{createFingerprintBundleSource}from"./injection/fp-bundle-source";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,sendBodyForMethod,getRequestPath}from"./utils/http-helpers";import{isHtmlContentType,isHtmlNavigation}from"./utils/content-type";import{flaggedResponse}from"./utils/flagged-response";import{maskEmail}from"./utils/mask-email";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=1500,FAIL_OPEN_TTL_MS=5e3,_lastFailOpenWarn={};function warnFailOpenThrottled(e){const t=Date.now();t-(_lastFailOpenWarn[e.reason]??0)<6e4||(_lastFailOpenWarn[e.reason]=t,console.warn(`[Unshared] Failing open (reason=${e.reason}${null!=e.status?`, status=${e.status}`:""}). Detection is not enforced for this request. Configure onFailOpen to observe this directly.`))}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:a,includePathPrefix:d,disableBotFilter:c=!1,checkUserTimeoutMs:l=CHECK_USER_TIMEOUT_MS,cacheMissStrategy:u="block",sessionId:p,deviceId:f,onFlagged:m,onError:h,onFailOpen:g,blockFlagged:I=!1,flaggedMode:v,autoInterstitial:S=!1,interstitialFlowType:_="email_verification"}=t,C=v??(I?"gate":void 0),y=S||"overlay"===C,k=e=>{if(g)try{g(e)}catch{}else"async_miss"!==e.reason&&warnFailOpenThrottled(e)},A=new VerdictCache(s),T=new RateLimitBackoff,E=new Map,F=new DispatchDedupe,x=Date.now().toString(36),U=generateFingerprintScript(n,x,{autoInterstitial:y,interstitialFlowType:_});let w="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");w=readFileSync(e,"utf8")}catch{}const O=createFingerprintBundleSource(),P=void 0!==C||y,M="gate"===C?"flaggedMode 'gate' (blockFlagged)":"overlay"===C?"flaggedMode 'overlay'":"autoInterstitial";if(P&&!w)throw new Error(`[Unshared] ${M} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(P&&"/__unshared"!==n)throw new Error(`[Unshared] ${M} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const b="gate"===C?generateGatePage(n,x):"",D=handleSubmitFingerprint({client:e,verdictCache:A,rateLimitBackoff:T,dispatchDedupe:F,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:p,resolveDeviceId:f,disableBotFilter:c,onError:h}),R=handleVerifyTrigger({client:e,verdictCache:A,resolveEmailAddress:i,resolveDeviceId:f,onError:h}),N=handleVerify({client:e,verdictCache:A,resolveUserId:r,resolveEmailAddress:i,resolveDeviceId:f,onError:h}),$=handleGetInterstitialFlow({client:e}),V=o?Array.isArray(o)?o:[o]:null,L=`${n}/fp.js`,j=`${n}/fingerprint.js`,B=`${n}/submit-fp`,q=`${n}/verify-trigger`,H=`${n}/verify`,G=`${n}/status`,J=`${n}/interstitial-flow`,K=function(t,o,s){const g=getRequestPath(t.url),I=t.url||g;if(g.startsWith(n+"/")){if(function(e,t){if(!V)return;const r=e.headers.origin??"";V.includes("*")?(t.setHeader("Access-Control-Allow-Origin","*"),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")):V.includes(r)&&(t.setHeader("Access-Control-Allow-Origin",r),t.setHeader("Vary","Origin"),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||"HEAD"===t.method)&&g===L)return o.setHeader("Content-Type","application/javascript"),o.setHeader("Cache-Control","public, max-age=3600"),void sendBodyForMethod(o,t.method,200,w);if(("GET"===t.method||"HEAD"===t.method)&&g===j){const e=O.get();return e?(o.setHeader("Content-Type","application/javascript"),o.setHeader("Cache-Control","public, max-age=3600"),void sendBodyForMethod(o,t.method,200,e)):void sendBodyForMethod(o,t.method,503,"/* fingerprint agent unavailable */")}if("POST"===t.method&&(g===B||g===q||g===H))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===B?void D(t,o):g===q?void R(t,o):void N(t,o);if("GET"===t.method&&g===J)return void $(t,o);if("GET"===t.method&&g===G){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=A.get(n);if((!r||A.isStale(n))&&s&&!T.isPaused()&&!A.isRefreshing(n)){A.markRefreshing(n);try{const i=extractDeviceIdOrUndefined(t,f),o=extractFingerprintId(t),a=extractSessionId(t,p),d=i??o??"unknown";await fetchAndCacheVerdict(e,A,n,s,d,o,a,l,(e,t)=>k({operation:"checkUser",reason:e,status:t,userId:n,emailAddress:s})),r=A.get(n)}catch(e){h&&h(e,{operation:"checkUser",userId:n,emailAddress:s}),k({operation:"checkUser",reason:"exception",userId:n,emailAddress:s})}finally{A.clearRefreshing(n)}}r&&r.isFlagged&&!r.isVerified&&(void 0!==C||m)&&s?sendJson(o,200,{status:"flagged",email:maskEmail(s)}):sendJson(o,200,{status:"ok"})})()}return void sendJson(o,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if(shouldSkipPath(g,a))return void s();if(!shouldIncludePath(g,d))return interceptForInjection(t,o,U),void s();let v;try{v=r(t)}catch{}if(isSentinelUserId(v)){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;v=e&&n?e:void 0}if(!v){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,U),void s()}const S=resolveEmail(t,i);if(setUserIdCookie(t,o,v),S&&setEmailCookie(t,o,S),!S)return interceptForInjection(t,o,U),void s();const _=extractSessionId(t,p),y=extractDeviceIdOrUndefined(t,f),x=extractFingerprintId(t),P=t.headers["user-agent"]??"",M=extractClientIp(t),K=y??x;if(!c&&isBot(P))return void s();const X=A.get(v);if(!X){if("async"===u)return A.isRefreshing(v)||(A.markRefreshing(v),fetchAndCacheVerdict(e,A,v,S,K??"unknown",x,_,l,(e,t)=>k({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:S})).catch(()=>k({operation:"checkUser",reason:"exception",userId:v,emailAddress:S})).finally(()=>A.clearRefreshing(v))),k({operation:"checkUser",reason:"async_miss",userId:v,emailAddress:S}),W(),interceptForInjection(t,o,U),void s();let r=E.get(v);return r||(r=fetchAndCacheVerdict(e,A,v,S,K??"unknown",x,_,l,(e,t)=>k({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:S})).finally(()=>E.delete(v)),E.set(v,r)),void r.then(e=>{e.isFlagged||W(),applyVerdict(e,v,S,t,o,s,U,m,C,b)}).catch(()=>{k({operation:"checkUser",reason:"exception",userId:v,emailAddress:S}),W(),interceptForInjection(t,o,U),s()})}function W(){"unknown"!==_&&K&&(T.isPaused()||dispatchUserEvent(e,A,T,F,{userId:v,emailAddress:S,sessionId:_,deviceId:K,fingerprintId:x,userAgent:P,ipAddress:M,eventType:I},h))}A.isStale(v)&&!A.isRefreshing(v)&&(A.markRefreshing(v),fetchAndCacheVerdict(e,A,v,S,K??"unknown",x,_,l,(e,t)=>k({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:S})).catch(()=>k({operation:"checkUser",reason:"exception",userId:v,emailAddress:S})).finally(()=>A.clearRefreshing(v))),X.isFlagged||W(),applyVerdict(X,v,S,t,o,s,U,m,C,b)};return K.destroy=()=>{A.destroy(),O.stop()},K}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,a,d,c){const l=e.isFlagged&&!e.isVerified;if("gate"===d&&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"===d&&l)isHtmlNavigation(i.method,i.headers.accept)?(interceptForInjection(i,n,s),o()):sendJson(n,403,flaggedResponse(r));else if(interceptForInjection(i,n,s),e.isFlagged&&!e.isVerified&&a)try{a({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,a=CHECK_USER_TIMEOUT_MS,d){const c={timeoutMs:a,maxRetries:0},l=n&&"unknown"!==n?n:void 0,u=l??o,p=o??l;let f;u&&(c.deviceId=u),p&&(c.fingerprintId=p);const m=await Promise.race([e.checkUser(i,c),new Promise(e=>{f=setTimeout(()=>e(null),a)})]);return clearTimeout(f),m?m.failedOpen?(d?.(m.failedOpen.reason??"http_error",m.failedOpen.status),cacheFailOpenVerdict(t,r,i,s)):(t.set(r,{isFlagged:m.data?.is_user_flagged??!1,isVerified:!1,emailAddress:i,sessionId:s}),t.get(r)):(d?.("timeout"),cacheFailOpenVerdict(t,r,i,s))}function cacheFailOpenVerdict(e,t,r,i){const n=e.get(t);return e.set(t,{isFlagged:n?.isFlagged??!1,isVerified:n?.isVerified??!1,emailAddress:r,sessionId:i},5e3),e.get(t)}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{createFingerprintBundleSource}from"./injection/fp-bundle-source";import{handleSubmitFingerprint}from"./routes/submit-fp";import{handleVerifyTrigger,handleVerify}from"./routes/verify";import{handleGetInterstitialFlow}from"./routes/interstitial";import{generateGatePage}from"./injection/gate-page";import{runProcessUserEvent,DISPATCH_TIMEOUT_MS}from"./dispatch-processing";import{sendJson,sendEmpty,sendBodyForMethod,getRequestPath}from"./utils/http-helpers";import{isHtmlContentType,isHtmlNavigation}from"./utils/content-type";import{flaggedResponse}from"./utils/flagged-response";import{maskEmail}from"./utils/mask-email";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=1500,FAIL_OPEN_TTL_MS=5e3,_lastFailOpenWarn={};function warnFailOpenThrottled(e){const t=Date.now();t-(_lastFailOpenWarn[e.reason]??0)<6e4||(_lastFailOpenWarn[e.reason]=t,console.warn(`[Unshared] Failing open (reason=${e.reason}${null!=e.status?`, status=${e.status}`:""}). Detection is not enforced for this request. Configure onFailOpen to observe this directly.`))}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,maxCacheSize:a,streamingIdleMs:d=50,skipPaths:c,includePathPrefix:l,disableBotFilter:u=!1,checkUserTimeoutMs:p=CHECK_USER_TIMEOUT_MS,cacheMissStrategy:f="block",sessionId:m,deviceId:h,onFlagged:g,onError:I,onFailOpen:S,blockFlagged:v=!1,flaggedMode:_,autoInterstitial:C=!1,interstitialFlowType:y="email_verification"}=t,k=_??(v?"gate":void 0),T=C||"overlay"===k,A=e=>{if(S)try{S(e)}catch{}else"async_miss"!==e.reason&&warnFailOpenThrottled(e)},E=new VerdictCache(s,a),F=new RateLimitBackoff,U=new Map,x=new DispatchDedupe,w=Date.now().toString(36),O=generateFingerprintScript(n,w,{autoInterstitial:T,interstitialFlowType:y});let M="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");M=readFileSync(e,"utf8")}catch{}const P=createFingerprintBundleSource(),b=void 0!==k||T,D="gate"===k?"flaggedMode 'gate' (blockFlagged)":"overlay"===k?"flaggedMode 'overlay'":"autoInterstitial";if(b&&!M)throw new Error(`[Unshared] ${D} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(b&&"/__unshared"!==n)throw new Error(`[Unshared] ${D} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const R="gate"===k?generateGatePage(n,w):"",N=handleSubmitFingerprint({client:e,verdictCache:E,rateLimitBackoff:F,dispatchDedupe:x,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:m,resolveDeviceId:h,disableBotFilter:u,onError:I}),$=handleVerifyTrigger({client:e,verdictCache:E,resolveEmailAddress:i,resolveDeviceId:h,onError:I}),V=handleVerify({client:e,verdictCache:E,resolveUserId:r,resolveEmailAddress:i,resolveDeviceId:h,onError:I}),L=handleGetInterstitialFlow({client:e}),j=o?Array.isArray(o)?o:[o]:null,H=`${n}/fp.js`,B=`${n}/fingerprint.js`,q=`${n}/submit-fp`,G=`${n}/verify-trigger`,J=`${n}/verify`,K=`${n}/status`,X=`${n}/interstitial-flow`,W=function(t,o,s){const a=getRequestPath(t.url),S=t.url||a;if(a.startsWith(n+"/")){if(function(e,t){if(!j)return;const r=e.headers.origin??"";j.includes("*")?(t.setHeader("Access-Control-Allow-Origin","*"),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")):j.includes(r)&&(t.setHeader("Access-Control-Allow-Origin",r),t.setHeader("Vary","Origin"),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||"HEAD"===t.method)&&a===H)return o.setHeader("Content-Type","application/javascript"),o.setHeader("Cache-Control","public, max-age=3600"),void sendBodyForMethod(o,t.method,200,M);if(("GET"===t.method||"HEAD"===t.method)&&a===B){const e=P.get();return e?(o.setHeader("Content-Type","application/javascript"),o.setHeader("Cache-Control","public, max-age=3600"),void sendBodyForMethod(o,t.method,200,e)):void sendBodyForMethod(o,t.method,503,"/* fingerprint agent unavailable */")}if("POST"===t.method&&(a===q||a===G||a===J))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."}}):a===q?void N(t,o):a===G?void $(t,o):void V(t,o);if("GET"===t.method&&a===X)return void L(t,o);if("GET"===t.method&&a===K){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=E.get(n);if((!r||E.isStale(n))&&s&&!F.isPaused()&&!E.isRefreshing(n)){E.markRefreshing(n);try{const i=extractDeviceIdOrUndefined(t,h),o=extractFingerprintId(t),a=extractSessionId(t,m),d=i??o??"unknown";await fetchAndCacheVerdict(e,E,n,s,d,o,a,p,(e,t)=>A({operation:"checkUser",reason:e,status:t,userId:n,emailAddress:s})),r=E.get(n)}catch(e){I&&I(e,{operation:"checkUser",userId:n,emailAddress:s}),A({operation:"checkUser",reason:"exception",userId:n,emailAddress:s})}finally{E.clearRefreshing(n)}}r&&r.isFlagged&&!r.isVerified&&(void 0!==k||g)&&s?sendJson(o,200,{status:"flagged",email:maskEmail(s)}):sendJson(o,200,{status:"ok"})})()}return void sendJson(o,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if(shouldSkipPath(a,c,void 0!==k))return void s();if(!shouldIncludePath(a,l))return interceptForInjection(t,o,O,d),void s();let v;try{v=r(t)}catch{}if(isSentinelUserId(v)){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;v=e&&n?e:void 0}if(!v){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,O,d),void s()}const _=resolveEmail(t,i);if(setUserIdCookie(t,o,v),_&&setEmailCookie(t,o,_),!_)return interceptForInjection(t,o,O,d),void s();const C=extractSessionId(t,m),y=extractDeviceIdOrUndefined(t,h),T=extractFingerprintId(t),w=t.headers["user-agent"]??"",b=extractClientIp(t),D=y??T;if(!u&&isBot(w))return void s();const W=E.get(v);if(!W){if("async"===f)return E.isRefreshing(v)||(E.markRefreshing(v),fetchAndCacheVerdict(e,E,v,_,D??"unknown",T,C,p,(e,t)=>A({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:_})).catch(()=>A({operation:"checkUser",reason:"exception",userId:v,emailAddress:_})).finally(()=>E.clearRefreshing(v))),A({operation:"checkUser",reason:"async_miss",userId:v,emailAddress:_}),z(),interceptForInjection(t,o,O,d),void s();let r=U.get(v);return r||(r=fetchAndCacheVerdict(e,E,v,_,D??"unknown",T,C,p,(e,t)=>A({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:_})).finally(()=>U.delete(v)),U.set(v,r)),void r.then(e=>{e.isFlagged||z(),applyVerdict(e,v,_,t,o,s,O,g,k,R,d)}).catch(()=>{A({operation:"checkUser",reason:"exception",userId:v,emailAddress:_}),z(),interceptForInjection(t,o,O,d),s()})}function z(){"unknown"!==C&&D&&(F.isPaused()||dispatchUserEvent(e,E,F,x,{userId:v,emailAddress:_,sessionId:C,deviceId:D,fingerprintId:T,userAgent:w,ipAddress:b,eventType:S},I))}E.isStale(v)&&!E.isRefreshing(v)&&(E.markRefreshing(v),fetchAndCacheVerdict(e,E,v,_,D??"unknown",T,C,p,(e,t)=>A({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:_})).catch(()=>A({operation:"checkUser",reason:"exception",userId:v,emailAddress:_})).finally(()=>E.clearRefreshing(v))),W.isFlagged||z(),applyVerdict(W,v,_,t,o,s,O,g,k,R,d)};return W.destroy=()=>{E.destroy(),P.stop()},W}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,a,d,c,l){const u=e.isFlagged&&!e.isVerified;if("gate"===d&&u)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"===d&&u)isHtmlNavigation(i.method,i.headers.accept)?(interceptForInjection(i,n,s,l),o()):sendJson(n,403,flaggedResponse(r));else if(interceptForInjection(i,n,s,l),e.isFlagged&&!e.isVerified&&a)try{a({userId:t,emailAddress:r,verdict:e,req:i,res:n,next:o})}catch{o()}else o()}function interceptForInjection(e,t,r,i){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,streamingIdleMs:i})}function dispatchUserEvent(e,t,r,i,n,o){i.mark(n.userId,n.eventType),runProcessUserEvent({client:e,cache:t,backoff:r,onError:o},{eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,sessionHash:n.sessionId,userAgent:n.userAgent},DISPATCH_TIMEOUT_MS)}async function fetchAndCacheVerdict(e,t,r,i,n,o,s,a=CHECK_USER_TIMEOUT_MS,d){const c={timeoutMs:a,maxRetries:0},l=n&&"unknown"!==n?n:void 0,u=l??o,p=o??l;let f;u&&(c.deviceId=u),p&&(c.fingerprintId=p);const m=await Promise.race([e.checkUser(i,c),new Promise(e=>{f=setTimeout(()=>e(null),a)})]);return clearTimeout(f),m?m.failedOpen?(d?.(m.failedOpen.reason??"http_error",m.failedOpen.status),cacheFailOpenVerdict(t,r,i,s)):(t.set(r,{isFlagged:m.data?.is_user_flagged??!1,isVerified:!1,emailAddress:i,sessionId:s}),t.get(r)):(d?.("timeout"),cacheFailOpenVerdict(t,r,i,s))}function cacheFailOpenVerdict(e,t,r,i){const n=e.get(t);return e.set(t,{isFlagged:n?.isFlagged??!1,isVerified:n?.isVerified??!1,emailAddress:r,sessionId:i},5e3),e.get(t)}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,14 +1,17 @@
1
- /**
2
- * Global rate-limit backoff for processUserEvent calls.
3
- *
4
- * When the Unshared API returns 429 with a Retry-After header,
5
- * the middleware pauses processUserEvent calls for the specified duration.
6
- * checkUser calls are not affected — enforcement takes priority.
7
- */
8
1
  export declare class RateLimitBackoff {
9
2
  private _resumeAtTimestamp;
10
- /** Pause processUserEvent calls for the given duration. */
3
+ private _failureResumeAtTimestamp;
4
+ private _consecutiveFailures;
5
+ /** Pause processUserEvent calls (server-directed 429), clamped to MAX_BACKOFF_MS. */
11
6
  pause(durationMs: number): void;
7
+ /** Record a network/5xx/timeout failure; engage failure backoff at the threshold. */
8
+ noteFailure(): void;
9
+ /** Record a success; resets the failure streak and lifts ONLY the failure-streak
10
+ * pause. It deliberately does NOT touch a server-directed 429 pause (pause()),
11
+ * so a success on one request can't wipe another request's legitimate 429 backoff. */
12
+ noteSuccess(): void;
12
13
  /** Returns true if processUserEvent calls should be skipped. */
13
14
  isPaused(): boolean;
15
+ /** Parse a Retry-After header value (delta-seconds OR HTTP-date) into ms, or null. */
16
+ static parseRetryAfter(headerValue: string | null, now?: number): number | null;
14
17
  }
@@ -1 +1 @@
1
- export class RateLimitBackoff{constructor(){this.t=0}pause(t){const s=Date.now()+t;s>this.t&&(this.t=s)}isPaused(){return Date.now()<this.t}}
1
+ const MAX_BACKOFF_MS=3e5,FAILURE_THRESHOLD=3,FAILURE_BACKOFF_MS=1e4;export class RateLimitBackoff{constructor(){this.t=0,this.i=0,this.h=0}pause(t){const s=Math.min(Math.max(0,t),3e5),e=Date.now()+s;e>this.t&&(this.t=e)}noteFailure(){if(this.h++,this.h>=3){const t=Date.now()+1e4;t>this.i&&(this.i=t)}}noteSuccess(){this.h=0,this.i=0}isPaused(){const t=Date.now();return t<this.t||t<this.i}static parseRetryAfter(t,s=Date.now()){if(null==t)return null;const e=parseInt(t,10);if(!isNaN(e)&&String(e)===t.trim())return 1e3*e;const i=Date.parse(t);return isNaN(i)?null:Math.max(0,i-s)}}
@@ -19,4 +19,5 @@ import type { UnsharedResponse } from '../types';
19
19
  */
20
20
  export declare function interceptResponse(res: UnsharedResponse, transform: (body: Buffer, contentType: string | undefined) => Buffer | string | null, options?: {
21
21
  preventCaching?: boolean;
22
+ streamingIdleMs?: number;
22
23
  }): void;
@@ -1 +1 @@
1
- const MAX_INJECT_BYTES=2e6;function findWriteHeadHeaders(e){return e.find(e=>e&&"object"==typeof e)}function readHeaderFromWriteHeadArgs(e,t){const n=findWriteHeadHeaders(e);if(!n)return;const r=t.toLowerCase();if(Array.isArray(n)){if(n.length&&Array.isArray(n[0])){for(const e of n)if(String(e[0]).toLowerCase()===r)return e[1]}else for(let e=0;e+1<n.length;e+=2)if(String(n[e]).toLowerCase()===r)return n[e+1]}else for(const e of Object.keys(n))if(e.toLowerCase()===r)return n[e]}function deleteHeaderFromWriteHeadArgs(e,t){const n=findWriteHeadHeaders(e);if(!n)return;const r=t.toLowerCase();if(Array.isArray(n))if(n.length&&Array.isArray(n[0]))for(let e=n.length-1;e>=0;e--)String(n[e][0]).toLowerCase()===r&&n.splice(e,1);else for(let e=n.length-2;e>=0;e-=2)String(n[e]).toLowerCase()===r&&n.splice(e,2);else for(const e of Object.keys(n))e.toLowerCase()===r&&delete n[e]}export function interceptResponse(e,t,n){const r=n?.preventCaching??!1,o=e.write.bind(e),f=e.end.bind(e),i=e.writeHead.bind(e),c=[];let u=0,l=!1,s=null;function d(){e.write=o,e.end=f;for(const e of c)o(e);c.length=0}function a(e,t){null===s&&null!=e&&(s=String(e).includes("text/html"),s&&t&&(s=!1),!1===s&&d())}function g(){a(e.getHeader("content-type"),e.getHeader("content-encoding"))}e.writeHead=function(t,...n){return a(readHeaderFromWriteHeadArgs(n,"content-type")??e.getHeader("content-type"),readHeaderFromWriteHeadArgs(n,"content-encoding")??e.getHeader("content-encoding")),s&&(r&&(e.setHeader("Cache-Control","no-store"),e.removeHeader("ETag"),e.removeHeader("Last-Modified")),e.removeHeader("Content-Length"),deleteHeaderFromWriteHeadArgs(n,"content-length")),i(t,...n)},e.write=function(e,t,n){if(g(),!1===s)return o(e,t,n);if(null!=e){const r=Buffer.isBuffer(e)?e:Buffer.from(e,"string"==typeof t?t:"utf8");if(u+=r.length,c.push(r),u>2e6)return d(),"function"==typeof t&&t(null),"function"==typeof n&&n(null),!0}return"function"==typeof t&&t(null),"function"==typeof n&&n(null),!0},e.end=function(n,r,i){if(l)return e;if(l=!0,g(),!1===s)return f(n,r,i);if(null!=n){const e=Buffer.isBuffer(n)?n:Buffer.from(n,"string"==typeof r?r:"utf8");u+=e.length,c.push(e)}if(u>2e6){d();const e="function"==typeof r?r:i;return e?f(e):f()}const a=Buffer.concat(c),y=e.getHeader("content-type");let p;try{p=t(a,y)}catch{p=null}if(null!=p){const t=Buffer.isBuffer(p)?p:Buffer.from(p,"utf8");e.headersSent||(e.setHeader("Content-Length",t.length),e.removeHeader("Content-Encoding")),o(t)}else a.length>0&&o(a);const H="function"==typeof r?r:i;return H?f(H):f(),e}}
1
+ const MAX_INJECT_BYTES=2e6;function findWriteHeadHeaders(e){return e.find(e=>e&&"object"==typeof e)}function readHeaderFromWriteHeadArgs(e,t){const n=findWriteHeadHeaders(e);if(!n)return;const r=t.toLowerCase();if(Array.isArray(n)){if(n.length&&Array.isArray(n[0])){for(const e of n)if(String(e[0]).toLowerCase()===r)return e[1]}else for(let e=0;e+1<n.length;e+=2)if(String(n[e]).toLowerCase()===r)return n[e+1]}else for(const e of Object.keys(n))if(e.toLowerCase()===r)return n[e]}function deleteHeaderFromWriteHeadArgs(e,t){const n=findWriteHeadHeaders(e);if(!n)return;const r=t.toLowerCase();if(Array.isArray(n))if(n.length&&Array.isArray(n[0]))for(let e=n.length-1;e>=0;e--)String(n[e][0]).toLowerCase()===r&&n.splice(e,1);else for(let e=n.length-2;e>=0;e-=2)String(n[e]).toLowerCase()===r&&n.splice(e,2);else for(const e of Object.keys(n))e.toLowerCase()===r&&delete n[e]}export function interceptResponse(e,t,n){const r=n?.preventCaching??!1,o=n?.streamingIdleMs??50,f=e.write.bind(e),i=e.end.bind(e),u=e.writeHead.bind(e),c=[];let l=0,d=!1,s=null,a=null;function g(){s&&(clearTimeout(s),s=null),e.write=f,e.end=i;for(const e of c)f(e);c.length=0}function H(e,t){null===a&&null!=e&&(a=String(e).includes("text/html"),a&&t&&(a=!1),!1===a&&g())}function y(){H(e.getHeader("content-type"),e.getHeader("content-encoding"))}e.writeHead=function(t,...n){return H(readHeaderFromWriteHeadArgs(n,"content-type")??e.getHeader("content-type"),readHeaderFromWriteHeadArgs(n,"content-encoding")??e.getHeader("content-encoding")),a&&(r&&(e.setHeader("Cache-Control","no-store"),e.removeHeader("ETag"),e.removeHeader("Last-Modified"),deleteHeaderFromWriteHeadArgs(n,"cache-control"),deleteHeaderFromWriteHeadArgs(n,"etag"),deleteHeaderFromWriteHeadArgs(n,"last-modified")),e.removeHeader("Content-Length"),deleteHeaderFromWriteHeadArgs(n,"content-length")),u(t,...n)},e.write=function(e,t,n){if(y(),!1===a)return f(e,t,n);if(null!=e){const r=Buffer.isBuffer(e)?e:Buffer.from(e,"string"==typeof t?t:"utf8");if(l+=r.length,c.push(r),l>2e6)return g(),"function"==typeof t&&t(null),"function"==typeof n&&n(null),!0;s||d||(s=setTimeout(()=>{d||g()},o),"function"==typeof s.unref&&s.unref())}return"function"==typeof t&&t(null),"function"==typeof n&&n(null),!0},e.end=function(n,r,o){if(d)return e;if(d=!0,s&&(clearTimeout(s),s=null),y(),!1===a)return i(n,r,o);if(null!=n){const e=Buffer.isBuffer(n)?n:Buffer.from(n,"string"==typeof r?r:"utf8");l+=e.length,c.push(e)}if(l>2e6){g();const e="function"==typeof r?r:o;return e?i(e):i()}const u=Buffer.concat(c),H=e.getHeader("content-type");let p;try{p=t(u,H)}catch{p=null}if(null!=p){const t=Buffer.isBuffer(p)?p:Buffer.from(p,"utf8");e.headersSent||(e.setHeader("Content-Length",t.length),e.removeHeader("Content-Encoding")),f(t)}else u.length>0&&f(u);const m="function"==typeof r?r:o;return m?i(m):i(),e}}
@@ -1 +1 @@
1
- import{sha256Hex}from"../../util";import{isBot}from"../utils/is-bot";import{extractClientIp}from"../utils/client-ip";import{parseCookie}from"../utils/cookies";import{extractDeviceId}from"../utils/device-id";import{isSecureRequest}from"../utils/secure";import{isSentinelUserId}from"../utils/sentinel-user-id";import{sendJson}from"../utils/http-helpers";export function handleSubmitFingerprint(e){return async(i,t)=>{try{const s=i.body??{},n={full_hash:s.hash??"",fingerprint_id:s.stable_hash??"",timestamp:s.collected_at??(new Date).toISOString(),isIncognito:s.is_incognito??!1,components:s.components??{},version:s.version??"inline-1.0.0",..."remote"===s.source||"bundled"===s.source?{source:s.source}:{}};let o,r,d;try{const t=e.resolveUserId?e.resolveUserId(i):void 0;t&&!isSentinelUserId(t)&&(o=t)}catch{}if(!o){const e="string"==typeof s.user_id?s.user_id:void 0;e&&!isSentinelUserId(e)&&(o=e)}if(!o){const e=parseCookie(i,"__unshared_uid");e&&!isSentinelUserId(e)&&(o=e)}try{r=e.resolveEmailAddress?e.resolveEmailAddress(i):void 0}catch{}r=r??parseCookie(i,"__unshared_email")??s.email??void 0;try{d=e.resolveSessionId?e.resolveSessionId(i):void 0}catch{}d=d??s.session_id??parseCookie(i,"__unshared_sid");const a=extractClientIp(i),c=i.headers["user-agent"]??"";if(!e.disableBotFilter&&isBot(c))return void sendJson(t,200,{success:!0});const p=(n.fingerprint_id&&n.fingerprint_id.length>0?n.fingerprint_id:void 0)??extractDeviceId(i,e.resolveDeviceId),u=n.fingerprint_id||void 0,l=n.full_hash||void 0,m=isSecureRequest(i)?"; Secure":"",_=[];if(l&&!parseCookie(i,"__unshared_fingerprint_id")&&_.push(`__unshared_fingerprint_id=${encodeURIComponent(l)}; HttpOnly; Path=/; SameSite=Lax${m}`),u){const e=parseCookie(i,"__unshared_fp_id");e&&e===u||_.push(`__unshared_fp_id=${encodeURIComponent(u)}; Path=/; SameSite=Lax; Max-Age=31536000${m}`)}if(r&&!parseCookie(i,"__unshared_email")&&_.push(`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${m}`),_.length>0){const e=t.getHeader("Set-Cookie");if(e){const i=Array.isArray(e)?[...e]:[String(e)];i.push(..._),t.setHeader("Set-Cookie",i)}else t.setHeader("Set-Cookie",_)}let f;if("string"==typeof s.event_type&&s.event_type)f=s.event_type;else{const e=i.headers.referer??i.headers.referrer;let t="unknown";if("string"==typeof e&&e.length>0)try{const i=new URL(e);t=(i.pathname||"/")+(i.search||"")}catch{}f=t}const h=i.headers["x-idempotency-key"],v="string"==typeof h&&h.length>0?h:void 0,g=Date.now(),y=v?`${v}|${g}`:u&&o?`${sha256Hex(`${u}|${o}|${f}`)}|${g}`:void 0;if(o&&e.client.submitFingerprintEvent(n,{userId:o,emailAddress:r,sessionHash:d,eventType:f,ipAddress:a,userAgent:c,idempotencyKey:y}).catch(i=>{e.onError&&e.onError(i,{operation:"submitFingerprintEvent",userId:o,emailAddress:r})}),o&&r&&!e.rateLimitBackoff.isPaused()&&!e.dispatchDedupe.wasRecentlyDispatched(o,f))try{const i=await e.client.processUserEvent({eventType:f,userId:o,emailAddress:r,ipAddress:a,deviceId:p,fingerprintId:u,sessionHash:d??"unknown",userAgent:c});i.success&&i.data?.analysis&&e.verdictCache.update(o,{isFlagged:i.data.analysis.is_user_flagged}),!i.success&&i.error?.retryAfter&&e.rateLimitBackoff.pause(1e3*i.error.retryAfter)}catch(i){e.onError&&e.onError(i,{operation:"processUserEvent",userId:o,emailAddress:r})}sendJson(t,200,{success:!0})}catch(i){e.onError&&e.onError(i,{operation:"submitFingerprintEvent"}),sendJson(t,200,{success:!0})}}}
1
+ import{sha256Hex}from"../../util";import{isBot}from"../utils/is-bot";import{extractClientIp}from"../utils/client-ip";import{parseCookie}from"../utils/cookies";import{extractDeviceId}from"../utils/device-id";import{isSecureRequest}from"../utils/secure";import{isSentinelUserId}from"../utils/sentinel-user-id";import{sendJson}from"../utils/http-helpers";import{runProcessUserEvent,SUBMIT_FP_TIMEOUT_MS}from"../dispatch-processing";export function handleSubmitFingerprint(e){return async(i,t)=>{try{const s=i.body??{},n={full_hash:s.hash??"",fingerprint_id:s.stable_hash??"",timestamp:s.collected_at??(new Date).toISOString(),isIncognito:s.is_incognito??!1,components:s.components??{},version:s.version??"inline-1.0.0",..."remote"===s.source||"bundled"===s.source?{source:s.source}:{}};let o,r,d;try{const t=e.resolveUserId?e.resolveUserId(i):void 0;t&&!isSentinelUserId(t)&&(o=t)}catch{}if(!o){const e="string"==typeof s.user_id?s.user_id:void 0;e&&!isSentinelUserId(e)&&(o=e)}if(!o){const e=parseCookie(i,"__unshared_uid");e&&!isSentinelUserId(e)&&(o=e)}try{r=e.resolveEmailAddress?e.resolveEmailAddress(i):void 0}catch{}r=r??parseCookie(i,"__unshared_email")??s.email??void 0;try{d=e.resolveSessionId?e.resolveSessionId(i):void 0}catch{}d=d??s.session_id??parseCookie(i,"__unshared_sid");const a=extractClientIp(i),c=i.headers["user-agent"]??"";if(!e.disableBotFilter&&isBot(c))return void sendJson(t,200,{success:!0});const p=(n.fingerprint_id&&n.fingerprint_id.length>0?n.fingerprint_id:void 0)??extractDeviceId(i,e.resolveDeviceId),u=n.fingerprint_id||void 0,_=n.full_hash||void 0,m=isSecureRequest(i)?"; Secure":"",l=[];if(_&&!parseCookie(i,"__unshared_fingerprint_id")&&l.push(`__unshared_fingerprint_id=${encodeURIComponent(_)}; HttpOnly; Path=/; SameSite=Lax${m}`),u){const e=parseCookie(i,"__unshared_fp_id");e&&e===u||l.push(`__unshared_fp_id=${encodeURIComponent(u)}; Path=/; SameSite=Lax; Max-Age=31536000${m}`)}if(r&&!parseCookie(i,"__unshared_email")&&l.push(`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${m}`),l.length>0){const e=t.getHeader("Set-Cookie");if(e){const i=Array.isArray(e)?[...e]:[String(e)];i.push(...l),t.setHeader("Set-Cookie",i)}else t.setHeader("Set-Cookie",l)}let f;if("string"==typeof s.event_type&&s.event_type)f=s.event_type;else{const e=i.headers.referer??i.headers.referrer;let t="unknown";if("string"==typeof e&&e.length>0)try{const i=new URL(e);t=(i.pathname||"/")+(i.search||"")}catch{}f=t}const h=i.headers["x-idempotency-key"],v="string"==typeof h&&h.length>0?h:void 0,S=Date.now(),I=v?`${v}|${S}`:u&&o?`${sha256Hex(`${u}|${o}|${f}`)}|${S}`:void 0;o&&e.client.submitFingerprintEvent(n,{userId:o,emailAddress:r,sessionHash:d,eventType:f,ipAddress:a,userAgent:c,idempotencyKey:I}).catch(i=>{e.onError&&e.onError(i,{operation:"submitFingerprintEvent",userId:o,emailAddress:r})}),o&&r&&!e.rateLimitBackoff.isPaused()&&!e.dispatchDedupe.wasRecentlyDispatched(o,f)&&await runProcessUserEvent({client:e.client,cache:e.verdictCache,backoff:e.rateLimitBackoff,onError:e.onError},{eventType:f,userId:o,emailAddress:r,ipAddress:a,deviceId:p,fingerprintId:u,sessionHash:d??"unknown",userAgent:c},SUBMIT_FP_TIMEOUT_MS),sendJson(t,200,{success:!0})}catch(i){e.onError&&e.onError(i,{operation:"submitFingerprintEvent"}),sendJson(t,200,{success:!0})}}}
@@ -1,5 +1,9 @@
1
1
  /**
2
2
  * Returns true if the request path looks like a static asset and should be
3
3
  * skipped by the middleware (no fingerprint injection, no verdict checks).
4
+ *
5
+ * Content-bearing extensions (pdf/txt/xml/media) are only skipped when no
6
+ * enforcement mode is active — otherwise a flagged user could pull protected
7
+ * downloads/media straight through a `blockFlagged`/`flaggedMode` gate.
4
8
  */
5
- export declare function shouldSkipPath(path: string, customSkipPaths?: string[]): boolean;
9
+ export declare function shouldSkipPath(path: string, customSkipPaths?: string[], enforcementActive?: boolean): boolean;
@@ -1 +1 @@
1
- const STATIC_EXTENSIONS=new Set([".js",".mjs",".cjs",".css",".map",".png",".jpg",".jpeg",".gif",".svg",".ico",".webp",".avif",".woff",".woff2",".ttf",".otf",".eot",".mp3",".mp4",".webm",".ogg",".wasm",".xml",".txt",".pdf"]),STATIC_PATH_PREFIXES=["/static/","/assets/","/public/","/_next/","/__vite/","/favicon"];export function shouldSkipPath(t,f){if(f)for(const o of f)if(t.startsWith(o))return!0;const o=t.lastIndexOf(".");if(-1!==o){const f=t.slice(o).toLowerCase().split("?")[0];if(STATIC_EXTENSIONS.has(f))return!0}for(const f of STATIC_PATH_PREFIXES)if(t.startsWith(f))return!0;return!1}
1
+ const STATIC_ASSET_EXTENSIONS=new Set([".js",".mjs",".cjs",".css",".map",".png",".jpg",".jpeg",".gif",".svg",".ico",".webp",".avif",".woff",".woff2",".ttf",".otf",".eot",".wasm"]),CONTENT_EXTENSIONS=new Set([".mp3",".mp4",".webm",".ogg",".xml",".txt",".pdf"]),STATIC_PATH_PREFIXES=["/static/","/assets/","/public/","/_next/","/__vite/","/favicon"];export function shouldSkipPath(t,f,S=!1){if(f)for(const S of f)if(t.startsWith(S))return!0;const T=t.lastIndexOf(".");if(-1!==T){const f=t.slice(T).toLowerCase().split("?")[0];if(STATIC_ASSET_EXTENSIONS.has(f))return!0;if(!S&&CONTENT_EXTENSIONS.has(f))return!0}for(const f of STATIC_PATH_PREFIXES)if(t.startsWith(f))return!0;return!1}
@@ -13,6 +13,7 @@ export declare class VerdictCache {
13
13
  private readonly _maxSize;
14
14
  private _sweepTimer;
15
15
  constructor(defaultTTL?: number, maxSize?: number);
16
+ private _ensureSweepTimer;
16
17
  get(userId: string): Verdict | undefined;
17
18
  set(userId: string, verdict: Omit<Verdict, 'cachedAt' | 'ttl'>, ttl?: number): void;
18
19
  /**
@@ -42,6 +43,12 @@ export declare class VerdictCache {
42
43
  destroy(): void;
43
44
  /** Remove all entries that are past their TTL + a 2x grace period. */
44
45
  private _sweep;
45
- /** Evict the oldest entry by cachedAt to make room. */
46
+ /**
47
+ * Evict the oldest entry by last WRITE (set/update) — O(1). Map preserves
48
+ * insertion order, and both set() and update() re-append an existing key on
49
+ * write, so the first key in iteration order is the least-recently-written.
50
+ * Reads (get/isStale/isRefreshing) don't refresh recency — this is LRU-by-write,
51
+ * not LRU-by-read.
52
+ */
46
53
  private _evictOldest;
47
54
  }
@@ -1 +1 @@
1
- const DEFAULT_TTL_MS=6e4,DEFAULT_MAX_SIZE=1e4,SWEEP_INTERVAL_MS=3e5;export class VerdictCache{constructor(t=6e4,s=1e4){this.t=new Map,this.i=new Set,this.h=null,this.l=t,this.o=s,this.h=setInterval(()=>this.u(),3e5),this.h&&"function"==typeof this.h.unref&&this.h.unref()}get(t){return this.t.get(t)}set(t,s,i){!this.t.has(t)&&this.t.size>=this.o&&this._(),this.t.set(t,{...s,cachedAt:Date.now(),ttl:i??this.l})}update(t,s){const i=this.t.get(t);i?(void 0!==s.isFlagged&&(i.isFlagged=s.isFlagged),void 0!==s.isVerified&&(i.isVerified=s.isVerified),i.cachedAt=Date.now()):(this.t.size>=this.o&&this._(),this.t.set(t,{isFlagged:s.isFlagged??!1,isVerified:s.isVerified??!1,emailAddress:"",sessionId:"",cachedAt:Date.now(),ttl:this.l}))}delete(t){this.t.delete(t),this.i.delete(t)}isStale(t){const s=this.t.get(t);return!!s&&Date.now()-s.cachedAt>s.ttl}isRefreshing(t){return this.i.has(t)}markRefreshing(t){this.i.add(t)}clearRefreshing(t){this.i.delete(t)}get size(){return this.t.size}clear(){this.t.clear(),this.i.clear()}destroy(){this.h&&(clearInterval(this.h),this.h=null),this.clear()}u(){const t=Date.now();for(const[s,i]of this.t)t-i.cachedAt>2*i.ttl&&(this.t.delete(s),this.i.delete(s))}_(){let t=null,s=1/0;for(const[i,e]of this.t)e.cachedAt<s&&(s=e.cachedAt,t=i);t&&(this.t.delete(t),this.i.delete(t))}}
1
+ const DEFAULT_TTL_MS=6e4,DEFAULT_MAX_SIZE=1e4,SWEEP_INTERVAL_MS=3e5;export class VerdictCache{constructor(t=6e4,s=1e4){this.t=new Map,this.i=new Set,this.h=null,this.l=t,this.o=s}u(){if(!this.h)try{this.h=setInterval(()=>this._(),3e5),this.h&&"function"==typeof this.h.unref&&this.h.unref()}catch{this.h=null}}get(t){return this.t.get(t)}set(t,s,i){this.u(),!this.t.has(t)&&this.t.size>=this.o?this.A():this.t.has(t)&&this.t.delete(t),this.t.set(t,{...s,cachedAt:Date.now(),ttl:i??this.l})}update(t,s){this.u();const i=this.t.get(t);i?(void 0!==s.isFlagged&&(i.isFlagged=s.isFlagged),void 0!==s.isVerified&&(i.isVerified=s.isVerified),i.cachedAt=Date.now(),this.t.delete(t),this.t.set(t,i)):(this.t.size>=this.o&&this.A(),this.t.set(t,{isFlagged:s.isFlagged??!1,isVerified:s.isVerified??!1,emailAddress:"",sessionId:"",cachedAt:Date.now(),ttl:this.l}))}delete(t){this.t.delete(t),this.i.delete(t)}isStale(t){const s=this.t.get(t);return!!s&&Date.now()-s.cachedAt>s.ttl}isRefreshing(t){return this.i.has(t)}markRefreshing(t){this.i.add(t)}clearRefreshing(t){this.i.delete(t)}get size(){return this.t.size}clear(){this.t.clear(),this.i.clear()}destroy(){this.h&&(clearInterval(this.h),this.h=null),this.clear()}_(){const t=Date.now();for(const[s,i]of this.t)t-i.cachedAt>2*i.ttl&&(this.t.delete(s),this.i.delete(s))}A(){const t=this.t.keys().next().value;void 0!==t&&(this.t.delete(t),this.i.delete(t))}}
@@ -1,4 +1,6 @@
1
1
  import type { UnsharedClient } from '../client';
2
+ import { VerdictCache } from '../middleware/verdict-cache';
3
+ import type { Verdict } from '../middleware/verdict-cache';
2
4
  import type { WebMiddleware, WebProtectionConfig } from './types';
3
5
  /**
4
6
  * Web Standard equivalent of `unsharedBoundToUser` from src/middleware/index.ts.
@@ -11,7 +13,15 @@ import type { WebMiddleware, WebProtectionConfig } from './types';
11
13
  * handler calls `next(request)` to get the downstream Response, transforms
12
14
  * it (injects the fingerprint script into HTML), and returns a new Response.
13
15
  *
14
- * @example Next.js App Router (in middleware.ts)
16
+ * NOTE: In Next.js middleware, `next(request)` is typically `() => NextResponse.next()`,
17
+ * which is a passthrough with a null body and no Content-Type — there is no HTML to
18
+ * inject a script into. This middleware still resolves the verdict and enforces it
19
+ * (gate/overlay/403), but fingerprint COLLECTION must be done by the browser/React SDK
20
+ * mounted in your app, not by injection here. See the Next.js section of the
21
+ * integration guide.
22
+ *
23
+ * @example Next.js App Router (in middleware.ts) — verdict + enforcement only;
24
+ * mount the browser/React SDK client-side to collect fingerprints.
15
25
  * ```typescript
16
26
  * import { createWebProtectionMiddleware } from 'unshared-clientjs-sdk/web';
17
27
  *
@@ -26,3 +36,21 @@ import type { WebMiddleware, WebProtectionConfig } from './types';
26
36
  * ```
27
37
  */
28
38
  export declare function createWebProtectionMiddleware(client: UnsharedClient, config: WebProtectionConfig): WebMiddleware;
39
+ /** Mutable holder for the one-time passthrough-injection warning (see below). */
40
+ interface InjectWarnState {
41
+ emitted: boolean;
42
+ }
43
+ /**
44
+ * Read the response body, inject the fingerprint script into HTML responses,
45
+ * merge Set-Cookie headers, and return a new Response. For non-HTML responses,
46
+ * only the cookies and cache-control override are applied.
47
+ *
48
+ * `warnState` scopes the one-time "passthrough, nothing injected" warning to a
49
+ * single middleware instance; when omitted, the module-level default is used.
50
+ */
51
+ declare function injectIntoHtmlResponse(response: Response, fingerprintScript: string, extraCookies?: string[], warnState?: InjectWarnState): Promise<Response>;
52
+ declare function fetchAndCacheVerdict(client: UnsharedClient, cache: VerdictCache, userId: string, emailAddress: string, deviceId: string, fingerprintId: string | undefined, sessionId: string, timeoutMs?: number, reportFailOpen?: (reason: 'timeout' | 'http_error' | 'no_device_id', status?: number) => void): Promise<Verdict>;
53
+ export declare const __test_fetchAndCacheVerdict: typeof fetchAndCacheVerdict;
54
+ export declare const __test_injectIntoHtmlResponse: typeof injectIntoHtmlResponse;
55
+ export declare function __test_resetInjectWarning(): void;
56
+ export {};
@@ -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{maskEmail}from"../middleware/utils/mask-email";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=1500,FAIL_OPEN_TTL_MS=5e3;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{}},E=Date.now().toString(36),C=void 0!==S||y,A="gate"===S?"flaggedMode 'gate' (blockFlagged)":"overlay"===S?"flaggedMode 'overlay'":"autoInterstitial";if(C&&!l)throw new Error(`[Unshared] ${A} requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the interstitial).`);if(C&&"/__unshared"!==n)throw new Error(`[Unshared] ${A} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const k="gate"===S?generateGatePage(n,E):"",T=new VerdictCache(o),F=new RateLimitBackoff,x=new DispatchDedupe,U=generateFingerprintScript(n,E,{autoInterstitial:y,interstitialFlowType:v}),q=`${n}/fp.js`,D=`${n}/submit-fp`,O=`${n}/verify-trigger`,j=`${n}/verify`,P=`${n}/status`,b=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(!b)return{};const t=e.headers.get("origin")??"",s=b.includes("*");if(!s&&!b.includes(t))return{};const r={"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"};return s||(r["Access-Control-Allow-Credentials"]="true"),r}(t);if("OPTIONS"===t.method)return emptyResponse(204,n);if(("GET"===t.method||"HEAD"===t.method)&&o===q){if(!l)return jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},n);const e={...n,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600","Content-Length":String((new TextEncoder).encode(l).byteLength)};return"HEAD"===t.method?new Response(null,{status:200,headers:new Headers(e)}):bodyResponse(200,l,e)}if("POST"===t.method&&(o===D||o===O||o===j)){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===D?handleSubmitFp(t,i,{client:e,verdictCache:T,rateLimitBackoff:F,dispatchDedupe:x,resolveUserId:s,resolveEmailAddress:r,resolveSessionId:c,resolveDeviceId:u,disableBotFilter:h,onError:m},n):o===O?handleVerifyTriggerWeb(t,i,{client:e,verdictCache:T,resolveUserId:s,resolveEmailAddress:r,resolveDeviceId:u,onError:m},n):handleVerifyWeb(t,i,{client:e,verdictCache:T,resolveUserId:s,resolveEmailAddress:r,resolveDeviceId:u,onError:m},n)}if("GET"===t.method&&o===P){let i;try{i=s(t)}catch{}if(!i)return jsonResponse(200,{status:"anonymous"},n);const o=resolveEmail(t,r);let a=T.get(i);if((!a||T.isStale(i))&&o&&!F.isPaused()&&!T.isRefreshing(i)){T.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,T,i,o,d,r,n,_,(e,t)=>w({operation:"checkUser",reason:e,status:t,userId:i,emailAddress:o})),a=T.get(i)}catch(e){m&&m(e,{operation:"checkUser",userId:i,emailAddress:o}),w({operation:"checkUser",reason:"exception",userId:i,emailAddress:o})}finally{T.clearRefreshing(i)}}return a&&a.isFlagged&&!a.isVerified&&(void 0!==S||p)&&o?jsonResponse(200,{status:"flagged",email:maskEmail(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),E=extractDeviceIdFromRequest(t,u),C=parseCookieFromRequest(t,"__unshared_fingerprint_id")||void 0,A=t.headers.get("user-agent")??"",H=extractClientIpFromRequest(t),L=E??C;if(!h&&isBot(A))return i(t);let N=T.get(g);if(N)T.isStale(g)&&!T.isRefreshing(g)&&(T.markRefreshing(g),fetchAndCacheVerdict(e,T,g,R,L??"unknown",C,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(()=>T.clearRefreshing(g)));else try{N=await fetchAndCacheVerdict(e,T,g,R,L??"unknown",C,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 $=N.isFlagged&&!N.isVerified;if("gate"===S&&$)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&&$)return isHtmlNavigation(t.method,t.headers.get("accept")??void 0)?injectIntoHtmlResponse(await i(t),U,I):jsonResponse(403,flaggedResponse(R));if(N.isFlagged&&!N.isVerified&&p)try{const e=await p({userId:g,emailAddress:R,verdict:N,request:t});if(e)return injectIntoHtmlResponse(e,U,I)}catch(e){m&&m(e,{operation:"checkUser",userId:g,emailAddress:R})}return N.isFlagged||"unknown"===y||!L||F.isPaused()||dispatchUserEvent(e,T,F,x,{userId:g,emailAddress:R,sessionId:y,deviceId:L,fingerprintId:C,userAgent:A,ipAddress:H,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=()=>{const e=t.get(s);return t.set(s,{isFlagged:e?.isFlagged??!1,isVerified:e?.isVerified??!1,emailAddress:r,sessionId:o},5e3),t.get(s)},u={};let l;n&&"unknown"!==n&&(u.deviceId=n),i&&(u.fingerprintId=i);const p=await Promise.race([e.checkUser(r,u),new Promise(e=>{l=setTimeout(()=>e(null),a)})]);return clearTimeout(l),p?p.failedOpen?(d?.(p.failedOpen.reason??"http_error",p.failedOpen.status),c()):(t.set(s,{isFlagged:p.data?.is_user_flagged??!1,isVerified:!1,emailAddress:r,sessionId:o}),t.get(s)):(d?.("timeout"),c())}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){let t;try{t=s.resolveUserId(e)}catch{}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)}}
1
+ import{sha256HexWeb}from"../webcrypto";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{maskEmail}from"../middleware/utils/mask-email";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{runProcessUserEvent,DISPATCH_TIMEOUT_MS,SUBMIT_FP_TIMEOUT_MS}from"../middleware/dispatch-processing";import{parseCookieFromRequest,extractClientIpFromRequest,extractDeviceIdFromRequest,extractDeviceIdFromRequestOrUnknown,isSecureWebRequest,jsonResponse,emptyResponse,bodyResponse,mergeResponseHeaders}from"./web-helpers";const CHECK_USER_TIMEOUT_MS=1500,FAIL_OPEN_TTL_MS=5e3;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,onFailOpen:f,disableBotFilter:h=!1,checkUserTimeoutMs:_=CHECK_USER_TIMEOUT_MS,blockFlagged:g=!1,flaggedMode:I,autoInterstitial:R=!1,interstitialFlowType:v="email_verification"}=t,S=I??(g?"gate":void 0),y=R||"overlay"===S,w=e=>{if(f)try{f(e)}catch{}},E=Date.now().toString(36),T=void 0!==S||y,C="gate"===S?"flaggedMode 'gate' (blockFlagged)":"overlay"===S?"flaggedMode 'overlay'":"autoInterstitial";if(T&&!l)throw new Error(`[Unshared] ${C} requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the interstitial).`);if(T&&"/__unshared"!==n)throw new Error(`[Unshared] ${C} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const A="gate"===S?generateGatePage(n,E):"",k=new VerdictCache(o),x=new Map,F=new RateLimitBackoff,U=new DispatchDedupe,j=generateFingerprintScript(n,E,{autoInterstitial:y,interstitialFlowType:v}),b={emitted:!1},q=`${n}/fp.js`,P=`${n}/submit-fp`,O=`${n}/verify-trigger`,D=`${n}/verify`,H=`${n}/status`,M=i?Array.isArray(i)?i:[i]:null;return async function(t,i,o){const f=e=>{o?.waitUntil&&o.waitUntil(e)};let g,I,R;try{const e=new URL(t.url);g=e.pathname,I=e.search}catch{return i(t)}if(g.startsWith(n+"/")){const n=function(e){if(!M)return{};const t=e.headers.get("origin")??"",r=M.includes("*");if(!r&&!M.includes(t))return{};const s={"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"};return r||(s["Access-Control-Allow-Credentials"]="true"),s}(t);if("OPTIONS"===t.method)return emptyResponse(204,n);if(("GET"===t.method||"HEAD"===t.method)&&g===q){if(!l)return jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},n);const e={...n,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600","Content-Length":String((new TextEncoder).encode(l).byteLength)};return"HEAD"===t.method?new Response(null,{status:200,headers:new Headers(e)}):bodyResponse(200,l,e)}if("POST"===t.method&&(g===P||g===O||g===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 g===P?handleSubmitFp(t,i,{client:e,verdictCache:k,rateLimitBackoff:F,dispatchDedupe:U,resolveUserId:r,resolveEmailAddress:s,resolveSessionId:c,resolveDeviceId:u,disableBotFilter:h,onError:m,registerBackground:f},n):g===O?handleVerifyTriggerWeb(t,i,{client:e,verdictCache:k,resolveUserId:r,resolveEmailAddress:s,resolveDeviceId:u,onError:m},n):handleVerifyWeb(t,i,{client:e,verdictCache:k,resolveUserId:r,resolveEmailAddress:s,resolveDeviceId:u,onError:m},n)}if("GET"===t.method&&g===H){let i;try{i=r(t)}catch{}if(!i)return jsonResponse(200,{status:"anonymous"},n);const o=resolveEmail(t,s);let a=k.get(i);if((!a||k.isStale(i))&&o&&!F.isPaused()&&!k.isRefreshing(i)){k.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,k,i,o,d,s,n,_,(e,t)=>w({operation:"checkUser",reason:e,status:t,userId:i,emailAddress:o})),a=k.get(i)}catch(e){m&&m(e,{operation:"checkUser",userId:i,emailAddress:o}),w({operation:"checkUser",reason:"exception",userId:i,emailAddress:o})}finally{k.clearRefreshing(i)}}return a&&a.isFlagged&&!a.isVerified&&(void 0!==S||p)&&o?jsonResponse(200,{status:"flagged",email:maskEmail(o)},n):jsonResponse(200,{status:"ok"},n)}return jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},n)}if(shouldSkipPath(g,a,void 0!==S))return i(t);if(!shouldIncludePath(g,d))return injectIntoHtmlResponse(await i(t),j,void 0,b);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),j,r,b)}const v=resolveEmail(t,s),y=[],E=isSecureWebRequest(t)?"; Secure":"";if(y.push(`__unshared_uid=${encodeURIComponent(R)}; Path=/; SameSite=Lax${E}`),y.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${E}`),v&&y.push(`__unshared_email=${encodeURIComponent(v)}; HttpOnly; Path=/; SameSite=Lax${E}`),!v)return injectIntoHtmlResponse(await i(t),j,y,b);const T=extractSessionIdFromRequest(t,c),C=extractDeviceIdFromRequest(t,u),N=parseCookieFromRequest(t,"__unshared_fingerprint_id")||void 0,L=t.headers.get("user-agent")??"",$=extractClientIpFromRequest(t),V=C??N;if(!h&&isBot(L))return i(t);let B=k.get(R);if(B)k.isStale(R)&&!k.isRefreshing(R)&&(k.markRefreshing(R),f(fetchAndCacheVerdict(e,k,R,v,V??"unknown",N,T,_,(e,t)=>w({operation:"checkUser",reason:e,status:t,userId:R,emailAddress:v})).catch(()=>w({operation:"checkUser",reason:"exception",userId:R,emailAddress:v})).finally(()=>k.clearRefreshing(R))));else try{let t=x.get(R);t||(t=fetchAndCacheVerdict(e,k,R,v,V??"unknown",N,T,_,(e,t)=>w({operation:"checkUser",reason:e,status:t,userId:R,emailAddress:v})).finally(()=>x.delete(R)),x.set(R,t)),B=await t}catch{return w({operation:"checkUser",reason:"exception",userId:R,emailAddress:v}),injectIntoHtmlResponse(await i(t),j,y,b)}const W=B.isFlagged&&!B.isVerified;if("gate"===S&&W)return isHtmlNavigation(t.method,t.headers.get("accept")??void 0)?bodyResponse(200,A,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}):jsonResponse(403,flaggedResponse(v));if("overlay"===S&&W)return isHtmlNavigation(t.method,t.headers.get("accept")??void 0)?injectIntoHtmlResponse(await i(t),j,y,b):jsonResponse(403,flaggedResponse(v));if(B.isFlagged&&!B.isVerified&&p)try{const e=await p({userId:R,emailAddress:v,verdict:B,request:t});if(e)return injectIntoHtmlResponse(e,j,y,b)}catch(e){m&&m(e,{operation:"checkUser",userId:R,emailAddress:v})}return B.isFlagged||"unknown"===T||!V||F.isPaused()||f(dispatchUserEvent(e,k,F,U,{userId:R,emailAddress:v,sessionId:T,deviceId:V,fingerprintId:N,userAgent:L,ipAddress:$,eventType:g+I},m)),injectIntoHtmlResponse(await i(t),j,y,b)}}const defaultInjectWarnState={emitted:!1};function passthroughResponse(e,t,r){if(!t||0===t.length)return e;const s=mergeResponseHeaders(e.headers,void 0,t);return new Response(r,{status:e.status,statusText:e.statusText,headers:s})}async function injectIntoHtmlResponse(e,t,r,s=defaultInjectWarnState){const n=e.headers.get("content-type");if(204===e.status||304===e.status)return passthroughResponse(e,r,null);if(null===e.body)return n||s.emitted||(s.emitted=!0,console.warn("[Unshared] The response returned by your handler has no body (e.g. NextResponse.next()), so fingerprint injection did not run. In Next.js middleware, fingerprints must be collected by the browser/React SDK, while this middleware handles verdict + enforcement. See the Next.js section of the integration guide.")),passthroughResponse(e,r,null);if(!isHtmlContentType(n??void 0))return passthroughResponse(e,r,e.body);const i=await e.text(),o=i.lastIndexOf("</body>"),a=-1===o?i+t:i.slice(0,o)+t+i.slice(o),d=mergeResponseHeaders(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(a).length)},r);return d.delete("ETag"),d.delete("Last-Modified"),d.delete("Content-Encoding"),new Response(a,{status:e.status,statusText:e.statusText,headers:d})}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){return s.mark(n.userId,n.eventType),runProcessUserEvent({client:e,cache:t,backoff:r,onError:i},{eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,sessionHash:n.sessionId,userAgent:n.userAgent},DISPATCH_TIMEOUT_MS)}async function fetchAndCacheVerdict(e,t,r,s,n,i,o,a=CHECK_USER_TIMEOUT_MS,d){const c=()=>{const e=t.get(r);return t.set(r,{isFlagged:e?.isFlagged??!1,isVerified:e?.isVerified??!1,emailAddress:s,sessionId:o},5e3),t.get(r)},u={timeoutMs:a,maxRetries:0};let l;n&&"unknown"!==n&&(u.deviceId=n),i&&(u.fingerprintId=i);const p=await Promise.race([e.checkUser(s,u),new Promise(e=>{l=setTimeout(()=>e(null),a)})]);return clearTimeout(l),p?p.failedOpen?(d?.(p.failedOpen.reason??"http_error",p.failedOpen.status),c()):(t.set(r,{isFlagged:p.data?.is_user_flagged??!1,isVerified:!1,emailAddress:s,sessionId:o}),t.get(r)):(d?.("timeout"),c())}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();let I;if(_?I=`${_}|${g}`:l&&i&&(I=`${await sha256HexWeb(`${l}|${i}|${h}`)}|${g}`),i){const e=r.client.submitFingerprintEvent(n,{userId:i,emailAddress:o,sessionHash:a,eventType:h,ipAddress:d,userAgent:c,idempotencyKey:I}).catch(e=>{r.onError&&r.onError(e,{operation:"submitFingerprintEvent",userId:i,emailAddress:o})});r.registerBackground?.(e)}if(i&&o&&!r.rateLimitBackoff.isPaused()&&!r.dispatchDedupe.wasRecentlyDispatched(i,h)){const e=runProcessUserEvent({client:r.client,cache:r.verdictCache,backoff:r.rateLimitBackoff,onError:r.onError},{eventType:h,userId:i,emailAddress:o,ipAddress:d,deviceId:u,fingerprintId:l,sessionHash:a??"unknown",userAgent:c},SUBMIT_FP_TIMEOUT_MS);r.registerBackground?.(e)}const R={...s,"Content-Type":"application/json"},v=new Response(JSON.stringify({success:!0}),{status:200,headers:R});for(const e of f)v.headers.append("Set-Cookie",e);return v}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){let t;try{t=r.resolveUserId(e)}catch{}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)}}export const __test_fetchAndCacheVerdict=fetchAndCacheVerdict;export const __test_injectIntoHtmlResponse=injectIntoHtmlResponse;export function __test_resetInjectWarning(){defaultInjectWarnState.emitted=!1}
@@ -12,7 +12,17 @@ import type { Verdict } from '../middleware/verdict-cache';
12
12
  /** A downstream handler that returns a Response. */
13
13
  export type WebHandler = (request: Request) => Response | Promise<Response>;
14
14
  /** Middleware that wraps a downstream handler, optionally intercepting the response. */
15
- export type WebMiddleware = (request: Request, next: WebHandler) => Response | Promise<Response>;
15
+ export type WebMiddleware = (request: Request, next: WebHandler,
16
+ /**
17
+ * Platform background-work hook (e.g. Cloudflare Workers / Vercel Edge
18
+ * `ExecutionContext.waitUntil`). When provided, background promises (SWR
19
+ * verdict refresh, event dispatch) are registered through it so the
20
+ * runtime keeps them alive past the response — otherwise they are killed
21
+ * at response end on edge runtimes.
22
+ */
23
+ ctx?: {
24
+ waitUntil?: (p: Promise<unknown>) => void;
25
+ }) => Response | Promise<Response>;
16
26
  export interface WebSubmitOptions {
17
27
  /** Override userId extractor. Falls back to body.user_id. */
18
28
  userIdExtractor?: (req: Request) => string | undefined;
@@ -1 +1 @@
1
- export function parseCookieFromRequest(e,t){const o=e.headers.get("cookie");if(!o)return;const n=o.match(new RegExp(`(?:^|; )${t}=([^;]*)`));return n?decodeURIComponent(n[1]):void 0}export function extractClientIpFromRequest(e){const t=e.headers.get("cf-connecting-ip");if(t)return t;const o=e.headers.get("x-real-ip");if(o)return o;const n=e.headers.get("x-forwarded-for");if(n){const e=n.split(",")[0]?.trim();if(e)return e}return""}export function extractDeviceIdFromRequest(e,t){if(t)try{const o=t(e);if(o)return o}catch{}const o=e.headers.get("x-device-id");if(o)return o;return parseCookieFromRequest(e,"__unshared_fp_id")||void 0}export function extractDeviceIdFromRequestOrUnknown(e,t){return extractDeviceIdFromRequest(e,t)??"unknown"}export function isSecureWebRequest(e){if("https"===e.headers.get("x-forwarded-proto"))return!0;try{return"https:"===new URL(e.url).protocol}catch{return!1}}export function jsonResponse(e,t,o){const n=new Headers(o);return n.set("Content-Type","application/json"),new Response(JSON.stringify(t),{status:e,headers:n})}export function emptyResponse(e,t){return new Response(null,{status:e,headers:new Headers(t)})}export function bodyResponse(e,t,o){return new Response(t,{status:e,headers:new Headers(o)})}export function appendCookieToHeaders(e,t){e.append("Set-Cookie",t)}export function mergeResponseHeaders(e,t,o){const n=new Headers;e.forEach((e,t)=>{"set-cookie"!==t.toLowerCase()&&n.set(t,e)});const r="function"==typeof e.getSetCookie?e.getSetCookie():[];for(const e of r)n.append("Set-Cookie",e);if(o)for(const e of o)n.append("Set-Cookie",e);if(t)for(const[e,o]of Object.entries(t))n.set(e,o);return n}
1
+ export function parseCookieFromRequest(e,t){const n=e.headers.get("cookie");if(!n)return;const o=n.match(new RegExp(`(?:^|; )${t}=([^;]*)`));if(o)try{return decodeURIComponent(o[1])}catch{return o[1]}}export function extractClientIpFromRequest(e){const t=e.headers.get("cf-connecting-ip");if(t)return t;const n=e.headers.get("x-real-ip");if(n)return n;const o=e.headers.get("x-forwarded-for");if(o){const e=o.split(",")[0]?.trim();if(e)return e}return""}export function extractDeviceIdFromRequest(e,t){if(t)try{const n=t(e);if(n)return n}catch{}const n=e.headers.get("x-device-id");if(n)return n;return parseCookieFromRequest(e,"__unshared_fp_id")||void 0}export function extractDeviceIdFromRequestOrUnknown(e,t){return extractDeviceIdFromRequest(e,t)??"unknown"}export function isSecureWebRequest(e){if("https"===e.headers.get("x-forwarded-proto"))return!0;try{return"https:"===new URL(e.url).protocol}catch{return!1}}export function jsonResponse(e,t,n){const o=new Headers(n);return o.set("Content-Type","application/json"),new Response(JSON.stringify(t),{status:e,headers:o})}export function emptyResponse(e,t){return new Response(null,{status:e,headers:new Headers(t)})}export function bodyResponse(e,t,n){return new Response(t,{status:e,headers:new Headers(n)})}export function appendCookieToHeaders(e,t){e.append("Set-Cookie",t)}export function mergeResponseHeaders(e,t,n){const o=new Headers;e.forEach((e,t)=>{"set-cookie"!==t.toLowerCase()&&o.set(t,e)});const r="function"==typeof e.getSetCookie?e.getSetCookie():[];for(const e of r)o.append("Set-Cookie",e);if(n)for(const e of n)o.append("Set-Cookie",e);if(t)for(const[e,n]of Object.entries(t))o.set(e,n);return o}
@@ -0,0 +1,8 @@
1
+ export declare function sha256Bytes(input: string): Promise<Uint8Array>;
2
+ export declare function sha256HexWeb(input: string): Promise<string>;
3
+ /**
4
+ * Import raw key bytes as an AES-GCM CryptoKey, once, so callers can reuse it
5
+ * across many encryptions instead of paying importKey's cost per call.
6
+ */
7
+ export declare function importAesGcmKey(keyBytes: Uint8Array): Promise<CryptoKey>;
8
+ export declare function encryptDataWeb(plaintext: string, key: CryptoKey): Promise<string>;
@@ -0,0 +1 @@
1
+ const subtle=globalThis.crypto.subtle;function toBase64(t){let e="";for(let n=0;n<t.length;n++)e+=String.fromCharCode(t[n]);return btoa(e)}export async function sha256Bytes(t){const e=await subtle.digest("SHA-256",(new TextEncoder).encode(t));return new Uint8Array(e)}export async function sha256HexWeb(t){const e=await sha256Bytes(t);return Array.from(e).map(t=>t.toString(16).padStart(2,"0")).join("")}export async function importAesGcmKey(t){return subtle.importKey("raw",new Uint8Array(t),{name:"AES-GCM"},!1,["encrypt"])}export async function encryptDataWeb(t,e){const n=globalThis.crypto.getRandomValues(new Uint8Array(12)),r=new Uint8Array(await subtle.encrypt({name:"AES-GCM",iv:n,tagLength:128},e,(new TextEncoder).encode(t))),a=r.slice(0,r.length-16),o=r.slice(r.length-16);return`${toBase64(n)}:${toBase64(o)}:${toBase64(a)}`}
@@ -1,6 +1,7 @@
1
1
  export declare class DispatchDedupe {
2
2
  private readonly _entries;
3
3
  private readonly _ttlMs;
4
+ private _lastSweepAt;
4
5
  constructor(ttlMs?: number);
5
6
  private _key;
6
7
  mark(userId: string, eventType: string): void;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.DispatchDedupe=void 0;const DEFAULT_TTL_MS=1e4,SWEEP_THRESHOLD=1e3;class DispatchDedupe{constructor(t=1e4){this.i=new Map,this.h=t}o(t,s){return`${t}|${s}`}mark(t,s){this.i.set(this.o(t,s),Date.now()),this.i.size>1e3&&this.p()}wasRecentlyDispatched(t,s){const e=this.o(t,s),i=this.i.get(e);return!(void 0===i||Date.now()-i>this.h&&(this.i.delete(e),1))}clear(){this.i.clear()}get size(){return this.i.size}p(){const t=Date.now()-this.h;for(const[s,e]of this.i)e<t&&this.i.delete(s)}}exports.DispatchDedupe=DispatchDedupe;
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.DispatchDedupe=void 0;const DEFAULT_TTL_MS=1e4,SWEEP_THRESHOLD=1e3,SWEEP_MIN_INTERVAL_MS=1e3;class DispatchDedupe{constructor(t=1e4){this.i=new Map,this.h=0,this.o=t}D(t,s){return`${t}|${s}`}mark(t,s){if(this.i.set(this.D(t,s),Date.now()),this.i.size>1e3){const t=Date.now();t-this.h>=1e3&&(this.h=t,this.p())}}wasRecentlyDispatched(t,s){const e=this.D(t,s),i=this.i.get(e);return!(void 0===i||Date.now()-i>this.o&&(this.i.delete(e),1))}clear(){this.i.clear()}get size(){return this.i.size}p(){const t=Date.now()-this.o;for(const[s,e]of this.i)e<t&&this.i.delete(s)}}exports.DispatchDedupe=DispatchDedupe;
@@ -0,0 +1,22 @@
1
+ import type { UnsharedClient, ProcessUserEventParams } from '../client';
2
+ import type { VerdictCache } from './verdict-cache';
3
+ import type { RateLimitBackoff } from './rate-limit-backoff';
4
+ /** Single policy point for background processUserEvent budgets. */
5
+ export declare const DISPATCH_TIMEOUT_MS = 2000;
6
+ export declare const SUBMIT_FP_TIMEOUT_MS = 3000;
7
+ export interface DispatchIo {
8
+ client: UnsharedClient;
9
+ cache: VerdictCache;
10
+ backoff: RateLimitBackoff;
11
+ onError?: (err: unknown, ctx: {
12
+ operation: 'processUserEvent';
13
+ userId: string;
14
+ emailAddress: string;
15
+ }) => void;
16
+ }
17
+ /**
18
+ * Run processUserEvent with the standard bounded budget and backoff wiring.
19
+ * Never rejects. All four dispatch sites (classic/web x dispatch/submit-fp)
20
+ * MUST go through this helper so the cap and backoff can't drift per-site.
21
+ */
22
+ export declare function runProcessUserEvent(io: DispatchIo, params: ProcessUserEventParams, timeoutMs: number): Promise<void>;
@@ -0,0 +1 @@
1
+ "use strict";async function runProcessUserEvent(e,s,t){try{const r=await e.client.processUserEvent(s,{timeoutMs:t,maxRetries:0});r.success&&r.data?.analysis&&e.cache.update(s.userId,{isFlagged:r.data.analysis.is_user_flagged}),r.success?e.backoff.noteSuccess():r.error?.retryAfter?e.backoff.pause(1e3*r.error.retryAfter):e.backoff.noteFailure()}catch(t){e.backoff.noteFailure();try{e.onError&&e.onError(t,{operation:"processUserEvent",userId:s.userId,emailAddress:s.emailAddress})}catch{}}}Object.defineProperty(exports,"t",{value:!0}),exports.SUBMIT_FP_TIMEOUT_MS=exports.DISPATCH_TIMEOUT_MS=void 0,exports.runProcessUserEvent=runProcessUserEvent,exports.DISPATCH_TIMEOUT_MS=2e3,exports.SUBMIT_FP_TIMEOUT_MS=3e3;
@@ -20,6 +20,18 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
20
20
  corsOrigins?: string | string[];
21
21
  /** Verdict cache TTL in ms. @default 60000 */
22
22
  cacheTTL?: number;
23
+ /** Max number of entries in the verdict cache before oldest-inserted entries are evicted. @default 10000 */
24
+ maxCacheSize?: number;
25
+ /**
26
+ * Budget in ms from the first buffered HTML write until res.end() before the
27
+ * response is treated as streaming SSR and fingerprint injection is skipped
28
+ * (the response itself is never delayed or altered past this point).
29
+ * Raise this if your handlers flush partial HTML early and finish rendering
30
+ * later (e.g. res.write(head) → await db → res.end(rest)) and you see the
31
+ * injection being skipped.
32
+ * @default 50
33
+ */
34
+ streamingIdleMs?: number;
23
35
  /** Paths to skip entirely (static assets, health checks). */
24
36
  skipPaths?: string[];
25
37
  /** When set, only paths matching one of these prefixes get events dispatched and checkUser called. */
@@ -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"),fp_bundle_source_1=require("./injection/fp-bundle-source"),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"),mask_email_1=require("./utils/mask-email"),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=1500,FAIL_OPEN_TTL_MS=5e3,_lastFailOpenWarn={};function warnFailOpenThrottled(e){const t=Date.now();t-(_lastFailOpenWarn[e.reason]??0)<6e4||(_lastFailOpenWarn[e.reason]=t,console.warn(`[Unshared] Failing open (reason=${e.reason}${null!=e.status?`, status=${e.status}`:""}). Detection is not enforced for this request. Configure onFailOpen to observe this directly.`))}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:a,includePathPrefix:d,disableBotFilter:c=!1,checkUserTimeoutMs:l=CHECK_USER_TIMEOUT_MS,cacheMissStrategy:u="block",sessionId:_,deviceId:p,onFlagged:h,onError:f,onFailOpen:g,blockFlagged:v=!1,flaggedMode:m,autoInterstitial:I=!1,interstitialFlowType:k="email_verification"}=t,y=m??(v?"gate":void 0),A=I||"overlay"===y,C=e=>{if(g)try{g(e)}catch{}else"async_miss"!==e.reason&&warnFailOpenThrottled(e)},S=new verdict_cache_1.VerdictCache(o),x=new rate_limit_backoff_1.RateLimitBackoff,T=new Map,b=new dispatch_dedupe_1.DispatchDedupe,E=Date.now().toString(36),w=(0,fingerprint_script_1.generateFingerprintScript)(s,E,{autoInterstitial:A,interstitialFlowType:k});let O="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");O=(0,fs_1.readFileSync)(e,"utf8")}catch{}const U=(0,fp_bundle_source_1.createFingerprintBundleSource)(),F=void 0!==y||A,q="gate"===y?"flaggedMode 'gate' (blockFlagged)":"overlay"===y?"flaggedMode 'overlay'":"autoInterstitial";if(F&&!O)throw new Error(`[Unshared] ${q} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(F&&"/__unshared"!==s)throw new Error(`[Unshared] ${q} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const M="gate"===y?(0,gate_page_1.generateGatePage)(s,E):"",P=(0,submit_fp_1.handleSubmitFingerprint)({client:e,verdictCache:S,rateLimitBackoff:x,dispatchDedupe:b,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:_,resolveDeviceId:p,disableBotFilter:c,onError:f}),$=(0,verify_1.handleVerifyTrigger)({client:e,verdictCache:S,resolveEmailAddress:i,resolveDeviceId:p,onError:f}),j=(0,verify_1.handleVerify)({client:e,verdictCache:S,resolveUserId:r,resolveEmailAddress:i,resolveDeviceId:p,onError:f}),D=(0,interstitial_1.handleGetInterstitialFlow)({client:e}),V=n?Array.isArray(n)?n:[n]:null,N=`${s}/fp.js`,L=`${s}/fingerprint.js`,R=`${s}/submit-fp`,G=`${s}/verify-trigger`,H=`${s}/verify`,B=`${s}/status`,K=`${s}/interstitial-flow`,X=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(!V)return;const r=e.headers.origin??"";V.includes("*")?(t.setHeader("Access-Control-Allow-Origin","*"),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")):V.includes(r)&&(t.setHeader("Access-Control-Allow-Origin",r),t.setHeader("Vary","Origin"),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||"HEAD"===t.method)&&g===N)return n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBodyForMethod)(n,t.method,200,O);if(("GET"===t.method||"HEAD"===t.method)&&g===L){const e=U.get();return e?(n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBodyForMethod)(n,t.method,200,e)):void(0,http_helpers_1.sendBodyForMethod)(n,t.method,503,"/* fingerprint agent unavailable */")}if("POST"===t.method&&(g===R||g===G||g===H))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===R?void P(t,n):g===G?void $(t,n):void j(t,n);if("GET"===t.method&&g===K)return void D(t,n);if("GET"===t.method&&g===B){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=S.get(s);if((!r||S.isStale(s))&&o&&!x.isPaused()&&!S.isRefreshing(s)){S.markRefreshing(s);try{const i=(0,device_id_1.extractDeviceIdOrUndefined)(t,p),n=extractFingerprintId(t),a=extractSessionId(t,_),d=i??n??"unknown";await fetchAndCacheVerdict(e,S,s,o,d,n,a,l,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:s,emailAddress:o})),r=S.get(s)}catch(e){f&&f(e,{operation:"checkUser",userId:s,emailAddress:o}),C({operation:"checkUser",reason:"exception",userId:s,emailAddress:o})}finally{S.clearRefreshing(s)}}r&&r.isFlagged&&!r.isVerified&&(void 0!==y||h)&&o?(0,http_helpers_1.sendJson)(n,200,{status:"flagged",email:(0,mask_email_1.maskEmail)(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,a))return void o();if(!(0,include_path_1.shouldIncludePath)(g,d))return interceptForInjection(t,n,w),void o();let m;try{m=r(t)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(m)){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;m=e&&s?e:void 0}if(!m){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,w),void o()}const I=resolveEmail(t,i);if(setUserIdCookie(t,n,m),I&&setEmailCookie(t,n,I),!I)return interceptForInjection(t,n,w),void o();const k=extractSessionId(t,_),A=(0,device_id_1.extractDeviceIdOrUndefined)(t,p),E=extractFingerprintId(t),F=t.headers["user-agent"]??"",q=(0,client_ip_1.extractClientIp)(t),X=A??E;if(!c&&(0,is_bot_1.isBot)(F))return void o();const W=S.get(m);if(!W){if("async"===u)return S.isRefreshing(m)||(S.markRefreshing(m),fetchAndCacheVerdict(e,S,m,I,X??"unknown",E,k,l,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:m,emailAddress:I})).catch(()=>C({operation:"checkUser",reason:"exception",userId:m,emailAddress:I})).finally(()=>S.clearRefreshing(m))),C({operation:"checkUser",reason:"async_miss",userId:m,emailAddress:I}),J(),interceptForInjection(t,n,w),void o();let r=T.get(m);return r||(r=fetchAndCacheVerdict(e,S,m,I,X??"unknown",E,k,l,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:m,emailAddress:I})).finally(()=>T.delete(m)),T.set(m,r)),void r.then(e=>{e.isFlagged||J(),applyVerdict(e,m,I,t,n,o,w,h,y,M)}).catch(()=>{C({operation:"checkUser",reason:"exception",userId:m,emailAddress:I}),J(),interceptForInjection(t,n,w),o()})}function J(){"unknown"!==k&&X&&(x.isPaused()||dispatchUserEvent(e,S,x,b,{userId:m,emailAddress:I,sessionId:k,deviceId:X,fingerprintId:E,userAgent:F,ipAddress:q,eventType:v},f))}S.isStale(m)&&!S.isRefreshing(m)&&(S.markRefreshing(m),fetchAndCacheVerdict(e,S,m,I,X??"unknown",E,k,l,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:m,emailAddress:I})).catch(()=>C({operation:"checkUser",reason:"exception",userId:m,emailAddress:I})).finally(()=>S.clearRefreshing(m))),W.isFlagged||J(),applyVerdict(W,m,I,t,n,o,w,h,y,M)};return X.destroy=()=>{S.destroy(),U.stop()},X}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,a,d,c){const l=e.isFlagged&&!e.isVerified;if("gate"===d&&l)(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"===d&&l)(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&&a)try{a({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,a=CHECK_USER_TIMEOUT_MS,d){const c={timeoutMs:a,maxRetries:0},l=s&&"unknown"!==s?s:void 0,u=l??n,_=n??l;let p;u&&(c.deviceId=u),_&&(c.fingerprintId=_);const h=await Promise.race([e.checkUser(i,c),new Promise(e=>{p=setTimeout(()=>e(null),a)})]);return clearTimeout(p),h?h.failedOpen?(d?.(h.failedOpen.reason??"http_error",h.failedOpen.status),cacheFailOpenVerdict(t,r,i,o)):(t.set(r,{isFlagged:h.data?.is_user_flagged??!1,isVerified:!1,emailAddress:i,sessionId:o}),t.get(r)):(d?.("timeout"),cacheFailOpenVerdict(t,r,i,o))}function cacheFailOpenVerdict(e,t,r,i){const s=e.get(t);return e.set(t,{isFlagged:s?.isFlagged??!1,isVerified:s?.isVerified??!1,emailAddress:r,sessionId:i},5e3),e.get(t)}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"),fp_bundle_source_1=require("./injection/fp-bundle-source"),submit_fp_1=require("./routes/submit-fp"),verify_1=require("./routes/verify"),interstitial_1=require("./routes/interstitial"),gate_page_1=require("./injection/gate-page"),dispatch_processing_1=require("./dispatch-processing"),http_helpers_1=require("./utils/http-helpers"),content_type_1=require("./utils/content-type"),flagged_response_1=require("./utils/flagged-response"),mask_email_1=require("./utils/mask-email"),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=1500,FAIL_OPEN_TTL_MS=5e3,_lastFailOpenWarn={};function warnFailOpenThrottled(e){const t=Date.now();t-(_lastFailOpenWarn[e.reason]??0)<6e4||(_lastFailOpenWarn[e.reason]=t,console.warn(`[Unshared] Failing open (reason=${e.reason}${null!=e.status?`, status=${e.status}`:""}). Detection is not enforced for this request. Configure onFailOpen to observe this directly.`))}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,maxCacheSize:a,streamingIdleMs:d=50,skipPaths:c,includePathPrefix:l,disableBotFilter:u=!1,checkUserTimeoutMs:_=CHECK_USER_TIMEOUT_MS,cacheMissStrategy:p="block",sessionId:h,deviceId:f,onFlagged:g,onError:v,onFailOpen:m,blockFlagged:I=!1,flaggedMode:k,autoInterstitial:y=!1,interstitialFlowType:A="email_verification"}=t,C=k??(I?"gate":void 0),S=y||"overlay"===C,x=e=>{if(m)try{m(e)}catch{}else"async_miss"!==e.reason&&warnFailOpenThrottled(e)},T=new verdict_cache_1.VerdictCache(o,a),b=new rate_limit_backoff_1.RateLimitBackoff,E=new Map,w=new dispatch_dedupe_1.DispatchDedupe,O=Date.now().toString(36),U=(0,fingerprint_script_1.generateFingerprintScript)(s,O,{autoInterstitial:S,interstitialFlowType:A});let F="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");F=(0,fs_1.readFileSync)(e,"utf8")}catch{}const q=(0,fp_bundle_source_1.createFingerprintBundleSource)(),M=void 0!==C||S,P="gate"===C?"flaggedMode 'gate' (blockFlagged)":"overlay"===C?"flaggedMode 'overlay'":"autoInterstitial";if(M&&!F)throw new Error(`[Unshared] ${P} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(M&&"/__unshared"!==s)throw new Error(`[Unshared] ${P} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const $="gate"===C?(0,gate_page_1.generateGatePage)(s,O):"",j=(0,submit_fp_1.handleSubmitFingerprint)({client:e,verdictCache:T,rateLimitBackoff:b,dispatchDedupe:w,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:h,resolveDeviceId:f,disableBotFilter:u,onError:v}),D=(0,verify_1.handleVerifyTrigger)({client:e,verdictCache:T,resolveEmailAddress:i,resolveDeviceId:f,onError:v}),V=(0,verify_1.handleVerify)({client:e,verdictCache:T,resolveUserId:r,resolveEmailAddress:i,resolveDeviceId:f,onError:v}),N=(0,interstitial_1.handleGetInterstitialFlow)({client:e}),L=n?Array.isArray(n)?n:[n]:null,R=`${s}/fp.js`,G=`${s}/fingerprint.js`,H=`${s}/submit-fp`,B=`${s}/verify-trigger`,K=`${s}/verify`,X=`${s}/status`,W=`${s}/interstitial-flow`,z=function(t,n,o){const a=(0,http_helpers_1.getRequestPath)(t.url),m=t.url||a;if(a.startsWith(s+"/")){if(function(e,t){if(!L)return;const r=e.headers.origin??"";L.includes("*")?(t.setHeader("Access-Control-Allow-Origin","*"),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")):L.includes(r)&&(t.setHeader("Access-Control-Allow-Origin",r),t.setHeader("Vary","Origin"),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||"HEAD"===t.method)&&a===R)return n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBodyForMethod)(n,t.method,200,F);if(("GET"===t.method||"HEAD"===t.method)&&a===G){const e=q.get();return e?(n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBodyForMethod)(n,t.method,200,e)):void(0,http_helpers_1.sendBodyForMethod)(n,t.method,503,"/* fingerprint agent unavailable */")}if("POST"===t.method&&(a===H||a===B||a===K))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."}}):a===H?void j(t,n):a===B?void D(t,n):void V(t,n);if("GET"===t.method&&a===W)return void N(t,n);if("GET"===t.method&&a===X){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=T.get(s);if((!r||T.isStale(s))&&o&&!b.isPaused()&&!T.isRefreshing(s)){T.markRefreshing(s);try{const i=(0,device_id_1.extractDeviceIdOrUndefined)(t,f),n=extractFingerprintId(t),a=extractSessionId(t,h),d=i??n??"unknown";await fetchAndCacheVerdict(e,T,s,o,d,n,a,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:s,emailAddress:o})),r=T.get(s)}catch(e){v&&v(e,{operation:"checkUser",userId:s,emailAddress:o}),x({operation:"checkUser",reason:"exception",userId:s,emailAddress:o})}finally{T.clearRefreshing(s)}}r&&r.isFlagged&&!r.isVerified&&(void 0!==C||g)&&o?(0,http_helpers_1.sendJson)(n,200,{status:"flagged",email:(0,mask_email_1.maskEmail)(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"}})}const I=void 0!==C;if((0,skip_paths_1.shouldSkipPath)(a,c,I))return void o();if(!(0,include_path_1.shouldIncludePath)(a,l))return interceptForInjection(t,n,U,d),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,U,d),void o()}const y=resolveEmail(t,i);if(setUserIdCookie(t,n,k),y&&setEmailCookie(t,n,y),!y)return interceptForInjection(t,n,U,d),void o();const A=extractSessionId(t,h),S=(0,device_id_1.extractDeviceIdOrUndefined)(t,f),O=extractFingerprintId(t),M=t.headers["user-agent"]??"",P=(0,client_ip_1.extractClientIp)(t),z=S??O;if(!u&&(0,is_bot_1.isBot)(M))return void o();const J=T.get(k);if(!J){if("async"===p)return T.isRefreshing(k)||(T.markRefreshing(k),fetchAndCacheVerdict(e,T,k,y,z??"unknown",O,A,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:k,emailAddress:y})).catch(()=>x({operation:"checkUser",reason:"exception",userId:k,emailAddress:y})).finally(()=>T.clearRefreshing(k))),x({operation:"checkUser",reason:"async_miss",userId:k,emailAddress:y}),Y(),interceptForInjection(t,n,U,d),void o();let r=E.get(k);return r||(r=fetchAndCacheVerdict(e,T,k,y,z??"unknown",O,A,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:k,emailAddress:y})).finally(()=>E.delete(k)),E.set(k,r)),void r.then(e=>{e.isFlagged||Y(),applyVerdict(e,k,y,t,n,o,U,g,C,$,d)}).catch(()=>{x({operation:"checkUser",reason:"exception",userId:k,emailAddress:y}),Y(),interceptForInjection(t,n,U,d),o()})}function Y(){"unknown"!==A&&z&&(b.isPaused()||dispatchUserEvent(e,T,b,w,{userId:k,emailAddress:y,sessionId:A,deviceId:z,fingerprintId:O,userAgent:M,ipAddress:P,eventType:m},v))}T.isStale(k)&&!T.isRefreshing(k)&&(T.markRefreshing(k),fetchAndCacheVerdict(e,T,k,y,z??"unknown",O,A,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:k,emailAddress:y})).catch(()=>x({operation:"checkUser",reason:"exception",userId:k,emailAddress:y})).finally(()=>T.clearRefreshing(k))),J.isFlagged||Y(),applyVerdict(J,k,y,t,n,o,U,g,C,$,d)};return z.destroy=()=>{T.destroy(),q.stop()},z}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,a,d,c,l){const u=e.isFlagged&&!e.isVerified;if("gate"===d&&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"===d&&u)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(interceptForInjection(i,s,o,l),n()):(0,http_helpers_1.sendJson)(s,403,(0,flagged_response_1.flaggedResponse)(r));else if(interceptForInjection(i,s,o,l),e.isFlagged&&!e.isVerified&&a)try{a({userId:t,emailAddress:r,verdict:e,req:i,res:s,next:n})}catch{n()}else n()}function interceptForInjection(e,t,r,i){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,streamingIdleMs:i})}function dispatchUserEvent(e,t,r,i,s,n){i.mark(s.userId,s.eventType),(0,dispatch_processing_1.runProcessUserEvent)({client:e,cache:t,backoff:r,onError:n},{eventType:s.eventType,userId:s.userId,emailAddress:s.emailAddress,ipAddress:s.ipAddress,deviceId:s.deviceId,fingerprintId:s.fingerprintId,sessionHash:s.sessionId,userAgent:s.userAgent},dispatch_processing_1.DISPATCH_TIMEOUT_MS)}async function fetchAndCacheVerdict(e,t,r,i,s,n,o,a=CHECK_USER_TIMEOUT_MS,d){const c={timeoutMs:a,maxRetries:0},l=s&&"unknown"!==s?s:void 0,u=l??n,_=n??l;let p;u&&(c.deviceId=u),_&&(c.fingerprintId=_);const h=await Promise.race([e.checkUser(i,c),new Promise(e=>{p=setTimeout(()=>e(null),a)})]);return clearTimeout(p),h?h.failedOpen?(d?.(h.failedOpen.reason??"http_error",h.failedOpen.status),cacheFailOpenVerdict(t,r,i,o)):(t.set(r,{isFlagged:h.data?.is_user_flagged??!1,isVerified:!1,emailAddress:i,sessionId:o}),t.get(r)):(d?.("timeout"),cacheFailOpenVerdict(t,r,i,o))}function cacheFailOpenVerdict(e,t,r,i){const s=e.get(t);return e.set(t,{isFlagged:s?.isFlagged??!1,isVerified:s?.isVerified??!1,emailAddress:r,sessionId:i},5e3),e.get(t)}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,14 +1,17 @@
1
- /**
2
- * Global rate-limit backoff for processUserEvent calls.
3
- *
4
- * When the Unshared API returns 429 with a Retry-After header,
5
- * the middleware pauses processUserEvent calls for the specified duration.
6
- * checkUser calls are not affected — enforcement takes priority.
7
- */
8
1
  export declare class RateLimitBackoff {
9
2
  private _resumeAtTimestamp;
10
- /** Pause processUserEvent calls for the given duration. */
3
+ private _failureResumeAtTimestamp;
4
+ private _consecutiveFailures;
5
+ /** Pause processUserEvent calls (server-directed 429), clamped to MAX_BACKOFF_MS. */
11
6
  pause(durationMs: number): void;
7
+ /** Record a network/5xx/timeout failure; engage failure backoff at the threshold. */
8
+ noteFailure(): void;
9
+ /** Record a success; resets the failure streak and lifts ONLY the failure-streak
10
+ * pause. It deliberately does NOT touch a server-directed 429 pause (pause()),
11
+ * so a success on one request can't wipe another request's legitimate 429 backoff. */
12
+ noteSuccess(): void;
12
13
  /** Returns true if processUserEvent calls should be skipped. */
13
14
  isPaused(): boolean;
15
+ /** Parse a Retry-After header value (delta-seconds OR HTTP-date) into ms, or null. */
16
+ static parseRetryAfter(headerValue: string | null, now?: number): number | null;
14
17
  }
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.RateLimitBackoff=void 0;class RateLimitBackoff{constructor(){this.i=0}pause(t){const s=Date.now()+t;s>this.i&&(this.i=s)}isPaused(){return Date.now()<this.i}}exports.RateLimitBackoff=RateLimitBackoff;
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.RateLimitBackoff=void 0;const MAX_BACKOFF_MS=3e5,FAILURE_THRESHOLD=3,FAILURE_BACKOFF_MS=1e4;class RateLimitBackoff{constructor(){this.i=0,this.o=0,this.h=0}pause(t){const s=Math.min(Math.max(0,t),3e5),e=Date.now()+s;e>this.i&&(this.i=e)}noteFailure(){if(this.h++,this.h>=3){const t=Date.now()+1e4;t>this.o&&(this.o=t)}}noteSuccess(){this.h=0,this.o=0}isPaused(){const t=Date.now();return t<this.i||t<this.o}static parseRetryAfter(t,s=Date.now()){if(null==t)return null;const e=parseInt(t,10);if(!isNaN(e)&&String(e)===t.trim())return 1e3*e;const i=Date.parse(t);return isNaN(i)?null:Math.max(0,i-s)}}exports.RateLimitBackoff=RateLimitBackoff;
@@ -19,4 +19,5 @@ import type { UnsharedResponse } from '../types';
19
19
  */
20
20
  export declare function interceptResponse(res: UnsharedResponse, transform: (body: Buffer, contentType: string | undefined) => Buffer | string | null, options?: {
21
21
  preventCaching?: boolean;
22
+ streamingIdleMs?: number;
22
23
  }): void;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.interceptResponse=interceptResponse;const MAX_INJECT_BYTES=2e6;function findWriteHeadHeaders(e){return e.find(e=>e&&"object"==typeof e)}function readHeaderFromWriteHeadArgs(e,t){const n=findWriteHeadHeaders(e);if(!n)return;const r=t.toLowerCase();if(Array.isArray(n)){if(n.length&&Array.isArray(n[0])){for(const e of n)if(String(e[0]).toLowerCase()===r)return e[1]}else for(let e=0;e+1<n.length;e+=2)if(String(n[e]).toLowerCase()===r)return n[e+1]}else for(const e of Object.keys(n))if(e.toLowerCase()===r)return n[e]}function deleteHeaderFromWriteHeadArgs(e,t){const n=findWriteHeadHeaders(e);if(!n)return;const r=t.toLowerCase();if(Array.isArray(n))if(n.length&&Array.isArray(n[0]))for(let e=n.length-1;e>=0;e--)String(n[e][0]).toLowerCase()===r&&n.splice(e,1);else for(let e=n.length-2;e>=0;e-=2)String(n[e]).toLowerCase()===r&&n.splice(e,2);else for(const e of Object.keys(n))e.toLowerCase()===r&&delete n[e]}function interceptResponse(e,t,n){const r=n?.preventCaching??!1,o=e.write.bind(e),f=e.end.bind(e),i=e.writeHead.bind(e),c=[];let u=0,s=!1,l=null;function d(){e.write=o,e.end=f;for(const e of c)o(e);c.length=0}function a(e,t){null===l&&null!=e&&(l=String(e).includes("text/html"),l&&t&&(l=!1),!1===l&&d())}function g(){a(e.getHeader("content-type"),e.getHeader("content-encoding"))}e.writeHead=function(t,...n){return a(readHeaderFromWriteHeadArgs(n,"content-type")??e.getHeader("content-type"),readHeaderFromWriteHeadArgs(n,"content-encoding")??e.getHeader("content-encoding")),l&&(r&&(e.setHeader("Cache-Control","no-store"),e.removeHeader("ETag"),e.removeHeader("Last-Modified")),e.removeHeader("Content-Length"),deleteHeaderFromWriteHeadArgs(n,"content-length")),i(t,...n)},e.write=function(e,t,n){if(g(),!1===l)return o(e,t,n);if(null!=e){const r=Buffer.isBuffer(e)?e:Buffer.from(e,"string"==typeof t?t:"utf8");if(u+=r.length,c.push(r),u>MAX_INJECT_BYTES)return d(),"function"==typeof t&&t(null),"function"==typeof n&&n(null),!0}return"function"==typeof t&&t(null),"function"==typeof n&&n(null),!0},e.end=function(n,r,i){if(s)return e;if(s=!0,g(),!1===l)return f(n,r,i);if(null!=n){const e=Buffer.isBuffer(n)?n:Buffer.from(n,"string"==typeof r?r:"utf8");u+=e.length,c.push(e)}if(u>MAX_INJECT_BYTES){d();const e="function"==typeof r?r:i;return e?f(e):f()}const a=Buffer.concat(c),p=e.getHeader("content-type");let y;try{y=t(a,p)}catch{y=null}if(null!=y){const t=Buffer.isBuffer(y)?y:Buffer.from(y,"utf8");e.headersSent||(e.setHeader("Content-Length",t.length),e.removeHeader("Content-Encoding")),o(t)}else a.length>0&&o(a);const H="function"==typeof r?r:i;return H?f(H):f(),e}}
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.interceptResponse=interceptResponse;const MAX_INJECT_BYTES=2e6;function findWriteHeadHeaders(e){return e.find(e=>e&&"object"==typeof e)}function readHeaderFromWriteHeadArgs(e,t){const n=findWriteHeadHeaders(e);if(!n)return;const r=t.toLowerCase();if(Array.isArray(n)){if(n.length&&Array.isArray(n[0])){for(const e of n)if(String(e[0]).toLowerCase()===r)return e[1]}else for(let e=0;e+1<n.length;e+=2)if(String(n[e]).toLowerCase()===r)return n[e+1]}else for(const e of Object.keys(n))if(e.toLowerCase()===r)return n[e]}function deleteHeaderFromWriteHeadArgs(e,t){const n=findWriteHeadHeaders(e);if(!n)return;const r=t.toLowerCase();if(Array.isArray(n))if(n.length&&Array.isArray(n[0]))for(let e=n.length-1;e>=0;e--)String(n[e][0]).toLowerCase()===r&&n.splice(e,1);else for(let e=n.length-2;e>=0;e-=2)String(n[e]).toLowerCase()===r&&n.splice(e,2);else for(const e of Object.keys(n))e.toLowerCase()===r&&delete n[e]}function interceptResponse(e,t,n){const r=n?.preventCaching??!1,o=n?.streamingIdleMs??50,f=e.write.bind(e),i=e.end.bind(e),u=e.writeHead.bind(e),c=[];let l=0,s=!1,d=null,a=null;function g(){d&&(clearTimeout(d),d=null),e.write=f,e.end=i;for(const e of c)f(e);c.length=0}function H(e,t){null===a&&null!=e&&(a=String(e).includes("text/html"),a&&t&&(a=!1),!1===a&&g())}function p(){H(e.getHeader("content-type"),e.getHeader("content-encoding"))}e.writeHead=function(t,...n){return H(readHeaderFromWriteHeadArgs(n,"content-type")??e.getHeader("content-type"),readHeaderFromWriteHeadArgs(n,"content-encoding")??e.getHeader("content-encoding")),a&&(r&&(e.setHeader("Cache-Control","no-store"),e.removeHeader("ETag"),e.removeHeader("Last-Modified"),deleteHeaderFromWriteHeadArgs(n,"cache-control"),deleteHeaderFromWriteHeadArgs(n,"etag"),deleteHeaderFromWriteHeadArgs(n,"last-modified")),e.removeHeader("Content-Length"),deleteHeaderFromWriteHeadArgs(n,"content-length")),u(t,...n)},e.write=function(e,t,n){if(p(),!1===a)return f(e,t,n);if(null!=e){const r=Buffer.isBuffer(e)?e:Buffer.from(e,"string"==typeof t?t:"utf8");if(l+=r.length,c.push(r),l>MAX_INJECT_BYTES)return g(),"function"==typeof t&&t(null),"function"==typeof n&&n(null),!0;d||s||(d=setTimeout(()=>{s||g()},o),"function"==typeof d.unref&&d.unref())}return"function"==typeof t&&t(null),"function"==typeof n&&n(null),!0},e.end=function(n,r,o){if(s)return e;if(s=!0,d&&(clearTimeout(d),d=null),p(),!1===a)return i(n,r,o);if(null!=n){const e=Buffer.isBuffer(n)?n:Buffer.from(n,"string"==typeof r?r:"utf8");l+=e.length,c.push(e)}if(l>MAX_INJECT_BYTES){g();const e="function"==typeof r?r:o;return e?i(e):i()}const u=Buffer.concat(c),H=e.getHeader("content-type");let y;try{y=t(u,H)}catch{y=null}if(null!=y){const t=Buffer.isBuffer(y)?y:Buffer.from(y,"utf8");e.headersSent||(e.setHeader("Content-Length",t.length),e.removeHeader("Content-Encoding")),f(t)}else u.length>0&&f(u);const A="function"==typeof r?r:o;return A?i(A):i(),e}}
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"i",{value:!0}),exports.handleSubmitFingerprint=handleSubmitFingerprint;const util_1=require("../../util"),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"),http_helpers_1=require("../utils/http-helpers");function handleSubmitFingerprint(e){return async(i,t)=>{try{const s=i.body??{},n={full_hash:s.hash??"",fingerprint_id:s.stable_hash??"",timestamp:s.collected_at??(new Date).toISOString(),isIncognito:s.is_incognito??!1,components:s.components??{},version:s.version??"inline-1.0.0",..."remote"===s.source||"bundled"===s.source?{source:s.source}:{}};let r,o,_;try{const t=e.resolveUserId?e.resolveUserId(i):void 0;t&&!(0,sentinel_user_id_1.isSentinelUserId)(t)&&(r=t)}catch{}if(!r){const e="string"==typeof s.user_id?s.user_id:void 0;e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(r=e)}if(!r){const e=(0,cookies_1.parseCookie)(i,"__unshared_uid");e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(r=e)}try{o=e.resolveEmailAddress?e.resolveEmailAddress(i):void 0}catch{}o=o??(0,cookies_1.parseCookie)(i,"__unshared_email")??s.email??void 0;try{_=e.resolveSessionId?e.resolveSessionId(i):void 0}catch{}_=_??s.session_id??(0,cookies_1.parseCookie)(i,"__unshared_sid");const d=(0,client_ip_1.extractClientIp)(i),c=i.headers["user-agent"]??"";if(!e.disableBotFilter&&(0,is_bot_1.isBot)(c))return void(0,http_helpers_1.sendJson)(t,200,{success:!0});const u=(n.fingerprint_id&&n.fingerprint_id.length>0?n.fingerprint_id:void 0)??(0,device_id_1.extractDeviceId)(i,e.resolveDeviceId),a=n.fingerprint_id||void 0,l=n.full_hash||void 0,p=(0,secure_1.isSecureRequest)(i)?"; Secure":"",h=[];if(l&&!(0,cookies_1.parseCookie)(i,"__unshared_fingerprint_id")&&h.push(`__unshared_fingerprint_id=${encodeURIComponent(l)}; HttpOnly; Path=/; SameSite=Lax${p}`),a){const e=(0,cookies_1.parseCookie)(i,"__unshared_fp_id");e&&e===a||h.push(`__unshared_fp_id=${encodeURIComponent(a)}; Path=/; SameSite=Lax; Max-Age=31536000${p}`)}if(o&&!(0,cookies_1.parseCookie)(i,"__unshared_email")&&h.push(`__unshared_email=${encodeURIComponent(o)}; HttpOnly; Path=/; SameSite=Lax${p}`),h.length>0){const e=t.getHeader("Set-Cookie");if(e){const i=Array.isArray(e)?[...e]:[String(e)];i.push(...h),t.setHeader("Set-Cookie",i)}else t.setHeader("Set-Cookie",h)}let m;if("string"==typeof s.event_type&&s.event_type)m=s.event_type;else{const e=i.headers.referer??i.headers.referrer;let t="unknown";if("string"==typeof e&&e.length>0)try{const i=new URL(e);t=(i.pathname||"/")+(i.search||"")}catch{}m=t}const v=i.headers["x-idempotency-key"],f="string"==typeof v&&v.length>0?v:void 0,g=Date.now(),y=f?`${f}|${g}`:a&&r?`${(0,util_1.sha256Hex)(`${a}|${r}|${m}`)}|${g}`:void 0;if(r&&e.client.submitFingerprintEvent(n,{userId:r,emailAddress:o,sessionHash:_,eventType:m,ipAddress:d,userAgent:c,idempotencyKey:y}).catch(i=>{e.onError&&e.onError(i,{operation:"submitFingerprintEvent",userId:r,emailAddress:o})}),r&&o&&!e.rateLimitBackoff.isPaused()&&!e.dispatchDedupe.wasRecentlyDispatched(r,m))try{const i=await e.client.processUserEvent({eventType:m,userId:r,emailAddress:o,ipAddress:d,deviceId:u,fingerprintId:a,sessionHash:_??"unknown",userAgent:c});i.success&&i.data?.analysis&&e.verdictCache.update(r,{isFlagged:i.data.analysis.is_user_flagged}),!i.success&&i.error?.retryAfter&&e.rateLimitBackoff.pause(1e3*i.error.retryAfter)}catch(i){e.onError&&e.onError(i,{operation:"processUserEvent",userId:r,emailAddress:o})}(0,http_helpers_1.sendJson)(t,200,{success:!0})}catch(i){e.onError&&e.onError(i,{operation:"submitFingerprintEvent"}),(0,http_helpers_1.sendJson)(t,200,{success:!0})}}}
1
+ "use strict";Object.defineProperty(exports,"i",{value:!0}),exports.handleSubmitFingerprint=handleSubmitFingerprint;const util_1=require("../../util"),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"),http_helpers_1=require("../utils/http-helpers"),dispatch_processing_1=require("../dispatch-processing");function handleSubmitFingerprint(e){return async(i,t)=>{try{const s=i.body??{},n={full_hash:s.hash??"",fingerprint_id:s.stable_hash??"",timestamp:s.collected_at??(new Date).toISOString(),isIncognito:s.is_incognito??!1,components:s.components??{},version:s.version??"inline-1.0.0",..."remote"===s.source||"bundled"===s.source?{source:s.source}:{}};let r,o,_;try{const t=e.resolveUserId?e.resolveUserId(i):void 0;t&&!(0,sentinel_user_id_1.isSentinelUserId)(t)&&(r=t)}catch{}if(!r){const e="string"==typeof s.user_id?s.user_id:void 0;e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(r=e)}if(!r){const e=(0,cookies_1.parseCookie)(i,"__unshared_uid");e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(r=e)}try{o=e.resolveEmailAddress?e.resolveEmailAddress(i):void 0}catch{}o=o??(0,cookies_1.parseCookie)(i,"__unshared_email")??s.email??void 0;try{_=e.resolveSessionId?e.resolveSessionId(i):void 0}catch{}_=_??s.session_id??(0,cookies_1.parseCookie)(i,"__unshared_sid");const c=(0,client_ip_1.extractClientIp)(i),d=i.headers["user-agent"]??"";if(!e.disableBotFilter&&(0,is_bot_1.isBot)(d))return void(0,http_helpers_1.sendJson)(t,200,{success:!0});const u=(n.fingerprint_id&&n.fingerprint_id.length>0?n.fingerprint_id:void 0)??(0,device_id_1.extractDeviceId)(i,e.resolveDeviceId),a=n.fingerprint_id||void 0,p=n.full_hash||void 0,l=(0,secure_1.isSecureRequest)(i)?"; Secure":"",h=[];if(p&&!(0,cookies_1.parseCookie)(i,"__unshared_fingerprint_id")&&h.push(`__unshared_fingerprint_id=${encodeURIComponent(p)}; HttpOnly; Path=/; SameSite=Lax${l}`),a){const e=(0,cookies_1.parseCookie)(i,"__unshared_fp_id");e&&e===a||h.push(`__unshared_fp_id=${encodeURIComponent(a)}; Path=/; SameSite=Lax; Max-Age=31536000${l}`)}if(o&&!(0,cookies_1.parseCookie)(i,"__unshared_email")&&h.push(`__unshared_email=${encodeURIComponent(o)}; HttpOnly; Path=/; SameSite=Lax${l}`),h.length>0){const e=t.getHeader("Set-Cookie");if(e){const i=Array.isArray(e)?[...e]:[String(e)];i.push(...h),t.setHeader("Set-Cookie",i)}else t.setHeader("Set-Cookie",h)}let f;if("string"==typeof s.event_type&&s.event_type)f=s.event_type;else{const e=i.headers.referer??i.headers.referrer;let t="unknown";if("string"==typeof e&&e.length>0)try{const i=new URL(e);t=(i.pathname||"/")+(i.search||"")}catch{}f=t}const g=i.headers["x-idempotency-key"],m="string"==typeof g&&g.length>0?g:void 0,v=Date.now(),y=m?`${m}|${v}`:a&&r?`${(0,util_1.sha256Hex)(`${a}|${r}|${f}`)}|${v}`:void 0;r&&e.client.submitFingerprintEvent(n,{userId:r,emailAddress:o,sessionHash:_,eventType:f,ipAddress:c,userAgent:d,idempotencyKey:y}).catch(i=>{e.onError&&e.onError(i,{operation:"submitFingerprintEvent",userId:r,emailAddress:o})}),r&&o&&!e.rateLimitBackoff.isPaused()&&!e.dispatchDedupe.wasRecentlyDispatched(r,f)&&await(0,dispatch_processing_1.runProcessUserEvent)({client:e.client,cache:e.verdictCache,backoff:e.rateLimitBackoff,onError:e.onError},{eventType:f,userId:r,emailAddress:o,ipAddress:c,deviceId:u,fingerprintId:a,sessionHash:_??"unknown",userAgent:d},dispatch_processing_1.SUBMIT_FP_TIMEOUT_MS),(0,http_helpers_1.sendJson)(t,200,{success:!0})}catch(i){e.onError&&e.onError(i,{operation:"submitFingerprintEvent"}),(0,http_helpers_1.sendJson)(t,200,{success:!0})}}}
@@ -1,5 +1,9 @@
1
1
  /**
2
2
  * Returns true if the request path looks like a static asset and should be
3
3
  * skipped by the middleware (no fingerprint injection, no verdict checks).
4
+ *
5
+ * Content-bearing extensions (pdf/txt/xml/media) are only skipped when no
6
+ * enforcement mode is active — otherwise a flagged user could pull protected
7
+ * downloads/media straight through a `blockFlagged`/`flaggedMode` gate.
4
8
  */
5
- export declare function shouldSkipPath(path: string, customSkipPaths?: string[]): boolean;
9
+ export declare function shouldSkipPath(path: string, customSkipPaths?: string[], enforcementActive?: boolean): boolean;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.shouldSkipPath=shouldSkipPath;const STATIC_EXTENSIONS=new Set([".js",".mjs",".cjs",".css",".map",".png",".jpg",".jpeg",".gif",".svg",".ico",".webp",".avif",".woff",".woff2",".ttf",".otf",".eot",".mp3",".mp4",".webm",".ogg",".wasm",".xml",".txt",".pdf"]),STATIC_PATH_PREFIXES=["/static/","/assets/","/public/","/_next/","/__vite/","/favicon"];function shouldSkipPath(t,s){if(s)for(const o of s)if(t.startsWith(o))return!0;const o=t.lastIndexOf(".");if(-1!==o){const s=t.slice(o).toLowerCase().split("?")[0];if(STATIC_EXTENSIONS.has(s))return!0}for(const s of STATIC_PATH_PREFIXES)if(t.startsWith(s))return!0;return!1}
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.shouldSkipPath=shouldSkipPath;const STATIC_ASSET_EXTENSIONS=new Set([".js",".mjs",".cjs",".css",".map",".png",".jpg",".jpeg",".gif",".svg",".ico",".webp",".avif",".woff",".woff2",".ttf",".otf",".eot",".wasm"]),CONTENT_EXTENSIONS=new Set([".mp3",".mp4",".webm",".ogg",".xml",".txt",".pdf"]),STATIC_PATH_PREFIXES=["/static/","/assets/","/public/","/_next/","/__vite/","/favicon"];function shouldSkipPath(t,e,s=!1){if(e)for(const s of e)if(t.startsWith(s))return!0;const o=t.lastIndexOf(".");if(-1!==o){const e=t.slice(o).toLowerCase().split("?")[0];if(STATIC_ASSET_EXTENSIONS.has(e))return!0;if(!s&&CONTENT_EXTENSIONS.has(e))return!0}for(const e of STATIC_PATH_PREFIXES)if(t.startsWith(e))return!0;return!1}
@@ -13,6 +13,7 @@ export declare class VerdictCache {
13
13
  private readonly _maxSize;
14
14
  private _sweepTimer;
15
15
  constructor(defaultTTL?: number, maxSize?: number);
16
+ private _ensureSweepTimer;
16
17
  get(userId: string): Verdict | undefined;
17
18
  set(userId: string, verdict: Omit<Verdict, 'cachedAt' | 'ttl'>, ttl?: number): void;
18
19
  /**
@@ -42,6 +43,12 @@ export declare class VerdictCache {
42
43
  destroy(): void;
43
44
  /** Remove all entries that are past their TTL + a 2x grace period. */
44
45
  private _sweep;
45
- /** Evict the oldest entry by cachedAt to make room. */
46
+ /**
47
+ * Evict the oldest entry by last WRITE (set/update) — O(1). Map preserves
48
+ * insertion order, and both set() and update() re-append an existing key on
49
+ * write, so the first key in iteration order is the least-recently-written.
50
+ * Reads (get/isStale/isRefreshing) don't refresh recency — this is LRU-by-write,
51
+ * not LRU-by-read.
52
+ */
46
53
  private _evictOldest;
47
54
  }
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.VerdictCache=void 0;const DEFAULT_TTL_MS=6e4,DEFAULT_MAX_SIZE=1e4,SWEEP_INTERVAL_MS=3e5;class VerdictCache{constructor(t=6e4,s=1e4){this.i=new Map,this.h=new Set,this.o=null,this.l=t,this.u=s,this.o=setInterval(()=>this._(),3e5),this.o&&"function"==typeof this.o.unref&&this.o.unref()}get(t){return this.i.get(t)}set(t,s,e){!this.i.has(t)&&this.i.size>=this.u&&this.p(),this.i.set(t,{...s,cachedAt:Date.now(),ttl:e??this.l})}update(t,s){const e=this.i.get(t);e?(void 0!==s.isFlagged&&(e.isFlagged=s.isFlagged),void 0!==s.isVerified&&(e.isVerified=s.isVerified),e.cachedAt=Date.now()):(this.i.size>=this.u&&this.p(),this.i.set(t,{isFlagged:s.isFlagged??!1,isVerified:s.isVerified??!1,emailAddress:"",sessionId:"",cachedAt:Date.now(),ttl:this.l}))}delete(t){this.i.delete(t),this.h.delete(t)}isStale(t){const s=this.i.get(t);return!!s&&Date.now()-s.cachedAt>s.ttl}isRefreshing(t){return this.h.has(t)}markRefreshing(t){this.h.add(t)}clearRefreshing(t){this.h.delete(t)}get size(){return this.i.size}clear(){this.i.clear(),this.h.clear()}destroy(){this.o&&(clearInterval(this.o),this.o=null),this.clear()}_(){const t=Date.now();for(const[s,e]of this.i)t-e.cachedAt>2*e.ttl&&(this.i.delete(s),this.h.delete(s))}p(){let t=null,s=1/0;for(const[e,i]of this.i)i.cachedAt<s&&(s=i.cachedAt,t=e);t&&(this.i.delete(t),this.h.delete(t))}}exports.VerdictCache=VerdictCache;
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.VerdictCache=void 0;const DEFAULT_TTL_MS=6e4,DEFAULT_MAX_SIZE=1e4,SWEEP_INTERVAL_MS=3e5;class VerdictCache{constructor(t=6e4,s=1e4){this.i=new Map,this.h=new Set,this.o=null,this.l=t,this.u=s}_(){if(!this.o)try{this.o=setInterval(()=>this.p(),3e5),this.o&&"function"==typeof this.o.unref&&this.o.unref()}catch{this.o=null}}get(t){return this.i.get(t)}set(t,s,i){this._(),!this.i.has(t)&&this.i.size>=this.u?this.v():this.i.has(t)&&this.i.delete(t),this.i.set(t,{...s,cachedAt:Date.now(),ttl:i??this.l})}update(t,s){this._();const i=this.i.get(t);i?(void 0!==s.isFlagged&&(i.isFlagged=s.isFlagged),void 0!==s.isVerified&&(i.isVerified=s.isVerified),i.cachedAt=Date.now(),this.i.delete(t),this.i.set(t,i)):(this.i.size>=this.u&&this.v(),this.i.set(t,{isFlagged:s.isFlagged??!1,isVerified:s.isVerified??!1,emailAddress:"",sessionId:"",cachedAt:Date.now(),ttl:this.l}))}delete(t){this.i.delete(t),this.h.delete(t)}isStale(t){const s=this.i.get(t);return!!s&&Date.now()-s.cachedAt>s.ttl}isRefreshing(t){return this.h.has(t)}markRefreshing(t){this.h.add(t)}clearRefreshing(t){this.h.delete(t)}get size(){return this.i.size}clear(){this.i.clear(),this.h.clear()}destroy(){this.o&&(clearInterval(this.o),this.o=null),this.clear()}p(){const t=Date.now();for(const[s,i]of this.i)t-i.cachedAt>2*i.ttl&&(this.i.delete(s),this.h.delete(s))}v(){const t=this.i.keys().next().value;void 0!==t&&(this.i.delete(t),this.h.delete(t))}}exports.VerdictCache=VerdictCache;
@@ -1,4 +1,6 @@
1
1
  import type { UnsharedClient } from '../client';
2
+ import { VerdictCache } from '../middleware/verdict-cache';
3
+ import type { Verdict } from '../middleware/verdict-cache';
2
4
  import type { WebMiddleware, WebProtectionConfig } from './types';
3
5
  /**
4
6
  * Web Standard equivalent of `unsharedBoundToUser` from src/middleware/index.ts.
@@ -11,7 +13,15 @@ import type { WebMiddleware, WebProtectionConfig } from './types';
11
13
  * handler calls `next(request)` to get the downstream Response, transforms
12
14
  * it (injects the fingerprint script into HTML), and returns a new Response.
13
15
  *
14
- * @example Next.js App Router (in middleware.ts)
16
+ * NOTE: In Next.js middleware, `next(request)` is typically `() => NextResponse.next()`,
17
+ * which is a passthrough with a null body and no Content-Type — there is no HTML to
18
+ * inject a script into. This middleware still resolves the verdict and enforces it
19
+ * (gate/overlay/403), but fingerprint COLLECTION must be done by the browser/React SDK
20
+ * mounted in your app, not by injection here. See the Next.js section of the
21
+ * integration guide.
22
+ *
23
+ * @example Next.js App Router (in middleware.ts) — verdict + enforcement only;
24
+ * mount the browser/React SDK client-side to collect fingerprints.
15
25
  * ```typescript
16
26
  * import { createWebProtectionMiddleware } from 'unshared-clientjs-sdk/web';
17
27
  *
@@ -26,3 +36,21 @@ import type { WebMiddleware, WebProtectionConfig } from './types';
26
36
  * ```
27
37
  */
28
38
  export declare function createWebProtectionMiddleware(client: UnsharedClient, config: WebProtectionConfig): WebMiddleware;
39
+ /** Mutable holder for the one-time passthrough-injection warning (see below). */
40
+ interface InjectWarnState {
41
+ emitted: boolean;
42
+ }
43
+ /**
44
+ * Read the response body, inject the fingerprint script into HTML responses,
45
+ * merge Set-Cookie headers, and return a new Response. For non-HTML responses,
46
+ * only the cookies and cache-control override are applied.
47
+ *
48
+ * `warnState` scopes the one-time "passthrough, nothing injected" warning to a
49
+ * single middleware instance; when omitted, the module-level default is used.
50
+ */
51
+ declare function injectIntoHtmlResponse(response: Response, fingerprintScript: string, extraCookies?: string[], warnState?: InjectWarnState): Promise<Response>;
52
+ declare function fetchAndCacheVerdict(client: UnsharedClient, cache: VerdictCache, userId: string, emailAddress: string, deviceId: string, fingerprintId: string | undefined, sessionId: string, timeoutMs?: number, reportFailOpen?: (reason: 'timeout' | 'http_error' | 'no_device_id', status?: number) => void): Promise<Verdict>;
53
+ export declare const __test_fetchAndCacheVerdict: typeof fetchAndCacheVerdict;
54
+ export declare const __test_injectIntoHtmlResponse: typeof injectIntoHtmlResponse;
55
+ export declare function __test_resetInjectWarning(): void;
56
+ export {};
@@ -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"),mask_email_1=require("../middleware/utils/mask-email"),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=1500,FAIL_OPEN_TTL_MS=5e3;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:p,onFailOpen:h,disableBotFilter:f=!1,checkUserTimeoutMs:m=CHECK_USER_TIMEOUT_MS,blockFlagged:w=!1,flaggedMode:g,autoInterstitial:v=!1,interstitialFlowType:I="email_verification"}=r,b=g??(w?"gate":void 0),y=v||"overlay"===b,A=e=>{if(h)try{h(e)}catch{}},S=Date.now().toString(36),E=void 0!==b||y,R="gate"===b?"flaggedMode 'gate' (blockFlagged)":"overlay"===b?"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"===b?(0,gate_page_1.generateGatePage)(i,S):"",k=new verdict_cache_1.VerdictCache(a),x=new rate_limit_backoff_1.RateLimitBackoff,C=new dispatch_dedupe_1.DispatchDedupe,U=(0,fingerprint_script_1.generateFingerprintScript)(i,S,{autoInterstitial:y,interstitialFlowType:I}),O=`${i}/fp.js`,$=`${i}/submit-fp`,F=`${i}/verify-trigger`,P=`${i}/verify`,L=`${i}/status`,q=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(!q)return{};const r=e.headers.get("origin")??"",t=q.includes("*");if(!t&&!q.includes(r))return{};const s={"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"};return t||(s["Access-Control-Allow-Credentials"]="true"),s}(r);if("OPTIONS"===r.method)return(0,web_helpers_1.emptyResponse)(204,i);if(("GET"===r.method||"HEAD"===r.method)&&a===O){if(!l)return(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},i);const e={...i,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600","Content-Length":String((new TextEncoder).encode(l).byteLength)};return"HEAD"===r.method?new Response(null,{status:200,headers:new Headers(e)}):(0,web_helpers_1.bodyResponse)(200,l,e)}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:k,rateLimitBackoff:x,dispatchDedupe:C,resolveUserId:t,resolveEmailAddress:s,resolveSessionId:_,resolveDeviceId:c,disableBotFilter:f,onError:p},i):a===F?handleVerifyTriggerWeb(r,n,{client:e,verdictCache:k,resolveUserId:t,resolveEmailAddress:s,resolveDeviceId:c,onError:p},i):handleVerifyWeb(r,n,{client:e,verdictCache:k,resolveUserId:t,resolveEmailAddress:s,resolveDeviceId:c,onError:p},i)}if("GET"===r.method&&a===L){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=k.get(n);if((!o||k.isStale(n))&&a&&!x.isPaused()&&!k.isRefreshing(n)){k.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,k,n,a,d,s,i,m,(e,r)=>A({operation:"checkUser",reason:e,status:r,userId:n,emailAddress:a})),o=k.get(n)}catch(e){p&&p(e,{operation:"checkUser",userId:n,emailAddress:a}),A({operation:"checkUser",reason:"exception",userId:n,emailAddress:a})}finally{k.clearRefreshing(n)}}return o&&o.isFlagged&&!o.isVerified&&(void 0!==b||u)&&a?(0,web_helpers_1.jsonResponse)(200,{status:"flagged",email:(0,mask_email_1.maskEmail)(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),v=[],I=(0,web_helpers_1.isSecureWebRequest)(r)?"; Secure":"";if(v.push(`__unshared_uid=${encodeURIComponent(w)}; Path=/; SameSite=Lax${I}`),v.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${I}`),g&&v.push(`__unshared_email=${encodeURIComponent(g)}; HttpOnly; Path=/; SameSite=Lax${I}`),!g)return injectIntoHtmlResponse(await n(r),U,v);const y=extractSessionIdFromRequest(r,_),S=(0,web_helpers_1.extractDeviceIdFromRequest)(r,c),E=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_fingerprint_id")||void 0,R=r.headers.get("user-agent")??"",D=(0,web_helpers_1.extractClientIpFromRequest)(r),M=S??E;if(!f&&(0,is_bot_1.isBot)(R))return n(r);let H=k.get(w);if(H)k.isStale(w)&&!k.isRefreshing(w)&&(k.markRefreshing(w),fetchAndCacheVerdict(e,k,w,g,M??"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(()=>k.clearRefreshing(w)));else try{H=await fetchAndCacheVerdict(e,k,w,g,M??"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,v)}const N=H.isFlagged&&!H.isVerified;if("gate"===b&&N)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"===b&&N)return(0,content_type_1.isHtmlNavigation)(r.method,r.headers.get("accept")??void 0)?injectIntoHtmlResponse(await n(r),U,v):(0,web_helpers_1.jsonResponse)(403,(0,flagged_response_1.flaggedResponse)(g));if(H.isFlagged&&!H.isVerified&&u)try{const e=await u({userId:w,emailAddress:g,verdict:H,request:r});if(e)return injectIntoHtmlResponse(e,U,v)}catch(e){p&&p(e,{operation:"checkUser",userId:w,emailAddress:g})}return H.isFlagged||"unknown"===y||!M||x.isPaused()||dispatchUserEvent(e,k,x,C,{userId:w,emailAddress:g,sessionId:y,deviceId:M,fingerprintId:E,userAgent:R,ipAddress:D,eventType:a+h},p),injectIntoHtmlResponse(await n(r),U,v)}}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 _=()=>{const e=r.get(t);return r.set(t,{isFlagged:e?.isFlagged??!1,isVerified:e?.isVerified??!1,emailAddress:s,sessionId:a},5e3),r.get(t)},c={};let l;i&&"unknown"!==i&&(c.deviceId=i),n&&(c.fingerprintId=n);const u=await Promise.race([e.checkUser(s,c),new Promise(e=>{l=setTimeout(()=>e(null),o)})]);return clearTimeout(l),u?u.failedOpen?(d?.(u.failedOpen.reason??"http_error",u.failedOpen.status),_()):(r.set(t,{isFlagged:u.data?.is_user_flagged??!1,isVerified:!1,emailAddress:s,sessionId:a}),r.get(t)):(d?.("timeout"),_())}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,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:_,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 v={...s,"Content-Type":"application/json"},I=new Response(JSON.stringify({success:!0}),{status:200,headers:v});for(const e of h)I.headers.append("Set-Cookie",e);return I}catch{return(0,web_helpers_1.jsonResponse)(200,{success:!0},s)}}async function handleVerifyTriggerWeb(e,r,t,s){try{const i=resolveEmailWithBody(e,r??{},t.resolveEmailAddress);if(!i)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},s);const n=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),a=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,o=await t.client.triggerEmailVerification(i,n,{fingerprintId:a});return o.success?(0,web_helpers_1.jsonResponse)(200,{success:!0,data:o.data},s):(0,web_helpers_1.jsonResponse)(200,{success:!1,error:o.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},s)}catch(e){return t.onError&&t.onError(e,{operation:"verifyTrigger"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},s)}}async function handleVerifyWeb(e,r,t,s){try{const i=resolveEmailWithBody(e,r??{},t.resolveEmailAddress),n=r?.code;if(!i||!n)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}},s);const a=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),o=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,d=await t.client.verify(i,a,n,{fingerprintId:o});if(d.success){let r;try{r=t.resolveUserId(e)}catch{}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.__test_injectIntoHtmlResponse=exports.__test_fetchAndCacheVerdict=void 0,exports.createWebProtectionMiddleware=createWebProtectionMiddleware,exports.__test_resetInjectWarning=__test_resetInjectWarning;const webcrypto_1=require("../webcrypto"),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"),mask_email_1=require("../middleware/utils/mask-email"),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"),dispatch_processing_1=require("../middleware/dispatch-processing"),web_helpers_1=require("./web-helpers"),CHECK_USER_TIMEOUT_MS=1500,FAIL_OPEN_TTL_MS=5e3;function createWebProtectionMiddleware(e,r){if(!r.userId)throw new Error("[Unshared] userId resolver is required");const{userId:t,emailAddress:s,routePrefix:n="/__unshared",corsOrigins:i,cacheTTL:o=6e4,skipPaths:a,includePathPrefix:d,sessionId:c,deviceId:_,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:I="email_verification"}=r,v=g??(w?"gate":void 0),y=b||"overlay"===v,S=e=>{if(h)try{h(e)}catch{}},A=Date.now().toString(36),E=void 0!==v||y,R="gate"===v?"flaggedMode 'gate' (blockFlagged)":"overlay"===v?"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"!==n)throw new Error(`[Unshared] ${R} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const x="gate"===v?(0,gate_page_1.generateGatePage)(n,A):"",T=new verdict_cache_1.VerdictCache(o),k=new Map,C=new rate_limit_backoff_1.RateLimitBackoff,U=new dispatch_dedupe_1.DispatchDedupe,O=(0,fingerprint_script_1.generateFingerprintScript)(n,A,{autoInterstitial:y,interstitialFlowType:I}),$={emitted:!1},P=`${n}/fp.js`,q=`${n}/submit-fp`,F=`${n}/verify-trigger`,L=`${n}/verify`,M=`${n}/status`,j=i?Array.isArray(i)?i:[i]:null;return async function(r,i,o){const h=e=>{o?.waitUntil&&o.waitUntil(e)};let w,g;try{const e=new URL(r.url);w=e.pathname,g=e.search}catch{return i(r)}if(w.startsWith(n+"/")){const n=function(e){if(!j)return{};const r=e.headers.get("origin")??"",t=j.includes("*");if(!t&&!j.includes(r))return{};const s={"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"};return t||(s["Access-Control-Allow-Credentials"]="true"),s}(r);if("OPTIONS"===r.method)return(0,web_helpers_1.emptyResponse)(204,n);if(("GET"===r.method||"HEAD"===r.method)&&w===P){if(!l)return(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},n);const e={...n,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600","Content-Length":String((new TextEncoder).encode(l).byteLength)};return"HEAD"===r.method?new Response(null,{status:200,headers:new Headers(e)}):(0,web_helpers_1.bodyResponse)(200,l,e)}if("POST"===r.method&&(w===q||w===F||w===L)){let i;try{i=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."}},n)}return w===q?handleSubmitFp(r,i,{client:e,verdictCache:T,rateLimitBackoff:C,dispatchDedupe:U,resolveUserId:t,resolveEmailAddress:s,resolveSessionId:c,resolveDeviceId:_,disableBotFilter:f,onError:p,registerBackground:h},n):w===F?handleVerifyTriggerWeb(r,i,{client:e,verdictCache:T,resolveUserId:t,resolveEmailAddress:s,resolveDeviceId:_,onError:p},n):handleVerifyWeb(r,i,{client:e,verdictCache:T,resolveUserId:t,resolveEmailAddress:s,resolveDeviceId:_,onError:p},n)}if("GET"===r.method&&w===M){let i;try{i=t(r)}catch{}if(!i)return(0,web_helpers_1.jsonResponse)(200,{status:"anonymous"},n);const o=resolveEmail(r,s);let a=T.get(i);if((!a||T.isStale(i))&&o&&!C.isPaused()&&!T.isRefreshing(i)){T.markRefreshing(i);try{const t=(0,web_helpers_1.extractDeviceIdFromRequest)(r,_),s=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_fingerprint_id")||void 0,n=extractSessionIdFromRequest(r,c),d=t??s??"unknown";await fetchAndCacheVerdict(e,T,i,o,d,s,n,m,(e,r)=>S({operation:"checkUser",reason:e,status:r,userId:i,emailAddress:o})),a=T.get(i)}catch(e){p&&p(e,{operation:"checkUser",userId:i,emailAddress:o}),S({operation:"checkUser",reason:"exception",userId:i,emailAddress:o})}finally{T.clearRefreshing(i)}}return a&&a.isFlagged&&!a.isVerified&&(void 0!==v||u)&&o?(0,web_helpers_1.jsonResponse)(200,{status:"flagged",email:(0,mask_email_1.maskEmail)(o)},n):(0,web_helpers_1.jsonResponse)(200,{status:"ok"},n)}return(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},n)}const b=void 0!==v;if((0,skip_paths_1.shouldSkipPath)(w,a,b))return i(r);if(!(0,include_path_1.shouldIncludePath)(w,d))return injectIntoHtmlResponse(await i(r),O,void 0,$);let I;try{I=t(r)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(I)){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,n=Number.isFinite(s)&&Date.now()-s<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;I=e&&n?e:void 0}if(!I){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 i(r),O,t,$)}const y=resolveEmail(r,s),A=[],E=(0,web_helpers_1.isSecureWebRequest)(r)?"; Secure":"";if(A.push(`__unshared_uid=${encodeURIComponent(I)}; Path=/; SameSite=Lax${E}`),A.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${E}`),y&&A.push(`__unshared_email=${encodeURIComponent(y)}; HttpOnly; Path=/; SameSite=Lax${E}`),!y)return injectIntoHtmlResponse(await i(r),O,A,$);const R=extractSessionIdFromRequest(r,c),D=(0,web_helpers_1.extractDeviceIdFromRequest)(r,_),N=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_fingerprint_id")||void 0,H=r.headers.get("user-agent")??"",V=(0,web_helpers_1.extractClientIpFromRequest)(r),W=D??N;if(!f&&(0,is_bot_1.isBot)(H))return i(r);let B=T.get(I);if(B)T.isStale(I)&&!T.isRefreshing(I)&&(T.markRefreshing(I),h(fetchAndCacheVerdict(e,T,I,y,W??"unknown",N,R,m,(e,r)=>S({operation:"checkUser",reason:e,status:r,userId:I,emailAddress:y})).catch(()=>S({operation:"checkUser",reason:"exception",userId:I,emailAddress:y})).finally(()=>T.clearRefreshing(I))));else try{let r=k.get(I);r||(r=fetchAndCacheVerdict(e,T,I,y,W??"unknown",N,R,m,(e,r)=>S({operation:"checkUser",reason:e,status:r,userId:I,emailAddress:y})).finally(()=>k.delete(I)),k.set(I,r)),B=await r}catch{return S({operation:"checkUser",reason:"exception",userId:I,emailAddress:y}),injectIntoHtmlResponse(await i(r),O,A,$)}const K=B.isFlagged&&!B.isVerified;if("gate"===v&&K)return(0,content_type_1.isHtmlNavigation)(r.method,r.headers.get("accept")??void 0)?(0,web_helpers_1.bodyResponse)(200,x,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}):(0,web_helpers_1.jsonResponse)(403,(0,flagged_response_1.flaggedResponse)(y));if("overlay"===v&&K)return(0,content_type_1.isHtmlNavigation)(r.method,r.headers.get("accept")??void 0)?injectIntoHtmlResponse(await i(r),O,A,$):(0,web_helpers_1.jsonResponse)(403,(0,flagged_response_1.flaggedResponse)(y));if(B.isFlagged&&!B.isVerified&&u)try{const e=await u({userId:I,emailAddress:y,verdict:B,request:r});if(e)return injectIntoHtmlResponse(e,O,A,$)}catch(e){p&&p(e,{operation:"checkUser",userId:I,emailAddress:y})}return B.isFlagged||"unknown"===R||!W||C.isPaused()||h(dispatchUserEvent(e,T,C,U,{userId:I,emailAddress:y,sessionId:R,deviceId:W,fingerprintId:N,userAgent:H,ipAddress:V,eventType:w+g},p)),injectIntoHtmlResponse(await i(r),O,A,$)}}const defaultInjectWarnState={emitted:!1};function passthroughResponse(e,r,t){if(!r||0===r.length)return e;const s=(0,web_helpers_1.mergeResponseHeaders)(e.headers,void 0,r);return new Response(t,{status:e.status,statusText:e.statusText,headers:s})}async function injectIntoHtmlResponse(e,r,t,s=defaultInjectWarnState){const n=e.headers.get("content-type");if(204===e.status||304===e.status)return passthroughResponse(e,t,null);if(null===e.body)return n||s.emitted||(s.emitted=!0,console.warn("[Unshared] The response returned by your handler has no body (e.g. NextResponse.next()), so fingerprint injection did not run. In Next.js middleware, fingerprints must be collected by the browser/React SDK, while this middleware handles verdict + enforcement. See the Next.js section of the integration guide.")),passthroughResponse(e,t,null);if(!(0,content_type_1.isHtmlContentType)(n??void 0))return passthroughResponse(e,t,e.body);const i=await e.text(),o=i.lastIndexOf("</body>"),a=-1===o?i+r:i.slice(0,o)+r+i.slice(o),d=(0,web_helpers_1.mergeResponseHeaders)(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(a).length)},t);return d.delete("ETag"),d.delete("Last-Modified"),d.delete("Content-Encoding"),new Response(a,{status:e.status,statusText:e.statusText,headers:d})}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 n=r.email;return"string"==typeof n&&n?n:void 0}function extractSessionIdFromRequest(e,r){if(r)try{const t=r(e);if(t)return t}catch{}return(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid")??"unknown"}function dispatchUserEvent(e,r,t,s,n,i){return s.mark(n.userId,n.eventType),(0,dispatch_processing_1.runProcessUserEvent)({client:e,cache:r,backoff:t,onError:i},{eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,sessionHash:n.sessionId,userAgent:n.userAgent},dispatch_processing_1.DISPATCH_TIMEOUT_MS)}async function fetchAndCacheVerdict(e,r,t,s,n,i,o,a=CHECK_USER_TIMEOUT_MS,d){const c=()=>{const e=r.get(t);return r.set(t,{isFlagged:e?.isFlagged??!1,isVerified:e?.isVerified??!1,emailAddress:s,sessionId:o},5e3),r.get(t)},_={timeoutMs:a,maxRetries:0};let l;n&&"unknown"!==n&&(_.deviceId=n),i&&(_.fingerprintId=i);const u=await Promise.race([e.checkUser(s,_),new Promise(e=>{l=setTimeout(()=>e(null),a)})]);return clearTimeout(l),u?u.failedOpen?(d?.(u.failedOpen.reason??"http_error",u.failedOpen.status),c()):(r.set(t,{isFlagged:u.data?.is_user_flagged??!1,isVerified:!1,emailAddress:s,sessionId:o}),r.get(t)):(d?.("timeout"),c())}async function handleSubmitFp(e,r,t,s){try{const n={full_hash:r.hash??"",fingerprint_id:r.stable_hash??"",timestamp:r.collected_at??(new Date).toISOString(),isIncognito:r.is_incognito??!1,components:r.components??{},version:r.version??"inline-1.0.0"};let i,o,a;try{const r=t.resolveUserId(e);r&&!(0,sentinel_user_id_1.isSentinelUserId)(r)&&(i=r)}catch{}if(!i){const e="string"==typeof r.user_id?r.user_id:void 0;e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(i=e)}if(!i){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_uid");r&&!(0,sentinel_user_id_1.isSentinelUserId)(r)&&(i=r)}try{o=t.resolveEmailAddress?t.resolveEmailAddress(e):void 0}catch{}o=o??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")??r.email??void 0;try{a=t.resolveSessionId?t.resolveSessionId(e):void 0}catch{}a=a??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 _=(n.fingerprint_id&&n.fingerprint_id.length>0?n.fingerprint_id:void 0)??(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),l=n.fingerprint_id||void 0,u=n.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(o&&!(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")&&h.push(`__unshared_email=${encodeURIComponent(o)}; 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();let g;if(m?g=`${m}|${w}`:l&&i&&(g=`${await(0,webcrypto_1.sha256HexWeb)(`${l}|${i}|${f}`)}|${w}`),i){const e=t.client.submitFingerprintEvent(n,{userId:i,emailAddress:o,sessionHash:a,eventType:f,ipAddress:d,userAgent:c,idempotencyKey:g}).catch(e=>{t.onError&&t.onError(e,{operation:"submitFingerprintEvent",userId:i,emailAddress:o})});t.registerBackground?.(e)}if(i&&o&&!t.rateLimitBackoff.isPaused()&&!t.dispatchDedupe.wasRecentlyDispatched(i,f)){const e=(0,dispatch_processing_1.runProcessUserEvent)({client:t.client,cache:t.verdictCache,backoff:t.rateLimitBackoff,onError:t.onError},{eventType:f,userId:i,emailAddress:o,ipAddress:d,deviceId:_,fingerprintId:l,sessionHash:a??"unknown",userAgent:c},dispatch_processing_1.SUBMIT_FP_TIMEOUT_MS);t.registerBackground?.(e)}const b={...s,"Content-Type":"application/json"},I=new Response(JSON.stringify({success:!0}),{status:200,headers:b});for(const e of h)I.headers.append("Set-Cookie",e);return I}catch{return(0,web_helpers_1.jsonResponse)(200,{success:!0},s)}}async function handleVerifyTriggerWeb(e,r,t,s){try{const n=resolveEmailWithBody(e,r??{},t.resolveEmailAddress);if(!n)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},s);const i=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),o=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,a=await t.client.triggerEmailVerification(n,i,{fingerprintId:o});return a.success?(0,web_helpers_1.jsonResponse)(200,{success:!0,data:a.data},s):(0,web_helpers_1.jsonResponse)(200,{success:!1,error:a.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},s)}catch(e){return t.onError&&t.onError(e,{operation:"verifyTrigger"}),(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 n=resolveEmailWithBody(e,r??{},t.resolveEmailAddress),i=r?.code;if(!n||!i)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}},s);const o=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),a=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,d=await t.client.verify(n,o,i,{fingerprintId:a});if(d.success){let r;try{r=t.resolveUserId(e)}catch{}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)}}function __test_resetInjectWarning(){defaultInjectWarnState.emitted=!1}exports.__test_fetchAndCacheVerdict=fetchAndCacheVerdict,exports.__test_injectIntoHtmlResponse=injectIntoHtmlResponse;
@@ -12,7 +12,17 @@ import type { Verdict } from '../middleware/verdict-cache';
12
12
  /** A downstream handler that returns a Response. */
13
13
  export type WebHandler = (request: Request) => Response | Promise<Response>;
14
14
  /** Middleware that wraps a downstream handler, optionally intercepting the response. */
15
- export type WebMiddleware = (request: Request, next: WebHandler) => Response | Promise<Response>;
15
+ export type WebMiddleware = (request: Request, next: WebHandler,
16
+ /**
17
+ * Platform background-work hook (e.g. Cloudflare Workers / Vercel Edge
18
+ * `ExecutionContext.waitUntil`). When provided, background promises (SWR
19
+ * verdict refresh, event dispatch) are registered through it so the
20
+ * runtime keeps them alive past the response — otherwise they are killed
21
+ * at response end on edge runtimes.
22
+ */
23
+ ctx?: {
24
+ waitUntil?: (p: Promise<unknown>) => void;
25
+ }) => Response | Promise<Response>;
16
26
  export interface WebSubmitOptions {
17
27
  /** Override userId extractor. Falls back to body.user_id. */
18
28
  userIdExtractor?: (req: Request) => string | undefined;
@@ -1 +1 @@
1
- "use strict";function parseCookieFromRequest(e,t){const o=e.headers.get("cookie");if(!o)return;const n=o.match(new RegExp(`(?:^|; )${t}=([^;]*)`));return n?decodeURIComponent(n[1]):void 0}function extractClientIpFromRequest(e){const t=e.headers.get("cf-connecting-ip");if(t)return t;const o=e.headers.get("x-real-ip");if(o)return o;const n=e.headers.get("x-forwarded-for");if(n){const e=n.split(",")[0]?.trim();if(e)return e}return""}function extractDeviceIdFromRequest(e,t){if(t)try{const o=t(e);if(o)return o}catch{}const o=e.headers.get("x-device-id");if(o)return o;return parseCookieFromRequest(e,"__unshared_fp_id")||void 0}function extractDeviceIdFromRequestOrUnknown(e,t){return extractDeviceIdFromRequest(e,t)??"unknown"}function isSecureWebRequest(e){if("https"===e.headers.get("x-forwarded-proto"))return!0;try{return"https:"===new URL(e.url).protocol}catch{return!1}}function jsonResponse(e,t,o){const n=new Headers(o);return n.set("Content-Type","application/json"),new Response(JSON.stringify(t),{status:e,headers:n})}function emptyResponse(e,t){return new Response(null,{status:e,headers:new Headers(t)})}function bodyResponse(e,t,o){return new Response(t,{status:e,headers:new Headers(o)})}function appendCookieToHeaders(e,t){e.append("Set-Cookie",t)}function mergeResponseHeaders(e,t,o){const n=new Headers;e.forEach((e,t)=>{"set-cookie"!==t.toLowerCase()&&n.set(t,e)});const r="function"==typeof e.getSetCookie?e.getSetCookie():[];for(const e of r)n.append("Set-Cookie",e);if(o)for(const e of o)n.append("Set-Cookie",e);if(t)for(const[e,o]of Object.entries(t))n.set(e,o);return n}Object.defineProperty(exports,"t",{value:!0}),exports.parseCookieFromRequest=parseCookieFromRequest,exports.extractClientIpFromRequest=extractClientIpFromRequest,exports.extractDeviceIdFromRequest=extractDeviceIdFromRequest,exports.extractDeviceIdFromRequestOrUnknown=extractDeviceIdFromRequestOrUnknown,exports.isSecureWebRequest=isSecureWebRequest,exports.jsonResponse=jsonResponse,exports.emptyResponse=emptyResponse,exports.bodyResponse=bodyResponse,exports.appendCookieToHeaders=appendCookieToHeaders,exports.mergeResponseHeaders=mergeResponseHeaders;
1
+ "use strict";function parseCookieFromRequest(e,t){const o=e.headers.get("cookie");if(!o)return;const n=o.match(new RegExp(`(?:^|; )${t}=([^;]*)`));if(n)try{return decodeURIComponent(n[1])}catch{return n[1]}}function extractClientIpFromRequest(e){const t=e.headers.get("cf-connecting-ip");if(t)return t;const o=e.headers.get("x-real-ip");if(o)return o;const n=e.headers.get("x-forwarded-for");if(n){const e=n.split(",")[0]?.trim();if(e)return e}return""}function extractDeviceIdFromRequest(e,t){if(t)try{const o=t(e);if(o)return o}catch{}const o=e.headers.get("x-device-id");if(o)return o;return parseCookieFromRequest(e,"__unshared_fp_id")||void 0}function extractDeviceIdFromRequestOrUnknown(e,t){return extractDeviceIdFromRequest(e,t)??"unknown"}function isSecureWebRequest(e){if("https"===e.headers.get("x-forwarded-proto"))return!0;try{return"https:"===new URL(e.url).protocol}catch{return!1}}function jsonResponse(e,t,o){const n=new Headers(o);return n.set("Content-Type","application/json"),new Response(JSON.stringify(t),{status:e,headers:n})}function emptyResponse(e,t){return new Response(null,{status:e,headers:new Headers(t)})}function bodyResponse(e,t,o){return new Response(t,{status:e,headers:new Headers(o)})}function appendCookieToHeaders(e,t){e.append("Set-Cookie",t)}function mergeResponseHeaders(e,t,o){const n=new Headers;e.forEach((e,t)=>{"set-cookie"!==t.toLowerCase()&&n.set(t,e)});const r="function"==typeof e.getSetCookie?e.getSetCookie():[];for(const e of r)n.append("Set-Cookie",e);if(o)for(const e of o)n.append("Set-Cookie",e);if(t)for(const[e,o]of Object.entries(t))n.set(e,o);return n}Object.defineProperty(exports,"t",{value:!0}),exports.parseCookieFromRequest=parseCookieFromRequest,exports.extractClientIpFromRequest=extractClientIpFromRequest,exports.extractDeviceIdFromRequest=extractDeviceIdFromRequest,exports.extractDeviceIdFromRequestOrUnknown=extractDeviceIdFromRequestOrUnknown,exports.isSecureWebRequest=isSecureWebRequest,exports.jsonResponse=jsonResponse,exports.emptyResponse=emptyResponse,exports.bodyResponse=bodyResponse,exports.appendCookieToHeaders=appendCookieToHeaders,exports.mergeResponseHeaders=mergeResponseHeaders;
@@ -0,0 +1,8 @@
1
+ export declare function sha256Bytes(input: string): Promise<Uint8Array>;
2
+ export declare function sha256HexWeb(input: string): Promise<string>;
3
+ /**
4
+ * Import raw key bytes as an AES-GCM CryptoKey, once, so callers can reuse it
5
+ * across many encryptions instead of paying importKey's cost per call.
6
+ */
7
+ export declare function importAesGcmKey(keyBytes: Uint8Array): Promise<CryptoKey>;
8
+ export declare function encryptDataWeb(plaintext: string, key: CryptoKey): Promise<string>;
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.sha256Bytes=sha256Bytes,exports.sha256HexWeb=sha256HexWeb,exports.importAesGcmKey=importAesGcmKey,exports.encryptDataWeb=encryptDataWeb;const subtle=globalThis.crypto.subtle;function toBase64(t){let e="";for(let n=0;n<t.length;n++)e+=String.fromCharCode(t[n]);return btoa(e)}async function sha256Bytes(t){const e=await subtle.digest("SHA-256",(new TextEncoder).encode(t));return new Uint8Array(e)}async function sha256HexWeb(t){const e=await sha256Bytes(t);return Array.from(e).map(t=>t.toString(16).padStart(2,"0")).join("")}async function importAesGcmKey(t){return subtle.importKey("raw",new Uint8Array(t),{name:"AES-GCM"},!1,["encrypt"])}async function encryptDataWeb(t,e){const n=globalThis.crypto.getRandomValues(new Uint8Array(12)),a=new Uint8Array(await subtle.encrypt({name:"AES-GCM",iv:n,tagLength:128},e,(new TextEncoder).encode(t))),r=a.slice(0,a.length-16),s=a.slice(a.length-16);return`${toBase64(n)}:${toBase64(s)}:${toBase64(r)}`}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unshared-clientjs-sdk",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
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",
@@ -46,13 +46,13 @@
46
46
  }
47
47
  },
48
48
  "engines": {
49
- "node": ">=18"
49
+ "node": ">=20"
50
50
  },
51
51
  "keywords": [],
52
52
  "author": "",
53
53
  "license": "MIT",
54
54
  "dependencies": {
55
- "unshared-frontend-sdk": "2.2.1"
55
+ "unshared-frontend-sdk": "2.3.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@unshared-labs/shared-types": "file:../../../shared/types",