unshared-clientjs-sdk 2.3.0 → 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 (68) hide show
  1. package/README.md +176 -151
  2. package/dist/client.d.ts +54 -25
  3. package/dist/client.js +1 -1
  4. package/dist/esm/client.d.mts +54 -25
  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-processing.d.mts +6 -3
  9. package/dist/esm/middleware/dispatch-processing.mjs +1 -1
  10. package/dist/esm/middleware/index.d.mts +7 -5
  11. package/dist/esm/middleware/index.mjs +1 -1
  12. package/dist/esm/middleware/injection/fingerprint-script.d.mts +7 -0
  13. package/dist/esm/middleware/injection/fingerprint-script.mjs +1 -1
  14. package/dist/esm/middleware/injection/fp-bundle-source.d.mts +5 -4
  15. package/dist/esm/middleware/injection/fp-bundle-source.mjs +1 -1
  16. package/dist/esm/middleware/response-interceptor.d.mts +1 -1
  17. package/dist/esm/middleware/routes/interstitial.d.mts +2 -2
  18. package/dist/esm/middleware/routes/interstitial.mjs +1 -1
  19. package/dist/esm/middleware/routes/submit-fp.d.mts +5 -5
  20. package/dist/esm/middleware/routes/submit-fp.mjs +1 -1
  21. package/dist/esm/middleware/routes/verify.d.mts +3 -3
  22. package/dist/esm/middleware/routes/verify.mjs +1 -1
  23. package/dist/esm/middleware/utils/client-ip.d.mts +1 -1
  24. package/dist/esm/middleware/utils/cookies.d.mts +1 -1
  25. package/dist/esm/middleware/utils/device-id.d.mts +1 -1
  26. package/dist/esm/middleware/utils/device-id.mjs +1 -1
  27. package/dist/esm/middleware/utils/flagged-response.mjs +1 -1
  28. package/dist/esm/middleware/utils/http-helpers.d.mts +1 -1
  29. package/dist/esm/middleware/utils/permanent-device-id.d.mts +7 -0
  30. package/dist/esm/middleware/utils/permanent-device-id.mjs +1 -0
  31. package/dist/esm/middleware/utils/read-json-body.d.mts +3 -0
  32. package/dist/esm/middleware/utils/read-json-body.mjs +1 -0
  33. package/dist/esm/middleware/utils/secure.d.mts +1 -1
  34. package/dist/esm/middleware.d.mts +5 -6
  35. package/dist/esm/middleware.mjs +1 -1
  36. package/dist/esm/shared-types.d.mts +148 -0
  37. package/dist/esm/web/index.d.mts +6 -6
  38. package/dist/esm/web/index.mjs +1 -1
  39. package/dist/esm/web/protection-handler.d.mts +5 -5
  40. package/dist/esm/web/protection-handler.mjs +1 -1
  41. package/dist/esm/web/submit-handler.d.mts +2 -2
  42. package/dist/esm/web/submit-handler.mjs +1 -1
  43. package/dist/esm/web/types.d.mts +1 -1
  44. package/dist/esm/web/web-helpers.d.mts +11 -0
  45. package/dist/esm/web/web-helpers.mjs +1 -1
  46. package/dist/middleware/dispatch-processing.d.ts +3 -0
  47. package/dist/middleware/dispatch-processing.js +1 -1
  48. package/dist/middleware/index.d.ts +2 -0
  49. package/dist/middleware/index.js +1 -1
  50. package/dist/middleware/injection/fingerprint-script.d.ts +7 -0
  51. package/dist/middleware/injection/fingerprint-script.js +1 -1
  52. package/dist/middleware/injection/fp-bundle-source.d.ts +5 -4
  53. package/dist/middleware/injection/fp-bundle-source.js +1 -1
  54. package/dist/middleware/routes/submit-fp.js +1 -1
  55. package/dist/middleware/routes/verify.js +1 -1
  56. package/dist/middleware/utils/permanent-device-id.d.ts +7 -0
  57. package/dist/middleware/utils/permanent-device-id.js +1 -0
  58. package/dist/middleware/utils/read-json-body.d.ts +3 -0
  59. package/dist/middleware/utils/read-json-body.js +1 -0
  60. package/dist/middleware.d.ts +3 -4
  61. package/dist/middleware.js +1 -1
  62. package/dist/shared-types.d.ts +148 -0
  63. package/dist/web/protection-handler.d.ts +1 -1
  64. package/dist/web/protection-handler.js +1 -1
  65. package/dist/web/submit-handler.js +1 -1
  66. package/dist/web/web-helpers.d.ts +11 -0
  67. package/dist/web/web-helpers.js +1 -1
  68. package/package.json +4 -4
