unshared-clientjs-sdk 2.1.0-rc.8 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +1 -0
- package/dist/client.js +1 -1
- package/dist/esm/client.d.mts +1 -0
- package/dist/esm/client.mjs +1 -1
- package/dist/esm/middleware/index.d.mts +7 -4
- package/dist/esm/middleware/index.mjs +1 -1
- package/dist/esm/middleware/injection/fingerprint-script.mjs +1 -1
- package/dist/esm/middleware/response-interceptor.d.mts +7 -0
- package/dist/esm/middleware/response-interceptor.mjs +1 -1
- package/dist/esm/middleware/routes/submit-fp.mjs +1 -1
- package/dist/esm/middleware/routes/verify.d.mts +1 -0
- package/dist/esm/middleware/routes/verify.mjs +1 -1
- package/dist/esm/middleware/utils/flagged-response.d.mts +4 -0
- package/dist/esm/middleware/utils/flagged-response.mjs +1 -1
- package/dist/esm/middleware/utils/mask-email.d.mts +9 -0
- package/dist/esm/middleware/utils/mask-email.mjs +1 -0
- package/dist/esm/web/protection-handler.mjs +1 -1
- package/dist/middleware/index.d.ts +7 -4
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/injection/fingerprint-script.js +1 -1
- package/dist/middleware/response-interceptor.d.ts +7 -0
- package/dist/middleware/response-interceptor.js +1 -1
- package/dist/middleware/routes/submit-fp.js +1 -1
- package/dist/middleware/routes/verify.d.ts +1 -0
- package/dist/middleware/routes/verify.js +1 -1
- package/dist/middleware/utils/flagged-response.d.ts +4 -0
- package/dist/middleware/utils/flagged-response.js +1 -1
- package/dist/middleware/utils/mask-email.d.ts +9 -0
- package/dist/middleware/utils/mask-email.js +1 -0
- package/dist/web/protection-handler.js +1 -1
- package/package.json +2 -2
package/dist/client.d.ts
CHANGED
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(
|
|
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(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(!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.l=e.maxRetries??3,this.u=(0,crypto_1.createHash)("sha256").update(e.apiKey).digest(),this._=e.fetch}p(e){return(0,util_1.encryptData)(e,this.u)}async m(e,s){const t=this.l+1;let i={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let r=1;r<=t;r++){r>1&&await sleep(retryDelay(r-1));const t=new AbortController,a=setTimeout(()=>t.abort(),this.h);try{const r=this._??globalThis.fetch,n=await r(e,{method:s.method,headers:{"X-API-Key":this.i,...s.headers},body:s.body,signal:t.signal});if(clearTimeout(a),n.ok){const e=await n.text().catch(()=>"{}");let s;try{s=JSON.parse(e)}catch{s={}}const t="data"in s?s.data:s;return{success:!0,status:n.status,data:t}}const o=await parseErrorBody(n);if(n.status>=400&&n.status<500){if(429===n.status){const e=n.headers.get("Retry-After");if(null!=e){const s=parseInt(e,10);isNaN(s)||(o.retryAfter=s)}}return{success:!1,status:n.status,error:o}}i={success:!1,status:n.status,error:o}}catch(e){clearTimeout(a),i={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return i}async submitFingerprintEvent(e,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};return null!=e.source&&(t.source=e.source),null!=s?.userId&&(t.user_id=this.p(s.userId)),null!=s?.emailAddress&&(t.email_address=this.p(s.emailAddress)),null!=s?.sessionHash&&(t.session_hash=s.sessionHash),null!=s?.eventType&&(t.event_type=s.eventType),null!=s?.ipAddress&&(t.ip_address=this.p(s.ipAddress)),null!=s?.userAgent&&(t.user_agent=this.p(s.userAgent)),this.m(`${this.o}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":s?.idempotencyKey??(0,crypto_1.randomUUID)()},body:JSON.stringify(t)})}async processUserEvent(e){const s={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&&(s.fingerprint_id=this.p(e.fingerprintId)),null!=e.subscriptionStatus&&(s.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(s.event_details=e.eventDetails),this.m(`${this.o}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})}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=new URLSearchParams;i.set("email_address",this.p(e)),t.deviceId&&i.set("device_id",this.p(t.deviceId)),t.fingerprintId&&i.set("fingerprint_id",this.p(t.fingerprintId));const r=await this.m(`${this.o}/v2/check-user?${i}`,{method:"GET"});return r.success?r:{success:!0,status:200,data:{is_user_flagged:!1},failedOpen:{status:r.status}}}async triggerEmailVerification(e,s,t){const i={email_address:this.p(e),device_id:this.p(s)};t?.fingerprintId&&(i.fingerprint_id=this.p(t.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,s,t,i){const r={email_address:this.p(e),device_id:this.p(s),code:this.p(t)};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 s=new URLSearchParams;return s.set("flow_type",e?.flowType||"email_verification"),s.set("platform",e?.platform||"web"),this.m(`${this.o}/v2/interstitial-flow?${s}`,{method:"GET"})}}exports.UnsharedClient=UnsharedClient;
|
package/dist/esm/client.d.mts
CHANGED
package/dist/esm/client.mjs
CHANGED
|
@@ -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 p(e,t){const s=this.h+1;let r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let i=1;i<=s;i++){i>1&&await sleep(retryDelay(i-1));const s=new AbortController,a=setTimeout(()=>s.abort(),this.o);try{const i=this.u??globalThis.fetch,n=await i(e,{method:t.method,headers:{"X-API-Key":this.t,...t.headers},body:t.body,signal:s.signal});if(clearTimeout(a),n.ok){const e=await n.text().catch(()=>"{}");let t;try{t=JSON.parse(e)}catch{t={}}const s="data"in t?t.data:t;return{success:!0,status:n.status,data:s}}const o=await parseErrorBody(n);if(n.status>=400&&n.status<500){if(429===n.status){const e=n.headers.get("Retry-After");if(null!=e){const t=parseInt(e,10);isNaN(t)||(o.retryAfter=t)}}return{success:!1,status:n.status,error:o}}r={success:!1,status:n.status,error:o}}catch(e){clearTimeout(a),r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return r}async submitFingerprintEvent(e,t){const s={hash:e.full_hash,stable_hash:e.fingerprint_id,collected_at:e.timestamp,is_incognito:e.isIncognito,components:e.components,version:e.version};return null!=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.p(`${this.i}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":t?.idempotencyKey??randomUUID()},body:JSON.stringify(s)})}async processUserEvent(e){const t={event_type:e.eventType,user_id:this._(e.userId),ip_address:this._(e.ipAddress),device_id:this._(e.deviceId),session_hash:e.sessionHash,user_agent:this._(e.userAgent),email_address:this._(e.emailAddress)};return null!=e.fingerprintId&&(t.fingerprint_id=this._(e.fingerprintId)),null!=e.subscriptionStatus&&(t.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(t.event_details=e.eventDetails),this.p(`${this.i}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}async checkUser(e,t){const s="string"==typeof t?{deviceId:t}:t;if(!s.deviceId&&!s.fingerprintId)return{success:!0,status:200,data:{is_user_flagged:!1}};const r=new URLSearchParams;r.set("email_address",this._(e)),s.deviceId&&r.set("device_id",this._(s.deviceId)),s.fingerprintId&&r.set("fingerprint_id",this._(s.fingerprintId));const i=await this.p(`${this.i}/v2/check-user?${r}`,{method:"GET"});return i.success?i:{success:!0,status:200,data:{is_user_flagged:!1},failedOpen:{status:i.status}}}async triggerEmailVerification(e,t,s){const r={email_address:this._(e),device_id:this._(t)};s?.fingerprintId&&(r.fingerprint_id=this._(s.fingerprintId));const i=await this.p(`${this.i}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});return!i.success&&(0===i.status||i.status>=500)?{success:!1,status:i.status,error:{code:"DELIVERY_FAILED",message:i.error?.message??"Delivery failed"}}:i}async verify(e,t,s,r){const i={email_address:this._(e),device_id:this._(t),code:this._(s)};r?.fingerprintId&&(i.fingerprint_id=this._(r.fingerprintId));const a=await this.p(`${this.i}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});return!a.success&&(0===a.status||a.status>=500)?{success:!1,status:a.status,error:{code:"DELIVERY_FAILED",message:a.error?.message??"Delivery failed"}}:a.success&&!1===a.data?.verified?{success:!1,status:a.status,error:{code:"VERIFICATION_FAILED",message:"Code is incorrect or expired",details:a.data.reason?{reason:a.data.reason}:void 0}}:a}async getInterstitialFlow(e){const t=new URLSearchParams;return t.set("flow_type",e?.flowType||"email_verification"),t.set("platform",e?.platform||"web"),this.p(`${this.i}/v2/interstitial-flow?${t}`,{method:"GET"})}}
|
|
1
|
+
import{createHash,randomUUID}from"crypto";import{encryptData}from"./util";const DEFAULT_BASE_URL="https://api.unshared.ai",DEFAULT_TIMEOUT_MS=1e4,DEFAULT_MAX_RETRIES=3,MAX_DELAY_MS=3e4,BASE_DELAY_MS=1e3;function sleep(e){return new Promise(t=>setTimeout(t,e))}function retryDelay(e){const t=Math.min(1e3*Math.pow(2,e-1),3e4),s=t*(.5*Math.random()-.25);return Math.max(0,t+s)}async function parseErrorBody(e){const t=await e.text().catch(()=>"");try{const s=JSON.parse(t);return s?.error?.code?{code:s.error.code,message:s.error.message??"Unknown error",details:s.error.details}:{code:"UNKNOWN_ERROR",message:t||e.statusText}}catch{return{code:"UNKNOWN_ERROR",message:t||e.statusText}}}export class UnsharedClient{constructor(e){if(!e.apiKey||""===e.apiKey.trim())throw new Error("apiKey is required");this.t=e.apiKey,this.i=e.baseUrl?e.baseUrl.replace(/\/$/,""):DEFAULT_BASE_URL,this.o=e.timeout??1e4,this.h=e.maxRetries??3,this.l=createHash("sha256").update(e.apiKey).digest(),this.u=e.fetch}_(e){return encryptData(e,this.l)}async p(e,t){const s=this.h+1;let r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let i=1;i<=s;i++){i>1&&await sleep(retryDelay(i-1));const s=new AbortController,a=setTimeout(()=>s.abort(),this.o);try{const i=this.u??globalThis.fetch,n=await i(e,{method:t.method,headers:{"X-API-Key":this.t,...t.headers},body:t.body,signal:s.signal});if(clearTimeout(a),n.ok){const e=await n.text().catch(()=>"{}");let t;try{t=JSON.parse(e)}catch{t={}}const s="data"in t?t.data:t;return{success:!0,status:n.status,data:s}}const o=await parseErrorBody(n);if(n.status>=400&&n.status<500){if(429===n.status){const e=n.headers.get("Retry-After");if(null!=e){const t=parseInt(e,10);isNaN(t)||(o.retryAfter=t)}}return{success:!1,status:n.status,error:o}}r={success:!1,status:n.status,error:o}}catch(e){clearTimeout(a),r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return r}async submitFingerprintEvent(e,t){const s={hash:e.full_hash,stable_hash:e.fingerprint_id,collected_at:e.timestamp,is_incognito:e.isIncognito,components:e.components,version:e.version};return null!=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.p(`${this.i}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":t?.idempotencyKey??randomUUID()},body:JSON.stringify(s)})}async processUserEvent(e){const t={event_type:e.eventType,user_id:this._(e.userId),ip_address:this._(e.ipAddress),device_id:this._(e.deviceId),session_hash:e.sessionHash,user_agent:this._(e.userAgent),email_address:this._(e.emailAddress)};return null!=e.fingerprintId&&(t.fingerprint_id=this._(e.fingerprintId)),null!=e.subscriptionStatus&&(t.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(t.event_details=e.eventDetails),this.p(`${this.i}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}async checkUser(e,t){const s="string"==typeof t?{deviceId:t}:t;if(!s.deviceId&&!s.fingerprintId)return{success:!0,status:200,data:{is_user_flagged:!1},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.p(`${this.i}/v2/check-user?${r}`,{method:"GET"});return i.success?i:{success:!0,status:200,data:{is_user_flagged:!1},failedOpen:{status:i.status}}}async triggerEmailVerification(e,t,s){const r={email_address:this._(e),device_id:this._(t)};s?.fingerprintId&&(r.fingerprint_id=this._(s.fingerprintId));const i=await this.p(`${this.i}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});return!i.success&&(0===i.status||i.status>=500)?{success:!1,status:i.status,error:{code:"DELIVERY_FAILED",message:i.error?.message??"Delivery failed"}}:i}async verify(e,t,s,r){const i={email_address:this._(e),device_id:this._(t),code:this._(s)};r?.fingerprintId&&(i.fingerprint_id=this._(r.fingerprintId));const a=await this.p(`${this.i}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});return!a.success&&(0===a.status||a.status>=500)?{success:!1,status:a.status,error:{code:"DELIVERY_FAILED",message:a.error?.message??"Delivery failed"}}:a.success&&!1===a.data?.verified?{success:!1,status:a.status,error:{code:"VERIFICATION_FAILED",message:"Code is incorrect or expired",details:a.data.reason?{reason:a.data.reason}:void 0}}:a}async getInterstitialFlow(e){const t=new URLSearchParams;return t.set("flow_type",e?.flowType||"email_verification"),t.set("platform",e?.platform||"web"),this.p(`${this.i}/v2/interstitial-flow?${t}`,{method:"GET"})}}
|
|
@@ -26,7 +26,7 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
|
|
|
26
26
|
includePathPrefix?: string[];
|
|
27
27
|
/** Skip the bot/crawler UA filter. Set to true in test environments so automated browsers (Playwright, Puppeteer, etc.) can observe verdicts. @default false */
|
|
28
28
|
disableBotFilter?: boolean;
|
|
29
|
-
/** Hard timeout (ms) for checkUser on cache miss. Fails open on timeout. @default
|
|
29
|
+
/** Hard timeout (ms) for checkUser on cache miss. Fails open on timeout. @default 1500 */
|
|
30
30
|
checkUserTimeoutMs?: number;
|
|
31
31
|
/** Resolves a custom session ID. Falls back to __unshared_sid cookie. */
|
|
32
32
|
sessionId?: (req: TReq) => string | undefined;
|
|
@@ -127,14 +127,15 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
|
|
|
127
127
|
* it is invoked best-effort and never blocks the request.
|
|
128
128
|
*
|
|
129
129
|
* `reason`: 'timeout' (exceeded checkUserTimeoutMs), 'http_error' (non-2xx
|
|
130
|
-
* masked into clean),
|
|
130
|
+
* masked into clean), 'exception' (checkUser threw), or 'no_device_id'
|
|
131
|
+
* (no device/fingerprint id yet — bootstrap window). `status` is present
|
|
131
132
|
* for 'http_error' (0 = network/transport).
|
|
132
133
|
*/
|
|
133
134
|
onFailOpen?: (context: FailOpenContext) => void;
|
|
134
135
|
}
|
|
135
136
|
export interface FailOpenContext {
|
|
136
137
|
operation: 'checkUser';
|
|
137
|
-
reason: 'timeout' | 'http_error' | 'exception';
|
|
138
|
+
reason: 'timeout' | 'http_error' | 'exception' | 'no_device_id';
|
|
138
139
|
status?: number;
|
|
139
140
|
userId?: string;
|
|
140
141
|
emailAddress?: string;
|
|
@@ -142,4 +143,6 @@ export interface FailOpenContext {
|
|
|
142
143
|
export type { Verdict };
|
|
143
144
|
export { VerdictCache };
|
|
144
145
|
export { flaggedResponse, ACCOUNT_FLAGGED_ERROR } from './utils/flagged-response';
|
|
145
|
-
export declare function unsharedBoundToUser<TReq extends UnsharedRequest = UnsharedRequest>(client: UnsharedClient, config: ProtectionConfig<TReq>): (req: TReq, res: UnsharedResponse, next: UnsharedNextFunction) => void
|
|
146
|
+
export declare function unsharedBoundToUser<TReq extends UnsharedRequest = UnsharedRequest>(client: UnsharedClient, config: ProtectionConfig<TReq>): ((req: TReq, res: UnsharedResponse, next: UnsharedNextFunction) => void) & {
|
|
147
|
+
destroy: () => void;
|
|
148
|
+
};
|
|
@@ -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,sendBody,getRequestPath}from"./utils/http-helpers";import{isHtmlContentType,isHtmlNavigation}from"./utils/content-type";import{flaggedResponse}from"./utils/flagged-response";import{shouldSkipPath}from"./utils/skip-paths";import{shouldIncludePath}from"./utils/include-path";import{isBot}from"./utils/is-bot";import{extractClientIp}from"./utils/client-ip";import{parseCookie}from"./utils/cookies";import{extractDeviceIdOrUndefined}from"./utils/device-id";import{isSecureRequest}from"./utils/secure";import{isSentinelUserId,SENTINEL_STICKINESS_TTL_MS}from"./utils/sentinel-user-id";export{VerdictCache};export{flaggedResponse,ACCOUNT_FLAGGED_ERROR}from"./utils/flagged-response";const CHECK_USER_TIMEOUT_MS=500;export function unsharedBoundToUser(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");if(!t.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:r,emailAddress:i,routePrefix:n="/__unshared",corsOrigins:o,cacheTTL:s=6e4,skipPaths:a,includePathPrefix:d,disableBotFilter:c=!1,checkUserTimeoutMs:l=CHECK_USER_TIMEOUT_MS,sessionId:u,deviceId:p,onFlagged:f,onError:m,onFailOpen:h,blockFlagged:g=!1,flaggedMode:S,autoInterstitial:v=!1,interstitialFlowType:I="email_verification"}=t,_=S??(g?"gate":void 0),C=v||"overlay"===_,y=e=>{if(h)try{h(e)}catch{}},k=new VerdictCache(s),A=new RateLimitBackoff,T=new DispatchDedupe,E=Date.now().toString(36),x=generateFingerprintScript(n,E,{autoInterstitial:C,interstitialFlowType:I});let U="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");U=readFileSync(e,"utf8")}catch{}const w=createFingerprintBundleSource(),F=void 0!==_||C,P="gate"===_?"flaggedMode 'gate' (blockFlagged)":"overlay"===_?"flaggedMode 'overlay'":"autoInterstitial";if(F&&!U)throw new Error(`[Unshared] ${P} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(F&&"/__unshared"!==n)throw new Error(`[Unshared] ${P} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const b="gate"===_?generateGatePage(n,E):"",R=handleSubmitFingerprint({client:e,verdictCache:k,rateLimitBackoff:A,dispatchDedupe:T,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:u,resolveDeviceId:p,disableBotFilter:c,onError:m}),O=handleVerifyTrigger({client:e,verdictCache:k,resolveEmailAddress:i,resolveDeviceId:p,onError:m}),N=handleVerify({client:e,verdictCache:k,resolveEmailAddress:i,resolveDeviceId:p,onError:m}),D=handleGetInterstitialFlow({client:e}),M=o?Array.isArray(o)?o:[o]:null,$=`${n}/fp.js`,j=`${n}/fingerprint.js`,L=`${n}/submit-fp`,V=`${n}/verify-trigger`,B=`${n}/verify`,q=`${n}/status`,G=`${n}/interstitial-flow`;return function(t,o,s){const h=getRequestPath(t.url),g=t.url||h;if(h.startsWith(n+"/")){if(function(e,t){if(!M)return;const r=e.headers.origin??"",i=M.includes("*");(i||M.includes(r))&&(t.setHeader("Access-Control-Allow-Origin",i?"*":r),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,o),"OPTIONS"===t.method)return void sendEmpty(o,204);if("GET"===t.method&&h===$)return o.setHeader("Content-Type","application/javascript"),o.setHeader("Cache-Control","public, max-age=3600"),void sendBody(o,200,U);if("GET"===t.method&&h===j){const e=w.get();return e?(o.setHeader("Content-Type","application/javascript"),o.setHeader("Cache-Control","public, max-age=3600"),void sendBody(o,200,e)):void sendBody(o,503,"/* fingerprint agent unavailable */")}if("POST"===t.method&&(h===L||h===V||h===B))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."}}):h===L?void R(t,o):h===V?void O(t,o):void N(t,o);if("GET"===t.method&&h===G)return void D(t,o);if("GET"===t.method&&h===q){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=k.get(n);if((!r||k.isStale(n))&&s&&!A.isPaused()&&!k.isRefreshing(n)){k.markRefreshing(n);try{const i=extractDeviceIdOrUndefined(t,p),o=extractFingerprintId(t),a=extractSessionId(t,u),d=i??o??"unknown";await fetchAndCacheVerdict(e,k,n,s,d,o,a,l,(e,t)=>y({operation:"checkUser",reason:e,status:t,userId:n,emailAddress:s})),r=k.get(n)}catch(e){m&&m(e,{operation:"checkUser",userId:n,emailAddress:s}),y({operation:"checkUser",reason:"exception",userId:n,emailAddress:s})}finally{k.clearRefreshing(n)}}r&&r.isFlagged&&!r.isVerified&&f&&s?sendJson(o,200,{status:"flagged",email:s}):sendJson(o,200,{status:"ok"})})()}return void sendJson(o,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if(shouldSkipPath(h,a))return void s();if(!shouldIncludePath(h,d))return interceptForInjection(t,o,x),void s();let S;try{S=r(t)}catch{}if(isSentinelUserId(S)){const e=parseCookie(t,"__unshared_uid"),r=parseCookie(t,"__unshared_uid_at"),i=r?Number(r):NaN,n=Number.isFinite(i)&&Date.now()-i<=SENTINEL_STICKINESS_TTL_MS;S=e&&n?e:void 0}if(!S){const e=isSecureRequest(t)?"; Secure":"";return appendSetCookie(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,x),void s()}const v=resolveEmail(t,i);if(setUserIdCookie(t,o,S),v&&setEmailCookie(t,o,v),!v)return interceptForInjection(t,o,x),void s();const I=extractSessionId(t,u),C=extractDeviceIdOrUndefined(t,p),E=extractFingerprintId(t),F=t.headers["user-agent"]??"",P=extractClientIp(t),H=C??E;if(!c&&isBot(F))return void s();const J=k.get(S);function K(){"unknown"!==I&&H&&(A.isPaused()||dispatchUserEvent(e,k,A,T,{userId:S,emailAddress:v,sessionId:I,deviceId:H,fingerprintId:E,userAgent:F,ipAddress:P,eventType:g},m))}J?(k.isStale(S)&&!k.isRefreshing(S)&&(k.markRefreshing(S),fetchAndCacheVerdict(e,k,S,v,H??"unknown",E,I,l,(e,t)=>y({operation:"checkUser",reason:e,status:t,userId:S,emailAddress:v})).catch(()=>y({operation:"checkUser",reason:"exception",userId:S,emailAddress:v})).finally(()=>k.clearRefreshing(S))),J.isFlagged||K(),applyVerdict(J,S,v,t,o,s,x,f,_,b)):fetchAndCacheVerdict(e,k,S,v,H??"unknown",E,I,l,(e,t)=>y({operation:"checkUser",reason:e,status:t,userId:S,emailAddress:v})).then(e=>{e.isFlagged||K(),applyVerdict(e,S,v,t,o,s,x,f,_,b)}).catch(()=>{y({operation:"checkUser",reason:"exception",userId:S,emailAddress:v}),K(),interceptForInjection(t,o,x),s()})}}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=parseCookie(e,"__unshared_email");if(r)return r;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,t,r,i,n,o,s,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={};let l;n&&"unknown"!==n&&(c.deviceId=n),o&&(c.fingerprintId=o);const u=await Promise.race([e.checkUser(i,c),new Promise(e=>{l=setTimeout(()=>e(null),a)})]);if(clearTimeout(l),!u)return d?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:s,cachedAt:0,ttl:0};u.failedOpen&&d?.("http_error",u.failedOpen.status);const p=u.data?.is_user_flagged??!1;return t.set(r,{isFlagged:p,isVerified:!1,emailAddress:i,sessionId:s}),t.get(r)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return parseCookie(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return parseCookie(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
|
|
1
|
+
import{readFileSync}from"fs";import{VerdictCache}from"./verdict-cache";import{RateLimitBackoff}from"./rate-limit-backoff";import{DispatchDedupe}from"./dispatch-dedupe";import{interceptResponse}from"./response-interceptor";import{generateFingerprintScript}from"./injection/fingerprint-script";import{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,sendBody,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,_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,sessionId:u,deviceId:p,onFlagged:f,onError:m,onFailOpen:h,blockFlagged:g=!1,flaggedMode:v,autoInterstitial:I=!1,interstitialFlowType:S="email_verification"}=t,_=v??(g?"gate":void 0),C=I||"overlay"===_,y=e=>{if(h)try{h(e)}catch{}else warnFailOpenThrottled(e)},k=new VerdictCache(s),A=new RateLimitBackoff,T=new DispatchDedupe,E=Date.now().toString(36),x=generateFingerprintScript(n,E,{autoInterstitial:C,interstitialFlowType:S});let F="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");F=readFileSync(e,"utf8")}catch{}const U=createFingerprintBundleSource(),w=void 0!==_||C,O="gate"===_?"flaggedMode 'gate' (blockFlagged)":"overlay"===_?"flaggedMode 'overlay'":"autoInterstitial";if(w&&!F)throw new Error(`[Unshared] ${O} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(w&&"/__unshared"!==n)throw new Error(`[Unshared] ${O} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const P="gate"===_?generateGatePage(n,E):"",b=handleSubmitFingerprint({client:e,verdictCache:k,rateLimitBackoff:A,dispatchDedupe:T,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:u,resolveDeviceId:p,disableBotFilter:c,onError:m}),R=handleVerifyTrigger({client:e,verdictCache:k,resolveEmailAddress:i,resolveDeviceId:p,onError:m}),D=handleVerify({client:e,verdictCache:k,resolveUserId:r,resolveEmailAddress:i,resolveDeviceId:p,onError:m}),N=handleGetInterstitialFlow({client:e}),$=o?Array.isArray(o)?o:[o]:null,M=`${n}/fp.js`,V=`${n}/fingerprint.js`,j=`${n}/submit-fp`,L=`${n}/verify-trigger`,B=`${n}/verify`,q=`${n}/status`,G=`${n}/interstitial-flow`,H=function(t,o,s){const h=getRequestPath(t.url),g=t.url||h;if(h.startsWith(n+"/")){if(function(e,t){if(!$)return;const r=e.headers.origin??"";$.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")):$.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&&h===M)return o.setHeader("Content-Type","application/javascript"),o.setHeader("Cache-Control","public, max-age=3600"),void sendBody(o,200,F);if("GET"===t.method&&h===V){const e=U.get();return e?(o.setHeader("Content-Type","application/javascript"),o.setHeader("Cache-Control","public, max-age=3600"),void sendBody(o,200,e)):void sendBody(o,503,"/* fingerprint agent unavailable */")}if("POST"===t.method&&(h===j||h===L||h===B))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."}}):h===j?void b(t,o):h===L?void R(t,o):void D(t,o);if("GET"===t.method&&h===G)return void N(t,o);if("GET"===t.method&&h===q){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=k.get(n);if((!r||k.isStale(n))&&s&&!A.isPaused()&&!k.isRefreshing(n)){k.markRefreshing(n);try{const i=extractDeviceIdOrUndefined(t,p),o=extractFingerprintId(t),a=extractSessionId(t,u),d=i??o??"unknown";await fetchAndCacheVerdict(e,k,n,s,d,o,a,l,(e,t)=>y({operation:"checkUser",reason:e,status:t,userId:n,emailAddress:s})),r=k.get(n)}catch(e){m&&m(e,{operation:"checkUser",userId:n,emailAddress:s}),y({operation:"checkUser",reason:"exception",userId:n,emailAddress:s})}finally{k.clearRefreshing(n)}}r&&r.isFlagged&&!r.isVerified&&(void 0!==_||f)&&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(h,a))return void s();if(!shouldIncludePath(h,d))return interceptForInjection(t,o,x),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,x),void s()}const I=resolveEmail(t,i);if(setUserIdCookie(t,o,v),I&&setEmailCookie(t,o,I),!I)return interceptForInjection(t,o,x),void s();const S=extractSessionId(t,u),C=extractDeviceIdOrUndefined(t,p),E=extractFingerprintId(t),w=t.headers["user-agent"]??"",O=extractClientIp(t),H=C??E;if(!c&&isBot(w))return void s();const J=k.get(v);function K(){"unknown"!==S&&H&&(A.isPaused()||dispatchUserEvent(e,k,A,T,{userId:v,emailAddress:I,sessionId:S,deviceId:H,fingerprintId:E,userAgent:w,ipAddress:O,eventType:g},m))}J?(k.isStale(v)&&!k.isRefreshing(v)&&(k.markRefreshing(v),fetchAndCacheVerdict(e,k,v,I,H??"unknown",E,S,l,(e,t)=>y({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:I})).catch(()=>y({operation:"checkUser",reason:"exception",userId:v,emailAddress:I})).finally(()=>k.clearRefreshing(v))),J.isFlagged||K(),applyVerdict(J,v,I,t,o,s,x,f,_,P)):fetchAndCacheVerdict(e,k,v,I,H??"unknown",E,S,l,(e,t)=>y({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:I})).then(e=>{e.isFlagged||K(),applyVerdict(e,v,I,t,o,s,x,f,_,P)}).catch(()=>{y({operation:"checkUser",reason:"exception",userId:v,emailAddress:I}),K(),interceptForInjection(t,o,x),s()})};return H.destroy=()=>{k.destroy(),U.stop()},H}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={},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)})]);if(clearTimeout(f),!m)return d?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:s,cachedAt:0,ttl:0};m.failedOpen&&d?.(m.failedOpen.reason??"http_error",m.failedOpen.status);const h=m.data?.is_user_flagged??!1;return t.set(r,{isFlagged:h,isVerified:!1,emailAddress:i,sessionId:s},m.failedOpen?5e3:void 0),t.get(r)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return parseCookie(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return parseCookie(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function generateFingerprintScript(e,t,n){const r=t?`?v=${escapeJavaScript(t)}`:"",s=n?.autoInterstitial?`\nvar _isdk=new UnsharedBrowser.UnsharedBrowser({baseUrl:""});\nwindow.addEventListener("unshared:flagged",function(){try{_isdk.showInterstitial({flowType:"${escapeJavaScript(n?.interstitialFlowType??"email_verification")}",onComplete:function(){try{location.reload()}catch(e){}}})}catch(e){}});`:"";return`<script>\n(function(){\ntry{\n// --- Bot drop (defense-in-depth) ---\n// Must be the first statement: we do not want to write cookies, localStorage,\n// session IDs, or any network requests for known-bot traffic. Mirrors the\n// regex in unshared-fingerprint-lib/src/detect/bot.ts and Node middleware\n// utils/is-bot.ts. Keep all three in sync.\nvar BOT_RE=/googlebot|bingbot|slurp|baiduspider|duckduckbot|yandex|sogou|exabot|ia_archiver|curl|wget|python-requests|python-urllib|axios|node-fetch|go-http-client|java\\/|libwww-perl|okhttp|apache-httpclient|http_request|httpie|headlesschrome|phantomjs|puppeteer|playwright|cypress|selenium|webdriver|electron|jsdom|vercel-screenshot|screenshot|prerender|lighthouse|chrome-lighthouse|pagespeed|gtmetrix|pingdom|nessus|nikto|sqlmap|burp|zap|qualys|openvas|nmap|masscan|facebookexternalhit|twitterbot|linkedinbot|whatsapp|telegrambot|slackbot|discordbot|bot|crawl|spider|scrape|fetch|scan/i;\nif(typeof navigator!=="undefined"&&navigator.userAgent&&BOT_RE.test(navigator.userAgent))return;\n\nvar pfx="${escapeJavaScript(e)}";\nvar SS_FP="__unshared_fp";\nvar SS_LAST_SUBMIT="__unshared_last_submit";\n\n// Dedup state: skip submit if (user_id + URL) matches last submission.\n// Modern SPAs (Next.js App Router, React Router, etc.) call replaceState\n// 3-5 times during hydration with the same URL — without this guard,\n// each call generates a redundant FP row with identical stable_hash.\n// Persisted to sessionStorage so hard reloads and framework double-boots\n// inside the same tab still dedupe (the in-memory value resets on reload).\nvar lastSubmitKey="";\ntry{lastSubmitKey=sessionStorage.getItem(SS_LAST_SUBMIT)||""}catch(e){}\n\n// Page-scoped dedup state SHARED with the frontend SDK (browser.ts getSharedDedup).\n// On a Tier 1 page both this inline script and the SDK submit; each holds its own\n// in-memory lastSubmitKey, so both could pass the check below and POST. window.__unshared\n// is read+written synchronously in submitFP (no await between), so whichever fires\n// first claims the uid|route key and the other no-ops — killing the read-before-write\n// race that doubled events. KEEP IN SYNC with browser.ts: namespace, lastKey, key formula.\nvar shared=(window.__unshared=window.__unshared||{});\n\n// --- Helpers ---\nfunction gC(n){var m=document.cookie.match(new RegExp("(?:^|; )"+n+"=([^;]*)"));return m?decodeURIComponent(m[1]):null}\nfunction sC(n,v,d){var e="";if(d){var dt=new Date();dt.setTime(dt.getTime()+d*864e5);e="; expires="+dt.toUTCString()}document.cookie=n+"="+encodeURIComponent(v)+e+"; path=/; SameSite=Lax"+(location.protocol==="https:"?"; Secure":"")}\nfunction uuid(){return(typeof crypto!=="undefined"&&crypto.randomUUID)?crypto.randomUUID():("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(c){var r=Math.random()*16|0;return(c==="x"?r:r&0x3|0x8).toString(16)}))}\n// Sentinel user IDs that must never be treated as real users. Mirrors\n// the Set in sentinel-user-id.ts — keep in sync. Empty string is handled\n// by the separate !uid checks below.\nvar SENTINEL_UIDS={"__pre_auth__":1,"anonymous":1,"guest":1,"undefined":1,"null":1};\nfunction isSentinelUid(v){return typeof v==="string"&&SENTINEL_UIDS.hasOwnProperty(v)}\n\n// --- Session + device IDs ---\n// Session ID is a UUID because it's supposed to be tab-scoped and random.\n// Device ID is intentionally NOT a UUID — Issue 9: random UUIDs wrote\n// meaningless device_ids to every fingerprint row. Instead we read the\n// stable fingerprint hash from localStorage if a previous submission\n// already persisted it; otherwise we leave did empty and let submitFP()\n// reconcile on the first successful collection. The Node middleware's\n// Issue 8 bootstrap-skip branch handles the empty-device_id window so we\n// never dispatch with a random or "unknown" value.\nvar sid=gC("__unshared_sid");\nif(!sid){sid=uuid();sC("__unshared_sid",sid,365)}\nvar did="";\ntry{did=localStorage.getItem("__unshared_device_id")||""}catch(e){}\nif(did){sC("__unshared_fp_id",did,365)}\n\n// --- Fingerprint cache (sessionStorage) ---\nfunction getFP(){try{var r=sessionStorage.getItem(SS_FP);return r?JSON.parse(r):null}catch(e){return null}}\nfunction setFP(fp){try{sessionStorage.setItem(SS_FP,JSON.stringify(fp))}catch(e){}}\n\n// --- Submit fingerprint to backend ---\nfunction submitFP(fp){\n var uid=gC("__unshared_uid");\n if(!uid||isSentinelUid(uid))return;\n // Issue 9: reconcile device_id to the stable fingerprint hash. This runs\n // before we send the X-Device-Id header so the very first submission\n // already carries the real value. Persist to localStorage so other tabs\n // (and future reloads) pick up the same stable ID without needing to\n // re-collect the fingerprint.\n if(fp.fingerprint_id){\n did=fp.fingerprint_id;\n try{localStorage.setItem("__unshared_device_id",did)}catch(e){}\n sC("__unshared_fp_id",did,365);\n }\n // event_type is the SPA route, not a fixed enum. Page-level event names\n // (page_load/route_change) collapsed every row into one of two buckets;\n // the URL is more useful for analytics and matches the frontend SDK.\n var route=(location.pathname||"/")+(location.search||"");\n var key=uid+"|"+route;\n // Check the shared window guard AND the in-memory key (last-key semantics, so an\n // SPA A->B->A revisit still submits the second A). The shared guard makes this\n // submitter and the frontend SDK see each other within the page.\n if(key===lastSubmitKey||shared.lastKey===key)return;\n shared.lastKey=key;\n lastSubmitKey=key;\n try{sessionStorage.setItem(SS_LAST_SUBMIT,key)}catch(e){}\n // collected_at is stamped fresh at submit time rather than carried from fp.timestamp,\n // because fp is cached per-tab in sessionStorage — reusing its original timestamp would\n // freeze collected_at at first load and drift against server created_at as the tab ages.\n // The server authoritatively overwrites this value again on ingress.\n var body={hash:fp.full_hash,stable_hash:fp.fingerprint_id,collected_at:(new Date()).toISOString(),is_incognito:fp.isIncognito,components:fp.components,version:fp.version,source:fp.source,session_id:sid,user_id:uid,event_type:route};\n // Idempotency key derived from (stable_hash, user_id, route), SHA-256-hashed\n // when WebCrypto is available so the user_id never appears in the platform's\n // stored idempotency_key column (raw fallback on non-secure contexts — the\n // middleware accepts both). NOTE: the middleware appends |Date.now() before\n // forwarding (submit-fp.ts), so the backend sees a unique value per\n // submission and dedups only PubSub redeliveries — NOT two distinct POSTs.\n // Cross-submitter / cross-reload dedup is client-side (the shared window\n // guard + sessionStorage above).\n var idem=fp.fingerprint_id+"|"+uid+"|"+route;\n function sendFP(idemKey){\n var xhr=new XMLHttpRequest();\n xhr.open("POST",pfx+"/submit-fp",true);\n xhr.setRequestHeader("Content-Type","application/json");\n xhr.setRequestHeader("X-Session-Id",sid);\n if(did)xhr.setRequestHeader("X-Device-Id",did);\n xhr.setRequestHeader("X-Idempotency-Key",idemKey);\n xhr.send(JSON.stringify(body));\n }\n if(window.crypto&&crypto.subtle&&window.TextEncoder){\n crypto.subtle.digest("SHA-256",new TextEncoder().encode(idem)).then(function(d){\n var a=new Uint8Array(d),s="";\n for(var i=0;i<a.length;i++){s+=("0"+a[i].toString(16)).slice(-2)}\n sendFP(s);\n }).catch(function(){sendFP(idem)});\n }else{sendFP(idem)}\n}\n\n// --- Collect fingerprint (loads fp.js if needed) then submit ---\nvar fpReady=false;\nfunction collectAndSubmit(){\n var uid=gC("__unshared_uid");\n if(!uid||isSentinelUid(uid))return;\n var cached=getFP();\n if(cached){submitFP(cached);return}\n if(!fpReady)return;\n try{\n var c=new UnsharedBrowser.UnsharedBrowser({baseUrl:""});\n c.collect({exclude:["timing","speech"]}).then(function(fp){setFP(fp);submitFP(fp)});\n }catch(e){}\n}\n\n// --- Load fp.js (always — browser caches it for 1h) ---\n// Submit cached FP immediately if available; load fp.js for fresh collection\nvar pageLoadSubmitted=false;\nvar _boot_uid=gC("__unshared_uid");\nif(getFP()&&_boot_uid&&!isSentinelUid(_boot_uid)){submitFP(getFP());pageLoadSubmitted=true;deferredCheck()}\nvar s=document.createElement("script");\ns.src=pfx+"/fp.js${r}";\ns.onload=function(){fpReady=true;${s}if(!pageLoadSubmitted){collectAndSubmit();deferredCheck()}};\ndocument.head.appendChild(s);\n\n// --- Deferred verdict check ---\n// After fingerprint submission, the backend processes the event async.\n// If the user was just flagged, the initial page load may have beaten\n// the verdict update. Re-check after a delay so newly flagged sessions\n// get caught without waiting for user interaction.\n// The endpoint always returns 200 so browsers don't log a scary red\n// network error — we inspect the body and dispatch the flagged event\n// ourselves when status==="flagged".\nfunction deferredCheck(){\n var uid=gC("__unshared_uid");\n if(!uid||isSentinelUid(uid))return;\n setTimeout(function(){\n try{fetch(pfx+"/status",{method:"GET",credentials:"same-origin"}).then(function(r){return r.json()}).then(function(b){if(b&&b.status==="flagged")emitFlagged(b)}).catch(function(){})}catch(e){}\n },500);\n}\n\n// --- SPA route change tracking (History API + popstate) ---\nvar oPush=history.pushState,oReplace=history.replaceState;\nhistory.pushState=function(){oPush.apply(this,arguments);try{collectAndSubmit()}catch(e){}};\nhistory.replaceState=function(){oReplace.apply(this,arguments);try{collectAndSubmit()}catch(e){}};\nwindow.addEventListener("popstate",function(){try{collectAndSubmit()}catch(e){}});\n\n// --- 403 interception: dispatch "unshared:flagged" event ---\nfunction emitFlagged(body){\n try{window.dispatchEvent(new CustomEvent("unshared:flagged",{detail:{email:body.email||""}}))}catch(e){}\n}\n\n// Patch fetch\nvar oFetch=window.fetch;\nif(oFetch){window.fetch=function(){return oFetch.apply(this,arguments).then(function(r){if(r.status===403){try{var cl=r.clone();cl.json().then(function(b){if(b&&b.error==="account_flagged")emitFlagged(b)}).catch(function(){})}catch(e){}}return r})}}\n\n// Patch XMLHttpRequest\nvar oXSend=XMLHttpRequest.prototype.send;\nXMLHttpRequest.prototype.send=function(){var x=this;x.addEventListener("load",function(){if(x.status===403){try{var b=JSON.parse(x.responseText);if(b&&b.error==="account_flagged")emitFlagged(b)}catch(e){}}});return oXSend.apply(this,arguments)};\n\n}catch(e){}\n})();\n<\/script>`}function escapeJavaScript(e){return e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/'/g,"\\'")}
|
|
1
|
+
export function generateFingerprintScript(e,t,n){const r=t?`?v=${escapeJavaScript(t)}`:"",s=n?.autoInterstitial?`\nvar _isdk=new UnsharedBrowser.UnsharedBrowser({baseUrl:""});\nwindow.addEventListener("unshared:flagged",function(){try{_isdk.showInterstitial({flowType:"${escapeJavaScript(n?.interstitialFlowType??"email_verification")}",onComplete:function(){try{location.reload()}catch(e){}}})}catch(e){}});`:"";return`<script>\n(function(){\ntry{\n// --- Bot drop (defense-in-depth) ---\n// Must be the first statement: we do not want to write cookies, localStorage,\n// session IDs, or any network requests for known-bot traffic. Mirrors the\n// regex in unshared-fingerprint-lib/src/detect/bot.ts and Node middleware\n// utils/is-bot.ts. Keep all three in sync.\nvar BOT_RE=/googlebot|bingbot|slurp|baiduspider|duckduckbot|yandex|sogou|exabot|ia_archiver|curl|wget|python-requests|python-urllib|axios|node-fetch|go-http-client|java\\/|libwww-perl|okhttp|apache-httpclient|http_request|httpie|headlesschrome|phantomjs|puppeteer|playwright|cypress|selenium|webdriver|electron|jsdom|vercel-screenshot|screenshot|prerender|lighthouse|chrome-lighthouse|pagespeed|gtmetrix|pingdom|nessus|nikto|sqlmap|burp|zap|qualys|openvas|nmap|masscan|facebookexternalhit|twitterbot|linkedinbot|whatsapp|telegrambot|slackbot|discordbot|bot|crawl|spider|scrape|fetch|scan/i;\nif(typeof navigator!=="undefined"&&navigator.userAgent&&BOT_RE.test(navigator.userAgent))return;\n\nvar pfx="${escapeJavaScript(e)}";\nvar SS_FP="__unshared_fp";\nvar SS_LAST_SUBMIT="__unshared_last_submit";\n\n// Dedup state: skip submit if (user_id + URL) matches last submission.\n// Modern SPAs (Next.js App Router, React Router, etc.) call replaceState\n// 3-5 times during hydration with the same URL — without this guard,\n// each call generates a redundant FP row with identical stable_hash.\n// Persisted to sessionStorage so hard reloads and framework double-boots\n// inside the same tab still dedupe (the in-memory value resets on reload).\nvar lastSubmitKey="";\ntry{lastSubmitKey=sessionStorage.getItem(SS_LAST_SUBMIT)||""}catch(e){}\n\n// Page-scoped dedup state SHARED with the frontend SDK (browser.ts getSharedDedup).\n// On a Tier 1 page both this inline script and the SDK submit; each holds its own\n// in-memory lastSubmitKey, so both could pass the check below and POST. window.__unshared\n// is read+written synchronously in submitFP (no await between), so whichever fires\n// first claims the uid|route key and the other no-ops — killing the read-before-write\n// race that doubled events. KEEP IN SYNC with browser.ts: namespace, lastKey, key formula.\nvar shared=(window.__unshared=window.__unshared||{});\n\n// --- Helpers ---\nfunction gC(n){var m=document.cookie.match(new RegExp("(?:^|; )"+n+"=([^;]*)"));return m?decodeURIComponent(m[1]):null}\nfunction sC(n,v,d){var e="";if(d){var dt=new Date();dt.setTime(dt.getTime()+d*864e5);e="; expires="+dt.toUTCString()}document.cookie=n+"="+encodeURIComponent(v)+e+"; path=/; SameSite=Lax"+(location.protocol==="https:"?"; Secure":"")}\nfunction uuid(){return(typeof crypto!=="undefined"&&crypto.randomUUID)?crypto.randomUUID():("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(c){var r=Math.random()*16|0;return(c==="x"?r:r&0x3|0x8).toString(16)}))}\n// Sentinel user IDs that must never be treated as real users. Mirrors\n// the Set in sentinel-user-id.ts — keep in sync. Empty string is handled\n// by the separate !uid checks below.\nvar SENTINEL_UIDS={"__pre_auth__":1,"anonymous":1,"guest":1,"undefined":1,"null":1};\nfunction isSentinelUid(v){return typeof v==="string"&&SENTINEL_UIDS.hasOwnProperty(v)}\n\n// --- Session + device IDs ---\n// Session ID is a UUID because it's supposed to be tab-scoped and random.\n// Device ID is intentionally NOT a UUID — Issue 9: random UUIDs wrote\n// meaningless device_ids to every fingerprint row. Instead we read the\n// stable fingerprint hash from localStorage if a previous submission\n// already persisted it; otherwise we leave did empty and let submitFP()\n// reconcile on the first successful collection. The Node middleware's\n// Issue 8 bootstrap-skip branch handles the empty-device_id window so we\n// never dispatch with a random or "unknown" value.\nvar sid=gC("__unshared_sid");\nif(!sid){sid=uuid();sC("__unshared_sid",sid,365)}\nvar did="";\ntry{did=localStorage.getItem("__unshared_device_id")||""}catch(e){}\nif(did){sC("__unshared_fp_id",did,365)}\n\n// --- Fingerprint cache (sessionStorage) ---\nfunction getFP(){try{var r=sessionStorage.getItem(SS_FP);return r?JSON.parse(r):null}catch(e){return null}}\nfunction setFP(fp){try{sessionStorage.setItem(SS_FP,JSON.stringify(fp))}catch(e){}}\n\n// --- Submit fingerprint to backend ---\nfunction submitFP(fp){\n var uid=gC("__unshared_uid");\n if(!uid||isSentinelUid(uid))return;\n // Issue 9: reconcile device_id to the stable fingerprint hash. This runs\n // before we send the X-Device-Id header so the very first submission\n // already carries the real value. Persist to localStorage so other tabs\n // (and future reloads) pick up the same stable ID without needing to\n // re-collect the fingerprint.\n if(fp.fingerprint_id){\n did=fp.fingerprint_id;\n try{localStorage.setItem("__unshared_device_id",did)}catch(e){}\n sC("__unshared_fp_id",did,365);\n }\n // event_type is the SPA route, not a fixed enum. Page-level event names\n // (page_load/route_change) collapsed every row into one of two buckets;\n // the URL is more useful for analytics and matches the frontend SDK.\n var route=(location.pathname||"/")+(location.search||"");\n var key=uid+"|"+route;\n // Check the shared window guard AND the in-memory key (last-key semantics, so an\n // SPA A->B->A revisit still submits the second A). The shared guard makes this\n // submitter and the frontend SDK see each other within the page.\n if(key===lastSubmitKey||shared.lastKey===key)return;\n shared.lastKey=key;\n lastSubmitKey=key;\n try{sessionStorage.setItem(SS_LAST_SUBMIT,key)}catch(e){}\n // collected_at is stamped fresh at submit time rather than carried from fp.timestamp,\n // because fp is cached per-tab in sessionStorage — reusing its original timestamp would\n // freeze collected_at at first load and drift against server created_at as the tab ages.\n // The server authoritatively overwrites this value again on ingress.\n var body={hash:fp.full_hash,stable_hash:fp.fingerprint_id,collected_at:(new Date()).toISOString(),is_incognito:fp.isIncognito,components:fp.components,version:fp.version,source:fp.source,session_id:sid,user_id:uid,event_type:route};\n // Idempotency key derived from (stable_hash, user_id, route), SHA-256-hashed\n // when WebCrypto is available so the user_id never appears in the platform's\n // stored idempotency_key column (raw fallback on non-secure contexts — the\n // middleware accepts both). NOTE: the middleware appends |Date.now() before\n // forwarding (submit-fp.ts), so the backend sees a unique value per\n // submission and dedups only PubSub redeliveries — NOT two distinct POSTs.\n // Cross-submitter / cross-reload dedup is client-side (the shared window\n // guard + sessionStorage above).\n var idem=fp.fingerprint_id+"|"+uid+"|"+route;\n function sendFP(idemKey){\n var xhr=new XMLHttpRequest();\n xhr.open("POST",pfx+"/submit-fp",true);\n xhr.setRequestHeader("Content-Type","application/json");\n xhr.setRequestHeader("X-Session-Id",sid);\n if(did)xhr.setRequestHeader("X-Device-Id",did);\n xhr.setRequestHeader("X-Idempotency-Key",idemKey);\n xhr.send(JSON.stringify(body));\n }\n if(window.crypto&&crypto.subtle&&window.TextEncoder){\n crypto.subtle.digest("SHA-256",new TextEncoder().encode(idem)).then(function(d){\n var a=new Uint8Array(d),s="";\n for(var i=0;i<a.length;i++){s+=("0"+a[i].toString(16)).slice(-2)}\n sendFP(s);\n }).catch(function(){sendFP(idem)});\n }else{sendFP(idem)}\n}\n\n// --- Collect fingerprint (loads fp.js if needed) then submit ---\nvar fpReady=false;\nfunction collectAndSubmit(){\n var uid=gC("__unshared_uid");\n if(!uid||isSentinelUid(uid))return;\n var cached=getFP();\n if(cached){submitFP(cached);return}\n if(!fpReady)return;\n try{\n var c=new UnsharedBrowser.UnsharedBrowser({baseUrl:""});\n c.collect({exclude:["timing","speech"]}).then(function(fp){setFP(fp);submitFP(fp)});\n }catch(e){}\n}\n\n// --- Load fp.js (always — browser caches it for 1h) ---\n// Submit cached FP immediately if available; load fp.js for fresh collection\nvar pageLoadSubmitted=false;\nvar _boot_uid=gC("__unshared_uid");\nif(getFP()&&_boot_uid&&!isSentinelUid(_boot_uid)){submitFP(getFP());pageLoadSubmitted=true;deferredCheck()}\nvar s=document.createElement("script");\ns.src=pfx+"/fp.js${r}";\ns.onload=function(){fpReady=true;${s}if(!pageLoadSubmitted){collectAndSubmit();deferredCheck()}};\ndocument.head.appendChild(s);\n\n// --- Deferred verdict check ---\n// After fingerprint submission, the backend processes the event async.\n// If the user was just flagged, the initial page load may have beaten\n// the verdict update. Re-check after a delay so newly flagged sessions\n// get caught without waiting for user interaction.\n// The endpoint always returns 200 so browsers don't log a scary red\n// network error — we inspect the body and dispatch the flagged event\n// ourselves when status==="flagged".\nfunction deferredCheck(){\n var uid=gC("__unshared_uid");\n if(!uid||isSentinelUid(uid))return;\n setTimeout(function(){\n try{fetch(pfx+"/status",{method:"GET",credentials:"same-origin"}).then(function(r){return r.json()}).then(function(b){if(b&&b.status==="flagged")emitFlagged(b)}).catch(function(){})}catch(e){}\n },500);\n}\n\n// --- SPA route change tracking (History API + popstate) ---\nvar oPush=history.pushState,oReplace=history.replaceState;\nhistory.pushState=function(){oPush.apply(this,arguments);try{collectAndSubmit()}catch(e){}};\nhistory.replaceState=function(){oReplace.apply(this,arguments);try{collectAndSubmit()}catch(e){}};\nwindow.addEventListener("popstate",function(){try{collectAndSubmit()}catch(e){}});\n\n// --- 403 interception: dispatch "unshared:flagged" event ---\nfunction emitFlagged(body){\n try{window.dispatchEvent(new CustomEvent("unshared:flagged",{detail:{email:body.email||""}}))}catch(e){}\n}\n\n// Patch fetch\nvar oFetch=window.fetch;\nif(oFetch){window.fetch=function(){return oFetch.apply(this,arguments).then(function(r){if(r.status===403){try{var cl=r.clone();cl.json().then(function(b){if(b&&b.error==="account_flagged")emitFlagged(b)}).catch(function(){})}catch(e){}}return r})}}\n\n// Patch XMLHttpRequest\nvar oXSend=XMLHttpRequest.prototype.send;\nXMLHttpRequest.prototype.send=function(){var x=this;x.addEventListener("load",function(){if(x.status===403){try{var b=JSON.parse(x.responseText);if(b&&b.error==="account_flagged")emitFlagged(b)}catch(e){}}});return oXSend.apply(this,arguments)};\n\n}catch(e){}\n})();\n<\/script>`}function escapeJavaScript(e){return JSON.stringify(e).slice(1,-1).replace(/<\/(script)/gi,"<\\/$1").replace(new RegExp("\u2028","g"),"\\u2028").replace(new RegExp("\u2029","g"),"\\u2029")}
|
|
@@ -6,6 +6,13 @@ import type { UnsharedResponse } from '../types';
|
|
|
6
6
|
* CSS, JS, etc.) pass through to the original write/end without buffering,
|
|
7
7
|
* avoiding unnecessary memory usage on large payloads.
|
|
8
8
|
*
|
|
9
|
+
* Compressed responses (Content-Encoding set) are always passed through
|
|
10
|
+
* untouched — the bytes are not HTML text so transforming them would corrupt
|
|
11
|
+
* the response.
|
|
12
|
+
*
|
|
13
|
+
* Bodies larger than MAX_INJECT_BYTES are passed through without injection to
|
|
14
|
+
* avoid unbounded memory usage.
|
|
15
|
+
*
|
|
9
16
|
* When res.end() is called on an HTML response, invokes the `transform`
|
|
10
17
|
* callback with the complete body buffer and the Content-Type header.
|
|
11
18
|
* The transform can return modified content or null to pass through unchanged.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function interceptResponse(n,t,
|
|
1
|
+
const MAX_INJECT_BYTES=2e6;export function interceptResponse(n,t,e){const f=e?.preventCaching??!1,o=n.write.bind(n),u=n.end.bind(n),r=n.writeHead.bind(n),c=[];let i=0,l=!1,s=null;function p(){n.write=o,n.end=u;for(const n of c)o(n);c.length=0}function y(){if(null!==s)return;const t=n.getHeader("content-type");null!=t&&(s=String(t).includes("text/html"),s&&n.getHeader("content-encoding")&&(s=!1),s||p())}n.writeHead=function(t,...e){return y(),f&&s&&(n.setHeader("Cache-Control","no-store"),n.removeHeader("ETag"),n.removeHeader("Last-Modified")),r(t,...e)},n.write=function(n,t,e){if(y(),!1===s)return o(n,t,e);if(null!=n){const f=Buffer.isBuffer(n)?n:Buffer.from(n,"string"==typeof t?t:"utf8");if(i+=f.length,c.push(f),i>2e6)return p(),"function"==typeof t&&t(null),"function"==typeof e&&e(null),!0}return"function"==typeof t&&t(null),"function"==typeof e&&e(null),!0},n.end=function(e,f,r){if(l)return n;if(l=!0,y(),!1===s)return u(e,f,r);if(null!=e){const n=Buffer.isBuffer(e)?e:Buffer.from(e,"string"==typeof f?f:"utf8");i+=n.length,c.push(n)}if(i>2e6){p();const n="function"==typeof f?f:r;return n?u(n):u()}const B=Buffer.concat(c),g=n.getHeader("content-type");let C;try{C=t(B,g)}catch{C=null}if(null!=C){const t=Buffer.isBuffer(C)?C:Buffer.from(C,"utf8");n.setHeader("Content-Length",t.length),n.removeHeader("Content-Encoding"),o(t)}else B.length>0&&o(B);const a="function"==typeof f?f:r;return a?u(a):u(),n}}
|
|
@@ -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,
|
|
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})}}}
|
|
@@ -6,6 +6,7 @@ export interface VerificationDependencies<TReq extends UnsharedRequest = Unshare
|
|
|
6
6
|
verdictCache: VerdictCache;
|
|
7
7
|
resolveEmailAddress?: (req: TReq) => string | undefined;
|
|
8
8
|
resolveDeviceId?: (req: TReq) => string | undefined;
|
|
9
|
+
resolveUserId?: (req: TReq) => string | undefined;
|
|
9
10
|
onError?: (error: unknown, context: {
|
|
10
11
|
operation: 'processUserEvent' | 'submitFingerprintEvent' | 'checkUser' | 'verifyTrigger' | 'verify';
|
|
11
12
|
userId?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{parseCookie}from"../utils/cookies";import{extractDeviceId}from"../utils/device-id";import{sendJson}from"../utils/http-helpers";export function handleVerifyTrigger(e){return async(r,
|
|
1
|
+
import{parseCookie}from"../utils/cookies";import{extractDeviceId}from"../utils/device-id";import{sendJson}from"../utils/http-helpers";export function handleVerifyTrigger(e){return async(r,i)=>{try{const s=resolveEmail(r,r.body??{},e.resolveEmailAddress);if(!s)return void sendJson(i,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}});const o=extractDeviceId(r,e.resolveDeviceId),n=parseCookie(r,"__unshared_fingerprint_id")||void 0,t=await e.client.triggerEmailVerification(s,o,{fingerprintId:n});t.success?sendJson(i,200,{success:!0,data:t.data}):sendJson(i,200,{success:!1,error:t.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}})}catch(r){e.onError&&e.onError(r,{operation:"verifyTrigger"}),sendJson(i,200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}})}}}export function handleVerify(e){return async(r,i)=>{try{const s=r.body??{},o=resolveEmail(r,s,e.resolveEmailAddress),n=s.code;if(!o||!n)return void sendJson(i,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}});const t=extractDeviceId(r,e.resolveDeviceId),c=parseCookie(r,"__unshared_fingerprint_id")||void 0,a=await e.client.verify(o,t,n,{fingerprintId:c});if(a.success){let s;try{s=e.resolveUserId?.(r)}catch{}s&&e.verdictCache.update(s,{isVerified:!0}),sendJson(i,200,{success:!0,data:{verified:!0}})}else sendJson(i,200,{success:!1,error:a.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}})}catch(r){e.onError&&e.onError(r,{operation:"verify"}),sendJson(i,200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}})}}}function resolveEmail(e,r,i){if(i)try{const r=i(e);if(r)return r}catch{}const s=parseCookie(e,"__unshared_email");if(s)return s;const o=r.email;return"string"==typeof o&&o?o:void 0}
|
|
@@ -3,6 +3,10 @@ export declare const ACCOUNT_FLAGGED_ERROR: "account_flagged";
|
|
|
3
3
|
* Standardized response body for a flagged, unverified user. The injected
|
|
4
4
|
* inline script keys off `error === 'account_flagged'` to emit the
|
|
5
5
|
* "unshared:flagged" event, so always use this helper for that body.
|
|
6
|
+
*
|
|
7
|
+
* The email is MASKED (`j***@domain`) — this body is browser-visible and reaches
|
|
8
|
+
* any same-origin script via `e.detail.email`. The SDK's own modal resolves the
|
|
9
|
+
* real identity server-side, so it never needs the full address here.
|
|
6
10
|
*/
|
|
7
11
|
export declare function flaggedResponse(email: string): {
|
|
8
12
|
readonly error: "account_flagged";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const ACCOUNT_FLAGGED_ERROR="account_flagged";export function flaggedResponse(
|
|
1
|
+
import{maskEmail}from"./mask-email";export const ACCOUNT_FLAGGED_ERROR="account_flagged";export function flaggedResponse(a){return{error:"account_flagged",email:maskEmail(a)}}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mask an email for client-facing surfaces (the `/status` body, the
|
|
3
|
+
* `account_flagged` 403 body, and thus the `unshared:flagged` event's
|
|
4
|
+
* `e.detail.email`). Keeps the first character of the local part and the full
|
|
5
|
+
* domain (`jason@unsharedlabs.com` → `j***@unsharedlabs.com`) so the value stays
|
|
6
|
+
* display-usable without exposing the full address to same-origin scripts.
|
|
7
|
+
* Returns `'***'` for malformed/empty input so nothing leaks.
|
|
8
|
+
*/
|
|
9
|
+
export declare function maskEmail(email: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function maskEmail(n){const t=n.lastIndexOf("@");return t<=0?"***":`${n[0]}***@${n.slice(t+1)}`}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{sha256Hex}from"../util";import{VerdictCache}from"../middleware/verdict-cache";import{RateLimitBackoff}from"../middleware/rate-limit-backoff";import{DispatchDedupe}from"../middleware/dispatch-dedupe";import{generateFingerprintScript}from"../middleware/injection/fingerprint-script";import{generateGatePage}from"../middleware/injection/gate-page";import{flaggedResponse}from"../middleware/utils/flagged-response";import{isHtmlContentType,isHtmlNavigation}from"../middleware/utils/content-type";import{shouldSkipPath}from"../middleware/utils/skip-paths";import{shouldIncludePath}from"../middleware/utils/include-path";import{isBot}from"../middleware/utils/is-bot";import{isSentinelUserId,SENTINEL_STICKINESS_TTL_MS}from"../middleware/utils/sentinel-user-id";import{parseCookieFromRequest,extractClientIpFromRequest,extractDeviceIdFromRequest,extractDeviceIdFromRequestOrUnknown,isSecureWebRequest,jsonResponse,emptyResponse,bodyResponse,mergeResponseHeaders}from"./web-helpers";const CHECK_USER_TIMEOUT_MS=500;export function createWebProtectionMiddleware(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");const{userId:s,emailAddress:r,routePrefix:n="/__unshared",corsOrigins:i,cacheTTL:o=6e4,skipPaths:a,includePathPrefix:d,sessionId:c,deviceId:u,fingerprintSdkBundle:l="",onFlagged:p,onError:m,onFailOpen:f,disableBotFilter:h=!1,checkUserTimeoutMs:_=CHECK_USER_TIMEOUT_MS,blockFlagged:g=!1,flaggedMode:R,autoInterstitial:I=!1,interstitialFlowType:v="email_verification"}=t,S=R??(g?"gate":void 0),y=I||"overlay"===S,w=e=>{if(f)try{f(e)}catch{}},C=Date.now().toString(36),A=void 0!==S||y,E="gate"===S?"flaggedMode 'gate' (blockFlagged)":"overlay"===S?"flaggedMode 'overlay'":"autoInterstitial";if(A&&!l)throw new Error(`[Unshared] ${E} requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the interstitial).`);if(A&&"/__unshared"!==n)throw new Error(`[Unshared] ${E} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const k="gate"===S?generateGatePage(n,C):"",F=new VerdictCache(o),T=new RateLimitBackoff,x=new DispatchDedupe,U=generateFingerprintScript(n,C,{autoInterstitial:y,interstitialFlowType:v}),q=`${n}/fp.js`,j=`${n}/submit-fp`,O=`${n}/verify-trigger`,D=`${n}/verify`,b=`${n}/status`,P=i?Array.isArray(i)?i:[i]:null;return async function(t,i){let o,f,g;try{const e=new URL(t.url);o=e.pathname,f=e.search}catch{return i(t)}if(o.startsWith(n+"/")){const n=function(e){if(!P)return{};const t=e.headers.get("origin")??"",s=P.includes("*");return s||P.includes(t)?{"Access-Control-Allow-Origin":s?"*":t,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id","Access-Control-Allow-Credentials":"true"}:{}}(t);if("OPTIONS"===t.method)return emptyResponse(204,n);if("GET"===t.method&&o===q)return l?bodyResponse(200,l,{...n,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600"}):jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},n);if("POST"===t.method&&(o===j||o===O||o===D)){let i;try{i=await t.json()}catch{return jsonResponse(400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"Request body is not valid JSON."}},n)}return o===j?handleSubmitFp(t,i,{client:e,verdictCache:F,rateLimitBackoff:T,dispatchDedupe:x,resolveUserId:s,resolveEmailAddress:r,resolveSessionId:c,resolveDeviceId:u,disableBotFilter:h,onError:m},n):o===O?handleVerifyTriggerWeb(t,i,{client:e,verdictCache:F,resolveEmailAddress:r,resolveDeviceId:u,onError:m},n):handleVerifyWeb(t,i,{client:e,verdictCache:F,resolveEmailAddress:r,resolveDeviceId:u,onError:m},n)}if("GET"===t.method&&o===b){let i;try{i=s(t)}catch{}if(!i)return jsonResponse(200,{status:"anonymous"},n);const o=resolveEmail(t,r);let a=F.get(i);if((!a||F.isStale(i))&&o&&!T.isPaused()&&!F.isRefreshing(i)){F.markRefreshing(i);try{const s=extractDeviceIdFromRequest(t,u),r=parseCookieFromRequest(t,"__unshared_fingerprint_id")||void 0,n=extractSessionIdFromRequest(t,c),d=s??r??"unknown";await fetchAndCacheVerdict(e,F,i,o,d,r,n,_,(e,t)=>w({operation:"checkUser",reason:e,status:t,userId:i,emailAddress:o})),a=F.get(i)}catch(e){m&&m(e,{operation:"checkUser",userId:i,emailAddress:o}),w({operation:"checkUser",reason:"exception",userId:i,emailAddress:o})}finally{F.clearRefreshing(i)}}return a&&a.isFlagged&&!a.isVerified&&p&&o?jsonResponse(200,{status:"flagged",email:o},n):jsonResponse(200,{status:"ok"},n)}return jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},n)}if(shouldSkipPath(o,a))return i(t);if(!shouldIncludePath(o,d))return injectIntoHtmlResponse(await i(t),U);try{g=s(t)}catch{}if(isSentinelUserId(g)){const e=parseCookieFromRequest(t,"__unshared_uid"),s=parseCookieFromRequest(t,"__unshared_uid_at"),r=s?Number(s):NaN,n=Number.isFinite(r)&&Date.now()-r<=SENTINEL_STICKINESS_TTL_MS;g=e&&n?e:void 0}if(!g){const e=isSecureWebRequest(t)?"; Secure":"",s=[`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`];return injectIntoHtmlResponse(await i(t),U,s)}const R=resolveEmail(t,r),I=[],v=isSecureWebRequest(t)?"; Secure":"";if(I.push(`__unshared_uid=${encodeURIComponent(g)}; Path=/; SameSite=Lax${v}`),I.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${v}`),R&&I.push(`__unshared_email=${encodeURIComponent(R)}; HttpOnly; Path=/; SameSite=Lax${v}`),!R)return injectIntoHtmlResponse(await i(t),U,I);const y=extractSessionIdFromRequest(t,c),C=extractDeviceIdFromRequest(t,u),A=parseCookieFromRequest(t,"__unshared_fingerprint_id")||void 0,E=t.headers.get("user-agent")??"",$=extractClientIpFromRequest(t),N=C??A;if(!h&&isBot(E))return i(t);let H=F.get(g);if(H)F.isStale(g)&&!F.isRefreshing(g)&&(F.markRefreshing(g),fetchAndCacheVerdict(e,F,g,R,N??"unknown",A,y,_,(e,t)=>w({operation:"checkUser",reason:e,status:t,userId:g,emailAddress:R})).catch(()=>w({operation:"checkUser",reason:"exception",userId:g,emailAddress:R})).finally(()=>F.clearRefreshing(g)));else try{H=await fetchAndCacheVerdict(e,F,g,R,N??"unknown",A,y,_,(e,t)=>w({operation:"checkUser",reason:e,status:t,userId:g,emailAddress:R}))}catch{return w({operation:"checkUser",reason:"exception",userId:g,emailAddress:R}),injectIntoHtmlResponse(await i(t),U,I)}const L=H.isFlagged&&!H.isVerified;if("gate"===S&&L)return isHtmlNavigation(t.method,t.headers.get("accept")??void 0)?bodyResponse(200,k,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}):jsonResponse(403,flaggedResponse(R));if("overlay"===S&&L)return isHtmlNavigation(t.method,t.headers.get("accept")??void 0)?injectIntoHtmlResponse(await i(t),U,I):jsonResponse(403,flaggedResponse(R));if(H.isFlagged&&!H.isVerified&&p)try{const e=await p({userId:g,emailAddress:R,verdict:H,request:t});if(e)return injectIntoHtmlResponse(e,U,I)}catch(e){m&&m(e,{operation:"checkUser",userId:g,emailAddress:R})}return H.isFlagged||"unknown"===y||!N||T.isPaused()||dispatchUserEvent(e,F,T,x,{userId:g,emailAddress:R,sessionId:y,deviceId:N,fingerprintId:A,userAgent:E,ipAddress:$,eventType:o+f},m),injectIntoHtmlResponse(await i(t),U,I)}}async function injectIntoHtmlResponse(e,t,s){const r=e.headers.get("content-type");if(!isHtmlContentType(r??void 0)){if(!s||0===s.length)return e;const t=mergeResponseHeaders(e.headers,void 0,s);return new Response(e.body,{status:e.status,statusText:e.statusText,headers:t})}const n=await e.text(),i=n.lastIndexOf("</body>"),o=-1===i?n+t:n.slice(0,i)+t+n.slice(i),a=mergeResponseHeaders(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(o).length)},s);return a.delete("ETag"),a.delete("Last-Modified"),a.delete("Content-Encoding"),new Response(o,{status:e.status,statusText:e.statusText,headers:a})}function resolveEmail(e,t){if(t)try{const s=t(e);if(s)return s}catch{}const s=parseCookieFromRequest(e,"__unshared_email");if(s)return s}function resolveEmailWithBody(e,t,s){const r=resolveEmail(e,s);if(r)return r;const n=t.email;return"string"==typeof n&&n?n:void 0}function extractSessionIdFromRequest(e,t){if(t)try{const s=t(e);if(s)return s}catch{}return parseCookieFromRequest(e,"__unshared_sid")??"unknown"}function dispatchUserEvent(e,t,s,r,n,i){r.mark(n.userId,n.eventType),e.processUserEvent({eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,sessionHash:n.sessionId,userAgent:n.userAgent}).then(e=>{e.success&&e.data?.analysis&&t.update(n.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&s.pause(1e3*e.error.retryAfter)}).catch(e=>{i&&i(e,{operation:"processUserEvent",userId:n.userId,emailAddress:n.emailAddress})})}async function fetchAndCacheVerdict(e,t,s,r,n,i,o,a=CHECK_USER_TIMEOUT_MS,d){const c={};let u;n&&"unknown"!==n&&(c.deviceId=n),i&&(c.fingerprintId=i);const l=await Promise.race([e.checkUser(r,c),new Promise(e=>{u=setTimeout(()=>e(null),a)})]);if(clearTimeout(u),!l)return d?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:r,sessionId:o,cachedAt:0,ttl:0};l.failedOpen&&d?.("http_error",l.failedOpen.status);const p=l.data?.is_user_flagged??!1;return t.set(s,{isFlagged:p,isVerified:!1,emailAddress:r,sessionId:o}),t.get(s)}async function handleSubmitFp(e,t,s,r){try{const n={full_hash:t.hash??"",fingerprint_id:t.stable_hash??"",timestamp:t.collected_at??(new Date).toISOString(),isIncognito:t.is_incognito??!1,components:t.components??{},version:t.version??"inline-1.0.0"};let i,o,a;try{const t=s.resolveUserId(e);t&&!isSentinelUserId(t)&&(i=t)}catch{}if(!i){const e="string"==typeof t.user_id?t.user_id:void 0;e&&!isSentinelUserId(e)&&(i=e)}if(!i){const t=parseCookieFromRequest(e,"__unshared_uid");t&&!isSentinelUserId(t)&&(i=t)}try{o=s.resolveEmailAddress?s.resolveEmailAddress(e):void 0}catch{}o=o??parseCookieFromRequest(e,"__unshared_email")??t.email??void 0;try{a=s.resolveSessionId?s.resolveSessionId(e):void 0}catch{}a=a??t.session_id??parseCookieFromRequest(e,"__unshared_sid");const d=extractClientIpFromRequest(e),c=e.headers.get("user-agent")??"";if(!s.disableBotFilter&&isBot(c))return jsonResponse(200,{success:!0},r);const u=(n.fingerprint_id&&n.fingerprint_id.length>0?n.fingerprint_id:void 0)??extractDeviceIdFromRequestOrUnknown(e,s.resolveDeviceId),l=n.fingerprint_id||void 0,p=n.full_hash||void 0,m=isSecureWebRequest(e)?"; Secure":"",f=[];if(p&&!parseCookieFromRequest(e,"__unshared_fingerprint_id")&&f.push(`__unshared_fingerprint_id=${encodeURIComponent(p)}; HttpOnly; Path=/; SameSite=Lax${m}`),l){const t=parseCookieFromRequest(e,"__unshared_fp_id");t&&t===l||f.push(`__unshared_fp_id=${encodeURIComponent(l)}; Path=/; SameSite=Lax; Max-Age=31536000${m}`)}let h;if(o&&!parseCookieFromRequest(e,"__unshared_email")&&f.push(`__unshared_email=${encodeURIComponent(o)}; HttpOnly; Path=/; SameSite=Lax${m}`),"string"==typeof t.event_type&&t.event_type)h=t.event_type;else{const t=e.headers.get("referer")??e.headers.get("referrer");let s="unknown";if(t)try{const e=new URL(t);s=(e.pathname||"/")+(e.search||"")}catch{}h=s}const _=e.headers.get("x-idempotency-key")||void 0,g=Date.now(),R=_?`${_}|${g}`:l&&i?`${sha256Hex(`${l}|${i}|${h}`)}|${g}`:void 0;i&&s.client.submitFingerprintEvent(n,{userId:i,emailAddress:o,sessionHash:a,eventType:h,ipAddress:d,userAgent:c,idempotencyKey:R}).catch(e=>{s.onError&&s.onError(e,{operation:"submitFingerprintEvent",userId:i,emailAddress:o})}),i&&o&&!s.rateLimitBackoff.isPaused()&&!s.dispatchDedupe.wasRecentlyDispatched(i,h)&&s.client.processUserEvent({eventType:h,userId:i,emailAddress:o,ipAddress:d,deviceId:u,fingerprintId:l,sessionHash:a??"unknown",userAgent:c}).then(e=>{e.success&&e.data?.analysis&&s.verdictCache.update(i,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&s.rateLimitBackoff.pause(1e3*e.error.retryAfter)}).catch(e=>{s.onError&&s.onError(e,{operation:"processUserEvent",userId:i,emailAddress:o})});const I={...r,"Content-Type":"application/json"},v=new Response(JSON.stringify({success:!0}),{status:200,headers:I});for(const e of f)v.headers.append("Set-Cookie",e);return v}catch{return jsonResponse(200,{success:!0},r)}}async function handleVerifyTriggerWeb(e,t,s,r){try{const n=resolveEmailWithBody(e,t??{},s.resolveEmailAddress);if(!n)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},r);const i=extractDeviceIdFromRequestOrUnknown(e,s.resolveDeviceId),o=parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,a=await s.client.triggerEmailVerification(n,i,{fingerprintId:o});return a.success?jsonResponse(200,{success:!0,data:a.data},r):jsonResponse(200,{success:!1,error:a.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},r)}catch(e){return s.onError&&s.onError(e,{operation:"verifyTrigger"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},r)}}async function handleVerifyWeb(e,t,s,r){try{const n=resolveEmailWithBody(e,t??{},s.resolveEmailAddress),i=t?.code;if(!n||!i)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}},r);const o=extractDeviceIdFromRequestOrUnknown(e,s.resolveDeviceId),a=parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,d=await s.client.verify(n,o,i,{fingerprintId:a});if(d.success){const t=parseCookieFromRequest(e,"__unshared_uid");return t&&s.verdictCache.update(t,{isVerified:!0}),jsonResponse(200,{success:!0,data:{verified:!0}},r)}return jsonResponse(200,{success:!1,error:d.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}},r)}catch(e){return s.onError&&s.onError(e,{operation:"verify"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},r)}}
|
|
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=500;export function createWebProtectionMiddleware(e,s){if(!s.userId)throw new Error("[Unshared] userId resolver is required");const{userId:t,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"}=s,S=R??(g?"gate":void 0),y=I||"overlay"===S,w=e=>{if(f)try{f(e)}catch{}},C=Date.now().toString(36),A=void 0!==S||y,k="gate"===S?"flaggedMode 'gate' (blockFlagged)":"overlay"===S?"flaggedMode 'overlay'":"autoInterstitial";if(A&&!l)throw new Error(`[Unshared] ${k} requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the interstitial).`);if(A&&"/__unshared"!==n)throw new Error(`[Unshared] ${k} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const E="gate"===S?generateGatePage(n,C):"",F=new VerdictCache(o),T=new RateLimitBackoff,x=new DispatchDedupe,U=generateFingerprintScript(n,C,{autoInterstitial:y,interstitialFlowType:v}),q=`${n}/fp.js`,j=`${n}/submit-fp`,O=`${n}/verify-trigger`,D=`${n}/verify`,b=`${n}/status`,P=i?Array.isArray(i)?i:[i]:null;return async function(s,i){let o,f,g;try{const e=new URL(s.url);o=e.pathname,f=e.search}catch{return i(s)}if(o.startsWith(n+"/")){const n=function(e){if(!P)return{};const s=e.headers.get("origin")??"",t=P.includes("*");return t||P.includes(s)?{"Access-Control-Allow-Origin":t?"*":s,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id","Access-Control-Allow-Credentials":"true"}:{}}(s);if("OPTIONS"===s.method)return emptyResponse(204,n);if("GET"===s.method&&o===q)return l?bodyResponse(200,l,{...n,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600"}):jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},n);if("POST"===s.method&&(o===j||o===O||o===D)){let i;try{i=await s.json()}catch{return jsonResponse(400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"Request body is not valid JSON."}},n)}return o===j?handleSubmitFp(s,i,{client:e,verdictCache:F,rateLimitBackoff:T,dispatchDedupe:x,resolveUserId:t,resolveEmailAddress:r,resolveSessionId:c,resolveDeviceId:u,disableBotFilter:h,onError:m},n):o===O?handleVerifyTriggerWeb(s,i,{client:e,verdictCache:F,resolveEmailAddress:r,resolveDeviceId:u,onError:m},n):handleVerifyWeb(s,i,{client:e,verdictCache:F,resolveEmailAddress:r,resolveDeviceId:u,onError:m},n)}if("GET"===s.method&&o===b){let i;try{i=t(s)}catch{}if(!i)return jsonResponse(200,{status:"anonymous"},n);const o=resolveEmail(s,r);let a=F.get(i);if((!a||F.isStale(i))&&o&&!T.isPaused()&&!F.isRefreshing(i)){F.markRefreshing(i);try{const t=extractDeviceIdFromRequest(s,u),r=parseCookieFromRequest(s,"__unshared_fingerprint_id")||void 0,n=extractSessionIdFromRequest(s,c),d=t??r??"unknown";await fetchAndCacheVerdict(e,F,i,o,d,r,n,_,(e,s)=>w({operation:"checkUser",reason:e,status:s,userId:i,emailAddress:o})),a=F.get(i)}catch(e){m&&m(e,{operation:"checkUser",userId:i,emailAddress:o}),w({operation:"checkUser",reason:"exception",userId:i,emailAddress:o})}finally{F.clearRefreshing(i)}}return a&&a.isFlagged&&!a.isVerified&&p&&o?jsonResponse(200,{status:"flagged",email: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(s);if(!shouldIncludePath(o,d))return injectIntoHtmlResponse(await i(s),U);try{g=t(s)}catch{}if(isSentinelUserId(g)){const e=parseCookieFromRequest(s,"__unshared_uid"),t=parseCookieFromRequest(s,"__unshared_uid_at"),r=t?Number(t):NaN,n=Number.isFinite(r)&&Date.now()-r<=SENTINEL_STICKINESS_TTL_MS;g=e&&n?e:void 0}if(!g){const e=isSecureWebRequest(s)?"; 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(s),U,t)}const R=resolveEmail(s,r),I=[],v=isSecureWebRequest(s)?"; 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(s),U,I);const y=extractSessionIdFromRequest(s,c),C=extractDeviceIdFromRequest(s,u),A=parseCookieFromRequest(s,"__unshared_fingerprint_id")||void 0,k=s.headers.get("user-agent")??"",$=extractClientIpFromRequest(s),N=C??A;if(!h&&isBot(k))return i(s);let H=F.get(g);if(H)F.isStale(g)&&!F.isRefreshing(g)&&(F.markRefreshing(g),fetchAndCacheVerdict(e,F,g,R,N??"unknown",A,y,_,(e,s)=>w({operation:"checkUser",reason:e,status:s,userId:g,emailAddress:R})).catch(()=>w({operation:"checkUser",reason:"exception",userId:g,emailAddress:R})).finally(()=>F.clearRefreshing(g)));else try{H=await fetchAndCacheVerdict(e,F,g,R,N??"unknown",A,y,_,(e,s)=>w({operation:"checkUser",reason:e,status:s,userId:g,emailAddress:R}))}catch{return w({operation:"checkUser",reason:"exception",userId:g,emailAddress:R}),injectIntoHtmlResponse(await i(s),U,I)}const L=H.isFlagged&&!H.isVerified;if("gate"===S&&L)return isHtmlNavigation(s.method,s.headers.get("accept")??void 0)?bodyResponse(200,E,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}):jsonResponse(403,flaggedResponse(R));if("overlay"===S&&L)return isHtmlNavigation(s.method,s.headers.get("accept")??void 0)?injectIntoHtmlResponse(await i(s),U,I):jsonResponse(403,flaggedResponse(R));if(H.isFlagged&&!H.isVerified&&p)try{const e=await p({userId:g,emailAddress:R,verdict:H,request:s});if(e)return injectIntoHtmlResponse(e,U,I)}catch(e){m&&m(e,{operation:"checkUser",userId:g,emailAddress:R})}return H.isFlagged||"unknown"===y||!N||T.isPaused()||dispatchUserEvent(e,F,T,x,{userId:g,emailAddress:R,sessionId:y,deviceId:N,fingerprintId:A,userAgent:k,ipAddress:$,eventType:o+f},m),injectIntoHtmlResponse(await i(s),U,I)}}async function injectIntoHtmlResponse(e,s,t){const r=e.headers.get("content-type");if(!isHtmlContentType(r??void 0)){if(!t||0===t.length)return e;const s=mergeResponseHeaders(e.headers,void 0,t);return new Response(e.body,{status:e.status,statusText:e.statusText,headers:s})}const n=await e.text(),i=n.lastIndexOf("</body>"),o=-1===i?n+s:n.slice(0,i)+s+n.slice(i),a=mergeResponseHeaders(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(o).length)},t);return a.delete("ETag"),a.delete("Last-Modified"),a.delete("Content-Encoding"),new Response(o,{status:e.status,statusText:e.statusText,headers:a})}function resolveEmail(e,s){if(s)try{const t=s(e);if(t)return t}catch{}const t=parseCookieFromRequest(e,"__unshared_email");if(t)return t}function resolveEmailWithBody(e,s,t){const r=resolveEmail(e,t);if(r)return r;const n=s.email;return"string"==typeof n&&n?n:void 0}function extractSessionIdFromRequest(e,s){if(s)try{const t=s(e);if(t)return t}catch{}return parseCookieFromRequest(e,"__unshared_sid")??"unknown"}function dispatchUserEvent(e,s,t,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&&s.update(n.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.pause(1e3*e.error.retryAfter)}).catch(e=>{i&&i(e,{operation:"processUserEvent",userId:n.userId,emailAddress:n.emailAddress})})}async function fetchAndCacheVerdict(e,s,t,r,n,i,o,a=CHECK_USER_TIMEOUT_MS,d){const c={};let u;n&&"unknown"!==n&&(c.deviceId=n),i&&(c.fingerprintId=i);const l=await Promise.race([e.checkUser(r,c),new Promise(e=>{u=setTimeout(()=>e(null),a)})]);if(clearTimeout(u),!l)return d?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:r,sessionId:o,cachedAt:0,ttl:0};l.failedOpen&&d?.("http_error",l.failedOpen.status);const p=l.data?.is_user_flagged??!1;return s.set(t,{isFlagged:p,isVerified:!1,emailAddress:r,sessionId:o}),s.get(t)}async function handleSubmitFp(e,s,t,r){try{const 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"};let i,o,a;try{const s=t.resolveUserId(e);s&&!isSentinelUserId(s)&&(i=s)}catch{}if(!i){const e="string"==typeof s.user_id?s.user_id:void 0;e&&!isSentinelUserId(e)&&(i=e)}if(!i){const s=parseCookieFromRequest(e,"__unshared_uid");s&&!isSentinelUserId(s)&&(i=s)}try{o=t.resolveEmailAddress?t.resolveEmailAddress(e):void 0}catch{}o=o??parseCookieFromRequest(e,"__unshared_email")??s.email??void 0;try{a=t.resolveSessionId?t.resolveSessionId(e):void 0}catch{}a=a??s.session_id??parseCookieFromRequest(e,"__unshared_sid");const d=extractClientIpFromRequest(e),c=e.headers.get("user-agent")??"";if(!t.disableBotFilter&&isBot(c))return jsonResponse(200,{success:!0},r);const u=(n.fingerprint_id&&n.fingerprint_id.length>0?n.fingerprint_id:void 0)??extractDeviceIdFromRequestOrUnknown(e,t.resolveDeviceId),l=n.fingerprint_id||void 0,p=n.full_hash||void 0,m=isSecureWebRequest(e)?"; Secure":"",f=[];if(p&&!parseCookieFromRequest(e,"__unshared_fingerprint_id")&&f.push(`__unshared_fingerprint_id=${encodeURIComponent(p)}; HttpOnly; Path=/; SameSite=Lax${m}`),l){const s=parseCookieFromRequest(e,"__unshared_fp_id");s&&s===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 s.event_type&&s.event_type)h=s.event_type;else{const s=e.headers.get("referer")??e.headers.get("referrer");let t="unknown";if(s)try{const e=new URL(s);t=(e.pathname||"/")+(e.search||"")}catch{}h=t}const _=e.headers.get("x-idempotency-key")||void 0,g=Date.now(),R=_?`${_}|${g}`:l&&i?`${sha256Hex(`${l}|${i}|${h}`)}|${g}`:void 0;i&&t.client.submitFingerprintEvent(n,{userId:i,emailAddress:o,sessionHash:a,eventType:h,ipAddress:d,userAgent:c,idempotencyKey:R}).catch(e=>{t.onError&&t.onError(e,{operation:"submitFingerprintEvent",userId:i,emailAddress:o})}),i&&o&&!t.rateLimitBackoff.isPaused()&&!t.dispatchDedupe.wasRecentlyDispatched(i,h)&&t.client.processUserEvent({eventType:h,userId:i,emailAddress:o,ipAddress:d,deviceId:u,fingerprintId:l,sessionHash:a??"unknown",userAgent:c}).then(e=>{e.success&&e.data?.analysis&&t.verdictCache.update(i,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.rateLimitBackoff.pause(1e3*e.error.retryAfter)}).catch(e=>{t.onError&&t.onError(e,{operation:"processUserEvent",userId:i,emailAddress:o})});const I={...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,s,t,r){try{const n=resolveEmailWithBody(e,s??{},t.resolveEmailAddress);if(!n)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},r);const i=extractDeviceIdFromRequestOrUnknown(e,t.resolveDeviceId),o=parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,a=await t.client.triggerEmailVerification(n,i,{fingerprintId:o});return a.success?jsonResponse(200,{success:!0,data:a.data},r):jsonResponse(200,{success:!1,error:a.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},r)}catch(e){return t.onError&&t.onError(e,{operation:"verifyTrigger"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},r)}}async function handleVerifyWeb(e,s,t,r){try{const n=resolveEmailWithBody(e,s??{},t.resolveEmailAddress),i=s?.code;if(!n||!i)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}},r);const o=extractDeviceIdFromRequestOrUnknown(e,t.resolveDeviceId),a=parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,d=await t.client.verify(n,o,i,{fingerprintId:a});if(d.success){const s=parseCookieFromRequest(e,"__unshared_uid");return s&&t.verdictCache.update(s,{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 t.onError&&t.onError(e,{operation:"verify"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},r)}}
|
|
@@ -26,7 +26,7 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
|
|
|
26
26
|
includePathPrefix?: string[];
|
|
27
27
|
/** Skip the bot/crawler UA filter. Set to true in test environments so automated browsers (Playwright, Puppeteer, etc.) can observe verdicts. @default false */
|
|
28
28
|
disableBotFilter?: boolean;
|
|
29
|
-
/** Hard timeout (ms) for checkUser on cache miss. Fails open on timeout. @default
|
|
29
|
+
/** Hard timeout (ms) for checkUser on cache miss. Fails open on timeout. @default 1500 */
|
|
30
30
|
checkUserTimeoutMs?: number;
|
|
31
31
|
/** Resolves a custom session ID. Falls back to __unshared_sid cookie. */
|
|
32
32
|
sessionId?: (req: TReq) => string | undefined;
|
|
@@ -127,14 +127,15 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
|
|
|
127
127
|
* it is invoked best-effort and never blocks the request.
|
|
128
128
|
*
|
|
129
129
|
* `reason`: 'timeout' (exceeded checkUserTimeoutMs), 'http_error' (non-2xx
|
|
130
|
-
* masked into clean),
|
|
130
|
+
* masked into clean), 'exception' (checkUser threw), or 'no_device_id'
|
|
131
|
+
* (no device/fingerprint id yet — bootstrap window). `status` is present
|
|
131
132
|
* for 'http_error' (0 = network/transport).
|
|
132
133
|
*/
|
|
133
134
|
onFailOpen?: (context: FailOpenContext) => void;
|
|
134
135
|
}
|
|
135
136
|
export interface FailOpenContext {
|
|
136
137
|
operation: 'checkUser';
|
|
137
|
-
reason: 'timeout' | 'http_error' | 'exception';
|
|
138
|
+
reason: 'timeout' | 'http_error' | 'exception' | 'no_device_id';
|
|
138
139
|
status?: number;
|
|
139
140
|
userId?: string;
|
|
140
141
|
emailAddress?: string;
|
|
@@ -142,4 +143,6 @@ export interface FailOpenContext {
|
|
|
142
143
|
export type { Verdict };
|
|
143
144
|
export { VerdictCache };
|
|
144
145
|
export { flaggedResponse, ACCOUNT_FLAGGED_ERROR } from './utils/flagged-response';
|
|
145
|
-
export declare function unsharedBoundToUser<TReq extends UnsharedRequest = UnsharedRequest>(client: UnsharedClient, config: ProtectionConfig<TReq>): (req: TReq, res: UnsharedResponse, next: UnsharedNextFunction) => void
|
|
146
|
+
export declare function unsharedBoundToUser<TReq extends UnsharedRequest = UnsharedRequest>(client: UnsharedClient, config: ProtectionConfig<TReq>): ((req: TReq, res: UnsharedResponse, next: UnsharedNextFunction) => void) & {
|
|
147
|
+
destroy: () => void;
|
|
148
|
+
};
|
package/dist/middleware/index.js
CHANGED
|
@@ -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"),skip_paths_1=require("./utils/skip-paths"),include_path_1=require("./utils/include-path"),is_bot_1=require("./utils/is-bot"),client_ip_1=require("./utils/client-ip"),cookies_1=require("./utils/cookies"),device_id_1=require("./utils/device-id"),secure_1=require("./utils/secure"),sentinel_user_id_1=require("./utils/sentinel-user-id");var flagged_response_2=require("./utils/flagged-response");Object.defineProperty(exports,"flaggedResponse",{enumerable:!0,get:function(){return flagged_response_2.flaggedResponse}}),Object.defineProperty(exports,"ACCOUNT_FLAGGED_ERROR",{enumerable:!0,get:function(){return flagged_response_2.ACCOUNT_FLAGGED_ERROR}});const CHECK_USER_TIMEOUT_MS=500;function unsharedBoundToUser(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");if(!t.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:r,emailAddress:i,routePrefix:s="/__unshared",corsOrigins:n,cacheTTL:o=6e4,skipPaths:a,includePathPrefix:d,disableBotFilter:c=!1,checkUserTimeoutMs:u=CHECK_USER_TIMEOUT_MS,sessionId:l,deviceId:_,onFlagged:p,onError:h,onFailOpen:f,blockFlagged:g=!1,flaggedMode:v,autoInterstitial:m=!1,interstitialFlowType:I="email_verification"}=t,k=v??(g?"gate":void 0),y=m||"overlay"===k,C=e=>{if(f)try{f(e)}catch{}},S=new verdict_cache_1.VerdictCache(o),A=new rate_limit_backoff_1.RateLimitBackoff,x=new dispatch_dedupe_1.DispatchDedupe,b=Date.now().toString(36),E=(0,fingerprint_script_1.generateFingerprintScript)(s,b,{autoInterstitial:y,interstitialFlowType:I});let T="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");T=(0,fs_1.readFileSync)(e,"utf8")}catch{}const U=(0,fp_bundle_source_1.createFingerprintBundleSource)(),w=void 0!==k||y,q="gate"===k?"flaggedMode 'gate' (blockFlagged)":"overlay"===k?"flaggedMode 'overlay'":"autoInterstitial";if(w&&!T)throw new Error(`[Unshared] ${q} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(w&&"/__unshared"!==s)throw new Error(`[Unshared] ${q} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const F="gate"===k?(0,gate_page_1.generateGatePage)(s,b):"",O=(0,submit_fp_1.handleSubmitFingerprint)({client:e,verdictCache:S,rateLimitBackoff:A,dispatchDedupe:x,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:l,resolveDeviceId:_,disableBotFilter:c,onError:h}),j=(0,verify_1.handleVerifyTrigger)({client:e,verdictCache:S,resolveEmailAddress:i,resolveDeviceId:_,onError:h}),M=(0,verify_1.handleVerify)({client:e,verdictCache:S,resolveEmailAddress:i,resolveDeviceId:_,onError:h}),P=(0,interstitial_1.handleGetInterstitialFlow)({client:e}),$=n?Array.isArray(n)?n:[n]:null,D=`${s}/fp.js`,N=`${s}/fingerprint.js`,R=`${s}/submit-fp`,L=`${s}/verify-trigger`,V=`${s}/verify`,G=`${s}/status`,B=`${s}/interstitial-flow`;return function(t,n,o){const f=(0,http_helpers_1.getRequestPath)(t.url),g=t.url||f;if(f.startsWith(s+"/")){if(function(e,t){if(!$)return;const r=e.headers.origin??"",i=$.includes("*");(i||$.includes(r))&&(t.setHeader("Access-Control-Allow-Origin",i?"*":r),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,n),"OPTIONS"===t.method)return void(0,http_helpers_1.sendEmpty)(n,204);if("GET"===t.method&&f===D)return n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBody)(n,200,T);if("GET"===t.method&&f===N){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.sendBody)(n,200,e)):void(0,http_helpers_1.sendBody)(n,503,"/* fingerprint agent unavailable */")}if("POST"===t.method&&(f===R||f===L||f===V))return void 0===t.body?void(0,http_helpers_1.sendJson)(n,400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"req.body is undefined. Mount a JSON body-parsing middleware (e.g., express.json()) before the Unshared middleware."}}):f===R?void O(t,n):f===L?void j(t,n):void M(t,n);if("GET"===t.method&&f===B)return void P(t,n);if("GET"===t.method&&f===G){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&&!A.isPaused()&&!S.isRefreshing(s)){S.markRefreshing(s);try{const i=(0,device_id_1.extractDeviceIdOrUndefined)(t,_),n=extractFingerprintId(t),a=extractSessionId(t,l),d=i??n??"unknown";await fetchAndCacheVerdict(e,S,s,o,d,n,a,u,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:s,emailAddress:o})),r=S.get(s)}catch(e){h&&h(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&&p&&o?(0,http_helpers_1.sendJson)(n,200,{status:"flagged",email:o}):(0,http_helpers_1.sendJson)(n,200,{status:"ok"})})()}return void(0,http_helpers_1.sendJson)(n,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if((0,skip_paths_1.shouldSkipPath)(f,a))return void o();if(!(0,include_path_1.shouldIncludePath)(f,d))return interceptForInjection(t,n,E),void o();let v;try{v=r(t)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(v)){const e=(0,cookies_1.parseCookie)(t,"__unshared_uid"),r=(0,cookies_1.parseCookie)(t,"__unshared_uid_at"),i=r?Number(r):NaN,s=Number.isFinite(i)&&Date.now()-i<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;v=e&&s?e:void 0}if(!v){const e=(0,secure_1.isSecureRequest)(t)?"; Secure":"";return appendSetCookie(n,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(t,n,E),void o()}const m=resolveEmail(t,i);if(setUserIdCookie(t,n,v),m&&setEmailCookie(t,n,m),!m)return interceptForInjection(t,n,E),void o();const I=extractSessionId(t,l),y=(0,device_id_1.extractDeviceIdOrUndefined)(t,_),b=extractFingerprintId(t),w=t.headers["user-agent"]??"",q=(0,client_ip_1.extractClientIp)(t),H=y??b;if(!c&&(0,is_bot_1.isBot)(w))return void o();const K=S.get(v);function X(){"unknown"!==I&&H&&(A.isPaused()||dispatchUserEvent(e,S,A,x,{userId:v,emailAddress:m,sessionId:I,deviceId:H,fingerprintId:b,userAgent:w,ipAddress:q,eventType:g},h))}K?(S.isStale(v)&&!S.isRefreshing(v)&&(S.markRefreshing(v),fetchAndCacheVerdict(e,S,v,m,H??"unknown",b,I,u,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:m})).catch(()=>C({operation:"checkUser",reason:"exception",userId:v,emailAddress:m})).finally(()=>S.clearRefreshing(v))),K.isFlagged||X(),applyVerdict(K,v,m,t,n,o,E,p,k,F)):fetchAndCacheVerdict(e,S,v,m,H??"unknown",b,I,u,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:m})).then(e=>{e.isFlagged||X(),applyVerdict(e,v,m,t,n,o,E,p,k,F)}).catch(()=>{C({operation:"checkUser",reason:"exception",userId:v,emailAddress:m}),X(),interceptForInjection(t,n,E),o()})}}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=(0,cookies_1.parseCookie)(e,"__unshared_email");if(r)return r;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,t,r,i,s,n,o,a,d,c){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),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={};let u;s&&"unknown"!==s&&(c.deviceId=s),n&&(c.fingerprintId=n);const l=await Promise.race([e.checkUser(i,c),new Promise(e=>{u=setTimeout(()=>e(null),a)})]);if(clearTimeout(u),!l)return d?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:o,cachedAt:0,ttl:0};l.failedOpen&&d?.("http_error",l.failedOpen.status);const _=l.data?.is_user_flagged??!1;return t.set(r,{isFlagged:_,isVerified:!1,emailAddress:i,sessionId:o}),t.get(r)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return(0,cookies_1.parseCookie)(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return(0,cookies_1.parseCookie)(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
|
|
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,_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,sessionId:u,deviceId:_,onFlagged:p,onError:h,onFailOpen:f,blockFlagged:g=!1,flaggedMode:v,autoInterstitial:m=!1,interstitialFlowType:I="email_verification"}=t,k=v??(g?"gate":void 0),y=m||"overlay"===k,C=e=>{if(f)try{f(e)}catch{}else warnFailOpenThrottled(e)},S=new verdict_cache_1.VerdictCache(o),A=new rate_limit_backoff_1.RateLimitBackoff,x=new dispatch_dedupe_1.DispatchDedupe,T=Date.now().toString(36),b=(0,fingerprint_script_1.generateFingerprintScript)(s,T,{autoInterstitial:y,interstitialFlowType:I});let E="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");E=(0,fs_1.readFileSync)(e,"utf8")}catch{}const w=(0,fp_bundle_source_1.createFingerprintBundleSource)(),U=void 0!==k||y,O="gate"===k?"flaggedMode 'gate' (blockFlagged)":"overlay"===k?"flaggedMode 'overlay'":"autoInterstitial";if(U&&!E)throw new Error(`[Unshared] ${O} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(U&&"/__unshared"!==s)throw new Error(`[Unshared] ${O} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const q="gate"===k?(0,gate_page_1.generateGatePage)(s,T):"",F=(0,submit_fp_1.handleSubmitFingerprint)({client:e,verdictCache:S,rateLimitBackoff:A,dispatchDedupe:x,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:u,resolveDeviceId:_,disableBotFilter:c,onError:h}),$=(0,verify_1.handleVerifyTrigger)({client:e,verdictCache:S,resolveEmailAddress:i,resolveDeviceId:_,onError:h}),P=(0,verify_1.handleVerify)({client:e,verdictCache:S,resolveUserId:r,resolveEmailAddress:i,resolveDeviceId:_,onError:h}),j=(0,interstitial_1.handleGetInterstitialFlow)({client:e}),M=n?Array.isArray(n)?n:[n]:null,D=`${s}/fp.js`,N=`${s}/fingerprint.js`,R=`${s}/submit-fp`,V=`${s}/verify-trigger`,L=`${s}/verify`,G=`${s}/status`,H=`${s}/interstitial-flow`,B=function(t,n,o){const f=(0,http_helpers_1.getRequestPath)(t.url),g=t.url||f;if(f.startsWith(s+"/")){if(function(e,t){if(!M)return;const r=e.headers.origin??"";M.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")):M.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&&f===D)return n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBody)(n,200,E);if("GET"===t.method&&f===N){const e=w.get();return e?(n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBody)(n,200,e)):void(0,http_helpers_1.sendBody)(n,503,"/* fingerprint agent unavailable */")}if("POST"===t.method&&(f===R||f===V||f===L))return void 0===t.body?void(0,http_helpers_1.sendJson)(n,400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"req.body is undefined. Mount a JSON body-parsing middleware (e.g., express.json()) before the Unshared middleware."}}):f===R?void F(t,n):f===V?void $(t,n):void P(t,n);if("GET"===t.method&&f===H)return void j(t,n);if("GET"===t.method&&f===G){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&&!A.isPaused()&&!S.isRefreshing(s)){S.markRefreshing(s);try{const i=(0,device_id_1.extractDeviceIdOrUndefined)(t,_),n=extractFingerprintId(t),a=extractSessionId(t,u),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){h&&h(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!==k||p)&&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)(f,a))return void o();if(!(0,include_path_1.shouldIncludePath)(f,d))return interceptForInjection(t,n,b),void o();let v;try{v=r(t)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(v)){const e=(0,cookies_1.parseCookie)(t,"__unshared_uid"),r=(0,cookies_1.parseCookie)(t,"__unshared_uid_at"),i=r?Number(r):NaN,s=Number.isFinite(i)&&Date.now()-i<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;v=e&&s?e:void 0}if(!v){const e=(0,secure_1.isSecureRequest)(t)?"; Secure":"";return appendSetCookie(n,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(t,n,b),void o()}const m=resolveEmail(t,i);if(setUserIdCookie(t,n,v),m&&setEmailCookie(t,n,m),!m)return interceptForInjection(t,n,b),void o();const I=extractSessionId(t,u),y=(0,device_id_1.extractDeviceIdOrUndefined)(t,_),T=extractFingerprintId(t),U=t.headers["user-agent"]??"",O=(0,client_ip_1.extractClientIp)(t),B=y??T;if(!c&&(0,is_bot_1.isBot)(U))return void o();const K=S.get(v);function X(){"unknown"!==I&&B&&(A.isPaused()||dispatchUserEvent(e,S,A,x,{userId:v,emailAddress:m,sessionId:I,deviceId:B,fingerprintId:T,userAgent:U,ipAddress:O,eventType:g},h))}K?(S.isStale(v)&&!S.isRefreshing(v)&&(S.markRefreshing(v),fetchAndCacheVerdict(e,S,v,m,B??"unknown",T,I,l,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:m})).catch(()=>C({operation:"checkUser",reason:"exception",userId:v,emailAddress:m})).finally(()=>S.clearRefreshing(v))),K.isFlagged||X(),applyVerdict(K,v,m,t,n,o,b,p,k,q)):fetchAndCacheVerdict(e,S,v,m,B??"unknown",T,I,l,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:m})).then(e=>{e.isFlagged||X(),applyVerdict(e,v,m,t,n,o,b,p,k,q)}).catch(()=>{C({operation:"checkUser",reason:"exception",userId:v,emailAddress:m}),X(),interceptForInjection(t,n,b),o()})};return B.destroy=()=>{S.destroy(),w.stop()},B}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={},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)})]);if(clearTimeout(p),!h)return d?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:o,cachedAt:0,ttl:0};h.failedOpen&&d?.(h.failedOpen.reason??"http_error",h.failedOpen.status);const f=h.data?.is_user_flagged??!1;return t.set(r,{isFlagged:f,isVerified:!1,emailAddress:i,sessionId:o},h.failedOpen?5e3:void 0),t.get(r)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return(0,cookies_1.parseCookie)(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return(0,cookies_1.parseCookie)(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";function generateFingerprintScript(e,t,n){const r=t?`?v=${escapeJavaScript(t)}`:"",s=n?.autoInterstitial?`\nvar _isdk=new UnsharedBrowser.UnsharedBrowser({baseUrl:""});\nwindow.addEventListener("unshared:flagged",function(){try{_isdk.showInterstitial({flowType:"${escapeJavaScript(n?.interstitialFlowType??"email_verification")}",onComplete:function(){try{location.reload()}catch(e){}}})}catch(e){}});`:"";return`<script>\n(function(){\ntry{\n// --- Bot drop (defense-in-depth) ---\n// Must be the first statement: we do not want to write cookies, localStorage,\n// session IDs, or any network requests for known-bot traffic. Mirrors the\n// regex in unshared-fingerprint-lib/src/detect/bot.ts and Node middleware\n// utils/is-bot.ts. Keep all three in sync.\nvar BOT_RE=/googlebot|bingbot|slurp|baiduspider|duckduckbot|yandex|sogou|exabot|ia_archiver|curl|wget|python-requests|python-urllib|axios|node-fetch|go-http-client|java\\/|libwww-perl|okhttp|apache-httpclient|http_request|httpie|headlesschrome|phantomjs|puppeteer|playwright|cypress|selenium|webdriver|electron|jsdom|vercel-screenshot|screenshot|prerender|lighthouse|chrome-lighthouse|pagespeed|gtmetrix|pingdom|nessus|nikto|sqlmap|burp|zap|qualys|openvas|nmap|masscan|facebookexternalhit|twitterbot|linkedinbot|whatsapp|telegrambot|slackbot|discordbot|bot|crawl|spider|scrape|fetch|scan/i;\nif(typeof navigator!=="undefined"&&navigator.userAgent&&BOT_RE.test(navigator.userAgent))return;\n\nvar pfx="${escapeJavaScript(e)}";\nvar SS_FP="__unshared_fp";\nvar SS_LAST_SUBMIT="__unshared_last_submit";\n\n// Dedup state: skip submit if (user_id + URL) matches last submission.\n// Modern SPAs (Next.js App Router, React Router, etc.) call replaceState\n// 3-5 times during hydration with the same URL — without this guard,\n// each call generates a redundant FP row with identical stable_hash.\n// Persisted to sessionStorage so hard reloads and framework double-boots\n// inside the same tab still dedupe (the in-memory value resets on reload).\nvar lastSubmitKey="";\ntry{lastSubmitKey=sessionStorage.getItem(SS_LAST_SUBMIT)||""}catch(e){}\n\n// Page-scoped dedup state SHARED with the frontend SDK (browser.ts getSharedDedup).\n// On a Tier 1 page both this inline script and the SDK submit; each holds its own\n// in-memory lastSubmitKey, so both could pass the check below and POST. window.__unshared\n// is read+written synchronously in submitFP (no await between), so whichever fires\n// first claims the uid|route key and the other no-ops — killing the read-before-write\n// race that doubled events. KEEP IN SYNC with browser.ts: namespace, lastKey, key formula.\nvar shared=(window.__unshared=window.__unshared||{});\n\n// --- Helpers ---\nfunction gC(n){var m=document.cookie.match(new RegExp("(?:^|; )"+n+"=([^;]*)"));return m?decodeURIComponent(m[1]):null}\nfunction sC(n,v,d){var e="";if(d){var dt=new Date();dt.setTime(dt.getTime()+d*864e5);e="; expires="+dt.toUTCString()}document.cookie=n+"="+encodeURIComponent(v)+e+"; path=/; SameSite=Lax"+(location.protocol==="https:"?"; Secure":"")}\nfunction uuid(){return(typeof crypto!=="undefined"&&crypto.randomUUID)?crypto.randomUUID():("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(c){var r=Math.random()*16|0;return(c==="x"?r:r&0x3|0x8).toString(16)}))}\n// Sentinel user IDs that must never be treated as real users. Mirrors\n// the Set in sentinel-user-id.ts — keep in sync. Empty string is handled\n// by the separate !uid checks below.\nvar SENTINEL_UIDS={"__pre_auth__":1,"anonymous":1,"guest":1,"undefined":1,"null":1};\nfunction isSentinelUid(v){return typeof v==="string"&&SENTINEL_UIDS.hasOwnProperty(v)}\n\n// --- Session + device IDs ---\n// Session ID is a UUID because it's supposed to be tab-scoped and random.\n// Device ID is intentionally NOT a UUID — Issue 9: random UUIDs wrote\n// meaningless device_ids to every fingerprint row. Instead we read the\n// stable fingerprint hash from localStorage if a previous submission\n// already persisted it; otherwise we leave did empty and let submitFP()\n// reconcile on the first successful collection. The Node middleware's\n// Issue 8 bootstrap-skip branch handles the empty-device_id window so we\n// never dispatch with a random or "unknown" value.\nvar sid=gC("__unshared_sid");\nif(!sid){sid=uuid();sC("__unshared_sid",sid,365)}\nvar did="";\ntry{did=localStorage.getItem("__unshared_device_id")||""}catch(e){}\nif(did){sC("__unshared_fp_id",did,365)}\n\n// --- Fingerprint cache (sessionStorage) ---\nfunction getFP(){try{var r=sessionStorage.getItem(SS_FP);return r?JSON.parse(r):null}catch(e){return null}}\nfunction setFP(fp){try{sessionStorage.setItem(SS_FP,JSON.stringify(fp))}catch(e){}}\n\n// --- Submit fingerprint to backend ---\nfunction submitFP(fp){\n var uid=gC("__unshared_uid");\n if(!uid||isSentinelUid(uid))return;\n // Issue 9: reconcile device_id to the stable fingerprint hash. This runs\n // before we send the X-Device-Id header so the very first submission\n // already carries the real value. Persist to localStorage so other tabs\n // (and future reloads) pick up the same stable ID without needing to\n // re-collect the fingerprint.\n if(fp.fingerprint_id){\n did=fp.fingerprint_id;\n try{localStorage.setItem("__unshared_device_id",did)}catch(e){}\n sC("__unshared_fp_id",did,365);\n }\n // event_type is the SPA route, not a fixed enum. Page-level event names\n // (page_load/route_change) collapsed every row into one of two buckets;\n // the URL is more useful for analytics and matches the frontend SDK.\n var route=(location.pathname||"/")+(location.search||"");\n var key=uid+"|"+route;\n // Check the shared window guard AND the in-memory key (last-key semantics, so an\n // SPA A->B->A revisit still submits the second A). The shared guard makes this\n // submitter and the frontend SDK see each other within the page.\n if(key===lastSubmitKey||shared.lastKey===key)return;\n shared.lastKey=key;\n lastSubmitKey=key;\n try{sessionStorage.setItem(SS_LAST_SUBMIT,key)}catch(e){}\n // collected_at is stamped fresh at submit time rather than carried from fp.timestamp,\n // because fp is cached per-tab in sessionStorage — reusing its original timestamp would\n // freeze collected_at at first load and drift against server created_at as the tab ages.\n // The server authoritatively overwrites this value again on ingress.\n var body={hash:fp.full_hash,stable_hash:fp.fingerprint_id,collected_at:(new Date()).toISOString(),is_incognito:fp.isIncognito,components:fp.components,version:fp.version,source:fp.source,session_id:sid,user_id:uid,event_type:route};\n // Idempotency key derived from (stable_hash, user_id, route), SHA-256-hashed\n // when WebCrypto is available so the user_id never appears in the platform's\n // stored idempotency_key column (raw fallback on non-secure contexts — the\n // middleware accepts both). NOTE: the middleware appends |Date.now() before\n // forwarding (submit-fp.ts), so the backend sees a unique value per\n // submission and dedups only PubSub redeliveries — NOT two distinct POSTs.\n // Cross-submitter / cross-reload dedup is client-side (the shared window\n // guard + sessionStorage above).\n var idem=fp.fingerprint_id+"|"+uid+"|"+route;\n function sendFP(idemKey){\n var xhr=new XMLHttpRequest();\n xhr.open("POST",pfx+"/submit-fp",true);\n xhr.setRequestHeader("Content-Type","application/json");\n xhr.setRequestHeader("X-Session-Id",sid);\n if(did)xhr.setRequestHeader("X-Device-Id",did);\n xhr.setRequestHeader("X-Idempotency-Key",idemKey);\n xhr.send(JSON.stringify(body));\n }\n if(window.crypto&&crypto.subtle&&window.TextEncoder){\n crypto.subtle.digest("SHA-256",new TextEncoder().encode(idem)).then(function(d){\n var a=new Uint8Array(d),s="";\n for(var i=0;i<a.length;i++){s+=("0"+a[i].toString(16)).slice(-2)}\n sendFP(s);\n }).catch(function(){sendFP(idem)});\n }else{sendFP(idem)}\n}\n\n// --- Collect fingerprint (loads fp.js if needed) then submit ---\nvar fpReady=false;\nfunction collectAndSubmit(){\n var uid=gC("__unshared_uid");\n if(!uid||isSentinelUid(uid))return;\n var cached=getFP();\n if(cached){submitFP(cached);return}\n if(!fpReady)return;\n try{\n var c=new UnsharedBrowser.UnsharedBrowser({baseUrl:""});\n c.collect({exclude:["timing","speech"]}).then(function(fp){setFP(fp);submitFP(fp)});\n }catch(e){}\n}\n\n// --- Load fp.js (always — browser caches it for 1h) ---\n// Submit cached FP immediately if available; load fp.js for fresh collection\nvar pageLoadSubmitted=false;\nvar _boot_uid=gC("__unshared_uid");\nif(getFP()&&_boot_uid&&!isSentinelUid(_boot_uid)){submitFP(getFP());pageLoadSubmitted=true;deferredCheck()}\nvar s=document.createElement("script");\ns.src=pfx+"/fp.js${r}";\ns.onload=function(){fpReady=true;${s}if(!pageLoadSubmitted){collectAndSubmit();deferredCheck()}};\ndocument.head.appendChild(s);\n\n// --- Deferred verdict check ---\n// After fingerprint submission, the backend processes the event async.\n// If the user was just flagged, the initial page load may have beaten\n// the verdict update. Re-check after a delay so newly flagged sessions\n// get caught without waiting for user interaction.\n// The endpoint always returns 200 so browsers don't log a scary red\n// network error — we inspect the body and dispatch the flagged event\n// ourselves when status==="flagged".\nfunction deferredCheck(){\n var uid=gC("__unshared_uid");\n if(!uid||isSentinelUid(uid))return;\n setTimeout(function(){\n try{fetch(pfx+"/status",{method:"GET",credentials:"same-origin"}).then(function(r){return r.json()}).then(function(b){if(b&&b.status==="flagged")emitFlagged(b)}).catch(function(){})}catch(e){}\n },500);\n}\n\n// --- SPA route change tracking (History API + popstate) ---\nvar oPush=history.pushState,oReplace=history.replaceState;\nhistory.pushState=function(){oPush.apply(this,arguments);try{collectAndSubmit()}catch(e){}};\nhistory.replaceState=function(){oReplace.apply(this,arguments);try{collectAndSubmit()}catch(e){}};\nwindow.addEventListener("popstate",function(){try{collectAndSubmit()}catch(e){}});\n\n// --- 403 interception: dispatch "unshared:flagged" event ---\nfunction emitFlagged(body){\n try{window.dispatchEvent(new CustomEvent("unshared:flagged",{detail:{email:body.email||""}}))}catch(e){}\n}\n\n// Patch fetch\nvar oFetch=window.fetch;\nif(oFetch){window.fetch=function(){return oFetch.apply(this,arguments).then(function(r){if(r.status===403){try{var cl=r.clone();cl.json().then(function(b){if(b&&b.error==="account_flagged")emitFlagged(b)}).catch(function(){})}catch(e){}}return r})}}\n\n// Patch XMLHttpRequest\nvar oXSend=XMLHttpRequest.prototype.send;\nXMLHttpRequest.prototype.send=function(){var x=this;x.addEventListener("load",function(){if(x.status===403){try{var b=JSON.parse(x.responseText);if(b&&b.error==="account_flagged")emitFlagged(b)}catch(e){}}});return oXSend.apply(this,arguments)};\n\n}catch(e){}\n})();\n<\/script>`}function escapeJavaScript(e){return e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/'/g,"\\'")}Object.defineProperty(exports,"t",{value:!0}),exports.generateFingerprintScript=generateFingerprintScript;
|
|
1
|
+
"use strict";function generateFingerprintScript(e,t,n){const r=t?`?v=${escapeJavaScript(t)}`:"",s=n?.autoInterstitial?`\nvar _isdk=new UnsharedBrowser.UnsharedBrowser({baseUrl:""});\nwindow.addEventListener("unshared:flagged",function(){try{_isdk.showInterstitial({flowType:"${escapeJavaScript(n?.interstitialFlowType??"email_verification")}",onComplete:function(){try{location.reload()}catch(e){}}})}catch(e){}});`:"";return`<script>\n(function(){\ntry{\n// --- Bot drop (defense-in-depth) ---\n// Must be the first statement: we do not want to write cookies, localStorage,\n// session IDs, or any network requests for known-bot traffic. Mirrors the\n// regex in unshared-fingerprint-lib/src/detect/bot.ts and Node middleware\n// utils/is-bot.ts. Keep all three in sync.\nvar BOT_RE=/googlebot|bingbot|slurp|baiduspider|duckduckbot|yandex|sogou|exabot|ia_archiver|curl|wget|python-requests|python-urllib|axios|node-fetch|go-http-client|java\\/|libwww-perl|okhttp|apache-httpclient|http_request|httpie|headlesschrome|phantomjs|puppeteer|playwright|cypress|selenium|webdriver|electron|jsdom|vercel-screenshot|screenshot|prerender|lighthouse|chrome-lighthouse|pagespeed|gtmetrix|pingdom|nessus|nikto|sqlmap|burp|zap|qualys|openvas|nmap|masscan|facebookexternalhit|twitterbot|linkedinbot|whatsapp|telegrambot|slackbot|discordbot|bot|crawl|spider|scrape|fetch|scan/i;\nif(typeof navigator!=="undefined"&&navigator.userAgent&&BOT_RE.test(navigator.userAgent))return;\n\nvar pfx="${escapeJavaScript(e)}";\nvar SS_FP="__unshared_fp";\nvar SS_LAST_SUBMIT="__unshared_last_submit";\n\n// Dedup state: skip submit if (user_id + URL) matches last submission.\n// Modern SPAs (Next.js App Router, React Router, etc.) call replaceState\n// 3-5 times during hydration with the same URL — without this guard,\n// each call generates a redundant FP row with identical stable_hash.\n// Persisted to sessionStorage so hard reloads and framework double-boots\n// inside the same tab still dedupe (the in-memory value resets on reload).\nvar lastSubmitKey="";\ntry{lastSubmitKey=sessionStorage.getItem(SS_LAST_SUBMIT)||""}catch(e){}\n\n// Page-scoped dedup state SHARED with the frontend SDK (browser.ts getSharedDedup).\n// On a Tier 1 page both this inline script and the SDK submit; each holds its own\n// in-memory lastSubmitKey, so both could pass the check below and POST. window.__unshared\n// is read+written synchronously in submitFP (no await between), so whichever fires\n// first claims the uid|route key and the other no-ops — killing the read-before-write\n// race that doubled events. KEEP IN SYNC with browser.ts: namespace, lastKey, key formula.\nvar shared=(window.__unshared=window.__unshared||{});\n\n// --- Helpers ---\nfunction gC(n){var m=document.cookie.match(new RegExp("(?:^|; )"+n+"=([^;]*)"));return m?decodeURIComponent(m[1]):null}\nfunction sC(n,v,d){var e="";if(d){var dt=new Date();dt.setTime(dt.getTime()+d*864e5);e="; expires="+dt.toUTCString()}document.cookie=n+"="+encodeURIComponent(v)+e+"; path=/; SameSite=Lax"+(location.protocol==="https:"?"; Secure":"")}\nfunction uuid(){return(typeof crypto!=="undefined"&&crypto.randomUUID)?crypto.randomUUID():("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(c){var r=Math.random()*16|0;return(c==="x"?r:r&0x3|0x8).toString(16)}))}\n// Sentinel user IDs that must never be treated as real users. Mirrors\n// the Set in sentinel-user-id.ts — keep in sync. Empty string is handled\n// by the separate !uid checks below.\nvar SENTINEL_UIDS={"__pre_auth__":1,"anonymous":1,"guest":1,"undefined":1,"null":1};\nfunction isSentinelUid(v){return typeof v==="string"&&SENTINEL_UIDS.hasOwnProperty(v)}\n\n// --- Session + device IDs ---\n// Session ID is a UUID because it's supposed to be tab-scoped and random.\n// Device ID is intentionally NOT a UUID — Issue 9: random UUIDs wrote\n// meaningless device_ids to every fingerprint row. Instead we read the\n// stable fingerprint hash from localStorage if a previous submission\n// already persisted it; otherwise we leave did empty and let submitFP()\n// reconcile on the first successful collection. The Node middleware's\n// Issue 8 bootstrap-skip branch handles the empty-device_id window so we\n// never dispatch with a random or "unknown" value.\nvar sid=gC("__unshared_sid");\nif(!sid){sid=uuid();sC("__unshared_sid",sid,365)}\nvar did="";\ntry{did=localStorage.getItem("__unshared_device_id")||""}catch(e){}\nif(did){sC("__unshared_fp_id",did,365)}\n\n// --- Fingerprint cache (sessionStorage) ---\nfunction getFP(){try{var r=sessionStorage.getItem(SS_FP);return r?JSON.parse(r):null}catch(e){return null}}\nfunction setFP(fp){try{sessionStorage.setItem(SS_FP,JSON.stringify(fp))}catch(e){}}\n\n// --- Submit fingerprint to backend ---\nfunction submitFP(fp){\n var uid=gC("__unshared_uid");\n if(!uid||isSentinelUid(uid))return;\n // Issue 9: reconcile device_id to the stable fingerprint hash. This runs\n // before we send the X-Device-Id header so the very first submission\n // already carries the real value. Persist to localStorage so other tabs\n // (and future reloads) pick up the same stable ID without needing to\n // re-collect the fingerprint.\n if(fp.fingerprint_id){\n did=fp.fingerprint_id;\n try{localStorage.setItem("__unshared_device_id",did)}catch(e){}\n sC("__unshared_fp_id",did,365);\n }\n // event_type is the SPA route, not a fixed enum. Page-level event names\n // (page_load/route_change) collapsed every row into one of two buckets;\n // the URL is more useful for analytics and matches the frontend SDK.\n var route=(location.pathname||"/")+(location.search||"");\n var key=uid+"|"+route;\n // Check the shared window guard AND the in-memory key (last-key semantics, so an\n // SPA A->B->A revisit still submits the second A). The shared guard makes this\n // submitter and the frontend SDK see each other within the page.\n if(key===lastSubmitKey||shared.lastKey===key)return;\n shared.lastKey=key;\n lastSubmitKey=key;\n try{sessionStorage.setItem(SS_LAST_SUBMIT,key)}catch(e){}\n // collected_at is stamped fresh at submit time rather than carried from fp.timestamp,\n // because fp is cached per-tab in sessionStorage — reusing its original timestamp would\n // freeze collected_at at first load and drift against server created_at as the tab ages.\n // The server authoritatively overwrites this value again on ingress.\n var body={hash:fp.full_hash,stable_hash:fp.fingerprint_id,collected_at:(new Date()).toISOString(),is_incognito:fp.isIncognito,components:fp.components,version:fp.version,source:fp.source,session_id:sid,user_id:uid,event_type:route};\n // Idempotency key derived from (stable_hash, user_id, route), SHA-256-hashed\n // when WebCrypto is available so the user_id never appears in the platform's\n // stored idempotency_key column (raw fallback on non-secure contexts — the\n // middleware accepts both). NOTE: the middleware appends |Date.now() before\n // forwarding (submit-fp.ts), so the backend sees a unique value per\n // submission and dedups only PubSub redeliveries — NOT two distinct POSTs.\n // Cross-submitter / cross-reload dedup is client-side (the shared window\n // guard + sessionStorage above).\n var idem=fp.fingerprint_id+"|"+uid+"|"+route;\n function sendFP(idemKey){\n var xhr=new XMLHttpRequest();\n xhr.open("POST",pfx+"/submit-fp",true);\n xhr.setRequestHeader("Content-Type","application/json");\n xhr.setRequestHeader("X-Session-Id",sid);\n if(did)xhr.setRequestHeader("X-Device-Id",did);\n xhr.setRequestHeader("X-Idempotency-Key",idemKey);\n xhr.send(JSON.stringify(body));\n }\n if(window.crypto&&crypto.subtle&&window.TextEncoder){\n crypto.subtle.digest("SHA-256",new TextEncoder().encode(idem)).then(function(d){\n var a=new Uint8Array(d),s="";\n for(var i=0;i<a.length;i++){s+=("0"+a[i].toString(16)).slice(-2)}\n sendFP(s);\n }).catch(function(){sendFP(idem)});\n }else{sendFP(idem)}\n}\n\n// --- Collect fingerprint (loads fp.js if needed) then submit ---\nvar fpReady=false;\nfunction collectAndSubmit(){\n var uid=gC("__unshared_uid");\n if(!uid||isSentinelUid(uid))return;\n var cached=getFP();\n if(cached){submitFP(cached);return}\n if(!fpReady)return;\n try{\n var c=new UnsharedBrowser.UnsharedBrowser({baseUrl:""});\n c.collect({exclude:["timing","speech"]}).then(function(fp){setFP(fp);submitFP(fp)});\n }catch(e){}\n}\n\n// --- Load fp.js (always — browser caches it for 1h) ---\n// Submit cached FP immediately if available; load fp.js for fresh collection\nvar pageLoadSubmitted=false;\nvar _boot_uid=gC("__unshared_uid");\nif(getFP()&&_boot_uid&&!isSentinelUid(_boot_uid)){submitFP(getFP());pageLoadSubmitted=true;deferredCheck()}\nvar s=document.createElement("script");\ns.src=pfx+"/fp.js${r}";\ns.onload=function(){fpReady=true;${s}if(!pageLoadSubmitted){collectAndSubmit();deferredCheck()}};\ndocument.head.appendChild(s);\n\n// --- Deferred verdict check ---\n// After fingerprint submission, the backend processes the event async.\n// If the user was just flagged, the initial page load may have beaten\n// the verdict update. Re-check after a delay so newly flagged sessions\n// get caught without waiting for user interaction.\n// The endpoint always returns 200 so browsers don't log a scary red\n// network error — we inspect the body and dispatch the flagged event\n// ourselves when status==="flagged".\nfunction deferredCheck(){\n var uid=gC("__unshared_uid");\n if(!uid||isSentinelUid(uid))return;\n setTimeout(function(){\n try{fetch(pfx+"/status",{method:"GET",credentials:"same-origin"}).then(function(r){return r.json()}).then(function(b){if(b&&b.status==="flagged")emitFlagged(b)}).catch(function(){})}catch(e){}\n },500);\n}\n\n// --- SPA route change tracking (History API + popstate) ---\nvar oPush=history.pushState,oReplace=history.replaceState;\nhistory.pushState=function(){oPush.apply(this,arguments);try{collectAndSubmit()}catch(e){}};\nhistory.replaceState=function(){oReplace.apply(this,arguments);try{collectAndSubmit()}catch(e){}};\nwindow.addEventListener("popstate",function(){try{collectAndSubmit()}catch(e){}});\n\n// --- 403 interception: dispatch "unshared:flagged" event ---\nfunction emitFlagged(body){\n try{window.dispatchEvent(new CustomEvent("unshared:flagged",{detail:{email:body.email||""}}))}catch(e){}\n}\n\n// Patch fetch\nvar oFetch=window.fetch;\nif(oFetch){window.fetch=function(){return oFetch.apply(this,arguments).then(function(r){if(r.status===403){try{var cl=r.clone();cl.json().then(function(b){if(b&&b.error==="account_flagged")emitFlagged(b)}).catch(function(){})}catch(e){}}return r})}}\n\n// Patch XMLHttpRequest\nvar oXSend=XMLHttpRequest.prototype.send;\nXMLHttpRequest.prototype.send=function(){var x=this;x.addEventListener("load",function(){if(x.status===403){try{var b=JSON.parse(x.responseText);if(b&&b.error==="account_flagged")emitFlagged(b)}catch(e){}}});return oXSend.apply(this,arguments)};\n\n}catch(e){}\n})();\n<\/script>`}function escapeJavaScript(e){return JSON.stringify(e).slice(1,-1).replace(/<\/(script)/gi,"<\\/$1").replace(new RegExp("\u2028","g"),"\\u2028").replace(new RegExp("\u2029","g"),"\\u2029")}Object.defineProperty(exports,"t",{value:!0}),exports.generateFingerprintScript=generateFingerprintScript;
|
|
@@ -6,6 +6,13 @@ import type { UnsharedResponse } from '../types';
|
|
|
6
6
|
* CSS, JS, etc.) pass through to the original write/end without buffering,
|
|
7
7
|
* avoiding unnecessary memory usage on large payloads.
|
|
8
8
|
*
|
|
9
|
+
* Compressed responses (Content-Encoding set) are always passed through
|
|
10
|
+
* untouched — the bytes are not HTML text so transforming them would corrupt
|
|
11
|
+
* the response.
|
|
12
|
+
*
|
|
13
|
+
* Bodies larger than MAX_INJECT_BYTES are passed through without injection to
|
|
14
|
+
* avoid unbounded memory usage.
|
|
15
|
+
*
|
|
9
16
|
* When res.end() is called on an HTML response, invokes the `transform`
|
|
10
17
|
* callback with the complete body buffer and the Content-Type header.
|
|
11
18
|
* The transform can return modified content or null to pass through unchanged.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";function interceptResponse(t,
|
|
1
|
+
"use strict";Object.defineProperty(exports,"t",{value:!0}),exports.interceptResponse=interceptResponse;const MAX_INJECT_BYTES=2e6;function interceptResponse(n,t,e){const f=e?.preventCaching??!1,o=n.write.bind(n),u=n.end.bind(n),r=n.writeHead.bind(n),c=[];let i=0,l=!1,s=null;function p(){n.write=o,n.end=u;for(const n of c)o(n);c.length=0}function y(){if(null!==s)return;const t=n.getHeader("content-type");null!=t&&(s=String(t).includes("text/html"),s&&n.getHeader("content-encoding")&&(s=!1),s||p())}n.writeHead=function(t,...e){return y(),f&&s&&(n.setHeader("Cache-Control","no-store"),n.removeHeader("ETag"),n.removeHeader("Last-Modified")),r(t,...e)},n.write=function(n,t,e){if(y(),!1===s)return o(n,t,e);if(null!=n){const f=Buffer.isBuffer(n)?n:Buffer.from(n,"string"==typeof t?t:"utf8");if(i+=f.length,c.push(f),i>MAX_INJECT_BYTES)return p(),"function"==typeof t&&t(null),"function"==typeof e&&e(null),!0}return"function"==typeof t&&t(null),"function"==typeof e&&e(null),!0},n.end=function(e,f,r){if(l)return n;if(l=!0,y(),!1===s)return u(e,f,r);if(null!=e){const n=Buffer.isBuffer(e)?e:Buffer.from(e,"string"==typeof f?f:"utf8");i+=n.length,c.push(n)}if(i>MAX_INJECT_BYTES){p();const n="function"==typeof f?f:r;return n?u(n):u()}const B=Buffer.concat(c),E=n.getHeader("content-type");let _;try{_=t(B,E)}catch{_=null}if(null!=_){const t=Buffer.isBuffer(_)?_:Buffer.from(_,"utf8");n.setHeader("Content-Length",t.length),n.removeHeader("Content-Encoding"),o(t)}else B.length>0&&o(B);const g="function"==typeof f?f:r;return g?u(g):u(),n}}
|
|
@@ -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
|
|
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})}}}
|
|
@@ -6,6 +6,7 @@ export interface VerificationDependencies<TReq extends UnsharedRequest = Unshare
|
|
|
6
6
|
verdictCache: VerdictCache;
|
|
7
7
|
resolveEmailAddress?: (req: TReq) => string | undefined;
|
|
8
8
|
resolveDeviceId?: (req: TReq) => string | undefined;
|
|
9
|
+
resolveUserId?: (req: TReq) => string | undefined;
|
|
9
10
|
onError?: (error: unknown, context: {
|
|
10
11
|
operation: 'processUserEvent' | 'submitFingerprintEvent' | 'checkUser' | 'verifyTrigger' | 'verify';
|
|
11
12
|
userId?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"i",{value:!0}),exports.handleVerifyTrigger=handleVerifyTrigger,exports.handleVerify=handleVerify;const cookies_1=require("../utils/cookies"),device_id_1=require("../utils/device-id"),http_helpers_1=require("../utils/http-helpers");function handleVerifyTrigger(e){return async(r,i)=>{try{const s=resolveEmail(r,r.body??{},e.resolveEmailAddress);if(!s)return void(0,http_helpers_1.sendJson)(i,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}});const t=(0,device_id_1.extractDeviceId)(r,e.resolveDeviceId),
|
|
1
|
+
"use strict";Object.defineProperty(exports,"i",{value:!0}),exports.handleVerifyTrigger=handleVerifyTrigger,exports.handleVerify=handleVerify;const cookies_1=require("../utils/cookies"),device_id_1=require("../utils/device-id"),http_helpers_1=require("../utils/http-helpers");function handleVerifyTrigger(e){return async(r,i)=>{try{const s=resolveEmail(r,r.body??{},e.resolveEmailAddress);if(!s)return void(0,http_helpers_1.sendJson)(i,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}});const t=(0,device_id_1.extractDeviceId)(r,e.resolveDeviceId),c=(0,cookies_1.parseCookie)(r,"__unshared_fingerprint_id")||void 0,o=await e.client.triggerEmailVerification(s,t,{fingerprintId:c});o.success?(0,http_helpers_1.sendJson)(i,200,{success:!0,data:o.data}):(0,http_helpers_1.sendJson)(i,200,{success:!1,error:o.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}})}catch(r){e.onError&&e.onError(r,{operation:"verifyTrigger"}),(0,http_helpers_1.sendJson)(i,200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}})}}}function handleVerify(e){return async(r,i)=>{try{const s=r.body??{},t=resolveEmail(r,s,e.resolveEmailAddress),c=s.code;if(!t||!c)return void(0,http_helpers_1.sendJson)(i,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}});const o=(0,device_id_1.extractDeviceId)(r,e.resolveDeviceId),a=(0,cookies_1.parseCookie)(r,"__unshared_fingerprint_id")||void 0,n=await e.client.verify(t,o,c,{fingerprintId:a});if(n.success){let s;try{s=e.resolveUserId?.(r)}catch{}s&&e.verdictCache.update(s,{isVerified:!0}),(0,http_helpers_1.sendJson)(i,200,{success:!0,data:{verified:!0}})}else(0,http_helpers_1.sendJson)(i,200,{success:!1,error:n.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}})}catch(r){e.onError&&e.onError(r,{operation:"verify"}),(0,http_helpers_1.sendJson)(i,200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}})}}}function resolveEmail(e,r,i){if(i)try{const r=i(e);if(r)return r}catch{}const s=(0,cookies_1.parseCookie)(e,"__unshared_email");if(s)return s;const t=r.email;return"string"==typeof t&&t?t:void 0}
|
|
@@ -3,6 +3,10 @@ export declare const ACCOUNT_FLAGGED_ERROR: "account_flagged";
|
|
|
3
3
|
* Standardized response body for a flagged, unverified user. The injected
|
|
4
4
|
* inline script keys off `error === 'account_flagged'` to emit the
|
|
5
5
|
* "unshared:flagged" event, so always use this helper for that body.
|
|
6
|
+
*
|
|
7
|
+
* The email is MASKED (`j***@domain`) — this body is browser-visible and reaches
|
|
8
|
+
* any same-origin script via `e.detail.email`. The SDK's own modal resolves the
|
|
9
|
+
* real identity server-side, so it never needs the full address here.
|
|
6
10
|
*/
|
|
7
11
|
export declare function flaggedResponse(email: string): {
|
|
8
12
|
readonly error: "account_flagged";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";
|
|
1
|
+
"use strict";Object.defineProperty(exports,"o",{value:!0}),exports.ACCOUNT_FLAGGED_ERROR=void 0,exports.flaggedResponse=flaggedResponse;const mask_email_1=require("./mask-email");function flaggedResponse(e){return{error:exports.ACCOUNT_FLAGGED_ERROR,email:(0,mask_email_1.maskEmail)(e)}}exports.ACCOUNT_FLAGGED_ERROR="account_flagged";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mask an email for client-facing surfaces (the `/status` body, the
|
|
3
|
+
* `account_flagged` 403 body, and thus the `unshared:flagged` event's
|
|
4
|
+
* `e.detail.email`). Keeps the first character of the local part and the full
|
|
5
|
+
* domain (`jason@unsharedlabs.com` → `j***@unsharedlabs.com`) so the value stays
|
|
6
|
+
* display-usable without exposing the full address to same-origin scripts.
|
|
7
|
+
* Returns `'***'` for malformed/empty input so nothing leaks.
|
|
8
|
+
*/
|
|
9
|
+
export declare function maskEmail(email: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";function maskEmail(e){const s=e.lastIndexOf("@");return s<=0?"***":`${e[0]}***@${e.slice(s+1)}`}Object.defineProperty(exports,"t",{value:!0}),exports.maskEmail=maskEmail;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"t",{value:!0}),exports.createWebProtectionMiddleware=createWebProtectionMiddleware;const util_1=require("../util"),verdict_cache_1=require("../middleware/verdict-cache"),rate_limit_backoff_1=require("../middleware/rate-limit-backoff"),dispatch_dedupe_1=require("../middleware/dispatch-dedupe"),fingerprint_script_1=require("../middleware/injection/fingerprint-script"),gate_page_1=require("../middleware/injection/gate-page"),flagged_response_1=require("../middleware/utils/flagged-response"),content_type_1=require("../middleware/utils/content-type"),skip_paths_1=require("../middleware/utils/skip-paths"),include_path_1=require("../middleware/utils/include-path"),is_bot_1=require("../middleware/utils/is-bot"),sentinel_user_id_1=require("../middleware/utils/sentinel-user-id"),web_helpers_1=require("./web-helpers"),CHECK_USER_TIMEOUT_MS=500;function createWebProtectionMiddleware(e,r){if(!r.userId)throw new Error("[Unshared] userId resolver is required");const{userId:t,emailAddress:s,routePrefix:i="/__unshared",corsOrigins:n,cacheTTL:a=6e4,skipPaths:o,includePathPrefix:d,sessionId:c,deviceId:_,fingerprintSdkBundle:l="",onFlagged:u,onError:p,onFailOpen:h,disableBotFilter:f=!1,checkUserTimeoutMs:m=CHECK_USER_TIMEOUT_MS,blockFlagged:w=!1,flaggedMode:g,autoInterstitial:b=!1,interstitialFlowType:v="email_verification"}=r,I=g??(w?"gate":void 0),y=b||"overlay"===I,A=e=>{if(h)try{h(e)}catch{}},S=Date.now().toString(36),E=void 0!==I||y,R="gate"===I?"flaggedMode 'gate' (blockFlagged)":"overlay"===I?"flaggedMode 'overlay'":"autoInterstitial";if(E&&!l)throw new Error(`[Unshared] ${R} requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the interstitial).`);if(E&&"/__unshared"!==i)throw new Error(`[Unshared] ${R} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const T="gate"===I?(0,gate_page_1.generateGatePage)(i,S):"",C=new verdict_cache_1.VerdictCache(a),x=new rate_limit_backoff_1.RateLimitBackoff,k=new dispatch_dedupe_1.DispatchDedupe,U=(0,fingerprint_script_1.generateFingerprintScript)(i,S,{autoInterstitial:y,interstitialFlowType:v}),O=`${i}/fp.js`,$=`${i}/submit-fp`,F=`${i}/verify-trigger`,P=`${i}/verify`,q=`${i}/status`,L=n?Array.isArray(n)?n:[n]:null;return async function(r,n){let a,h,w;try{const e=new URL(r.url);a=e.pathname,h=e.search}catch{return n(r)}if(a.startsWith(i+"/")){const i=function(e){if(!L)return{};const r=e.headers.get("origin")??"",t=L.includes("*");return t||L.includes(r)?{"Access-Control-Allow-Origin":t?"*":r,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id","Access-Control-Allow-Credentials":"true"}:{}}(r);if("OPTIONS"===r.method)return(0,web_helpers_1.emptyResponse)(204,i);if("GET"===r.method&&a===O)return l?(0,web_helpers_1.bodyResponse)(200,l,{...i,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600"}):(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},i);if("POST"===r.method&&(a===$||a===F||a===P)){let n;try{n=await r.json()}catch{return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"Request body is not valid JSON."}},i)}return a===$?handleSubmitFp(r,n,{client:e,verdictCache:C,rateLimitBackoff:x,dispatchDedupe:k,resolveUserId:t,resolveEmailAddress:s,resolveSessionId:c,resolveDeviceId:_,disableBotFilter:f,onError:p},i):a===F?handleVerifyTriggerWeb(r,n,{client:e,verdictCache:C,resolveEmailAddress:s,resolveDeviceId:_,onError:p},i):handleVerifyWeb(r,n,{client:e,verdictCache:C,resolveEmailAddress:s,resolveDeviceId:_,onError:p},i)}if("GET"===r.method&&a===q){let n;try{n=t(r)}catch{}if(!n)return(0,web_helpers_1.jsonResponse)(200,{status:"anonymous"},i);const a=resolveEmail(r,s);let o=C.get(n);if((!o||C.isStale(n))&&a&&!x.isPaused()&&!C.isRefreshing(n)){C.markRefreshing(n);try{const t=(0,web_helpers_1.extractDeviceIdFromRequest)(r,_),s=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_fingerprint_id")||void 0,i=extractSessionIdFromRequest(r,c),d=t??s??"unknown";await fetchAndCacheVerdict(e,C,n,a,d,s,i,m,(e,r)=>A({operation:"checkUser",reason:e,status:r,userId:n,emailAddress:a})),o=C.get(n)}catch(e){p&&p(e,{operation:"checkUser",userId:n,emailAddress:a}),A({operation:"checkUser",reason:"exception",userId:n,emailAddress:a})}finally{C.clearRefreshing(n)}}return o&&o.isFlagged&&!o.isVerified&&u&&a?(0,web_helpers_1.jsonResponse)(200,{status:"flagged",email:a},i):(0,web_helpers_1.jsonResponse)(200,{status:"ok"},i)}return(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},i)}if((0,skip_paths_1.shouldSkipPath)(a,o))return n(r);if(!(0,include_path_1.shouldIncludePath)(a,d))return injectIntoHtmlResponse(await n(r),U);try{w=t(r)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(w)){const e=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_uid"),t=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_uid_at"),s=t?Number(t):NaN,i=Number.isFinite(s)&&Date.now()-s<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;w=e&&i?e:void 0}if(!w){const e=(0,web_helpers_1.isSecureWebRequest)(r)?"; Secure":"",t=[`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`];return injectIntoHtmlResponse(await n(r),U,t)}const g=resolveEmail(r,s),b=[],v=(0,web_helpers_1.isSecureWebRequest)(r)?"; Secure":"";if(b.push(`__unshared_uid=${encodeURIComponent(w)}; Path=/; SameSite=Lax${v}`),b.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${v}`),g&&b.push(`__unshared_email=${encodeURIComponent(g)}; HttpOnly; Path=/; SameSite=Lax${v}`),!g)return injectIntoHtmlResponse(await n(r),U,b);const y=extractSessionIdFromRequest(r,c),S=(0,web_helpers_1.extractDeviceIdFromRequest)(r,_),E=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_fingerprint_id")||void 0,R=r.headers.get("user-agent")??"",M=(0,web_helpers_1.extractClientIpFromRequest)(r),D=S??E;if(!f&&(0,is_bot_1.isBot)(R))return n(r);let N=C.get(w);if(N)C.isStale(w)&&!C.isRefreshing(w)&&(C.markRefreshing(w),fetchAndCacheVerdict(e,C,w,g,D??"unknown",E,y,m,(e,r)=>A({operation:"checkUser",reason:e,status:r,userId:w,emailAddress:g})).catch(()=>A({operation:"checkUser",reason:"exception",userId:w,emailAddress:g})).finally(()=>C.clearRefreshing(w)));else try{N=await fetchAndCacheVerdict(e,C,w,g,D??"unknown",E,y,m,(e,r)=>A({operation:"checkUser",reason:e,status:r,userId:w,emailAddress:g}))}catch{return A({operation:"checkUser",reason:"exception",userId:w,emailAddress:g}),injectIntoHtmlResponse(await n(r),U,b)}const H=N.isFlagged&&!N.isVerified;if("gate"===I&&H)return(0,content_type_1.isHtmlNavigation)(r.method,r.headers.get("accept")??void 0)?(0,web_helpers_1.bodyResponse)(200,T,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}):(0,web_helpers_1.jsonResponse)(403,(0,flagged_response_1.flaggedResponse)(g));if("overlay"===I&&H)return(0,content_type_1.isHtmlNavigation)(r.method,r.headers.get("accept")??void 0)?injectIntoHtmlResponse(await n(r),U,b):(0,web_helpers_1.jsonResponse)(403,(0,flagged_response_1.flaggedResponse)(g));if(N.isFlagged&&!N.isVerified&&u)try{const e=await u({userId:w,emailAddress:g,verdict:N,request:r});if(e)return injectIntoHtmlResponse(e,U,b)}catch(e){p&&p(e,{operation:"checkUser",userId:w,emailAddress:g})}return N.isFlagged||"unknown"===y||!D||x.isPaused()||dispatchUserEvent(e,C,x,k,{userId:w,emailAddress:g,sessionId:y,deviceId:D,fingerprintId:E,userAgent:R,ipAddress:M,eventType:a+h},p),injectIntoHtmlResponse(await n(r),U,b)}}async function injectIntoHtmlResponse(e,r,t){const s=e.headers.get("content-type");if(!(0,content_type_1.isHtmlContentType)(s??void 0)){if(!t||0===t.length)return e;const r=(0,web_helpers_1.mergeResponseHeaders)(e.headers,void 0,t);return new Response(e.body,{status:e.status,statusText:e.statusText,headers:r})}const i=await e.text(),n=i.lastIndexOf("</body>"),a=-1===n?i+r:i.slice(0,n)+r+i.slice(n),o=(0,web_helpers_1.mergeResponseHeaders)(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(a).length)},t);return o.delete("ETag"),o.delete("Last-Modified"),o.delete("Content-Encoding"),new Response(a,{status:e.status,statusText:e.statusText,headers:o})}function resolveEmail(e,r){if(r)try{const t=r(e);if(t)return t}catch{}const t=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email");if(t)return t}function resolveEmailWithBody(e,r,t){const s=resolveEmail(e,t);if(s)return s;const i=r.email;return"string"==typeof i&&i?i:void 0}function extractSessionIdFromRequest(e,r){if(r)try{const t=r(e);if(t)return t}catch{}return(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid")??"unknown"}function dispatchUserEvent(e,r,t,s,i,n){s.mark(i.userId,i.eventType),e.processUserEvent({eventType:i.eventType,userId:i.userId,emailAddress:i.emailAddress,ipAddress:i.ipAddress,deviceId:i.deviceId,fingerprintId:i.fingerprintId,sessionHash:i.sessionId,userAgent:i.userAgent}).then(e=>{e.success&&e.data?.analysis&&r.update(i.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.pause(1e3*e.error.retryAfter)}).catch(e=>{n&&n(e,{operation:"processUserEvent",userId:i.userId,emailAddress:i.emailAddress})})}async function fetchAndCacheVerdict(e,r,t,s,i,n,a,o=CHECK_USER_TIMEOUT_MS,d){const c={};let _;i&&"unknown"!==i&&(c.deviceId=i),n&&(c.fingerprintId=n);const l=await Promise.race([e.checkUser(s,c),new Promise(e=>{_=setTimeout(()=>e(null),o)})]);if(clearTimeout(_),!l)return d?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:s,sessionId:a,cachedAt:0,ttl:0};l.failedOpen&&d?.("http_error",l.failedOpen.status);const u=l.data?.is_user_flagged??!1;return r.set(t,{isFlagged:u,isVerified:!1,emailAddress:s,sessionId:a}),r.get(t)}async function handleSubmitFp(e,r,t,s){try{const i={full_hash:r.hash??"",fingerprint_id:r.stable_hash??"",timestamp:r.collected_at??(new Date).toISOString(),isIncognito:r.is_incognito??!1,components:r.components??{},version:r.version??"inline-1.0.0"};let n,a,o;try{const r=t.resolveUserId(e);r&&!(0,sentinel_user_id_1.isSentinelUserId)(r)&&(n=r)}catch{}if(!n){const e="string"==typeof r.user_id?r.user_id:void 0;e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(n=e)}if(!n){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_uid");r&&!(0,sentinel_user_id_1.isSentinelUserId)(r)&&(n=r)}try{a=t.resolveEmailAddress?t.resolveEmailAddress(e):void 0}catch{}a=a??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")??r.email??void 0;try{o=t.resolveSessionId?t.resolveSessionId(e):void 0}catch{}o=o??r.session_id??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid");const d=(0,web_helpers_1.extractClientIpFromRequest)(e),c=e.headers.get("user-agent")??"";if(!t.disableBotFilter&&(0,is_bot_1.isBot)(c))return(0,web_helpers_1.jsonResponse)(200,{success:!0},s);const _=(i.fingerprint_id&&i.fingerprint_id.length>0?i.fingerprint_id:void 0)??(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),l=i.fingerprint_id||void 0,u=i.full_hash||void 0,p=(0,web_helpers_1.isSecureWebRequest)(e)?"; Secure":"",h=[];if(u&&!(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")&&h.push(`__unshared_fingerprint_id=${encodeURIComponent(u)}; HttpOnly; Path=/; SameSite=Lax${p}`),l){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fp_id");r&&r===l||h.push(`__unshared_fp_id=${encodeURIComponent(l)}; Path=/; SameSite=Lax; Max-Age=31536000${p}`)}let f;if(a&&!(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")&&h.push(`__unshared_email=${encodeURIComponent(a)}; HttpOnly; Path=/; SameSite=Lax${p}`),"string"==typeof r.event_type&&r.event_type)f=r.event_type;else{const r=e.headers.get("referer")??e.headers.get("referrer");let t="unknown";if(r)try{const e=new URL(r);t=(e.pathname||"/")+(e.search||"")}catch{}f=t}const m=e.headers.get("x-idempotency-key")||void 0,w=Date.now(),g=m?`${m}|${w}`:l&&n?`${(0,util_1.sha256Hex)(`${l}|${n}|${f}`)}|${w}`:void 0;n&&t.client.submitFingerprintEvent(i,{userId:n,emailAddress:a,sessionHash:o,eventType:f,ipAddress:d,userAgent:c,idempotencyKey:g}).catch(e=>{t.onError&&t.onError(e,{operation:"submitFingerprintEvent",userId:n,emailAddress:a})}),n&&a&&!t.rateLimitBackoff.isPaused()&&!t.dispatchDedupe.wasRecentlyDispatched(n,f)&&t.client.processUserEvent({eventType:f,userId:n,emailAddress:a,ipAddress:d,deviceId:_,fingerprintId:l,sessionHash:o??"unknown",userAgent:c}).then(e=>{e.success&&e.data?.analysis&&t.verdictCache.update(n,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.rateLimitBackoff.pause(1e3*e.error.retryAfter)}).catch(e=>{t.onError&&t.onError(e,{operation:"processUserEvent",userId:n,emailAddress:a})});const b={...s,"Content-Type":"application/json"},v=new Response(JSON.stringify({success:!0}),{status:200,headers:b});for(const e of h)v.headers.append("Set-Cookie",e);return v}catch{return(0,web_helpers_1.jsonResponse)(200,{success:!0},s)}}async function handleVerifyTriggerWeb(e,r,t,s){try{const i=resolveEmailWithBody(e,r??{},t.resolveEmailAddress);if(!i)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},s);const n=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),a=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,o=await t.client.triggerEmailVerification(i,n,{fingerprintId:a});return o.success?(0,web_helpers_1.jsonResponse)(200,{success:!0,data:o.data},s):(0,web_helpers_1.jsonResponse)(200,{success:!1,error:o.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},s)}catch(e){return t.onError&&t.onError(e,{operation:"verifyTrigger"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},s)}}async function handleVerifyWeb(e,r,t,s){try{const i=resolveEmailWithBody(e,r??{},t.resolveEmailAddress),n=r?.code;if(!i||!n)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}},s);const a=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),o=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,d=await t.client.verify(i,a,n,{fingerprintId:o});if(d.success){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_uid");return r&&t.verdictCache.update(r,{isVerified:!0}),(0,web_helpers_1.jsonResponse)(200,{success:!0,data:{verified:!0}},s)}return(0,web_helpers_1.jsonResponse)(200,{success:!1,error:d.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}},s)}catch(e){return t.onError&&t.onError(e,{operation:"verify"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},s)}}
|
|
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=500;function createWebProtectionMiddleware(e,r){if(!r.userId)throw new Error("[Unshared] userId resolver is required");const{userId:t,emailAddress:s,routePrefix:i="/__unshared",corsOrigins:n,cacheTTL:a=6e4,skipPaths:o,includePathPrefix:d,sessionId:_,deviceId:c,fingerprintSdkBundle:l="",onFlagged:u,onError:p,onFailOpen:h,disableBotFilter:f=!1,checkUserTimeoutMs:m=CHECK_USER_TIMEOUT_MS,blockFlagged:w=!1,flaggedMode:g,autoInterstitial:b=!1,interstitialFlowType:v="email_verification"}=r,I=g??(w?"gate":void 0),y=b||"overlay"===I,A=e=>{if(h)try{h(e)}catch{}},S=Date.now().toString(36),E=void 0!==I||y,R="gate"===I?"flaggedMode 'gate' (blockFlagged)":"overlay"===I?"flaggedMode 'overlay'":"autoInterstitial";if(E&&!l)throw new Error(`[Unshared] ${R} requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the interstitial).`);if(E&&"/__unshared"!==i)throw new Error(`[Unshared] ${R} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const T="gate"===I?(0,gate_page_1.generateGatePage)(i,S):"",k=new verdict_cache_1.VerdictCache(a),C=new rate_limit_backoff_1.RateLimitBackoff,x=new dispatch_dedupe_1.DispatchDedupe,U=(0,fingerprint_script_1.generateFingerprintScript)(i,S,{autoInterstitial:y,interstitialFlowType:v}),O=`${i}/fp.js`,$=`${i}/submit-fp`,F=`${i}/verify-trigger`,P=`${i}/verify`,q=`${i}/status`,L=n?Array.isArray(n)?n:[n]:null;return async function(r,n){let a,h,w;try{const e=new URL(r.url);a=e.pathname,h=e.search}catch{return n(r)}if(a.startsWith(i+"/")){const i=function(e){if(!L)return{};const r=e.headers.get("origin")??"",t=L.includes("*");return t||L.includes(r)?{"Access-Control-Allow-Origin":t?"*":r,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id","Access-Control-Allow-Credentials":"true"}:{}}(r);if("OPTIONS"===r.method)return(0,web_helpers_1.emptyResponse)(204,i);if("GET"===r.method&&a===O)return l?(0,web_helpers_1.bodyResponse)(200,l,{...i,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600"}):(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},i);if("POST"===r.method&&(a===$||a===F||a===P)){let n;try{n=await r.json()}catch{return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"Request body is not valid JSON."}},i)}return a===$?handleSubmitFp(r,n,{client:e,verdictCache:k,rateLimitBackoff:C,dispatchDedupe:x,resolveUserId:t,resolveEmailAddress:s,resolveSessionId:_,resolveDeviceId:c,disableBotFilter:f,onError:p},i):a===F?handleVerifyTriggerWeb(r,n,{client:e,verdictCache:k,resolveEmailAddress:s,resolveDeviceId:c,onError:p},i):handleVerifyWeb(r,n,{client:e,verdictCache:k,resolveEmailAddress:s,resolveDeviceId:c,onError:p},i)}if("GET"===r.method&&a===q){let n;try{n=t(r)}catch{}if(!n)return(0,web_helpers_1.jsonResponse)(200,{status:"anonymous"},i);const a=resolveEmail(r,s);let o=k.get(n);if((!o||k.isStale(n))&&a&&!C.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&&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),b=[],v=(0,web_helpers_1.isSecureWebRequest)(r)?"; Secure":"";if(b.push(`__unshared_uid=${encodeURIComponent(w)}; Path=/; SameSite=Lax${v}`),b.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${v}`),g&&b.push(`__unshared_email=${encodeURIComponent(g)}; HttpOnly; Path=/; SameSite=Lax${v}`),!g)return injectIntoHtmlResponse(await n(r),U,b);const y=extractSessionIdFromRequest(r,_),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")??"",M=(0,web_helpers_1.extractClientIpFromRequest)(r),D=S??E;if(!f&&(0,is_bot_1.isBot)(R))return n(r);let N=k.get(w);if(N)k.isStale(w)&&!k.isRefreshing(w)&&(k.markRefreshing(w),fetchAndCacheVerdict(e,k,w,g,D??"unknown",E,y,m,(e,r)=>A({operation:"checkUser",reason:e,status:r,userId:w,emailAddress:g})).catch(()=>A({operation:"checkUser",reason:"exception",userId:w,emailAddress:g})).finally(()=>k.clearRefreshing(w)));else try{N=await fetchAndCacheVerdict(e,k,w,g,D??"unknown",E,y,m,(e,r)=>A({operation:"checkUser",reason:e,status:r,userId:w,emailAddress:g}))}catch{return A({operation:"checkUser",reason:"exception",userId:w,emailAddress:g}),injectIntoHtmlResponse(await n(r),U,b)}const H=N.isFlagged&&!N.isVerified;if("gate"===I&&H)return(0,content_type_1.isHtmlNavigation)(r.method,r.headers.get("accept")??void 0)?(0,web_helpers_1.bodyResponse)(200,T,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}):(0,web_helpers_1.jsonResponse)(403,(0,flagged_response_1.flaggedResponse)(g));if("overlay"===I&&H)return(0,content_type_1.isHtmlNavigation)(r.method,r.headers.get("accept")??void 0)?injectIntoHtmlResponse(await n(r),U,b):(0,web_helpers_1.jsonResponse)(403,(0,flagged_response_1.flaggedResponse)(g));if(N.isFlagged&&!N.isVerified&&u)try{const e=await u({userId:w,emailAddress:g,verdict:N,request:r});if(e)return injectIntoHtmlResponse(e,U,b)}catch(e){p&&p(e,{operation:"checkUser",userId:w,emailAddress:g})}return N.isFlagged||"unknown"===y||!D||C.isPaused()||dispatchUserEvent(e,k,C,x,{userId:w,emailAddress:g,sessionId:y,deviceId:D,fingerprintId:E,userAgent:R,ipAddress:M,eventType:a+h},p),injectIntoHtmlResponse(await n(r),U,b)}}async function injectIntoHtmlResponse(e,r,t){const s=e.headers.get("content-type");if(!(0,content_type_1.isHtmlContentType)(s??void 0)){if(!t||0===t.length)return e;const r=(0,web_helpers_1.mergeResponseHeaders)(e.headers,void 0,t);return new Response(e.body,{status:e.status,statusText:e.statusText,headers:r})}const i=await e.text(),n=i.lastIndexOf("</body>"),a=-1===n?i+r:i.slice(0,n)+r+i.slice(n),o=(0,web_helpers_1.mergeResponseHeaders)(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(a).length)},t);return o.delete("ETag"),o.delete("Last-Modified"),o.delete("Content-Encoding"),new Response(a,{status:e.status,statusText:e.statusText,headers:o})}function resolveEmail(e,r){if(r)try{const t=r(e);if(t)return t}catch{}const t=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email");if(t)return t}function resolveEmailWithBody(e,r,t){const s=resolveEmail(e,t);if(s)return s;const i=r.email;return"string"==typeof i&&i?i:void 0}function extractSessionIdFromRequest(e,r){if(r)try{const t=r(e);if(t)return t}catch{}return(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid")??"unknown"}function dispatchUserEvent(e,r,t,s,i,n){s.mark(i.userId,i.eventType),e.processUserEvent({eventType:i.eventType,userId:i.userId,emailAddress:i.emailAddress,ipAddress:i.ipAddress,deviceId:i.deviceId,fingerprintId:i.fingerprintId,sessionHash:i.sessionId,userAgent:i.userAgent}).then(e=>{e.success&&e.data?.analysis&&r.update(i.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.pause(1e3*e.error.retryAfter)}).catch(e=>{n&&n(e,{operation:"processUserEvent",userId:i.userId,emailAddress:i.emailAddress})})}async function fetchAndCacheVerdict(e,r,t,s,i,n,a,o=CHECK_USER_TIMEOUT_MS,d){const _={};let c;i&&"unknown"!==i&&(_.deviceId=i),n&&(_.fingerprintId=n);const l=await Promise.race([e.checkUser(s,_),new Promise(e=>{c=setTimeout(()=>e(null),o)})]);if(clearTimeout(c),!l)return d?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:s,sessionId:a,cachedAt:0,ttl:0};l.failedOpen&&d?.("http_error",l.failedOpen.status);const u=l.data?.is_user_flagged??!1;return r.set(t,{isFlagged:u,isVerified:!1,emailAddress:s,sessionId:a}),r.get(t)}async function handleSubmitFp(e,r,t,s){try{const i={full_hash:r.hash??"",fingerprint_id:r.stable_hash??"",timestamp:r.collected_at??(new Date).toISOString(),isIncognito:r.is_incognito??!1,components:r.components??{},version:r.version??"inline-1.0.0"};let n,a,o;try{const r=t.resolveUserId(e);r&&!(0,sentinel_user_id_1.isSentinelUserId)(r)&&(n=r)}catch{}if(!n){const e="string"==typeof r.user_id?r.user_id:void 0;e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(n=e)}if(!n){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_uid");r&&!(0,sentinel_user_id_1.isSentinelUserId)(r)&&(n=r)}try{a=t.resolveEmailAddress?t.resolveEmailAddress(e):void 0}catch{}a=a??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")??r.email??void 0;try{o=t.resolveSessionId?t.resolveSessionId(e):void 0}catch{}o=o??r.session_id??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid");const d=(0,web_helpers_1.extractClientIpFromRequest)(e),_=e.headers.get("user-agent")??"";if(!t.disableBotFilter&&(0,is_bot_1.isBot)(_))return(0,web_helpers_1.jsonResponse)(200,{success:!0},s);const c=(i.fingerprint_id&&i.fingerprint_id.length>0?i.fingerprint_id:void 0)??(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),l=i.fingerprint_id||void 0,u=i.full_hash||void 0,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 b={...s,"Content-Type":"application/json"},v=new Response(JSON.stringify({success:!0}),{status:200,headers:b});for(const e of h)v.headers.append("Set-Cookie",e);return v}catch{return(0,web_helpers_1.jsonResponse)(200,{success:!0},s)}}async function handleVerifyTriggerWeb(e,r,t,s){try{const i=resolveEmailWithBody(e,r??{},t.resolveEmailAddress);if(!i)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},s);const n=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),a=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,o=await t.client.triggerEmailVerification(i,n,{fingerprintId:a});return o.success?(0,web_helpers_1.jsonResponse)(200,{success:!0,data:o.data},s):(0,web_helpers_1.jsonResponse)(200,{success:!1,error:o.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},s)}catch(e){return t.onError&&t.onError(e,{operation:"verifyTrigger"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},s)}}async function handleVerifyWeb(e,r,t,s){try{const i=resolveEmailWithBody(e,r??{},t.resolveEmailAddress),n=r?.code;if(!i||!n)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}},s);const a=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),o=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,d=await t.client.verify(i,a,n,{fingerprintId:o});if(d.success){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_uid");return r&&t.verdictCache.update(r,{isVerified:!0}),(0,web_helpers_1.jsonResponse)(200,{success:!0,data:{verified:!0}},s)}return(0,web_helpers_1.jsonResponse)(200,{success:!1,error:d.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}},s)}catch(e){return t.onError&&t.onError(e,{operation:"verify"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},s)}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unshared-clientjs-sdk",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Server-side Node.js SDK for the Unshared Labs V2 API",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/esm/index.mjs",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"author": "",
|
|
53
53
|
"license": "MIT",
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"unshared-frontend-sdk": "2.1.
|
|
55
|
+
"unshared-frontend-sdk": "2.1.1"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@unshared-labs/shared-types": "file:../../../shared/types",
|