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

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 (92) hide show
  1. package/README.md +176 -151
  2. package/dist/client.d.ts +58 -24
  3. package/dist/client.js +1 -1
  4. package/dist/esm/client.d.mts +58 -24
  5. package/dist/esm/client.mjs +1 -1
  6. package/dist/esm/index.d.mts +8 -8
  7. package/dist/esm/index.mjs +1 -1
  8. package/dist/esm/middleware/dispatch-dedupe.d.mts +1 -0
  9. package/dist/esm/middleware/dispatch-dedupe.mjs +1 -1
  10. package/dist/esm/middleware/dispatch-processing.d.mts +25 -0
  11. package/dist/esm/middleware/dispatch-processing.mjs +1 -0
  12. package/dist/esm/middleware/index.d.mts +19 -5
  13. package/dist/esm/middleware/index.mjs +1 -1
  14. package/dist/esm/middleware/injection/fingerprint-script.d.mts +7 -0
  15. package/dist/esm/middleware/injection/fingerprint-script.mjs +1 -1
  16. package/dist/esm/middleware/injection/fp-bundle-source.d.mts +5 -4
  17. package/dist/esm/middleware/injection/fp-bundle-source.mjs +1 -1
  18. package/dist/esm/middleware/rate-limit-backoff.d.mts +11 -8
  19. package/dist/esm/middleware/rate-limit-backoff.mjs +1 -1
  20. package/dist/esm/middleware/response-interceptor.d.mts +2 -1
  21. package/dist/esm/middleware/response-interceptor.mjs +1 -1
  22. package/dist/esm/middleware/routes/interstitial.d.mts +2 -2
  23. package/dist/esm/middleware/routes/interstitial.mjs +1 -1
  24. package/dist/esm/middleware/routes/submit-fp.d.mts +5 -5
  25. package/dist/esm/middleware/routes/submit-fp.mjs +1 -1
  26. package/dist/esm/middleware/routes/verify.d.mts +3 -3
  27. package/dist/esm/middleware/routes/verify.mjs +1 -1
  28. package/dist/esm/middleware/utils/client-ip.d.mts +1 -1
  29. package/dist/esm/middleware/utils/cookies.d.mts +1 -1
  30. package/dist/esm/middleware/utils/device-id.d.mts +1 -1
  31. package/dist/esm/middleware/utils/device-id.mjs +1 -1
  32. package/dist/esm/middleware/utils/flagged-response.mjs +1 -1
  33. package/dist/esm/middleware/utils/http-helpers.d.mts +1 -1
  34. package/dist/esm/middleware/utils/permanent-device-id.d.mts +7 -0
  35. package/dist/esm/middleware/utils/permanent-device-id.mjs +1 -0
  36. package/dist/esm/middleware/utils/read-json-body.d.mts +3 -0
  37. package/dist/esm/middleware/utils/read-json-body.mjs +1 -0
  38. package/dist/esm/middleware/utils/secure.d.mts +1 -1
  39. package/dist/esm/middleware/utils/skip-paths.d.mts +5 -1
  40. package/dist/esm/middleware/utils/skip-paths.mjs +1 -1
  41. package/dist/esm/middleware/verdict-cache.d.mts +8 -1
  42. package/dist/esm/middleware/verdict-cache.mjs +1 -1
  43. package/dist/esm/middleware.d.mts +5 -6
  44. package/dist/esm/middleware.mjs +1 -1
  45. package/dist/esm/shared-types.d.mts +148 -0
  46. package/dist/esm/web/index.d.mts +6 -6
  47. package/dist/esm/web/index.mjs +1 -1
  48. package/dist/esm/web/protection-handler.d.mts +31 -3
  49. package/dist/esm/web/protection-handler.mjs +1 -1
  50. package/dist/esm/web/submit-handler.d.mts +2 -2
  51. package/dist/esm/web/submit-handler.mjs +1 -1
  52. package/dist/esm/web/types.d.mts +12 -2
  53. package/dist/esm/web/web-helpers.d.mts +11 -0
  54. package/dist/esm/web/web-helpers.mjs +1 -1
  55. package/dist/esm/webcrypto.d.mts +8 -0
  56. package/dist/esm/webcrypto.mjs +1 -0
  57. package/dist/middleware/dispatch-dedupe.d.ts +1 -0
  58. package/dist/middleware/dispatch-dedupe.js +1 -1
  59. package/dist/middleware/dispatch-processing.d.ts +25 -0
  60. package/dist/middleware/dispatch-processing.js +1 -0
  61. package/dist/middleware/index.d.ts +14 -0
  62. package/dist/middleware/index.js +1 -1
  63. package/dist/middleware/injection/fingerprint-script.d.ts +7 -0
  64. package/dist/middleware/injection/fingerprint-script.js +1 -1
  65. package/dist/middleware/injection/fp-bundle-source.d.ts +5 -4
  66. package/dist/middleware/injection/fp-bundle-source.js +1 -1
  67. package/dist/middleware/rate-limit-backoff.d.ts +11 -8
  68. package/dist/middleware/rate-limit-backoff.js +1 -1
  69. package/dist/middleware/response-interceptor.d.ts +1 -0
  70. package/dist/middleware/response-interceptor.js +1 -1
  71. package/dist/middleware/routes/submit-fp.js +1 -1
  72. package/dist/middleware/routes/verify.js +1 -1
  73. package/dist/middleware/utils/permanent-device-id.d.ts +7 -0
  74. package/dist/middleware/utils/permanent-device-id.js +1 -0
  75. package/dist/middleware/utils/read-json-body.d.ts +3 -0
  76. package/dist/middleware/utils/read-json-body.js +1 -0
  77. package/dist/middleware/utils/skip-paths.d.ts +5 -1
  78. package/dist/middleware/utils/skip-paths.js +1 -1
  79. package/dist/middleware/verdict-cache.d.ts +8 -1
  80. package/dist/middleware/verdict-cache.js +1 -1
  81. package/dist/middleware.d.ts +3 -4
  82. package/dist/middleware.js +1 -1
  83. package/dist/shared-types.d.ts +148 -0
  84. package/dist/web/protection-handler.d.ts +29 -1
  85. package/dist/web/protection-handler.js +1 -1
  86. package/dist/web/submit-handler.js +1 -1
  87. package/dist/web/types.d.ts +11 -1
  88. package/dist/web/web-helpers.d.ts +11 -0
  89. package/dist/web/web-helpers.js +1 -1
  90. package/dist/webcrypto.d.ts +8 -0
  91. package/dist/webcrypto.js +1 -0
  92. package/package.json +5 -5
