unshared-clientjs-sdk 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -28,6 +28,18 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
|
|
|
28
28
|
disableBotFilter?: boolean;
|
|
29
29
|
/** Hard timeout (ms) for checkUser on cache miss. Fails open on timeout. @default 1500 */
|
|
30
30
|
checkUserTimeoutMs?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Behaviour on a verdict cache MISS.
|
|
33
|
+
* - `'block'` (default): hold the request until `checkUser` resolves or
|
|
34
|
+
* `checkUserTimeoutMs` fires (then fail open). Enforced from the first request.
|
|
35
|
+
* - `'async'`: render the request immediately and warm the verdict cache in the
|
|
36
|
+
* background (a miss behaves like a stale-while-revalidate hit). Removes the
|
|
37
|
+
* first-hit latency; the trade-off is that the first uncached request per user
|
|
38
|
+
* per `cacheTTL` window is unenforced (it passes through before the verdict
|
|
39
|
+
* lands). Observable via `onFailOpen` with reason `'async_miss'`. Subsequent
|
|
40
|
+
* requests are enforced from the warmed cache. @default 'block'
|
|
41
|
+
*/
|
|
42
|
+
cacheMissStrategy?: 'block' | 'async';
|
|
31
43
|
/** Resolves a custom session ID. Falls back to __unshared_sid cookie. */
|
|
32
44
|
sessionId?: (req: TReq) => string | undefined;
|
|
33
45
|
/**
|
|
@@ -127,15 +139,17 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
|
|
|
127
139
|
* it is invoked best-effort and never blocks the request.
|
|
128
140
|
*
|
|
129
141
|
* `reason`: 'timeout' (exceeded checkUserTimeoutMs), 'http_error' (non-2xx
|
|
130
|
-
* masked into clean), 'exception' (checkUser threw),
|
|
131
|
-
* (no device/fingerprint id yet — bootstrap window)
|
|
142
|
+
* masked into clean), 'exception' (checkUser threw), 'no_device_id'
|
|
143
|
+
* (no device/fingerprint id yet — bootstrap window), or 'async_miss'
|
|
144
|
+
* (cacheMissStrategy: 'async' rendered the first uncached request before the
|
|
145
|
+
* verdict landed — intentional, not a degradation). `status` is present
|
|
132
146
|
* for 'http_error' (0 = network/transport).
|
|
133
147
|
*/
|
|
134
148
|
onFailOpen?: (context: FailOpenContext) => void;
|
|
135
149
|
}
|
|
136
150
|
export interface FailOpenContext {
|
|
137
151
|
operation: 'checkUser';
|
|
138
|
-
reason: 'timeout' | 'http_error' | 'exception' | 'no_device_id';
|
|
152
|
+
reason: 'timeout' | 'http_error' | 'exception' | 'no_device_id' | 'async_miss';
|
|
139
153
|
status?: number;
|
|
140
154
|
userId?: string;
|
|
141
155
|
emailAddress?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFileSync}from"fs";import{VerdictCache}from"./verdict-cache";import{RateLimitBackoff}from"./rate-limit-backoff";import{DispatchDedupe}from"./dispatch-dedupe";import{interceptResponse}from"./response-interceptor";import{generateFingerprintScript}from"./injection/fingerprint-script";import{createFingerprintBundleSource}from"./injection/fp-bundle-source";import{handleSubmitFingerprint}from"./routes/submit-fp";import{handleVerifyTrigger,handleVerify}from"./routes/verify";import{handleGetInterstitialFlow}from"./routes/interstitial";import{generateGatePage}from"./injection/gate-page";import{sendJson,sendEmpty,sendBody,getRequestPath}from"./utils/http-helpers";import{isHtmlContentType,isHtmlNavigation}from"./utils/content-type";import{flaggedResponse}from"./utils/flagged-response";import{maskEmail}from"./utils/mask-email";import{shouldSkipPath}from"./utils/skip-paths";import{shouldIncludePath}from"./utils/include-path";import{isBot}from"./utils/is-bot";import{extractClientIp}from"./utils/client-ip";import{parseCookie}from"./utils/cookies";import{extractDeviceIdOrUndefined}from"./utils/device-id";import{isSecureRequest}from"./utils/secure";import{isSentinelUserId,SENTINEL_STICKINESS_TTL_MS}from"./utils/sentinel-user-id";export{VerdictCache};export{flaggedResponse,ACCOUNT_FLAGGED_ERROR}from"./utils/flagged-response";const CHECK_USER_TIMEOUT_MS=1500,_lastFailOpenWarn={};function warnFailOpenThrottled(e){const t=Date.now();t-(_lastFailOpenWarn[e.reason]??0)<6e4||(_lastFailOpenWarn[e.reason]=t,console.warn(`[Unshared] Failing open (reason=${e.reason}${null!=e.status?`, status=${e.status}`:""}). Detection is not enforced for this request. Configure onFailOpen to observe this directly.`))}export function unsharedBoundToUser(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");if(!t.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:r,emailAddress:i,routePrefix:n="/__unshared",corsOrigins:o,cacheTTL:s=6e4,skipPaths:a,includePathPrefix:d,disableBotFilter:c=!1,checkUserTimeoutMs:l=CHECK_USER_TIMEOUT_MS,sessionId:u,deviceId:p,onFlagged:f,onError:m,onFailOpen:h,blockFlagged:g=!1,flaggedMode:v,autoInterstitial:I=!1,interstitialFlowType:S="email_verification"}=t,_=v??(g?"gate":void 0),C=I||"overlay"===_,y=e=>{if(h)try{h(e)}catch{}else warnFailOpenThrottled(e)},k=new VerdictCache(s),A=new RateLimitBackoff,T=new DispatchDedupe,E=Date.now().toString(36),x=generateFingerprintScript(n,E,{autoInterstitial:C,interstitialFlowType:S});let F="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");F=readFileSync(e,"utf8")}catch{}const U=createFingerprintBundleSource(),w=void 0!==_||C,O="gate"===_?"flaggedMode 'gate' (blockFlagged)":"overlay"===_?"flaggedMode 'overlay'":"autoInterstitial";if(w&&!F)throw new Error(`[Unshared] ${O} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(w&&"/__unshared"!==n)throw new Error(`[Unshared] ${O} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const P="gate"===_?generateGatePage(n,E):"",b=handleSubmitFingerprint({client:e,verdictCache:k,rateLimitBackoff:A,dispatchDedupe:T,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:u,resolveDeviceId:p,disableBotFilter:c,onError:m}),R=handleVerifyTrigger({client:e,verdictCache:k,resolveEmailAddress:i,resolveDeviceId:p,onError:m}),D=handleVerify({client:e,verdictCache:k,resolveUserId:r,resolveEmailAddress:i,resolveDeviceId:p,onError:m}),N=handleGetInterstitialFlow({client:e}),$=o?Array.isArray(o)?o:[o]:null,M=`${n}/fp.js`,V=`${n}/fingerprint.js`,j=`${n}/submit-fp`,L=`${n}/verify-trigger`,B=`${n}/verify`,q=`${n}/status`,G=`${n}/interstitial-flow`,H=function(t,o,s){const h=getRequestPath(t.url),g=t.url||h;if(h.startsWith(n+"/")){if(function(e,t){if(!$)return;const r=e.headers.origin??"";$.includes("*")?(t.setHeader("Access-Control-Allow-Origin","*"),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id")):$.includes(r)&&(t.setHeader("Access-Control-Allow-Origin",r),t.setHeader("Vary","Origin"),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,o),"OPTIONS"===t.method)return void sendEmpty(o,204);if("GET"===t.method&&h===M)return o.setHeader("Content-Type","application/javascript"),o.setHeader("Cache-Control","public, max-age=3600"),void sendBody(o,200,F);if("GET"===t.method&&h===V){const e=U.get();return e?(o.setHeader("Content-Type","application/javascript"),o.setHeader("Cache-Control","public, max-age=3600"),void sendBody(o,200,e)):void sendBody(o,503,"/* fingerprint agent unavailable */")}if("POST"===t.method&&(h===j||h===L||h===B))return void 0===t.body?void sendJson(o,400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"req.body is undefined. Mount a JSON body-parsing middleware (e.g., express.json()) before the Unshared middleware."}}):h===j?void b(t,o):h===L?void R(t,o):void D(t,o);if("GET"===t.method&&h===G)return void N(t,o);if("GET"===t.method&&h===q){let n;try{n=r(t)}catch{}if(!n)return void sendJson(o,200,{status:"anonymous"});const s=resolveEmail(t,i);return void(async()=>{let r=k.get(n);if((!r||k.isStale(n))&&s&&!A.isPaused()&&!k.isRefreshing(n)){k.markRefreshing(n);try{const i=extractDeviceIdOrUndefined(t,p),o=extractFingerprintId(t),a=extractSessionId(t,u),d=i??o??"unknown";await fetchAndCacheVerdict(e,k,n,s,d,o,a,l,(e,t)=>y({operation:"checkUser",reason:e,status:t,userId:n,emailAddress:s})),r=k.get(n)}catch(e){m&&m(e,{operation:"checkUser",userId:n,emailAddress:s}),y({operation:"checkUser",reason:"exception",userId:n,emailAddress:s})}finally{k.clearRefreshing(n)}}r&&r.isFlagged&&!r.isVerified&&(void 0!==_||f)&&s?sendJson(o,200,{status:"flagged",email:maskEmail(s)}):sendJson(o,200,{status:"ok"})})()}return void sendJson(o,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if(shouldSkipPath(h,a))return void s();if(!shouldIncludePath(h,d))return interceptForInjection(t,o,x),void s();let v;try{v=r(t)}catch{}if(isSentinelUserId(v)){const e=parseCookie(t,"__unshared_uid"),r=parseCookie(t,"__unshared_uid_at"),i=r?Number(r):NaN,n=Number.isFinite(i)&&Date.now()-i<=SENTINEL_STICKINESS_TTL_MS;v=e&&n?e:void 0}if(!v){const e=isSecureRequest(t)?"; Secure":"";return appendSetCookie(o,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(o,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(o,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(o,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(t,o,x),void s()}const I=resolveEmail(t,i);if(setUserIdCookie(t,o,v),I&&setEmailCookie(t,o,I),!I)return interceptForInjection(t,o,x),void s();const S=extractSessionId(t,u),C=extractDeviceIdOrUndefined(t,p),E=extractFingerprintId(t),w=t.headers["user-agent"]??"",O=extractClientIp(t),H=C??E;if(!c&&isBot(w))return void s();const J=k.get(v);function K(){"unknown"!==S&&H&&(A.isPaused()||dispatchUserEvent(e,k,A,T,{userId:v,emailAddress:I,sessionId:S,deviceId:H,fingerprintId:E,userAgent:w,ipAddress:O,eventType:g},m))}J?(k.isStale(v)&&!k.isRefreshing(v)&&(k.markRefreshing(v),fetchAndCacheVerdict(e,k,v,I,H??"unknown",E,S,l,(e,t)=>y({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:I})).catch(()=>y({operation:"checkUser",reason:"exception",userId:v,emailAddress:I})).finally(()=>k.clearRefreshing(v))),J.isFlagged||K(),applyVerdict(J,v,I,t,o,s,x,f,_,P)):fetchAndCacheVerdict(e,k,v,I,H??"unknown",E,S,l,(e,t)=>y({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:I})).then(e=>{e.isFlagged||K(),applyVerdict(e,v,I,t,o,s,x,f,_,P)}).catch(()=>{y({operation:"checkUser",reason:"exception",userId:v,emailAddress:I}),K(),interceptForInjection(t,o,x),s()})};return H.destroy=()=>{k.destroy(),U.stop()},H}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=parseCookie(e,"__unshared_email");if(r)return r;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,t,r,i,n,o,s,a,d,c){const l=e.isFlagged&&!e.isVerified;if("gate"===d&&l)isHtmlNavigation(i.method,i.headers.accept)?(n.statusCode=200,n.setHeader("Content-Type","text/html; charset=utf-8"),n.setHeader("Cache-Control","no-store"),n.end(c)):sendJson(n,403,flaggedResponse(r));else if("overlay"===d&&l)isHtmlNavigation(i.method,i.headers.accept)?(interceptForInjection(i,n,s),o()):sendJson(n,403,flaggedResponse(r));else if(interceptForInjection(i,n,s),e.isFlagged&&!e.isVerified&&a)try{a({userId:t,emailAddress:r,verdict:e,req:i,res:n,next:o})}catch{o()}else o()}function interceptForInjection(e,t,r){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],interceptResponse(t,(e,t)=>{if(!isHtmlContentType(t))return null;const i=e.toString("utf8"),n=i.lastIndexOf("</body>");return-1===n?i+r:i.slice(0,n)+r+i.slice(n)},{preventCaching:!0})}function dispatchUserEvent(e,t,r,i,n,o){i.mark(n.userId,n.eventType),e.processUserEvent({eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,sessionHash:n.sessionId,userAgent:n.userAgent}).then(e=>{e.success&&e.data?.analysis&&t.update(n.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&r.pause(1e3*e.error.retryAfter)}).catch(e=>{o&&o(e,{operation:"processUserEvent",userId:n.userId,emailAddress:n.emailAddress})})}async function fetchAndCacheVerdict(e,t,r,i,n,o,s,a=CHECK_USER_TIMEOUT_MS,d){const c={},l=n&&"unknown"!==n?n:void 0,u=l??o,p=o??l;let f;u&&(c.deviceId=u),p&&(c.fingerprintId=p);const m=await Promise.race([e.checkUser(i,c),new Promise(e=>{f=setTimeout(()=>e(null),a)})]);if(clearTimeout(f),!m)return d?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:s,cachedAt:0,ttl:0};m.failedOpen&&d?.(m.failedOpen.reason??"http_error",m.failedOpen.status);const h=m.data?.is_user_flagged??!1;return t.set(r,{isFlagged:h,isVerified:!1,emailAddress:i,sessionId:s},m.failedOpen?5e3:void 0),t.get(r)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return parseCookie(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return parseCookie(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
|
|
1
|
+
import{readFileSync}from"fs";import{VerdictCache}from"./verdict-cache";import{RateLimitBackoff}from"./rate-limit-backoff";import{DispatchDedupe}from"./dispatch-dedupe";import{interceptResponse}from"./response-interceptor";import{generateFingerprintScript}from"./injection/fingerprint-script";import{createFingerprintBundleSource}from"./injection/fp-bundle-source";import{handleSubmitFingerprint}from"./routes/submit-fp";import{handleVerifyTrigger,handleVerify}from"./routes/verify";import{handleGetInterstitialFlow}from"./routes/interstitial";import{generateGatePage}from"./injection/gate-page";import{sendJson,sendEmpty,sendBody,getRequestPath}from"./utils/http-helpers";import{isHtmlContentType,isHtmlNavigation}from"./utils/content-type";import{flaggedResponse}from"./utils/flagged-response";import{maskEmail}from"./utils/mask-email";import{shouldSkipPath}from"./utils/skip-paths";import{shouldIncludePath}from"./utils/include-path";import{isBot}from"./utils/is-bot";import{extractClientIp}from"./utils/client-ip";import{parseCookie}from"./utils/cookies";import{extractDeviceIdOrUndefined}from"./utils/device-id";import{isSecureRequest}from"./utils/secure";import{isSentinelUserId,SENTINEL_STICKINESS_TTL_MS}from"./utils/sentinel-user-id";export{VerdictCache};export{flaggedResponse,ACCOUNT_FLAGGED_ERROR}from"./utils/flagged-response";const CHECK_USER_TIMEOUT_MS=1500,FAIL_OPEN_TTL_MS=5e3,_lastFailOpenWarn={};function warnFailOpenThrottled(e){const t=Date.now();t-(_lastFailOpenWarn[e.reason]??0)<6e4||(_lastFailOpenWarn[e.reason]=t,console.warn(`[Unshared] Failing open (reason=${e.reason}${null!=e.status?`, status=${e.status}`:""}). Detection is not enforced for this request. Configure onFailOpen to observe this directly.`))}export function unsharedBoundToUser(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");if(!t.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:r,emailAddress:i,routePrefix:n="/__unshared",corsOrigins:s,cacheTTL:o=6e4,skipPaths:a,includePathPrefix:d,disableBotFilter:c=!1,checkUserTimeoutMs:l=CHECK_USER_TIMEOUT_MS,cacheMissStrategy:u="block",sessionId:p,deviceId:f,onFlagged:m,onError:h,onFailOpen:g,blockFlagged:I=!1,flaggedMode:v,autoInterstitial:S=!1,interstitialFlowType:_="email_verification"}=t,C=v??(I?"gate":void 0),y=S||"overlay"===C,k=e=>{if(g)try{g(e)}catch{}else"async_miss"!==e.reason&&warnFailOpenThrottled(e)},A=new VerdictCache(o),T=new RateLimitBackoff,E=new DispatchDedupe,F=Date.now().toString(36),U=generateFingerprintScript(n,F,{autoInterstitial:y,interstitialFlowType:_});let x="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");x=readFileSync(e,"utf8")}catch{}const w=createFingerprintBundleSource(),O=void 0!==C||y,P="gate"===C?"flaggedMode 'gate' (blockFlagged)":"overlay"===C?"flaggedMode 'overlay'":"autoInterstitial";if(O&&!x)throw new Error(`[Unshared] ${P} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(O&&"/__unshared"!==n)throw new Error(`[Unshared] ${P} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const b="gate"===C?generateGatePage(n,F):"",R=handleSubmitFingerprint({client:e,verdictCache:A,rateLimitBackoff:T,dispatchDedupe:E,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:p,resolveDeviceId:f,disableBotFilter:c,onError:h}),D=handleVerifyTrigger({client:e,verdictCache:A,resolveEmailAddress:i,resolveDeviceId:f,onError:h}),M=handleVerify({client:e,verdictCache:A,resolveUserId:r,resolveEmailAddress:i,resolveDeviceId:f,onError:h}),N=handleGetInterstitialFlow({client:e}),$=s?Array.isArray(s)?s:[s]:null,V=`${n}/fp.js`,L=`${n}/fingerprint.js`,j=`${n}/submit-fp`,B=`${n}/verify-trigger`,q=`${n}/verify`,G=`${n}/status`,H=`${n}/interstitial-flow`,J=function(t,s,o){const g=getRequestPath(t.url),I=t.url||g;if(g.startsWith(n+"/")){if(function(e,t){if(!$)return;const r=e.headers.origin??"";$.includes("*")?(t.setHeader("Access-Control-Allow-Origin","*"),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id")):$.includes(r)&&(t.setHeader("Access-Control-Allow-Origin",r),t.setHeader("Vary","Origin"),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,s),"OPTIONS"===t.method)return void sendEmpty(s,204);if("GET"===t.method&&g===V)return s.setHeader("Content-Type","application/javascript"),s.setHeader("Cache-Control","public, max-age=3600"),void sendBody(s,200,x);if("GET"===t.method&&g===L){const e=w.get();return e?(s.setHeader("Content-Type","application/javascript"),s.setHeader("Cache-Control","public, max-age=3600"),void sendBody(s,200,e)):void sendBody(s,503,"/* fingerprint agent unavailable */")}if("POST"===t.method&&(g===j||g===B||g===q))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."}}):g===j?void R(t,s):g===B?void D(t,s):void M(t,s);if("GET"===t.method&&g===H)return void N(t,s);if("GET"===t.method&&g===G){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=A.get(n);if((!r||A.isStale(n))&&o&&!T.isPaused()&&!A.isRefreshing(n)){A.markRefreshing(n);try{const i=extractDeviceIdOrUndefined(t,f),s=extractFingerprintId(t),a=extractSessionId(t,p),d=i??s??"unknown";await fetchAndCacheVerdict(e,A,n,o,d,s,a,l,(e,t)=>k({operation:"checkUser",reason:e,status:t,userId:n,emailAddress:o})),r=A.get(n)}catch(e){h&&h(e,{operation:"checkUser",userId:n,emailAddress:o}),k({operation:"checkUser",reason:"exception",userId:n,emailAddress:o})}finally{A.clearRefreshing(n)}}r&&r.isFlagged&&!r.isVerified&&(void 0!==C||m)&&o?sendJson(s,200,{status:"flagged",email:maskEmail(o)}):sendJson(s,200,{status:"ok"})})()}return void sendJson(s,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if(shouldSkipPath(g,a))return void o();if(!shouldIncludePath(g,d))return interceptForInjection(t,s,U),void o();let v;try{v=r(t)}catch{}if(isSentinelUserId(v)){const e=parseCookie(t,"__unshared_uid"),r=parseCookie(t,"__unshared_uid_at"),i=r?Number(r):NaN,n=Number.isFinite(i)&&Date.now()-i<=SENTINEL_STICKINESS_TTL_MS;v=e&&n?e:void 0}if(!v){const e=isSecureRequest(t)?"; Secure":"";return appendSetCookie(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,U),void o()}const S=resolveEmail(t,i);if(setUserIdCookie(t,s,v),S&&setEmailCookie(t,s,S),!S)return interceptForInjection(t,s,U),void o();const _=extractSessionId(t,p),y=extractDeviceIdOrUndefined(t,f),F=extractFingerprintId(t),O=t.headers["user-agent"]??"",P=extractClientIp(t),J=y??F;if(!c&&isBot(O))return void o();const K=A.get(v);if(!K)return"async"===u?(A.isRefreshing(v)||(A.markRefreshing(v),fetchAndCacheVerdict(e,A,v,S,J??"unknown",F,_,l,(e,t)=>k({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:S})).catch(()=>k({operation:"checkUser",reason:"exception",userId:v,emailAddress:S})).finally(()=>A.clearRefreshing(v))),k({operation:"checkUser",reason:"async_miss",userId:v,emailAddress:S}),X(),interceptForInjection(t,s,U),void o()):void fetchAndCacheVerdict(e,A,v,S,J??"unknown",F,_,l,(e,t)=>k({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:S})).then(e=>{e.isFlagged||X(),applyVerdict(e,v,S,t,s,o,U,m,C,b)}).catch(()=>{k({operation:"checkUser",reason:"exception",userId:v,emailAddress:S}),X(),interceptForInjection(t,s,U),o()});function X(){"unknown"!==_&&J&&(T.isPaused()||dispatchUserEvent(e,A,T,E,{userId:v,emailAddress:S,sessionId:_,deviceId:J,fingerprintId:F,userAgent:O,ipAddress:P,eventType:I},h))}A.isStale(v)&&!A.isRefreshing(v)&&(A.markRefreshing(v),fetchAndCacheVerdict(e,A,v,S,J??"unknown",F,_,l,(e,t)=>k({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:S})).catch(()=>k({operation:"checkUser",reason:"exception",userId:v,emailAddress:S})).finally(()=>A.clearRefreshing(v))),K.isFlagged||X(),applyVerdict(K,v,S,t,s,o,U,m,C,b)};return J.destroy=()=>{A.destroy(),w.stop()},J}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,a,d,c){const l=e.isFlagged&&!e.isVerified;if("gate"===d&&l)isHtmlNavigation(i.method,i.headers.accept)?(n.statusCode=200,n.setHeader("Content-Type","text/html; charset=utf-8"),n.setHeader("Cache-Control","no-store"),n.end(c)):sendJson(n,403,flaggedResponse(r));else if("overlay"===d&&l)isHtmlNavigation(i.method,i.headers.accept)?(interceptForInjection(i,n,o),s()):sendJson(n,403,flaggedResponse(r));else if(interceptForInjection(i,n,o),e.isFlagged&&!e.isVerified&&a)try{a({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,a=CHECK_USER_TIMEOUT_MS,d){const c={},l=n&&"unknown"!==n?n:void 0,u=l??s,p=s??l;let f;u&&(c.deviceId=u),p&&(c.fingerprintId=p);const m=await Promise.race([e.checkUser(i,c),new Promise(e=>{f=setTimeout(()=>e(null),a)})]);return clearTimeout(f),m?m.failedOpen?(d?.(m.failedOpen.reason??"http_error",m.failedOpen.status),cacheFailOpenVerdict(t,r,i,o)):(t.set(r,{isFlagged:m.data?.is_user_flagged??!1,isVerified:!1,emailAddress:i,sessionId:o}),t.get(r)):(d?.("timeout"),cacheFailOpenVerdict(t,r,i,o))}function cacheFailOpenVerdict(e,t,r,i){const n=e.get(t);return e.set(t,{isFlagged:n?.isFlagged??!1,isVerified:n?.isVerified??!1,emailAddress:r,sessionId:i},5e3),e.get(t)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return parseCookie(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return parseCookie(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
|
|
@@ -28,6 +28,18 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
|
|
|
28
28
|
disableBotFilter?: boolean;
|
|
29
29
|
/** Hard timeout (ms) for checkUser on cache miss. Fails open on timeout. @default 1500 */
|
|
30
30
|
checkUserTimeoutMs?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Behaviour on a verdict cache MISS.
|
|
33
|
+
* - `'block'` (default): hold the request until `checkUser` resolves or
|
|
34
|
+
* `checkUserTimeoutMs` fires (then fail open). Enforced from the first request.
|
|
35
|
+
* - `'async'`: render the request immediately and warm the verdict cache in the
|
|
36
|
+
* background (a miss behaves like a stale-while-revalidate hit). Removes the
|
|
37
|
+
* first-hit latency; the trade-off is that the first uncached request per user
|
|
38
|
+
* per `cacheTTL` window is unenforced (it passes through before the verdict
|
|
39
|
+
* lands). Observable via `onFailOpen` with reason `'async_miss'`. Subsequent
|
|
40
|
+
* requests are enforced from the warmed cache. @default 'block'
|
|
41
|
+
*/
|
|
42
|
+
cacheMissStrategy?: 'block' | 'async';
|
|
31
43
|
/** Resolves a custom session ID. Falls back to __unshared_sid cookie. */
|
|
32
44
|
sessionId?: (req: TReq) => string | undefined;
|
|
33
45
|
/**
|
|
@@ -127,15 +139,17 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
|
|
|
127
139
|
* it is invoked best-effort and never blocks the request.
|
|
128
140
|
*
|
|
129
141
|
* `reason`: 'timeout' (exceeded checkUserTimeoutMs), 'http_error' (non-2xx
|
|
130
|
-
* masked into clean), 'exception' (checkUser threw),
|
|
131
|
-
* (no device/fingerprint id yet — bootstrap window)
|
|
142
|
+
* masked into clean), 'exception' (checkUser threw), 'no_device_id'
|
|
143
|
+
* (no device/fingerprint id yet — bootstrap window), or 'async_miss'
|
|
144
|
+
* (cacheMissStrategy: 'async' rendered the first uncached request before the
|
|
145
|
+
* verdict landed — intentional, not a degradation). `status` is present
|
|
132
146
|
* for 'http_error' (0 = network/transport).
|
|
133
147
|
*/
|
|
134
148
|
onFailOpen?: (context: FailOpenContext) => void;
|
|
135
149
|
}
|
|
136
150
|
export interface FailOpenContext {
|
|
137
151
|
operation: 'checkUser';
|
|
138
|
-
reason: 'timeout' | 'http_error' | 'exception' | 'no_device_id';
|
|
152
|
+
reason: 'timeout' | 'http_error' | 'exception' | 'no_device_id' | 'async_miss';
|
|
139
153
|
status?: number;
|
|
140
154
|
userId?: string;
|
|
141
155
|
emailAddress?: string;
|
package/dist/middleware/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"t",{value:!0}),exports.ACCOUNT_FLAGGED_ERROR=exports.flaggedResponse=exports.VerdictCache=void 0,exports.unsharedBoundToUser=unsharedBoundToUser;const fs_1=require("fs"),verdict_cache_1=require("./verdict-cache");Object.defineProperty(exports,"VerdictCache",{enumerable:!0,get:function(){return verdict_cache_1.VerdictCache}});const rate_limit_backoff_1=require("./rate-limit-backoff"),dispatch_dedupe_1=require("./dispatch-dedupe"),response_interceptor_1=require("./response-interceptor"),fingerprint_script_1=require("./injection/fingerprint-script"),fp_bundle_source_1=require("./injection/fp-bundle-source"),submit_fp_1=require("./routes/submit-fp"),verify_1=require("./routes/verify"),interstitial_1=require("./routes/interstitial"),gate_page_1=require("./injection/gate-page"),http_helpers_1=require("./utils/http-helpers"),content_type_1=require("./utils/content-type"),flagged_response_1=require("./utils/flagged-response"),mask_email_1=require("./utils/mask-email"),skip_paths_1=require("./utils/skip-paths"),include_path_1=require("./utils/include-path"),is_bot_1=require("./utils/is-bot"),client_ip_1=require("./utils/client-ip"),cookies_1=require("./utils/cookies"),device_id_1=require("./utils/device-id"),secure_1=require("./utils/secure"),sentinel_user_id_1=require("./utils/sentinel-user-id");var flagged_response_2=require("./utils/flagged-response");Object.defineProperty(exports,"flaggedResponse",{enumerable:!0,get:function(){return flagged_response_2.flaggedResponse}}),Object.defineProperty(exports,"ACCOUNT_FLAGGED_ERROR",{enumerable:!0,get:function(){return flagged_response_2.ACCOUNT_FLAGGED_ERROR}});const CHECK_USER_TIMEOUT_MS=1500,_lastFailOpenWarn={};function warnFailOpenThrottled(e){const t=Date.now();t-(_lastFailOpenWarn[e.reason]??0)<6e4||(_lastFailOpenWarn[e.reason]=t,console.warn(`[Unshared] Failing open (reason=${e.reason}${null!=e.status?`, status=${e.status}`:""}). Detection is not enforced for this request. Configure onFailOpen to observe this directly.`))}function unsharedBoundToUser(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");if(!t.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:r,emailAddress:i,routePrefix:s="/__unshared",corsOrigins:n,cacheTTL:o=6e4,skipPaths:a,includePathPrefix:d,disableBotFilter:c=!1,checkUserTimeoutMs:l=CHECK_USER_TIMEOUT_MS,sessionId:u,deviceId:_,onFlagged:p,onError:h,onFailOpen:f,blockFlagged:g=!1,flaggedMode:v,autoInterstitial:m=!1,interstitialFlowType:I="email_verification"}=t,k=v??(g?"gate":void 0),y=m||"overlay"===k,C=e=>{if(f)try{f(e)}catch{}else warnFailOpenThrottled(e)},S=new verdict_cache_1.VerdictCache(o),A=new rate_limit_backoff_1.RateLimitBackoff,x=new dispatch_dedupe_1.DispatchDedupe,T=Date.now().toString(36),b=(0,fingerprint_script_1.generateFingerprintScript)(s,T,{autoInterstitial:y,interstitialFlowType:I});let E="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");E=(0,fs_1.readFileSync)(e,"utf8")}catch{}const w=(0,fp_bundle_source_1.createFingerprintBundleSource)(),U=void 0!==k||y,O="gate"===k?"flaggedMode 'gate' (blockFlagged)":"overlay"===k?"flaggedMode 'overlay'":"autoInterstitial";if(U&&!E)throw new Error(`[Unshared] ${O} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(U&&"/__unshared"!==s)throw new Error(`[Unshared] ${O} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const q="gate"===k?(0,gate_page_1.generateGatePage)(s,T):"",F=(0,submit_fp_1.handleSubmitFingerprint)({client:e,verdictCache:S,rateLimitBackoff:A,dispatchDedupe:x,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:u,resolveDeviceId:_,disableBotFilter:c,onError:h}),$=(0,verify_1.handleVerifyTrigger)({client:e,verdictCache:S,resolveEmailAddress:i,resolveDeviceId:_,onError:h}),P=(0,verify_1.handleVerify)({client:e,verdictCache:S,resolveUserId:r,resolveEmailAddress:i,resolveDeviceId:_,onError:h}),j=(0,interstitial_1.handleGetInterstitialFlow)({client:e}),M=n?Array.isArray(n)?n:[n]:null,D=`${s}/fp.js`,N=`${s}/fingerprint.js`,R=`${s}/submit-fp`,V=`${s}/verify-trigger`,L=`${s}/verify`,G=`${s}/status`,H=`${s}/interstitial-flow`,B=function(t,n,o){const f=(0,http_helpers_1.getRequestPath)(t.url),g=t.url||f;if(f.startsWith(s+"/")){if(function(e,t){if(!M)return;const r=e.headers.origin??"";M.includes("*")?(t.setHeader("Access-Control-Allow-Origin","*"),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id")):M.includes(r)&&(t.setHeader("Access-Control-Allow-Origin",r),t.setHeader("Vary","Origin"),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,n),"OPTIONS"===t.method)return void(0,http_helpers_1.sendEmpty)(n,204);if("GET"===t.method&&f===D)return n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBody)(n,200,E);if("GET"===t.method&&f===N){const e=w.get();return e?(n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBody)(n,200,e)):void(0,http_helpers_1.sendBody)(n,503,"/* fingerprint agent unavailable */")}if("POST"===t.method&&(f===R||f===V||f===L))return void 0===t.body?void(0,http_helpers_1.sendJson)(n,400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"req.body is undefined. Mount a JSON body-parsing middleware (e.g., express.json()) before the Unshared middleware."}}):f===R?void F(t,n):f===V?void $(t,n):void P(t,n);if("GET"===t.method&&f===H)return void j(t,n);if("GET"===t.method&&f===G){let s;try{s=r(t)}catch{}if(!s)return void(0,http_helpers_1.sendJson)(n,200,{status:"anonymous"});const o=resolveEmail(t,i);return void(async()=>{let r=S.get(s);if((!r||S.isStale(s))&&o&&!A.isPaused()&&!S.isRefreshing(s)){S.markRefreshing(s);try{const i=(0,device_id_1.extractDeviceIdOrUndefined)(t,_),n=extractFingerprintId(t),a=extractSessionId(t,u),d=i??n??"unknown";await fetchAndCacheVerdict(e,S,s,o,d,n,a,l,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:s,emailAddress:o})),r=S.get(s)}catch(e){h&&h(e,{operation:"checkUser",userId:s,emailAddress:o}),C({operation:"checkUser",reason:"exception",userId:s,emailAddress:o})}finally{S.clearRefreshing(s)}}r&&r.isFlagged&&!r.isVerified&&(void 0!==k||p)&&o?(0,http_helpers_1.sendJson)(n,200,{status:"flagged",email:(0,mask_email_1.maskEmail)(o)}):(0,http_helpers_1.sendJson)(n,200,{status:"ok"})})()}return void(0,http_helpers_1.sendJson)(n,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if((0,skip_paths_1.shouldSkipPath)(f,a))return void o();if(!(0,include_path_1.shouldIncludePath)(f,d))return interceptForInjection(t,n,b),void o();let v;try{v=r(t)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(v)){const e=(0,cookies_1.parseCookie)(t,"__unshared_uid"),r=(0,cookies_1.parseCookie)(t,"__unshared_uid_at"),i=r?Number(r):NaN,s=Number.isFinite(i)&&Date.now()-i<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;v=e&&s?e:void 0}if(!v){const e=(0,secure_1.isSecureRequest)(t)?"; Secure":"";return appendSetCookie(n,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(t,n,b),void o()}const m=resolveEmail(t,i);if(setUserIdCookie(t,n,v),m&&setEmailCookie(t,n,m),!m)return interceptForInjection(t,n,b),void o();const I=extractSessionId(t,u),y=(0,device_id_1.extractDeviceIdOrUndefined)(t,_),T=extractFingerprintId(t),U=t.headers["user-agent"]??"",O=(0,client_ip_1.extractClientIp)(t),B=y??T;if(!c&&(0,is_bot_1.isBot)(U))return void o();const K=S.get(v);function X(){"unknown"!==I&&B&&(A.isPaused()||dispatchUserEvent(e,S,A,x,{userId:v,emailAddress:m,sessionId:I,deviceId:B,fingerprintId:T,userAgent:U,ipAddress:O,eventType:g},h))}K?(S.isStale(v)&&!S.isRefreshing(v)&&(S.markRefreshing(v),fetchAndCacheVerdict(e,S,v,m,B??"unknown",T,I,l,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:m})).catch(()=>C({operation:"checkUser",reason:"exception",userId:v,emailAddress:m})).finally(()=>S.clearRefreshing(v))),K.isFlagged||X(),applyVerdict(K,v,m,t,n,o,b,p,k,q)):fetchAndCacheVerdict(e,S,v,m,B??"unknown",T,I,l,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:m})).then(e=>{e.isFlagged||X(),applyVerdict(e,v,m,t,n,o,b,p,k,q)}).catch(()=>{C({operation:"checkUser",reason:"exception",userId:v,emailAddress:m}),X(),interceptForInjection(t,n,b),o()})};return B.destroy=()=>{S.destroy(),w.stop()},B}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=(0,cookies_1.parseCookie)(e,"__unshared_email");if(r)return r;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,t,r,i,s,n,o,a,d,c){const l=e.isFlagged&&!e.isVerified;if("gate"===d&&l)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(s.statusCode=200,s.setHeader("Content-Type","text/html; charset=utf-8"),s.setHeader("Cache-Control","no-store"),s.end(c)):(0,http_helpers_1.sendJson)(s,403,(0,flagged_response_1.flaggedResponse)(r));else if("overlay"===d&&l)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(interceptForInjection(i,s,o),n()):(0,http_helpers_1.sendJson)(s,403,(0,flagged_response_1.flaggedResponse)(r));else if(interceptForInjection(i,s,o),e.isFlagged&&!e.isVerified&&a)try{a({userId:t,emailAddress:r,verdict:e,req:i,res:s,next:n})}catch{n()}else n()}function interceptForInjection(e,t,r){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],(0,response_interceptor_1.interceptResponse)(t,(e,t)=>{if(!(0,content_type_1.isHtmlContentType)(t))return null;const i=e.toString("utf8"),s=i.lastIndexOf("</body>");return-1===s?i+r:i.slice(0,s)+r+i.slice(s)},{preventCaching:!0})}function dispatchUserEvent(e,t,r,i,s,n){i.mark(s.userId,s.eventType),e.processUserEvent({eventType:s.eventType,userId:s.userId,emailAddress:s.emailAddress,ipAddress:s.ipAddress,deviceId:s.deviceId,fingerprintId:s.fingerprintId,sessionHash:s.sessionId,userAgent:s.userAgent}).then(e=>{e.success&&e.data?.analysis&&t.update(s.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&r.pause(1e3*e.error.retryAfter)}).catch(e=>{n&&n(e,{operation:"processUserEvent",userId:s.userId,emailAddress:s.emailAddress})})}async function fetchAndCacheVerdict(e,t,r,i,s,n,o,a=CHECK_USER_TIMEOUT_MS,d){const c={},l=s&&"unknown"!==s?s:void 0,u=l??n,_=n??l;let p;u&&(c.deviceId=u),_&&(c.fingerprintId=_);const h=await Promise.race([e.checkUser(i,c),new Promise(e=>{p=setTimeout(()=>e(null),a)})]);if(clearTimeout(p),!h)return d?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:o,cachedAt:0,ttl:0};h.failedOpen&&d?.(h.failedOpen.reason??"http_error",h.failedOpen.status);const f=h.data?.is_user_flagged??!1;return t.set(r,{isFlagged:f,isVerified:!1,emailAddress:i,sessionId:o},h.failedOpen?5e3:void 0),t.get(r)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return(0,cookies_1.parseCookie)(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return(0,cookies_1.parseCookie)(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
|
|
1
|
+
"use strict";Object.defineProperty(exports,"t",{value:!0}),exports.ACCOUNT_FLAGGED_ERROR=exports.flaggedResponse=exports.VerdictCache=void 0,exports.unsharedBoundToUser=unsharedBoundToUser;const fs_1=require("fs"),verdict_cache_1=require("./verdict-cache");Object.defineProperty(exports,"VerdictCache",{enumerable:!0,get:function(){return verdict_cache_1.VerdictCache}});const rate_limit_backoff_1=require("./rate-limit-backoff"),dispatch_dedupe_1=require("./dispatch-dedupe"),response_interceptor_1=require("./response-interceptor"),fingerprint_script_1=require("./injection/fingerprint-script"),fp_bundle_source_1=require("./injection/fp-bundle-source"),submit_fp_1=require("./routes/submit-fp"),verify_1=require("./routes/verify"),interstitial_1=require("./routes/interstitial"),gate_page_1=require("./injection/gate-page"),http_helpers_1=require("./utils/http-helpers"),content_type_1=require("./utils/content-type"),flagged_response_1=require("./utils/flagged-response"),mask_email_1=require("./utils/mask-email"),skip_paths_1=require("./utils/skip-paths"),include_path_1=require("./utils/include-path"),is_bot_1=require("./utils/is-bot"),client_ip_1=require("./utils/client-ip"),cookies_1=require("./utils/cookies"),device_id_1=require("./utils/device-id"),secure_1=require("./utils/secure"),sentinel_user_id_1=require("./utils/sentinel-user-id");var flagged_response_2=require("./utils/flagged-response");Object.defineProperty(exports,"flaggedResponse",{enumerable:!0,get:function(){return flagged_response_2.flaggedResponse}}),Object.defineProperty(exports,"ACCOUNT_FLAGGED_ERROR",{enumerable:!0,get:function(){return flagged_response_2.ACCOUNT_FLAGGED_ERROR}});const CHECK_USER_TIMEOUT_MS=1500,FAIL_OPEN_TTL_MS=5e3,_lastFailOpenWarn={};function warnFailOpenThrottled(e){const t=Date.now();t-(_lastFailOpenWarn[e.reason]??0)<6e4||(_lastFailOpenWarn[e.reason]=t,console.warn(`[Unshared] Failing open (reason=${e.reason}${null!=e.status?`, status=${e.status}`:""}). Detection is not enforced for this request. Configure onFailOpen to observe this directly.`))}function unsharedBoundToUser(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");if(!t.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:r,emailAddress:i,routePrefix:s="/__unshared",corsOrigins:n,cacheTTL:o=6e4,skipPaths:a,includePathPrefix:d,disableBotFilter:c=!1,checkUserTimeoutMs:l=CHECK_USER_TIMEOUT_MS,cacheMissStrategy:u="block",sessionId:_,deviceId:p,onFlagged:h,onError:f,onFailOpen:g,blockFlagged:v=!1,flaggedMode:m,autoInterstitial:I=!1,interstitialFlowType:k="email_verification"}=t,y=m??(v?"gate":void 0),A=I||"overlay"===y,C=e=>{if(g)try{g(e)}catch{}else"async_miss"!==e.reason&&warnFailOpenThrottled(e)},S=new verdict_cache_1.VerdictCache(o),T=new rate_limit_backoff_1.RateLimitBackoff,x=new dispatch_dedupe_1.DispatchDedupe,b=Date.now().toString(36),E=(0,fingerprint_script_1.generateFingerprintScript)(s,b,{autoInterstitial:A,interstitialFlowType:k});let O="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");O=(0,fs_1.readFileSync)(e,"utf8")}catch{}const U=(0,fp_bundle_source_1.createFingerprintBundleSource)(),w=void 0!==y||A,F="gate"===y?"flaggedMode 'gate' (blockFlagged)":"overlay"===y?"flaggedMode 'overlay'":"autoInterstitial";if(w&&!O)throw new Error(`[Unshared] ${F} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(w&&"/__unshared"!==s)throw new Error(`[Unshared] ${F} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const q="gate"===y?(0,gate_page_1.generateGatePage)(s,b):"",M=(0,submit_fp_1.handleSubmitFingerprint)({client:e,verdictCache:S,rateLimitBackoff:T,dispatchDedupe:x,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:_,resolveDeviceId:p,disableBotFilter:c,onError:f}),P=(0,verify_1.handleVerifyTrigger)({client:e,verdictCache:S,resolveEmailAddress:i,resolveDeviceId:p,onError:f}),$=(0,verify_1.handleVerify)({client:e,verdictCache:S,resolveUserId:r,resolveEmailAddress:i,resolveDeviceId:p,onError:f}),j=(0,interstitial_1.handleGetInterstitialFlow)({client:e}),D=n?Array.isArray(n)?n:[n]:null,V=`${s}/fp.js`,N=`${s}/fingerprint.js`,L=`${s}/submit-fp`,R=`${s}/verify-trigger`,G=`${s}/verify`,H=`${s}/status`,B=`${s}/interstitial-flow`,K=function(t,n,o){const g=(0,http_helpers_1.getRequestPath)(t.url),v=t.url||g;if(g.startsWith(s+"/")){if(function(e,t){if(!D)return;const r=e.headers.origin??"";D.includes("*")?(t.setHeader("Access-Control-Allow-Origin","*"),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id")):D.includes(r)&&(t.setHeader("Access-Control-Allow-Origin",r),t.setHeader("Vary","Origin"),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,n),"OPTIONS"===t.method)return void(0,http_helpers_1.sendEmpty)(n,204);if("GET"===t.method&&g===V)return n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBody)(n,200,O);if("GET"===t.method&&g===N){const e=U.get();return e?(n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBody)(n,200,e)):void(0,http_helpers_1.sendBody)(n,503,"/* fingerprint agent unavailable */")}if("POST"===t.method&&(g===L||g===R||g===G))return void 0===t.body?void(0,http_helpers_1.sendJson)(n,400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"req.body is undefined. Mount a JSON body-parsing middleware (e.g., express.json()) before the Unshared middleware."}}):g===L?void M(t,n):g===R?void P(t,n):void $(t,n);if("GET"===t.method&&g===B)return void j(t,n);if("GET"===t.method&&g===H){let s;try{s=r(t)}catch{}if(!s)return void(0,http_helpers_1.sendJson)(n,200,{status:"anonymous"});const o=resolveEmail(t,i);return void(async()=>{let r=S.get(s);if((!r||S.isStale(s))&&o&&!T.isPaused()&&!S.isRefreshing(s)){S.markRefreshing(s);try{const i=(0,device_id_1.extractDeviceIdOrUndefined)(t,p),n=extractFingerprintId(t),a=extractSessionId(t,_),d=i??n??"unknown";await fetchAndCacheVerdict(e,S,s,o,d,n,a,l,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:s,emailAddress:o})),r=S.get(s)}catch(e){f&&f(e,{operation:"checkUser",userId:s,emailAddress:o}),C({operation:"checkUser",reason:"exception",userId:s,emailAddress:o})}finally{S.clearRefreshing(s)}}r&&r.isFlagged&&!r.isVerified&&(void 0!==y||h)&&o?(0,http_helpers_1.sendJson)(n,200,{status:"flagged",email:(0,mask_email_1.maskEmail)(o)}):(0,http_helpers_1.sendJson)(n,200,{status:"ok"})})()}return void(0,http_helpers_1.sendJson)(n,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if((0,skip_paths_1.shouldSkipPath)(g,a))return void o();if(!(0,include_path_1.shouldIncludePath)(g,d))return interceptForInjection(t,n,E),void o();let m;try{m=r(t)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(m)){const e=(0,cookies_1.parseCookie)(t,"__unshared_uid"),r=(0,cookies_1.parseCookie)(t,"__unshared_uid_at"),i=r?Number(r):NaN,s=Number.isFinite(i)&&Date.now()-i<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;m=e&&s?e:void 0}if(!m){const e=(0,secure_1.isSecureRequest)(t)?"; Secure":"";return appendSetCookie(n,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(t,n,E),void o()}const I=resolveEmail(t,i);if(setUserIdCookie(t,n,m),I&&setEmailCookie(t,n,I),!I)return interceptForInjection(t,n,E),void o();const k=extractSessionId(t,_),A=(0,device_id_1.extractDeviceIdOrUndefined)(t,p),b=extractFingerprintId(t),w=t.headers["user-agent"]??"",F=(0,client_ip_1.extractClientIp)(t),K=A??b;if(!c&&(0,is_bot_1.isBot)(w))return void o();const X=S.get(m);if(!X)return"async"===u?(S.isRefreshing(m)||(S.markRefreshing(m),fetchAndCacheVerdict(e,S,m,I,K??"unknown",b,k,l,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:m,emailAddress:I})).catch(()=>C({operation:"checkUser",reason:"exception",userId:m,emailAddress:I})).finally(()=>S.clearRefreshing(m))),C({operation:"checkUser",reason:"async_miss",userId:m,emailAddress:I}),W(),interceptForInjection(t,n,E),void o()):void fetchAndCacheVerdict(e,S,m,I,K??"unknown",b,k,l,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:m,emailAddress:I})).then(e=>{e.isFlagged||W(),applyVerdict(e,m,I,t,n,o,E,h,y,q)}).catch(()=>{C({operation:"checkUser",reason:"exception",userId:m,emailAddress:I}),W(),interceptForInjection(t,n,E),o()});function W(){"unknown"!==k&&K&&(T.isPaused()||dispatchUserEvent(e,S,T,x,{userId:m,emailAddress:I,sessionId:k,deviceId:K,fingerprintId:b,userAgent:w,ipAddress:F,eventType:v},f))}S.isStale(m)&&!S.isRefreshing(m)&&(S.markRefreshing(m),fetchAndCacheVerdict(e,S,m,I,K??"unknown",b,k,l,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:m,emailAddress:I})).catch(()=>C({operation:"checkUser",reason:"exception",userId:m,emailAddress:I})).finally(()=>S.clearRefreshing(m))),X.isFlagged||W(),applyVerdict(X,m,I,t,n,o,E,h,y,q)};return K.destroy=()=>{S.destroy(),U.stop()},K}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=(0,cookies_1.parseCookie)(e,"__unshared_email");if(r)return r;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,t,r,i,s,n,o,a,d,c){const l=e.isFlagged&&!e.isVerified;if("gate"===d&&l)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(s.statusCode=200,s.setHeader("Content-Type","text/html; charset=utf-8"),s.setHeader("Cache-Control","no-store"),s.end(c)):(0,http_helpers_1.sendJson)(s,403,(0,flagged_response_1.flaggedResponse)(r));else if("overlay"===d&&l)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(interceptForInjection(i,s,o),n()):(0,http_helpers_1.sendJson)(s,403,(0,flagged_response_1.flaggedResponse)(r));else if(interceptForInjection(i,s,o),e.isFlagged&&!e.isVerified&&a)try{a({userId:t,emailAddress:r,verdict:e,req:i,res:s,next:n})}catch{n()}else n()}function interceptForInjection(e,t,r){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],(0,response_interceptor_1.interceptResponse)(t,(e,t)=>{if(!(0,content_type_1.isHtmlContentType)(t))return null;const i=e.toString("utf8"),s=i.lastIndexOf("</body>");return-1===s?i+r:i.slice(0,s)+r+i.slice(s)},{preventCaching:!0})}function dispatchUserEvent(e,t,r,i,s,n){i.mark(s.userId,s.eventType),e.processUserEvent({eventType:s.eventType,userId:s.userId,emailAddress:s.emailAddress,ipAddress:s.ipAddress,deviceId:s.deviceId,fingerprintId:s.fingerprintId,sessionHash:s.sessionId,userAgent:s.userAgent}).then(e=>{e.success&&e.data?.analysis&&t.update(s.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&r.pause(1e3*e.error.retryAfter)}).catch(e=>{n&&n(e,{operation:"processUserEvent",userId:s.userId,emailAddress:s.emailAddress})})}async function fetchAndCacheVerdict(e,t,r,i,s,n,o,a=CHECK_USER_TIMEOUT_MS,d){const c={},l=s&&"unknown"!==s?s:void 0,u=l??n,_=n??l;let p;u&&(c.deviceId=u),_&&(c.fingerprintId=_);const h=await Promise.race([e.checkUser(i,c),new Promise(e=>{p=setTimeout(()=>e(null),a)})]);return clearTimeout(p),h?h.failedOpen?(d?.(h.failedOpen.reason??"http_error",h.failedOpen.status),cacheFailOpenVerdict(t,r,i,o)):(t.set(r,{isFlagged:h.data?.is_user_flagged??!1,isVerified:!1,emailAddress:i,sessionId:o}),t.get(r)):(d?.("timeout"),cacheFailOpenVerdict(t,r,i,o))}function cacheFailOpenVerdict(e,t,r,i){const s=e.get(t);return e.set(t,{isFlagged:s?.isFlagged??!1,isVerified:s?.isVerified??!1,emailAddress:r,sessionId:i},5e3),e.get(t)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return(0,cookies_1.parseCookie)(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return(0,cookies_1.parseCookie)(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unshared-clientjs-sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Server-side Node.js SDK for the Unshared Labs V2 API",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/esm/index.mjs",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"author": "",
|
|
53
53
|
"license": "MIT",
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"unshared-frontend-sdk": "2.
|
|
55
|
+
"unshared-frontend-sdk": "2.2.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@unshared-labs/shared-types": "file:../../../shared/types",
|