unshared-clientjs-sdk 3.0.0-rc.14 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +187 -15
  2. package/dist/esm/middleware/dispatch-processing.d.mts +5 -2
  3. package/dist/esm/middleware/dispatch-processing.mjs +1 -1
  4. package/dist/esm/middleware/index.d.mts +3 -1
  5. package/dist/esm/middleware/index.mjs +1 -1
  6. package/dist/esm/middleware/injection/fingerprint-script.d.mts +2 -0
  7. package/dist/esm/middleware/injection/fingerprint-script.mjs +1 -1
  8. package/dist/esm/middleware/routes/submit-fp.mjs +1 -1
  9. package/dist/esm/middleware/routes/verify.d.mts +2 -1
  10. package/dist/esm/middleware/routes/verify.mjs +1 -1
  11. package/dist/esm/middleware/utils/device-id.d.mts +4 -2
  12. package/dist/esm/middleware/utils/device-id.mjs +1 -1
  13. package/dist/esm/web/protection-handler.d.mts +1 -1
  14. package/dist/esm/web/protection-handler.mjs +1 -1
  15. package/dist/esm/web/types.d.mts +4 -2
  16. package/dist/esm/web/web-helpers.d.mts +2 -1
  17. package/dist/esm/web/web-helpers.mjs +1 -1
  18. package/dist/middleware/dispatch-processing.d.ts +5 -2
  19. package/dist/middleware/dispatch-processing.js +1 -1
  20. package/dist/middleware/index.d.ts +3 -1
  21. package/dist/middleware/index.js +1 -1
  22. package/dist/middleware/injection/fingerprint-script.d.ts +2 -0
  23. package/dist/middleware/injection/fingerprint-script.js +1 -1
  24. package/dist/middleware/routes/submit-fp.js +1 -1
  25. package/dist/middleware/routes/verify.d.ts +2 -1
  26. package/dist/middleware/routes/verify.js +1 -1
  27. package/dist/middleware/utils/device-id.d.ts +4 -2
  28. package/dist/middleware/utils/device-id.js +1 -1
  29. package/dist/web/protection-handler.d.ts +1 -1
  30. package/dist/web/protection-handler.js +1 -1
  31. package/dist/web/types.d.ts +4 -2
  32. package/dist/web/web-helpers.d.ts +2 -1
  33. package/dist/web/web-helpers.js +1 -1
  34. package/package.json +2 -2