@@ -1 +1 @@
1
- import{sha256Hex}from"../util";import{VerdictCache}from"../middleware/verdict-cache";import{RateLimitBackoff}from"../middleware/rate-limit-backoff";import{DispatchDedupe}from"../middleware/dispatch-dedupe";import{generateFingerprintScript}from"../middleware/injection/fingerprint-script";import{generateGatePage}from"../middleware/injection/gate-page";import{flaggedResponse}from"../middleware/utils/flagged-response";import{maskEmail}from"../middleware/utils/mask-email";import{isHtmlContentType,isHtmlNavigation}from"../middleware/utils/content-type";import{shouldSkipPath}from"../middleware/utils/skip-paths";import{shouldIncludePath}from"../middleware/utils/include-path";import{isBot}from"../middleware/utils/is-bot";import{isSentinelUserId,SENTINEL_STICKINESS_TTL_MS}from"../middleware/utils/sentinel-user-id";import{parseCookieFromRequest,extractClientIpFromRequest,extractDeviceIdFromRequest,extractDeviceIdFromRequestOrUnknown,isSecureWebRequest,jsonResponse,emptyResponse,bodyResponse,mergeResponseHeaders}from"./web-helpers";const CHECK_USER_TIMEOUT_MS=1500,FAIL_OPEN_TTL_MS=5e3;export function createWebProtectionMiddleware(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");const{userId:s,emailAddress:r,routePrefix:n="/__unshared",corsOrigins:i,cacheTTL:o=6e4,skipPaths:a,includePathPrefix:d,sessionId:c,deviceId:u,fingerprintSdkBundle:l="",onFlagged:p,onError:m,onFailOpen:f,disableBotFilter:h=!1,checkUserTimeoutMs:_=CHECK_USER_TIMEOUT_MS,blockFlagged:g=!1,flaggedMode:R,autoInterstitial:I=!1,interstitialFlowType:v="email_verification"}=t,S=R??(g?"gate":void 0),y=I||"overlay"===S,w=e=>{if(f)try{f(e)}catch{}},E=Date.now().toString(36),C=void 0!==S||y,A="gate"===S?"flaggedMode 'gate' (blockFlagged)":"overlay"===S?"flaggedMode 'overlay'":"autoInterstitial";if(C&&!l)throw new Error(`[Unshared] ${A} requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the interstitial).`);if(C&&"/__unshared"!==n)throw new Error(`[Unshared] ${A} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const k="gate"===S?generateGatePage(n,E):"",T=new VerdictCache(o),F=new RateLimitBackoff,x=new DispatchDedupe,U=generateFingerprintScript(n,E,{autoInterstitial:y,interstitialFlowType:v}),q=`${n}/fp.js`,D=`${n}/submit-fp`,O=`${n}/verify-trigger`,j=`${n}/verify`,P=`${n}/status`,b=i?Array.isArray(i)?i:[i]:null;return async function(t,i){let o,f,g;try{const e=new URL(t.url);o=e.pathname,f=e.search}catch{return i(t)}if(o.startsWith(n+"/")){const n=function(e){if(!b)return{};const t=e.headers.get("origin")??"",s=b.includes("*");if(!s&&!b.includes(t))return{};const r={"Access-Control-Allow-Origin":s?"*":t,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"};return s||(r["Access-Control-Allow-Credentials"]="true"),r}(t);if("OPTIONS"===t.method)return emptyResponse(204,n);if(("GET"===t.method||"HEAD"===t.method)&&o===q){if(!l)return jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},n);const e={...n,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600","Content-Length":String((new TextEncoder).encode(l).byteLength)};return"HEAD"===t.method?new Response(null,{status:200,headers:new Headers(e)}):bodyResponse(200,l,e)}if("POST"===t.method&&(o===D||o===O||o===j)){let i;try{i=await t.json()}catch{return jsonResponse(400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"Request body is not valid JSON."}},n)}return o===D?handleSubmitFp(t,i,{client:e,verdictCache:T,rateLimitBackoff:F,dispatchDedupe:x,resolveUserId:s,resolveEmailAddress:r,resolveSessionId:c,resolveDeviceId:u,disableBotFilter:h,onError:m},n):o===O?handleVerifyTriggerWeb(t,i,{client:e,verdictCache:T,resolveUserId:s,resolveEmailAddress:r,resolveDeviceId:u,onError:m},n):handleVerifyWeb(t,i,{client:e,verdictCache:T,resolveUserId:s,resolveEmailAddress:r,resolveDeviceId:u,onError:m},n)}if("GET"===t.method&&o===P){let i;try{i=s(t)}catch{}if(!i)return jsonResponse(200,{status:"anonymous"},n);const o=resolveEmail(t,r);let a=T.get(i);if((!a||T.isStale(i))&&o&&!F.isPaused()&&!T.isRefreshing(i)){T.markRefreshing(i);try{const s=extractDeviceIdFromRequest(t,u),r=parseCookieFromRequest(t,"__unshared_fingerprint_id")||void 0,n=extractSessionIdFromRequest(t,c),d=s??r??"unknown";await fetchAndCacheVerdict(e,T,i,o,d,r,n,_,(e,t)=>w({operation:"checkUser",reason:e,status:t,userId:i,emailAddress:o})),a=T.get(i)}catch(e){m&&m(e,{operation:"checkUser",userId:i,emailAddress:o}),w({operation:"checkUser",reason:"exception",userId:i,emailAddress:o})}finally{T.clearRefreshing(i)}}return a&&a.isFlagged&&!a.isVerified&&(void 0!==S||p)&&o?jsonResponse(200,{status:"flagged",email:maskEmail(o)},n):jsonResponse(200,{status:"ok"},n)}return jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},n)}if(shouldSkipPath(o,a))return i(t);if(!shouldIncludePath(o,d))return injectIntoHtmlResponse(await i(t),U);try{g=s(t)}catch{}if(isSentinelUserId(g)){const e=parseCookieFromRequest(t,"__unshared_uid"),s=parseCookieFromRequest(t,"__unshared_uid_at"),r=s?Number(s):NaN,n=Number.isFinite(r)&&Date.now()-r<=SENTINEL_STICKINESS_TTL_MS;g=e&&n?e:void 0}if(!g){const e=isSecureWebRequest(t)?"; Secure":"",s=[`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`];return injectIntoHtmlResponse(await i(t),U,s)}const R=resolveEmail(t,r),I=[],v=isSecureWebRequest(t)?"; Secure":"";if(I.push(`__unshared_uid=${encodeURIComponent(g)}; Path=/; SameSite=Lax${v}`),I.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${v}`),R&&I.push(`__unshared_email=${encodeURIComponent(R)}; HttpOnly; Path=/; SameSite=Lax${v}`),!R)return injectIntoHtmlResponse(await i(t),U,I);const y=extractSessionIdFromRequest(t,c),E=extractDeviceIdFromRequest(t,u),C=parseCookieFromRequest(t,"__unshared_fingerprint_id")||void 0,A=t.headers.get("user-agent")??"",H=extractClientIpFromRequest(t),L=E??C;if(!h&&isBot(A))return i(t);let N=T.get(g);if(N)T.isStale(g)&&!T.isRefreshing(g)&&(T.markRefreshing(g),fetchAndCacheVerdict(e,T,g,R,L??"unknown",C,y,_,(e,t)=>w({operation:"checkUser",reason:e,status:t,userId:g,emailAddress:R})).catch(()=>w({operation:"checkUser",reason:"exception",userId:g,emailAddress:R})).finally(()=>T.clearRefreshing(g)));else try{N=await fetchAndCacheVerdict(e,T,g,R,L??"unknown",C,y,_,(e,t)=>w({operation:"checkUser",reason:e,status:t,userId:g,emailAddress:R}))}catch{return w({operation:"checkUser",reason:"exception",userId:g,emailAddress:R}),injectIntoHtmlResponse(await i(t),U,I)}const $=N.isFlagged&&!N.isVerified;if("gate"===S&&$)return isHtmlNavigation(t.method,t.headers.get("accept")??void 0)?bodyResponse(200,k,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}):jsonResponse(403,flaggedResponse(R));if("overlay"===S&&$)return isHtmlNavigation(t.method,t.headers.get("accept")??void 0)?injectIntoHtmlResponse(await i(t),U,I):jsonResponse(403,flaggedResponse(R));if(N.isFlagged&&!N.isVerified&&p)try{const e=await p({userId:g,emailAddress:R,verdict:N,request:t});if(e)return injectIntoHtmlResponse(e,U,I)}catch(e){m&&m(e,{operation:"checkUser",userId:g,emailAddress:R})}return N.isFlagged||"unknown"===y||!L||F.isPaused()||dispatchUserEvent(e,T,F,x,{userId:g,emailAddress:R,sessionId:y,deviceId:L,fingerprintId:C,userAgent:A,ipAddress:H,eventType:o+f},m),injectIntoHtmlResponse(await i(t),U,I)}}async function injectIntoHtmlResponse(e,t,s){const r=e.headers.get("content-type");if(!isHtmlContentType(r??void 0)){if(!s||0===s.length)return e;const t=mergeResponseHeaders(e.headers,void 0,s);return new Response(e.body,{status:e.status,statusText:e.statusText,headers:t})}const n=await e.text(),i=n.lastIndexOf("</body>"),o=-1===i?n+t:n.slice(0,i)+t+n.slice(i),a=mergeResponseHeaders(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(o).length)},s);return a.delete("ETag"),a.delete("Last-Modified"),a.delete("Content-Encoding"),new Response(o,{status:e.status,statusText:e.statusText,headers:a})}function resolveEmail(e,t){if(t)try{const s=t(e);if(s)return s}catch{}const s=parseCookieFromRequest(e,"__unshared_email");if(s)return s}function resolveEmailWithBody(e,t,s){const r=resolveEmail(e,s);if(r)return r;const n=t.email;return"string"==typeof n&&n?n:void 0}function extractSessionIdFromRequest(e,t){if(t)try{const s=t(e);if(s)return s}catch{}return parseCookieFromRequest(e,"__unshared_sid")??"unknown"}function dispatchUserEvent(e,t,s,r,n,i){r.mark(n.userId,n.eventType),e.processUserEvent({eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,sessionHash:n.sessionId,userAgent:n.userAgent}).then(e=>{e.success&&e.data?.analysis&&t.update(n.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&s.pause(1e3*e.error.retryAfter)}).catch(e=>{i&&i(e,{operation:"processUserEvent",userId:n.userId,emailAddress:n.emailAddress})})}async function fetchAndCacheVerdict(e,t,s,r,n,i,o,a=CHECK_USER_TIMEOUT_MS,d){const c=()=>{const e=t.get(s);return t.set(s,{isFlagged:e?.isFlagged??!1,isVerified:e?.isVerified??!1,emailAddress:r,sessionId:o},5e3),t.get(s)},u={};let l;n&&"unknown"!==n&&(u.deviceId=n),i&&(u.fingerprintId=i);const p=await Promise.race([e.checkUser(r,u),new Promise(e=>{l=setTimeout(()=>e(null),a)})]);return clearTimeout(l),p?p.failedOpen?(d?.(p.failedOpen.reason??"http_error",p.failedOpen.status),c()):(t.set(s,{isFlagged:p.data?.is_user_flagged??!1,isVerified:!1,emailAddress:r,sessionId:o}),t.get(s)):(d?.("timeout"),c())}async function handleSubmitFp(e,t,s,r){try{const n={full_hash:t.hash??"",fingerprint_id:t.stable_hash??"",timestamp:t.collected_at??(new Date).toISOString(),isIncognito:t.is_incognito??!1,components:t.components??{},version:t.version??"inline-1.0.0"};let i,o,a;try{const t=s.resolveUserId(e);t&&!isSentinelUserId(t)&&(i=t)}catch{}if(!i){const e="string"==typeof t.user_id?t.user_id:void 0;e&&!isSentinelUserId(e)&&(i=e)}if(!i){const t=parseCookieFromRequest(e,"__unshared_uid");t&&!isSentinelUserId(t)&&(i=t)}try{o=s.resolveEmailAddress?s.resolveEmailAddress(e):void 0}catch{}o=o??parseCookieFromRequest(e,"__unshared_email")??t.email??void 0;try{a=s.resolveSessionId?s.resolveSessionId(e):void 0}catch{}a=a??t.session_id??parseCookieFromRequest(e,"__unshared_sid");const d=extractClientIpFromRequest(e),c=e.headers.get("user-agent")??"";if(!s.disableBotFilter&&isBot(c))return jsonResponse(200,{success:!0},r);const u=(n.fingerprint_id&&n.fingerprint_id.length>0?n.fingerprint_id:void 0)??extractDeviceIdFromRequestOrUnknown(e,s.resolveDeviceId),l=n.fingerprint_id||void 0,p=n.full_hash||void 0,m=isSecureWebRequest(e)?"; Secure":"",f=[];if(p&&!parseCookieFromRequest(e,"__unshared_fingerprint_id")&&f.push(`__unshared_fingerprint_id=${encodeURIComponent(p)}; HttpOnly; Path=/; SameSite=Lax${m}`),l){const t=parseCookieFromRequest(e,"__unshared_fp_id");t&&t===l||f.push(`__unshared_fp_id=${encodeURIComponent(l)}; Path=/; SameSite=Lax; Max-Age=31536000${m}`)}let h;if(o&&!parseCookieFromRequest(e,"__unshared_email")&&f.push(`__unshared_email=${encodeURIComponent(o)}; HttpOnly; Path=/; SameSite=Lax${m}`),"string"==typeof t.event_type&&t.event_type)h=t.event_type;else{const t=e.headers.get("referer")??e.headers.get("referrer");let s="unknown";if(t)try{const e=new URL(t);s=(e.pathname||"/")+(e.search||"")}catch{}h=s}const _=e.headers.get("x-idempotency-key")||void 0,g=Date.now(),R=_?`${_}|${g}`:l&&i?`${sha256Hex(`${l}|${i}|${h}`)}|${g}`:void 0;i&&s.client.submitFingerprintEvent(n,{userId:i,emailAddress:o,sessionHash:a,eventType:h,ipAddress:d,userAgent:c,idempotencyKey:R}).catch(e=>{s.onError&&s.onError(e,{operation:"submitFingerprintEvent",userId:i,emailAddress:o})}),i&&o&&!s.rateLimitBackoff.isPaused()&&!s.dispatchDedupe.wasRecentlyDispatched(i,h)&&s.client.processUserEvent({eventType:h,userId:i,emailAddress:o,ipAddress:d,deviceId:u,fingerprintId:l,sessionHash:a??"unknown",userAgent:c}).then(e=>{e.success&&e.data?.analysis&&s.verdictCache.update(i,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&s.rateLimitBackoff.pause(1e3*e.error.retryAfter)}).catch(e=>{s.onError&&s.onError(e,{operation:"processUserEvent",userId:i,emailAddress:o})});const I={...r,"Content-Type":"application/json"},v=new Response(JSON.stringify({success:!0}),{status:200,headers:I});for(const e of f)v.headers.append("Set-Cookie",e);return v}catch{return jsonResponse(200,{success:!0},r)}}async function handleVerifyTriggerWeb(e,t,s,r){try{const n=resolveEmailWithBody(e,t??{},s.resolveEmailAddress);if(!n)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},r);const i=extractDeviceIdFromRequestOrUnknown(e,s.resolveDeviceId),o=parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,a=await s.client.triggerEmailVerification(n,i,{fingerprintId:o});return a.success?jsonResponse(200,{success:!0,data:a.data},r):jsonResponse(200,{success:!1,error:a.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},r)}catch(e){return s.onError&&s.onError(e,{operation:"verifyTrigger"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},r)}}async function handleVerifyWeb(e,t,s,r){try{const n=resolveEmailWithBody(e,t??{},s.resolveEmailAddress),i=t?.code;if(!n||!i)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}},r);const o=extractDeviceIdFromRequestOrUnknown(e,s.resolveDeviceId),a=parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,d=await s.client.verify(n,o,i,{fingerprintId:a});if(d.success){let t;try{t=s.resolveUserId(e)}catch{}return t&&s.verdictCache.update(t,{isVerified:!0}),jsonResponse(200,{success:!0,data:{verified:!0}},r)}return jsonResponse(200,{success:!1,error:d.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}},r)}catch(e){return s.onError&&s.onError(e,{operation:"verify"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},r)}}
1
+ import{sha256HexWeb}from"../webcrypto.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,5 +1,5 @@
1
- import type { UnsharedClient } from '../client';
2
- import type { WebSubmitOptions } from './types';
1
+ import type { UnsharedClient } from '../client.mjs';
2
+ import type { WebSubmitOptions } from './types.mjs';
3
3
  /**
4
4
  * Web Standard equivalent of `createUnsharedMiddleware` from src/middleware.ts.
5
5
  *
@@ -1 +1 @@
1
- import{jsonResponse,emptyResponse,extractClientIpFromRequest}from"./web-helpers";export function createWebSubmitHandler(e,s){const{userIdExtractor:r,eventTypeExtractor:n,sessionIdExtractor:o,ipAddressExtractor:t,defaultEventType:c="browser_event",routePrefix:i="/unshared",corsOrigins:a}=s??{},d=`${i}/submit-fingerprint-event`,u=a?Array.isArray(a)?a:[a]:null;return async s=>{let i;try{i=new URL(s.url).pathname}catch{return jsonResponse(400,{success:!1,error:{code:"INVALID_URL",message:"Unable to parse request URL"}})}const a=u&&i===d?function(e){if(!u)return{};const s=e.headers.get("origin")??"",r=u.includes("*");return r||u.includes(s)?{"Access-Control-Allow-Origin":r?"*":s,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id"}:{}}(s):{};if("OPTIONS"===s.method&&i===d)return emptyResponse(204,a);if("POST"!==s.method||i!==d)return jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},a);try{let i;try{i=await s.json()}catch{return jsonResponse(400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"Request body is not valid JSON."}},a)}if(!(i&&i.hash&&i.stable_hash&&i.collected_at))return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Missing required fingerprint fields: hash, stable_hash, collected_at"}},a);if(!s.headers.get("x-session-id"))return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Missing required header: X-Session-Id"}},a);const d={full_hash:i.hash,fingerprint_id:i.stable_hash,timestamp:i.collected_at,isIncognito:i.is_incognito??!1,components:i.components??{},version:i.version??"unknown"};let u,R,p,l;try{u=(r?r(s):void 0)??i.user_id}catch{u=i.user_id}if(!u)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Missing required field: user_id"}},a);try{R=(n?n(s):void 0)??i.event_type??c}catch{R=i.event_type??c}try{p=(o?o(s):void 0)??s.headers.get("x-session-id")??i.session_id}catch{p=s.headers.get("x-session-id")??i.session_id}const I=extractClientIpFromRequest(s);try{l=(t?t(s):void 0)??I}catch{l=I}const m=await e.submitFingerprintEvent(d,{userId:u,sessionHash:p,eventType:R,ipAddress:l});return m.success?jsonResponse(202,{success:!0,data:m.data},a):jsonResponse(200,{success:!1,error:{code:"UPSTREAM_ERROR",message:m.error?.message??"Upstream request failed"}},a)}catch(e){return jsonResponse(200,{success:!1,error:{code:"MIDDLEWARE_ERROR",message:e instanceof Error?e.message:"Handler error"}},a)}}}
1
+ import{jsonResponse,emptyResponse,extractClientIpFromRequest,readJsonRequest}from"./web-helpers.mjs";export function createWebSubmitHandler(e,t){const{userIdExtractor:s,eventTypeExtractor:r,sessionIdExtractor:o,ipAddressExtractor:n,defaultEventType:c="browser_event",routePrefix:i="/unshared",corsOrigins:a}=t??{},p=`${i}/submit-fingerprint-event`,d=a?Array.isArray(a)?a:[a]:null;return async t=>{let i;try{i=new URL(t.url).pathname}catch{return jsonResponse(400,{success:!1,error:{code:"INVALID_URL",message:"Unable to parse request URL"}})}const a=d&&i===p?function(e){if(!d)return{};const t=e.headers.get("origin")??"",s=d.includes("*");return s||d.includes(t)?{"Access-Control-Allow-Origin":s?"*":t,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Permanent-Device-Id"}:{}}(t):{};if("OPTIONS"===t.method&&i===p)return emptyResponse(204,a);if("POST"!==t.method||i!==p)return jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},a);try{const i=await readJsonRequest(t);if(!i.success)return jsonResponse(i.status,{success:!1,error:i.error},a);const p=i.data;if(!p||"object"!=typeof p||Array.isArray(p))return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Request body must be a JSON object"}},a);const d=p.context&&"object"==typeof p.context?p.context:{},y=d.identity&&"object"==typeof d.identity?d.identity:{},u=d.identifiers&&"object"==typeof d.identifiers?d.identifiers:{},f=d.event&&"object"==typeof d.event?d.event:{};let R,j,b,l;try{R=(s?s(t):void 0)??y.user_id}catch{R=y.user_id}try{j=(r?r(t):void 0)??f.type??c}catch{j=f.type??c}try{b=(o?o(t):void 0)??t.headers.get("x-session-id")??u.session_hash}catch{b=t.headers.get("x-session-id")??u.session_hash}const m=extractClientIpFromRequest(t);try{l=(n?n(t):void 0)??m}catch{l=m}const A=t.headers.get("x-permanent-device-id")??u.unshared_device_id,O=d.sdk&&"object"==typeof d.sdk?d.sdk.name:void 0,g="string"==typeof O&&O.startsWith("unshared-")&&p.fingerprint&&"object"==typeof p.fingerprint&&p.context&&"object"==typeof p.context&&!Array.isArray(p.context)&&p.context.identity&&"object"==typeof p.context.identity&&!Array.isArray(p.context.identity)&&p.context.identifiers&&"object"==typeof p.context.identifiers&&!Array.isArray(p.context.identifiers)&&p.context.event&&"object"==typeof p.context.event&&!Array.isArray(p.context.event),v=!Object.prototype.hasOwnProperty.call(p,"context")&&"string"==typeof p.hash&&"string"==typeof p.stable_hash&&"string"==typeof p.collected_at&&p.components&&"object"==typeof p.components&&!Array.isArray(p.components),x=g||v?{...p,context:{...d,identity:{...y,...R?{user_id:R}:{}},identifiers:{...u,...b?{session_hash:b}:{},...A?{unshared_device_id:A}:{}},event:{...f,type:j,...l?{ip_address:l}:{},user_agent:t.headers.get("user-agent")??""}}}:p,I=await e.submitFingerprint(x,{idempotencyKey:t.headers.get("x-idempotency-key")??void 0});return I.success?jsonResponse(202,{success:!0,data:I.data},a):jsonResponse(200,{success:!1,error:I.error??{code:"UPSTREAM_ERROR",message:"Upstream request failed"}},a)}catch(e){return jsonResponse(200,{success:!1,error:{code:"MIDDLEWARE_ERROR",message:e instanceof Error?e.message:"Handler error"}},a)}}}
@@ -8,11 +8,21 @@
8
8
  * The DOM lib reference above scopes the Fetch API globals to this file only,
9
9
  * avoiding `window`/`document` pollution in the Node.js middleware code.
10
10
  */
11
- import type { Verdict } from '../middleware/verdict-cache';
11
+ import type { Verdict } from '../middleware/verdict-cache.mjs';
12
12
  /** A downstream handler that returns a Response. */
13
13
  export type WebHandler = (request: Request) => Response | Promise<Response>;
14
14
  /** Middleware that wraps a downstream handler, optionally intercepting the response. */
15
- export type WebMiddleware = (request: Request, next: WebHandler) => Response | Promise<Response>;
15
+ export type WebMiddleware = (request: Request, next: WebHandler,
16
+ /**
17
+ * Platform background-work hook (e.g. Cloudflare Workers / Vercel Edge
18
+ * `ExecutionContext.waitUntil`). When provided, background promises (SWR
19
+ * verdict refresh, event dispatch) are registered through it so the
20
+ * runtime keeps them alive past the response — otherwise they are killed
21
+ * at response end on edge runtimes.
22
+ */
23
+ ctx?: {
24
+ waitUntil?: (p: Promise<unknown>) => void;
25
+ }) => Response | Promise<Response>;
16
26
  export interface WebSubmitOptions {
17
27
  /** Override userId extractor. Falls back to body.user_id. */
18
28
  userIdExtractor?: (req: Request) => string | undefined;
@@ -32,6 +32,17 @@ export declare function extractDeviceIdFromRequestOrUnknown(request: Request, re
32
32
  * 2. The URL protocol (for direct HTTPS requests)
33
33
  */
34
34
  export declare function isSecureWebRequest(request: Request): boolean;
35
+ export declare function readJsonRequest(request: Request, maxBytes?: number): Promise<{
36
+ success: true;
37
+ data: unknown;
38
+ } | {
39
+ success: false;
40
+ status: 400 | 413;
41
+ error: {
42
+ code: string;
43
+ message: string;
44
+ };
45
+ }>;
35
46
  /**
36
47
  * Build a JSON response. Mirrors `sendJson(res, status, data)` in
37
48
  * src/middleware/utils/http-helpers.ts, but returns a new Response instead
@@ -1 +1 @@
1
- export function parseCookieFromRequest(e,t){const o=e.headers.get("cookie");if(!o)return;const n=o.match(new RegExp(`(?:^|; )${t}=([^;]*)`));return n?decodeURIComponent(n[1]):void 0}export function extractClientIpFromRequest(e){const t=e.headers.get("cf-connecting-ip");if(t)return t;const o=e.headers.get("x-real-ip");if(o)return o;const n=e.headers.get("x-forwarded-for");if(n){const e=n.split(",")[0]?.trim();if(e)return e}return""}export function extractDeviceIdFromRequest(e,t){if(t)try{const o=t(e);if(o)return o}catch{}const o=e.headers.get("x-device-id");if(o)return o;return parseCookieFromRequest(e,"__unshared_fp_id")||void 0}export function extractDeviceIdFromRequestOrUnknown(e,t){return extractDeviceIdFromRequest(e,t)??"unknown"}export function isSecureWebRequest(e){if("https"===e.headers.get("x-forwarded-proto"))return!0;try{return"https:"===new URL(e.url).protocol}catch{return!1}}export function jsonResponse(e,t,o){const n=new Headers(o);return n.set("Content-Type","application/json"),new Response(JSON.stringify(t),{status:e,headers:n})}export function emptyResponse(e,t){return new Response(null,{status:e,headers:new Headers(t)})}export function bodyResponse(e,t,o){return new Response(t,{status:e,headers:new Headers(o)})}export function appendCookieToHeaders(e,t){e.append("Set-Cookie",t)}export function mergeResponseHeaders(e,t,o){const n=new Headers;e.forEach((e,t)=>{"set-cookie"!==t.toLowerCase()&&n.set(t,e)});const r="function"==typeof e.getSetCookie?e.getSetCookie():[];for(const e of r)n.append("Set-Cookie",e);if(o)for(const e of o)n.append("Set-Cookie",e);if(t)for(const[e,o]of Object.entries(t))n.set(e,o);return n}
1
+ export function parseCookieFromRequest(e,t){const 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}
@@ -0,0 +1,8 @@
1
+ export declare function sha256Bytes(input: string): Promise<Uint8Array>;
2
+ export declare function sha256HexWeb(input: string): Promise<string>;
3
+ /**
4
+ * Import raw key bytes as an AES-GCM CryptoKey, once, so callers can reuse it
5
+ * across many encryptions instead of paying importKey's cost per call.
6
+ */
7
+ export declare function importAesGcmKey(keyBytes: Uint8Array): Promise<CryptoKey>;
8
+ export declare function encryptDataWeb(plaintext: string, key: CryptoKey): Promise<string>;
@@ -0,0 +1 @@
1
+ const subtle=globalThis.crypto.subtle;function toBase64(t){let e="";for(let n=0;n<t.length;n++)e+=String.fromCharCode(t[n]);return btoa(e)}export async function sha256Bytes(t){const e=await subtle.digest("SHA-256",(new TextEncoder).encode(t));return new Uint8Array(e)}export async function sha256HexWeb(t){const e=await sha256Bytes(t);return Array.from(e).map(t=>t.toString(16).padStart(2,"0")).join("")}export async function importAesGcmKey(t){return subtle.importKey("raw",new Uint8Array(t),{name:"AES-GCM"},!1,["encrypt"])}export async function encryptDataWeb(t,e){const n=globalThis.crypto.getRandomValues(new Uint8Array(12)),r=new Uint8Array(await subtle.encrypt({name:"AES-GCM",iv:n,tagLength:128},e,(new TextEncoder).encode(t))),a=r.slice(0,r.length-16),o=r.slice(r.length-16);return`${toBase64(n)}:${toBase64(o)}:${toBase64(a)}`}
@@ -1,6 +1,7 @@
1
1
  export declare class DispatchDedupe {
2
2
  private readonly _entries;
3
3
  private readonly _ttlMs;
4
+ private _lastSweepAt;
4
5
  constructor(ttlMs?: number);
5
6
  private _key;
6
7
  mark(userId: string, eventType: string): void;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.DispatchDedupe=void 0;const DEFAULT_TTL_MS=1e4,SWEEP_THRESHOLD=1e3;class DispatchDedupe{constructor(t=1e4){this.i=new Map,this.h=t}o(t,s){return`${t}|${s}`}mark(t,s){this.i.set(this.o(t,s),Date.now()),this.i.size>1e3&&this.p()}wasRecentlyDispatched(t,s){const e=this.o(t,s),i=this.i.get(e);return!(void 0===i||Date.now()-i>this.h&&(this.i.delete(e),1))}clear(){this.i.clear()}get size(){return this.i.size}p(){const t=Date.now()-this.h;for(const[s,e]of this.i)e<t&&this.i.delete(s)}}exports.DispatchDedupe=DispatchDedupe;
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.DispatchDedupe=void 0;const DEFAULT_TTL_MS=1e4,SWEEP_THRESHOLD=1e3,SWEEP_MIN_INTERVAL_MS=1e3;class DispatchDedupe{constructor(t=1e4){this.i=new Map,this.h=0,this.o=t}D(t,s){return`${t}|${s}`}mark(t,s){if(this.i.set(this.D(t,s),Date.now()),this.i.size>1e3){const t=Date.now();t-this.h>=1e3&&(this.h=t,this.p())}}wasRecentlyDispatched(t,s){const e=this.D(t,s),i=this.i.get(e);return!(void 0===i||Date.now()-i>this.o&&(this.i.delete(e),1))}clear(){this.i.clear()}get size(){return this.i.size}p(){const t=Date.now()-this.o;for(const[s,e]of this.i)e<t&&this.i.delete(s)}}exports.DispatchDedupe=DispatchDedupe;
@@ -0,0 +1,25 @@
1
+ import type { UnsharedClient, ProcessUserEventParams } from '../client';
2
+ import type { VerdictCache } from './verdict-cache';
3
+ import type { RateLimitBackoff } from './rate-limit-backoff';
4
+ import type { CheckUserRequest } from '../shared-types';
5
+ /** Single policy point for background processUserEvent budgets. */
6
+ export declare const DISPATCH_TIMEOUT_MS = 2000;
7
+ export declare const SUBMIT_FP_TIMEOUT_MS = 3000;
8
+ export interface DispatchIo {
9
+ client: UnsharedClient;
10
+ cache: VerdictCache;
11
+ backoff: RateLimitBackoff;
12
+ onError?: (err: unknown, ctx: {
13
+ operation: 'processUserEvent';
14
+ userId: string;
15
+ emailAddress: string;
16
+ }) => void;
17
+ }
18
+ /** Warm the verdict cache after fingerprint ingestion without creating a duplicate event. */
19
+ export declare function runCheckUser(io: DispatchIo, request: CheckUserRequest, userId: string, emailAddress: string, timeoutMs: number): Promise<void>;
20
+ /**
21
+ * Run processUserEvent with the standard bounded budget and backoff wiring.
22
+ * Never rejects. All four dispatch sites (classic/web x dispatch/submit-fp)
23
+ * MUST go through this helper so the cap and backoff can't drift per-site.
24
+ */
25
+ export declare function runProcessUserEvent(io: DispatchIo, params: ProcessUserEventParams, timeoutMs: number): Promise<void>;
@@ -0,0 +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;
@@ -20,12 +20,26 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
20
20
  corsOrigins?: string | string[];
21
21
  /** Verdict cache TTL in ms. @default 60000 */
22
22
  cacheTTL?: number;
23
+ /** Max number of entries in the verdict cache before oldest-inserted entries are evicted. @default 10000 */
24
+ maxCacheSize?: number;
25
+ /**
26
+ * Budget in ms from the first buffered HTML write until res.end() before the
27
+ * response is treated as streaming SSR and fingerprint injection is skipped
28
+ * (the response itself is never delayed or altered past this point).
29
+ * Raise this if your handlers flush partial HTML early and finish rendering
30
+ * later (e.g. res.write(head) → await db → res.end(rest)) and you see the
31
+ * injection being skipped.
32
+ * @default 50
33
+ */
34
+ streamingIdleMs?: number;
23
35
  /** Paths to skip entirely (static assets, health checks). */
24
36
  skipPaths?: string[];
25
37
  /** When set, only paths matching one of these prefixes get events dispatched and checkUser called. */
26
38
  includePathPrefix?: string[];
27
39
  /** Skip the bot/crawler UA filter. Set to true in test environments so automated browsers (Playwright, Puppeteer, etc.) can observe verdicts. @default false */
28
40
  disableBotFilter?: boolean;
41
+ /** @internal Serve only the bundled fingerprint library, without S3 fetches. @default false */
42
+ disableS3Fingerprint?: boolean;
29
43
  /** Hard timeout (ms) for checkUser on cache miss. Fails open on timeout. @default 1500 */
30
44
  checkUserTimeoutMs?: number;
31
45
  /**
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.ACCOUNT_FLAGGED_ERROR=exports.flaggedResponse=exports.VerdictCache=void 0,exports.unsharedBoundToUser=unsharedBoundToUser;const fs_1=require("fs"),verdict_cache_1=require("./verdict-cache");Object.defineProperty(exports,"VerdictCache",{enumerable:!0,get:function(){return verdict_cache_1.VerdictCache}});const rate_limit_backoff_1=require("./rate-limit-backoff"),dispatch_dedupe_1=require("./dispatch-dedupe"),response_interceptor_1=require("./response-interceptor"),fingerprint_script_1=require("./injection/fingerprint-script"),fp_bundle_source_1=require("./injection/fp-bundle-source"),submit_fp_1=require("./routes/submit-fp"),verify_1=require("./routes/verify"),interstitial_1=require("./routes/interstitial"),gate_page_1=require("./injection/gate-page"),http_helpers_1=require("./utils/http-helpers"),content_type_1=require("./utils/content-type"),flagged_response_1=require("./utils/flagged-response"),mask_email_1=require("./utils/mask-email"),skip_paths_1=require("./utils/skip-paths"),include_path_1=require("./utils/include-path"),is_bot_1=require("./utils/is-bot"),client_ip_1=require("./utils/client-ip"),cookies_1=require("./utils/cookies"),device_id_1=require("./utils/device-id"),secure_1=require("./utils/secure"),sentinel_user_id_1=require("./utils/sentinel-user-id");var flagged_response_2=require("./utils/flagged-response");Object.defineProperty(exports,"flaggedResponse",{enumerable:!0,get:function(){return flagged_response_2.flaggedResponse}}),Object.defineProperty(exports,"ACCOUNT_FLAGGED_ERROR",{enumerable:!0,get:function(){return flagged_response_2.ACCOUNT_FLAGGED_ERROR}});const CHECK_USER_TIMEOUT_MS=1500,FAIL_OPEN_TTL_MS=5e3,_lastFailOpenWarn={};function warnFailOpenThrottled(e){const t=Date.now();t-(_lastFailOpenWarn[e.reason]??0)<6e4||(_lastFailOpenWarn[e.reason]=t,console.warn(`[Unshared] Failing open (reason=${e.reason}${null!=e.status?`, status=${e.status}`:""}). Detection is not enforced for this request. Configure onFailOpen to observe this directly.`))}function unsharedBoundToUser(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");if(!t.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:r,emailAddress:i,routePrefix:s="/__unshared",corsOrigins:n,cacheTTL:o=6e4,skipPaths:a,includePathPrefix:d,disableBotFilter:c=!1,checkUserTimeoutMs:l=CHECK_USER_TIMEOUT_MS,cacheMissStrategy:u="block",sessionId:_,deviceId:p,onFlagged:h,onError:f,onFailOpen:g,blockFlagged:v=!1,flaggedMode:m,autoInterstitial:I=!1,interstitialFlowType:k="email_verification"}=t,y=m??(v?"gate":void 0),A=I||"overlay"===y,C=e=>{if(g)try{g(e)}catch{}else"async_miss"!==e.reason&&warnFailOpenThrottled(e)},S=new verdict_cache_1.VerdictCache(o),x=new rate_limit_backoff_1.RateLimitBackoff,T=new Map,b=new dispatch_dedupe_1.DispatchDedupe,E=Date.now().toString(36),w=(0,fingerprint_script_1.generateFingerprintScript)(s,E,{autoInterstitial:A,interstitialFlowType:k});let O="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");O=(0,fs_1.readFileSync)(e,"utf8")}catch{}const U=(0,fp_bundle_source_1.createFingerprintBundleSource)(),F=void 0!==y||A,q="gate"===y?"flaggedMode 'gate' (blockFlagged)":"overlay"===y?"flaggedMode 'overlay'":"autoInterstitial";if(F&&!O)throw new Error(`[Unshared] ${q} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(F&&"/__unshared"!==s)throw new Error(`[Unshared] ${q} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const M="gate"===y?(0,gate_page_1.generateGatePage)(s,E):"",P=(0,submit_fp_1.handleSubmitFingerprint)({client:e,verdictCache:S,rateLimitBackoff:x,dispatchDedupe:b,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:_,resolveDeviceId:p,disableBotFilter:c,onError:f}),$=(0,verify_1.handleVerifyTrigger)({client:e,verdictCache:S,resolveEmailAddress:i,resolveDeviceId:p,onError:f}),j=(0,verify_1.handleVerify)({client:e,verdictCache:S,resolveUserId:r,resolveEmailAddress:i,resolveDeviceId:p,onError:f}),D=(0,interstitial_1.handleGetInterstitialFlow)({client:e}),V=n?Array.isArray(n)?n:[n]:null,N=`${s}/fp.js`,L=`${s}/fingerprint.js`,R=`${s}/submit-fp`,G=`${s}/verify-trigger`,H=`${s}/verify`,B=`${s}/status`,K=`${s}/interstitial-flow`,X=function(t,n,o){const g=(0,http_helpers_1.getRequestPath)(t.url),v=t.url||g;if(g.startsWith(s+"/")){if(function(e,t){if(!V)return;const r=e.headers.origin??"";V.includes("*")?(t.setHeader("Access-Control-Allow-Origin","*"),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id")):V.includes(r)&&(t.setHeader("Access-Control-Allow-Origin",r),t.setHeader("Vary","Origin"),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,n),"OPTIONS"===t.method)return void(0,http_helpers_1.sendEmpty)(n,204);if(("GET"===t.method||"HEAD"===t.method)&&g===N)return n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBodyForMethod)(n,t.method,200,O);if(("GET"===t.method||"HEAD"===t.method)&&g===L){const e=U.get();return e?(n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBodyForMethod)(n,t.method,200,e)):void(0,http_helpers_1.sendBodyForMethod)(n,t.method,503,"/* fingerprint agent unavailable */")}if("POST"===t.method&&(g===R||g===G||g===H))return void 0===t.body?void(0,http_helpers_1.sendJson)(n,400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"req.body is undefined. Mount a JSON body-parsing middleware (e.g., express.json()) before the Unshared middleware."}}):g===R?void P(t,n):g===G?void $(t,n):void j(t,n);if("GET"===t.method&&g===K)return void D(t,n);if("GET"===t.method&&g===B){let s;try{s=r(t)}catch{}if(!s)return void(0,http_helpers_1.sendJson)(n,200,{status:"anonymous"});const o=resolveEmail(t,i);return void(async()=>{let r=S.get(s);if((!r||S.isStale(s))&&o&&!x.isPaused()&&!S.isRefreshing(s)){S.markRefreshing(s);try{const i=(0,device_id_1.extractDeviceIdOrUndefined)(t,p),n=extractFingerprintId(t),a=extractSessionId(t,_),d=i??n??"unknown";await fetchAndCacheVerdict(e,S,s,o,d,n,a,l,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:s,emailAddress:o})),r=S.get(s)}catch(e){f&&f(e,{operation:"checkUser",userId:s,emailAddress:o}),C({operation:"checkUser",reason:"exception",userId:s,emailAddress:o})}finally{S.clearRefreshing(s)}}r&&r.isFlagged&&!r.isVerified&&(void 0!==y||h)&&o?(0,http_helpers_1.sendJson)(n,200,{status:"flagged",email:(0,mask_email_1.maskEmail)(o)}):(0,http_helpers_1.sendJson)(n,200,{status:"ok"})})()}return void(0,http_helpers_1.sendJson)(n,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if((0,skip_paths_1.shouldSkipPath)(g,a))return void o();if(!(0,include_path_1.shouldIncludePath)(g,d))return interceptForInjection(t,n,w),void o();let m;try{m=r(t)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(m)){const e=(0,cookies_1.parseCookie)(t,"__unshared_uid"),r=(0,cookies_1.parseCookie)(t,"__unshared_uid_at"),i=r?Number(r):NaN,s=Number.isFinite(i)&&Date.now()-i<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;m=e&&s?e:void 0}if(!m){const e=(0,secure_1.isSecureRequest)(t)?"; Secure":"";return appendSetCookie(n,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(t,n,w),void o()}const I=resolveEmail(t,i);if(setUserIdCookie(t,n,m),I&&setEmailCookie(t,n,I),!I)return interceptForInjection(t,n,w),void o();const k=extractSessionId(t,_),A=(0,device_id_1.extractDeviceIdOrUndefined)(t,p),E=extractFingerprintId(t),F=t.headers["user-agent"]??"",q=(0,client_ip_1.extractClientIp)(t),X=A??E;if(!c&&(0,is_bot_1.isBot)(F))return void o();const W=S.get(m);if(!W){if("async"===u)return S.isRefreshing(m)||(S.markRefreshing(m),fetchAndCacheVerdict(e,S,m,I,X??"unknown",E,k,l,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:m,emailAddress:I})).catch(()=>C({operation:"checkUser",reason:"exception",userId:m,emailAddress:I})).finally(()=>S.clearRefreshing(m))),C({operation:"checkUser",reason:"async_miss",userId:m,emailAddress:I}),J(),interceptForInjection(t,n,w),void o();let r=T.get(m);return r||(r=fetchAndCacheVerdict(e,S,m,I,X??"unknown",E,k,l,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:m,emailAddress:I})).finally(()=>T.delete(m)),T.set(m,r)),void r.then(e=>{e.isFlagged||J(),applyVerdict(e,m,I,t,n,o,w,h,y,M)}).catch(()=>{C({operation:"checkUser",reason:"exception",userId:m,emailAddress:I}),J(),interceptForInjection(t,n,w),o()})}function J(){"unknown"!==k&&X&&(x.isPaused()||dispatchUserEvent(e,S,x,b,{userId:m,emailAddress:I,sessionId:k,deviceId:X,fingerprintId:E,userAgent:F,ipAddress:q,eventType:v},f))}S.isStale(m)&&!S.isRefreshing(m)&&(S.markRefreshing(m),fetchAndCacheVerdict(e,S,m,I,X??"unknown",E,k,l,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:m,emailAddress:I})).catch(()=>C({operation:"checkUser",reason:"exception",userId:m,emailAddress:I})).finally(()=>S.clearRefreshing(m))),W.isFlagged||J(),applyVerdict(W,m,I,t,n,o,w,h,y,M)};return X.destroy=()=>{S.destroy(),U.stop()},X}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=(0,cookies_1.parseCookie)(e,"__unshared_email");if(r)return r;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,t,r,i,s,n,o,a,d,c){const l=e.isFlagged&&!e.isVerified;if("gate"===d&&l)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(s.statusCode=200,s.setHeader("Content-Type","text/html; charset=utf-8"),s.setHeader("Cache-Control","no-store"),s.end(c)):(0,http_helpers_1.sendJson)(s,403,(0,flagged_response_1.flaggedResponse)(r));else if("overlay"===d&&l)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(interceptForInjection(i,s,o),n()):(0,http_helpers_1.sendJson)(s,403,(0,flagged_response_1.flaggedResponse)(r));else if(interceptForInjection(i,s,o),e.isFlagged&&!e.isVerified&&a)try{a({userId:t,emailAddress:r,verdict:e,req:i,res:s,next:n})}catch{n()}else n()}function interceptForInjection(e,t,r){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],(0,response_interceptor_1.interceptResponse)(t,(e,t)=>{if(!(0,content_type_1.isHtmlContentType)(t))return null;const i=e.toString("utf8"),s=i.lastIndexOf("</body>");return-1===s?i+r:i.slice(0,s)+r+i.slice(s)},{preventCaching:!0})}function dispatchUserEvent(e,t,r,i,s,n){i.mark(s.userId,s.eventType),e.processUserEvent({eventType:s.eventType,userId:s.userId,emailAddress:s.emailAddress,ipAddress:s.ipAddress,deviceId:s.deviceId,fingerprintId:s.fingerprintId,sessionHash:s.sessionId,userAgent:s.userAgent}).then(e=>{e.success&&e.data?.analysis&&t.update(s.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&r.pause(1e3*e.error.retryAfter)}).catch(e=>{n&&n(e,{operation:"processUserEvent",userId:s.userId,emailAddress:s.emailAddress})})}async function fetchAndCacheVerdict(e,t,r,i,s,n,o,a=CHECK_USER_TIMEOUT_MS,d){const c={timeoutMs:a,maxRetries:0},l=s&&"unknown"!==s?s:void 0,u=l??n,_=n??l;let p;u&&(c.deviceId=u),_&&(c.fingerprintId=_);const h=await Promise.race([e.checkUser(i,c),new Promise(e=>{p=setTimeout(()=>e(null),a)})]);return clearTimeout(p),h?h.failedOpen?(d?.(h.failedOpen.reason??"http_error",h.failedOpen.status),cacheFailOpenVerdict(t,r,i,o)):(t.set(r,{isFlagged:h.data?.is_user_flagged??!1,isVerified:!1,emailAddress:i,sessionId:o}),t.get(r)):(d?.("timeout"),cacheFailOpenVerdict(t,r,i,o))}function cacheFailOpenVerdict(e,t,r,i){const s=e.get(t);return e.set(t,{isFlagged:s?.isFlagged??!1,isVerified:s?.isVerified??!1,emailAddress:r,sessionId:i},5e3),e.get(t)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return(0,cookies_1.parseCookie)(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return(0,cookies_1.parseCookie)(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.ACCOUNT_FLAGGED_ERROR=exports.flaggedResponse=exports.VerdictCache=void 0,exports.unsharedBoundToUser=unsharedBoundToUser;const fs_1=require("fs"),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}`)}
@@ -12,6 +12,13 @@
12
12
  * window.addEventListener("unshared:flagged", (e) => {
13
13
  * e.detail.email — the flagged user's email (from 403 response body)
14
14
  * });
15
+ *
16
+ * Sticky-signal contract (ordering-proof alternative to the event): the signal
17
+ * is also recorded on the shared namespace, so listeners that attach AFTER the
18
+ * CustomEvent fired (late hydration, app crash-and-remount) can recover it:
19
+ * window.__unshared.flagged — { email } once flagged, else undefined
20
+ * window.__unshared.whenFlagged(cb) — subscribes cb AND replays immediately
21
+ * if the signal already fired
15
22
  */
16
23
  export declare function generateFingerprintScript(routePrefix: string, version?: string, opts?: {
17
24
  autoInterstitial?: boolean;
@@ -1 +1 @@
1
- "use strict";function generateFingerprintScript(e,t,n){const r=t?`?v=${escapeJavaScript(t)}`:"",s=n?.autoInterstitial?`\nvar _isdk=new UnsharedBrowser.UnsharedBrowser({baseUrl:""});\nwindow.addEventListener("unshared:flagged",function(){try{_isdk.showInterstitial({flowType:"${escapeJavaScript(n?.interstitialFlowType??"email_verification")}",onComplete:function(){try{location.reload()}catch(e){}}})}catch(e){}});`:"";return`<script>\n(function(){\ntry{\n// --- Bot drop (defense-in-depth) ---\n// Must be the first statement: we do not want to write cookies, localStorage,\n// session IDs, or any network requests for known-bot traffic. Mirrors the\n// regex in unshared-fingerprint-lib/src/detect/bot.ts and Node middleware\n// utils/is-bot.ts. Keep all three in sync.\nvar BOT_RE=/googlebot|bingbot|slurp|baiduspider|duckduckbot|yandex|sogou|exabot|ia_archiver|curl|wget|python-requests|python-urllib|axios|node-fetch|go-http-client|java\\/|libwww-perl|okhttp|apache-httpclient|http_request|httpie|headlesschrome|phantomjs|puppeteer|playwright|cypress|selenium|webdriver|electron|jsdom|vercel-screenshot|screenshot|prerender|lighthouse|chrome-lighthouse|pagespeed|gtmetrix|pingdom|nessus|nikto|sqlmap|burp|zap|qualys|openvas|nmap|masscan|facebookexternalhit|twitterbot|linkedinbot|whatsapp|telegrambot|slackbot|discordbot|bot|crawl|spider|scrape|fetch|scan/i;\nif(typeof navigator!=="undefined"&&navigator.userAgent&&BOT_RE.test(navigator.userAgent))return;\n\nvar pfx="${escapeJavaScript(e)}";\nvar SS_FP="__unshared_fp";\nvar SS_LAST_SUBMIT="__unshared_last_submit";\n\n// Dedup state: skip submit if (user_id + URL) matches last submission.\n// Modern SPAs (Next.js App Router, React Router, etc.) call replaceState\n// 3-5 times during hydration with the same URL — without this guard,\n// each call generates a redundant FP row with identical stable_hash.\n// Persisted to sessionStorage so hard reloads and framework double-boots\n// inside the same tab still dedupe (the in-memory value resets on reload).\nvar lastSubmitKey="";\ntry{lastSubmitKey=sessionStorage.getItem(SS_LAST_SUBMIT)||""}catch(e){}\n\n// Page-scoped dedup state SHARED with the frontend SDK (browser.ts getSharedDedup).\n// On a Tier 1 page both this inline script and the SDK submit; each holds its own\n// in-memory lastSubmitKey, so both could pass the check below and POST. window.__unshared\n// is read+written synchronously in submitFP (no await between), so whichever fires\n// first claims the uid|route key and the other no-ops — killing the read-before-write\n// race that doubled events. KEEP IN SYNC with browser.ts: namespace, lastKey, key formula.\nvar shared=(window.__unshared=window.__unshared||{});\n\n// --- Helpers ---\nfunction gC(n){var m=document.cookie.match(new RegExp("(?:^|; )"+n+"=([^;]*)"));return m?decodeURIComponent(m[1]):null}\nfunction sC(n,v,d){var e="";if(d){var dt=new Date();dt.setTime(dt.getTime()+d*864e5);e="; expires="+dt.toUTCString()}document.cookie=n+"="+encodeURIComponent(v)+e+"; path=/; SameSite=Lax"+(location.protocol==="https:"?"; Secure":"")}\nfunction uuid(){return(typeof crypto!=="undefined"&&crypto.randomUUID)?crypto.randomUUID():("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(c){var r=Math.random()*16|0;return(c==="x"?r:r&0x3|0x8).toString(16)}))}\n// Sentinel user IDs that must never be treated as real users. Mirrors\n// the Set in sentinel-user-id.ts — keep in sync. Empty string is handled\n// by the separate !uid checks below.\nvar SENTINEL_UIDS={"__pre_auth__":1,"anonymous":1,"guest":1,"undefined":1,"null":1};\nfunction isSentinelUid(v){return typeof v==="string"&&SENTINEL_UIDS.hasOwnProperty(v)}\n\n// --- Session + device IDs ---\n// Session ID is a UUID because it's supposed to be tab-scoped and random.\n// Device ID is intentionally NOT a UUID — Issue 9: random UUIDs wrote\n// meaningless device_ids to every fingerprint row. Instead we read the\n// stable fingerprint hash from localStorage if a previous submission\n// already persisted it; otherwise we leave did empty and let submitFP()\n// reconcile on the first successful collection. The Node middleware's\n// Issue 8 bootstrap-skip branch handles the empty-device_id window so we\n// never dispatch with a random or "unknown" value.\nvar sid=gC("__unshared_sid");\nif(!sid){sid=uuid();sC("__unshared_sid",sid,365)}\nvar did="";\ntry{did=localStorage.getItem("__unshared_device_id")||""}catch(e){}\nif(did){sC("__unshared_fp_id",did,365)}\n\n// --- Fingerprint cache (sessionStorage) ---\nfunction getFP(){try{var r=sessionStorage.getItem(SS_FP);return r?JSON.parse(r):null}catch(e){return null}}\nfunction setFP(fp){try{sessionStorage.setItem(SS_FP,JSON.stringify(fp))}catch(e){}}\n\n// --- Submit fingerprint to backend ---\nfunction submitFP(fp){\n var uid=gC("__unshared_uid");\n if(!uid||isSentinelUid(uid))return;\n // Issue 9: reconcile device_id to the stable fingerprint hash. This runs\n // before we send the X-Device-Id header so the very first submission\n // already carries the real value. Persist to localStorage so other tabs\n // (and future reloads) pick up the same stable ID without needing to\n // re-collect the fingerprint.\n if(fp.fingerprint_id){\n did=fp.fingerprint_id;\n try{localStorage.setItem("__unshared_device_id",did)}catch(e){}\n sC("__unshared_fp_id",did,365);\n }\n // event_type is the SPA route, not a fixed enum. Page-level event names\n // (page_load/route_change) collapsed every row into one of two buckets;\n // the URL is more useful for analytics and matches the frontend SDK.\n var route=(location.pathname||"/")+(location.search||"");\n var key=uid+"|"+route;\n // Check the shared window guard AND the in-memory key (last-key semantics, so an\n // SPA A->B->A revisit still submits the second A). The shared guard makes this\n // submitter and the frontend SDK see each other within the page.\n if(key===lastSubmitKey||shared.lastKey===key)return;\n shared.lastKey=key;\n lastSubmitKey=key;\n try{sessionStorage.setItem(SS_LAST_SUBMIT,key)}catch(e){}\n // collected_at is stamped fresh at submit time rather than carried from fp.timestamp,\n // because fp is cached per-tab in sessionStorage — reusing its original timestamp would\n // freeze collected_at at first load and drift against server created_at as the tab ages.\n // The server authoritatively overwrites this value again on ingress.\n var body={hash:fp.full_hash,stable_hash:fp.fingerprint_id,collected_at:(new Date()).toISOString(),is_incognito:fp.isIncognito,components:fp.components,version:fp.version,source:fp.source,session_id:sid,user_id:uid,event_type:route};\n // Idempotency key derived from (stable_hash, user_id, route), SHA-256-hashed\n // when WebCrypto is available so the user_id never appears in the platform's\n // stored idempotency_key column (raw fallback on non-secure contexts — the\n // middleware accepts both). NOTE: the middleware appends |Date.now() before\n // forwarding (submit-fp.ts), so the backend sees a unique value per\n // submission and dedups only PubSub redeliveries — NOT two distinct POSTs.\n // Cross-submitter / cross-reload dedup is client-side (the shared window\n // guard + sessionStorage above).\n var idem=fp.fingerprint_id+"|"+uid+"|"+route;\n function sendFP(idemKey){\n var xhr=new XMLHttpRequest();\n xhr.open("POST",pfx+"/submit-fp",true);\n xhr.setRequestHeader("Content-Type","application/json");\n xhr.setRequestHeader("X-Session-Id",sid);\n if(did)xhr.setRequestHeader("X-Device-Id",did);\n xhr.setRequestHeader("X-Idempotency-Key",idemKey);\n xhr.send(JSON.stringify(body));\n }\n if(window.crypto&&crypto.subtle&&window.TextEncoder){\n crypto.subtle.digest("SHA-256",new TextEncoder().encode(idem)).then(function(d){\n var a=new Uint8Array(d),s="";\n for(var i=0;i<a.length;i++){s+=("0"+a[i].toString(16)).slice(-2)}\n sendFP(s);\n }).catch(function(){sendFP(idem)});\n }else{sendFP(idem)}\n}\n\n// --- Collect fingerprint (loads fp.js if needed) then submit ---\nvar fpReady=false;\nfunction collectAndSubmit(){\n var uid=gC("__unshared_uid");\n if(!uid||isSentinelUid(uid))return;\n var cached=getFP();\n if(cached){submitFP(cached);return}\n if(!fpReady)return;\n try{\n var c=new UnsharedBrowser.UnsharedBrowser({baseUrl:""});\n c.collect({exclude:["timing","speech"]}).then(function(fp){setFP(fp);submitFP(fp)});\n }catch(e){}\n}\n\n// --- Load fp.js (always — browser caches it for 1h) ---\n// Submit cached FP immediately if available; load fp.js for fresh collection\nvar pageLoadSubmitted=false;\nvar _boot_uid=gC("__unshared_uid");\nif(getFP()&&_boot_uid&&!isSentinelUid(_boot_uid)){submitFP(getFP());pageLoadSubmitted=true;deferredCheck()}\nvar s=document.createElement("script");\ns.src=pfx+"/fp.js${r}";\ns.onload=function(){fpReady=true;${s}if(!pageLoadSubmitted){collectAndSubmit();deferredCheck()}};\ndocument.head.appendChild(s);\n\n// --- Deferred verdict check ---\n// After fingerprint submission, the backend processes the event async.\n// If the user was just flagged, the initial page load may have beaten\n// the verdict update. Re-check after a delay so newly flagged sessions\n// get caught without waiting for user interaction.\n// The endpoint always returns 200 so browsers don't log a scary red\n// network error — we inspect the body and dispatch the flagged event\n// ourselves when status==="flagged".\nfunction deferredCheck(){\n var uid=gC("__unshared_uid");\n if(!uid||isSentinelUid(uid))return;\n setTimeout(function(){\n try{fetch(pfx+"/status",{method:"GET",credentials:"same-origin"}).then(function(r){return r.json()}).then(function(b){if(b&&b.status==="flagged")emitFlagged(b)}).catch(function(){})}catch(e){}\n },500);\n}\n\n// --- SPA route change tracking (History API + popstate) ---\nvar oPush=history.pushState,oReplace=history.replaceState;\nhistory.pushState=function(){oPush.apply(this,arguments);try{collectAndSubmit()}catch(e){}};\nhistory.replaceState=function(){oReplace.apply(this,arguments);try{collectAndSubmit()}catch(e){}};\nwindow.addEventListener("popstate",function(){try{collectAndSubmit()}catch(e){}});\n\n// --- 403 interception: dispatch "unshared:flagged" event ---\nfunction emitFlagged(body){\n try{window.dispatchEvent(new CustomEvent("unshared:flagged",{detail:{email:body.email||""}}))}catch(e){}\n}\n\n// Patch fetch\nvar oFetch=window.fetch;\nif(oFetch){window.fetch=function(){return oFetch.apply(this,arguments).then(function(r){if(r.status===403){try{var cl=r.clone();cl.json().then(function(b){if(b&&b.error==="account_flagged")emitFlagged(b)}).catch(function(){})}catch(e){}}return r})}}\n\n// Patch XMLHttpRequest\nvar oXSend=XMLHttpRequest.prototype.send;\nXMLHttpRequest.prototype.send=function(){var x=this;x.addEventListener("load",function(){if(x.status===403){try{var b=JSON.parse(x.responseText);if(b&&b.error==="account_flagged")emitFlagged(b)}catch(e){}}});return oXSend.apply(this,arguments)};\n\n}catch(e){}\n})();\n<\/script>`}function escapeJavaScript(e){return 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,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,13 +1,15 @@
1
1
  export declare const S3_FINGERPRINT_URL = "https://unshared-public.s3.us-east-2.amazonaws.com/fingerprint@1/fingerprint.umd.js";
2
2
  export interface FingerprintBundleSource {
3
- /** Current UMD string: freshest S3 copy, else bundled fallback, else ''. */
4
- get(): string;
3
+ /** Wait for the initial bounded S3 attempt, then return the cached UMD or fallback. */
4
+ get(): Promise<string>;
5
5
  /** Run one refresh cycle now (also called by the background timer). */
6
6
  refresh(): Promise<void>;
7
7
  /** Stop the background timer. */
8
8
  stop(): void;
9
9
  }
10
10
  export interface FingerprintBundleSourceOptions {
11
+ /** @internal Disable all remote fetches and serve only the bundled fallback. */
12
+ disabled?: boolean;
11
13
  url?: string;
12
14
  intervalMs?: number;
13
15
  fetchTimeoutMs?: number;
@@ -18,8 +20,7 @@ export interface FingerprintBundleSourceOptions {
18
20
  /**
19
21
  * Start the background refresh timer + immediate boot fetch.
20
22
  * @default true in production, false under NODE_ENV==='test' (so unit tests
21
- * never make a real network call to S3 — they seed with the bundled
22
- * fallback and drive `refresh()` with an injected fetch).
23
+ * drive `get()` / `refresh()` with an injected fetch).
23
24
  */
24
25
  autoStart?: boolean;
25
26
  }
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.S3_FINGERPRINT_URL=void 0,exports.createFingerprintBundleSource=createFingerprintBundleSource;const fs_1=require("fs"),module_1=require("module"),path_1=require("path");exports.S3_FINGERPRINT_URL="https://unshared-public.s3.us-east-2.amazonaws.com/fingerprint@1/fingerprint.umd.js";const DEFAULT_REFRESH_INTERVAL_MS=9e5,DEFAULT_FETCH_TIMEOUT_MS=5e3;function defaultLoadFallback(){try{const e=(0,module_1.createRequire)(__filename).resolve("unshared-frontend-sdk/package.json"),t=(0,module_1.createRequire)(e).resolve("unshared-fingerprint-lib"),r=(0,path_1.join)((0,path_1.dirname)(t),"fingerprint.umd.js");return(0,fs_1.readFileSync)(r,"utf8")}catch{return""}}function createFingerprintBundleSource(e={}){const t=e.url??exports.S3_FINGERPRINT_URL,r=e.intervalMs??DEFAULT_REFRESH_INTERVAL_MS,n=e.fetchTimeoutMs??DEFAULT_FETCH_TIMEOUT_MS,o=e.fetchImpl??globalThis.fetch;let s,a,i=(e.loadFallback??defaultLoadFallback)();async function u(){if(!o)return;const e=new AbortController,r=setTimeout(()=>e.abort(),n);try{const r={};s&&(r["If-None-Match"]=s);const n=await o(t,{headers:r,signal:e.signal});if(304===n.status)return;if(!n.ok)return;const a=await n.text();a&&(i=a,s=n.headers.get("etag")??void 0)}catch{}finally{clearTimeout(r)}}return(e.autoStart??"test"!==process.env.NODE_ENV)&&(u(),a=setInterval(()=>{u()},r),"function"==typeof a.unref&&a.unref()),{get:()=>i,refresh:u,stop:()=>{a&&clearInterval(a),a=void 0}}}
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.S3_FINGERPRINT_URL=void 0,exports.createFingerprintBundleSource=createFingerprintBundleSource;const fs_1=require("fs"),module_1=require("module"),path_1=require("path");exports.S3_FINGERPRINT_URL="https://unshared-public.s3.us-east-2.amazonaws.com/fingerprint@1/fingerprint.umd.js";const DEFAULT_REFRESH_INTERVAL_MS=9e5,DEFAULT_FETCH_TIMEOUT_MS=1e3;function defaultLoadFallback(){try{const e=(0,module_1.createRequire)(__filename).resolve("unshared-frontend-sdk/package.json"),t=(0,module_1.createRequire)(e).resolve("unshared-fingerprint-lib"),r=(0,path_1.join)((0,path_1.dirname)(t),"fingerprint.umd.js");return(0,fs_1.readFileSync)(r,"utf8")}catch{return""}}function createFingerprintBundleSource(e={}){const t=e.disabled??!1,r=e.url??exports.S3_FINGERPRINT_URL,n=e.intervalMs??DEFAULT_REFRESH_INTERVAL_MS,o=e.fetchTimeoutMs??DEFAULT_FETCH_TIMEOUT_MS,s=e.fetchImpl??globalThis.fetch;let a,i,u,c,l=(e.loadFallback??defaultLoadFallback)();function _(){if(t)return Promise.resolve();if(c)return c;const e=new AbortController;let n;const i=new Promise(t=>{n=setTimeout(()=>{e.abort(),t()},o)});return c=Promise.race([(async()=>{try{if(!s)return;const t={};a&&(t["If-None-Match"]=a);const n=await s(r,{headers:t,signal:e.signal});if(304===n.status||!n.ok)return;const o=await n.text();o&&!e.signal.aborted&&(l=o,a=n.headers.get("etag")??void 0)}catch{}})(),i]).finally(()=>{clearTimeout(n),c=void 0}),u??(u=c),c}return(e.autoStart??"test"!==process.env.NODE_ENV)&&!t&&(_(),i=setInterval(()=>{_()},n),"function"==typeof i.unref&&i.unref()),{get:async()=>(await(u??_()),l),refresh:_,stop:()=>{i&&clearInterval(i),i=void 0}}}
@@ -1,14 +1,17 @@
1
- /**
2
- * Global rate-limit backoff for processUserEvent calls.
3
- *
4
- * When the Unshared API returns 429 with a Retry-After header,
5
- * the middleware pauses processUserEvent calls for the specified duration.
6
- * checkUser calls are not affected — enforcement takes priority.
7
- */
8
1
  export declare class RateLimitBackoff {
9
2
  private _resumeAtTimestamp;
10
- /** Pause processUserEvent calls for the given duration. */
3
+ private _failureResumeAtTimestamp;
4
+ private _consecutiveFailures;
5
+ /** Pause processUserEvent calls (server-directed 429), clamped to MAX_BACKOFF_MS. */
11
6
  pause(durationMs: number): void;
7
+ /** Record a network/5xx/timeout failure; engage failure backoff at the threshold. */
8
+ noteFailure(): void;
9
+ /** Record a success; resets the failure streak and lifts ONLY the failure-streak
10
+ * pause. It deliberately does NOT touch a server-directed 429 pause (pause()),
11
+ * so a success on one request can't wipe another request's legitimate 429 backoff. */
12
+ noteSuccess(): void;
12
13
  /** Returns true if processUserEvent calls should be skipped. */
13
14
  isPaused(): boolean;
15
+ /** Parse a Retry-After header value (delta-seconds OR HTTP-date) into ms, or null. */
16
+ static parseRetryAfter(headerValue: string | null, now?: number): number | null;
14
17
  }
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.RateLimitBackoff=void 0;class RateLimitBackoff{constructor(){this.i=0}pause(t){const s=Date.now()+t;s>this.i&&(this.i=s)}isPaused(){return Date.now()<this.i}}exports.RateLimitBackoff=RateLimitBackoff;
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.RateLimitBackoff=void 0;const MAX_BACKOFF_MS=3e5,FAILURE_THRESHOLD=3,FAILURE_BACKOFF_MS=1e4;class RateLimitBackoff{constructor(){this.i=0,this.o=0,this.h=0}pause(t){const s=Math.min(Math.max(0,t),3e5),e=Date.now()+s;e>this.i&&(this.i=e)}noteFailure(){if(this.h++,this.h>=3){const t=Date.now()+1e4;t>this.o&&(this.o=t)}}noteSuccess(){this.h=0,this.o=0}isPaused(){const t=Date.now();return t<this.i||t<this.o}static parseRetryAfter(t,s=Date.now()){if(null==t)return null;const e=parseInt(t,10);if(!isNaN(e)&&String(e)===t.trim())return 1e3*e;const i=Date.parse(t);return isNaN(i)?null:Math.max(0,i-s)}}exports.RateLimitBackoff=RateLimitBackoff;
@@ -19,4 +19,5 @@ import type { UnsharedResponse } from '../types';
19
19
  */
20
20
  export declare function interceptResponse(res: UnsharedResponse, transform: (body: Buffer, contentType: string | undefined) => Buffer | string | null, options?: {
21
21
  preventCaching?: boolean;
22
+ streamingIdleMs?: number;
22
23
  }): void;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.interceptResponse=interceptResponse;const MAX_INJECT_BYTES=2e6;function findWriteHeadHeaders(e){return e.find(e=>e&&"object"==typeof e)}function readHeaderFromWriteHeadArgs(e,t){const n=findWriteHeadHeaders(e);if(!n)return;const r=t.toLowerCase();if(Array.isArray(n)){if(n.length&&Array.isArray(n[0])){for(const e of n)if(String(e[0]).toLowerCase()===r)return e[1]}else for(let e=0;e+1<n.length;e+=2)if(String(n[e]).toLowerCase()===r)return n[e+1]}else for(const e of Object.keys(n))if(e.toLowerCase()===r)return n[e]}function deleteHeaderFromWriteHeadArgs(e,t){const n=findWriteHeadHeaders(e);if(!n)return;const r=t.toLowerCase();if(Array.isArray(n))if(n.length&&Array.isArray(n[0]))for(let e=n.length-1;e>=0;e--)String(n[e][0]).toLowerCase()===r&&n.splice(e,1);else for(let e=n.length-2;e>=0;e-=2)String(n[e]).toLowerCase()===r&&n.splice(e,2);else for(const e of Object.keys(n))e.toLowerCase()===r&&delete n[e]}function interceptResponse(e,t,n){const r=n?.preventCaching??!1,o=e.write.bind(e),f=e.end.bind(e),i=e.writeHead.bind(e),c=[];let u=0,s=!1,l=null;function d(){e.write=o,e.end=f;for(const e of c)o(e);c.length=0}function a(e,t){null===l&&null!=e&&(l=String(e).includes("text/html"),l&&t&&(l=!1),!1===l&&d())}function g(){a(e.getHeader("content-type"),e.getHeader("content-encoding"))}e.writeHead=function(t,...n){return a(readHeaderFromWriteHeadArgs(n,"content-type")??e.getHeader("content-type"),readHeaderFromWriteHeadArgs(n,"content-encoding")??e.getHeader("content-encoding")),l&&(r&&(e.setHeader("Cache-Control","no-store"),e.removeHeader("ETag"),e.removeHeader("Last-Modified")),e.removeHeader("Content-Length"),deleteHeaderFromWriteHeadArgs(n,"content-length")),i(t,...n)},e.write=function(e,t,n){if(g(),!1===l)return o(e,t,n);if(null!=e){const r=Buffer.isBuffer(e)?e:Buffer.from(e,"string"==typeof t?t:"utf8");if(u+=r.length,c.push(r),u>MAX_INJECT_BYTES)return d(),"function"==typeof t&&t(null),"function"==typeof n&&n(null),!0}return"function"==typeof t&&t(null),"function"==typeof n&&n(null),!0},e.end=function(n,r,i){if(s)return e;if(s=!0,g(),!1===l)return f(n,r,i);if(null!=n){const e=Buffer.isBuffer(n)?n:Buffer.from(n,"string"==typeof r?r:"utf8");u+=e.length,c.push(e)}if(u>MAX_INJECT_BYTES){d();const e="function"==typeof r?r:i;return e?f(e):f()}const a=Buffer.concat(c),p=e.getHeader("content-type");let y;try{y=t(a,p)}catch{y=null}if(null!=y){const t=Buffer.isBuffer(y)?y:Buffer.from(y,"utf8");e.headersSent||(e.setHeader("Content-Length",t.length),e.removeHeader("Content-Encoding")),o(t)}else a.length>0&&o(a);const H="function"==typeof r?r:i;return H?f(H):f(),e}}
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.interceptResponse=interceptResponse;const MAX_INJECT_BYTES=2e6;function findWriteHeadHeaders(e){return e.find(e=>e&&"object"==typeof e)}function readHeaderFromWriteHeadArgs(e,t){const n=findWriteHeadHeaders(e);if(!n)return;const r=t.toLowerCase();if(Array.isArray(n)){if(n.length&&Array.isArray(n[0])){for(const e of n)if(String(e[0]).toLowerCase()===r)return e[1]}else for(let e=0;e+1<n.length;e+=2)if(String(n[e]).toLowerCase()===r)return n[e+1]}else for(const e of Object.keys(n))if(e.toLowerCase()===r)return n[e]}function deleteHeaderFromWriteHeadArgs(e,t){const n=findWriteHeadHeaders(e);if(!n)return;const r=t.toLowerCase();if(Array.isArray(n))if(n.length&&Array.isArray(n[0]))for(let e=n.length-1;e>=0;e--)String(n[e][0]).toLowerCase()===r&&n.splice(e,1);else for(let e=n.length-2;e>=0;e-=2)String(n[e]).toLowerCase()===r&&n.splice(e,2);else for(const e of Object.keys(n))e.toLowerCase()===r&&delete n[e]}function interceptResponse(e,t,n){const r=n?.preventCaching??!1,o=n?.streamingIdleMs??50,f=e.write.bind(e),i=e.end.bind(e),u=e.writeHead.bind(e),c=[];let l=0,s=!1,d=null,a=null;function g(){d&&(clearTimeout(d),d=null),e.write=f,e.end=i;for(const e of c)f(e);c.length=0}function H(e,t){null===a&&null!=e&&(a=String(e).includes("text/html"),a&&t&&(a=!1),!1===a&&g())}function p(){H(e.getHeader("content-type"),e.getHeader("content-encoding"))}e.writeHead=function(t,...n){return H(readHeaderFromWriteHeadArgs(n,"content-type")??e.getHeader("content-type"),readHeaderFromWriteHeadArgs(n,"content-encoding")??e.getHeader("content-encoding")),a&&(r&&(e.setHeader("Cache-Control","no-store"),e.removeHeader("ETag"),e.removeHeader("Last-Modified"),deleteHeaderFromWriteHeadArgs(n,"cache-control"),deleteHeaderFromWriteHeadArgs(n,"etag"),deleteHeaderFromWriteHeadArgs(n,"last-modified")),e.removeHeader("Content-Length"),deleteHeaderFromWriteHeadArgs(n,"content-length")),u(t,...n)},e.write=function(e,t,n){if(p(),!1===a)return f(e,t,n);if(null!=e){const r=Buffer.isBuffer(e)?e:Buffer.from(e,"string"==typeof t?t:"utf8");if(l+=r.length,c.push(r),l>MAX_INJECT_BYTES)return g(),"function"==typeof t&&t(null),"function"==typeof n&&n(null),!0;d||s||(d=setTimeout(()=>{s||g()},o),"function"==typeof d.unref&&d.unref())}return"function"==typeof t&&t(null),"function"==typeof n&&n(null),!0},e.end=function(n,r,o){if(s)return e;if(s=!0,d&&(clearTimeout(d),d=null),p(),!1===a)return i(n,r,o);if(null!=n){const e=Buffer.isBuffer(n)?n:Buffer.from(n,"string"==typeof r?r:"utf8");l+=e.length,c.push(e)}if(l>MAX_INJECT_BYTES){g();const e="function"==typeof r?r:o;return e?i(e):i()}const u=Buffer.concat(c),H=e.getHeader("content-type");let y;try{y=t(u,H)}catch{y=null}if(null!=y){const t=Buffer.isBuffer(y)?y:Buffer.from(y,"utf8");e.headersSent||(e.setHeader("Content-Length",t.length),e.removeHeader("Content-Encoding")),f(t)}else u.length>0&&f(u);const A="function"==typeof r?r:o;return A?i(A):i(),e}}
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"i",{value:!0}),exports.handleSubmitFingerprint=handleSubmitFingerprint;const util_1=require("../../util"),is_bot_1=require("../utils/is-bot"),client_ip_1=require("../utils/client-ip"),cookies_1=require("../utils/cookies"),device_id_1=require("../utils/device-id"),secure_1=require("../utils/secure"),sentinel_user_id_1=require("../utils/sentinel-user-id"),http_helpers_1=require("../utils/http-helpers");function handleSubmitFingerprint(e){return async(i,t)=>{try{const s=i.body??{},n={full_hash:s.hash??"",fingerprint_id:s.stable_hash??"",timestamp:s.collected_at??(new Date).toISOString(),isIncognito:s.is_incognito??!1,components:s.components??{},version:s.version??"inline-1.0.0",..."remote"===s.source||"bundled"===s.source?{source:s.source}:{}};let r,o,_;try{const t=e.resolveUserId?e.resolveUserId(i):void 0;t&&!(0,sentinel_user_id_1.isSentinelUserId)(t)&&(r=t)}catch{}if(!r){const e="string"==typeof s.user_id?s.user_id:void 0;e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(r=e)}if(!r){const e=(0,cookies_1.parseCookie)(i,"__unshared_uid");e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(r=e)}try{o=e.resolveEmailAddress?e.resolveEmailAddress(i):void 0}catch{}o=o??(0,cookies_1.parseCookie)(i,"__unshared_email")??s.email??void 0;try{_=e.resolveSessionId?e.resolveSessionId(i):void 0}catch{}_=_??s.session_id??(0,cookies_1.parseCookie)(i,"__unshared_sid");const d=(0,client_ip_1.extractClientIp)(i),c=i.headers["user-agent"]??"";if(!e.disableBotFilter&&(0,is_bot_1.isBot)(c))return void(0,http_helpers_1.sendJson)(t,200,{success:!0});const u=(n.fingerprint_id&&n.fingerprint_id.length>0?n.fingerprint_id:void 0)??(0,device_id_1.extractDeviceId)(i,e.resolveDeviceId),a=n.fingerprint_id||void 0,l=n.full_hash||void 0,p=(0,secure_1.isSecureRequest)(i)?"; Secure":"",h=[];if(l&&!(0,cookies_1.parseCookie)(i,"__unshared_fingerprint_id")&&h.push(`__unshared_fingerprint_id=${encodeURIComponent(l)}; HttpOnly; Path=/; SameSite=Lax${p}`),a){const e=(0,cookies_1.parseCookie)(i,"__unshared_fp_id");e&&e===a||h.push(`__unshared_fp_id=${encodeURIComponent(a)}; Path=/; SameSite=Lax; Max-Age=31536000${p}`)}if(o&&!(0,cookies_1.parseCookie)(i,"__unshared_email")&&h.push(`__unshared_email=${encodeURIComponent(o)}; HttpOnly; Path=/; SameSite=Lax${p}`),h.length>0){const e=t.getHeader("Set-Cookie");if(e){const i=Array.isArray(e)?[...e]:[String(e)];i.push(...h),t.setHeader("Set-Cookie",i)}else t.setHeader("Set-Cookie",h)}let m;if("string"==typeof s.event_type&&s.event_type)m=s.event_type;else{const e=i.headers.referer??i.headers.referrer;let t="unknown";if("string"==typeof e&&e.length>0)try{const i=new URL(e);t=(i.pathname||"/")+(i.search||"")}catch{}m=t}const v=i.headers["x-idempotency-key"],f="string"==typeof v&&v.length>0?v:void 0,g=Date.now(),y=f?`${f}|${g}`:a&&r?`${(0,util_1.sha256Hex)(`${a}|${r}|${m}`)}|${g}`:void 0;if(r&&e.client.submitFingerprintEvent(n,{userId:r,emailAddress:o,sessionHash:_,eventType:m,ipAddress:d,userAgent:c,idempotencyKey:y}).catch(i=>{e.onError&&e.onError(i,{operation:"submitFingerprintEvent",userId:r,emailAddress:o})}),r&&o&&!e.rateLimitBackoff.isPaused()&&!e.dispatchDedupe.wasRecentlyDispatched(r,m))try{const i=await e.client.processUserEvent({eventType:m,userId:r,emailAddress:o,ipAddress:d,deviceId:u,fingerprintId:a,sessionHash:_??"unknown",userAgent:c});i.success&&i.data?.analysis&&e.verdictCache.update(r,{isFlagged:i.data.analysis.is_user_flagged}),!i.success&&i.error?.retryAfter&&e.rateLimitBackoff.pause(1e3*i.error.retryAfter)}catch(i){e.onError&&e.onError(i,{operation:"processUserEvent",userId:r,emailAddress:o})}(0,http_helpers_1.sendJson)(t,200,{success:!0})}catch(i){e.onError&&e.onError(i,{operation:"submitFingerprintEvent"}),(0,http_helpers_1.sendJson)(t,200,{success:!0})}}}
1
+ "use strict";Object.defineProperty(exports,"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 +1 @@
1
- "use strict";Object.defineProperty(exports,"i",{value:!0}),exports.handleVerifyTrigger=handleVerifyTrigger,exports.handleVerify=handleVerify;const cookies_1=require("../utils/cookies"),device_id_1=require("../utils/device-id"),http_helpers_1=require("../utils/http-helpers");function handleVerifyTrigger(e){return async(r,i)=>{try{const s=resolveEmail(r,r.body??{},e.resolveEmailAddress);if(!s)return void(0,http_helpers_1.sendJson)(i,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}});const t=(0,device_id_1.extractDeviceId)(r,e.resolveDeviceId),c=(0,cookies_1.parseCookie)(r,"__unshared_fingerprint_id")||void 0,o=await e.client.triggerEmailVerification(s,t,{fingerprintId:c});o.success?(0,http_helpers_1.sendJson)(i,200,{success:!0,data:o.data}):(0,http_helpers_1.sendJson)(i,200,{success:!1,error:o.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}})}catch(r){e.onError&&e.onError(r,{operation:"verifyTrigger"}),(0,http_helpers_1.sendJson)(i,200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}})}}}function handleVerify(e){return async(r,i)=>{try{const s=r.body??{},t=resolveEmail(r,s,e.resolveEmailAddress),c=s.code;if(!t||!c)return void(0,http_helpers_1.sendJson)(i,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}});const o=(0,device_id_1.extractDeviceId)(r,e.resolveDeviceId),a=(0,cookies_1.parseCookie)(r,"__unshared_fingerprint_id")||void 0,n=await e.client.verify(t,o,c,{fingerprintId:a});if(n.success){let s;try{s=e.resolveUserId?.(r)}catch{}s&&e.verdictCache.update(s,{isVerified:!0}),(0,http_helpers_1.sendJson)(i,200,{success:!0,data:{verified:!0}})}else(0,http_helpers_1.sendJson)(i,200,{success:!1,error:n.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}})}catch(r){e.onError&&e.onError(r,{operation:"verify"}),(0,http_helpers_1.sendJson)(i,200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}})}}}function resolveEmail(e,r,i){if(i)try{const r=i(e);if(r)return r}catch{}const s=(0,cookies_1.parseCookie)(e,"__unshared_email");if(s)return s;const t=r.email;return"string"==typeof t&&t?t:void 0}
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}