@@ -1,8 +1,8 @@
1
- import type { UnsharedRequest, UnsharedResponse } from '../../types';
2
- import type { UnsharedClient } from '../../client';
3
- import type { VerdictCache } from '../verdict-cache';
4
- import type { RateLimitBackoff } from '../rate-limit-backoff';
5
- import type { DispatchDedupe } from '../dispatch-dedupe';
1
+ import type { UnsharedRequest, UnsharedResponse } from '../../types.mjs';
2
+ import type { UnsharedClient } from '../../client.mjs';
3
+ import type { VerdictCache } from '../verdict-cache.mjs';
4
+ import type { RateLimitBackoff } from '../rate-limit-backoff.mjs';
5
+ import type { DispatchDedupe } from '../dispatch-dedupe.mjs';
6
6
  export interface SubmitFingerprintDependencies<TReq extends UnsharedRequest = UnsharedRequest> {
7
7
  client: UnsharedClient;
8
8
  verdictCache: VerdictCache;
@@ -1 +1 @@
1
- import{sha256Hex}from"../../util";import{isBot}from"../utils/is-bot";import{extractClientIp}from"../utils/client-ip";import{parseCookie}from"../utils/cookies";import{extractDeviceId}from"../utils/device-id";import{isSecureRequest}from"../utils/secure";import{isSentinelUserId}from"../utils/sentinel-user-id";import{sendJson}from"../utils/http-helpers";import{runProcessUserEvent,SUBMIT_FP_TIMEOUT_MS}from"../dispatch-processing";export function handleSubmitFingerprint(e){return async(i,t)=>{try{const s=i.body??{},n={full_hash:s.hash??"",fingerprint_id:s.stable_hash??"",timestamp:s.collected_at??(new Date).toISOString(),isIncognito:s.is_incognito??!1,components:s.components??{},version:s.version??"inline-1.0.0",..."remote"===s.source||"bundled"===s.source?{source:s.source}:{}};let o,r,d;try{const t=e.resolveUserId?e.resolveUserId(i):void 0;t&&!isSentinelUserId(t)&&(o=t)}catch{}if(!o){const e="string"==typeof s.user_id?s.user_id:void 0;e&&!isSentinelUserId(e)&&(o=e)}if(!o){const e=parseCookie(i,"__unshared_uid");e&&!isSentinelUserId(e)&&(o=e)}try{r=e.resolveEmailAddress?e.resolveEmailAddress(i):void 0}catch{}r=r??parseCookie(i,"__unshared_email")??s.email??void 0;try{d=e.resolveSessionId?e.resolveSessionId(i):void 0}catch{}d=d??s.session_id??parseCookie(i,"__unshared_sid");const a=extractClientIp(i),c=i.headers["user-agent"]??"";if(!e.disableBotFilter&&isBot(c))return void sendJson(t,200,{success:!0});const p=(n.fingerprint_id&&n.fingerprint_id.length>0?n.fingerprint_id:void 0)??extractDeviceId(i,e.resolveDeviceId),u=n.fingerprint_id||void 0,_=n.full_hash||void 0,m=isSecureRequest(i)?"; Secure":"",l=[];if(_&&!parseCookie(i,"__unshared_fingerprint_id")&&l.push(`__unshared_fingerprint_id=${encodeURIComponent(_)}; HttpOnly; Path=/; SameSite=Lax${m}`),u){const e=parseCookie(i,"__unshared_fp_id");e&&e===u||l.push(`__unshared_fp_id=${encodeURIComponent(u)}; Path=/; SameSite=Lax; Max-Age=31536000${m}`)}if(r&&!parseCookie(i,"__unshared_email")&&l.push(`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${m}`),l.length>0){const e=t.getHeader("Set-Cookie");if(e){const i=Array.isArray(e)?[...e]:[String(e)];i.push(...l),t.setHeader("Set-Cookie",i)}else t.setHeader("Set-Cookie",l)}let f;if("string"==typeof s.event_type&&s.event_type)f=s.event_type;else{const e=i.headers.referer??i.headers.referrer;let t="unknown";if("string"==typeof e&&e.length>0)try{const i=new URL(e);t=(i.pathname||"/")+(i.search||"")}catch{}f=t}const h=i.headers["x-idempotency-key"],v="string"==typeof h&&h.length>0?h:void 0,S=Date.now(),I=v?`${v}|${S}`:u&&o?`${sha256Hex(`${u}|${o}|${f}`)}|${S}`:void 0;o&&e.client.submitFingerprintEvent(n,{userId:o,emailAddress:r,sessionHash:d,eventType:f,ipAddress:a,userAgent:c,idempotencyKey:I}).catch(i=>{e.onError&&e.onError(i,{operation:"submitFingerprintEvent",userId:o,emailAddress:r})}),o&&r&&!e.rateLimitBackoff.isPaused()&&!e.dispatchDedupe.wasRecentlyDispatched(o,f)&&await runProcessUserEvent({client:e.client,cache:e.verdictCache,backoff:e.rateLimitBackoff,onError:e.onError},{eventType:f,userId:o,emailAddress:r,ipAddress:a,deviceId:p,fingerprintId:u,sessionHash:d??"unknown",userAgent:c},SUBMIT_FP_TIMEOUT_MS),sendJson(t,200,{success:!0})}catch(i){e.onError&&e.onError(i,{operation:"submitFingerprintEvent"}),sendJson(t,200,{success:!0})}}}
1
+ import{sha256Hex}from"../../util.mjs";import{isBot}from"../utils/is-bot.mjs";import{extractClientIp}from"../utils/client-ip.mjs";import{parseCookie}from"../utils/cookies.mjs";import{extractDeviceId}from"../utils/device-id.mjs";import{isSecureRequest}from"../utils/secure.mjs";import{isSentinelUserId}from"../utils/sentinel-user-id.mjs";import{sendJson}from"../utils/http-helpers.mjs";import{resolveOrCreatePermanentDeviceId}from"../utils/permanent-device-id.mjs";import{runCheckUser,SUBMIT_FP_TIMEOUT_MS}from"../dispatch-processing.mjs";function asRecord(e){return null===e||"object"!=typeof e||Array.isArray(e)?{}:e}export function handleSubmitFingerprint(e){return async(t,i)=>{try{const s=asRecord(t.body),r=asRecord(s.fingerprint??s),o=asRecord(s.context),n=asRecord(o.identity),a=asRecord(o.identifiers),d=asRecord(o.event),c={...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 p,m,u;try{const i=e.resolveUserId?e.resolveUserId(t):void 0;i&&!isSentinelUserId(i)&&(p=i)}catch{}if(!p){const e="string"==typeof n.user_id?n.user_id:"string"==typeof s.user_id?s.user_id:void 0;e&&!isSentinelUserId(e)&&(p=e)}if(!p){const e=parseCookie(t,"__unshared_uid");e&&!isSentinelUserId(e)&&(p=e)}try{m=e.resolveEmailAddress?e.resolveEmailAddress(t):void 0}catch{}m=m??parseCookie(t,"__unshared_email")??n.email_address??s.email??void 0;try{u=e.resolveSessionId?e.resolveSessionId(t):void 0}catch{}u=u??a.session_hash??s.session_id??parseCookie(t,"__unshared_sid");const f=extractClientIp(t),_=t.headers["user-agent"]??"";if(!e.disableBotFilter&&isBot(_))return void sendJson(i,200,{success:!0,data:{skipped:!0}});const l=(c.fingerprint_id&&c.fingerprint_id.length>0?c.fingerprint_id:void 0)??extractDeviceId(t,e.resolveDeviceId),h=c.fingerprint_id||void 0,y=c.full_hash||void 0,g=resolveOrCreatePermanentDeviceId(t,a.unshared_device_id??s.permanent_device_id),v=isSecureRequest(t)?"; Secure":"",S=[];S.push(`__unshared_device_id=${encodeURIComponent(g.id)}; Path=/; SameSite=Lax; Max-Age=34560000${v}`);const I=parseCookie(t,"__unshared_fingerprint_id")||y;if(I&&S.push(`__unshared_fingerprint_id=${encodeURIComponent(I)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=31536000${v}`),h&&S.push(`__unshared_fp_id=${encodeURIComponent(h)}; Path=/; SameSite=Lax; Max-Age=31536000${v}`),m&&S.push(`__unshared_email=${encodeURIComponent(m)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=31536000${v}`),S.length>0){const e=i.getHeader("Set-Cookie");if(e){const t=Array.isArray(e)?[...e]:[String(e)];t.push(...S),i.setHeader("Set-Cookie",t)}else i.setHeader("Set-Cookie",S)}let j;if("string"==typeof d.type&&d.type)j=d.type;else if("string"==typeof s.event_type&&s.event_type)j=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{}j=i}const C=t.headers["x-idempotency-key"],x="string"==typeof C&&C.length>0?C:void 0,R=Date.now(),k=x?`${x}|${R}`:h&&p?`${sha256Hex(`${h}|${p}|${j}`)}|${R}`:void 0,b={...a,unshared_device_id:g.id,...l?{device_id:l}:{},...h?{stable_hash:h}:{},...y?{full_hash:y}:{},...u?{session_hash:u}:{}},U="string"==typeof asRecord(o.sdk).name?asRecord(o.sdk).name:void 0,$="string"==typeof U&&U.startsWith("unshared-")&&Object.prototype.hasOwnProperty.call(s,"fingerprint")&&s.context&&"object"==typeof s.context&&!Array.isArray(s.context)&&o.identity&&"object"==typeof o.identity&&!Array.isArray(o.identity)&&o.identifiers&&"object"==typeof o.identifiers&&!Array.isArray(o.identifiers)&&o.event&&"object"==typeof o.event&&!Array.isArray(o.event),A=!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),M=$||A?{...s,fingerprint:r,context:{...o,sdk:asRecord(o.sdk),identity:{...n,...p?{user_id:p}:{},...m?{email_address:m}:{}},identifiers:b,event:{...d,type:j,ip_address:f,user_agent:_}}}:s,O=await e.client.submitFingerprint(M,{idempotencyKey:k});O.success||e.onError?.(O.error,{operation:"submitFingerprintEvent",userId:p,emailAddress:m}),p&&m&&!e.rateLimitBackoff.isPaused()&&!e.dispatchDedupe.wasRecentlyDispatched(p,j)&&await runCheckUser({client:e.client,cache:e.verdictCache,backoff:e.rateLimitBackoff,onError:e.onError},{user_id:p,email_address:m,identifiers:b},p,m,SUBMIT_FP_TIMEOUT_MS),O.success?sendJson(i,200,{success:!0,data:O.data}):sendJson(i,200,{success:!1,error:O.error})}catch(t){e.onError&&e.onError(t,{operation:"submitFingerprintEvent"}),sendJson(i,200,{success:!0,data:{accepted:!1}})}}}
@@ -1,6 +1,6 @@
1
- import type { UnsharedRequest, UnsharedResponse } from '../../types';
2
- import type { UnsharedClient } from '../../client';
3
- import type { VerdictCache } from '../verdict-cache';
1
+ import type { UnsharedRequest, UnsharedResponse } from '../../types.mjs';
2
+ import type { UnsharedClient } from '../../client.mjs';
3
+ import type { VerdictCache } from '../verdict-cache.mjs';
4
4
  export interface VerificationDependencies<TReq extends UnsharedRequest = UnsharedRequest> {
5
5
  client: UnsharedClient;
6
6
  verdictCache: VerdictCache;
@@ -1 +1 @@
1
- import{parseCookie}from"../utils/cookies";import{extractDeviceId}from"../utils/device-id";import{sendJson}from"../utils/http-helpers";export function handleVerifyTrigger(e){return async(r,i)=>{try{const s=resolveEmail(r,r.body??{},e.resolveEmailAddress);if(!s)return void sendJson(i,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}});const o=extractDeviceId(r,e.resolveDeviceId),n=parseCookie(r,"__unshared_fingerprint_id")||void 0,t=await e.client.triggerEmailVerification(s,o,{fingerprintId:n});t.success?sendJson(i,200,{success:!0,data:t.data}):sendJson(i,200,{success:!1,error:t.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}})}catch(r){e.onError&&e.onError(r,{operation:"verifyTrigger"}),sendJson(i,200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}})}}}export function handleVerify(e){return async(r,i)=>{try{const s=r.body??{},o=resolveEmail(r,s,e.resolveEmailAddress),n=s.code;if(!o||!n)return void sendJson(i,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}});const t=extractDeviceId(r,e.resolveDeviceId),c=parseCookie(r,"__unshared_fingerprint_id")||void 0,a=await e.client.verify(o,t,n,{fingerprintId:c});if(a.success){let s;try{s=e.resolveUserId?.(r)}catch{}s&&e.verdictCache.update(s,{isVerified:!0}),sendJson(i,200,{success:!0,data:{verified:!0}})}else sendJson(i,200,{success:!1,error:a.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}})}catch(r){e.onError&&e.onError(r,{operation:"verify"}),sendJson(i,200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}})}}}function resolveEmail(e,r,i){if(i)try{const r=i(e);if(r)return r}catch{}const s=parseCookie(e,"__unshared_email");if(s)return s;const o=r.email;return"string"==typeof o&&o?o:void 0}
1
+ import{parseCookie}from"../utils/cookies.mjs";import{extractDeviceId}from"../utils/device-id.mjs";import{sendJson}from"../utils/http-helpers.mjs";import{extractPermanentDeviceId}from"../utils/permanent-device-id.mjs";import{isSecureRequest}from"../utils/secure.mjs";const VERIFICATION_COOKIE="__unshared_verification_id";function appendCookie(e,i){const s=e.getHeader("Set-Cookie"),r=s?Array.isArray(s)?[...s]:[String(s)]:[];r.push(i),e.setHeader("Set-Cookie",r)}export function handleVerifyTrigger(e){return async(i,s)=>{try{const r=resolveEmail(i,i.body??{},e.resolveEmailAddress);if(!r)return void sendJson(s,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}});const o=extractDeviceId(i,e.resolveDeviceId),t=parseCookie(i,"__unshared_fp_id")||void 0,n=parseCookie(i,"__unshared_fingerprint_id")||void 0,a=extractPermanentDeviceId(i),c=await e.client.triggerEmailVerification(r,o,{fingerprintId:t,fullHash:n,permanentDeviceId:a});if(c.success){if(c.data?.verification_id){const e=isSecureRequest(i)?"; Secure":"";appendCookie(s,`${VERIFICATION_COOKIE}=${encodeURIComponent(c.data.verification_id)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=600${e}`)}sendJson(s,200,{success:!0,data:c.data})}else sendJson(s,200,{success:!1,error:c.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}})}catch(i){e.onError&&e.onError(i,{operation:"verifyTrigger"}),sendJson(s,200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}})}}}export function handleVerify(e){return async(i,s)=>{try{const r=(i.body??{}).code,o=parseCookie(i,VERIFICATION_COOKIE);if(!o||!r)return void sendJson(s,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Verification ID and code are required"}});const t=await e.client.verifyEmail({verification_id:o,code:r});if(t.success&&t.data?.verified){let r,o=!1;try{r=e.resolveUserId?.(i)}catch{}const n=resolveEmail(i,{},e.resolveEmailAddress),a=extractDeviceId(i,e.resolveDeviceId),c=parseCookie(i,"__unshared_fp_id")||void 0,d=parseCookie(i,"__unshared_fingerprint_id")||void 0,u=extractPermanentDeviceId(i);if(r&&n){const i={...a?{device_id:a}:{},...c?{stable_hash:c}:{},...d?{full_hash:d}:{},...u?{unshared_device_id:u}:{}},s=await e.client.checkUser({user_id:r,email_address:n,identifiers:i},{maxRetries:0});s.success&&s.data&&!s.failedOpen&&!s.data.is_user_flagged&&(e.verdictCache.update(r,{isVerified:!0}),o=!0)}const f=isSecureRequest(i)?"; Secure":"";appendCookie(s,`${VERIFICATION_COOKIE}=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0${f}`),o&&s.setHeader("X-Unshared-Enforcement-Reconciled","clean"),sendJson(s,200,{success:!0,data:t.data})}else t.success?sendJson(s,200,{success:!0,data:t.data}):sendJson(s,200,{success:!1,error:t.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}})}catch(i){e.onError&&e.onError(i,{operation:"verify"}),sendJson(s,200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}})}}}function resolveEmail(e,i,s){if(s)try{const i=s(e);if(i)return i}catch{}const r=parseCookie(e,"__unshared_email");if(r)return r;const o=i.email;return"string"==typeof o&&o?o:void 0}
@@ -1,4 +1,4 @@
1
- import type { UnsharedRequest } from '../../types';
1
+ import type { UnsharedRequest } from '../../types.mjs';
2
2
  /**
3
3
  * Extract the real client IP from proxy headers, falling back to req.ip or socket address.
4
4
  * Checked in order: CF-Connecting-IP (Cloudflare) → X-Real-IP (nginx/ALB) → req.ip → socket.
@@ -1,4 +1,4 @@
1
- import type { UnsharedRequest } from '../../types';
1
+ import type { UnsharedRequest } from '../../types.mjs';
2
2
  /**
3
3
  * Reads a single cookie value from the raw Cookie header.
4
4
  * Works without cookie-parser middleware.
@@ -1,4 +1,4 @@
1
- import type { UnsharedRequest } from '../../types';
1
+ import type { UnsharedRequest } from '../../types.mjs';
2
2
  /**
3
3
  * Resolves device ID from: custom resolver → X-Device-Id header → __unshared_fp_id cookie.
4
4
  *
@@ -1 +1 @@
1
- import{parseCookie}from"./cookies";export function extractDeviceId(e,r){return extractDeviceIdOrUndefined(e,r)??"unknown"}export function extractDeviceIdOrUndefined(e,r){if(r)try{const t=r(e);if(t)return t}catch{}const t=e.headers["x-device-id"];if("string"==typeof t&&t)return t;return parseCookie(e,"__unshared_fp_id")||void 0}
1
+ import{parseCookie}from"./cookies.mjs";export function extractDeviceId(e,r){return extractDeviceIdOrUndefined(e,r)??"unknown"}export function extractDeviceIdOrUndefined(e,r){if(r)try{const t=r(e);if(t)return t}catch{}const t=e.headers["x-device-id"];if("string"==typeof t&&t)return t;return parseCookie(e,"__unshared_fp_id")||void 0}
@@ -1 +1 @@
1
- import{maskEmail}from"./mask-email";export const ACCOUNT_FLAGGED_ERROR="account_flagged";export function flaggedResponse(a){return{error:"account_flagged",email:maskEmail(a)}}
1
+ import{maskEmail}from"./mask-email.mjs";export const ACCOUNT_FLAGGED_ERROR="account_flagged";export function flaggedResponse(a){return{error:"account_flagged",email:maskEmail(a)}}
@@ -1,4 +1,4 @@
1
- import type { UnsharedResponse } from '../../types';
1
+ import type { UnsharedResponse } from '../../types.mjs';
2
2
  /**
3
3
  * Send a JSON response. Framework-agnostic replacement for Express's
4
4
  * `res.status(code).json(data)`.
@@ -0,0 +1,7 @@
1
+ import type { UnsharedRequest } from '../../types.mjs';
2
+ export declare function isPermanentDeviceId(value: unknown): value is string;
3
+ export declare function extractPermanentDeviceId(req: UnsharedRequest): string | undefined;
4
+ export declare function resolveOrCreatePermanentDeviceId(req: UnsharedRequest, candidate?: unknown): {
5
+ id: string;
6
+ shouldSetCookie: boolean;
7
+ };
@@ -0,0 +1 @@
1
+ import{randomUUID}from"crypto";import{parseCookie}from"./cookies.mjs";const PERMANENT_DEVICE_ID_PATTERN=/^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;export function isPermanentDeviceId(e){return"string"==typeof e&&PERMANENT_DEVICE_ID_PATTERN.test(e)}export function extractPermanentDeviceId(e){const i=parseCookie(e,"__unshared_device_id");if(isPermanentDeviceId(i))return i;const r=e.headers["x-permanent-device-id"];return isPermanentDeviceId(r)?r:void 0}export function resolveOrCreatePermanentDeviceId(e,i){const r=parseCookie(e,"__unshared_device_id");if(isPermanentDeviceId(r))return{id:r,shouldSetCookie:!1};const n=e.headers["x-permanent-device-id"];return{id:isPermanentDeviceId(n)?n:isPermanentDeviceId(i)?i:`upid_${randomUUID()}`,shouldSetCookie:!0}}
@@ -0,0 +1,3 @@
1
+ import type { UnsharedRequest, UnsharedResponse } from '../../types.mjs';
2
+ /** Only call for SDK-owned routes, never for application requests. */
3
+ export declare function readJsonBody(req: UnsharedRequest, res: UnsharedResponse): Promise<boolean>;
@@ -0,0 +1 @@
1
+ import{sendJson}from"./http-helpers.mjs";const MAX_REQUEST_BYTES=1<<20;export async function readJsonBody(e,t){const o=e,r=(e,r,s)=>{if(!o.destroyed&&!o.readableEnded&&o.resume&&o.once&&o.removeListener&&o.destroy){const e=setTimeout(()=>o.destroy(),3e4);e.unref();const t=()=>{clearTimeout(e),o.removeListener("end",t),o.removeListener("close",t),o.removeListener("error",t)};o.once("end",t),o.once("close",t),o.once("error",t),o.resume()}return sendJson(t,e,{success:!1,error:{code:r,message:s}}),!1},s=()=>r(413,"REQUEST_TOO_LARGE","Request body exceeds 1 MiB");if(void 0!==e.body)try{return!(Buffer.byteLength(JSON.stringify(e.body),"utf8")>1048576)||s()}catch{return r(400,"INVALID_JSON","Request body is not valid JSON.")}return o.on&&o.removeListener&&o.pause?o.destroyed||o.readableEnded?r(400,"INVALID_JSON","Request body is unavailable or incomplete."):Number(e.headers["content-length"])>1048576?s():e.headers["content-encoding"]&&"identity"!==e.headers["content-encoding"]?r(415,"UNSUPPORTED_ENCODING","Request body must use identity encoding."):new Promise(t=>{const s=[];let d=0,n=!1;const u=(e,d,u)=>{n||(n=!0,clearTimeout(N),o.removeListener("data",a),o.removeListener("end",c),o.removeListener("aborted",i),o.removeListener("error",i),o.removeListener("close",i),s.length=0,t(!e||r(e,d,u)))},i=()=>u(400,"INVALID_JSON","Request body is unavailable or incomplete."),a=e=>{const t=Buffer.isBuffer(e)?e:Buffer.from(e,"utf8");d+=t.length,d>1048576?u(413,"REQUEST_TOO_LARGE","Request body exceeds 1 MiB"):s.push(t)},c=()=>{try{e.body=JSON.parse(Buffer.concat(s,d).toString("utf8"))}catch{return void u(400,"INVALID_JSON","Request body is not valid JSON.")}u()},N=setTimeout(()=>u(408,"REQUEST_TIMEOUT","Timed out reading request body."),3e4);N.unref(),o.on("data",a),o.on("end",c),o.on("aborted",i),o.on("error",i),o.on("close",i)}):r(400,"BODY_PARSER_MISSING","Provide a readable Node request stream or a parsed JSON body.")}
@@ -1,3 +1,3 @@
1
- import type { UnsharedRequest } from '../../types';
1
+ import type { UnsharedRequest } from '../../types.mjs';
2
2
  /** Returns true if the request arrived over HTTPS (direct or via reverse proxy). */
3
3
  export declare function isSecureRequest(req: UnsharedRequest): boolean;
@@ -1,5 +1,5 @@
1
- import type { UnsharedRequest, UnsharedResponse, UnsharedNextFunction } from './types';
2
- import type { UnsharedClient } from './client';
1
+ import type { UnsharedRequest, UnsharedResponse, UnsharedNextFunction } from './types.mjs';
2
+ import type { UnsharedClient } from './client.mjs';
3
3
  export interface MiddlewareOptions<TReq extends UnsharedRequest = UnsharedRequest> {
4
4
  /** Override userId extractor. Falls back to req.body.user_id. */
5
5
  userIdExtractor?: (req: TReq) => string | undefined;
@@ -38,7 +38,6 @@ export interface MiddlewareOptions<TReq extends UnsharedRequest = UnsharedReques
38
38
  * @example
39
39
  * ```typescript
40
40
  * assertTrustProxy(app); // throws at startup if not set
41
- * app.use(express.json());
42
41
  * app.use(createUnsharedMiddleware(client, options));
43
42
  * ```
44
43
  */
@@ -51,8 +50,8 @@ export declare function assertTrustProxy(app: any): void;
51
50
  * Passes all other requests to next().
52
51
  *
53
52
  * **Prerequisites:**
54
- * - Mount `express.json()` (or equivalent body-parser) **before** this middleware,
55
- * otherwise `req.body` will be undefined and every request will return 400.
53
+ * - Existing default JSON parser order is supported by the browser's 100 KiB
54
+ * proxy limit. Unparsed SDK routes are also read automatically up to 1 MiB.
56
55
  * - For cross-origin frontends, pass `corsOrigins` instead of configuring CORS
57
56
  * separately — the middleware handles OPTIONS preflight automatically.
58
57
  * - `user_id` is automatically scrubbed from `req.body` after it is read, so
@@ -67,10 +66,10 @@ export declare function assertTrustProxy(app: any): void;
67
66
  * ```typescript
68
67
  * import { createUnsharedMiddleware } from "unshared-clientjs-sdk";
69
68
  *
70
- * app.use(express.json()); // must come first
71
69
  * app.use(createUnsharedMiddleware(client, {
72
70
  * userIdExtractor: (req) => req.user?.id,
73
71
  * }));
72
+ * app.use(express.json()); // application routes keep their own default limit
74
73
  * ```
75
74
  */
76
75
  export declare function createUnsharedMiddleware<TReq extends UnsharedRequest = UnsharedRequest>(client: UnsharedClient, options?: MiddlewareOptions<TReq>): (req: TReq, res: UnsharedResponse, next: UnsharedNextFunction) => Promise<void>;
@@ -1 +1 @@
1
- import{sendJson,sendEmpty,getRequestPath}from"./middleware/utils/http-helpers";export function assertTrustProxy(e){if(!e?.get?.("trust proxy"))throw new Error('[unshared-labs] Express "trust proxy" is not set.\n\n Fix: add this line before mounting any middleware:\n app.set("trust proxy", 1);\n\n Why: without it, req.ip returns the proxy/load-balancer IP instead of\n the real client IP, which degrades account-sharing detection accuracy.\n Set to 1 if behind a single reverse proxy (Nginx, ALB, CloudFront),\n or the number of trusted proxies in your chain.')}export function createUnsharedMiddleware(e,s){const{userIdExtractor:r,eventTypeExtractor:t,sessionIdExtractor:n,ipAddressExtractor:o,defaultEventType:i="browser_event",routePrefix:d="/unshared",corsOrigins:c}=s??{},a=`${d}/submit-fingerprint-event`,u=c?Array.isArray(c)?c:[c]:null;return async(s,d,c)=>{const l=getRequestPath(s.url);if(u&&l===a){const e=s.headers.origin??"",r=u.includes("*");if((r||u.includes(e))&&(d.setHeader("Access-Control-Allow-Origin",r?"*":e),d.setHeader("Access-Control-Allow-Methods","POST, OPTIONS"),d.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id")),"OPTIONS"===s.method)return void sendEmpty(d,204)}if("POST"===s.method&&l===a)try{if(void 0===s.body)return void sendJson(d,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."}});const c=s.body??{};if(!c.hash||!c.stable_hash||!c.collected_at)return void sendJson(d,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Missing required fingerprint fields: hash, stable_hash, collected_at"}});if(!s.headers["x-session-id"])return void sendJson(d,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Missing required header: X-Session-Id"}});const a={full_hash:c.hash,fingerprint_id:c.stable_hash,timestamp:c.collected_at,isIncognito:c.is_incognito??!1,components:c.components??{},version:c.version??"unknown"};let u,l,h,p;try{u=(r?r(s):void 0)??c.user_id}catch{u=c.user_id}if(s.body&&"object"==typeof s.body&&"user_id"in s.body&&delete s.body.user_id,!u)return void sendJson(d,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Missing required field: user_id"}});try{l=(t?t(s):void 0)??c.event_type??i}catch{l=c.event_type??i}try{h=(n?n(s):void 0)??s.headers["x-session-id"]?.toString()??c.session_id}catch{h=s.headers["x-session-id"]?.toString()??c.session_id}const f=s.ip??s.socket?.remoteAddress??"";try{p=(o?o(s):void 0)??f}catch{p=f}const y=await e.submitFingerprintEvent(a,{userId:u,sessionHash:h,eventType:l,ipAddress:p});if(!y.success)return void sendJson(d,200,{success:!1,error:{code:"UPSTREAM_ERROR",message:y.error?.message??"Upstream request failed"}});sendJson(d,202,{success:!0,data:y.data})}catch(e){sendJson(d,200,{success:!1,error:{code:"MIDDLEWARE_ERROR",message:e instanceof Error?e.message:"Middleware error"}})}else c()}}
1
+ import{sendJson,sendEmpty,getRequestPath}from"./middleware/utils/http-helpers.mjs";import{readJsonBody}from"./middleware/utils/read-json-body.mjs";export function assertTrustProxy(e){if(!e?.get?.("trust proxy"))throw new Error('[unshared-labs] Express "trust proxy" is not set.\n\n Fix: add this line before mounting any middleware:\n app.set("trust proxy", 1);\n\n Why: without it, req.ip returns the proxy/load-balancer IP instead of\n the real client IP, which degrades account-sharing detection accuracy.\n Set to 1 if behind a single reverse proxy (Nginx, ALB, CloudFront),\n or the number of trusted proxies in your chain.')}export function createUnsharedMiddleware(e,t){const{userIdExtractor:r,eventTypeExtractor:o,sessionIdExtractor:s,ipAddressExtractor:n,defaultEventType:i="browser_event",routePrefix:c="/unshared",corsOrigins:d}=t??{},a=`${c}/submit-fingerprint-event`,y=d?Array.isArray(d)?d:[d]:null;return async(t,c,d)=>{const p=getRequestPath(t.url);if(y&&p===a){const e=t.headers.origin??"",r=y.includes("*");if((r||y.includes(e))&&(c.setHeader("Access-Control-Allow-Origin",r?"*":e),c.setHeader("Access-Control-Allow-Methods","POST, OPTIONS"),c.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Permanent-Device-Id")),"OPTIONS"===t.method)return void sendEmpty(c,204)}if("POST"===t.method&&p===a)try{if(!await readJsonBody(t,c))return;const d=t.body??{};if(!d||"object"!=typeof d||Array.isArray(d))return void sendJson(c,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Request body must be a JSON object"}});const a=d.context&&"object"==typeof d.context?d.context:{},y=a.identity&&"object"==typeof a.identity?a.identity:{},p=a.identifiers&&"object"==typeof a.identifiers?a.identifiers:{},u=a.event&&"object"==typeof a.event?a.event:{};let f,l,h,b;try{f=(r?r(t):void 0)??y.user_id}catch{f=y.user_id}try{l=(o?o(t):void 0)??u.type??i}catch{l=u.type??i}try{h=(s?s(t):void 0)??t.headers["x-session-id"]?.toString()??p.session_hash}catch{h=t.headers["x-session-id"]?.toString()??p.session_hash}const m=t.ip??t.socket?.remoteAddress??"";try{b=(n?n(t):void 0)??m}catch{b=m}const x=t.headers["x-permanent-device-id"]?.toString()??p.unshared_device_id,A=a.sdk&&"object"==typeof a.sdk?a.sdk.name:void 0,g="string"==typeof A&&A.startsWith("unshared-")&&d.fingerprint&&"object"==typeof d.fingerprint&&d.context&&"object"==typeof d.context&&!Array.isArray(d.context)&&d.context.identity&&"object"==typeof d.context.identity&&!Array.isArray(d.context.identity)&&d.context.identifiers&&"object"==typeof d.context.identifiers&&!Array.isArray(d.context.identifiers)&&d.context.event&&"object"==typeof d.context.event&&!Array.isArray(d.context.event),j=!Object.prototype.hasOwnProperty.call(d,"context")&&"string"==typeof d.hash&&"string"==typeof d.stable_hash&&"string"==typeof d.collected_at&&d.components&&"object"==typeof d.components&&!Array.isArray(d.components),v=g||j?{...d,context:{...a,identity:{...y,...f?{user_id:f}:{}},identifiers:{...p,...h?{session_hash:h}:{},...x?{unshared_device_id:x}:{}},event:{...u,type:l,...b?{ip_address:b}:{},user_agent:t.headers["user-agent"]?.toString()??""}}}:d,E=await e.submitFingerprint(v,{idempotencyKey:t.headers["x-idempotency-key"]?.toString()});if(!E.success)return void sendJson(c,200,{success:!1,error:E.error??{code:"UPSTREAM_ERROR",message:"Upstream request failed"}});sendJson(c,202,{success:!0,data:E.data})}catch(e){sendJson(c,200,{success:!1,error:{code:"MIDDLEWARE_ERROR",message:e instanceof Error?e.message:"Middleware error"}})}else d()}}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Canonical wire-format for fingerprint data transmitted from the browser SDK to the backend.
3
+ *
4
+ * Single source of truth shared by:
5
+ * - @unshared-labs/frontend-fingerprint (serializer: _buildBody in browser.ts)
6
+ * - @unshared-labs/sdk (submitFingerprintEvent in client.ts)
7
+ *
8
+ * SCHEMA AUTHORITY: The browser SDK serializer is the writer. The Node.js SDK is the
9
+ * reader. Any field added here must be produced by the browser SDK and consumed by the
10
+ * server SDK. Do not add fields that only one side uses.
11
+ */
12
+ export type JSONPrimitive = string | number | boolean | null;
13
+ export type JSONValue = JSONPrimitive | JSONObject | JSONValue[];
14
+ export interface JSONObject {
15
+ [key: string]: JSONValue;
16
+ }
17
+
18
+ export interface BrowserStorageEntry extends JSONObject {
19
+ name: string;
20
+ value: string;
21
+ }
22
+
23
+ export interface BrowserStorageSnapshot extends JSONObject {
24
+ cookies: BrowserStorageEntry[];
25
+ local_storage: BrowserStorageEntry[];
26
+ session_storage: BrowserStorageEntry[];
27
+ }
28
+
29
+ export interface EncryptedEventEnvelope {
30
+ encrypted_data: string;
31
+ }
32
+
33
+ export interface FingerprintEventContext extends JSONObject {
34
+ sdk: JSONObject;
35
+ identity: JSONObject;
36
+ identifiers: JSONObject;
37
+ event: JSONObject;
38
+ browser_storage?: BrowserStorageSnapshot;
39
+ }
40
+
41
+ export type V3Identifiers = Record<string, string>;
42
+
43
+ export interface V3Success<T> {
44
+ success: true;
45
+ data: T;
46
+ }
47
+
48
+ export interface V3Error {
49
+ success: false;
50
+ error: {
51
+ code: string;
52
+ message: string;
53
+ retryAfter?: number;
54
+ nextAllowedAt?: string;
55
+ };
56
+ }
57
+
58
+ export type V3Result<T> = V3Success<T> | V3Error;
59
+
60
+ export interface IngestResult {
61
+ event_id: string;
62
+ collected_at: string;
63
+ endpoint_version: 'v3';
64
+ }
65
+
66
+ export interface TriggerEmailRequest {
67
+ email_address: string;
68
+ identifiers: V3Identifiers;
69
+ }
70
+
71
+ export interface TriggerEmailResult {
72
+ verification_id: string;
73
+ next_allowed_at: string;
74
+ retry_after_seconds: number;
75
+ }
76
+
77
+ export interface VerifyEmailRequest {
78
+ verification_id: string;
79
+ code: string;
80
+ }
81
+
82
+ export type VerifyEmailResult =
83
+ | { verified: true; verification_scope: 'email' }
84
+ | { verified: false; verification_scope: 'email'; reason: 'invalid_code' };
85
+
86
+ export interface CheckUserRequest {
87
+ user_id?: string;
88
+ email_address: string;
89
+ identifiers: V3Identifiers;
90
+ }
91
+
92
+ export type CheckDecisionCode =
93
+ | 'FLAGGED_RISK_MATCH'
94
+ | 'NO_ENFORCING_RISK'
95
+ | 'NO_IDENTITY_MATCH'
96
+ | 'IDENTITY_UNCERTAIN'
97
+ | 'CHECK_UNAVAILABLE';
98
+
99
+ export interface CheckUserResult {
100
+ is_user_flagged: boolean;
101
+ decision_id: string;
102
+ decision_code: CheckDecisionCode;
103
+ }
104
+
105
+ export interface FingerprintEventPayload extends JSONObject {
106
+ fingerprint: JSONObject;
107
+ context: FingerprintEventContext;
108
+ }
109
+
110
+ export interface FingerprintWireFormat {
111
+ [key: string]: unknown;
112
+ /**
113
+ * Primary device identifier — stable hash excluding volatile signals (audio, timing).
114
+ * Consistent across normal and incognito sessions on the same device.
115
+ * 32-character hex string.
116
+ */
117
+ fingerprint_id: string;
118
+ /**
119
+ * Full hash including all active signals (volatile signals included).
120
+ * Maximum entropy; may vary slightly between sessions on the same device.
121
+ * 32-character hex string.
122
+ */
123
+ full_hash: string;
124
+ /**
125
+ * Raw browser signal data keyed by signal name (e.g. `webgl`, `hardware`, `audio`).
126
+ *
127
+ * This blob is forwarded as-is in the `components` JSON field and stored verbatim
128
+ * in a `components JSONB` column on `fingerprint_events`. No server-side decomposition
129
+ * is performed; the full component tree is preserved for querying.
130
+ */
131
+ components: Record<string, unknown>;
132
+ /** ISO timestamp when the fingerprint was collected. */
133
+ timestamp: string;
134
+ /** Whether private/incognito browsing was detected at collection time. */
135
+ isIncognito: boolean;
136
+ /** Fingerprint library semver, e.g. "1.0.0". */
137
+ version: string;
138
+ /**
139
+ * Where the fingerprint agent that produced this result was loaded from:
140
+ * - `'remote'` — the runtime-loaded UMD (S3 directly in browser direct mode,
141
+ * or the Node SDK's same-origin proxy of S3).
142
+ * - `'bundled'` — the copy compiled into the SDK, used as the fallback when
143
+ * the remote load failed/timed out.
144
+ * Optional: older SDK artifacts and the mobile SDKs omit it, so the reader
145
+ * must tolerate its absence. Observability for S3 fast-path vs fallback rates.
146
+ */
147
+ source?: 'remote' | 'bundled';
148
+ }
@@ -10,9 +10,9 @@
10
10
  * is compatible with all frameworks that expose `http.IncomingMessage`/
11
11
  * `http.ServerResponse` objects.
12
12
  */
13
- export { createWebSubmitHandler } from './submit-handler';
14
- export { createWebProtectionMiddleware } from './protection-handler';
15
- export { flaggedResponse, ACCOUNT_FLAGGED_ERROR } from '../middleware/utils/flagged-response';
16
- export type { WebHandler, WebMiddleware, WebSubmitOptions, WebProtectionConfig, } from './types';
17
- export { VerdictCache } from '../middleware/verdict-cache';
18
- export type { Verdict } from '../middleware/verdict-cache';
13
+ export { createWebSubmitHandler } from './submit-handler.mjs';
14
+ export { createWebProtectionMiddleware } from './protection-handler.mjs';
15
+ export { flaggedResponse, ACCOUNT_FLAGGED_ERROR } from '../middleware/utils/flagged-response.mjs';
16
+ export type { WebHandler, WebMiddleware, WebSubmitOptions, WebProtectionConfig, } from './types.mjs';
17
+ export { VerdictCache } from '../middleware/verdict-cache.mjs';
18
+ export type { Verdict } from '../middleware/verdict-cache.mjs';
@@ -1 +1 @@
1
- export{createWebSubmitHandler}from"./submit-handler";export{createWebProtectionMiddleware}from"./protection-handler";export{flaggedResponse,ACCOUNT_FLAGGED_ERROR}from"../middleware/utils/flagged-response";export{VerdictCache}from"../middleware/verdict-cache";
1
+ export{createWebSubmitHandler}from"./submit-handler.mjs";export{createWebProtectionMiddleware}from"./protection-handler.mjs";export{flaggedResponse,ACCOUNT_FLAGGED_ERROR}from"../middleware/utils/flagged-response.mjs";export{VerdictCache}from"../middleware/verdict-cache.mjs";
@@ -1,7 +1,7 @@
1
- import type { UnsharedClient } from '../client';
2
- import { VerdictCache } from '../middleware/verdict-cache';
3
- import type { Verdict } from '../middleware/verdict-cache';
4
- import type { WebMiddleware, WebProtectionConfig } from './types';
1
+ import type { UnsharedClient } from '../client.mjs';
2
+ import { VerdictCache } from '../middleware/verdict-cache.mjs';
3
+ import type { Verdict } from '../middleware/verdict-cache.mjs';
4
+ import type { WebMiddleware, WebProtectionConfig } from './types.mjs';
5
5
  /**
6
6
  * Web Standard equivalent of `unsharedBoundToUser` from src/middleware/index.ts.
7
7
  *
@@ -49,7 +49,7 @@ interface InjectWarnState {
49
49
  * single middleware instance; when omitted, the module-level default is used.
50
50
  */
51
51
  declare function injectIntoHtmlResponse(response: Response, fingerprintScript: string, extraCookies?: string[], warnState?: InjectWarnState): Promise<Response>;
52
- declare function fetchAndCacheVerdict(client: UnsharedClient, cache: VerdictCache, userId: string, emailAddress: string, deviceId: string, fingerprintId: string | undefined, sessionId: string, timeoutMs?: number, reportFailOpen?: (reason: 'timeout' | 'http_error' | 'no_device_id', status?: number) => void): Promise<Verdict>;
52
+ declare function fetchAndCacheVerdict(client: UnsharedClient, cache: VerdictCache, userId: string, emailAddress: string, deviceId: string, fingerprintId: string | undefined, permanentDeviceId: string | undefined, sessionId: string, timeoutMs?: number, reportFailOpen?: (reason: 'timeout' | 'http_error' | 'no_device_id', status?: number) => void): Promise<Verdict>;
53
53
  export declare const __test_fetchAndCacheVerdict: typeof fetchAndCacheVerdict;
54
54
  export declare const __test_injectIntoHtmlResponse: typeof injectIntoHtmlResponse;
55
55
  export declare function __test_resetInjectWarning(): void;
@@ -1 +1 @@
1
- import{sha256HexWeb}from"../webcrypto";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{runProcessUserEvent,DISPATCH_TIMEOUT_MS,SUBMIT_FP_TIMEOUT_MS}from"../middleware/dispatch-processing";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:r,emailAddress:s,routePrefix:n="/__unshared",corsOrigins:i,cacheTTL:o=6e4,skipPaths:a,includePathPrefix:d,sessionId:c,deviceId:u,fingerprintSdkBundle:l="",onFlagged:p,onError:m,onFailOpen:f,disableBotFilter:h=!1,checkUserTimeoutMs:_=CHECK_USER_TIMEOUT_MS,blockFlagged:g=!1,flaggedMode:I,autoInterstitial:R=!1,interstitialFlowType:v="email_verification"}=t,S=I??(g?"gate":void 0),y=R||"overlay"===S,w=e=>{if(f)try{f(e)}catch{}},E=Date.now().toString(36),T=void 0!==S||y,C="gate"===S?"flaggedMode 'gate' (blockFlagged)":"overlay"===S?"flaggedMode 'overlay'":"autoInterstitial";if(T&&!l)throw new Error(`[Unshared] ${C} requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the interstitial).`);if(T&&"/__unshared"!==n)throw new Error(`[Unshared] ${C} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const A="gate"===S?generateGatePage(n,E):"",k=new VerdictCache(o),x=new Map,F=new RateLimitBackoff,U=new DispatchDedupe,j=generateFingerprintScript(n,E,{autoInterstitial:y,interstitialFlowType:v}),b={emitted:!1},q=`${n}/fp.js`,P=`${n}/submit-fp`,O=`${n}/verify-trigger`,D=`${n}/verify`,H=`${n}/status`,M=i?Array.isArray(i)?i:[i]:null;return async function(t,i,o){const f=e=>{o?.waitUntil&&o.waitUntil(e)};let g,I,R;try{const e=new URL(t.url);g=e.pathname,I=e.search}catch{return i(t)}if(g.startsWith(n+"/")){const n=function(e){if(!M)return{};const t=e.headers.get("origin")??"",r=M.includes("*");if(!r&&!M.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"};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===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&&(g===P||g===O||g===D)){let i;try{i=await t.json()}catch{return jsonResponse(400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"Request body is not valid JSON."}},n)}return g===P?handleSubmitFp(t,i,{client:e,verdictCache:k,rateLimitBackoff:F,dispatchDedupe:U,resolveUserId:r,resolveEmailAddress:s,resolveSessionId:c,resolveDeviceId:u,disableBotFilter:h,onError:m,registerBackground:f},n):g===O?handleVerifyTriggerWeb(t,i,{client:e,verdictCache:k,resolveUserId:r,resolveEmailAddress:s,resolveDeviceId:u,onError:m},n):handleVerifyWeb(t,i,{client:e,verdictCache:k,resolveUserId:r,resolveEmailAddress:s,resolveDeviceId:u,onError:m},n)}if("GET"===t.method&&g===H){let i;try{i=r(t)}catch{}if(!i)return jsonResponse(200,{status:"anonymous"},n);const o=resolveEmail(t,s);let a=k.get(i);if((!a||k.isStale(i))&&o&&!F.isPaused()&&!k.isRefreshing(i)){k.markRefreshing(i);try{const r=extractDeviceIdFromRequest(t,u),s=parseCookieFromRequest(t,"__unshared_fingerprint_id")||void 0,n=extractSessionIdFromRequest(t,c),d=r??s??"unknown";await fetchAndCacheVerdict(e,k,i,o,d,s,n,_,(e,t)=>w({operation:"checkUser",reason:e,status:t,userId:i,emailAddress:o})),a=k.get(i)}catch(e){m&&m(e,{operation:"checkUser",userId:i,emailAddress:o}),w({operation:"checkUser",reason:"exception",userId:i,emailAddress:o})}finally{k.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(g,a,void 0!==S))return i(t);if(!shouldIncludePath(g,d))return injectIntoHtmlResponse(await i(t),j,void 0,b);try{R=r(t)}catch{}if(isSentinelUserId(R)){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;R=e&&n?e:void 0}if(!R){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}`];return injectIntoHtmlResponse(await i(t),j,r,b)}const v=resolveEmail(t,s),y=[],E=isSecureWebRequest(t)?"; Secure":"";if(y.push(`__unshared_uid=${encodeURIComponent(R)}; Path=/; SameSite=Lax${E}`),y.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${E}`),v&&y.push(`__unshared_email=${encodeURIComponent(v)}; HttpOnly; Path=/; SameSite=Lax${E}`),!v)return injectIntoHtmlResponse(await i(t),j,y,b);const T=extractSessionIdFromRequest(t,c),C=extractDeviceIdFromRequest(t,u),N=parseCookieFromRequest(t,"__unshared_fingerprint_id")||void 0,L=t.headers.get("user-agent")??"",$=extractClientIpFromRequest(t),V=C??N;if(!h&&isBot(L))return i(t);let B=k.get(R);if(B)k.isStale(R)&&!k.isRefreshing(R)&&(k.markRefreshing(R),f(fetchAndCacheVerdict(e,k,R,v,V??"unknown",N,T,_,(e,t)=>w({operation:"checkUser",reason:e,status:t,userId:R,emailAddress:v})).catch(()=>w({operation:"checkUser",reason:"exception",userId:R,emailAddress:v})).finally(()=>k.clearRefreshing(R))));else try{let t=x.get(R);t||(t=fetchAndCacheVerdict(e,k,R,v,V??"unknown",N,T,_,(e,t)=>w({operation:"checkUser",reason:e,status:t,userId:R,emailAddress:v})).finally(()=>x.delete(R)),x.set(R,t)),B=await t}catch{return w({operation:"checkUser",reason:"exception",userId:R,emailAddress:v}),injectIntoHtmlResponse(await i(t),j,y,b)}const W=B.isFlagged&&!B.isVerified;if("gate"===S&&W)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"===S&&W)return isHtmlNavigation(t.method,t.headers.get("accept")??void 0)?injectIntoHtmlResponse(await i(t),j,y,b):jsonResponse(403,flaggedResponse(v));if(B.isFlagged&&!B.isVerified&&p)try{const e=await p({userId:R,emailAddress:v,verdict:B,request:t});if(e)return injectIntoHtmlResponse(e,j,y,b)}catch(e){m&&m(e,{operation:"checkUser",userId:R,emailAddress:v})}return B.isFlagged||"unknown"===T||!V||F.isPaused()||f(dispatchUserEvent(e,k,F,U,{userId:R,emailAddress:v,sessionId:T,deviceId:V,fingerprintId:N,userAgent:L,ipAddress:$,eventType:g+I},m)),injectIntoHtmlResponse(await i(t),j,y,b)}}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 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,sessionHash:n.sessionId,userAgent:n.userAgent},DISPATCH_TIMEOUT_MS)}async function fetchAndCacheVerdict(e,t,r,s,n,i,o,a=CHECK_USER_TIMEOUT_MS,d){const c=()=>{const e=t.get(r);return t.set(r,{isFlagged:e?.isFlagged??!1,isVerified:e?.isVerified??!1,emailAddress:s,sessionId:o},5e3),t.get(r)},u={timeoutMs:a,maxRetries:0};let l;n&&"unknown"!==n&&(u.deviceId=n),i&&(u.fingerprintId=i);const p=await Promise.race([e.checkUser(s,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(r,{isFlagged:p.data?.is_user_flagged??!1,isVerified:!1,emailAddress:s,sessionId:o}),t.get(r)):(d?.("timeout"),c())}async function handleSubmitFp(e,t,r,s){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=r.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=r.resolveEmailAddress?r.resolveEmailAddress(e):void 0}catch{}o=o??parseCookieFromRequest(e,"__unshared_email")??t.email??void 0;try{a=r.resolveSessionId?r.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(!r.disableBotFilter&&isBot(c))return jsonResponse(200,{success:!0},s);const u=(n.fingerprint_id&&n.fingerprint_id.length>0?n.fingerprint_id:void 0)??extractDeviceIdFromRequestOrUnknown(e,r.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 r="unknown";if(t)try{const e=new URL(t);r=(e.pathname||"/")+(e.search||"")}catch{}h=r}const _=e.headers.get("x-idempotency-key")||void 0,g=Date.now();let I;if(_?I=`${_}|${g}`:l&&i&&(I=`${await sha256HexWeb(`${l}|${i}|${h}`)}|${g}`),i){const e=r.client.submitFingerprintEvent(n,{userId:i,emailAddress:o,sessionHash:a,eventType:h,ipAddress:d,userAgent:c,idempotencyKey:I}).catch(e=>{r.onError&&r.onError(e,{operation:"submitFingerprintEvent",userId:i,emailAddress:o})});r.registerBackground?.(e)}if(i&&o&&!r.rateLimitBackoff.isPaused()&&!r.dispatchDedupe.wasRecentlyDispatched(i,h)){const e=runProcessUserEvent({client:r.client,cache:r.verdictCache,backoff:r.rateLimitBackoff,onError:r.onError},{eventType:h,userId:i,emailAddress:o,ipAddress:d,deviceId:u,fingerprintId:l,sessionHash:a??"unknown",userAgent:c},SUBMIT_FP_TIMEOUT_MS);r.registerBackground?.(e)}const R={...s,"Content-Type":"application/json"},v=new Response(JSON.stringify({success:!0}),{status:200,headers:R});for(const e of f)v.headers.append("Set-Cookie",e);return v}catch{return jsonResponse(200,{success:!0},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_fingerprint_id")||void 0,a=await r.client.triggerEmailVerification(n,i,{fingerprintId:o});return a.success?jsonResponse(200,{success:!0,data:a.data},s):jsonResponse(200,{success:!1,error:a.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},s)}catch(e){return 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=resolveEmailWithBody(e,t??{},r.resolveEmailAddress),i=t?.code;if(!n||!i)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}},s);const o=extractDeviceIdFromRequestOrUnknown(e,r.resolveDeviceId),a=parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,d=await r.client.verify(n,o,i,{fingerprintId:a});if(d.success){let t;try{t=r.resolveUserId(e)}catch{}return t&&r.verdictCache.update(t,{isVerified:!0}),jsonResponse(200,{success:!0,data:{verified:!0}},s)}return jsonResponse(200,{success:!1,error:d.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}},s)}catch(e){return r.onError&&r.onError(e,{operation:"verify"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},s)}}export const __test_fetchAndCacheVerdict=fetchAndCacheVerdict;export const __test_injectIntoHtmlResponse=injectIntoHtmlResponse;export function __test_resetInjectWarning(){defaultInjectWarnState.emitted=!1}
1
+ import{sha256HexWeb}from"../webcrypto.mjs";import{VerdictCache}from"../middleware/verdict-cache.mjs";import{RateLimitBackoff}from"../middleware/rate-limit-backoff.mjs";import{DispatchDedupe}from"../middleware/dispatch-dedupe.mjs";import{generateFingerprintScript}from"../middleware/injection/fingerprint-script.mjs";import{generateGatePage}from"../middleware/injection/gate-page.mjs";import{flaggedResponse}from"../middleware/utils/flagged-response.mjs";import{maskEmail}from"../middleware/utils/mask-email.mjs";import{isHtmlContentType,isHtmlNavigation}from"../middleware/utils/content-type.mjs";import{shouldSkipPath}from"../middleware/utils/skip-paths.mjs";import{shouldIncludePath}from"../middleware/utils/include-path.mjs";import{isBot}from"../middleware/utils/is-bot.mjs";import{isSentinelUserId,SENTINEL_STICKINESS_TTL_MS}from"../middleware/utils/sentinel-user-id.mjs";import{runCheckUser,runProcessUserEvent,DISPATCH_TIMEOUT_MS,SUBMIT_FP_TIMEOUT_MS}from"../middleware/dispatch-processing.mjs";import{parseCookieFromRequest,extractClientIpFromRequest,extractDeviceIdFromRequest,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,7 +8,7 @@
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. */
@@ -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 n=e.headers.get("cookie");if(!n)return;const o=n.match(new RegExp(`(?:^|; )${t}=([^;]*)`));if(o)try{return decodeURIComponent(o[1])}catch{return o[1]}}export function extractClientIpFromRequest(e){const t=e.headers.get("cf-connecting-ip");if(t)return t;const n=e.headers.get("x-real-ip");if(n)return n;const o=e.headers.get("x-forwarded-for");if(o){const e=o.split(",")[0]?.trim();if(e)return e}return""}export function extractDeviceIdFromRequest(e,t){if(t)try{const n=t(e);if(n)return n}catch{}const n=e.headers.get("x-device-id");if(n)return n;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,n){const o=new Headers(n);return o.set("Content-Type","application/json"),new Response(JSON.stringify(t),{status:e,headers:o})}export function emptyResponse(e,t){return new Response(null,{status:e,headers:new Headers(t)})}export function bodyResponse(e,t,n){return new Response(t,{status:e,headers:new Headers(n)})}export function appendCookieToHeaders(e,t){e.append("Set-Cookie",t)}export function mergeResponseHeaders(e,t,n){const o=new Headers;e.forEach((e,t)=>{"set-cookie"!==t.toLowerCase()&&o.set(t,e)});const r="function"==typeof e.getSetCookie?e.getSetCookie():[];for(const e of r)o.append("Set-Cookie",e);if(n)for(const e of n)o.append("Set-Cookie",e);if(t)for(const[e,n]of Object.entries(t))o.set(e,n);return o}
1
+ export function parseCookieFromRequest(e,t){const o=e.headers.get("cookie");if(!o)return;const r=o.match(new RegExp(`(?:^|; )${t}=([^;]*)`));if(r)try{return decodeURIComponent(r[1])}catch{return r[1]}}export function extractClientIpFromRequest(e){const t=e.headers.get("cf-connecting-ip");if(t)return t;const o=e.headers.get("x-real-ip");if(o)return o;const r=e.headers.get("x-forwarded-for");if(r){const e=r.split(",")[0]?.trim();if(e)return e}return""}export function extractDeviceIdFromRequest(e,t){if(t)try{const o=t(e);if(o)return o}catch{}const o=e.headers.get("x-device-id");if(o)return o;return parseCookieFromRequest(e,"__unshared_fp_id")||void 0}export function extractDeviceIdFromRequestOrUnknown(e,t){return extractDeviceIdFromRequest(e,t)??"unknown"}export function isSecureWebRequest(e){if("https"===e.headers.get("x-forwarded-proto"))return!0;try{return"https:"===new URL(e.url).protocol}catch{return!1}}export async function readJsonRequest(e,t=1<<20){const o=Number(e.headers.get("content-length"));if(Number.isFinite(o)&&o>t)return{success:!1,status:413,error:{code:"REQUEST_TOO_LARGE",message:"Request body exceeds 1 MiB"}};try{if(!e.body)throw new Error("Missing body");const o=e.body.getReader(),r=[];let n=0;for(;;){const{done:e,value:s}=await o.read();if(e)break;if(n+=s.byteLength,n>t)return o.cancel().catch(()=>{}),{success:!1,status:413,error:{code:"REQUEST_TOO_LARGE",message:"Request body exceeds 1 MiB"}};r.push(s)}const s=new Uint8Array(n);let c=0;for(const e of r)s.set(e,c),c+=e.byteLength;return{success:!0,data:JSON.parse((new TextDecoder).decode(s))}}catch{return{success:!1,status:400,error:{code:"INVALID_JSON",message:"Request body is not valid JSON."}}}}export function jsonResponse(e,t,o){const r=new Headers(o);return r.set("Content-Type","application/json"),new Response(JSON.stringify(t),{status:e,headers:r})}export function emptyResponse(e,t){return new Response(null,{status:e,headers:new Headers(t)})}export function bodyResponse(e,t,o){return new Response(t,{status:e,headers:new Headers(o)})}export function appendCookieToHeaders(e,t){e.append("Set-Cookie",t)}export function mergeResponseHeaders(e,t,o){const r=new Headers;e.forEach((e,t)=>{"set-cookie"!==t.toLowerCase()&&r.set(t,e)});const n="function"==typeof e.getSetCookie?e.getSetCookie():[];for(const e of n)r.append("Set-Cookie",e);if(o)for(const e of o)r.append("Set-Cookie",e);if(t)for(const[e,o]of Object.entries(t))r.set(e,o);return r}
@@ -1,6 +1,7 @@
1
1
  import type { UnsharedClient, ProcessUserEventParams } from '../client';
2
2
  import type { VerdictCache } from './verdict-cache';
3
3
  import type { RateLimitBackoff } from './rate-limit-backoff';
4
+ import type { CheckUserRequest } from '../shared-types';
4
5
  /** Single policy point for background processUserEvent budgets. */
5
6
  export declare const DISPATCH_TIMEOUT_MS = 2000;
6
7
  export declare const SUBMIT_FP_TIMEOUT_MS = 3000;
@@ -14,6 +15,8 @@ export interface DispatchIo {
14
15
  emailAddress: string;
15
16
  }) => void;
16
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>;
17
20
  /**
18
21
  * Run processUserEvent with the standard bounded budget and backoff wiring.
19
22
  * Never rejects. All four dispatch sites (classic/web x dispatch/submit-fp)
@@ -1 +1 @@
1
- "use strict";async function runProcessUserEvent(e,s,t){try{const r=await e.client.processUserEvent(s,{timeoutMs:t,maxRetries:0});r.success&&r.data?.analysis&&e.cache.update(s.userId,{isFlagged:r.data.analysis.is_user_flagged}),r.success?e.backoff.noteSuccess():r.error?.retryAfter?e.backoff.pause(1e3*r.error.retryAfter):e.backoff.noteFailure()}catch(t){e.backoff.noteFailure();try{e.onError&&e.onError(t,{operation:"processUserEvent",userId:s.userId,emailAddress:s.emailAddress})}catch{}}}Object.defineProperty(exports,"t",{value:!0}),exports.SUBMIT_FP_TIMEOUT_MS=exports.DISPATCH_TIMEOUT_MS=void 0,exports.runProcessUserEvent=runProcessUserEvent,exports.DISPATCH_TIMEOUT_MS=2e3,exports.SUBMIT_FP_TIMEOUT_MS=3e3;
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;