unshared-clientjs-sdk 2.1.0-rc.6 → 2.1.0-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/middleware/index.d.mts +22 -0
- package/dist/esm/middleware/index.mjs +1 -1
- package/dist/esm/web/protection-handler.mjs +1 -1
- package/dist/esm/web/types.d.mts +18 -0
- package/dist/middleware/index.d.ts +22 -0
- package/dist/middleware/index.js +1 -1
- package/dist/web/protection-handler.js +1 -1
- package/dist/web/types.d.ts +18 -0
- package/package.json +2 -2
|
@@ -60,8 +60,30 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
|
|
|
60
60
|
*
|
|
61
61
|
* When `true`, the SDK owns the flagged response and `onFlagged` is ignored. Requires
|
|
62
62
|
* `unshared-frontend-sdk` to be installed and the default `routePrefix`. @default false
|
|
63
|
+
*
|
|
64
|
+
* `blockFlagged: true` is equivalent to `flaggedMode: 'gate'` (see {@link flaggedMode}).
|
|
63
65
|
*/
|
|
64
66
|
blockFlagged?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* How the SDK enforces a flagged + unverified user. Selects the flagged-user
|
|
69
|
+
* experience; setting it enables enforcement on its own (you do NOT also need
|
|
70
|
+
* `blockFlagged`). When both are set, `flaggedMode` wins.
|
|
71
|
+
*
|
|
72
|
+
* - `'gate'` — **hard gate** (the strongest guarantee, == `blockFlagged: true`). HTML
|
|
73
|
+
* navigations get a standalone verification gate page with no app markup; everything
|
|
74
|
+
* else gets `403 { error: 'account_flagged' }`. The protected content is never sent.
|
|
75
|
+
* - `'overlay'` — **modal over blurred live content**. HTML navigations render normally
|
|
76
|
+
* (the page IS delivered) and the SDK auto-renders the (non-dismissible) interstitial
|
|
77
|
+
* modal over the blurred page; non-HTML/data requests get `403 account_flagged`.
|
|
78
|
+
* Implies `autoInterstitial`. Use `includePathPrefix` to scope which data routes are
|
|
79
|
+
* gated. Honest trade-off: the HTML is delivered to the client and the blur is
|
|
80
|
+
* cosmetic — the real protection is the `403` on the gated data, so put sensitive
|
|
81
|
+
* content behind gated endpoints.
|
|
82
|
+
*
|
|
83
|
+
* Both modes ignore `onFlagged` (the SDK owns the response) and require
|
|
84
|
+
* `unshared-frontend-sdk` installed + the default `routePrefix`. @default undefined
|
|
85
|
+
*/
|
|
86
|
+
flaggedMode?: 'gate' | 'overlay';
|
|
65
87
|
/**
|
|
66
88
|
* Auto-render the interstitial modal on SPA/JSON `403 account_flagged` paths
|
|
67
89
|
* (and the deferred `/status` poll) with no app code. When `true`, the injected
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFileSync}from"fs";import{VerdictCache}from"./verdict-cache";import{RateLimitBackoff}from"./rate-limit-backoff";import{DispatchDedupe}from"./dispatch-dedupe";import{interceptResponse}from"./response-interceptor";import{generateFingerprintScript}from"./injection/fingerprint-script";import{handleSubmitFingerprint}from"./routes/submit-fp";import{handleVerifyTrigger,handleVerify}from"./routes/verify";import{handleGetInterstitialFlow}from"./routes/interstitial";import{generateGatePage}from"./injection/gate-page";import{sendJson,sendEmpty,sendBody,getRequestPath}from"./utils/http-helpers";import{isHtmlContentType,isHtmlNavigation}from"./utils/content-type";import{flaggedResponse}from"./utils/flagged-response";import{shouldSkipPath}from"./utils/skip-paths";import{shouldIncludePath}from"./utils/include-path";import{isBot}from"./utils/is-bot";import{extractClientIp}from"./utils/client-ip";import{parseCookie}from"./utils/cookies";import{extractDeviceIdOrUndefined}from"./utils/device-id";import{isSecureRequest}from"./utils/secure";import{isSentinelUserId,SENTINEL_STICKINESS_TTL_MS}from"./utils/sentinel-user-id";export{VerdictCache};export{flaggedResponse,ACCOUNT_FLAGGED_ERROR}from"./utils/flagged-response";const CHECK_USER_TIMEOUT_MS=500;export function unsharedBoundToUser(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");if(!t.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:r,emailAddress:i,routePrefix:n="/__unshared",corsOrigins:s,cacheTTL:o=6e4,skipPaths:d,includePathPrefix:a,disableBotFilter:c=!1,checkUserTimeoutMs:u=CHECK_USER_TIMEOUT_MS,sessionId:l,deviceId:p,onFlagged:f,onError:h,onFailOpen:m,blockFlagged:g=!1,autoInterstitial:I=!1,interstitialFlowType:S="email_verification"}=t,_=e=>{if(m)try{m(e)}catch{}},v=new VerdictCache(o),C=new RateLimitBackoff,k=new DispatchDedupe,y=Date.now().toString(36),A=generateFingerprintScript(n,y,{autoInterstitial:I,interstitialFlowType:S});let E="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");E=readFileSync(e,"utf8")}catch{}if(g&&!E)throw new Error("[Unshared] blockFlagged requires unshared-frontend-sdk to be installed (its UMD bundle is the gate-page renderer).");if(g&&"/__unshared"!==n)throw new Error('[Unshared] blockFlagged requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');if(I&&!E)throw new Error("[Unshared] autoInterstitial requires unshared-frontend-sdk to be installed (its UMD bundle boots the auto-rendered interstitial).");if(I&&"/__unshared"!==n)throw new Error('[Unshared] autoInterstitial requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');const x=g?generateGatePage(n,y):"",T=handleSubmitFingerprint({client:e,verdictCache:v,rateLimitBackoff:C,dispatchDedupe:k,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:l,resolveDeviceId:p,disableBotFilter:c,onError:h}),U=handleVerifyTrigger({client:e,verdictCache:v,resolveEmailAddress:i,resolveDeviceId:p,onError:h}),w=handleVerify({client:e,verdictCache:v,resolveEmailAddress:i,resolveDeviceId:p,onError:h}),F=handleGetInterstitialFlow({client:e}),P=s?Array.isArray(s)?s:[s]:null,b=`${n}/fp.js`,R=`${n}/submit-fp`,D=`${n}/verify-trigger`,O=`${n}/verify`,N=`${n}/status`,M=`${n}/interstitial-flow`;return function(t,s,o){const m=getRequestPath(t.url),I=t.url||m;if(m.startsWith(n+"/")){if(function(e,t){if(!P)return;const r=e.headers.origin??"",i=P.includes("*");(i||P.includes(r))&&(t.setHeader("Access-Control-Allow-Origin",i?"*":r),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,s),"OPTIONS"===t.method)return void sendEmpty(s,204);if("GET"===t.method&&m===b)return s.setHeader("Content-Type","application/javascript"),s.setHeader("Cache-Control","public, max-age=3600"),void sendBody(s,200,E);if("POST"===t.method&&(m===R||m===D||m===O))return void 0===t.body?void sendJson(s,400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"req.body is undefined. Mount a JSON body-parsing middleware (e.g., express.json()) before the Unshared middleware."}}):m===R?void T(t,s):m===D?void U(t,s):void w(t,s);if("GET"===t.method&&m===M)return void F(t,s);if("GET"===t.method&&m===N){let n;try{n=r(t)}catch{}if(!n)return void sendJson(s,200,{status:"anonymous"});const o=resolveEmail(t,i);return void(async()=>{let r=v.get(n);if((!r||v.isStale(n))&&o&&!C.isPaused()&&!v.isRefreshing(n)){v.markRefreshing(n);try{const i=extractDeviceIdOrUndefined(t,p),s=extractFingerprintId(t),d=extractSessionId(t,l),a=i??s??"unknown";await fetchAndCacheVerdict(e,v,n,o,a,s,d,u,(e,t)=>_({operation:"checkUser",reason:e,status:t,userId:n,emailAddress:o})),r=v.get(n)}catch(e){h&&h(e,{operation:"checkUser",userId:n,emailAddress:o}),_({operation:"checkUser",reason:"exception",userId:n,emailAddress:o})}finally{v.clearRefreshing(n)}}r&&r.isFlagged&&!r.isVerified&&f&&o?sendJson(s,200,{status:"flagged",email:o}):sendJson(s,200,{status:"ok"})})()}return void sendJson(s,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if(shouldSkipPath(m,d))return void o();if(!shouldIncludePath(m,a))return interceptForInjection(t,s,A),void o();let S;try{S=r(t)}catch{}if(isSentinelUserId(S)){const e=parseCookie(t,"__unshared_uid"),r=parseCookie(t,"__unshared_uid_at"),i=r?Number(r):NaN,n=Number.isFinite(i)&&Date.now()-i<=SENTINEL_STICKINESS_TTL_MS;S=e&&n?e:void 0}if(!S){const e=isSecureRequest(t)?"; Secure":"";return appendSetCookie(s,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(t,s,A),void o()}const y=resolveEmail(t,i);if(setUserIdCookie(t,s,S),y&&setEmailCookie(t,s,y),!y)return interceptForInjection(t,s,A),void o();const L=extractSessionId(t,l),V=extractDeviceIdOrUndefined(t,p),$=extractFingerprintId(t),q=t.headers["user-agent"]??"",j=extractClientIp(t),B=V??$;if(!c&&isBot(q))return void o();const G=v.get(S);function H(){"unknown"!==L&&B&&(C.isPaused()||dispatchUserEvent(e,v,C,k,{userId:S,emailAddress:y,sessionId:L,deviceId:B,fingerprintId:$,userAgent:q,ipAddress:j,eventType:I},h))}G?(v.isStale(S)&&!v.isRefreshing(S)&&(v.markRefreshing(S),fetchAndCacheVerdict(e,v,S,y,B??"unknown",$,L,u,(e,t)=>_({operation:"checkUser",reason:e,status:t,userId:S,emailAddress:y})).catch(()=>_({operation:"checkUser",reason:"exception",userId:S,emailAddress:y})).finally(()=>v.clearRefreshing(S))),G.isFlagged||H(),applyVerdict(G,S,y,t,s,o,A,f,g,x)):fetchAndCacheVerdict(e,v,S,y,B??"unknown",$,L,u,(e,t)=>_({operation:"checkUser",reason:e,status:t,userId:S,emailAddress:y})).then(e=>{e.isFlagged||H(),applyVerdict(e,S,y,t,s,o,A,f,g,x)}).catch(()=>{_({operation:"checkUser",reason:"exception",userId:S,emailAddress:y}),H(),interceptForInjection(t,s,A),o()})}}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=parseCookie(e,"__unshared_email");if(r)return r;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,t,r,i,n,s,o,d,a,c){if(a&&e.isFlagged&&!e.isVerified)isHtmlNavigation(i.method,i.headers.accept)?(n.statusCode=200,n.setHeader("Content-Type","text/html; charset=utf-8"),n.setHeader("Cache-Control","no-store"),n.end(c)):sendJson(n,403,flaggedResponse(r));else if(interceptForInjection(i,n,o),e.isFlagged&&!e.isVerified&&d)try{d({userId:t,emailAddress:r,verdict:e,req:i,res:n,next:s})}catch{s()}else s()}function interceptForInjection(e,t,r){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],interceptResponse(t,(e,t)=>{if(!isHtmlContentType(t))return null;const i=e.toString("utf8"),n=i.lastIndexOf("</body>");return-1===n?i+r:i.slice(0,n)+r+i.slice(n)},{preventCaching:!0})}function dispatchUserEvent(e,t,r,i,n,s){i.mark(n.userId,n.eventType),e.processUserEvent({eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,sessionHash:n.sessionId,userAgent:n.userAgent}).then(e=>{e.success&&e.data?.analysis&&t.update(n.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&r.pause(1e3*e.error.retryAfter)}).catch(e=>{s&&s(e,{operation:"processUserEvent",userId:n.userId,emailAddress:n.emailAddress})})}async function fetchAndCacheVerdict(e,t,r,i,n,s,o,d=CHECK_USER_TIMEOUT_MS,a){const c={};let u;n&&"unknown"!==n&&(c.deviceId=n),s&&(c.fingerprintId=s);const l=await Promise.race([e.checkUser(i,c),new Promise(e=>{u=setTimeout(()=>e(null),d)})]);if(clearTimeout(u),!l)return a?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:o,cachedAt:0,ttl:0};l.failedOpen&&a?.("http_error",l.failedOpen.status);const p=l.data?.is_user_flagged??!1;return t.set(r,{isFlagged:p,isVerified:!1,emailAddress:i,sessionId:o}),t.get(r)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return parseCookie(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return parseCookie(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
|
|
1
|
+
import{readFileSync}from"fs";import{VerdictCache}from"./verdict-cache";import{RateLimitBackoff}from"./rate-limit-backoff";import{DispatchDedupe}from"./dispatch-dedupe";import{interceptResponse}from"./response-interceptor";import{generateFingerprintScript}from"./injection/fingerprint-script";import{handleSubmitFingerprint}from"./routes/submit-fp";import{handleVerifyTrigger,handleVerify}from"./routes/verify";import{handleGetInterstitialFlow}from"./routes/interstitial";import{generateGatePage}from"./injection/gate-page";import{sendJson,sendEmpty,sendBody,getRequestPath}from"./utils/http-helpers";import{isHtmlContentType,isHtmlNavigation}from"./utils/content-type";import{flaggedResponse}from"./utils/flagged-response";import{shouldSkipPath}from"./utils/skip-paths";import{shouldIncludePath}from"./utils/include-path";import{isBot}from"./utils/is-bot";import{extractClientIp}from"./utils/client-ip";import{parseCookie}from"./utils/cookies";import{extractDeviceIdOrUndefined}from"./utils/device-id";import{isSecureRequest}from"./utils/secure";import{isSentinelUserId,SENTINEL_STICKINESS_TTL_MS}from"./utils/sentinel-user-id";export{VerdictCache};export{flaggedResponse,ACCOUNT_FLAGGED_ERROR}from"./utils/flagged-response";const CHECK_USER_TIMEOUT_MS=500;export function unsharedBoundToUser(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");if(!t.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:r,emailAddress:i,routePrefix:n="/__unshared",corsOrigins:s,cacheTTL:o=6e4,skipPaths:d,includePathPrefix:a,disableBotFilter:c=!1,checkUserTimeoutMs:l=CHECK_USER_TIMEOUT_MS,sessionId:u,deviceId:p,onFlagged:f,onError:m,onFailOpen:h,blockFlagged:g=!1,flaggedMode:I,autoInterstitial:S=!1,interstitialFlowType:v="email_verification"}=t,_=I??(g?"gate":void 0),C=S||"overlay"===_,y=e=>{if(h)try{h(e)}catch{}},k=new VerdictCache(o),A=new RateLimitBackoff,E=new DispatchDedupe,T=Date.now().toString(36),x=generateFingerprintScript(n,T,{autoInterstitial:C,interstitialFlowType:v});let U="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");U=readFileSync(e,"utf8")}catch{}const w=void 0!==_||C,F="gate"===_?"flaggedMode 'gate' (blockFlagged)":"overlay"===_?"flaggedMode 'overlay'":"autoInterstitial";if(w&&!U)throw new Error(`[Unshared] ${F} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(w&&"/__unshared"!==n)throw new Error(`[Unshared] ${F} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const P="gate"===_?generateGatePage(n,T):"",R=handleSubmitFingerprint({client:e,verdictCache:k,rateLimitBackoff:A,dispatchDedupe:E,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:u,resolveDeviceId:p,disableBotFilter:c,onError:m}),b=handleVerifyTrigger({client:e,verdictCache:k,resolveEmailAddress:i,resolveDeviceId:p,onError:m}),O=handleVerify({client:e,verdictCache:k,resolveEmailAddress:i,resolveDeviceId:p,onError:m}),N=handleGetInterstitialFlow({client:e}),D=s?Array.isArray(s)?s:[s]:null,M=`${n}/fp.js`,$=`${n}/submit-fp`,L=`${n}/verify-trigger`,V=`${n}/verify`,j=`${n}/status`,q=`${n}/interstitial-flow`;return function(t,s,o){const h=getRequestPath(t.url),g=t.url||h;if(h.startsWith(n+"/")){if(function(e,t){if(!D)return;const r=e.headers.origin??"",i=D.includes("*");(i||D.includes(r))&&(t.setHeader("Access-Control-Allow-Origin",i?"*":r),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,s),"OPTIONS"===t.method)return void sendEmpty(s,204);if("GET"===t.method&&h===M)return s.setHeader("Content-Type","application/javascript"),s.setHeader("Cache-Control","public, max-age=3600"),void sendBody(s,200,U);if("POST"===t.method&&(h===$||h===L||h===V))return void 0===t.body?void sendJson(s,400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"req.body is undefined. Mount a JSON body-parsing middleware (e.g., express.json()) before the Unshared middleware."}}):h===$?void R(t,s):h===L?void b(t,s):void O(t,s);if("GET"===t.method&&h===q)return void N(t,s);if("GET"===t.method&&h===j){let n;try{n=r(t)}catch{}if(!n)return void sendJson(s,200,{status:"anonymous"});const o=resolveEmail(t,i);return void(async()=>{let r=k.get(n);if((!r||k.isStale(n))&&o&&!A.isPaused()&&!k.isRefreshing(n)){k.markRefreshing(n);try{const i=extractDeviceIdOrUndefined(t,p),s=extractFingerprintId(t),d=extractSessionId(t,u),a=i??s??"unknown";await fetchAndCacheVerdict(e,k,n,o,a,s,d,l,(e,t)=>y({operation:"checkUser",reason:e,status:t,userId:n,emailAddress:o})),r=k.get(n)}catch(e){m&&m(e,{operation:"checkUser",userId:n,emailAddress:o}),y({operation:"checkUser",reason:"exception",userId:n,emailAddress:o})}finally{k.clearRefreshing(n)}}r&&r.isFlagged&&!r.isVerified&&f&&o?sendJson(s,200,{status:"flagged",email:o}):sendJson(s,200,{status:"ok"})})()}return void sendJson(s,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if(shouldSkipPath(h,d))return void o();if(!shouldIncludePath(h,a))return interceptForInjection(t,s,x),void o();let I;try{I=r(t)}catch{}if(isSentinelUserId(I)){const e=parseCookie(t,"__unshared_uid"),r=parseCookie(t,"__unshared_uid_at"),i=r?Number(r):NaN,n=Number.isFinite(i)&&Date.now()-i<=SENTINEL_STICKINESS_TTL_MS;I=e&&n?e:void 0}if(!I){const e=isSecureRequest(t)?"; Secure":"";return appendSetCookie(s,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(t,s,x),void o()}const S=resolveEmail(t,i);if(setUserIdCookie(t,s,I),S&&setEmailCookie(t,s,S),!S)return interceptForInjection(t,s,x),void o();const v=extractSessionId(t,u),C=extractDeviceIdOrUndefined(t,p),T=extractFingerprintId(t),w=t.headers["user-agent"]??"",F=extractClientIp(t),B=C??T;if(!c&&isBot(w))return void o();const G=k.get(I);function H(){"unknown"!==v&&B&&(A.isPaused()||dispatchUserEvent(e,k,A,E,{userId:I,emailAddress:S,sessionId:v,deviceId:B,fingerprintId:T,userAgent:w,ipAddress:F,eventType:g},m))}G?(k.isStale(I)&&!k.isRefreshing(I)&&(k.markRefreshing(I),fetchAndCacheVerdict(e,k,I,S,B??"unknown",T,v,l,(e,t)=>y({operation:"checkUser",reason:e,status:t,userId:I,emailAddress:S})).catch(()=>y({operation:"checkUser",reason:"exception",userId:I,emailAddress:S})).finally(()=>k.clearRefreshing(I))),G.isFlagged||H(),applyVerdict(G,I,S,t,s,o,x,f,_,P)):fetchAndCacheVerdict(e,k,I,S,B??"unknown",T,v,l,(e,t)=>y({operation:"checkUser",reason:e,status:t,userId:I,emailAddress:S})).then(e=>{e.isFlagged||H(),applyVerdict(e,I,S,t,s,o,x,f,_,P)}).catch(()=>{y({operation:"checkUser",reason:"exception",userId:I,emailAddress:S}),H(),interceptForInjection(t,s,x),o()})}}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=parseCookie(e,"__unshared_email");if(r)return r;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,t,r,i,n,s,o,d,a,c){const l=e.isFlagged&&!e.isVerified;if("gate"===a&&l)isHtmlNavigation(i.method,i.headers.accept)?(n.statusCode=200,n.setHeader("Content-Type","text/html; charset=utf-8"),n.setHeader("Cache-Control","no-store"),n.end(c)):sendJson(n,403,flaggedResponse(r));else if("overlay"===a&&l)isHtmlNavigation(i.method,i.headers.accept)?(interceptForInjection(i,n,o),s()):sendJson(n,403,flaggedResponse(r));else if(interceptForInjection(i,n,o),e.isFlagged&&!e.isVerified&&d)try{d({userId:t,emailAddress:r,verdict:e,req:i,res:n,next:s})}catch{s()}else s()}function interceptForInjection(e,t,r){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],interceptResponse(t,(e,t)=>{if(!isHtmlContentType(t))return null;const i=e.toString("utf8"),n=i.lastIndexOf("</body>");return-1===n?i+r:i.slice(0,n)+r+i.slice(n)},{preventCaching:!0})}function dispatchUserEvent(e,t,r,i,n,s){i.mark(n.userId,n.eventType),e.processUserEvent({eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,sessionHash:n.sessionId,userAgent:n.userAgent}).then(e=>{e.success&&e.data?.analysis&&t.update(n.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&r.pause(1e3*e.error.retryAfter)}).catch(e=>{s&&s(e,{operation:"processUserEvent",userId:n.userId,emailAddress:n.emailAddress})})}async function fetchAndCacheVerdict(e,t,r,i,n,s,o,d=CHECK_USER_TIMEOUT_MS,a){const c={};let l;n&&"unknown"!==n&&(c.deviceId=n),s&&(c.fingerprintId=s);const u=await Promise.race([e.checkUser(i,c),new Promise(e=>{l=setTimeout(()=>e(null),d)})]);if(clearTimeout(l),!u)return a?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:o,cachedAt:0,ttl:0};u.failedOpen&&a?.("http_error",u.failedOpen.status);const p=u.data?.is_user_flagged??!1;return t.set(r,{isFlagged:p,isVerified:!1,emailAddress:i,sessionId:o}),t.get(r)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return parseCookie(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return parseCookie(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
|
|
@@ -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,r){if(!r.userId)throw new Error("[Unshared] userId resolver is required");const{userId:t,emailAddress:s,routePrefix:n="/__unshared",corsOrigins:i,cacheTTL:o=6e4,skipPaths:a,includePathPrefix:d,sessionId:c,deviceId:u,fingerprintSdkBundle:l="",onFlagged:p,onError:m,onFailOpen:f,disableBotFilter:h=!1,checkUserTimeoutMs:_=CHECK_USER_TIMEOUT_MS,blockFlagged:g=!1,autoInterstitial:R=!1,interstitialFlowType:I="email_verification"}=r,S=e=>{if(f)try{f(e)}catch{}};if(g&&!l)throw new Error("[Unshared] blockFlagged requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the gate page).");if(g&&"/__unshared"!==n)throw new Error('[Unshared] blockFlagged requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');if(R&&!l)throw new Error("[Unshared] autoInterstitial requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js boots the auto-rendered interstitial).");if(R&&"/__unshared"!==n)throw new Error('[Unshared] autoInterstitial requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');const v=g?generateGatePage(n):"",y=new VerdictCache(o),w=new RateLimitBackoff,C=new DispatchDedupe,A=Date.now().toString(36),E=generateFingerprintScript(n,A,{autoInterstitial:R,interstitialFlowType:I}),k=`${n}/fp.js`,F=`${n}/submit-fp`,x=`${n}/verify-trigger`,T=`${n}/verify`,U=`${n}/status`,q=i?Array.isArray(i)?i:[i]:null;return async function(r,i){let o,f,R;try{const e=new URL(r.url);o=e.pathname,f=e.search}catch{return i(r)}if(o.startsWith(n+"/")){const n=function(e){if(!q)return{};const r=e.headers.get("origin")??"",t=q.includes("*");return t||q.includes(r)?{"Access-Control-Allow-Origin":t?"*":r,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id","Access-Control-Allow-Credentials":"true"}:{}}(r);if("OPTIONS"===r.method)return emptyResponse(204,n);if("GET"===r.method&&o===k)return l?bodyResponse(200,l,{...n,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600"}):jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},n);if("POST"===r.method&&(o===F||o===x||o===T)){let i;try{i=await r.json()}catch{return jsonResponse(400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"Request body is not valid JSON."}},n)}return o===F?handleSubmitFp(r,i,{client:e,verdictCache:y,rateLimitBackoff:w,dispatchDedupe:C,resolveUserId:t,resolveEmailAddress:s,resolveSessionId:c,resolveDeviceId:u,disableBotFilter:h,onError:m},n):o===x?handleVerifyTriggerWeb(r,i,{client:e,verdictCache:y,resolveEmailAddress:s,resolveDeviceId:u,onError:m},n):handleVerifyWeb(r,i,{client:e,verdictCache:y,resolveEmailAddress:s,resolveDeviceId:u,onError:m},n)}if("GET"===r.method&&o===U){let i;try{i=t(r)}catch{}if(!i)return jsonResponse(200,{status:"anonymous"},n);const o=resolveEmail(r,s);let a=y.get(i);if((!a||y.isStale(i))&&o&&!w.isPaused()&&!y.isRefreshing(i)){y.markRefreshing(i);try{const t=extractDeviceIdFromRequest(r,u),s=parseCookieFromRequest(r,"__unshared_fingerprint_id")||void 0,n=extractSessionIdFromRequest(r,c),d=t??s??"unknown";await fetchAndCacheVerdict(e,y,i,o,d,s,n,_,(e,r)=>S({operation:"checkUser",reason:e,status:r,userId:i,emailAddress:o})),a=y.get(i)}catch(e){m&&m(e,{operation:"checkUser",userId:i,emailAddress:o}),S({operation:"checkUser",reason:"exception",userId:i,emailAddress:o})}finally{y.clearRefreshing(i)}}return a&&a.isFlagged&&!a.isVerified&&p&&o?jsonResponse(200,{status:"flagged",email:o},n):jsonResponse(200,{status:"ok"},n)}return jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},n)}if(shouldSkipPath(o,a))return i(r);if(!shouldIncludePath(o,d))return injectIntoHtmlResponse(await i(r),E);try{R=t(r)}catch{}if(isSentinelUserId(R)){const e=parseCookieFromRequest(r,"__unshared_uid"),t=parseCookieFromRequest(r,"__unshared_uid_at"),s=t?Number(t):NaN,n=Number.isFinite(s)&&Date.now()-s<=SENTINEL_STICKINESS_TTL_MS;R=e&&n?e:void 0}if(!R){const e=isSecureWebRequest(r)?"; Secure":"",t=[`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`];return injectIntoHtmlResponse(await i(r),E,t)}const I=resolveEmail(r,s),A=[],b=isSecureWebRequest(r)?"; Secure":"";if(A.push(`__unshared_uid=${encodeURIComponent(R)}; Path=/; SameSite=Lax${b}`),A.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${b}`),I&&A.push(`__unshared_email=${encodeURIComponent(I)}; HttpOnly; Path=/; SameSite=Lax${b}`),!I)return injectIntoHtmlResponse(await i(r),E,A);const D=extractSessionIdFromRequest(r,c),O=extractDeviceIdFromRequest(r,u),P=parseCookieFromRequest(r,"__unshared_fingerprint_id")||void 0,j=r.headers.get("user-agent")??"",L=extractClientIpFromRequest(r),N=O??P;if(!h&&isBot(j))return i(r);let $=y.get(R);if($)y.isStale(R)&&!y.isRefreshing(R)&&(y.markRefreshing(R),fetchAndCacheVerdict(e,y,R,I,N??"unknown",P,D,_,(e,r)=>S({operation:"checkUser",reason:e,status:r,userId:R,emailAddress:I})).catch(()=>S({operation:"checkUser",reason:"exception",userId:R,emailAddress:I})).finally(()=>y.clearRefreshing(R)));else try{$=await fetchAndCacheVerdict(e,y,R,I,N??"unknown",P,D,_,(e,r)=>S({operation:"checkUser",reason:e,status:r,userId:R,emailAddress:I}))}catch{return S({operation:"checkUser",reason:"exception",userId:R,emailAddress:I}),injectIntoHtmlResponse(await i(r),E,A)}if(g&&$.isFlagged&&!$.isVerified)return isHtmlNavigation(r.method,r.headers.get("accept")??void 0)?bodyResponse(200,v,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}):jsonResponse(403,flaggedResponse(I));if($.isFlagged&&!$.isVerified&&p)try{const e=await p({userId:R,emailAddress:I,verdict:$,request:r});if(e)return injectIntoHtmlResponse(e,E,A)}catch(e){m&&m(e,{operation:"checkUser",userId:R,emailAddress:I})}return $.isFlagged||"unknown"===D||!N||w.isPaused()||dispatchUserEvent(e,y,w,C,{userId:R,emailAddress:I,sessionId:D,deviceId:N,fingerprintId:P,userAgent:j,ipAddress:L,eventType:o+f},m),injectIntoHtmlResponse(await i(r),E,A)}}async function injectIntoHtmlResponse(e,r,t){const s=e.headers.get("content-type");if(!isHtmlContentType(s??void 0)){if(!t||0===t.length)return e;const r=mergeResponseHeaders(e.headers,void 0,t);return new Response(e.body,{status:e.status,statusText:e.statusText,headers:r})}const n=await e.text(),i=n.lastIndexOf("</body>"),o=-1===i?n+r:n.slice(0,i)+r+n.slice(i),a=mergeResponseHeaders(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(o).length)},t);return a.delete("ETag"),a.delete("Last-Modified"),a.delete("Content-Encoding"),new Response(o,{status:e.status,statusText:e.statusText,headers:a})}function resolveEmail(e,r){if(r)try{const t=r(e);if(t)return t}catch{}const t=parseCookieFromRequest(e,"__unshared_email");if(t)return t}function resolveEmailWithBody(e,r,t){const s=resolveEmail(e,t);if(s)return s;const n=r.email;return"string"==typeof n&&n?n:void 0}function extractSessionIdFromRequest(e,r){if(r)try{const t=r(e);if(t)return t}catch{}return parseCookieFromRequest(e,"__unshared_sid")??"unknown"}function dispatchUserEvent(e,r,t,s,n,i){s.mark(n.userId,n.eventType),e.processUserEvent({eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,sessionHash:n.sessionId,userAgent:n.userAgent}).then(e=>{e.success&&e.data?.analysis&&r.update(n.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.pause(1e3*e.error.retryAfter)}).catch(e=>{i&&i(e,{operation:"processUserEvent",userId:n.userId,emailAddress:n.emailAddress})})}async function fetchAndCacheVerdict(e,r,t,s,n,i,o,a=CHECK_USER_TIMEOUT_MS,d){const c={};let u;n&&"unknown"!==n&&(c.deviceId=n),i&&(c.fingerprintId=i);const l=await Promise.race([e.checkUser(s,c),new Promise(e=>{u=setTimeout(()=>e(null),a)})]);if(clearTimeout(u),!l)return d?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:s,sessionId:o,cachedAt:0,ttl:0};l.failedOpen&&d?.("http_error",l.failedOpen.status);const p=l.data?.is_user_flagged??!1;return r.set(t,{isFlagged:p,isVerified:!1,emailAddress:s,sessionId:o}),r.get(t)}async function handleSubmitFp(e,r,t,s){try{const n={full_hash:r.hash??"",fingerprint_id:r.stable_hash??"",timestamp:r.collected_at??(new Date).toISOString(),isIncognito:r.is_incognito??!1,components:r.components??{},version:r.version??"inline-1.0.0"};let i,o,a;try{const r=t.resolveUserId(e);r&&!isSentinelUserId(r)&&(i=r)}catch{}if(!i){const e="string"==typeof r.user_id?r.user_id:void 0;e&&!isSentinelUserId(e)&&(i=e)}if(!i){const r=parseCookieFromRequest(e,"__unshared_uid");r&&!isSentinelUserId(r)&&(i=r)}try{o=t.resolveEmailAddress?t.resolveEmailAddress(e):void 0}catch{}o=o??parseCookieFromRequest(e,"__unshared_email")??r.email??void 0;try{a=t.resolveSessionId?t.resolveSessionId(e):void 0}catch{}a=a??r.session_id??parseCookieFromRequest(e,"__unshared_sid");const d=extractClientIpFromRequest(e),c=e.headers.get("user-agent")??"";if(!t.disableBotFilter&&isBot(c))return jsonResponse(200,{success:!0},s);const u=(n.fingerprint_id&&n.fingerprint_id.length>0?n.fingerprint_id:void 0)??extractDeviceIdFromRequestOrUnknown(e,t.resolveDeviceId),l=n.fingerprint_id||void 0,p=n.full_hash||void 0,m=isSecureWebRequest(e)?"; Secure":"",f=[];if(p&&!parseCookieFromRequest(e,"__unshared_fingerprint_id")&&f.push(`__unshared_fingerprint_id=${encodeURIComponent(p)}; HttpOnly; Path=/; SameSite=Lax${m}`),l){const r=parseCookieFromRequest(e,"__unshared_fp_id");r&&r===l||f.push(`__unshared_fp_id=${encodeURIComponent(l)}; Path=/; SameSite=Lax; Max-Age=31536000${m}`)}let h;if(o&&!parseCookieFromRequest(e,"__unshared_email")&&f.push(`__unshared_email=${encodeURIComponent(o)}; HttpOnly; Path=/; SameSite=Lax${m}`),"string"==typeof r.event_type&&r.event_type)h=r.event_type;else{const r=e.headers.get("referer")??e.headers.get("referrer");let t="unknown";if(r)try{const e=new URL(r);t=(e.pathname||"/")+(e.search||"")}catch{}h=t}const _=e.headers.get("x-idempotency-key")||void 0,g=Date.now(),R=_?`${_}|${g}`:l&&i?`${sha256Hex(`${l}|${i}|${h}`)}|${g}`:void 0;i&&t.client.submitFingerprintEvent(n,{userId:i,emailAddress:o,sessionHash:a,eventType:h,ipAddress:d,userAgent:c,idempotencyKey:R}).catch(e=>{t.onError&&t.onError(e,{operation:"submitFingerprintEvent",userId:i,emailAddress:o})}),i&&o&&!t.rateLimitBackoff.isPaused()&&!t.dispatchDedupe.wasRecentlyDispatched(i,h)&&t.client.processUserEvent({eventType:h,userId:i,emailAddress:o,ipAddress:d,deviceId:u,fingerprintId:l,sessionHash:a??"unknown",userAgent:c}).then(e=>{e.success&&e.data?.analysis&&t.verdictCache.update(i,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.rateLimitBackoff.pause(1e3*e.error.retryAfter)}).catch(e=>{t.onError&&t.onError(e,{operation:"processUserEvent",userId:i,emailAddress:o})});const I={...s,"Content-Type":"application/json"},S=new Response(JSON.stringify({success:!0}),{status:200,headers:I});for(const e of f)S.headers.append("Set-Cookie",e);return S}catch{return jsonResponse(200,{success:!0},s)}}async function handleVerifyTriggerWeb(e,r,t,s){try{const n=resolveEmailWithBody(e,r??{},t.resolveEmailAddress);if(!n)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},s);const i=extractDeviceIdFromRequestOrUnknown(e,t.resolveDeviceId),o=parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,a=await t.client.triggerEmailVerification(n,i,{fingerprintId:o});return a.success?jsonResponse(200,{success:!0,data:a.data},s):jsonResponse(200,{success:!1,error:a.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},s)}catch(e){return t.onError&&t.onError(e,{operation:"verifyTrigger"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},s)}}async function handleVerifyWeb(e,r,t,s){try{const n=resolveEmailWithBody(e,r??{},t.resolveEmailAddress),i=r?.code;if(!n||!i)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}},s);const o=extractDeviceIdFromRequestOrUnknown(e,t.resolveDeviceId),a=parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,d=await t.client.verify(n,o,i,{fingerprintId:a});if(d.success){const r=parseCookieFromRequest(e,"__unshared_uid");return r&&t.verdictCache.update(r,{isVerified:!0}),jsonResponse(200,{success:!0,data:{verified:!0}},s)}return jsonResponse(200,{success:!1,error:d.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}},s)}catch(e){return t.onError&&t.onError(e,{operation:"verify"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},s)}}
|
|
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)}}
|
package/dist/esm/web/types.d.mts
CHANGED
|
@@ -93,8 +93,26 @@ export interface WebProtectionConfig {
|
|
|
93
93
|
* `403 { error: 'account_flagged' }`. `onFlagged` is ignored in this mode.
|
|
94
94
|
* Requires `fingerprintSdkBundle` to be provided and the default `routePrefix`.
|
|
95
95
|
* @default false
|
|
96
|
+
*
|
|
97
|
+
* `blockFlagged: true` is equivalent to `flaggedMode: 'gate'` (see {@link flaggedMode}).
|
|
96
98
|
*/
|
|
97
99
|
blockFlagged?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* How the SDK enforces a flagged + unverified user (Web/edge equivalent of the Node
|
|
102
|
+
* middleware's `flaggedMode`). Setting it enables enforcement on its own; when both
|
|
103
|
+
* are set, `flaggedMode` wins.
|
|
104
|
+
*
|
|
105
|
+
* - `'gate'` — hard gate (== `blockFlagged: true`): HTML navigations get the standalone
|
|
106
|
+
* verification gate page; everything else gets `403 account_flagged`. Content never sent.
|
|
107
|
+
* - `'overlay'` — modal over blurred live content: HTML navigations render normally and
|
|
108
|
+
* the SDK auto-renders the modal over the blurred page; non-HTML/data requests get
|
|
109
|
+
* `403 account_flagged`. Implies `autoInterstitial`. The HTML is delivered (blur is
|
|
110
|
+
* cosmetic) — the real protection is the `403` on the gated data.
|
|
111
|
+
*
|
|
112
|
+
* Both modes ignore `onFlagged` and require `fingerprintSdkBundle` + the default
|
|
113
|
+
* `routePrefix`. @default undefined
|
|
114
|
+
*/
|
|
115
|
+
flaggedMode?: 'gate' | 'overlay';
|
|
98
116
|
/**
|
|
99
117
|
* Auto-render the interstitial modal on SPA/JSON `403 account_flagged` paths
|
|
100
118
|
* (and the deferred `/status` poll) with no app code. When `true`, the injected
|
|
@@ -60,8 +60,30 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
|
|
|
60
60
|
*
|
|
61
61
|
* When `true`, the SDK owns the flagged response and `onFlagged` is ignored. Requires
|
|
62
62
|
* `unshared-frontend-sdk` to be installed and the default `routePrefix`. @default false
|
|
63
|
+
*
|
|
64
|
+
* `blockFlagged: true` is equivalent to `flaggedMode: 'gate'` (see {@link flaggedMode}).
|
|
63
65
|
*/
|
|
64
66
|
blockFlagged?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* How the SDK enforces a flagged + unverified user. Selects the flagged-user
|
|
69
|
+
* experience; setting it enables enforcement on its own (you do NOT also need
|
|
70
|
+
* `blockFlagged`). When both are set, `flaggedMode` wins.
|
|
71
|
+
*
|
|
72
|
+
* - `'gate'` — **hard gate** (the strongest guarantee, == `blockFlagged: true`). HTML
|
|
73
|
+
* navigations get a standalone verification gate page with no app markup; everything
|
|
74
|
+
* else gets `403 { error: 'account_flagged' }`. The protected content is never sent.
|
|
75
|
+
* - `'overlay'` — **modal over blurred live content**. HTML navigations render normally
|
|
76
|
+
* (the page IS delivered) and the SDK auto-renders the (non-dismissible) interstitial
|
|
77
|
+
* modal over the blurred page; non-HTML/data requests get `403 account_flagged`.
|
|
78
|
+
* Implies `autoInterstitial`. Use `includePathPrefix` to scope which data routes are
|
|
79
|
+
* gated. Honest trade-off: the HTML is delivered to the client and the blur is
|
|
80
|
+
* cosmetic — the real protection is the `403` on the gated data, so put sensitive
|
|
81
|
+
* content behind gated endpoints.
|
|
82
|
+
*
|
|
83
|
+
* Both modes ignore `onFlagged` (the SDK owns the response) and require
|
|
84
|
+
* `unshared-frontend-sdk` installed + the default `routePrefix`. @default undefined
|
|
85
|
+
*/
|
|
86
|
+
flaggedMode?: 'gate' | 'overlay';
|
|
65
87
|
/**
|
|
66
88
|
* Auto-render the interstitial modal on SPA/JSON `403 account_flagged` paths
|
|
67
89
|
* (and the deferred `/status` poll) with no app code. When `true`, the injected
|
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"),submit_fp_1=require("./routes/submit-fp"),verify_1=require("./routes/verify"),interstitial_1=require("./routes/interstitial"),gate_page_1=require("./injection/gate-page"),http_helpers_1=require("./utils/http-helpers"),content_type_1=require("./utils/content-type"),flagged_response_1=require("./utils/flagged-response"),skip_paths_1=require("./utils/skip-paths"),include_path_1=require("./utils/include-path"),is_bot_1=require("./utils/is-bot"),client_ip_1=require("./utils/client-ip"),cookies_1=require("./utils/cookies"),device_id_1=require("./utils/device-id"),secure_1=require("./utils/secure"),sentinel_user_id_1=require("./utils/sentinel-user-id");var flagged_response_2=require("./utils/flagged-response");Object.defineProperty(exports,"flaggedResponse",{enumerable:!0,get:function(){return flagged_response_2.flaggedResponse}}),Object.defineProperty(exports,"ACCOUNT_FLAGGED_ERROR",{enumerable:!0,get:function(){return flagged_response_2.ACCOUNT_FLAGGED_ERROR}});const CHECK_USER_TIMEOUT_MS=500;function unsharedBoundToUser(e,r){if(!r.userId)throw new Error("[Unshared] userId resolver is required");if(!r.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:t,emailAddress:i,routePrefix:s="/__unshared",corsOrigins:n,cacheTTL:o=6e4,skipPaths:d,includePathPrefix:a,disableBotFilter:c=!1,checkUserTimeoutMs:u=CHECK_USER_TIMEOUT_MS,sessionId:l,deviceId:_,onFlagged:p,onError:h,onFailOpen:f,blockFlagged:g=!1,autoInterstitial:v=!1,interstitialFlowType:m="email_verification"}=r,I=e=>{if(f)try{f(e)}catch{}},k=new verdict_cache_1.VerdictCache(o),S=new rate_limit_backoff_1.RateLimitBackoff,C=new dispatch_dedupe_1.DispatchDedupe,y=Date.now().toString(36),A=(0,fingerprint_script_1.generateFingerprintScript)(s,y,{autoInterstitial:v,interstitialFlowType:m});let x="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");x=(0,fs_1.readFileSync)(e,"utf8")}catch{}if(g&&!x)throw new Error("[Unshared] blockFlagged requires unshared-frontend-sdk to be installed (its UMD bundle is the gate-page renderer).");if(g&&"/__unshared"!==s)throw new Error('[Unshared] blockFlagged requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');if(v&&!x)throw new Error("[Unshared] autoInterstitial requires unshared-frontend-sdk to be installed (its UMD bundle boots the auto-rendered interstitial).");if(v&&"/__unshared"!==s)throw new Error('[Unshared] autoInterstitial requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');const b=g?(0,gate_page_1.generateGatePage)(s,y):"",E=(0,submit_fp_1.handleSubmitFingerprint)({client:e,verdictCache:k,rateLimitBackoff:S,dispatchDedupe:C,resolveUserId:t,resolveEmailAddress:i,resolveSessionId:l,resolveDeviceId:_,disableBotFilter:c,onError:h}),w=(0,verify_1.handleVerifyTrigger)({client:e,verdictCache:k,resolveEmailAddress:i,resolveDeviceId:_,onError:h}),U=(0,verify_1.handleVerify)({client:e,verdictCache:k,resolveEmailAddress:i,resolveDeviceId:_,onError:h}),T=(0,interstitial_1.handleGetInterstitialFlow)({client:e}),q=n?Array.isArray(n)?n:[n]:null,F=`${s}/fp.js`,O=`${s}/submit-fp`,P=`${s}/verify-trigger`,M=`${s}/verify`,j=`${s}/status`,$=`${s}/interstitial-flow`;return function(r,n,o){const f=(0,http_helpers_1.getRequestPath)(r.url),v=r.url||f;if(f.startsWith(s+"/")){if(function(e,r){if(!q)return;const t=e.headers.origin??"",i=q.includes("*");(i||q.includes(t))&&(r.setHeader("Access-Control-Allow-Origin",i?"*":t),r.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),r.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),r.setHeader("Access-Control-Allow-Credentials","true"))}(r,n),"OPTIONS"===r.method)return void(0,http_helpers_1.sendEmpty)(n,204);if("GET"===r.method&&f===F)return n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBody)(n,200,x);if("POST"===r.method&&(f===O||f===P||f===M))return void 0===r.body?void(0,http_helpers_1.sendJson)(n,400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"req.body is undefined. Mount a JSON body-parsing middleware (e.g., express.json()) before the Unshared middleware."}}):f===O?void E(r,n):f===P?void w(r,n):void U(r,n);if("GET"===r.method&&f===$)return void T(r,n);if("GET"===r.method&&f===j){let s;try{s=t(r)}catch{}if(!s)return void(0,http_helpers_1.sendJson)(n,200,{status:"anonymous"});const o=resolveEmail(r,i);return void(async()=>{let t=k.get(s);if((!t||k.isStale(s))&&o&&!S.isPaused()&&!k.isRefreshing(s)){k.markRefreshing(s);try{const i=(0,device_id_1.extractDeviceIdOrUndefined)(r,_),n=extractFingerprintId(r),d=extractSessionId(r,l),a=i??n??"unknown";await fetchAndCacheVerdict(e,k,s,o,a,n,d,u,(e,r)=>I({operation:"checkUser",reason:e,status:r,userId:s,emailAddress:o})),t=k.get(s)}catch(e){h&&h(e,{operation:"checkUser",userId:s,emailAddress:o}),I({operation:"checkUser",reason:"exception",userId:s,emailAddress:o})}finally{k.clearRefreshing(s)}}t&&t.isFlagged&&!t.isVerified&&p&&o?(0,http_helpers_1.sendJson)(n,200,{status:"flagged",email:o}):(0,http_helpers_1.sendJson)(n,200,{status:"ok"})})()}return void(0,http_helpers_1.sendJson)(n,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if((0,skip_paths_1.shouldSkipPath)(f,d))return void o();if(!(0,include_path_1.shouldIncludePath)(f,a))return interceptForInjection(r,n,A),void o();let m;try{m=t(r)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(m)){const e=(0,cookies_1.parseCookie)(r,"__unshared_uid"),t=(0,cookies_1.parseCookie)(r,"__unshared_uid_at"),i=t?Number(t):NaN,s=Number.isFinite(i)&&Date.now()-i<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;m=e&&s?e:void 0}if(!m){const e=(0,secure_1.isSecureRequest)(r)?"; Secure":"";return appendSetCookie(n,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(r,n,A),void o()}const y=resolveEmail(r,i);if(setUserIdCookie(r,n,m),y&&setEmailCookie(r,n,y),!y)return interceptForInjection(r,n,A),void o();const D=extractSessionId(r,l),N=(0,device_id_1.extractDeviceIdOrUndefined)(r,_),R=extractFingerprintId(r),L=r.headers["user-agent"]??"",V=(0,client_ip_1.extractClientIp)(r),G=N??R;if(!c&&(0,is_bot_1.isBot)(L))return void o();const B=k.get(m);function H(){"unknown"!==D&&G&&(S.isPaused()||dispatchUserEvent(e,k,S,C,{userId:m,emailAddress:y,sessionId:D,deviceId:G,fingerprintId:R,userAgent:L,ipAddress:V,eventType:v},h))}B?(k.isStale(m)&&!k.isRefreshing(m)&&(k.markRefreshing(m),fetchAndCacheVerdict(e,k,m,y,G??"unknown",R,D,u,(e,r)=>I({operation:"checkUser",reason:e,status:r,userId:m,emailAddress:y})).catch(()=>I({operation:"checkUser",reason:"exception",userId:m,emailAddress:y})).finally(()=>k.clearRefreshing(m))),B.isFlagged||H(),applyVerdict(B,m,y,r,n,o,A,p,g,b)):fetchAndCacheVerdict(e,k,m,y,G??"unknown",R,D,u,(e,r)=>I({operation:"checkUser",reason:e,status:r,userId:m,emailAddress:y})).then(e=>{e.isFlagged||H(),applyVerdict(e,m,y,r,n,o,A,p,g,b)}).catch(()=>{I({operation:"checkUser",reason:"exception",userId:m,emailAddress:y}),H(),interceptForInjection(r,n,A),o()})}}function resolveEmail(e,r){if(r)try{const t=r(e);if(t)return t}catch{}const t=(0,cookies_1.parseCookie)(e,"__unshared_email");if(t)return t;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,r,t,i,s,n,o,d,a,c){if(a&&e.isFlagged&&!e.isVerified)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(s.statusCode=200,s.setHeader("Content-Type","text/html; charset=utf-8"),s.setHeader("Cache-Control","no-store"),s.end(c)):(0,http_helpers_1.sendJson)(s,403,(0,flagged_response_1.flaggedResponse)(t));else if(interceptForInjection(i,s,o),e.isFlagged&&!e.isVerified&&d)try{d({userId:r,emailAddress:t,verdict:e,req:i,res:s,next:n})}catch{n()}else n()}function interceptForInjection(e,r,t){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],(0,response_interceptor_1.interceptResponse)(r,(e,r)=>{if(!(0,content_type_1.isHtmlContentType)(r))return null;const i=e.toString("utf8"),s=i.lastIndexOf("</body>");return-1===s?i+t:i.slice(0,s)+t+i.slice(s)},{preventCaching:!0})}function dispatchUserEvent(e,r,t,i,s,n){i.mark(s.userId,s.eventType),e.processUserEvent({eventType:s.eventType,userId:s.userId,emailAddress:s.emailAddress,ipAddress:s.ipAddress,deviceId:s.deviceId,fingerprintId:s.fingerprintId,sessionHash:s.sessionId,userAgent:s.userAgent}).then(e=>{e.success&&e.data?.analysis&&r.update(s.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.pause(1e3*e.error.retryAfter)}).catch(e=>{n&&n(e,{operation:"processUserEvent",userId:s.userId,emailAddress:s.emailAddress})})}async function fetchAndCacheVerdict(e,r,t,i,s,n,o,d=CHECK_USER_TIMEOUT_MS,a){const c={};let u;s&&"unknown"!==s&&(c.deviceId=s),n&&(c.fingerprintId=n);const l=await Promise.race([e.checkUser(i,c),new Promise(e=>{u=setTimeout(()=>e(null),d)})]);if(clearTimeout(u),!l)return a?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:o,cachedAt:0,ttl:0};l.failedOpen&&a?.("http_error",l.failedOpen.status);const _=l.data?.is_user_flagged??!1;return r.set(t,{isFlagged:_,isVerified:!1,emailAddress:i,sessionId:o}),r.get(t)}function extractSessionId(e,r){if(r)try{const t=r(e);if(t)return t}catch{}return(0,cookies_1.parseCookie)(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return(0,cookies_1.parseCookie)(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,r){const t=e.getHeader("Set-Cookie");if(t){const i=Array.isArray(t)?[...t]:[String(t)];i.push(r),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",r)}function setUserIdCookie(e,r,t){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(r,`__unshared_uid=${encodeURIComponent(t)}; Path=/; SameSite=Lax${i}`),appendSetCookie(r,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,r,t){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(r,`__unshared_email=${encodeURIComponent(t)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
|
|
1
|
+
"use strict";Object.defineProperty(exports,"t",{value:!0}),exports.ACCOUNT_FLAGGED_ERROR=exports.flaggedResponse=exports.VerdictCache=void 0,exports.unsharedBoundToUser=unsharedBoundToUser;const fs_1=require("fs"),verdict_cache_1=require("./verdict-cache");Object.defineProperty(exports,"VerdictCache",{enumerable:!0,get:function(){return verdict_cache_1.VerdictCache}});const rate_limit_backoff_1=require("./rate-limit-backoff"),dispatch_dedupe_1=require("./dispatch-dedupe"),response_interceptor_1=require("./response-interceptor"),fingerprint_script_1=require("./injection/fingerprint-script"),submit_fp_1=require("./routes/submit-fp"),verify_1=require("./routes/verify"),interstitial_1=require("./routes/interstitial"),gate_page_1=require("./injection/gate-page"),http_helpers_1=require("./utils/http-helpers"),content_type_1=require("./utils/content-type"),flagged_response_1=require("./utils/flagged-response"),skip_paths_1=require("./utils/skip-paths"),include_path_1=require("./utils/include-path"),is_bot_1=require("./utils/is-bot"),client_ip_1=require("./utils/client-ip"),cookies_1=require("./utils/cookies"),device_id_1=require("./utils/device-id"),secure_1=require("./utils/secure"),sentinel_user_id_1=require("./utils/sentinel-user-id");var flagged_response_2=require("./utils/flagged-response");Object.defineProperty(exports,"flaggedResponse",{enumerable:!0,get:function(){return flagged_response_2.flaggedResponse}}),Object.defineProperty(exports,"ACCOUNT_FLAGGED_ERROR",{enumerable:!0,get:function(){return flagged_response_2.ACCOUNT_FLAGGED_ERROR}});const CHECK_USER_TIMEOUT_MS=500;function unsharedBoundToUser(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");if(!t.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:r,emailAddress:i,routePrefix:s="/__unshared",corsOrigins:n,cacheTTL:o=6e4,skipPaths:d,includePathPrefix:a,disableBotFilter:c=!1,checkUserTimeoutMs:u=CHECK_USER_TIMEOUT_MS,sessionId:l,deviceId:_,onFlagged:p,onError:h,onFailOpen:f,blockFlagged:g=!1,flaggedMode:v,autoInterstitial:m=!1,interstitialFlowType:I="email_verification"}=t,k=v??(g?"gate":void 0),y=m||"overlay"===k,S=e=>{if(f)try{f(e)}catch{}},C=new verdict_cache_1.VerdictCache(o),A=new rate_limit_backoff_1.RateLimitBackoff,x=new dispatch_dedupe_1.DispatchDedupe,E=Date.now().toString(36),b=(0,fingerprint_script_1.generateFingerprintScript)(s,E,{autoInterstitial:y,interstitialFlowType:I});let U="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");U=(0,fs_1.readFileSync)(e,"utf8")}catch{}const T=void 0!==k||y,w="gate"===k?"flaggedMode 'gate' (blockFlagged)":"overlay"===k?"flaggedMode 'overlay'":"autoInterstitial";if(T&&!U)throw new Error(`[Unshared] ${w} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(T&&"/__unshared"!==s)throw new Error(`[Unshared] ${w} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const q="gate"===k?(0,gate_page_1.generateGatePage)(s,E):"",F=(0,submit_fp_1.handleSubmitFingerprint)({client:e,verdictCache:C,rateLimitBackoff:A,dispatchDedupe:x,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:l,resolveDeviceId:_,disableBotFilter:c,onError:h}),O=(0,verify_1.handleVerifyTrigger)({client:e,verdictCache:C,resolveEmailAddress:i,resolveDeviceId:_,onError:h}),M=(0,verify_1.handleVerify)({client:e,verdictCache:C,resolveEmailAddress:i,resolveDeviceId:_,onError:h}),P=(0,interstitial_1.handleGetInterstitialFlow)({client:e}),$=n?Array.isArray(n)?n:[n]:null,j=`${s}/fp.js`,D=`${s}/submit-fp`,N=`${s}/verify-trigger`,R=`${s}/verify`,L=`${s}/status`,V=`${s}/interstitial-flow`;return function(t,n,o){const f=(0,http_helpers_1.getRequestPath)(t.url),g=t.url||f;if(f.startsWith(s+"/")){if(function(e,t){if(!$)return;const r=e.headers.origin??"",i=$.includes("*");(i||$.includes(r))&&(t.setHeader("Access-Control-Allow-Origin",i?"*":r),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,n),"OPTIONS"===t.method)return void(0,http_helpers_1.sendEmpty)(n,204);if("GET"===t.method&&f===j)return n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBody)(n,200,U);if("POST"===t.method&&(f===D||f===N||f===R))return void 0===t.body?void(0,http_helpers_1.sendJson)(n,400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"req.body is undefined. Mount a JSON body-parsing middleware (e.g., express.json()) before the Unshared middleware."}}):f===D?void F(t,n):f===N?void O(t,n):void M(t,n);if("GET"===t.method&&f===V)return void P(t,n);if("GET"===t.method&&f===L){let s;try{s=r(t)}catch{}if(!s)return void(0,http_helpers_1.sendJson)(n,200,{status:"anonymous"});const o=resolveEmail(t,i);return void(async()=>{let r=C.get(s);if((!r||C.isStale(s))&&o&&!A.isPaused()&&!C.isRefreshing(s)){C.markRefreshing(s);try{const i=(0,device_id_1.extractDeviceIdOrUndefined)(t,_),n=extractFingerprintId(t),d=extractSessionId(t,l),a=i??n??"unknown";await fetchAndCacheVerdict(e,C,s,o,a,n,d,u,(e,t)=>S({operation:"checkUser",reason:e,status:t,userId:s,emailAddress:o})),r=C.get(s)}catch(e){h&&h(e,{operation:"checkUser",userId:s,emailAddress:o}),S({operation:"checkUser",reason:"exception",userId:s,emailAddress:o})}finally{C.clearRefreshing(s)}}r&&r.isFlagged&&!r.isVerified&&p&&o?(0,http_helpers_1.sendJson)(n,200,{status:"flagged",email:o}):(0,http_helpers_1.sendJson)(n,200,{status:"ok"})})()}return void(0,http_helpers_1.sendJson)(n,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if((0,skip_paths_1.shouldSkipPath)(f,d))return void o();if(!(0,include_path_1.shouldIncludePath)(f,a))return interceptForInjection(t,n,b),void o();let v;try{v=r(t)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(v)){const e=(0,cookies_1.parseCookie)(t,"__unshared_uid"),r=(0,cookies_1.parseCookie)(t,"__unshared_uid_at"),i=r?Number(r):NaN,s=Number.isFinite(i)&&Date.now()-i<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;v=e&&s?e:void 0}if(!v){const e=(0,secure_1.isSecureRequest)(t)?"; Secure":"";return appendSetCookie(n,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(t,n,b),void o()}const m=resolveEmail(t,i);if(setUserIdCookie(t,n,v),m&&setEmailCookie(t,n,m),!m)return interceptForInjection(t,n,b),void o();const I=extractSessionId(t,l),y=(0,device_id_1.extractDeviceIdOrUndefined)(t,_),E=extractFingerprintId(t),T=t.headers["user-agent"]??"",w=(0,client_ip_1.extractClientIp)(t),G=y??E;if(!c&&(0,is_bot_1.isBot)(T))return void o();const B=C.get(v);function H(){"unknown"!==I&&G&&(A.isPaused()||dispatchUserEvent(e,C,A,x,{userId:v,emailAddress:m,sessionId:I,deviceId:G,fingerprintId:E,userAgent:T,ipAddress:w,eventType:g},h))}B?(C.isStale(v)&&!C.isRefreshing(v)&&(C.markRefreshing(v),fetchAndCacheVerdict(e,C,v,m,G??"unknown",E,I,u,(e,t)=>S({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:m})).catch(()=>S({operation:"checkUser",reason:"exception",userId:v,emailAddress:m})).finally(()=>C.clearRefreshing(v))),B.isFlagged||H(),applyVerdict(B,v,m,t,n,o,b,p,k,q)):fetchAndCacheVerdict(e,C,v,m,G??"unknown",E,I,u,(e,t)=>S({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:m})).then(e=>{e.isFlagged||H(),applyVerdict(e,v,m,t,n,o,b,p,k,q)}).catch(()=>{S({operation:"checkUser",reason:"exception",userId:v,emailAddress:m}),H(),interceptForInjection(t,n,b),o()})}}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=(0,cookies_1.parseCookie)(e,"__unshared_email");if(r)return r;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,t,r,i,s,n,o,d,a,c){const u=e.isFlagged&&!e.isVerified;if("gate"===a&&u)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(s.statusCode=200,s.setHeader("Content-Type","text/html; charset=utf-8"),s.setHeader("Cache-Control","no-store"),s.end(c)):(0,http_helpers_1.sendJson)(s,403,(0,flagged_response_1.flaggedResponse)(r));else if("overlay"===a&&u)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(interceptForInjection(i,s,o),n()):(0,http_helpers_1.sendJson)(s,403,(0,flagged_response_1.flaggedResponse)(r));else if(interceptForInjection(i,s,o),e.isFlagged&&!e.isVerified&&d)try{d({userId:t,emailAddress:r,verdict:e,req:i,res:s,next:n})}catch{n()}else n()}function interceptForInjection(e,t,r){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],(0,response_interceptor_1.interceptResponse)(t,(e,t)=>{if(!(0,content_type_1.isHtmlContentType)(t))return null;const i=e.toString("utf8"),s=i.lastIndexOf("</body>");return-1===s?i+r:i.slice(0,s)+r+i.slice(s)},{preventCaching:!0})}function dispatchUserEvent(e,t,r,i,s,n){i.mark(s.userId,s.eventType),e.processUserEvent({eventType:s.eventType,userId:s.userId,emailAddress:s.emailAddress,ipAddress:s.ipAddress,deviceId:s.deviceId,fingerprintId:s.fingerprintId,sessionHash:s.sessionId,userAgent:s.userAgent}).then(e=>{e.success&&e.data?.analysis&&t.update(s.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&r.pause(1e3*e.error.retryAfter)}).catch(e=>{n&&n(e,{operation:"processUserEvent",userId:s.userId,emailAddress:s.emailAddress})})}async function fetchAndCacheVerdict(e,t,r,i,s,n,o,d=CHECK_USER_TIMEOUT_MS,a){const c={};let u;s&&"unknown"!==s&&(c.deviceId=s),n&&(c.fingerprintId=n);const l=await Promise.race([e.checkUser(i,c),new Promise(e=>{u=setTimeout(()=>e(null),d)})]);if(clearTimeout(u),!l)return a?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:o,cachedAt:0,ttl:0};l.failedOpen&&a?.("http_error",l.failedOpen.status);const _=l.data?.is_user_flagged??!1;return t.set(r,{isFlagged:_,isVerified:!1,emailAddress:i,sessionId:o}),t.get(r)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return(0,cookies_1.parseCookie)(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return(0,cookies_1.parseCookie)(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"t",{value:!0}),exports.createWebProtectionMiddleware=createWebProtectionMiddleware;const util_1=require("../util"),verdict_cache_1=require("../middleware/verdict-cache"),rate_limit_backoff_1=require("../middleware/rate-limit-backoff"),dispatch_dedupe_1=require("../middleware/dispatch-dedupe"),fingerprint_script_1=require("../middleware/injection/fingerprint-script"),gate_page_1=require("../middleware/injection/gate-page"),flagged_response_1=require("../middleware/utils/flagged-response"),content_type_1=require("../middleware/utils/content-type"),skip_paths_1=require("../middleware/utils/skip-paths"),include_path_1=require("../middleware/utils/include-path"),is_bot_1=require("../middleware/utils/is-bot"),sentinel_user_id_1=require("../middleware/utils/sentinel-user-id"),web_helpers_1=require("./web-helpers"),CHECK_USER_TIMEOUT_MS=500;function createWebProtectionMiddleware(e,r){if(!r.userId)throw new Error("[Unshared] userId resolver is required");const{userId:t,emailAddress:s,routePrefix:i="/__unshared",corsOrigins:n,cacheTTL:a=6e4,skipPaths:o,includePathPrefix:d,sessionId:_,deviceId:c,fingerprintSdkBundle:l="",onFlagged:u,onError:h,onFailOpen:p,disableBotFilter:f=!1,checkUserTimeoutMs:w=CHECK_USER_TIMEOUT_MS,blockFlagged:m=!1,autoInterstitial:g=!1,interstitialFlowType:b="email_verification"}=r,I=e=>{if(p)try{p(e)}catch{}};if(m&&!l)throw new Error("[Unshared] blockFlagged requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the gate page).");if(m&&"/__unshared"!==i)throw new Error('[Unshared] blockFlagged requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');if(g&&!l)throw new Error("[Unshared] autoInterstitial requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js boots the auto-rendered interstitial).");if(g&&"/__unshared"!==i)throw new Error('[Unshared] autoInterstitial requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.');const v=m?(0,gate_page_1.generateGatePage)(i):"",y=new verdict_cache_1.VerdictCache(a),A=new rate_limit_backoff_1.RateLimitBackoff,S=new dispatch_dedupe_1.DispatchDedupe,E=Date.now().toString(36),R=(0,fingerprint_script_1.generateFingerprintScript)(i,E,{autoInterstitial:g,interstitialFlowType:b}),T=`${i}/fp.js`,x=`${i}/submit-fp`,C=`${i}/verify-trigger`,U=`${i}/verify`,k=`${i}/status`,O=n?Array.isArray(n)?n:[n]:null;return async function(r,n){let a,p,g;try{const e=new URL(r.url);a=e.pathname,p=e.search}catch{return n(r)}if(a.startsWith(i+"/")){const i=function(e){if(!O)return{};const r=e.headers.get("origin")??"",t=O.includes("*");return t||O.includes(r)?{"Access-Control-Allow-Origin":t?"*":r,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id","Access-Control-Allow-Credentials":"true"}:{}}(r);if("OPTIONS"===r.method)return(0,web_helpers_1.emptyResponse)(204,i);if("GET"===r.method&&a===T)return l?(0,web_helpers_1.bodyResponse)(200,l,{...i,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600"}):(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},i);if("POST"===r.method&&(a===x||a===C||a===U)){let n;try{n=await r.json()}catch{return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"Request body is not valid JSON."}},i)}return a===x?handleSubmitFp(r,n,{client:e,verdictCache:y,rateLimitBackoff:A,dispatchDedupe:S,resolveUserId:t,resolveEmailAddress:s,resolveSessionId:_,resolveDeviceId:c,disableBotFilter:f,onError:h},i):a===C?handleVerifyTriggerWeb(r,n,{client:e,verdictCache:y,resolveEmailAddress:s,resolveDeviceId:c,onError:h},i):handleVerifyWeb(r,n,{client:e,verdictCache:y,resolveEmailAddress:s,resolveDeviceId:c,onError:h},i)}if("GET"===r.method&&a===k){let n;try{n=t(r)}catch{}if(!n)return(0,web_helpers_1.jsonResponse)(200,{status:"anonymous"},i);const a=resolveEmail(r,s);let o=y.get(n);if((!o||y.isStale(n))&&a&&!A.isPaused()&&!y.isRefreshing(n)){y.markRefreshing(n);try{const t=(0,web_helpers_1.extractDeviceIdFromRequest)(r,c),s=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_fingerprint_id")||void 0,i=extractSessionIdFromRequest(r,_),d=t??s??"unknown";await fetchAndCacheVerdict(e,y,n,a,d,s,i,w,(e,r)=>I({operation:"checkUser",reason:e,status:r,userId:n,emailAddress:a})),o=y.get(n)}catch(e){h&&h(e,{operation:"checkUser",userId:n,emailAddress:a}),I({operation:"checkUser",reason:"exception",userId:n,emailAddress:a})}finally{y.clearRefreshing(n)}}return o&&o.isFlagged&&!o.isVerified&&u&&a?(0,web_helpers_1.jsonResponse)(200,{status:"flagged",email:a},i):(0,web_helpers_1.jsonResponse)(200,{status:"ok"},i)}return(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},i)}if((0,skip_paths_1.shouldSkipPath)(a,o))return n(r);if(!(0,include_path_1.shouldIncludePath)(a,d))return injectIntoHtmlResponse(await n(r),R);try{g=t(r)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(g)){const e=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_uid"),t=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_uid_at"),s=t?Number(t):NaN,i=Number.isFinite(s)&&Date.now()-s<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;g=e&&i?e:void 0}if(!g){const e=(0,web_helpers_1.isSecureWebRequest)(r)?"; Secure":"",t=[`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`];return injectIntoHtmlResponse(await n(r),R,t)}const b=resolveEmail(r,s),E=[],P=(0,web_helpers_1.isSecureWebRequest)(r)?"; Secure":"";if(E.push(`__unshared_uid=${encodeURIComponent(g)}; Path=/; SameSite=Lax${P}`),E.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${P}`),b&&E.push(`__unshared_email=${encodeURIComponent(b)}; HttpOnly; Path=/; SameSite=Lax${P}`),!b)return injectIntoHtmlResponse(await n(r),R,E);const $=extractSessionIdFromRequest(r,_),F=(0,web_helpers_1.extractDeviceIdFromRequest)(r,c),q=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_fingerprint_id")||void 0,D=r.headers.get("user-agent")??"",L=(0,web_helpers_1.extractClientIpFromRequest)(r),M=F??q;if(!f&&(0,is_bot_1.isBot)(D))return n(r);let N=y.get(g);if(N)y.isStale(g)&&!y.isRefreshing(g)&&(y.markRefreshing(g),fetchAndCacheVerdict(e,y,g,b,M??"unknown",q,$,w,(e,r)=>I({operation:"checkUser",reason:e,status:r,userId:g,emailAddress:b})).catch(()=>I({operation:"checkUser",reason:"exception",userId:g,emailAddress:b})).finally(()=>y.clearRefreshing(g)));else try{N=await fetchAndCacheVerdict(e,y,g,b,M??"unknown",q,$,w,(e,r)=>I({operation:"checkUser",reason:e,status:r,userId:g,emailAddress:b}))}catch{return I({operation:"checkUser",reason:"exception",userId:g,emailAddress:b}),injectIntoHtmlResponse(await n(r),R,E)}if(m&&N.isFlagged&&!N.isVerified)return(0,content_type_1.isHtmlNavigation)(r.method,r.headers.get("accept")??void 0)?(0,web_helpers_1.bodyResponse)(200,v,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}):(0,web_helpers_1.jsonResponse)(403,(0,flagged_response_1.flaggedResponse)(b));if(N.isFlagged&&!N.isVerified&&u)try{const e=await u({userId:g,emailAddress:b,verdict:N,request:r});if(e)return injectIntoHtmlResponse(e,R,E)}catch(e){h&&h(e,{operation:"checkUser",userId:g,emailAddress:b})}return N.isFlagged||"unknown"===$||!M||A.isPaused()||dispatchUserEvent(e,y,A,S,{userId:g,emailAddress:b,sessionId:$,deviceId:M,fingerprintId:q,userAgent:D,ipAddress:L,eventType:a+p},h),injectIntoHtmlResponse(await n(r),R,E)}}async function injectIntoHtmlResponse(e,r,t){const s=e.headers.get("content-type");if(!(0,content_type_1.isHtmlContentType)(s??void 0)){if(!t||0===t.length)return e;const r=(0,web_helpers_1.mergeResponseHeaders)(e.headers,void 0,t);return new Response(e.body,{status:e.status,statusText:e.statusText,headers:r})}const i=await e.text(),n=i.lastIndexOf("</body>"),a=-1===n?i+r:i.slice(0,n)+r+i.slice(n),o=(0,web_helpers_1.mergeResponseHeaders)(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(a).length)},t);return o.delete("ETag"),o.delete("Last-Modified"),o.delete("Content-Encoding"),new Response(a,{status:e.status,statusText:e.statusText,headers:o})}function resolveEmail(e,r){if(r)try{const t=r(e);if(t)return t}catch{}const t=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email");if(t)return t}function resolveEmailWithBody(e,r,t){const s=resolveEmail(e,t);if(s)return s;const i=r.email;return"string"==typeof i&&i?i:void 0}function extractSessionIdFromRequest(e,r){if(r)try{const t=r(e);if(t)return t}catch{}return(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid")??"unknown"}function dispatchUserEvent(e,r,t,s,i,n){s.mark(i.userId,i.eventType),e.processUserEvent({eventType:i.eventType,userId:i.userId,emailAddress:i.emailAddress,ipAddress:i.ipAddress,deviceId:i.deviceId,fingerprintId:i.fingerprintId,sessionHash:i.sessionId,userAgent:i.userAgent}).then(e=>{e.success&&e.data?.analysis&&r.update(i.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.pause(1e3*e.error.retryAfter)}).catch(e=>{n&&n(e,{operation:"processUserEvent",userId:i.userId,emailAddress:i.emailAddress})})}async function fetchAndCacheVerdict(e,r,t,s,i,n,a,o=CHECK_USER_TIMEOUT_MS,d){const _={};let c;i&&"unknown"!==i&&(_.deviceId=i),n&&(_.fingerprintId=n);const l=await Promise.race([e.checkUser(s,_),new Promise(e=>{c=setTimeout(()=>e(null),o)})]);if(clearTimeout(c),!l)return d?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:s,sessionId:a,cachedAt:0,ttl:0};l.failedOpen&&d?.("http_error",l.failedOpen.status);const u=l.data?.is_user_flagged??!1;return r.set(t,{isFlagged:u,isVerified:!1,emailAddress:s,sessionId:a}),r.get(t)}async function handleSubmitFp(e,r,t,s){try{const i={full_hash:r.hash??"",fingerprint_id:r.stable_hash??"",timestamp:r.collected_at??(new Date).toISOString(),isIncognito:r.is_incognito??!1,components:r.components??{},version:r.version??"inline-1.0.0"};let n,a,o;try{const r=t.resolveUserId(e);r&&!(0,sentinel_user_id_1.isSentinelUserId)(r)&&(n=r)}catch{}if(!n){const e="string"==typeof r.user_id?r.user_id:void 0;e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(n=e)}if(!n){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_uid");r&&!(0,sentinel_user_id_1.isSentinelUserId)(r)&&(n=r)}try{a=t.resolveEmailAddress?t.resolveEmailAddress(e):void 0}catch{}a=a??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")??r.email??void 0;try{o=t.resolveSessionId?t.resolveSessionId(e):void 0}catch{}o=o??r.session_id??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid");const d=(0,web_helpers_1.extractClientIpFromRequest)(e),_=e.headers.get("user-agent")??"";if(!t.disableBotFilter&&(0,is_bot_1.isBot)(_))return(0,web_helpers_1.jsonResponse)(200,{success:!0},s);const c=(i.fingerprint_id&&i.fingerprint_id.length>0?i.fingerprint_id:void 0)??(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),l=i.fingerprint_id||void 0,u=i.full_hash||void 0,h=(0,web_helpers_1.isSecureWebRequest)(e)?"; Secure":"",p=[];if(u&&!(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")&&p.push(`__unshared_fingerprint_id=${encodeURIComponent(u)}; HttpOnly; Path=/; SameSite=Lax${h}`),l){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fp_id");r&&r===l||p.push(`__unshared_fp_id=${encodeURIComponent(l)}; Path=/; SameSite=Lax; Max-Age=31536000${h}`)}let f;if(a&&!(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")&&p.push(`__unshared_email=${encodeURIComponent(a)}; HttpOnly; Path=/; SameSite=Lax${h}`),"string"==typeof r.event_type&&r.event_type)f=r.event_type;else{const r=e.headers.get("referer")??e.headers.get("referrer");let t="unknown";if(r)try{const e=new URL(r);t=(e.pathname||"/")+(e.search||"")}catch{}f=t}const w=e.headers.get("x-idempotency-key")||void 0,m=Date.now(),g=w?`${w}|${m}`:l&&n?`${(0,util_1.sha256Hex)(`${l}|${n}|${f}`)}|${m}`:void 0;n&&t.client.submitFingerprintEvent(i,{userId:n,emailAddress:a,sessionHash:o,eventType:f,ipAddress:d,userAgent:_,idempotencyKey:g}).catch(e=>{t.onError&&t.onError(e,{operation:"submitFingerprintEvent",userId:n,emailAddress:a})}),n&&a&&!t.rateLimitBackoff.isPaused()&&!t.dispatchDedupe.wasRecentlyDispatched(n,f)&&t.client.processUserEvent({eventType:f,userId:n,emailAddress:a,ipAddress:d,deviceId:c,fingerprintId:l,sessionHash:o??"unknown",userAgent:_}).then(e=>{e.success&&e.data?.analysis&&t.verdictCache.update(n,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.rateLimitBackoff.pause(1e3*e.error.retryAfter)}).catch(e=>{t.onError&&t.onError(e,{operation:"processUserEvent",userId:n,emailAddress:a})});const b={...s,"Content-Type":"application/json"},I=new Response(JSON.stringify({success:!0}),{status:200,headers:b});for(const e of p)I.headers.append("Set-Cookie",e);return I}catch{return(0,web_helpers_1.jsonResponse)(200,{success:!0},s)}}async function handleVerifyTriggerWeb(e,r,t,s){try{const i=resolveEmailWithBody(e,r??{},t.resolveEmailAddress);if(!i)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},s);const n=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),a=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,o=await t.client.triggerEmailVerification(i,n,{fingerprintId:a});return o.success?(0,web_helpers_1.jsonResponse)(200,{success:!0,data:o.data},s):(0,web_helpers_1.jsonResponse)(200,{success:!1,error:o.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},s)}catch(e){return t.onError&&t.onError(e,{operation:"verifyTrigger"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},s)}}async function handleVerifyWeb(e,r,t,s){try{const i=resolveEmailWithBody(e,r??{},t.resolveEmailAddress),n=r?.code;if(!i||!n)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}},s);const a=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),o=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,d=await t.client.verify(i,a,n,{fingerprintId:o});if(d.success){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_uid");return r&&t.verdictCache.update(r,{isVerified:!0}),(0,web_helpers_1.jsonResponse)(200,{success:!0,data:{verified:!0}},s)}return(0,web_helpers_1.jsonResponse)(200,{success:!1,error:d.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}},s)}catch(e){return t.onError&&t.onError(e,{operation:"verify"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},s)}}
|
|
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)}}
|
package/dist/web/types.d.ts
CHANGED
|
@@ -93,8 +93,26 @@ export interface WebProtectionConfig {
|
|
|
93
93
|
* `403 { error: 'account_flagged' }`. `onFlagged` is ignored in this mode.
|
|
94
94
|
* Requires `fingerprintSdkBundle` to be provided and the default `routePrefix`.
|
|
95
95
|
* @default false
|
|
96
|
+
*
|
|
97
|
+
* `blockFlagged: true` is equivalent to `flaggedMode: 'gate'` (see {@link flaggedMode}).
|
|
96
98
|
*/
|
|
97
99
|
blockFlagged?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* How the SDK enforces a flagged + unverified user (Web/edge equivalent of the Node
|
|
102
|
+
* middleware's `flaggedMode`). Setting it enables enforcement on its own; when both
|
|
103
|
+
* are set, `flaggedMode` wins.
|
|
104
|
+
*
|
|
105
|
+
* - `'gate'` — hard gate (== `blockFlagged: true`): HTML navigations get the standalone
|
|
106
|
+
* verification gate page; everything else gets `403 account_flagged`. Content never sent.
|
|
107
|
+
* - `'overlay'` — modal over blurred live content: HTML navigations render normally and
|
|
108
|
+
* the SDK auto-renders the modal over the blurred page; non-HTML/data requests get
|
|
109
|
+
* `403 account_flagged`. Implies `autoInterstitial`. The HTML is delivered (blur is
|
|
110
|
+
* cosmetic) — the real protection is the `403` on the gated data.
|
|
111
|
+
*
|
|
112
|
+
* Both modes ignore `onFlagged` and require `fingerprintSdkBundle` + the default
|
|
113
|
+
* `routePrefix`. @default undefined
|
|
114
|
+
*/
|
|
115
|
+
flaggedMode?: 'gate' | 'overlay';
|
|
98
116
|
/**
|
|
99
117
|
* Auto-render the interstitial modal on SPA/JSON `403 account_flagged` paths
|
|
100
118
|
* (and the deferred `/status` poll) with no app code. When `true`, the injected
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unshared-clientjs-sdk",
|
|
3
|
-
"version": "2.1.0-rc.
|
|
3
|
+
"version": "2.1.0-rc.7",
|
|
4
4
|
"description": "Server-side Node.js SDK for the Unshared Labs V2 API",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/esm/index.mjs",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"author": "",
|
|
53
53
|
"license": "MIT",
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"unshared-frontend-sdk": "2.1.0-rc.
|
|
55
|
+
"unshared-frontend-sdk": "2.1.0-rc.7"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@unshared-labs/shared-types": "file:../../../shared/types",
|