@@ -1 +1 @@
1
- import{sha256HexWeb}from"../webcrypto.mjs";import{VerdictCache}from"../middleware/verdict-cache.mjs";import{RateLimitBackoff}from"../middleware/rate-limit-backoff.mjs";import{DispatchDedupe}from"../middleware/dispatch-dedupe.mjs";import{generateFingerprintScript}from"../middleware/injection/fingerprint-script.mjs";import{generateGatePage}from"../middleware/injection/gate-page.mjs";import{flaggedResponse}from"../middleware/utils/flagged-response.mjs";import{maskEmail}from"../middleware/utils/mask-email.mjs";import{isHtmlContentType,isHtmlNavigation}from"../middleware/utils/content-type.mjs";import{shouldSkipPath}from"../middleware/utils/skip-paths.mjs";import{shouldIncludePath}from"../middleware/utils/include-path.mjs";import{isBot}from"../middleware/utils/is-bot.mjs";import{isSentinelUserId,SENTINEL_STICKINESS_TTL_MS}from"../middleware/utils/sentinel-user-id.mjs";import{runCheckUser,runProcessUserEvent,DISPATCH_TIMEOUT_MS,SUBMIT_FP_TIMEOUT_MS}from"../middleware/dispatch-processing.mjs";import{parseCookieFromRequest,extractClientIpFromRequest,extractDeviceIdFromRequest,extractDeviceIdFromRequestOrUnknown,isSecureWebRequest,jsonResponse,emptyResponse,bodyResponse,mergeResponseHeaders,readJsonRequest}from"./web-helpers.mjs";const CHECK_USER_TIMEOUT_MS=1500,FAIL_OPEN_TTL_MS=5e3;export function createWebProtectionMiddleware(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");const{userId:r,emailAddress:s,routePrefix:n="/__unshared",corsOrigins:i,cacheTTL:o=6e4,skipPaths:a,includePathPrefix:d,sessionId:c,deviceId:u,fingerprintSdkBundle:p="",onFlagged:l,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,y=R??(g?"gate":void 0),S=I||"overlay"===y,x=e=>{if(f)try{f(e)}catch{}},w=Date.now().toString(36),C=void 0!==y||S,j="gate"===y?"flaggedMode 'gate' (blockFlagged)":"overlay"===y?"flaggedMode 'overlay'":"autoInterstitial";if(C&&!p)throw new Error(`[Unshared] ${j} requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the interstitial).`);if(C&&"/__unshared"!==n)throw new Error(`[Unshared] ${j} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const A="gate"===y?generateGatePage(n,w):"",E=new VerdictCache(o),T=new Map,k=new RateLimitBackoff,F=new DispatchDedupe,b=generateFingerprintScript(n,w,{autoInterstitial:S,interstitialFlowType:v}),U={emitted:!1},P=`${n}/fp.js`,q=`${n}/submit-fp`,D=`${n}/verify-trigger`,M=`${n}/verify`,O=`${n}/status`,H=i?Array.isArray(i)?i:[i]:null;return async function(t,i,o){const f=e=>{o?.waitUntil&&o.waitUntil(e)};let g,R,I;try{const e=new URL(t.url);g=e.pathname,R=e.search}catch{return i(t)}if(g.startsWith(n+"/")){const n=function(e){if(!H)return{};const t=e.headers.get("origin")??"",r=H.includes("*");if(!r&&!H.includes(t))return{};const s={"Access-Control-Allow-Origin":r?"*":t,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id, X-Permanent-Device-Id","Access-Control-Expose-Headers":"X-Unshared-Enforcement-Reconciled"};return r||(s["Access-Control-Allow-Credentials"]="true"),s}(t);if("OPTIONS"===t.method)return emptyResponse(204,n);if(("GET"===t.method||"HEAD"===t.method)&&g===P){if(!p)return jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},n);const e={...n,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600","Content-Length":String((new TextEncoder).encode(p).byteLength)};return"HEAD"===t.method?new Response(null,{status:200,headers:new Headers(e)}):bodyResponse(200,p,e)}if("POST"===t.method&&(g===q||g===D||g===M)){const i=await readJsonRequest(t);if(!i.success)return jsonResponse(i.status,{success:!1,error:i.error},n);const o=i.data;return g===q?handleSubmitFp(t,o,{client:e,verdictCache:E,rateLimitBackoff:k,dispatchDedupe:F,resolveUserId:r,resolveEmailAddress:s,resolveSessionId:c,resolveDeviceId:u,disableBotFilter:h,onError:m,registerBackground:f},n):g===D?handleVerifyTriggerWeb(t,o,{client:e,verdictCache:E,resolveUserId:r,resolveEmailAddress:s,resolveDeviceId:u,onError:m},n):handleVerifyWeb(t,o,{client:e,verdictCache:E,resolveUserId:r,resolveEmailAddress:s,resolveDeviceId:u,onError:m},n)}if("GET"===t.method&&g===O){let i;try{i=r(t)}catch{}if(!i)return jsonResponse(200,{status:"anonymous"},n);const o=resolveEmail(t,s);let a=E.get(i);if((!a||E.isStale(i))&&o&&!k.isPaused()&&!E.isRefreshing(i)){E.markRefreshing(i);try{const r=extractDeviceIdFromRequest(t,u),s=parseCookieFromRequest(t,"__unshared_fingerprint_id")||void 0,n=extractPermanentDeviceIdFromRequest(t),d=extractSessionIdFromRequest(t,c),p=r??s??"unknown";await fetchAndCacheVerdict(e,E,i,o,p,s,n,d,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:i,emailAddress:o})),a=E.get(i)}catch(e){m&&m(e,{operation:"checkUser",userId:i,emailAddress:o}),x({operation:"checkUser",reason:"exception",userId:i,emailAddress:o})}finally{E.clearRefreshing(i)}}return a&&a.isFlagged&&!a.isVerified&&(void 0!==y||l)&&o?jsonResponse(200,{status:"flagged",email:maskEmail(o)},n):jsonResponse(200,{status:"ok"},n)}return jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},n)}if(shouldSkipPath(g,a,void 0!==y))return i(t);if(!shouldIncludePath(g,d))return injectIntoHtmlResponse(await i(t),b,void 0,U);try{I=r(t)}catch{}if(isSentinelUserId(I)){const e=parseCookieFromRequest(t,"__unshared_uid"),r=parseCookieFromRequest(t,"__unshared_uid_at"),s=r?Number(r):NaN,n=Number.isFinite(s)&&Date.now()-s<=SENTINEL_STICKINESS_TTL_MS;I=e&&n?e:void 0}if(!I){const e=isSecureWebRequest(t)?"; Secure":"",r=[`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_verification_id=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0${e}`];return injectIntoHtmlResponse(await i(t),b,r,U)}const v=resolveEmail(t,s),S=[],w=isSecureWebRequest(t)?"; Secure":"";if(S.push(`__unshared_uid=${encodeURIComponent(I)}; Path=/; SameSite=Lax; Max-Age=31536000${w}`),S.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax; Max-Age=31536000${w}`),v&&S.push(`__unshared_email=${encodeURIComponent(v)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=31536000${w}`),!v)return injectIntoHtmlResponse(await i(t),b,S,U);const C=extractSessionIdFromRequest(t,c),j=extractDeviceIdFromRequest(t,u),$=parseCookieFromRequest(t,"__unshared_fingerprint_id")||void 0,L=extractPermanentDeviceIdFromRequest(t),N=t.headers.get("user-agent")??"",V=extractClientIpFromRequest(t),W=j??$;if(!h&&isBot(N))return i(t);let B=E.get(I);if(B)E.isStale(I)&&!E.isRefreshing(I)&&(E.markRefreshing(I),f(fetchAndCacheVerdict(e,E,I,v,W??"unknown",$,L,C,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:I,emailAddress:v})).catch(()=>x({operation:"checkUser",reason:"exception",userId:I,emailAddress:v})).finally(()=>E.clearRefreshing(I))));else try{let t=T.get(I);t||(t=fetchAndCacheVerdict(e,E,I,v,W??"unknown",$,L,C,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:I,emailAddress:v})).finally(()=>T.delete(I)),T.set(I,t)),B=await t}catch{return x({operation:"checkUser",reason:"exception",userId:I,emailAddress:v}),injectIntoHtmlResponse(await i(t),b,S,U)}const K=B.isFlagged&&!B.isVerified;if("gate"===y&&K)return isHtmlNavigation(t.method,t.headers.get("accept")??void 0)?bodyResponse(200,A,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}):jsonResponse(403,flaggedResponse(v));if("overlay"===y&&K)return isHtmlNavigation(t.method,t.headers.get("accept")??void 0)?injectIntoHtmlResponse(await i(t),b,S,U):jsonResponse(403,flaggedResponse(v));if(B.isFlagged&&!B.isVerified&&l)try{const e=await l({userId:I,emailAddress:v,verdict:B,request:t});if(e)return injectIntoHtmlResponse(e,b,S,U)}catch(e){m&&m(e,{operation:"checkUser",userId:I,emailAddress:v})}return B.isFlagged||"unknown"===C||!W||k.isPaused()||f(dispatchUserEvent(e,E,k,F,{userId:I,emailAddress:v,sessionId:C,deviceId:W,fingerprintId:$,permanentDeviceId:L,userAgent:N,ipAddress:V,eventType:g+R},m)),injectIntoHtmlResponse(await i(t),b,S,U)}}const defaultInjectWarnState={emitted:!1};function passthroughResponse(e,t,r){if(!t||0===t.length)return e;const s=mergeResponseHeaders(e.headers,void 0,t);return new Response(r,{status:e.status,statusText:e.statusText,headers:s})}async function injectIntoHtmlResponse(e,t,r,s=defaultInjectWarnState){const n=e.headers.get("content-type");if(204===e.status||304===e.status)return passthroughResponse(e,r,null);if(null===e.body)return n||s.emitted||(s.emitted=!0,console.warn("[Unshared] The response returned by your handler has no body (e.g. NextResponse.next()), so fingerprint injection did not run. In Next.js middleware, fingerprints must be collected by the browser/React SDK, while this middleware handles verdict + enforcement. See the Next.js section of the integration guide.")),passthroughResponse(e,r,null);if(!isHtmlContentType(n??void 0))return passthroughResponse(e,r,e.body);const i=await e.text(),o=i.lastIndexOf("</body>"),a=-1===o?i+t:i.slice(0,o)+t+i.slice(o),d=mergeResponseHeaders(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(a).length)},r);return d.delete("ETag"),d.delete("Last-Modified"),d.delete("Content-Encoding"),new Response(a,{status:e.status,statusText:e.statusText,headers:d})}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=parseCookieFromRequest(e,"__unshared_email");if(r)return r}function resolveEmailWithBody(e,t,r){const s=resolveEmail(e,r);if(s)return s;const n=t.email;return"string"==typeof n&&n?n:void 0}function extractSessionIdFromRequest(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return parseCookieFromRequest(e,"__unshared_sid")??"unknown"}function isPermanentDeviceId(e){return"string"==typeof e&&/^upid_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(e)}function extractPermanentDeviceIdFromRequest(e){const t=parseCookieFromRequest(e,"__unshared_device_id");if(isPermanentDeviceId(t))return t;const r=e.headers.get("x-permanent-device-id");return isPermanentDeviceId(r)?r:void 0}function dispatchUserEvent(e,t,r,s,n,i){return s.mark(n.userId,n.eventType),runProcessUserEvent({client:e,cache:t,backoff:r,onError:i},{eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,permanentDeviceId:n.permanentDeviceId,sessionHash:n.sessionId,userAgent:n.userAgent},DISPATCH_TIMEOUT_MS)}async function fetchAndCacheVerdict(e,t,r,s,n,i,o,a,d=CHECK_USER_TIMEOUT_MS,c){const u=()=>{const e=t.get(r);return t.set(r,{isFlagged:e?.isFlagged??!1,isVerified:e?.isVerified??!1,emailAddress:s,sessionId:a},5e3),t.get(r)},p={timeoutMs:d,maxRetries:0};let l;n&&"unknown"!==n&&(p.deviceId=n),i&&(p.fullHash=i),o&&(p.permanentDeviceId=o);const m=await Promise.race([e.checkUser(s,p),new Promise(e=>{l=setTimeout(()=>e(null),d)})]);return clearTimeout(l),m?m.failedOpen?(c?.(m.failedOpen.reason??"http_error",m.failedOpen.status),u()):(t.set(r,{isFlagged:m.data?.is_user_flagged??!1,isVerified:!1,emailAddress:s,sessionId:a}),t.get(r)):(c?.("timeout"),u())}async function handleSubmitFp(e,t,r,s){try{const n=t?.fingerprint&&"object"==typeof t.fingerprint?t.fingerprint:t??{},i=t?.context&&"object"==typeof t.context?t.context:{},o=i.identity&&"object"==typeof i.identity?i.identity:{},a=i.identifiers&&"object"==typeof i.identifiers?i.identifiers:{},d=i.event&&"object"==typeof i.event?i.event:{},c={full_hash:n.hash??n.full_hash??"",fingerprint_id:n.stableHash??n.stable_hash??n.fingerprint_id??""};let u,p,l;try{const t=r.resolveUserId(e);t&&!isSentinelUserId(t)&&(u=t)}catch{}if(!u){const e="string"==typeof o.user_id?o.user_id:"string"==typeof t.user_id?t.user_id:void 0;e&&!isSentinelUserId(e)&&(u=e)}if(!u){const t=parseCookieFromRequest(e,"__unshared_uid");t&&!isSentinelUserId(t)&&(u=t)}try{p=r.resolveEmailAddress?r.resolveEmailAddress(e):void 0}catch{}p=p??parseCookieFromRequest(e,"__unshared_email")??o.email_address??t.email??void 0;try{l=r.resolveSessionId?r.resolveSessionId(e):void 0}catch{}l=l??a.session_hash??t.session_id??parseCookieFromRequest(e,"__unshared_sid");const m=extractClientIpFromRequest(e),f=e.headers.get("user-agent")??"";if(!r.disableBotFilter&&isBot(f))return jsonResponse(200,{success:!0,data:{skipped:!0}},s);const h=(c.fingerprint_id&&c.fingerprint_id.length>0?c.fingerprint_id:void 0)??extractDeviceIdFromRequestOrUnknown(e,r.resolveDeviceId),_=c.fingerprint_id||void 0,g=c.full_hash||void 0,R=extractPermanentDeviceIdFromRequest(e),I=e.headers.get("x-permanent-device-id")??a.unshared_device_id??t.permanent_device_id,v=R??(isPermanentDeviceId(I)?I:`upid_${crypto.randomUUID()}`),y=isSecureWebRequest(e)?"; Secure":"",S=[];S.push(`__unshared_device_id=${encodeURIComponent(v)}; Path=/; SameSite=Lax; Max-Age=34560000${y}`);const x=parseCookieFromRequest(e,"__unshared_fingerprint_id")||g;let w;if(x&&S.push(`__unshared_fingerprint_id=${encodeURIComponent(x)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=31536000${y}`),_&&S.push(`__unshared_fp_id=${encodeURIComponent(_)}; Path=/; SameSite=Lax; Max-Age=31536000${y}`),p&&S.push(`__unshared_email=${encodeURIComponent(p)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=31536000${y}`),"string"==typeof d.type&&d.type)w=d.type;else if("string"==typeof t.event_type&&t.event_type)w=t.event_type;else{const t=e.headers.get("referer")??e.headers.get("referrer");let r="unknown";if(t)try{const e=new URL(t);r=(e.pathname||"/")+(e.search||"")}catch{}w=r}const C=e.headers.get("x-idempotency-key")||void 0,j=Date.now();let A;C?A=`${C}|${j}`:_&&u&&(A=`${await sha256HexWeb(`${_}|${u}|${w}`)}|${j}`);const E={...a,unshared_device_id:v,...h&&"unknown"!==h?{device_id:h}:{},..._?{stable_hash:_}:{},...g?{full_hash:g}:{},...l?{session_hash:l}:{}},T=i.sdk&&"object"==typeof i.sdk?i.sdk.name:void 0,k="string"==typeof T&&T.startsWith("unshared-")&&Object.prototype.hasOwnProperty.call(t,"fingerprint")&&t.context&&"object"==typeof t.context&&!Array.isArray(t.context)&&i.identity&&"object"==typeof i.identity&&!Array.isArray(i.identity)&&i.identifiers&&"object"==typeof i.identifiers&&!Array.isArray(i.identifiers)&&i.event&&"object"==typeof i.event&&!Array.isArray(i.event),F=!Object.prototype.hasOwnProperty.call(t,"context")&&"string"==typeof t.hash&&"string"==typeof t.stable_hash&&"string"==typeof t.collected_at&&t.components&&"object"==typeof t.components&&!Array.isArray(t.components),b=k||F?{...t,fingerprint:n,context:{...i,identity:{...o,...u?{user_id:u}:{},...p?{email_address:p}:{}},identifiers:E,event:{...d,type:w,ip_address:m,user_agent:f}}}:t,U=await r.client.submitFingerprint(b,{idempotencyKey:A});if(U.success||r.onError?.(U.error,{operation:"submitFingerprintEvent",userId:u,emailAddress:p}),u&&p&&!r.rateLimitBackoff.isPaused()&&!r.dispatchDedupe.wasRecentlyDispatched(u,w)){const e=runCheckUser({client:r.client,cache:r.verdictCache,backoff:r.rateLimitBackoff,onError:r.onError},{user_id:u,email_address:p,identifiers:E},u,p,SUBMIT_FP_TIMEOUT_MS);r.registerBackground?.(e)}const P={...s,"Content-Type":"application/json"},q=new Response(JSON.stringify(U.success?{success:!0,data:U.data}:{success:!1,error:U.error}),{status:200,headers:P});for(const e of S)q.headers.append("Set-Cookie",e);return q}catch{return jsonResponse(200,{success:!0,data:{accepted:!1}},s)}}async function handleVerifyTriggerWeb(e,t,r,s){try{const n=resolveEmailWithBody(e,t??{},r.resolveEmailAddress);if(!n)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},s);const i=extractDeviceIdFromRequestOrUnknown(e,r.resolveDeviceId),o=parseCookieFromRequest(e,"__unshared_fp_id")||void 0,a=parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,d=extractPermanentDeviceIdFromRequest(e),c=await r.client.triggerEmailVerification(n,i,{fingerprintId:o,fullHash:a,permanentDeviceId:d});if(c.success){const t=jsonResponse(200,{success:!0,data:c.data},s);if(c.data?.verification_id){const r=isSecureWebRequest(e)?"; Secure":"";t.headers.append("Set-Cookie",`__unshared_verification_id=${encodeURIComponent(c.data.verification_id)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=600${r}`)}return t}return jsonResponse(200,{success:!1,error:c.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},s)}catch(e){return r.onError&&r.onError(e,{operation:"verifyTrigger"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},s)}}async function handleVerifyWeb(e,t,r,s){try{const n=t?.code,i=parseCookieFromRequest(e,"__unshared_verification_id");if(!i||!n)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Verification ID and code are required"}},s);const o=await r.client.verifyEmail({verification_id:i,code:n});if(o.success&&o.data?.verified){let t,n=!1;try{t=r.resolveUserId(e)}catch{}const i=resolveEmail(e,r.resolveEmailAddress),a=extractDeviceIdFromRequest(e,r.resolveDeviceId),d=parseCookieFromRequest(e,"__unshared_fp_id")||void 0,c=parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,u=extractPermanentDeviceIdFromRequest(e);if(t&&i){const e={...a?{device_id:a}:{},...d?{stable_hash:d}:{},...c?{full_hash:c}:{},...u?{unshared_device_id:u}:{}},s=await r.client.checkUser({user_id:t,email_address:i,identifiers:e},{maxRetries:0});s.success&&s.data&&!s.failedOpen&&!s.data.is_user_flagged&&(r.verdictCache.update(t,{isVerified:!0}),n=!0)}const p=jsonResponse(200,{success:!0,data:o.data},s);n&&p.headers.set("X-Unshared-Enforcement-Reconciled","clean");const l=isSecureWebRequest(e)?"; Secure":"";return p.headers.append("Set-Cookie",`__unshared_verification_id=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0${l}`),p}return o.success?jsonResponse(200,{success:!0,data:o.data},s):jsonResponse(200,{success:!1,error:o.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}},s)}catch(e){return r.onError&&r.onError(e,{operation:"verify"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},s)}}export const __test_fetchAndCacheVerdict=fetchAndCacheVerdict;export const __test_injectIntoHtmlResponse=injectIntoHtmlResponse;export function __test_resetInjectWarning(){defaultInjectWarnState.emitted=!1}
1
+ import{sha256HexWeb}from"../webcrypto.mjs";import{VerdictCache}from"../middleware/verdict-cache.mjs";import{RateLimitBackoff}from"../middleware/rate-limit-backoff.mjs";import{DispatchDedupe}from"../middleware/dispatch-dedupe.mjs";import{generateFingerprintScript}from"../middleware/injection/fingerprint-script.mjs";import{generateGatePage}from"../middleware/injection/gate-page.mjs";import{flaggedResponse}from"../middleware/utils/flagged-response.mjs";import{maskEmail}from"../middleware/utils/mask-email.mjs";import{isHtmlContentType,isHtmlNavigation}from"../middleware/utils/content-type.mjs";import{shouldSkipPath}from"../middleware/utils/skip-paths.mjs";import{shouldIncludePath}from"../middleware/utils/include-path.mjs";import{isBot}from"../middleware/utils/is-bot.mjs";import{isSentinelUserId,SENTINEL_STICKINESS_TTL_MS}from"../middleware/utils/sentinel-user-id.mjs";import{runCheckUser,runProcessUserEvent,DISPATCH_TIMEOUT_MS,SUBMIT_FP_TIMEOUT_MS}from"../middleware/dispatch-processing.mjs";import{parseCookieFromRequest,extractClientIpFromRequest,extractDeviceIdFromRequest,isSecureWebRequest,jsonResponse,emptyResponse,bodyResponse,mergeResponseHeaders,readJsonRequest}from"./web-helpers.mjs";const CHECK_USER_TIMEOUT_MS=1500,FAIL_OPEN_TTL_MS=5e3;export function createWebProtectionMiddleware(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");const{userId:s,emailAddress:r,routePrefix:n="/__unshared",corsOrigins:o,cacheTTL:i=6e4,skipPaths:a,includePathPrefix:d,sessionId:c,deviceId:u,fingerprintSdkBundle:p="",onFlagged:l,onError:m,onFailOpen:h,disableBotFilter:_=!1,checkUserTimeoutMs:f=CHECK_USER_TIMEOUT_MS,blockFlagged:R=!1,flaggedMode:g,autoInterstitial:I=!1,interstitialFlowType:v="email_verification"}=t,y=g??(R?"gate":void 0),S=I||"overlay"===y,C=e=>{if(h)try{h(e)}catch{}},x=Date.now().toString(36),w=void 0!==y||S,k="gate"===y?"flaggedMode 'gate' (blockFlagged)":"overlay"===y?"flaggedMode 'overlay'":"autoInterstitial";if(w&&!p)throw new Error(`[Unshared] ${k} requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the interstitial).`);if(w&&"/__unshared"!==n)throw new Error(`[Unshared] ${k} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const F="gate"===y?generateGatePage(n,x):"",A=new VerdictCache(i),j=new Map,E=new RateLimitBackoff,b=new DispatchDedupe,T=generateFingerprintScript(n,x,{autoInterstitial:S,interstitialFlowType:v,debug:t.debug,disableBotFilter:t.disableBotFilter}),q={emitted:!1},U=`${n}/fp.js`,P=`${n}/submit-fp`,D=`${n}/verify-trigger`,M=`${n}/verify`,H=`${n}/status`,$=o?Array.isArray(o)?o:[o]:null;return async function(t,o,i){const h=e=>{i?.waitUntil&&i.waitUntil(e)};let R,g,I;try{const e=new URL(t.url);R=e.pathname,g=e.search}catch{return o(t)}if(R.startsWith(n+"/")){const n=function(e){if(!$)return{};const t=e.headers.get("origin")??"",s=$.includes("*");if(!s&&!$.includes(t))return{};const r={"Access-Control-Allow-Origin":s?"*":t,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id, X-Permanent-Device-Id","Access-Control-Expose-Headers":"X-Unshared-Enforcement-Reconciled"};return s||(r["Access-Control-Allow-Credentials"]="true"),r}(t);if("OPTIONS"===t.method)return emptyResponse(204,n);if(("GET"===t.method||"HEAD"===t.method)&&R===U){if(!p)return new Response("HEAD"===t.method?null:"/* fingerprint SDK unavailable */",{status:503,headers:{...n,"Content-Type":"application/javascript","Cache-Control":"no-store"}});const e={...n,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600","Content-Length":String((new TextEncoder).encode(p).byteLength)};return"HEAD"===t.method?new Response(null,{status:200,headers:new Headers(e)}):bodyResponse(200,p,e)}if("POST"===t.method&&(R===P||R===D||R===M)){const o=await readJsonRequest(t);if(!o.success)return jsonResponse(o.status,{success:!1,error:o.error},n);const i=o.data;return R===P?handleSubmitFp(t,i,{client:e,verdictCache:A,rateLimitBackoff:E,dispatchDedupe:b,resolveUserId:s,resolveEmailAddress:r,resolveSessionId:c,resolveDeviceId:u,disableBotFilter:_,onError:m,registerBackground:h},n):R===D?handleVerifyTriggerWeb(t,i,{client:e,verdictCache:A,resolveUserId:s,resolveEmailAddress:r,resolveDeviceId:u,resolveSessionId:c,onError:m},n):handleVerifyWeb(t,i,{client:e,verdictCache:A,resolveUserId:s,resolveEmailAddress:r,resolveDeviceId:u,resolveSessionId:c,onError:m},n)}if("GET"===t.method&&R===H){let o;try{o=s(t)}catch{}if(!o)return jsonResponse(200,{status:"anonymous"},n);const i=resolveEmail(t,r);let a=A.get(o);if((!a||A.isStale(o))&&i&&!E.isPaused()&&!A.isRefreshing(o)){A.markRefreshing(o);try{const s=extractDeviceIdFromRequest(t,u),r=parseCookieFromRequest(t,"__unshared_stable_hash")||parseCookieFromRequest(t,"__unshared_fp_id")||void 0,n=parseCookieFromRequest(t,"__unshared_full_hash")||parseCookieFromRequest(t,"__unshared_fingerprint_id")||void 0,d=extractPermanentDeviceIdFromRequest(t),p=extractSessionIdFromRequest(t,c);await fetchAndCacheVerdict(e,A,o,i,s,r,n,d,p,f,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:o,emailAddress:i})),a=A.get(o)}catch(e){m&&m(e,{operation:"checkUser",userId:o,emailAddress:i}),C({operation:"checkUser",reason:"exception",userId:o,emailAddress:i})}finally{A.clearRefreshing(o)}}return a&&a.isFlagged&&!a.isVerified&&(void 0!==y||l)&&i?jsonResponse(200,{status:"flagged",email:maskEmail(i)},n):jsonResponse(200,{status:"ok"},n)}return jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},n)}if(shouldSkipPath(R,a,void 0!==y))return o(t);try{I=s(t)}catch{}if(isSentinelUserId(I)){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;I=e&&n?e:void 0}if(!I){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}`,`__unshared_verification_id=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0${e}`];return injectIntoHtmlResponse(await o(t),T,s,q)}const v=resolveEmail(t,r),S=[],x=isSecureWebRequest(t)?"; Secure":"";if(S.push(`__unshared_uid=${encodeURIComponent(I)}; Path=/; SameSite=Lax; Max-Age=31536000${x}`),S.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax; Max-Age=31536000${x}`),v&&S.push(`__unshared_email=${encodeURIComponent(v)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=31536000${x}`),!shouldIncludePath(R,d))return injectIntoHtmlResponse(await o(t),T,S,q);if(!v)return injectIntoHtmlResponse(await o(t),T,S,q);const w=extractSessionIdFromRequest(t,c),k=extractDeviceIdFromRequest(t,u),L=parseCookieFromRequest(t,"__unshared_stable_hash")||parseCookieFromRequest(t,"__unshared_fp_id")||void 0,O=parseCookieFromRequest(t,"__unshared_full_hash")||parseCookieFromRequest(t,"__unshared_fingerprint_id")||void 0,N=extractPermanentDeviceIdFromRequest(t),V=t.headers.get("user-agent")??"",W=extractClientIpFromRequest(t);if(!_&&isBot(V)){const e=await o(t);return passthroughResponse(e,S,e.body)}let B=A.get(I);if(B)A.isStale(I)&&!A.isRefreshing(I)&&(A.markRefreshing(I),h(fetchAndCacheVerdict(e,A,I,v,k,L,O,N,w,f,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:I,emailAddress:v})).catch(()=>C({operation:"checkUser",reason:"exception",userId:I,emailAddress:v})).finally(()=>A.clearRefreshing(I))));else try{let t=j.get(I);t||(t=fetchAndCacheVerdict(e,A,I,v,k,L,O,N,w,f,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:I,emailAddress:v})).finally(()=>j.delete(I)),j.set(I,t)),B=await t}catch{return C({operation:"checkUser",reason:"exception",userId:I,emailAddress:v}),injectIntoHtmlResponse(await o(t),T,S,q)}const K=B.isFlagged&&!B.isVerified;if("gate"===y&&K){if(isHtmlNavigation(t.method,t.headers.get("accept")??void 0)){const e=bodyResponse(200,F,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"});return passthroughResponse(e,S,e.body)}const e=jsonResponse(403,flaggedResponse(v));return passthroughResponse(e,S,e.body)}if("overlay"===y&&K){if(isHtmlNavigation(t.method,t.headers.get("accept")??void 0))return injectIntoHtmlResponse(await o(t),T,S,q);const e=jsonResponse(403,flaggedResponse(v));return passthroughResponse(e,S,e.body)}if(B.isFlagged&&!B.isVerified&&l)try{const e=await l({userId:I,emailAddress:v,verdict:B,request:t});if(e)return injectIntoHtmlResponse(e,T,S,q)}catch(e){m&&m(e,{operation:"checkUser",userId:I,emailAddress:v})}return B.isFlagged||!w||"unknown"===w||E.isPaused()||h(dispatchUserEvent(e,A,E,b,{userId:I,emailAddress:v,sessionId:w,deviceId:k,fingerprintId:L,fullHash:O,permanentDeviceId:N,userAgent:V,ipAddress:W,eventType:R+g},m)),injectIntoHtmlResponse(await o(t),T,S,q)}}const defaultInjectWarnState={emitted:!1};function passthroughResponse(e,t,s){if(!t||0===t.length)return e;const r=mergeResponseHeaders(e.headers,void 0,t);return new Response(s,{status:e.status,statusText:e.statusText,headers:r})}async function injectIntoHtmlResponse(e,t,s,r=defaultInjectWarnState){const n=e.headers.get("content-type");if(204===e.status||304===e.status)return passthroughResponse(e,s,null);if(null===e.body)return n||r.emitted||(r.emitted=!0,console.warn("[Unshared] The response returned by your handler has no body (e.g. NextResponse.next()), so fingerprint injection did not run. In Next.js middleware, fingerprints must be collected by the browser/React SDK, while this middleware handles verdict + enforcement. See the Next.js section of the integration guide.")),passthroughResponse(e,s,null);if(!isHtmlContentType(n??void 0))return passthroughResponse(e,s,e.body);const o=await e.text(),i=o.lastIndexOf("</body>"),a=-1===i?o+t:o.slice(0,i)+t+o.slice(i),d=mergeResponseHeaders(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(a).length)},s);return d.delete("ETag"),d.delete("Last-Modified"),d.delete("Content-Encoding"),new Response(a,{status:e.status,statusText:e.statusText,headers:d})}function resolveEmail(e,t){if(t)try{const 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 isPermanentDeviceId(e){return"string"==typeof e&&/^upid_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(e)}function extractPermanentDeviceIdFromRequest(e){const t=parseCookieFromRequest(e,"__unshared_device_id");if(isPermanentDeviceId(t))return t;const s=e.headers.get("x-permanent-device-id");return isPermanentDeviceId(s)?s:void 0}function dispatchUserEvent(e,t,s,r,n,o){return r.mark(n.userId,n.eventType),runProcessUserEvent({client:e,cache:t,backoff:s,onError:o},{eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,fullHash:n.fullHash,permanentDeviceId:n.permanentDeviceId,sessionHash:n.sessionId,userAgent:n.userAgent},DISPATCH_TIMEOUT_MS)}async function fetchAndCacheVerdict(e,t,s,r,n,o,i,a,d,c=CHECK_USER_TIMEOUT_MS,u){const p=()=>{const e=t.get(s);return t.set(s,{isFlagged:e?.isFlagged??!1,isVerified:e?.isVerified??!1,emailAddress:r,sessionId:d},5e3),t.get(s)},l={timeoutMs:c,maxRetries:0};let m;n&&"unknown"!==n&&(l.deviceId=n),o&&"unknown"!==o&&(l.fingerprintId=o),i&&"unknown"!==i&&(l.fullHash=i),a&&(l.permanentDeviceId=a),d&&"unknown"!==d&&(l.sessionHash=d);const h=await Promise.race([e.checkUser(r,l),new Promise(e=>{m=setTimeout(()=>e(null),c)})]);return clearTimeout(m),h?h.failedOpen?(u?.(h.failedOpen.reason??"http_error",h.failedOpen.status),p()):(t.set(s,{isFlagged:h.data?.is_user_flagged??!1,isVerified:!1,emailAddress:r,sessionId:d}),t.get(s)):(u?.("timeout"),p())}async function handleSubmitFp(e,t,s,r){try{const n=t?.fingerprint&&"object"==typeof t.fingerprint?t.fingerprint:t??{},o=t?.context&&"object"==typeof t.context?t.context:{},i=o.identity&&"object"==typeof o.identity?o.identity:{},a=o.identifiers&&"object"==typeof o.identifiers?o.identifiers:{},d=o.event&&"object"==typeof o.event?o.event:{},c={full_hash:n.hash??n.full_hash??"",fingerprint_id:n.stableHash??n.stable_hash??n.fingerprint_id??""};let u,p,l;try{const t=s.resolveUserId(e);t&&!isSentinelUserId(t)&&(u=t)}catch{}if(!u){const e="string"==typeof i.user_id?i.user_id:"string"==typeof t.user_id?t.user_id:void 0;e&&!isSentinelUserId(e)&&(u=e)}if(!u){const t=parseCookieFromRequest(e,"__unshared_uid");t&&!isSentinelUserId(t)&&(u=t)}try{p=s.resolveEmailAddress?s.resolveEmailAddress(e):void 0}catch{}p=p??parseCookieFromRequest(e,"__unshared_email")??i.email_address??t.email??void 0;try{l=s.resolveSessionId?s.resolveSessionId(e):void 0}catch{}l=l??a.session_hash??t.session_id??parseCookieFromRequest(e,"__unshared_sid");const m=extractClientIpFromRequest(e),h=e.headers.get("user-agent")??"";if(!s.disableBotFilter&&isBot(h))return jsonResponse(200,{success:!0,data:{skipped:!0}},r);let _;try{_=s.resolveDeviceId?.(e)}catch{}const f=[_,e.headers.get("x-device-id"),a.device_id,c.fingerprint_id,parseCookieFromRequest(e,"__unshared_stable_hash"),parseCookieFromRequest(e,"__unshared_fp_id")].find(e=>"string"==typeof e&&""!==e.trim()&&"unknown"!==e.trim()),R=c.fingerprint_id||void 0,g=c.full_hash||void 0,I=extractPermanentDeviceIdFromRequest(e),v=e.headers.get("x-permanent-device-id")??a.unshared_device_id??t.permanent_device_id,y=I??(isPermanentDeviceId(v)?v:`upid_${crypto.randomUUID()}`),S=isSecureWebRequest(e)?"; Secure":"",C=[];C.push(`__unshared_device_id=${encodeURIComponent(y)}; Path=/; SameSite=Lax; Max-Age=34560000${S}`);const x=parseCookieFromRequest(e,"__unshared_fingerprint_id")||g;let w;if(x&&C.push(`__unshared_fingerprint_id=${encodeURIComponent(x)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=34560000${S}`),g&&C.push(`__unshared_full_hash=${encodeURIComponent(g)}; Path=/; SameSite=Lax; Max-Age=34560000${S}`),R&&(C.push(`__unshared_fp_id=${encodeURIComponent(R)}; Path=/; SameSite=Lax; Max-Age=34560000${S}`),C.push(`__unshared_stable_hash=${encodeURIComponent(R)}; Path=/; SameSite=Lax; Max-Age=34560000${S}`)),p&&C.push(`__unshared_email=${encodeURIComponent(p)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=31536000${S}`),"string"==typeof d.type&&d.type)w=d.type;else if("string"==typeof t.event_type&&t.event_type)w=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{}w=s}const k=e.headers.get("x-idempotency-key")||void 0,F=Date.now();let A;k?A=`${k}|${F}`:R&&u&&(A=`${await sha256HexWeb(`${R}|${u}|${w}`)}|${F}`);const j={...a,unshared_device_id:y,...f&&"unknown"!==f?{device_id:f}:{},...R?{stable_hash:R}:{},...g?{full_hash:g}:{},...l?{session_hash:l}:{}};f||delete j.device_id;const E=o.sdk&&"object"==typeof o.sdk?o.sdk.name:void 0,b="string"==typeof E&&E.startsWith("unshared-")&&Object.prototype.hasOwnProperty.call(t,"fingerprint")&&t.context&&"object"==typeof t.context&&!Array.isArray(t.context)&&o.identity&&"object"==typeof o.identity&&!Array.isArray(o.identity)&&o.identifiers&&"object"==typeof o.identifiers&&!Array.isArray(o.identifiers)&&o.event&&"object"==typeof o.event&&!Array.isArray(o.event),T=!Object.prototype.hasOwnProperty.call(t,"context")&&"string"==typeof t.hash&&"string"==typeof t.stable_hash&&"string"==typeof t.collected_at&&t.components&&"object"==typeof t.components&&!Array.isArray(t.components),q=b||T?{...t,fingerprint:n,context:{...o,identity:{...i,...u?{user_id:u}:{},...p?{email_address:p}:{}},identifiers:j,event:{...d,type:w,ip_address:m,user_agent:h}}}:t,U=await s.client.submitFingerprint(q,{idempotencyKey:A});if(U.success||s.onError?.(U.error,{operation:"submitFingerprintEvent",userId:u,emailAddress:p}),u&&p&&!s.rateLimitBackoff.isPaused()&&!s.dispatchDedupe.wasRecentlyDispatched(u,w)){const e=runCheckUser({client:s.client,cache:s.verdictCache,backoff:s.rateLimitBackoff,onError:s.onError},{user_id:u,email_address:p,identifiers:j},u,p,SUBMIT_FP_TIMEOUT_MS);s.registerBackground?.(e)}const P={...r,"Content-Type":"application/json"},D=new Response(JSON.stringify(U.success?{success:!0,data:U.data}:{success:!1,error:U.error}),{status:200,headers:P});for(const e of C)D.headers.append("Set-Cookie",e);return D}catch{return jsonResponse(200,{success:!0,data:{accepted:!1}},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 o=extractDeviceIdFromRequest(e,s.resolveDeviceId),i=parseCookieFromRequest(e,"__unshared_stable_hash")||parseCookieFromRequest(e,"__unshared_fp_id")||void 0,a=parseCookieFromRequest(e,"__unshared_full_hash")||parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,d=extractPermanentDeviceIdFromRequest(e),c=extractSessionIdFromRequest(e,s.resolveSessionId),u=await s.client.triggerEmailVerification(n,o&&"unknown"!==o?o:"",{fingerprintId:"unknown"!==i?i:void 0,fullHash:"unknown"!==a?a:void 0,permanentDeviceId:d,sessionHash:"unknown"!==c?c:void 0});if(u.success){const t=jsonResponse(200,{success:!0,data:u.data},r);if(u.data?.verification_id){const s=isSecureWebRequest(e)?"; Secure":"";t.headers.append("Set-Cookie",`__unshared_verification_id=${encodeURIComponent(u.data.verification_id)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=600${s}`)}return t}return jsonResponse(200,{success:!1,error:u.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=t?.code,o=parseCookieFromRequest(e,"__unshared_verification_id");if(!o||!n)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Verification ID and code are required"}},r);const i=await s.client.verifyEmail({verification_id:o,code:n});if(i.success&&i.data?.verified){let t,n=!1;try{t=s.resolveUserId(e)}catch{}const o=resolveEmail(e,s.resolveEmailAddress),a=extractDeviceIdFromRequest(e,s.resolveDeviceId),d=parseCookieFromRequest(e,"__unshared_stable_hash")||parseCookieFromRequest(e,"__unshared_fp_id")||void 0,c=parseCookieFromRequest(e,"__unshared_full_hash")||parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,u=extractPermanentDeviceIdFromRequest(e),p=extractSessionIdFromRequest(e,s.resolveSessionId);if(t&&o){const e={...a&&"unknown"!==a?{device_id:a}:{},...d&&"unknown"!==d?{stable_hash:d}:{},...c&&"unknown"!==c?{full_hash:c}:{},...u?{unshared_device_id:u}:{},...p&&"unknown"!==p?{session_hash:p}:{}},r=await s.client.checkUser({user_id:t,email_address:o,identifiers:e},{maxRetries:0});r.success&&r.data&&!r.failedOpen&&!r.data.is_user_flagged&&(s.verdictCache.update(t,{isVerified:!0}),n=!0)}const l=jsonResponse(200,{success:!0,data:i.data},r);n&&l.headers.set("X-Unshared-Enforcement-Reconciled","clean");const m=isSecureWebRequest(e)?"; Secure":"";return l.headers.append("Set-Cookie",`__unshared_verification_id=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0${m}`),l}return i.success?jsonResponse(200,{success:!0,data:i.data},r):jsonResponse(200,{success:!1,error:i.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)}}export const __test_fetchAndCacheVerdict=fetchAndCacheVerdict;export const __test_injectIntoHtmlResponse=injectIntoHtmlResponse;export function __test_resetInjectWarning(){defaultInjectWarnState.emitted=!1}
@@ -69,13 +69,15 @@ export interface WebProtectionConfig {
69
69
  includePathPrefix?: string[];
70
70
  /** Skip the bot/crawler UA filter. Set to true in test environments so automated browsers (Playwright, Puppeteer, etc.) can observe verdicts. @default false */
71
71
  disableBotFilter?: boolean;
72
+ /** Enable diagnostic logging in the injected fingerprint script. @default false */
73
+ debug?: boolean;
72
74
  /** Hard timeout (ms) for checkUser on cache miss. Fails open on timeout. @default 1500 */
73
75
  checkUserTimeoutMs?: number;
74
76
  /** Resolves a custom session ID. Falls back to __unshared_sid cookie. */
75
77
  sessionId?: (req: Request) => string | undefined;
76
78
  /**
77
79
  * Resolves a device ID from the request.
78
- * Falls back to X-Device-Id header → __unshared_fp_id cookie.
80
+ * Falls back to X-Device-Id header, then the canonical or legacy stable-hash cookie.
79
81
  */
80
82
  deviceId?: (req: Request) => string | undefined;
81
83
  /**
@@ -91,7 +93,7 @@ export interface WebProtectionConfig {
91
93
  * createWebProtectionMiddleware(client, { ..., fingerprintSdkBundle });
92
94
  * ```
93
95
  *
94
- * If omitted, the fp.js route returns 404. The inline script degrades
96
+ * If omitted, the fp.js route returns a noncacheable 503. The inline script degrades
95
97
  * gracefully — the cached-fingerprint path still works.
96
98
  */
97
99
  fingerprintSdkBundle?: string;
@@ -18,7 +18,8 @@ export declare function parseCookieFromRequest(request: Request, name: string):
18
18
  */
19
19
  export declare function extractClientIpFromRequest(request: Request): string;
20
20
  /**
21
- * Resolves device ID from: custom resolver → X-Device-Id header → __unshared_fp_id cookie.
21
+ * Resolves device ID from the custom resolver, X-Device-Id header, then the
22
+ * canonical __unshared_stable_hash cookie or legacy __unshared_fp_id cookie.
22
23
  * Mirrors `extractDeviceIdOrUndefined` in src/middleware/utils/device-id.ts.
23
24
  */
24
25
  export declare function extractDeviceIdFromRequest(request: Request, resolveDeviceId?: (req: Request) => string | undefined): string | undefined;
@@ -1 +1 @@
1
- export function parseCookieFromRequest(e,t){const o=e.headers.get("cookie");if(!o)return;const r=o.match(new RegExp(`(?:^|; )${t}=([^;]*)`));if(r)try{return decodeURIComponent(r[1])}catch{return r[1]}}export function extractClientIpFromRequest(e){const t=e.headers.get("cf-connecting-ip");if(t)return t;const o=e.headers.get("x-real-ip");if(o)return o;const r=e.headers.get("x-forwarded-for");if(r){const e=r.split(",")[0]?.trim();if(e)return e}return""}export function extractDeviceIdFromRequest(e,t){if(t)try{const o=t(e);if(o)return o}catch{}const o=e.headers.get("x-device-id");if(o)return o;return parseCookieFromRequest(e,"__unshared_fp_id")||void 0}export function extractDeviceIdFromRequestOrUnknown(e,t){return extractDeviceIdFromRequest(e,t)??"unknown"}export function isSecureWebRequest(e){if("https"===e.headers.get("x-forwarded-proto"))return!0;try{return"https:"===new URL(e.url).protocol}catch{return!1}}export async function readJsonRequest(e,t=1<<20){const o=Number(e.headers.get("content-length"));if(Number.isFinite(o)&&o>t)return{success:!1,status:413,error:{code:"REQUEST_TOO_LARGE",message:"Request body exceeds 1 MiB"}};try{if(!e.body)throw new Error("Missing body");const o=e.body.getReader(),r=[];let n=0;for(;;){const{done:e,value:s}=await o.read();if(e)break;if(n+=s.byteLength,n>t)return o.cancel().catch(()=>{}),{success:!1,status:413,error:{code:"REQUEST_TOO_LARGE",message:"Request body exceeds 1 MiB"}};r.push(s)}const s=new Uint8Array(n);let c=0;for(const e of r)s.set(e,c),c+=e.byteLength;return{success:!0,data:JSON.parse((new TextDecoder).decode(s))}}catch{return{success:!1,status:400,error:{code:"INVALID_JSON",message:"Request body is not valid JSON."}}}}export function jsonResponse(e,t,o){const r=new Headers(o);return r.set("Content-Type","application/json"),new Response(JSON.stringify(t),{status:e,headers:r})}export function emptyResponse(e,t){return new Response(null,{status:e,headers:new Headers(t)})}export function bodyResponse(e,t,o){return new Response(t,{status:e,headers:new Headers(o)})}export function appendCookieToHeaders(e,t){e.append("Set-Cookie",t)}export function mergeResponseHeaders(e,t,o){const r=new Headers;e.forEach((e,t)=>{"set-cookie"!==t.toLowerCase()&&r.set(t,e)});const n="function"==typeof e.getSetCookie?e.getSetCookie():[];for(const e of n)r.append("Set-Cookie",e);if(o)for(const e of o)r.append("Set-Cookie",e);if(t)for(const[e,o]of Object.entries(t))r.set(e,o);return r}
1
+ export function parseCookieFromRequest(e,t){const o=e.headers.get("cookie");if(!o)return;const r=o.match(new RegExp(`(?:^|; )${t}=([^;]*)`));if(r)try{return decodeURIComponent(r[1])}catch{return r[1]}}export function extractClientIpFromRequest(e){const t=e.headers.get("cf-connecting-ip");if(t)return t;const o=e.headers.get("x-real-ip");if(o)return o;const r=e.headers.get("x-forwarded-for");if(r){const e=r.split(",")[0]?.trim();if(e)return e}return""}export function extractDeviceIdFromRequest(e,t){if(t)try{const o=t(e);if(o)return o}catch{}const o=e.headers.get("x-device-id");if(o)return o;return parseCookieFromRequest(e,"__unshared_stable_hash")||parseCookieFromRequest(e,"__unshared_fp_id")||void 0}export function extractDeviceIdFromRequestOrUnknown(e,t){return extractDeviceIdFromRequest(e,t)??"unknown"}export function isSecureWebRequest(e){if("https"===e.headers.get("x-forwarded-proto"))return!0;try{return"https:"===new URL(e.url).protocol}catch{return!1}}export async function readJsonRequest(e,t=1<<20){const o=Number(e.headers.get("content-length"));if(Number.isFinite(o)&&o>t)return{success:!1,status:413,error:{code:"REQUEST_TOO_LARGE",message:"Request body exceeds 1 MiB"}};try{if(!e.body)throw new Error("Missing body");const o=e.body.getReader(),r=[];let n=0;for(;;){const{done:e,value:s}=await o.read();if(e)break;if(n+=s.byteLength,n>t)return o.cancel().catch(()=>{}),{success:!1,status:413,error:{code:"REQUEST_TOO_LARGE",message:"Request body exceeds 1 MiB"}};r.push(s)}const s=new Uint8Array(n);let c=0;for(const e of r)s.set(e,c),c+=e.byteLength;return{success:!0,data:JSON.parse((new TextDecoder).decode(s))}}catch{return{success:!1,status:400,error:{code:"INVALID_JSON",message:"Request body is not valid JSON."}}}}export function jsonResponse(e,t,o){const r=new Headers(o);return r.set("Content-Type","application/json"),new Response(JSON.stringify(t),{status:e,headers:r})}export function emptyResponse(e,t){return new Response(null,{status:e,headers:new Headers(t)})}export function bodyResponse(e,t,o){return new Response(t,{status:e,headers:new Headers(o)})}export function appendCookieToHeaders(e,t){e.append("Set-Cookie",t)}export function mergeResponseHeaders(e,t,o){const r=new Headers;e.forEach((e,t)=>{"set-cookie"!==t.toLowerCase()&&r.set(t,e)});const n="function"==typeof e.getSetCookie?e.getSetCookie():[];for(const e of n)r.append("Set-Cookie",e);if(o)for(const e of o)r.append("Set-Cookie",e);if(t)for(const[e,o]of Object.entries(t))r.set(e,o);return r}
@@ -19,7 +19,10 @@ export interface DispatchIo {
19
19
  export declare function runCheckUser(io: DispatchIo, request: CheckUserRequest, userId: string, emailAddress: string, timeoutMs: number): Promise<void>;
20
20
  /**
21
21
  * Run processUserEvent with the standard bounded budget and backoff wiring.
22
- * Never rejects. All four dispatch sites (classic/web x dispatch/submit-fp)
22
+ * Never rejects. Both ordinary dispatch sites (classic/web)
23
23
  * MUST go through this helper so the cap and backoff can't drift per-site.
24
24
  */
25
- export declare function runProcessUserEvent(io: DispatchIo, params: ProcessUserEventParams, timeoutMs: number): Promise<void>;
25
+ export declare function runProcessUserEvent(io: DispatchIo, params: Omit<ProcessUserEventParams, 'deviceId'> & {
26
+ deviceId?: string;
27
+ fullHash?: string;
28
+ }, timeoutMs: number): Promise<void>;
@@ -1 +1 @@
1
- "use strict";async function runCheckUser(e,r,s,t,o){try{const t=await e.client.checkUser(r,{timeoutMs:o,maxRetries:0});if(t.success&&t.data&&!t.failedOpen)return e.cache.update(s,{isFlagged:t.data.is_user_flagged}),void e.backoff.noteSuccess();if(t.failedOpen)return void e.backoff.noteFailure();t.error?.retryAfter?e.backoff.pause(1e3*t.error.retryAfter):e.backoff.noteFailure()}catch(r){e.backoff.noteFailure();try{e.onError&&e.onError(r,{operation:"processUserEvent",userId:s,emailAddress:t})}catch{}}}async function runProcessUserEvent(e,r,s){try{const t=await e.client.processUserEvent(r,{timeoutMs:s,maxRetries:0});if(t.success&&t.data?.analysis&&!t.failedOpen&&e.cache.update(r.userId,{isFlagged:t.data.analysis.is_user_flagged}),t.failedOpen)return void e.backoff.noteFailure();t.success?e.backoff.noteSuccess():t.error?.retryAfter?e.backoff.pause(1e3*t.error.retryAfter):e.backoff.noteFailure()}catch(s){e.backoff.noteFailure();try{e.onError&&e.onError(s,{operation:"processUserEvent",userId:r.userId,emailAddress:r.emailAddress})}catch{}}}Object.defineProperty(exports,"t",{value:!0}),exports.SUBMIT_FP_TIMEOUT_MS=exports.DISPATCH_TIMEOUT_MS=void 0,exports.runCheckUser=runCheckUser,exports.runProcessUserEvent=runProcessUserEvent,exports.DISPATCH_TIMEOUT_MS=2e3,exports.SUBMIT_FP_TIMEOUT_MS=3e3;
1
+ "use strict";async function runCheckUser(e,s,t,r,i){try{const r=await e.client.checkUser(s,{timeoutMs:i,maxRetries:0});if(r.success&&r.data&&!r.failedOpen)return e.cache.update(t,{isFlagged:r.data.is_user_flagged}),void e.backoff.noteSuccess();if(r.failedOpen)return void e.backoff.noteFailure();r.error?.retryAfter?e.backoff.pause(1e3*r.error.retryAfter):e.backoff.noteFailure()}catch(s){e.backoff.noteFailure();try{e.onError&&e.onError(s,{operation:"processUserEvent",userId:t,emailAddress:r})}catch{}}}async function runProcessUserEvent(e,s,t){try{const r=Object.fromEntries(Object.entries({device_id:s.deviceId,stable_hash:s.fingerprintId,full_hash:s.fullHash,unshared_device_id:s.permanentDeviceId,session_hash:s.sessionHash}).filter(e=>!!e[1]&&"unknown"!==e[1])),i=await e.client.processUserEvent({event_type:s.eventType,identity:{user_id:s.userId,email_address:s.emailAddress},identifiers:r,context:{ip_address:s.ipAddress,user_agent:s.userAgent}},{timeoutMs:t,maxRetries:0});if(i.failedOpen)return void e.backoff.noteFailure();i.success?await runCheckUser(e,{user_id:s.userId,email_address:s.emailAddress,identifiers:r},s.userId,s.emailAddress,t):i.error?.retryAfter?e.backoff.pause(1e3*i.error.retryAfter):e.backoff.noteFailure()}catch(t){e.backoff.noteFailure();try{e.onError&&e.onError(t,{operation:"processUserEvent",userId:s.userId,emailAddress:s.emailAddress})}catch{}}}Object.defineProperty(exports,"t",{value:!0}),exports.SUBMIT_FP_TIMEOUT_MS=exports.DISPATCH_TIMEOUT_MS=void 0,exports.runCheckUser=runCheckUser,exports.runProcessUserEvent=runProcessUserEvent,exports.DISPATCH_TIMEOUT_MS=2e3,exports.SUBMIT_FP_TIMEOUT_MS=3e3;
@@ -38,6 +38,8 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
38
38
  includePathPrefix?: string[];
39
39
  /** Skip the bot/crawler UA filter. Set to true in test environments so automated browsers (Playwright, Puppeteer, etc.) can observe verdicts. @default false */
40
40
  disableBotFilter?: boolean;
41
+ /** Enable diagnostic logging in the injected fingerprint script. @default false */
42
+ debug?: boolean;
41
43
  /** @internal Serve only the bundled fingerprint library, without S3 fetches. @default false */
42
44
  disableS3Fingerprint?: boolean;
43
45
  /** Hard timeout (ms) for checkUser on cache miss. Fails open on timeout. @default 1500 */
@@ -58,7 +60,7 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
58
60
  sessionId?: (req: TReq) => string | undefined;
59
61
  /**
60
62
  * Resolves a device ID from the request.
61
- * Falls back to X-Device-Id header → __unshared_fp_id cookie.
63
+ * Falls back to X-Device-Id header, then the canonical or legacy stable-hash cookie.
62
64
  */
63
65
  deviceId?: (req: TReq) => string | undefined;
64
66
  /**
@@ -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"),read_json_body_1=require("./utils/read-json-body"),verdict_cache_1=require("./verdict-cache");Object.defineProperty(exports,"VerdictCache",{enumerable:!0,get:function(){return verdict_cache_1.VerdictCache}});const rate_limit_backoff_1=require("./rate-limit-backoff"),dispatch_dedupe_1=require("./dispatch-dedupe"),response_interceptor_1=require("./response-interceptor"),fingerprint_script_1=require("./injection/fingerprint-script"),fp_bundle_source_1=require("./injection/fp-bundle-source"),submit_fp_1=require("./routes/submit-fp"),verify_1=require("./routes/verify"),interstitial_1=require("./routes/interstitial"),gate_page_1=require("./injection/gate-page"),dispatch_processing_1=require("./dispatch-processing"),http_helpers_1=require("./utils/http-helpers"),content_type_1=require("./utils/content-type"),flagged_response_1=require("./utils/flagged-response"),mask_email_1=require("./utils/mask-email"),skip_paths_1=require("./utils/skip-paths"),include_path_1=require("./utils/include-path"),is_bot_1=require("./utils/is-bot"),client_ip_1=require("./utils/client-ip"),cookies_1=require("./utils/cookies"),device_id_1=require("./utils/device-id"),permanent_device_id_1=require("./utils/permanent-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:n="/__unshared",corsOrigins:s,cacheTTL:o=6e4,maxCacheSize:a,streamingIdleMs:d=50,skipPaths:c,includePathPrefix:l,disableBotFilter:u=!1,checkUserTimeoutMs:_=CHECK_USER_TIMEOUT_MS,cacheMissStrategy:p="block",sessionId:h,deviceId:f,onFlagged:g,onError:m,onFailOpen:v,blockFlagged:I=!1,flaggedMode:k,autoInterstitial:y=!1,interstitialFlowType:A="email_verification"}=t,C=k??(I?"gate":void 0),S=y||"overlay"===C,x=e=>{if(v)try{v(e)}catch{}else"async_miss"!==e.reason&&warnFailOpenThrottled(e)},E=new verdict_cache_1.VerdictCache(o,a),b=new rate_limit_backoff_1.RateLimitBackoff,T=new Map,U=new dispatch_dedupe_1.DispatchDedupe,O=Date.now().toString(36),w=(0,fingerprint_script_1.generateFingerprintScript)(n,O,{autoInterstitial:S,interstitialFlowType:A});let q="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");q=(0,fs_1.readFileSync)(e,"utf8")}catch{}const F=(0,fp_bundle_source_1.createFingerprintBundleSource)({disabled:t.disableS3Fingerprint}),M=void 0!==C||S,P="gate"===C?"flaggedMode 'gate' (blockFlagged)":"overlay"===C?"flaggedMode 'overlay'":"autoInterstitial";if(M&&!q)throw new Error(`[Unshared] ${P} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(M&&"/__unshared"!==n)throw new Error(`[Unshared] ${P} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const j="gate"===C?(0,gate_page_1.generateGatePage)(n,O):"",$=(0,submit_fp_1.handleSubmitFingerprint)({client:e,verdictCache:E,rateLimitBackoff:b,dispatchDedupe:U,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:h,resolveDeviceId:f,disableBotFilter:u,onError:m}),D=(0,verify_1.handleVerifyTrigger)({client:e,verdictCache:E,resolveEmailAddress:i,resolveDeviceId:f,onError:m}),V=(0,verify_1.handleVerify)({client:e,verdictCache:E,resolveUserId:r,resolveEmailAddress:i,resolveDeviceId:f,onError:m}),L=(0,interstitial_1.handleGetInterstitialFlow)({client:e}),H=s?Array.isArray(s)?s:[s]:null,N=`${n}/fp.js`,R=`${n}/fingerprint.js`,X=`${n}/submit-fp`,G=`${n}/verify-trigger`,K=`${n}/verify`,B=`${n}/status`,W=`${n}/interstitial-flow`,z=function(t,s,o){const a=(0,http_helpers_1.getRequestPath)(t.url),v=t.url||a;if(a.startsWith(n+"/")){if(function(e,t){if(!H)return;const r=e.headers.origin??"";H.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, X-Permanent-Device-Id"),t.setHeader("Access-Control-Expose-Headers","X-Unshared-Enforcement-Reconciled")):H.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, X-Permanent-Device-Id"),t.setHeader("Access-Control-Expose-Headers","X-Unshared-Enforcement-Reconciled"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,s),"OPTIONS"===t.method)return void(0,http_helpers_1.sendEmpty)(s,204);if(("GET"===t.method||"HEAD"===t.method)&&a===N)return s.setHeader("Content-Type","application/javascript"),s.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBodyForMethod)(s,t.method,200,q);if(("GET"===t.method||"HEAD"===t.method)&&a===R)return void F.get().then(e=>{e?(s.setHeader("Content-Type","application/javascript"),s.setHeader("Cache-Control","public, max-age=3600"),(0,http_helpers_1.sendBodyForMethod)(s,t.method,200,e)):(0,http_helpers_1.sendBodyForMethod)(s,t.method,503,"/* fingerprint agent unavailable */")});if("POST"===t.method&&(a===X||a===G||a===K))return void(0,read_json_body_1.readJsonBody)(t,s).then(e=>{if(e)return a===X?$(t,s):a===G?D(t,s):V(t,s)}).catch(o);if("GET"===t.method&&a===W)return void L(t,s);if("GET"===t.method&&a===B){let n;try{n=r(t)}catch{}if(!n)return void(0,http_helpers_1.sendJson)(s,200,{status:"anonymous"});const o=resolveEmail(t,i);return void(async()=>{let r=E.get(n);if((!r||E.isStale(n))&&o&&!b.isPaused()&&!E.isRefreshing(n)){E.markRefreshing(n);try{const i=(0,device_id_1.extractDeviceIdOrUndefined)(t,f),s=extractFingerprintId(t),a=(0,permanent_device_id_1.extractPermanentDeviceId)(t),d=extractSessionId(t,h),c=i??s??"unknown";await fetchAndCacheVerdict(e,E,n,o,c,s,a,d,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:n,emailAddress:o})),r=E.get(n)}catch(e){m&&m(e,{operation:"checkUser",userId:n,emailAddress:o}),x({operation:"checkUser",reason:"exception",userId:n,emailAddress:o})}finally{E.clearRefreshing(n)}}r&&r.isFlagged&&!r.isVerified&&(void 0!==C||g)&&o?(0,http_helpers_1.sendJson)(s,200,{status:"flagged",email:(0,mask_email_1.maskEmail)(o)}):(0,http_helpers_1.sendJson)(s,200,{status:"ok"})})()}return void(0,http_helpers_1.sendJson)(s,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}const I=void 0!==C;if((0,skip_paths_1.shouldSkipPath)(a,c,I))return void o();if(!(0,include_path_1.shouldIncludePath)(a,l))return interceptForInjection(t,s,w,d),void o();let k;try{k=r(t)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(k)){const e=(0,cookies_1.parseCookie)(t,"__unshared_uid"),r=(0,cookies_1.parseCookie)(t,"__unshared_uid_at"),i=r?Number(r):NaN,n=Number.isFinite(i)&&Date.now()-i<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;k=e&&n?e:void 0}if(!k){const e=(0,secure_1.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}`),appendSetCookie(s,`__unshared_verification_id=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(t,s,w,d),void o()}const y=resolveEmail(t,i);if(setUserIdCookie(t,s,k),y&&setEmailCookie(t,s,y),!y)return interceptForInjection(t,s,w,d),void o();const A=extractSessionId(t,h),S=(0,device_id_1.extractDeviceIdOrUndefined)(t,f),O=extractFingerprintId(t),M=(0,permanent_device_id_1.extractPermanentDeviceId)(t),P=t.headers["user-agent"]??"",z=(0,client_ip_1.extractClientIp)(t),J=S??O;if(!u&&(0,is_bot_1.isBot)(P))return void o();const Q=E.get(k);if(!Q){if("async"===p)return E.isRefreshing(k)||(E.markRefreshing(k),fetchAndCacheVerdict(e,E,k,y,J??"unknown",O,M,A,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:k,emailAddress:y})).catch(()=>x({operation:"checkUser",reason:"exception",userId:k,emailAddress:y})).finally(()=>E.clearRefreshing(k))),x({operation:"checkUser",reason:"async_miss",userId:k,emailAddress:y}),Y(),interceptForInjection(t,s,w,d),void o();let r=T.get(k);return r||(r=fetchAndCacheVerdict(e,E,k,y,J??"unknown",O,M,A,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:k,emailAddress:y})).finally(()=>T.delete(k)),T.set(k,r)),void r.then(e=>{e.isFlagged||Y(),applyVerdict(e,k,y,t,s,o,w,g,C,j,d)}).catch(()=>{x({operation:"checkUser",reason:"exception",userId:k,emailAddress:y}),Y(),interceptForInjection(t,s,w,d),o()})}function Y(){"unknown"!==A&&J&&(b.isPaused()||dispatchUserEvent(e,E,b,U,{userId:k,emailAddress:y,sessionId:A,deviceId:J,fingerprintId:O,permanentDeviceId:M,userAgent:P,ipAddress:z,eventType:v},m))}E.isStale(k)&&!E.isRefreshing(k)&&(E.markRefreshing(k),fetchAndCacheVerdict(e,E,k,y,J??"unknown",O,M,A,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:k,emailAddress:y})).catch(()=>x({operation:"checkUser",reason:"exception",userId:k,emailAddress:y})).finally(()=>E.clearRefreshing(k))),Q.isFlagged||Y(),applyVerdict(Q,k,y,t,s,o,w,g,C,j,d)};return z.destroy=()=>{E.destroy(),F.stop()},z}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=(0,cookies_1.parseCookie)(e,"__unshared_email");if(r)return r;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,t,r,i,n,s,o,a,d,c,l){const u=e.isFlagged&&!e.isVerified;if("gate"===d&&u)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(n.statusCode=200,n.setHeader("Content-Type","text/html; charset=utf-8"),n.setHeader("Cache-Control","no-store"),n.end(c)):(0,http_helpers_1.sendJson)(n,403,(0,flagged_response_1.flaggedResponse)(r));else if("overlay"===d&&u)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(interceptForInjection(i,n,o,l),s()):(0,http_helpers_1.sendJson)(n,403,(0,flagged_response_1.flaggedResponse)(r));else if(interceptForInjection(i,n,o,l),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,i){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],(0,response_interceptor_1.interceptResponse)(t,(e,t)=>{if(!(0,content_type_1.isHtmlContentType)(t))return null;const i=e.toString("utf8"),n=i.lastIndexOf("</body>");return-1===n?i+r:i.slice(0,n)+r+i.slice(n)},{preventCaching:!0,streamingIdleMs:i})}function dispatchUserEvent(e,t,r,i,n,s){i.mark(n.userId,n.eventType),(0,dispatch_processing_1.runProcessUserEvent)({client:e,cache:t,backoff:r,onError:s},{eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,permanentDeviceId:n.permanentDeviceId,sessionHash:n.sessionId,userAgent:n.userAgent},dispatch_processing_1.DISPATCH_TIMEOUT_MS)}async function fetchAndCacheVerdict(e,t,r,i,n,s,o,a,d=CHECK_USER_TIMEOUT_MS,c){const l={timeoutMs:d,maxRetries:0},u=n&&"unknown"!==n?n:void 0,_=u??s,p=s??u;let h;_&&(l.deviceId=_),p&&(l.fullHash=p),o&&(l.permanentDeviceId=o);const f=await Promise.race([e.checkUser(i,l),new Promise(e=>{h=setTimeout(()=>e(null),d)})]);return clearTimeout(h),f?f.failedOpen?(c?.(f.failedOpen.reason??"http_error",f.failedOpen.status),cacheFailOpenVerdict(t,r,i,a)):(t.set(r,{isFlagged:f.data?.is_user_flagged??!1,isVerified:!1,emailAddress:i,sessionId:a}),t.get(r)):(c?.("timeout"),cacheFailOpenVerdict(t,r,i,a))}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(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; Max-Age=31536000${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax; Max-Age=31536000${i}`)}function setEmailCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=31536000${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"),read_json_body_1=require("./utils/read-json-body"),verdict_cache_1=require("./verdict-cache");Object.defineProperty(exports,"VerdictCache",{enumerable:!0,get:function(){return verdict_cache_1.VerdictCache}});const rate_limit_backoff_1=require("./rate-limit-backoff"),dispatch_dedupe_1=require("./dispatch-dedupe"),response_interceptor_1=require("./response-interceptor"),fingerprint_script_1=require("./injection/fingerprint-script"),fp_bundle_source_1=require("./injection/fp-bundle-source"),submit_fp_1=require("./routes/submit-fp"),verify_1=require("./routes/verify"),interstitial_1=require("./routes/interstitial"),gate_page_1=require("./injection/gate-page"),dispatch_processing_1=require("./dispatch-processing"),http_helpers_1=require("./utils/http-helpers"),content_type_1=require("./utils/content-type"),flagged_response_1=require("./utils/flagged-response"),mask_email_1=require("./utils/mask-email"),skip_paths_1=require("./utils/skip-paths"),include_path_1=require("./utils/include-path"),is_bot_1=require("./utils/is-bot"),client_ip_1=require("./utils/client-ip"),cookies_1=require("./utils/cookies"),device_id_1=require("./utils/device-id"),permanent_device_id_1=require("./utils/permanent-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:n="/__unshared",corsOrigins:s,cacheTTL:o=6e4,maxCacheSize:a,streamingIdleMs:d=50,skipPaths:c,includePathPrefix:l,disableBotFilter:u=!1,checkUserTimeoutMs:_=CHECK_USER_TIMEOUT_MS,cacheMissStrategy:p="block",sessionId:h,deviceId:f,onFlagged:g,onError:v,onFailOpen:m,blockFlagged:I=!1,flaggedMode:k,autoInterstitial:C=!1,interstitialFlowType:y="email_verification"}=t,A=k??(I?"gate":void 0),S=C||"overlay"===A,x=e=>{if(m)try{m(e)}catch{}else"async_miss"!==e.reason&&warnFailOpenThrottled(e)},b=new verdict_cache_1.VerdictCache(o,a),E=new rate_limit_backoff_1.RateLimitBackoff,T=new Map,F=new dispatch_dedupe_1.DispatchDedupe,U=Date.now().toString(36),O=(0,fingerprint_script_1.generateFingerprintScript)(n,U,{autoInterstitial:S,interstitialFlowType:y,debug:t.debug,disableBotFilter:t.disableBotFilter});let w="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");w=(0,fs_1.readFileSync)(e,"utf8")}catch{}const q=(0,fp_bundle_source_1.createFingerprintBundleSource)({disabled:t.disableS3Fingerprint}),M=void 0!==A||S,P="gate"===A?"flaggedMode 'gate' (blockFlagged)":"overlay"===A?"flaggedMode 'overlay'":"autoInterstitial";if(M&&!w)throw new Error(`[Unshared] ${P} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(M&&"/__unshared"!==n)throw new Error(`[Unshared] ${P} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const j="gate"===A?(0,gate_page_1.generateGatePage)(n,U):"",$=(0,submit_fp_1.handleSubmitFingerprint)({client:e,verdictCache:b,rateLimitBackoff:E,dispatchDedupe:F,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:h,resolveDeviceId:f,disableBotFilter:u,onError:v}),D=(0,verify_1.handleVerifyTrigger)({client:e,verdictCache:b,resolveEmailAddress:i,resolveDeviceId:f,resolveSessionId:h,onError:v}),H=(0,verify_1.handleVerify)({client:e,verdictCache:b,resolveUserId:r,resolveEmailAddress:i,resolveDeviceId:f,resolveSessionId:h,onError:v}),V=(0,interstitial_1.handleGetInterstitialFlow)({client:e}),L=s?Array.isArray(s)?s:[s]:null,N=`${n}/fp.js`,R=`${n}/fingerprint.js`,X=`${n}/submit-fp`,G=`${n}/verify-trigger`,K=`${n}/verify`,B=`${n}/status`,W=`${n}/interstitial-flow`,z=function(t,s,o){const a=(0,http_helpers_1.getRequestPath)(t.url),m=t.url||a;if(a.startsWith(n+"/")){if(function(e,t){if(!L)return;const r=e.headers.origin??"";L.includes("*")?(t.setHeader("Access-Control-Allow-Origin","*"),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id, X-Permanent-Device-Id"),t.setHeader("Access-Control-Expose-Headers","X-Unshared-Enforcement-Reconciled")):L.includes(r)&&(t.setHeader("Access-Control-Allow-Origin",r),t.setHeader("Vary","Origin"),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id, X-Permanent-Device-Id"),t.setHeader("Access-Control-Expose-Headers","X-Unshared-Enforcement-Reconciled"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,s),"OPTIONS"===t.method)return void(0,http_helpers_1.sendEmpty)(s,204);if(("GET"===t.method||"HEAD"===t.method)&&a===N)return s.setHeader("Content-Type","application/javascript"),w?(s.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBodyForMethod)(s,t.method,200,w)):(s.setHeader("Cache-Control","no-store"),void(0,http_helpers_1.sendBodyForMethod)(s,t.method,503,"/* fingerprint SDK unavailable */"));if(("GET"===t.method||"HEAD"===t.method)&&a===R)return void q.get().then(e=>{if(!e)return s.setHeader("Cache-Control","no-store"),void(0,http_helpers_1.sendBodyForMethod)(s,t.method,503,"/* fingerprint agent unavailable */");s.setHeader("Content-Type","application/javascript"),s.setHeader("Cache-Control","public, max-age=3600"),(0,http_helpers_1.sendBodyForMethod)(s,t.method,200,e)});if("POST"===t.method&&(a===X||a===G||a===K))return void(0,read_json_body_1.readJsonBody)(t,s).then(e=>{if(e)return a===X?$(t,s):a===G?D(t,s):H(t,s)}).catch(o);if("GET"===t.method&&a===W)return void V(t,s);if("GET"===t.method&&a===B){let n;try{n=r(t)}catch{}if(!n)return void(0,http_helpers_1.sendJson)(s,200,{status:"anonymous"});const o=resolveEmail(t,i);return void(async()=>{let r=b.get(n);if((!r||b.isStale(n))&&o&&!E.isPaused()&&!b.isRefreshing(n)){b.markRefreshing(n);try{const i=(0,device_id_1.extractDeviceIdOrUndefined)(t,f),s=extractFingerprintId(t),a=extractFullHash(t),d=(0,permanent_device_id_1.extractPermanentDeviceId)(t),c=extractSessionId(t,h);await fetchAndCacheVerdict(e,b,n,o,i,s,a,d,c,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:n,emailAddress:o})),r=b.get(n)}catch(e){v&&v(e,{operation:"checkUser",userId:n,emailAddress:o}),x({operation:"checkUser",reason:"exception",userId:n,emailAddress:o})}finally{b.clearRefreshing(n)}}r&&r.isFlagged&&!r.isVerified&&(void 0!==A||g)&&o?(0,http_helpers_1.sendJson)(s,200,{status:"flagged",email:(0,mask_email_1.maskEmail)(o)}):(0,http_helpers_1.sendJson)(s,200,{status:"ok"})})()}return void(0,http_helpers_1.sendJson)(s,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}const I=void 0!==A;if((0,skip_paths_1.shouldSkipPath)(a,c,I))return void o();let k;try{k=r(t)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(k)){const e=(0,cookies_1.parseCookie)(t,"__unshared_uid"),r=(0,cookies_1.parseCookie)(t,"__unshared_uid_at"),i=r?Number(r):NaN,n=Number.isFinite(i)&&Date.now()-i<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;k=e&&n?e:void 0}if(!k){const e=(0,secure_1.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}`),appendSetCookie(s,`__unshared_verification_id=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(t,s,O,d),void o()}const C=resolveEmail(t,i);if(setUserIdCookie(t,s,k),C&&setEmailCookie(t,s,C),!(0,include_path_1.shouldIncludePath)(a,l))return interceptForInjection(t,s,O,d),void o();if(!C)return interceptForInjection(t,s,O,d),void o();const y=extractSessionId(t,h),S=(0,device_id_1.extractDeviceIdOrUndefined)(t,f),U=extractFingerprintId(t),M=extractFullHash(t),P=(0,permanent_device_id_1.extractPermanentDeviceId)(t),z=t.headers["user-agent"]??"",J=(0,client_ip_1.extractClientIp)(t);if(!u&&(0,is_bot_1.isBot)(z))return void o();const Q=b.get(k);if(!Q){if("async"===p)return b.isRefreshing(k)||(b.markRefreshing(k),fetchAndCacheVerdict(e,b,k,C,S,U,M,P,y,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:k,emailAddress:C})).catch(()=>x({operation:"checkUser",reason:"exception",userId:k,emailAddress:C})).finally(()=>b.clearRefreshing(k))),x({operation:"checkUser",reason:"async_miss",userId:k,emailAddress:C}),Y(),interceptForInjection(t,s,O,d),void o();let r=T.get(k);return r||(r=fetchAndCacheVerdict(e,b,k,C,S,U,M,P,y,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:k,emailAddress:C})).finally(()=>T.delete(k)),T.set(k,r)),void r.then(e=>{e.isFlagged||Y(),applyVerdict(e,k,C,t,s,o,O,g,A,j,d)}).catch(()=>{x({operation:"checkUser",reason:"exception",userId:k,emailAddress:C}),Y(),interceptForInjection(t,s,O,d),o()})}function Y(){y&&"unknown"!==y&&(E.isPaused()||dispatchUserEvent(e,b,E,F,{userId:k,emailAddress:C,sessionId:y,deviceId:S,fingerprintId:U,fullHash:M,permanentDeviceId:P,userAgent:z,ipAddress:J,eventType:m},v))}b.isStale(k)&&!b.isRefreshing(k)&&(b.markRefreshing(k),fetchAndCacheVerdict(e,b,k,C,S,U,M,P,y,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:k,emailAddress:C})).catch(()=>x({operation:"checkUser",reason:"exception",userId:k,emailAddress:C})).finally(()=>b.clearRefreshing(k))),Q.isFlagged||Y(),applyVerdict(Q,k,C,t,s,o,O,g,A,j,d)};return z.destroy=()=>{b.destroy(),q.stop()},z}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=(0,cookies_1.parseCookie)(e,"__unshared_email");if(r)return r;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,t,r,i,n,s,o,a,d,c,l){const u=e.isFlagged&&!e.isVerified;if("gate"===d&&u)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(n.statusCode=200,n.setHeader("Content-Type","text/html; charset=utf-8"),n.setHeader("Cache-Control","no-store"),n.end(c)):(0,http_helpers_1.sendJson)(n,403,(0,flagged_response_1.flaggedResponse)(r));else if("overlay"===d&&u)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(interceptForInjection(i,n,o,l),s()):(0,http_helpers_1.sendJson)(n,403,(0,flagged_response_1.flaggedResponse)(r));else if(interceptForInjection(i,n,o,l),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,i){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],(0,response_interceptor_1.interceptResponse)(t,(e,t)=>{if(!(0,content_type_1.isHtmlContentType)(t))return null;const i=e.toString("utf8"),n=i.lastIndexOf("</body>");return-1===n?i+r:i.slice(0,n)+r+i.slice(n)},{preventCaching:!0,streamingIdleMs:i})}function dispatchUserEvent(e,t,r,i,n,s){i.mark(n.userId,n.eventType),(0,dispatch_processing_1.runProcessUserEvent)({client:e,cache:t,backoff:r,onError:s},{eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,fullHash:n.fullHash,permanentDeviceId:n.permanentDeviceId,sessionHash:n.sessionId,userAgent:n.userAgent},dispatch_processing_1.DISPATCH_TIMEOUT_MS)}async function fetchAndCacheVerdict(e,t,r,i,n,s,o,a,d,c=CHECK_USER_TIMEOUT_MS,l){const u={timeoutMs:c,maxRetries:0};let _;n&&"unknown"!==n&&(u.deviceId=n),s&&"unknown"!==s&&(u.fingerprintId=s),o&&"unknown"!==o&&(u.fullHash=o),a&&(u.permanentDeviceId=a),d&&"unknown"!==d&&(u.sessionHash=d);const p=await Promise.race([e.checkUser(i,u),new Promise(e=>{_=setTimeout(()=>e(null),c)})]);return clearTimeout(_),p?p.failedOpen?(l?.(p.failedOpen.reason??"http_error",p.failedOpen.status),cacheFailOpenVerdict(t,r,i,d)):(t.set(r,{isFlagged:p.data?.is_user_flagged??!1,isVerified:!1,emailAddress:i,sessionId:d}),t.get(r)):(l?.("timeout"),cacheFailOpenVerdict(t,r,i,d))}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(0,cookies_1.parseCookie)(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return(0,cookies_1.parseCookie)(e,"__unshared_stable_hash")||(0,cookies_1.parseCookie)(e,"__unshared_fp_id")||void 0}function extractFullHash(e){return(0,cookies_1.parseCookie)(e,"__unshared_full_hash")||(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; Max-Age=31536000${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax; Max-Age=31536000${i}`)}function setEmailCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=31536000${i}`)}
@@ -23,4 +23,6 @@
23
23
  export declare function generateFingerprintScript(routePrefix: string, version?: string, opts?: {
24
24
  autoInterstitial?: boolean;
25
25
  interstitialFlowType?: string;
26
+ debug?: boolean;
27
+ disableBotFilter?: boolean;
26
28
  }): string;
@@ -1 +1 @@
1
- "use strict";function generateFingerprintScript(e,t,n){const r=t?`?v=${escapeJavaScript(t)}`:"",a=n?.autoInterstitial?`\nvar _isdk=new UnsharedBrowser.UnsharedBrowser({baseUrl:""});\nvar _isdkShow=function(){try{_isdk.showInterstitial({flowType:"${escapeJavaScript(n?.interstitialFlowType??"email_verification")}",onComplete:function(){try{location.reload()}catch(e){}}})}catch(e){}};\nwindow.addEventListener("unshared:flagged",_isdkShow);\nif(shared.flagged){_isdkShow()}`:"";return`<script>\n(function(){\ntry{\n// --- Bot drop (defense-in-depth) ---\n// Must be the first statement: we do not want to write cookies, localStorage,\n// session IDs, or any network requests for known-bot traffic. Mirrors the\n// regex in unshared-fingerprint-lib/src/detect/bot.ts and Node middleware\n// utils/is-bot.ts. Keep all three in sync.\nvar BOT_RE=/googlebot|bingbot|slurp|baiduspider|duckduckbot|yandex|sogou|exabot|ia_archiver|curl|wget|python-requests|python-urllib|axios|node-fetch|go-http-client|java\\/|libwww-perl|okhttp|apache-httpclient|http_request|httpie|headlesschrome|phantomjs|puppeteer|playwright|cypress|selenium|webdriver|electron|jsdom|vercel-screenshot|screenshot|prerender|lighthouse|chrome-lighthouse|pagespeed|gtmetrix|pingdom|nessus|nikto|sqlmap|burp|zap|qualys|openvas|nmap|masscan|facebookexternalhit|twitterbot|linkedinbot|whatsapp|telegrambot|slackbot|discordbot|bot|crawl|spider|scrape|fetch|scan/i;\nif(typeof navigator!=="undefined"&&navigator.userAgent&&BOT_RE.test(navigator.userAgent))return;\n\nvar pfx="${escapeJavaScript(e)}";\nvar SS_FP="__unshared_fp";\nvar SS_FP_RAW="__unshared_fp_raw";\nvar SS_LAST_SUBMIT="__unshared_last_submit";\nvar CAPTURE_SESSION_KEYS=["__unshared_email","__unshared_last_submit","__unshared_verification_id"];\nvar MAX_REQUEST_BYTES=102400;\n\n// Dedup state: skip submit if (user_id + URL) matches last submission.\n// Modern SPAs (Next.js App Router, React Router, etc.) call replaceState\n// 3-5 times during hydration with the same URL — without this guard,\n// each call generates a redundant FP row with identical stable_hash.\n// Persisted to sessionStorage so hard reloads and framework double-boots\n// inside the same tab still dedupe (the in-memory value resets on reload).\nvar lastSubmitKey="";\ntry{lastSubmitKey=sessionStorage.getItem(SS_LAST_SUBMIT)||""}catch(e){}\n\n// Page-scoped dedup state SHARED with the frontend SDK (browser.ts getSharedDedup).\n// On a Tier 1 page both this inline script and the SDK submit; each holds its own\n// in-memory lastSubmitKey, so both could pass the check below and POST. window.__unshared\n// is read+written synchronously in submitFP (no await between), so whichever fires\n// first claims the uid|route key and the other no-ops — killing the read-before-write\n// race that doubled events. KEEP IN SYNC with browser.ts: namespace, lastKey, key formula.\nvar shared=(window.__unshared=window.__unshared||{});\n// Sticky flagged signal: emitFlagged() records shared.flagged and drains\n// shared.flaggedCbs, so listeners that attach AFTER the CustomEvent fired\n// (late hydration, fp.js still loading) can recover it via whenFlagged().\n// KEEP IN SYNC with browser.ts getSharedDedup: flagged, flaggedCbs, whenFlagged.\nshared.whenFlagged=shared.whenFlagged||function(cb){(shared.flaggedCbs=shared.flaggedCbs||[]).push(cb);if(shared.flagged){try{cb(shared.flagged)}catch(e){}}};\n\n// --- Helpers ---\nfunction gC(n){var m=document.cookie.match(new RegExp("(?:^|; )"+n+"=([^;]*)"));return m?decodeURIComponent(m[1]):null}\nfunction sC(n,v,d){var e="";if(d){var dt=new Date();dt.setTime(dt.getTime()+d*864e5);e="; expires="+dt.toUTCString()}document.cookie=n+"="+encodeURIComponent(v)+e+"; path=/; SameSite=Lax"+(location.protocol==="https:"?"; Secure":"")}\nfunction uuid(){return(typeof crypto!=="undefined"&&crypto.randomUUID)?crypto.randomUUID():("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(c){var r=Math.random()*16|0;return(c==="x"?r:r&0x3|0x8).toString(16)}))}\nfunction validPid(v){return typeof v==="string"&&/^upid_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(v)}\nfunction bLen(v){return typeof TextEncoder!=="undefined"?new TextEncoder().encode(v).length:encodeURIComponent(v).replace(/%[0-9A-F]{2}/g,"x").length}\nfunction cCookies(){try{return document.cookie.split(";").map(function(v){return v.trim()}).filter(Boolean).map(function(v){var i=v.indexOf("="),n=i<0?v:v.slice(0,i),r=i<0?"":v.slice(i+1),d=r;try{d=decodeURIComponent(r)}catch(e){}return{name:n,value:d}}).sort(function(a,b){return a.name<b.name?-1:a.name>b.name?1:0})}catch(e){return[]}}\nfunction cStorage(s,keys){try{var out=[];for(var i=0;i<keys.length;i++){var v=s.getItem(keys[i]);if(v!==null)out.push({name:keys[i],value:v})}return out.sort(function(a,b){return a.name<b.name?-1:a.name>b.name?1:0})}catch(e){return[]}}\nfunction browserStorage(){var l=[],s=[];try{l=cStorage(localStorage,["__unshared_device_id"])}catch(e){}try{s=cStorage(sessionStorage,CAPTURE_SESSION_KEYS)}catch(e){}return{cookies:cCookies(),local_storage:l,session_storage:s}}\n// Sentinel user IDs that must never be treated as real users. Mirrors\n// the Set in sentinel-user-id.ts — keep in sync. Empty string is handled\n// by the separate !uid checks below.\nvar SENTINEL_UIDS={"__pre_auth__":1,"anonymous":1,"guest":1,"undefined":1,"null":1};\nfunction isSentinelUid(v){return typeof v==="string"&&SENTINEL_UIDS.hasOwnProperty(v)}\n\n// --- Session + device IDs ---\n// Session ID is a UUID because it's supposed to be tab-scoped and random.\n// Device ID is intentionally NOT a UUID — Issue 9: random UUIDs wrote\n// meaningless device_ids to every fingerprint row. Instead we read the\n// stable fingerprint hash from localStorage if a previous submission\n// already persisted it; otherwise we leave did empty and let submitFP()\n// reconcile on the first successful collection. The Node middleware's\n// Issue 8 bootstrap-skip branch handles the empty-device_id window so we\n// never dispatch with a random or "unknown" value.\nvar sid=gC("__unshared_sid");\nif(!sid){sid=uuid()}sC("__unshared_sid",sid,365);\nvar did="",pid="";\n\n// --- Fingerprint cache (sessionStorage) ---\n// Old normalized-only caches cannot recover overwritten raw fields; recollect them.\nfunction getFP(){try{var w=sessionStorage.getItem(SS_FP),r=sessionStorage.getItem(SS_FP_RAW);return w&&r?{wire:JSON.parse(w),raw:JSON.parse(r)}:null}catch(e){return null}}\nfunction setFP(collected){try{sessionStorage.removeItem(SS_FP_RAW);sessionStorage.setItem(SS_FP,JSON.stringify(collected.wire));sessionStorage.setItem(SS_FP_RAW,JSON.stringify(collected.raw))}catch(e){}}\n\n// --- Submit fingerprint to backend ---\nfunction submitFP(collected){\n var fp=collected.wire;\n var uid=gC("__unshared_uid");\n if(!uid||isSentinelUid(uid))return;\n try{pid=localStorage.getItem("__unshared_device_id")||""}catch(e){}\n if(!validPid(pid)){pid="upid_"+uuid();try{localStorage.setItem("__unshared_device_id",pid)}catch(e){}}\n sC("__unshared_device_id",pid,400);\n // Issue 9: reconcile device_id to the stable fingerprint hash. This runs\n // before we send the X-Device-Id header so the very first submission\n // already carries the real value. Persist to localStorage so other tabs\n // (and future reloads) pick up the same stable ID without needing to\n // re-collect the fingerprint.\n if(fp.fingerprint_id){\n did=fp.fingerprint_id;\n sC("__unshared_fp_id",did,365);\n }\n // event_type is the SPA route, not a fixed enum. Page-level event names\n // (page_load/route_change) collapsed every row into one of two buckets;\n // the URL is more useful for analytics and matches the frontend SDK.\n var route=(location.pathname||"/")+(location.search||"");\n var key=uid+"|"+route;\n // Check the shared window guard AND the in-memory key (last-key semantics, so an\n // SPA A->B->A revisit still submits the second A). The shared guard makes this\n // submitter and the frontend SDK see each other within the page.\n if(key===lastSubmitKey||shared.lastKey===key)return;\n // collected_at is stamped fresh at submit time rather than carried from fp.timestamp,\n // because fp is cached per-tab in sessionStorage — reusing its original timestamp would\n // freeze collected_at at first load and drift against server created_at as the tab ages.\n // The server authoritatively overwrites this value again on ingress.\n var body={fingerprint:collected.raw,context:{sdk:{name:"unshared-inline-sdk",version:"3.0.0",agent_source:fp.source||"unknown"},identity:{user_id:uid},identifiers:{unshared_device_id:pid,device_id:did,stable_hash:fp.fingerprint_id||"",full_hash:fp.full_hash||"",session_hash:sid},browser_storage:browserStorage(),event:{type:route,collected_at:(new Date()).toISOString()}}};\n var payload=JSON.stringify(body);\n // Keep in sync with the structured browser builder: full, cookies only, core only.\n if(bLen(payload)>MAX_REQUEST_BYTES){body.context.browser_storage.local_storage=[];body.context.browser_storage.session_storage=[];payload=JSON.stringify(body);}\n if(bLen(payload)>MAX_REQUEST_BYTES){delete body.context.browser_storage;payload=JSON.stringify(body);}\n if(bLen(payload)>MAX_REQUEST_BYTES){if(window.console&&console.warn)console.warn("[Unshared] REQUEST_TOO_LARGE: Request body exceeds 100 KiB");return;}\n shared.lastKey=key;\n lastSubmitKey=key;\n try{sessionStorage.setItem(SS_LAST_SUBMIT,key)}catch(e){}\n // Idempotency key derived from (stable_hash, user_id, route), SHA-256-hashed\n // when WebCrypto is available so the user_id never appears in the platform's\n // stored idempotency_key column (raw fallback on non-secure contexts — the\n // middleware accepts both). NOTE: the middleware appends |Date.now() before\n // forwarding (submit-fp.ts), so the backend sees a unique value per\n // submission and dedups only PubSub redeliveries — NOT two distinct POSTs.\n // Cross-submitter / cross-reload dedup is client-side (the shared window\n // guard + sessionStorage above).\n var idem=fp.fingerprint_id+"|"+uid+"|"+route;\n function sendFP(idemKey){\n var xhr=new XMLHttpRequest();\n xhr.open("POST",pfx+"/submit-fp",true);\n xhr.setRequestHeader("Content-Type","application/json");\n xhr.setRequestHeader("X-Session-Id",sid);\n if(did)xhr.setRequestHeader("X-Device-Id",did);\n if(pid)xhr.setRequestHeader("X-Permanent-Device-Id",pid);\n xhr.setRequestHeader("X-Idempotency-Key",idemKey);\n xhr.send(payload);\n }\n if(window.crypto&&crypto.subtle&&window.TextEncoder){\n crypto.subtle.digest("SHA-256",new TextEncoder().encode(idem)).then(function(d){\n var a=new Uint8Array(d),s="";\n for(var i=0;i<a.length;i++){s+=("0"+a[i].toString(16)).slice(-2)}\n sendFP(s);\n }).catch(function(){sendFP(idem)});\n }else{sendFP(idem)}\n}\n\n// --- Collect fingerprint (loads fp.js if needed) then submit ---\nvar fpReady=false;\nfunction collectAndSubmit(){\n var uid=gC("__unshared_uid");\n if(!uid||isSentinelUid(uid))return;\n var cached=getFP();\n if(cached){submitFP(cached);return}\n if(!fpReady)return;\n try{\n var c=new UnsharedBrowser.UnsharedBrowser({baseUrl:""});\n c.collectWithMetadata({exclude:["timing","speech"]}).then(function(fp){setFP(fp);submitFP(fp)}).catch(function(){});\n }catch(e){}\n}\n\n// --- Load fp.js (always — browser caches it for 1h) ---\n// Submit cached FP immediately if available; load fp.js for fresh collection\nvar pageLoadSubmitted=false;\nvar _boot_uid=gC("__unshared_uid");\nif(getFP()&&_boot_uid&&!isSentinelUid(_boot_uid)){submitFP(getFP());pageLoadSubmitted=true;deferredCheck()}\nvar s=document.createElement("script");\ns.src=pfx+"/fp.js${r}";\ns.onload=function(){fpReady=true;${a}if(!pageLoadSubmitted){collectAndSubmit();deferredCheck()}};\ndocument.head.appendChild(s);\n\n// --- Deferred verdict check ---\n// After fingerprint submission, the backend processes the event async.\n// If the user was just flagged, the initial page load may have beaten\n// the verdict update. Re-check after a delay so newly flagged sessions\n// get caught without waiting for user interaction.\n// The endpoint always returns 200 so browsers don't log a scary red\n// network error — we inspect the body and dispatch the flagged event\n// ourselves when status==="flagged".\nfunction deferredCheck(){\n var uid=gC("__unshared_uid");\n if(!uid||isSentinelUid(uid))return;\n setTimeout(function(){\n try{fetch(pfx+"/status",{method:"GET",credentials:"same-origin"}).then(function(r){return r.json()}).then(function(b){if(b&&b.status==="flagged")emitFlagged(b)}).catch(function(){})}catch(e){}\n },500);\n}\n\n// --- SPA route change tracking (History API + popstate) ---\nvar oPush=history.pushState,oReplace=history.replaceState;\nhistory.pushState=function(){oPush.apply(this,arguments);try{collectAndSubmit()}catch(e){}};\nhistory.replaceState=function(){oReplace.apply(this,arguments);try{collectAndSubmit()}catch(e){}};\nwindow.addEventListener("popstate",function(){try{collectAndSubmit()}catch(e){}});\n\n// --- 403 interception: record sticky state + dispatch "unshared:flagged" ---\nfunction emitFlagged(body){\n var d={email:body.email||""};\n shared.flagged=d;\n var cbs=shared.flaggedCbs||[];\n for(var i=0;i<cbs.length;i++){try{cbs[i](d)}catch(e){}}\n try{window.dispatchEvent(new CustomEvent("unshared:flagged",{detail:d}))}catch(e){}\n}\n\n// Patch fetch\nvar oFetch=window.fetch;\nif(oFetch){window.fetch=function(){return oFetch.apply(this,arguments).then(function(r){if(r.status===403){try{var cl=r.clone();cl.json().then(function(b){if(b&&b.error==="account_flagged")emitFlagged(b)}).catch(function(){})}catch(e){}}return r})}}\n\n// Patch XMLHttpRequest\nvar oXSend=XMLHttpRequest.prototype.send;\nXMLHttpRequest.prototype.send=function(){var x=this;x.addEventListener("load",function(){if(x.status===403){try{var b=JSON.parse(x.responseText);if(b&&b.error==="account_flagged")emitFlagged(b)}catch(e){}}});return oXSend.apply(this,arguments)};\n\n}catch(e){}\n})();\n<\/script>`}function escapeJavaScript(e){return JSON.stringify(e).slice(1,-1).replace(/<\/(script)/gi,"<\\/$1").replace(new RegExp("\u2028","g"),"\\u2028").replace(new RegExp("\u2029","g"),"\\u2029")}Object.defineProperty(exports,"t",{value:!0}),exports.generateFingerprintScript=generateFingerprintScript;
1
+ "use strict";function generateFingerprintScript(e,n,t){const r=n?`?v=${escapeJavaScript(n)}`:"",i=t?.autoInterstitial?`\nvar _isdk=new UnsharedBrowser.UnsharedBrowser({baseUrl:""});\nvar _isdkShow=function(){try{_isdk.showInterstitial({flowType:"${escapeJavaScript(t?.interstitialFlowType??"email_verification")}",onComplete:function(){try{location.reload()}catch(e){}}})}catch(e){}};\nwindow.addEventListener("unshared:flagged",_isdkShow);\nif(shared.flagged){_isdkShow()}`:"";return`<script>\n(function(){\nvar debug=${!0===t?.debug},shared;\n// Diagnostics are bounded and contain only reason codes and HTTP status, never user data.\nfunction decision(code,status){\n if(!debug)return;\n var d={code:code};if(typeof status==="number")d.status=status;\n try{var prev=shared.lastDecision;shared.lastDecision=d;if(prev&&prev.code===code&&prev.status===status)return}catch(e){}\n try{if(window.console&&typeof window.console.debug==="function")window.console.debug("[Unshared]",d)}catch(e){}\n}\ntry{\nshared=(window.__unshared=window.__unshared||{});\n// --- Bot drop (defense-in-depth) ---\n// Initialize diagnostics first, but do not write cookies, localStorage,\n// session IDs, or make network requests for known-bot traffic. Mirrors the\n// regex in unshared-fingerprint-lib/src/detect/bot.ts and Node middleware\n// utils/is-bot.ts. Keep all three in sync.\nvar BOT_RE=/googlebot|bingbot|slurp|baiduspider|duckduckbot|yandex|sogou|exabot|ia_archiver|curl|wget|python-requests|python-urllib|axios|node-fetch|go-http-client|java\\/|libwww-perl|okhttp|apache-httpclient|http_request|httpie|headlesschrome|phantomjs|puppeteer|playwright|cypress|selenium|webdriver|electron|jsdom|vercel-screenshot|screenshot|prerender|lighthouse|chrome-lighthouse|pagespeed|gtmetrix|pingdom|nessus|nikto|sqlmap|burp|zap|qualys|openvas|nmap|masscan|facebookexternalhit|twitterbot|linkedinbot|whatsapp|telegrambot|slackbot|discordbot|bot|crawl|spider|scrape|fetch|scan/i;\nif(!${!0===t?.disableBotFilter}&&typeof navigator!=="undefined"&&navigator.userAgent&&BOT_RE.test(navigator.userAgent)){decision("BOT_SKIPPED");return}\n\nvar pfx="${escapeJavaScript(e)}";\nvar SS_FP="__unshared_fp";\nvar SS_FP_RAW="__unshared_fp_raw";\nvar SS_LAST_SUBMIT="__unshared_last_submit";\nvar CAPTURE_SESSION_KEYS=["__unshared_email","__unshared_last_submit","__unshared_verification_id"];\nvar MAX_REQUEST_BYTES=102400;\n\n// Dedup state: skip submit if (user_id + URL) matches last submission.\n// Modern SPAs (Next.js App Router, React Router, etc.) call replaceState\n// 3-5 times during hydration with the same URL — without this guard,\n// each call generates a redundant FP row with identical stable_hash.\n// Persisted to sessionStorage so hard reloads and framework double-boots\n// inside the same tab still dedupe (the in-memory value resets on reload).\ntry{if(!shared.generation&&shared.lastKey===undefined)shared.lastKey=sessionStorage.getItem(SS_LAST_SUBMIT)||undefined}catch(e){}\n\n// Page-scoped dedup state SHARED with the frontend SDK (browser.ts getSharedDedup).\n// On a Tier 1 page both this inline script and the SDK submit. window.__unshared\n// is read+written synchronously in submitFP (no await between), so whichever fires\n// first claims the uid|route key and the other no-ops — killing the read-before-write\n// race that doubled events. KEEP IN SYNC with browser.ts: namespace, lastKey, key formula.\n// Sticky flagged signal: emitFlagged() records shared.flagged and drains\n// shared.flaggedCbs, so listeners that attach AFTER the CustomEvent fired\n// (late hydration, fp.js still loading) can recover it via whenFlagged().\n// KEEP IN SYNC with browser.ts getSharedDedup: flagged, flaggedCbs, whenFlagged.\nshared.whenFlagged=shared.whenFlagged||function(cb){(shared.flaggedCbs=shared.flaggedCbs||[]).push(cb);if(shared.flagged){try{cb(shared.flagged)}catch(e){}}};\n\n// --- Helpers ---\nfunction gC(n){try{var m=document.cookie.match(new RegExp("(?:^|; )"+n+"=([^;]*)"));return m?decodeURIComponent(m[1]):null}catch(e){return null}}\nfunction sC(n,v,d){try{var dt=new Date();dt.setTime(dt.getTime()+d*864e5);document.cookie=n+"="+encodeURIComponent(v)+"; Max-Age="+(d*86400)+"; expires="+dt.toUTCString()+"; path=/; SameSite=Lax"+(location.protocol==="https:"?"; Secure":"")}catch(e){}}\nfunction uuid(){return(typeof crypto!=="undefined"&&crypto.randomUUID)?crypto.randomUUID():("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(c){var r=Math.random()*16|0;return(c==="x"?r:r&0x3|0x8).toString(16)}))}\nfunction validPid(v){return typeof v==="string"&&/^upid_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(v)}\nfunction bLen(v){return typeof TextEncoder!=="undefined"?new TextEncoder().encode(v).length:encodeURIComponent(v).replace(/%[0-9A-F]{2}/g,"x").length}\nfunction cCookies(){try{return document.cookie.split(";").map(function(v){return v.trim()}).filter(Boolean).map(function(v){var i=v.indexOf("="),n=i<0?v:v.slice(0,i),r=i<0?"":v.slice(i+1),d=r;try{d=decodeURIComponent(r)}catch(e){}return{name:n,value:d}}).sort(function(a,b){return a.name<b.name?-1:a.name>b.name?1:0})}catch(e){return[]}}\nfunction cStorage(s,keys){try{var out=[];for(var i=0;i<keys.length;i++){var v=s.getItem(keys[i]);if(v!==null)out.push({name:keys[i],value:v})}return out.sort(function(a,b){return a.name<b.name?-1:a.name>b.name?1:0})}catch(e){return[]}}\nfunction browserStorage(){var l=[],s=[];try{l=cStorage(localStorage,["__unshared_device_id"])}catch(e){}try{s=cStorage(sessionStorage,CAPTURE_SESSION_KEYS)}catch(e){}return{cookies:cCookies(),local_storage:l,session_storage:s}}\n// Sentinel user IDs that must never be treated as real users. Mirrors\n// the Set in sentinel-user-id.ts — keep in sync. Empty string is handled\n// by the separate !uid checks below.\nvar SENTINEL_UIDS={"__pre_auth__":1,"anonymous":1,"guest":1,"undefined":1,"null":1};\nfunction isSentinelUid(v){return typeof v==="string"&&SENTINEL_UIDS.hasOwnProperty(v)}\n\n// --- Session + device IDs ---\n// Permanent identity is independent of the collected stable hash. Keep an\n// in-memory fallback so blocked storage does not regenerate it on every route.\nvar sid=gC("__unshared_sid");\nvar did="",pid="";\n\n// --- Fingerprint cache (sessionStorage) ---\n// Old normalized-only caches cannot recover overwritten raw fields; recollect them.\n// Public page-memory fields match browser.ts and must not be property-mangled.\nfunction validFP(fp){return fp&&fp.wire&&typeof fp.wire.fingerprint_id==="string"&&typeof fp.wire.full_hash==="string"&&fp.raw&&typeof fp.raw==="object"&&!Array.isArray(fp.raw)}\nfunction getFP(){if(validFP(shared.collection))return shared.collection;if(shared.collectionPromise||shared.generation)return null;try{var w=sessionStorage.getItem(SS_FP),r=sessionStorage.getItem(SS_FP_RAW),fp=w&&r?{wire:JSON.parse(w),raw:JSON.parse(r)}:null;if(validFP(fp))return shared.collection=fp;return null}catch(e){return null}}\nfunction setFP(collected){shared.collection=collected;try{sessionStorage.removeItem(SS_FP_RAW);sessionStorage.setItem(SS_FP,JSON.stringify(collected.wire));sessionStorage.setItem(SS_FP_RAW,JSON.stringify(collected.raw))}catch(e){}}\n\n// --- Submit fingerprint to backend ---\nfunction submitFP(collected){\n var generation=shared.generation||0;\n var fp=collected.wire;\n var uid=gC("__unshared_uid");\n if(!uid){decision("MISSING_UID");return}\n if(isSentinelUid(uid)){decision("SENTINEL_UID");return}\n var storedPid="";try{storedPid=localStorage.getItem("__unshared_device_id")||""}catch(e){}\n var cookiePid=gC("__unshared_device_id");\n if(validPid(storedPid)){pid=storedPid}else if(validPid(cookiePid)){pid=cookiePid}else if(!validPid(pid)){pid="upid_"+uuid()}\n try{localStorage.setItem("__unshared_device_id",pid)}catch(e){}\n // Renew correlation cookies on eligible activity, even when submission dedups.\n sid=shared.sessionId||gC("__unshared_sid")||sid||uuid();shared.sessionId=sid;sC("__unshared_sid",sid,400);\n sC("__unshared_device_id",pid,400);\n if(fp.fingerprint_id){\n did=fp.fingerprint_id;\n sC("__unshared_fp_id",did,400);\n sC("__unshared_stable_hash",fp.fingerprint_id,400);\n }\n if(fp.full_hash)sC("__unshared_full_hash",fp.full_hash,400);\n // event_type is the SPA route, not a fixed enum. Page-level event names\n // (page_load/route_change) collapsed every row into one of two buckets;\n // the URL is more useful for analytics and matches the frontend SDK.\n var route=(location.pathname||"/")+(location.search||"");\n var key=uid+"|"+route;\n // Check the shared window guard (last-key semantics, so an\n // SPA A->B->A revisit still submits the second A). The shared guard makes this\n // submitter and the frontend SDK see each other within the page.\n if(shared.lastKey===key){decision("DEDUP_SKIPPED");return}\n // collected_at is stamped fresh at submit time rather than carried from fp.timestamp,\n // because fp is cached per-tab in sessionStorage — reusing its original timestamp would\n // freeze collected_at at first load and drift against server created_at as the tab ages.\n // The server authoritatively overwrites this value again on ingress.\n var body={fingerprint:collected.raw,context:{sdk:{name:"unshared-inline-sdk",version:"3.0.0",agent_source:fp.source||"unknown"},identity:{user_id:uid},identifiers:{unshared_device_id:pid,device_id:did,stable_hash:fp.fingerprint_id||"",full_hash:fp.full_hash||"",session_hash:sid},browser_storage:browserStorage(),event:{type:route,collected_at:(new Date()).toISOString()}}};\n var payload=JSON.stringify(body);\n // Keep in sync with the structured browser builder: full, cookies only, core only.\n if(bLen(payload)>MAX_REQUEST_BYTES){body.context.browser_storage.local_storage=[];body.context.browser_storage.session_storage=[];payload=JSON.stringify(body);}\n if(bLen(payload)>MAX_REQUEST_BYTES){delete body.context.browser_storage;payload=JSON.stringify(body);}\n if(bLen(payload)>MAX_REQUEST_BYTES){decision("REQUEST_TOO_LARGE");try{if(window.console&&window.console.warn)window.console.warn("[Unshared] REQUEST_TOO_LARGE: Request body exceeds 100 KiB")}catch(e){}return;}\n shared.lastKey=key;\n try{sessionStorage.setItem(SS_LAST_SUBMIT,key)}catch(e){}\n // Idempotency key derived from (stable_hash, user_id, route), SHA-256-hashed\n // when WebCrypto is available so the user_id never appears in the platform's\n // stored idempotency_key column (raw fallback on non-secure contexts — the\n // middleware accepts both). NOTE: the middleware appends |Date.now() before\n // forwarding (submit-fp.ts), so the backend sees a unique value per\n // submission and dedups only PubSub redeliveries — NOT two distinct POSTs.\n // Cross-submitter / cross-reload dedup is client-side (the shared window\n // guard + sessionStorage above).\n var idem=fp.fingerprint_id+"|"+uid+"|"+route;\n function sendFP(idemKey){\n if(generation!==(shared.generation||0))return;\n try{\n var xhr=new XMLHttpRequest();\n xhr.open("POST",pfx+"/submit-fp",true);\n xhr.setRequestHeader("Content-Type","application/json");\n xhr.setRequestHeader("X-Session-Id",sid);\n if(did)xhr.setRequestHeader("X-Device-Id",did);\n if(pid)xhr.setRequestHeader("X-Permanent-Device-Id",pid);\n xhr.setRequestHeader("X-Idempotency-Key",idemKey);\n xhr.addEventListener("load",function(){\n if(xhr.status<200||xhr.status>=300){decision("SUBMIT_HTTP_ERROR",xhr.status);return}\n var b;try{b=JSON.parse(xhr.responseText)}catch(e){decision("SUBMIT_INVALID_RESPONSE",xhr.status);return}\n if(b&&(b.success===false||b.accepted===false||(b.data&&(b.data.accepted===false||b.data.skipped===true)))){decision("SUBMIT_REJECTED",xhr.status);return}\n decision(b&&b.success===true&&b.data&&typeof b.data.event_id==="string"&&typeof b.data.collected_at==="string"&&b.data.endpoint_version==="v3"?"SUBMIT_SUCCESS":"SUBMIT_INVALID_RESPONSE",xhr.status);\n });\n xhr.addEventListener("error",function(){decision("SUBMIT_NETWORK_ERROR")});\n xhr.addEventListener("abort",function(){decision("SUBMIT_ABORTED")});\n xhr.addEventListener("timeout",function(){decision("SUBMIT_TIMEOUT")});\n decision("SUBMIT_STARTED");\n xhr.send(payload);\n }catch(e){decision("SUBMIT_FAILED")}\n }\n if(window.crypto&&crypto.subtle&&window.TextEncoder){\n crypto.subtle.digest("SHA-256",new TextEncoder().encode(idem)).then(function(d){\n var a=new Uint8Array(d),s="";\n for(var i=0;i<a.length;i++){s+=("0"+a[i].toString(16)).slice(-2)}\n sendFP(s);\n }).catch(function(){sendFP(idem)});\n }else{sendFP(idem)}\n}\n\n// --- Collect fingerprint (loads fp.js if needed) then submit ---\nvar fpReady=false;\nfunction collectAndSubmit(){\n var generation=shared.generation||0;\n var uid=gC("__unshared_uid");\n if(!uid){decision("MISSING_UID");return}\n if(isSentinelUid(uid)){decision("SENTINEL_UID");return}\n var cached=getFP();\n if(cached){submitFP(cached);return}\n try{\n var pending=shared.collectionPromise;\n if(!pending){\n if(!fpReady){decision("COLLECTOR_NOT_READY");return}\n var c=new UnsharedBrowser.UnsharedBrowser({baseUrl:""});\n decision("COLLECTION_STARTED");\n pending=c.collectWithMetadata({exclude:["timing","speech"]}).then(function(fp){\n if(generation!==(shared.generation||0)||shared.collectionPromise!==pending)throw new Error("Collection cancelled");\n if(!validFP(fp))throw new Error("Invalid collection");\n setFP(fp);return fp;\n });\n shared.collectionPromise=pending;\n var clear=function(){if(shared.collectionPromise===pending)delete shared.collectionPromise};\n pending.then(clear,clear);\n }\n pending.then(function(fp){if(generation===(shared.generation||0))submitFP(fp)}).catch(function(){if(generation===(shared.generation||0))decision("COLLECTION_ERROR")});\n }catch(e){decision("COLLECTION_ERROR")}\n}\n\n// --- Load fp.js (always — browser caches it for 1h) ---\n// Submit cached FP immediately if available; load fp.js for fresh collection\nvar pageLoadSubmitted=false;\nvar _boot_uid=gC("__unshared_uid");\nif(getFP()&&_boot_uid&&!isSentinelUid(_boot_uid)){submitFP(getFP());pageLoadSubmitted=true;deferredCheck()}else{collectAndSubmit()}\nvar s=document.createElement("script");\ns.src=pfx+"/fp.js${r}";\ns.onload=function(){fpReady=true;try{${i}if(!pageLoadSubmitted){collectAndSubmit();deferredCheck()}}catch(e){decision("OUTER_EXCEPTION")}};\ns.onerror=function(){decision("ASSET_LOAD_ERROR")};\ndocument.head.appendChild(s);\n\n// --- Deferred verdict check ---\n// After fingerprint submission, the backend processes the event async.\n// If the user was just flagged, the initial page load may have beaten\n// the verdict update. Re-check after a delay so newly flagged sessions\n// get caught without waiting for user interaction.\n// The endpoint always returns 200 so browsers don't log a scary red\n// network error — we inspect the body and dispatch the flagged event\n// ourselves when status==="flagged".\nfunction deferredCheck(){\n var uid=gC("__unshared_uid");\n if(!uid||isSentinelUid(uid))return;\n setTimeout(function(){\n try{fetch(pfx+"/status",{method:"GET",credentials:"same-origin"}).then(function(r){return r.json()}).then(function(b){if(b&&b.status==="flagged")emitFlagged(b)}).catch(function(){})}catch(e){}\n },500);\n}\n\n// --- SPA route change tracking (History API + popstate) ---\nvar oPush=history.pushState,oReplace=history.replaceState;\nhistory.pushState=function(){oPush.apply(this,arguments);try{collectAndSubmit()}catch(e){decision("OUTER_EXCEPTION")}};\nhistory.replaceState=function(){oReplace.apply(this,arguments);try{collectAndSubmit()}catch(e){decision("OUTER_EXCEPTION")}};\nwindow.addEventListener("popstate",function(){try{collectAndSubmit()}catch(e){decision("OUTER_EXCEPTION")}});\n\n// --- 403 interception: record sticky state + dispatch "unshared:flagged" ---\nfunction emitFlagged(body){\n var d={email:body.email||""};\n shared.flagged=d;\n var cbs=shared.flaggedCbs||[];\n for(var i=0;i<cbs.length;i++){try{cbs[i](d)}catch(e){}}\n try{window.dispatchEvent(new CustomEvent("unshared:flagged",{detail:d}))}catch(e){}\n}\n\n// Patch fetch\nvar oFetch=window.fetch;\nif(oFetch){window.fetch=function(){return oFetch.apply(this,arguments).then(function(r){if(r.status===403){try{var cl=r.clone();cl.json().then(function(b){if(b&&b.error==="account_flagged")emitFlagged(b)}).catch(function(){})}catch(e){}}return r})}}\n\n// Patch XMLHttpRequest\nvar oXSend=XMLHttpRequest.prototype.send;\nXMLHttpRequest.prototype.send=function(){var x=this;x.addEventListener("load",function(){if(x.status===403){try{var b=JSON.parse(x.responseText);if(b&&b.error==="account_flagged")emitFlagged(b)}catch(e){}}});return oXSend.apply(this,arguments)};\n\n}catch(e){decision("OUTER_EXCEPTION")}\n})();\n<\/script>`}function escapeJavaScript(e){return JSON.stringify(e).slice(1,-1).replace(/<\/(script)/gi,"<\\/$1").replace(new RegExp("\u2028","g"),"\\u2028").replace(new RegExp("\u2029","g"),"\\u2029")}Object.defineProperty(exports,"t",{value:!0}),exports.generateFingerprintScript=generateFingerprintScript;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.handleSubmitFingerprint=handleSubmitFingerprint;const util_1=require("../../util"),is_bot_1=require("../utils/is-bot"),client_ip_1=require("../utils/client-ip"),cookies_1=require("../utils/cookies"),device_id_1=require("../utils/device-id"),secure_1=require("../utils/secure"),sentinel_user_id_1=require("../utils/sentinel-user-id"),http_helpers_1=require("../utils/http-helpers"),permanent_device_id_1=require("../utils/permanent-device-id"),dispatch_processing_1=require("../dispatch-processing");function asRecord(e){return null===e||"object"!=typeof e||Array.isArray(e)?{}:e}function handleSubmitFingerprint(e){return async(t,i)=>{try{const s=asRecord(t.body),r=asRecord(s.fingerprint??s),n=asRecord(s.context),o=asRecord(n.identity),c=asRecord(n.identifiers),_=asRecord(n.event),d={...r,full_hash:r.hash??r.full_hash??"",fingerprint_id:r.stableHash??r.stable_hash??r.fingerprint_id??"",timestamp:r.timestamp??r.collected_at??(new Date).toISOString(),isIncognito:r.isIncognito??r.is_incognito??!1,components:r.components??{},version:r.version??"unknown",..."remote"===r.source||"bundled"===r.source?{source:r.source}:{}};let a,p,u;try{const i=e.resolveUserId?e.resolveUserId(t):void 0;i&&!(0,sentinel_user_id_1.isSentinelUserId)(i)&&(a=i)}catch{}if(!a){const e="string"==typeof o.user_id?o.user_id:"string"==typeof s.user_id?s.user_id:void 0;e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(a=e)}if(!a){const e=(0,cookies_1.parseCookie)(t,"__unshared_uid");e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(a=e)}try{p=e.resolveEmailAddress?e.resolveEmailAddress(t):void 0}catch{}p=p??(0,cookies_1.parseCookie)(t,"__unshared_email")??o.email_address??s.email??void 0;try{u=e.resolveSessionId?e.resolveSessionId(t):void 0}catch{}u=u??c.session_hash??s.session_id??(0,cookies_1.parseCookie)(t,"__unshared_sid");const l=(0,client_ip_1.extractClientIp)(t),h=t.headers["user-agent"]??"";if(!e.disableBotFilter&&(0,is_bot_1.isBot)(h))return void(0,http_helpers_1.sendJson)(i,200,{success:!0,data:{skipped:!0}});const f=(d.fingerprint_id&&d.fingerprint_id.length>0?d.fingerprint_id:void 0)??(0,device_id_1.extractDeviceId)(t,e.resolveDeviceId),y=d.fingerprint_id||void 0,g=d.full_hash||void 0,v=(0,permanent_device_id_1.resolveOrCreatePermanentDeviceId)(t,c.unshared_device_id??s.permanent_device_id),m=(0,secure_1.isSecureRequest)(t)?"; Secure":"",b=[];b.push(`__unshared_device_id=${encodeURIComponent(v.id)}; Path=/; SameSite=Lax; Max-Age=34560000${m}`);const R=(0,cookies_1.parseCookie)(t,"__unshared_fingerprint_id")||g;if(R&&b.push(`__unshared_fingerprint_id=${encodeURIComponent(R)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=31536000${m}`),y&&b.push(`__unshared_fp_id=${encodeURIComponent(y)}; Path=/; SameSite=Lax; Max-Age=31536000${m}`),p&&b.push(`__unshared_email=${encodeURIComponent(p)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=31536000${m}`),b.length>0){const e=i.getHeader("Set-Cookie");if(e){const t=Array.isArray(e)?[...e]:[String(e)];t.push(...b),i.setHeader("Set-Cookie",t)}else i.setHeader("Set-Cookie",b)}let S;if("string"==typeof _.type&&_.type)S=_.type;else if("string"==typeof s.event_type&&s.event_type)S=s.event_type;else{const e=t.headers.referer??t.headers.referrer;let i="unknown";if("string"==typeof e&&e.length>0)try{const t=new URL(e);i=(t.pathname||"/")+(t.search||"")}catch{}S=i}const $=t.headers["x-idempotency-key"],k="string"==typeof $&&$.length>0?$:void 0,x=Date.now(),A=k?`${k}|${x}`:y&&a?`${(0,util_1.sha256Hex)(`${y}|${a}|${S}`)}|${x}`:void 0,q={...c,unshared_device_id:v.id,...f?{device_id:f}:{},...y?{stable_hash:y}:{},...g?{full_hash:g}:{},...u?{session_hash:u}:{}},j="string"==typeof asRecord(n.sdk).name?asRecord(n.sdk).name:void 0,C="string"==typeof j&&j.startsWith("unshared-")&&Object.prototype.hasOwnProperty.call(s,"fingerprint")&&s.context&&"object"==typeof s.context&&!Array.isArray(s.context)&&n.identity&&"object"==typeof n.identity&&!Array.isArray(n.identity)&&n.identifiers&&"object"==typeof n.identifiers&&!Array.isArray(n.identifiers)&&n.event&&"object"==typeof n.event&&!Array.isArray(n.event),w=!Object.prototype.hasOwnProperty.call(s,"context")&&"string"==typeof s.hash&&"string"==typeof s.stable_hash&&"string"==typeof s.collected_at&&s.components&&"object"==typeof s.components&&!Array.isArray(s.components),I=C||w?{...s,fingerprint:r,context:{...n,sdk:asRecord(n.sdk),identity:{...o,...a?{user_id:a}:{},...p?{email_address:p}:{}},identifiers:q,event:{..._,type:S,ip_address:l,user_agent:h}}}:s,L=await e.client.submitFingerprint(I,{idempotencyKey:A});L.success||e.onError?.(L.error,{operation:"submitFingerprintEvent",userId:a,emailAddress:p}),a&&p&&!e.rateLimitBackoff.isPaused()&&!e.dispatchDedupe.wasRecentlyDispatched(a,S)&&await(0,dispatch_processing_1.runCheckUser)({client:e.client,cache:e.verdictCache,backoff:e.rateLimitBackoff,onError:e.onError},{user_id:a,email_address:p,identifiers:q},a,p,dispatch_processing_1.SUBMIT_FP_TIMEOUT_MS),L.success?(0,http_helpers_1.sendJson)(i,200,{success:!0,data:L.data}):(0,http_helpers_1.sendJson)(i,200,{success:!1,error:L.error})}catch(t){e.onError&&e.onError(t,{operation:"submitFingerprintEvent"}),(0,http_helpers_1.sendJson)(i,200,{success:!0,data:{accepted:!1}})}}}
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.handleSubmitFingerprint=handleSubmitFingerprint;const util_1=require("../../util"),is_bot_1=require("../utils/is-bot"),client_ip_1=require("../utils/client-ip"),cookies_1=require("../utils/cookies"),secure_1=require("../utils/secure"),sentinel_user_id_1=require("../utils/sentinel-user-id"),http_helpers_1=require("../utils/http-helpers"),permanent_device_id_1=require("../utils/permanent-device-id"),dispatch_processing_1=require("../dispatch-processing");function asRecord(e){return null===e||"object"!=typeof e||Array.isArray(e)?{}:e}function handleSubmitFingerprint(e){return async(t,i)=>{try{const s=asRecord(t.body),n=asRecord(s.fingerprint??s),r=asRecord(s.context),o=asRecord(r.identity),_=asRecord(r.identifiers),a=asRecord(r.event),c={...n,full_hash:n.hash??n.full_hash??"",fingerprint_id:n.stableHash??n.stable_hash??n.fingerprint_id??"",timestamp:n.timestamp??n.collected_at??(new Date).toISOString(),isIncognito:n.isIncognito??n.is_incognito??!1,components:n.components??{},version:n.version??"unknown",..."remote"===n.source||"bundled"===n.source?{source:n.source}:{}};let d,p,u;try{const i=e.resolveUserId?e.resolveUserId(t):void 0;i&&!(0,sentinel_user_id_1.isSentinelUserId)(i)&&(d=i)}catch{}if(!d){const e="string"==typeof o.user_id?o.user_id:"string"==typeof s.user_id?s.user_id:void 0;e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(d=e)}if(!d){const e=(0,cookies_1.parseCookie)(t,"__unshared_uid");e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(d=e)}try{p=e.resolveEmailAddress?e.resolveEmailAddress(t):void 0}catch{}p=p??(0,cookies_1.parseCookie)(t,"__unshared_email")??o.email_address??s.email??void 0;try{u=e.resolveSessionId?e.resolveSessionId(t):void 0}catch{}u=u??_.session_hash??s.session_id??(0,cookies_1.parseCookie)(t,"__unshared_sid");const h=(0,client_ip_1.extractClientIp)(t),l=t.headers["user-agent"]??"";if(!e.disableBotFilter&&(0,is_bot_1.isBot)(l))return void(0,http_helpers_1.sendJson)(i,200,{success:!0,data:{skipped:!0}});let f;try{f=e.resolveDeviceId?.(t)}catch{}const y=[f,t.headers["x-device-id"],_.device_id,c.fingerprint_id,(0,cookies_1.parseCookie)(t,"__unshared_stable_hash"),(0,cookies_1.parseCookie)(t,"__unshared_fp_id")].find(e=>"string"==typeof e&&""!==e.trim()&&"unknown"!==e.trim()),g=c.fingerprint_id||void 0,m=c.full_hash||void 0,v=(0,permanent_device_id_1.resolveOrCreatePermanentDeviceId)(t,_.unshared_device_id??s.permanent_device_id),b=(0,secure_1.isSecureRequest)(t)?"; Secure":"",S=[];S.push(`__unshared_device_id=${encodeURIComponent(v.id)}; Path=/; SameSite=Lax; Max-Age=34560000${b}`);const $=(0,cookies_1.parseCookie)(t,"__unshared_fingerprint_id")||m;if($&&S.push(`__unshared_fingerprint_id=${encodeURIComponent($)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=34560000${b}`),m&&S.push(`__unshared_full_hash=${encodeURIComponent(m)}; Path=/; SameSite=Lax; Max-Age=34560000${b}`),g&&(S.push(`__unshared_fp_id=${encodeURIComponent(g)}; Path=/; SameSite=Lax; Max-Age=34560000${b}`),S.push(`__unshared_stable_hash=${encodeURIComponent(g)}; Path=/; SameSite=Lax; Max-Age=34560000${b}`)),p&&S.push(`__unshared_email=${encodeURIComponent(p)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=31536000${b}`),S.length>0){const e=i.getHeader("Set-Cookie");if(e){const t=Array.isArray(e)?[...e]:[String(e)];t.push(...S),i.setHeader("Set-Cookie",t)}else i.setHeader("Set-Cookie",S)}let k;if("string"==typeof a.type&&a.type)k=a.type;else if("string"==typeof s.event_type&&s.event_type)k=s.event_type;else{const e=t.headers.referer??t.headers.referrer;let i="unknown";if("string"==typeof e&&e.length>0)try{const t=new URL(e);i=(t.pathname||"/")+(t.search||"")}catch{}k=i}const R=t.headers["x-idempotency-key"],x="string"==typeof R&&R.length>0?R:void 0,A=Date.now(),j=x?`${x}|${A}`:g&&d?`${(0,util_1.sha256Hex)(`${g}|${d}|${k}`)}|${A}`:void 0,q={..._,unshared_device_id:v.id,...y&&"unknown"!==y?{device_id:y}:{},...g?{stable_hash:g}:{},...m?{full_hash:m}:{},...u?{session_hash:u}:{}};y||delete q.device_id;const C="string"==typeof asRecord(r.sdk).name?asRecord(r.sdk).name:void 0,w="string"==typeof C&&C.startsWith("unshared-")&&Object.prototype.hasOwnProperty.call(s,"fingerprint")&&s.context&&"object"==typeof s.context&&!Array.isArray(s.context)&&r.identity&&"object"==typeof r.identity&&!Array.isArray(r.identity)&&r.identifiers&&"object"==typeof r.identifiers&&!Array.isArray(r.identifiers)&&r.event&&"object"==typeof r.event&&!Array.isArray(r.event),I=!Object.prototype.hasOwnProperty.call(s,"context")&&"string"==typeof s.hash&&"string"==typeof s.stable_hash&&"string"==typeof s.collected_at&&s.components&&"object"==typeof s.components&&!Array.isArray(s.components),L=w||I?{...s,fingerprint:n,context:{...r,sdk:asRecord(r.sdk),identity:{...o,...d?{user_id:d}:{},...p?{email_address:p}:{}},identifiers:q,event:{...a,type:k,ip_address:h,user_agent:l}}}:s,M=await e.client.submitFingerprint(L,{idempotencyKey:j});M.success||e.onError?.(M.error,{operation:"submitFingerprintEvent",userId:d,emailAddress:p}),d&&p&&!e.rateLimitBackoff.isPaused()&&!e.dispatchDedupe.wasRecentlyDispatched(d,k)&&await(0,dispatch_processing_1.runCheckUser)({client:e.client,cache:e.verdictCache,backoff:e.rateLimitBackoff,onError:e.onError},{user_id:d,email_address:p,identifiers:q},d,p,dispatch_processing_1.SUBMIT_FP_TIMEOUT_MS),M.success?(0,http_helpers_1.sendJson)(i,200,{success:!0,data:M.data}):(0,http_helpers_1.sendJson)(i,200,{success:!1,error:M.error})}catch(t){e.onError&&e.onError(t,{operation:"submitFingerprintEvent"}),(0,http_helpers_1.sendJson)(i,200,{success:!0,data:{accepted:!1}})}}}
@@ -6,6 +6,7 @@ export interface VerificationDependencies<TReq extends UnsharedRequest = Unshare
6
6
  verdictCache: VerdictCache;
7
7
  resolveEmailAddress?: (req: TReq) => string | undefined;
8
8
  resolveDeviceId?: (req: TReq) => string | undefined;
9
+ resolveSessionId?: (req: TReq) => string | undefined;
9
10
  resolveUserId?: (req: TReq) => string | undefined;
10
11
  onError?: (error: unknown, context: {
11
12
  operation: 'processUserEvent' | 'submitFingerprintEvent' | 'checkUser' | 'verifyTrigger' | 'verify';
@@ -19,7 +20,7 @@ export interface VerificationDependencies<TReq extends UnsharedRequest = Unshare
19
20
  *
20
21
  * Body: { email: string }
21
22
  *
22
- * The deviceId is resolved via extractDeviceId (same as the middleware).
23
+ * Device and hash identifiers are resolved from the same sources as enforcement.
23
24
  */
24
25
  export declare function handleVerifyTrigger<TReq extends UnsharedRequest = UnsharedRequest>(dependencies: VerificationDependencies<TReq>): (req: TReq, res: UnsharedResponse) => Promise<void>;
25
26
  /**
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"i",{value:!0}),exports.handleVerifyTrigger=handleVerifyTrigger,exports.handleVerify=handleVerify;const cookies_1=require("../utils/cookies"),device_id_1=require("../utils/device-id"),http_helpers_1=require("../utils/http-helpers"),permanent_device_id_1=require("../utils/permanent-device-id"),secure_1=require("../utils/secure"),VERIFICATION_COOKIE="__unshared_verification_id";function appendCookie(e,i){const r=e.getHeader("Set-Cookie"),s=r?Array.isArray(r)?[...r]:[String(r)]:[];s.push(i),e.setHeader("Set-Cookie",s)}function handleVerifyTrigger(e){return async(i,r)=>{try{const s=resolveEmail(i,i.body??{},e.resolveEmailAddress);if(!s)return void(0,http_helpers_1.sendJson)(r,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}});const t=(0,device_id_1.extractDeviceId)(i,e.resolveDeviceId),c=(0,cookies_1.parseCookie)(i,"__unshared_fp_id")||void 0,o=(0,cookies_1.parseCookie)(i,"__unshared_fingerprint_id")||void 0,_=(0,permanent_device_id_1.extractPermanentDeviceId)(i),n=await e.client.triggerEmailVerification(s,t,{fingerprintId:c,fullHash:o,permanentDeviceId:_});if(n.success){if(n.data?.verification_id){const e=(0,secure_1.isSecureRequest)(i)?"; Secure":"";appendCookie(r,`${VERIFICATION_COOKIE}=${encodeURIComponent(n.data.verification_id)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=600${e}`)}(0,http_helpers_1.sendJson)(r,200,{success:!0,data:n.data})}else(0,http_helpers_1.sendJson)(r,200,{success:!1,error:n.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}})}catch(i){e.onError&&e.onError(i,{operation:"verifyTrigger"}),(0,http_helpers_1.sendJson)(r,200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}})}}}function handleVerify(e){return async(i,r)=>{try{const s=(i.body??{}).code,t=(0,cookies_1.parseCookie)(i,VERIFICATION_COOKIE);if(!t||!s)return void(0,http_helpers_1.sendJson)(r,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Verification ID and code are required"}});const c=await e.client.verifyEmail({verification_id:t,code:s});if(c.success&&c.data?.verified){let s,t=!1;try{s=e.resolveUserId?.(i)}catch{}const o=resolveEmail(i,{},e.resolveEmailAddress),_=(0,device_id_1.extractDeviceId)(i,e.resolveDeviceId),n=(0,cookies_1.parseCookie)(i,"__unshared_fp_id")||void 0,a=(0,cookies_1.parseCookie)(i,"__unshared_fingerprint_id")||void 0,d=(0,permanent_device_id_1.extractPermanentDeviceId)(i);if(s&&o){const i={..._?{device_id:_}:{},...n?{stable_hash:n}:{},...a?{full_hash:a}:{},...d?{unshared_device_id:d}:{}},r=await e.client.checkUser({user_id:s,email_address:o,identifiers:i},{maxRetries:0});r.success&&r.data&&!r.failedOpen&&!r.data.is_user_flagged&&(e.verdictCache.update(s,{isVerified:!0}),t=!0)}const u=(0,secure_1.isSecureRequest)(i)?"; Secure":"";appendCookie(r,`${VERIFICATION_COOKIE}=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0${u}`),t&&r.setHeader("X-Unshared-Enforcement-Reconciled","clean"),(0,http_helpers_1.sendJson)(r,200,{success:!0,data:c.data})}else c.success?(0,http_helpers_1.sendJson)(r,200,{success:!0,data:c.data}):(0,http_helpers_1.sendJson)(r,200,{success:!1,error:c.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}})}catch(i){e.onError&&e.onError(i,{operation:"verify"}),(0,http_helpers_1.sendJson)(r,200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}})}}}function resolveEmail(e,i,r){if(r)try{const i=r(e);if(i)return i}catch{}const s=(0,cookies_1.parseCookie)(e,"__unshared_email");if(s)return s;const t=i.email;return"string"==typeof t&&t?t:void 0}
1
+ "use strict";Object.defineProperty(exports,"i",{value:!0}),exports.handleVerifyTrigger=handleVerifyTrigger,exports.handleVerify=handleVerify;const cookies_1=require("../utils/cookies"),device_id_1=require("../utils/device-id"),http_helpers_1=require("../utils/http-helpers"),permanent_device_id_1=require("../utils/permanent-device-id"),secure_1=require("../utils/secure"),VERIFICATION_COOKIE="__unshared_verification_id";function appendCookie(e,i){const s=e.getHeader("Set-Cookie"),r=s?Array.isArray(s)?[...s]:[String(s)]:[];r.push(i),e.setHeader("Set-Cookie",r)}function handleVerifyTrigger(e){return async(i,s)=>{try{const r=resolveEmail(i,i.body??{},e.resolveEmailAddress);if(!r)return void(0,http_helpers_1.sendJson)(s,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}});const o=(0,device_id_1.extractDeviceIdOrUndefined)(i,e.resolveDeviceId),n=(0,cookies_1.parseCookie)(i,"__unshared_stable_hash")||(0,cookies_1.parseCookie)(i,"__unshared_fp_id")||void 0,t=(0,cookies_1.parseCookie)(i,"__unshared_full_hash")||(0,cookies_1.parseCookie)(i,"__unshared_fingerprint_id")||void 0,_=(0,permanent_device_id_1.extractPermanentDeviceId)(i),a=resolveSessionHash(i,e.resolveSessionId),c=await e.client.triggerEmailVerification(r,o&&"unknown"!==o?o:"",{fingerprintId:"unknown"!==n?n:void 0,fullHash:"unknown"!==t?t:void 0,permanentDeviceId:_,sessionHash:a});if(c.success){if(c.data?.verification_id){const e=(0,secure_1.isSecureRequest)(i)?"; Secure":"";appendCookie(s,`${VERIFICATION_COOKIE}=${encodeURIComponent(c.data.verification_id)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=600${e}`)}(0,http_helpers_1.sendJson)(s,200,{success:!0,data:c.data})}else(0,http_helpers_1.sendJson)(s,200,{success:!1,error:c.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}})}catch(i){e.onError&&e.onError(i,{operation:"verifyTrigger"}),(0,http_helpers_1.sendJson)(s,200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}})}}}function handleVerify(e){return async(i,s)=>{try{const r=(i.body??{}).code,o=(0,cookies_1.parseCookie)(i,VERIFICATION_COOKIE);if(!o||!r)return void(0,http_helpers_1.sendJson)(s,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Verification ID and code are required"}});const n=await e.client.verifyEmail({verification_id:o,code:r});if(n.success&&n.data?.verified){let r,o=!1;try{r=e.resolveUserId?.(i)}catch{}const t=resolveEmail(i,{},e.resolveEmailAddress),_=(0,device_id_1.extractDeviceIdOrUndefined)(i,e.resolveDeviceId),a=(0,cookies_1.parseCookie)(i,"__unshared_stable_hash")||(0,cookies_1.parseCookie)(i,"__unshared_fp_id")||void 0,c=(0,cookies_1.parseCookie)(i,"__unshared_full_hash")||(0,cookies_1.parseCookie)(i,"__unshared_fingerprint_id")||void 0,d=(0,permanent_device_id_1.extractPermanentDeviceId)(i),u=resolveSessionHash(i,e.resolveSessionId);if(r&&t){const i={..._&&"unknown"!==_?{device_id:_}:{},...a&&"unknown"!==a?{stable_hash:a}:{},...c&&"unknown"!==c?{full_hash:c}:{},...d?{unshared_device_id:d}:{},...u?{session_hash:u}:{}},s=await e.client.checkUser({user_id:r,email_address:t,identifiers:i},{maxRetries:0});s.success&&s.data&&!s.failedOpen&&!s.data.is_user_flagged&&(e.verdictCache.update(r,{isVerified:!0}),o=!0)}const h=(0,secure_1.isSecureRequest)(i)?"; Secure":"";appendCookie(s,`${VERIFICATION_COOKIE}=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0${h}`),o&&s.setHeader("X-Unshared-Enforcement-Reconciled","clean"),(0,http_helpers_1.sendJson)(s,200,{success:!0,data:n.data})}else n.success?(0,http_helpers_1.sendJson)(s,200,{success:!0,data:n.data}):(0,http_helpers_1.sendJson)(s,200,{success:!1,error:n.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}})}catch(i){e.onError&&e.onError(i,{operation:"verify"}),(0,http_helpers_1.sendJson)(s,200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}})}}}function resolveSessionHash(e,i){let s;try{s=i?.(e)}catch{}return s=s||(0,cookies_1.parseCookie)(e,"__unshared_sid"),s&&"unknown"!==s?s:void 0}function resolveEmail(e,i,s){if(s)try{const i=s(e);if(i)return i}catch{}const r=(0,cookies_1.parseCookie)(e,"__unshared_email");if(r)return r;const o=i.email;return"string"==typeof o&&o?o:void 0}
@@ -1,6 +1,7 @@
1
1
  import type { UnsharedRequest } from '../../types';
2
2
  /**
3
- * Resolves device ID from: custom resolver → X-Device-Id header → __unshared_fp_id cookie.
3
+ * Resolves device ID from the custom resolver, X-Device-Id header, then the
4
+ * canonical __unshared_stable_hash cookie or legacy __unshared_fp_id cookie.
4
5
  *
5
6
  * Returns the literal string `"unknown"` when no source provides a value.
6
7
  * Callers that can tolerate a tri-state should use `extractDeviceIdOrUndefined`
@@ -11,7 +12,8 @@ import type { UnsharedRequest } from '../../types';
11
12
  */
12
13
  export declare function extractDeviceId<TReq extends UnsharedRequest = UnsharedRequest>(req: TReq, resolveDeviceId?: (req: TReq) => string | undefined): string;
13
14
  /**
14
- * Resolves device ID from: custom resolver → X-Device-Id header → __unshared_fp_id cookie.
15
+ * Resolves device ID from the custom resolver, X-Device-Id header, then the
16
+ * canonical __unshared_stable_hash cookie or legacy __unshared_fp_id cookie.
15
17
  * Returns `undefined` when nothing is available. Use this at any call site
16
18
  * that can take a "skip dispatch" branch during the bootstrap window, so we
17
19
  * stop writing `device_id="unknown"` rows to the analytics table on first request.
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.extractDeviceId=extractDeviceId,exports.extractDeviceIdOrUndefined=extractDeviceIdOrUndefined;const cookies_1=require("./cookies");function extractDeviceId(e,t){return extractDeviceIdOrUndefined(e,t)??"unknown"}function extractDeviceIdOrUndefined(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=e.headers["x-device-id"];if("string"==typeof r&&r)return r;return(0,cookies_1.parseCookie)(e,"__unshared_fp_id")||void 0}
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.extractDeviceId=extractDeviceId,exports.extractDeviceIdOrUndefined=extractDeviceIdOrUndefined;const cookies_1=require("./cookies");function extractDeviceId(e,t){return extractDeviceIdOrUndefined(e,t)??"unknown"}function extractDeviceIdOrUndefined(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=e.headers["x-device-id"];if("string"==typeof r&&r)return r;return(0,cookies_1.parseCookie)(e,"__unshared_stable_hash")||(0,cookies_1.parseCookie)(e,"__unshared_fp_id")||void 0}
@@ -49,7 +49,7 @@ interface InjectWarnState {
49
49
  * single middleware instance; when omitted, the module-level default is used.
50
50
  */
51
51
  declare function injectIntoHtmlResponse(response: Response, fingerprintScript: string, extraCookies?: string[], warnState?: InjectWarnState): Promise<Response>;
52
- declare function fetchAndCacheVerdict(client: UnsharedClient, cache: VerdictCache, userId: string, emailAddress: string, deviceId: string, fingerprintId: string | undefined, permanentDeviceId: string | undefined, sessionId: string, timeoutMs?: number, reportFailOpen?: (reason: 'timeout' | 'http_error' | 'no_device_id', status?: number) => void): Promise<Verdict>;
52
+ declare function fetchAndCacheVerdict(client: UnsharedClient, cache: VerdictCache, userId: string, emailAddress: string, deviceId: string | undefined, fingerprintId: string | undefined, fullHash: string | undefined, permanentDeviceId: string | undefined, sessionId: string, timeoutMs?: number, reportFailOpen?: (reason: 'timeout' | 'http_error' | 'no_device_id', status?: number) => void): Promise<Verdict>;
53
53
  export declare const __test_fetchAndCacheVerdict: typeof fetchAndCacheVerdict;
54
54
  export declare const __test_injectIntoHtmlResponse: typeof injectIntoHtmlResponse;
55
55
  export declare function __test_resetInjectWarning(): void;