unshared-clientjs-sdk 2.0.2 → 2.1.0-rc.10

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 (64) hide show
  1. package/README.md +27 -2
  2. package/dist/client.d.ts +20 -0
  3. package/dist/client.js +1 -1
  4. package/dist/esm/client.d.mts +20 -0
  5. package/dist/esm/client.mjs +1 -1
  6. package/dist/esm/middleware/index.d.mts +81 -3
  7. package/dist/esm/middleware/index.mjs +1 -1
  8. package/dist/esm/middleware/injection/fingerprint-script.d.mts +4 -1
  9. package/dist/esm/middleware/injection/fingerprint-script.mjs +1 -1
  10. package/dist/esm/middleware/injection/fp-bundle-source.d.mts +26 -0
  11. package/dist/esm/middleware/injection/fp-bundle-source.mjs +1 -0
  12. package/dist/esm/middleware/injection/gate-page.d.mts +15 -0
  13. package/dist/esm/middleware/injection/gate-page.mjs +1 -0
  14. package/dist/esm/middleware/response-interceptor.d.mts +7 -0
  15. package/dist/esm/middleware/response-interceptor.mjs +1 -1
  16. package/dist/esm/middleware/routes/interstitial.d.mts +17 -0
  17. package/dist/esm/middleware/routes/interstitial.mjs +1 -0
  18. package/dist/esm/middleware/routes/submit-fp.d.mts +2 -0
  19. package/dist/esm/middleware/routes/submit-fp.mjs +1 -1
  20. package/dist/esm/middleware/routes/verify.d.mts +1 -0
  21. package/dist/esm/middleware/routes/verify.mjs +1 -1
  22. package/dist/esm/middleware/utils/content-type.d.mts +6 -0
  23. package/dist/esm/middleware/utils/content-type.mjs +1 -1
  24. package/dist/esm/middleware/utils/flagged-response.d.mts +9 -0
  25. package/dist/esm/middleware/utils/flagged-response.mjs +1 -1
  26. package/dist/esm/middleware/utils/is-bot.mjs +1 -1
  27. package/dist/esm/middleware/utils/mask-email.d.mts +9 -0
  28. package/dist/esm/middleware/utils/mask-email.mjs +1 -0
  29. package/dist/esm/util.d.mts +6 -0
  30. package/dist/esm/util.mjs +1 -1
  31. package/dist/esm/web/index.d.mts +1 -0
  32. package/dist/esm/web/index.mjs +1 -1
  33. package/dist/esm/web/protection-handler.mjs +1 -1
  34. package/dist/esm/web/types.d.mts +64 -0
  35. package/dist/middleware/index.d.ts +81 -3
  36. package/dist/middleware/index.js +1 -1
  37. package/dist/middleware/injection/fingerprint-script.d.ts +4 -1
  38. package/dist/middleware/injection/fingerprint-script.js +1 -1
  39. package/dist/middleware/injection/fp-bundle-source.d.ts +26 -0
  40. package/dist/middleware/injection/fp-bundle-source.js +1 -0
  41. package/dist/middleware/injection/gate-page.d.ts +15 -0
  42. package/dist/middleware/injection/gate-page.js +1 -0
  43. package/dist/middleware/response-interceptor.d.ts +7 -0
  44. package/dist/middleware/response-interceptor.js +1 -1
  45. package/dist/middleware/routes/interstitial.d.ts +17 -0
  46. package/dist/middleware/routes/interstitial.js +1 -0
  47. package/dist/middleware/routes/submit-fp.d.ts +2 -0
  48. package/dist/middleware/routes/submit-fp.js +1 -1
  49. package/dist/middleware/routes/verify.d.ts +1 -0
  50. package/dist/middleware/routes/verify.js +1 -1
  51. package/dist/middleware/utils/content-type.d.ts +6 -0
  52. package/dist/middleware/utils/content-type.js +1 -1
  53. package/dist/middleware/utils/flagged-response.d.ts +9 -0
  54. package/dist/middleware/utils/flagged-response.js +1 -1
  55. package/dist/middleware/utils/is-bot.js +1 -1
  56. package/dist/middleware/utils/mask-email.d.ts +9 -0
  57. package/dist/middleware/utils/mask-email.js +1 -0
  58. package/dist/util.d.ts +6 -0
  59. package/dist/util.js +1 -1
  60. package/dist/web/index.d.ts +1 -0
  61. package/dist/web/index.js +1 -1
  62. package/dist/web/protection-handler.js +1 -1
  63. package/dist/web/types.d.ts +64 -0
  64. package/package.json +3 -2
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"i",{value:!0}),exports.handleSubmitFingerprint=handleSubmitFingerprint;const 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,s)=>{try{const t=i.body??{},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 r,o,_;try{const s=e.resolveUserId?e.resolveUserId(i):void 0;s&&!(0,sentinel_user_id_1.isSentinelUserId)(s)&&(r=s)}catch{}if(!r){const e="string"==typeof t.user_id?t.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")??t.email??void 0;try{_=e.resolveSessionId?e.resolveSessionId(i):void 0}catch{}_=_??t.session_id??(0,cookies_1.parseCookie)(i,"__unshared_sid");const d=(0,client_ip_1.extractClientIp)(i),c=i.headers["user-agent"]??"";if((0,is_bot_1.isBot)(c))return void(0,http_helpers_1.sendJson)(s,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,p=n.full_hash||void 0,l=(0,secure_1.isSecureRequest)(i)?"; Secure":"",h=[];if(p&&!(0,cookies_1.parseCookie)(i,"__unshared_fingerprint_id")&&h.push(`__unshared_fingerprint_id=${encodeURIComponent(p)}; HttpOnly; Path=/; SameSite=Lax${l}`),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${l}`)}if(o&&!(0,cookies_1.parseCookie)(i,"__unshared_email")&&h.push(`__unshared_email=${encodeURIComponent(o)}; HttpOnly; Path=/; SameSite=Lax${l}`),h.length>0){const e=s.getHeader("Set-Cookie");if(e){const i=Array.isArray(e)?[...e]:[String(e)];i.push(...h),s.setHeader("Set-Cookie",i)}else s.setHeader("Set-Cookie",h)}let f;if("string"==typeof t.event_type&&t.event_type)f=t.event_type;else{const e=i.headers.referer??i.headers.referrer;let s="unknown";if("string"==typeof e&&e.length>0)try{const i=new URL(e);s=(i.pathname||"/")+(i.search||"")}catch{}f=s}const v=i.headers["x-idempotency-key"],m="string"==typeof v&&v.length>0?v:void 0,g=Date.now(),y=m?`${m}|${g}`:a&&r?`${a}|${r}|${f}|${g}`:void 0;if(r&&e.client.submitFingerprintEvent(n,{userId:r,emailAddress:o,sessionHash:_,eventType:f,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,f))try{const i=await e.client.processUserEvent({eventType:f,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)(s,200,{success:!0})}catch{(0,http_helpers_1.sendJson)(s,200,{success:!0})}}}
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})}}}
@@ -6,6 +6,7 @@ export interface VerificationDependencies<TReq extends UnsharedRequest = Unshare
6
6
  verdictCache: VerdictCache;
7
7
  resolveEmailAddress?: (req: TReq) => string | undefined;
8
8
  resolveDeviceId?: (req: TReq) => string | undefined;
9
+ resolveUserId?: (req: TReq) => string | undefined;
9
10
  onError?: (error: unknown, context: {
10
11
  operation: 'processUserEvent' | 'submitFingerprintEvent' | 'checkUser' | 'verifyTrigger' | 'verify';
11
12
  userId?: string;
@@ -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),o=(0,cookies_1.parseCookie)(r,"__unshared_fingerprint_id")||void 0,c=await e.client.triggerEmailVerification(s,t,{fingerprintId:o});c.success?(0,http_helpers_1.sendJson)(i,200,{success:!0,data:c.data}):(0,http_helpers_1.sendJson)(i,200,{success:!1,error:c.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),o=s.code;if(!t||!o)return void(0,http_helpers_1.sendJson)(i,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}});const c=(0,device_id_1.extractDeviceId)(r,e.resolveDeviceId),_=(0,cookies_1.parseCookie)(r,"__unshared_fingerprint_id")||void 0,n=await e.client.verify(t,c,o,{fingerprintId:_});if(n.success){const s=(0,cookies_1.parseCookie)(r,"__unshared_uid");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");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,5 +1,11 @@
1
1
  /** Check if a Content-Type header value indicates HTML. */
2
2
  export declare function isHtmlContentType(contentType: string | undefined): boolean;
3
+ /**
4
+ * Whether a request is a top-level HTML navigation (a page load) rather than an API/XHR
5
+ * call: a GET whose Accept header asks for text/html. Used by block mode to decide
6
+ * between serving the HTML gate page and returning a 403 JSON.
7
+ */
8
+ export declare function isHtmlNavigation(method: string | undefined, accept: string | undefined): boolean;
3
9
  /** Check if a Content-Type header value indicates JSON. */
4
10
  export declare function isJsonContentType(contentType: string | undefined): boolean;
5
11
  /** Check if a Content-Type indicates a static asset (images, fonts, etc). */
@@ -1 +1 @@
1
- "use strict";function isHtmlContentType(t){return!!t&&t.includes("text/html")}function isJsonContentType(t){return!!t&&t.includes("application/json")}function isStaticContentType(t){return!!t&&["image/","font/","audio/","video/","application/javascript","text/javascript","text/css","application/wasm"].some(e=>t.includes(e))}Object.defineProperty(exports,"t",{value:!0}),exports.isHtmlContentType=isHtmlContentType,exports.isJsonContentType=isJsonContentType,exports.isStaticContentType=isStaticContentType;
1
+ "use strict";function isHtmlContentType(t){return!!t&&t.includes("text/html")}function isHtmlNavigation(t,n){return"GET"===(t??"GET").toUpperCase()&&!!n&&n.includes("text/html")}function isJsonContentType(t){return!!t&&t.includes("application/json")}function isStaticContentType(t){return!!t&&["image/","font/","audio/","video/","application/javascript","text/javascript","text/css","application/wasm"].some(n=>t.includes(n))}Object.defineProperty(exports,"t",{value:!0}),exports.isHtmlContentType=isHtmlContentType,exports.isHtmlNavigation=isHtmlNavigation,exports.isJsonContentType=isJsonContentType,exports.isStaticContentType=isStaticContentType;
@@ -1,4 +1,13 @@
1
1
  export declare const ACCOUNT_FLAGGED_ERROR: "account_flagged";
2
+ /**
3
+ * Standardized response body for a flagged, unverified user. The injected
4
+ * inline script keys off `error === 'account_flagged'` to emit the
5
+ * "unshared:flagged" event, so always use this helper for that body.
6
+ *
7
+ * The email is MASKED (`j***@domain`) — this body is browser-visible and reaches
8
+ * any same-origin script via `e.detail.email`. The SDK's own modal resolves the
9
+ * real identity server-side, so it never needs the full address here.
10
+ */
2
11
  export declare function flaggedResponse(email: string): {
3
12
  readonly error: "account_flagged";
4
13
  readonly email: string;
@@ -1 +1 @@
1
- "use strict";function flaggedResponse(e){return{error:exports.ACCOUNT_FLAGGED_ERROR,email:e}}Object.defineProperty(exports,"o",{value:!0}),exports.ACCOUNT_FLAGGED_ERROR=void 0,exports.flaggedResponse=flaggedResponse,exports.ACCOUNT_FLAGGED_ERROR="account_flagged";
1
+ "use strict";Object.defineProperty(exports,"o",{value:!0}),exports.ACCOUNT_FLAGGED_ERROR=void 0,exports.flaggedResponse=flaggedResponse;const mask_email_1=require("./mask-email");function flaggedResponse(e){return{error:exports.ACCOUNT_FLAGGED_ERROR,email:(0,mask_email_1.maskEmail)(e)}}exports.ACCOUNT_FLAGGED_ERROR="account_flagged";
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.isBot=isBot;const BOT_PATTERNS=["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"],BOT_RE=new RegExp(BOT_PATTERNS.join("|"),"i");function isBot(e){return!!e&&BOT_RE.test(e)}
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.isBot=isBot;const BOT_PATTERNS=["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"],BOT_RE=new RegExp(BOT_PATTERNS.map(e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join("|"),"i");function isBot(e){return!!e&&BOT_RE.test(e)}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Mask an email for client-facing surfaces (the `/status` body, the
3
+ * `account_flagged` 403 body, and thus the `unshared:flagged` event's
4
+ * `e.detail.email`). Keeps the first character of the local part and the full
5
+ * domain (`jason@unsharedlabs.com` → `j***@unsharedlabs.com`) so the value stays
6
+ * display-usable without exposing the full address to same-origin scripts.
7
+ * Returns `'***'` for malformed/empty input so nothing leaks.
8
+ */
9
+ export declare function maskEmail(email: string): string;
@@ -0,0 +1 @@
1
+ "use strict";function maskEmail(e){const s=e.lastIndexOf("@");return s<=0?"***":`${e[0]}***@${e.slice(s+1)}`}Object.defineProperty(exports,"t",{value:!0}),exports.maskEmail=maskEmail;
package/dist/util.d.ts CHANGED
@@ -1 +1,7 @@
1
+ /**
2
+ * SHA-256 hex digest. Used for idempotency keys: deterministic (same input,
3
+ * same digest) yet irreversible, so user IDs and emails never appear in the
4
+ * platform's stored idempotency_key column. Matches the browser SDK's hashing.
5
+ */
6
+ export declare function sha256Hex(value: string): string;
1
7
  export declare function encryptData(data: string, key: Buffer): string;
package/dist/util.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.encryptData=encryptData;const crypto_1=require("crypto");function encryptData(t,e){const c=(0,crypto_1.randomBytes)(12),r=(0,crypto_1.createCipheriv)("aes-256-gcm",e,c);let s=r.update(t,"utf8","base64");s+=r.final("base64");const o=r.getAuthTag();return c.toString("base64")+":"+o.toString("base64")+":"+s}
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.sha256Hex=sha256Hex,exports.encryptData=encryptData;const crypto_1=require("crypto");function sha256Hex(t){return(0,crypto_1.createHash)("sha256").update(t).digest("hex")}function encryptData(t,e){const r=(0,crypto_1.randomBytes)(12),s=(0,crypto_1.createCipheriv)("aes-256-gcm",e,r);let c=s.update(t,"utf8","base64");c+=s.final("base64");const o=s.getAuthTag();return r.toString("base64")+":"+o.toString("base64")+":"+c}
@@ -12,6 +12,7 @@
12
12
  */
13
13
  export { createWebSubmitHandler } from './submit-handler';
14
14
  export { createWebProtectionMiddleware } from './protection-handler';
15
+ export { flaggedResponse, ACCOUNT_FLAGGED_ERROR } from '../middleware/utils/flagged-response';
15
16
  export type { WebHandler, WebMiddleware, WebSubmitOptions, WebProtectionConfig, } from './types';
16
17
  export { VerdictCache } from '../middleware/verdict-cache';
17
18
  export type { Verdict } from '../middleware/verdict-cache';
package/dist/web/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.VerdictCache=exports.createWebProtectionMiddleware=exports.createWebSubmitHandler=void 0;var submit_handler_1=require("./submit-handler");Object.defineProperty(exports,"createWebSubmitHandler",{enumerable:!0,get:function(){return submit_handler_1.createWebSubmitHandler}});var protection_handler_1=require("./protection-handler");Object.defineProperty(exports,"createWebProtectionMiddleware",{enumerable:!0,get:function(){return protection_handler_1.createWebProtectionMiddleware}});var verdict_cache_1=require("../middleware/verdict-cache");Object.defineProperty(exports,"VerdictCache",{enumerable:!0,get:function(){return verdict_cache_1.VerdictCache}});
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.VerdictCache=exports.ACCOUNT_FLAGGED_ERROR=exports.flaggedResponse=exports.createWebProtectionMiddleware=exports.createWebSubmitHandler=void 0;var submit_handler_1=require("./submit-handler");Object.defineProperty(exports,"createWebSubmitHandler",{enumerable:!0,get:function(){return submit_handler_1.createWebSubmitHandler}});var protection_handler_1=require("./protection-handler");Object.defineProperty(exports,"createWebProtectionMiddleware",{enumerable:!0,get:function(){return protection_handler_1.createWebProtectionMiddleware}});var flagged_response_1=require("../middleware/utils/flagged-response");Object.defineProperty(exports,"flaggedResponse",{enumerable:!0,get:function(){return flagged_response_1.flaggedResponse}}),Object.defineProperty(exports,"ACCOUNT_FLAGGED_ERROR",{enumerable:!0,get:function(){return flagged_response_1.ACCOUNT_FLAGGED_ERROR}});var verdict_cache_1=require("../middleware/verdict-cache");Object.defineProperty(exports,"VerdictCache",{enumerable:!0,get:function(){return verdict_cache_1.VerdictCache}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.createWebProtectionMiddleware=createWebProtectionMiddleware;const verdict_cache_1=require("../middleware/verdict-cache"),rate_limit_backoff_1=require("../middleware/rate-limit-backoff"),dispatch_dedupe_1=require("../middleware/dispatch-dedupe"),fingerprint_script_1=require("../middleware/injection/fingerprint-script"),content_type_1=require("../middleware/utils/content-type"),skip_paths_1=require("../middleware/utils/skip-paths"),include_path_1=require("../middleware/utils/include-path"),is_bot_1=require("../middleware/utils/is-bot"),sentinel_user_id_1=require("../middleware/utils/sentinel-user-id"),web_helpers_1=require("./web-helpers"),CHECK_USER_TIMEOUT_MS=500;function createWebProtectionMiddleware(e,r){if(!r.userId)throw new Error("[Unshared] userId resolver is required");const{userId:s,emailAddress:t,routePrefix:i="/__unshared",corsOrigins:n,cacheTTL:a=6e4,skipPaths:o,includePathPrefix:d,sessionId:c,deviceId:_,fingerprintSdkBundle:l="",onFlagged:u,onError:p}=r,h=new verdict_cache_1.VerdictCache(a),f=new rate_limit_backoff_1.RateLimitBackoff,m=new dispatch_dedupe_1.DispatchDedupe,w=Date.now().toString(36),g=(0,fingerprint_script_1.generateFingerprintScript)(i,w),b=`${i}/fp.js`,v=`${i}/submit-fp`,I=`${i}/verify-trigger`,y=`${i}/verify`,A=`${i}/status`,S=n?Array.isArray(n)?n:[n]:null;return async function(r,n){let a,w,E;try{const e=new URL(r.url);a=e.pathname,w=e.search}catch{return n(r)}if(a.startsWith(i+"/")){const i=function(e){if(!S)return{};const r=e.headers.get("origin")??"",s=S.includes("*");return s||S.includes(r)?{"Access-Control-Allow-Origin":s?"*":r,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id","Access-Control-Allow-Credentials":"true"}:{}}(r);if("OPTIONS"===r.method)return(0,web_helpers_1.emptyResponse)(204,i);if("GET"===r.method&&a===b)return l?(0,web_helpers_1.bodyResponse)(200,l,{...i,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600"}):(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},i);if("POST"===r.method&&(a===v||a===I||a===y)){let n;try{n=await r.json()}catch{return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"Request body is not valid JSON."}},i)}return a===v?handleSubmitFp(r,n,{client:e,verdictCache:h,rateLimitBackoff:f,dispatchDedupe:m,resolveUserId:s,resolveEmailAddress:t,resolveSessionId:c,resolveDeviceId:_,onError:p},i):a===I?handleVerifyTriggerWeb(r,n,{client:e,verdictCache:h,resolveEmailAddress:t,resolveDeviceId:_,onError:p},i):handleVerifyWeb(r,n,{client:e,verdictCache:h,resolveEmailAddress:t,resolveDeviceId:_,onError:p},i)}if("GET"===r.method&&a===A){let e;try{e=s(r)}catch{}if(!e)return(0,web_helpers_1.jsonResponse)(200,{status:"anonymous"},i);const n=resolveEmail(r,t),a=h.get(e);return a&&a.isFlagged&&!a.isVerified&&u&&n?(0,web_helpers_1.jsonResponse)(403,{error:"account_flagged",email:n},i):(0,web_helpers_1.jsonResponse)(200,{status:"ok"},i)}return(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},i)}if((0,skip_paths_1.shouldSkipPath)(a,o))return n(r);if(!(0,include_path_1.shouldIncludePath)(a,d))return injectIntoHtmlResponse(await n(r),g);try{E=s(r)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(E)){const e=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_uid"),s=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_uid_at"),t=s?Number(s):NaN,i=Number.isFinite(t)&&Date.now()-t<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;E=e&&i?e:void 0}if(!E){const e=(0,web_helpers_1.isSecureWebRequest)(r)?"; 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 n(r),g,s)}const R=resolveEmail(r,t),T=[],C=(0,web_helpers_1.isSecureWebRequest)(r)?"; Secure":"";if(T.push(`__unshared_uid=${encodeURIComponent(E)}; Path=/; SameSite=Lax${C}`),T.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${C}`),R&&T.push(`__unshared_email=${encodeURIComponent(R)}; HttpOnly; Path=/; SameSite=Lax${C}`),!R)return injectIntoHtmlResponse(await n(r),g,T);const O=extractSessionIdFromRequest(r,c),$=(0,web_helpers_1.extractDeviceIdFromRequest)(r,_),x=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_fingerprint_id")||void 0,P=r.headers.get("user-agent")??"",k=(0,web_helpers_1.extractClientIpFromRequest)(r),L=$??x;if((0,is_bot_1.isBot)(P))return n(r);let N=h.get(E);if(N)h.isStale(E)&&!h.isRefreshing(E)&&(h.markRefreshing(E),fetchAndCacheVerdict(e,h,E,R,L??"unknown",x,O).finally(()=>h.clearRefreshing(E)));else try{N=await fetchAndCacheVerdict(e,h,E,R,L??"unknown",x,O)}catch{return injectIntoHtmlResponse(await n(r),g,T)}if(N.isFlagged&&!N.isVerified&&u)try{const e=await u({userId:E,emailAddress:R,verdict:N,request:r});if(e)return injectIntoHtmlResponse(e,g,T)}catch(e){p&&p(e,{operation:"checkUser",userId:E,emailAddress:R})}return N.isFlagged||"unknown"===O||!L||f.isPaused()||dispatchUserEvent(e,h,f,m,{userId:E,emailAddress:R,sessionId:O,deviceId:L,fingerprintId:x,userAgent:P,ipAddress:k,eventType:a+w},p),injectIntoHtmlResponse(await n(r),g,T)}}async function injectIntoHtmlResponse(e,r,s){const t=e.headers.get("content-type");if(!(0,content_type_1.isHtmlContentType)(t??void 0)){if(!s||0===s.length)return e;const r=(0,web_helpers_1.mergeResponseHeaders)(e.headers,void 0,s);return new Response(e.body,{status:e.status,statusText:e.statusText,headers:r})}const i=await e.text(),n=i.lastIndexOf("</body>"),a=-1===n?i+r:i.slice(0,n)+r+i.slice(n),o=(0,web_helpers_1.mergeResponseHeaders)(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(a).length)},s);return o.delete("ETag"),o.delete("Last-Modified"),o.delete("Content-Encoding"),new Response(a,{status:e.status,statusText:e.statusText,headers:o})}function resolveEmail(e,r){if(r)try{const s=r(e);if(s)return s}catch{}const s=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email");if(s)return s}function resolveEmailWithBody(e,r,s){const t=resolveEmail(e,s);if(t)return t;const i=r.email;return"string"==typeof i&&i?i:void 0}function extractSessionIdFromRequest(e,r){if(r)try{const s=r(e);if(s)return s}catch{}return(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid")??"unknown"}function dispatchUserEvent(e,r,s,t,i,n){t.mark(i.userId,i.eventType),e.processUserEvent({eventType:i.eventType,userId:i.userId,emailAddress:i.emailAddress,ipAddress:i.ipAddress,deviceId:i.deviceId,fingerprintId:i.fingerprintId,sessionHash:i.sessionId,userAgent:i.userAgent}).then(e=>{e.success&&e.data?.analysis&&r.update(i.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&s.pause(1e3*e.error.retryAfter)}).catch(e=>{n&&n(e,{operation:"processUserEvent",userId:i.userId,emailAddress:i.emailAddress})})}async function fetchAndCacheVerdict(e,r,s,t,i,n,a){const o={};let d;i&&"unknown"!==i&&(o.deviceId=i),n&&(o.fingerprintId=n);const c=await Promise.race([e.checkUser(t,o),new Promise(e=>{d=setTimeout(()=>e(null),500)})]);if(clearTimeout(d),!c)return{isFlagged:!1,isVerified:!1,emailAddress:t,sessionId:a,cachedAt:0,ttl:0};const _=c.data?.is_user_flagged??!1;return r.set(s,{isFlagged:_,isVerified:!1,emailAddress:t,sessionId:a}),r.get(s)}async function handleSubmitFp(e,r,s,t){try{const i={full_hash:r.hash??"",fingerprint_id:r.stable_hash??"",timestamp:r.collected_at??(new Date).toISOString(),isIncognito:r.is_incognito??!1,components:r.components??{},version:r.version??"inline-1.0.0"};let n,a,o;try{const r=s.resolveUserId(e);r&&!(0,sentinel_user_id_1.isSentinelUserId)(r)&&(n=r)}catch{}if(!n){const e="string"==typeof r.user_id?r.user_id:void 0;e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(n=e)}if(!n){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_uid");r&&!(0,sentinel_user_id_1.isSentinelUserId)(r)&&(n=r)}try{a=s.resolveEmailAddress?s.resolveEmailAddress(e):void 0}catch{}a=a??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")??r.email??void 0;try{o=s.resolveSessionId?s.resolveSessionId(e):void 0}catch{}o=o??r.session_id??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid");const d=(0,web_helpers_1.extractClientIpFromRequest)(e),c=e.headers.get("user-agent")??"";if((0,is_bot_1.isBot)(c))return(0,web_helpers_1.jsonResponse)(200,{success:!0},t);const _=(i.fingerprint_id&&i.fingerprint_id.length>0?i.fingerprint_id:void 0)??(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,s.resolveDeviceId),l=i.fingerprint_id||void 0,u=i.full_hash||void 0,p=(0,web_helpers_1.isSecureWebRequest)(e)?"; Secure":"",h=[];if(u&&!(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")&&h.push(`__unshared_fingerprint_id=${encodeURIComponent(u)}; HttpOnly; Path=/; SameSite=Lax${p}`),l){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fp_id");r&&r===l||h.push(`__unshared_fp_id=${encodeURIComponent(l)}; Path=/; SameSite=Lax; Max-Age=31536000${p}`)}let f;if(a&&!(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")&&h.push(`__unshared_email=${encodeURIComponent(a)}; HttpOnly; Path=/; SameSite=Lax${p}`),"string"==typeof r.event_type&&r.event_type)f=r.event_type;else{const r=e.headers.get("referer")??e.headers.get("referrer");let s="unknown";if(r)try{const e=new URL(r);s=(e.pathname||"/")+(e.search||"")}catch{}f=s}const m=e.headers.get("x-idempotency-key")||void 0,w=Date.now(),g=m?`${m}|${w}`:l&&n?`${l}|${n}|${f}|${w}`:void 0;n&&s.client.submitFingerprintEvent(i,{userId:n,emailAddress:a,sessionHash:o,eventType:f,ipAddress:d,userAgent:c,idempotencyKey:g}).catch(e=>{s.onError&&s.onError(e,{operation:"submitFingerprintEvent",userId:n,emailAddress:a})}),n&&a&&!s.rateLimitBackoff.isPaused()&&!s.dispatchDedupe.wasRecentlyDispatched(n,f)&&s.client.processUserEvent({eventType:f,userId:n,emailAddress:a,ipAddress:d,deviceId:_,fingerprintId:l,sessionHash:o??"unknown",userAgent:c}).then(e=>{e.success&&e.data?.analysis&&s.verdictCache.update(n,{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:n,emailAddress:a})});const b={...t,"Content-Type":"application/json"},v=new Response(JSON.stringify({success:!0}),{status:200,headers:b});for(const e of h)v.headers.append("Set-Cookie",e);return v}catch{return(0,web_helpers_1.jsonResponse)(200,{success:!0},t)}}async function handleVerifyTriggerWeb(e,r,s,t){try{const i=resolveEmailWithBody(e,r??{},s.resolveEmailAddress);if(!i)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},t);const n=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,s.resolveDeviceId),a=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,o=await s.client.triggerEmailVerification(i,n,{fingerprintId:a});return o.success?(0,web_helpers_1.jsonResponse)(200,{success:!0,data:o.data},t):(0,web_helpers_1.jsonResponse)(200,{success:!1,error:o.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},t)}catch(e){return s.onError&&s.onError(e,{operation:"verifyTrigger"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},t)}}async function handleVerifyWeb(e,r,s,t){try{const i=resolveEmailWithBody(e,r??{},s.resolveEmailAddress),n=r?.code;if(!i||!n)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}},t);const a=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,s.resolveDeviceId),o=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,d=await s.client.verify(i,a,n,{fingerprintId:o});if(d.success){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_uid");return r&&s.verdictCache.update(r,{isVerified:!0}),(0,web_helpers_1.jsonResponse)(200,{success:!0,data:{verified:!0}},t)}return(0,web_helpers_1.jsonResponse)(200,{success:!1,error:d.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}},t)}catch(e){return s.onError&&s.onError(e,{operation:"verify"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},t)}}
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.createWebProtectionMiddleware=createWebProtectionMiddleware;const util_1=require("../util"),verdict_cache_1=require("../middleware/verdict-cache"),rate_limit_backoff_1=require("../middleware/rate-limit-backoff"),dispatch_dedupe_1=require("../middleware/dispatch-dedupe"),fingerprint_script_1=require("../middleware/injection/fingerprint-script"),gate_page_1=require("../middleware/injection/gate-page"),flagged_response_1=require("../middleware/utils/flagged-response"),mask_email_1=require("../middleware/utils/mask-email"),content_type_1=require("../middleware/utils/content-type"),skip_paths_1=require("../middleware/utils/skip-paths"),include_path_1=require("../middleware/utils/include-path"),is_bot_1=require("../middleware/utils/is-bot"),sentinel_user_id_1=require("../middleware/utils/sentinel-user-id"),web_helpers_1=require("./web-helpers"),CHECK_USER_TIMEOUT_MS=500;function createWebProtectionMiddleware(e,r){if(!r.userId)throw new Error("[Unshared] userId resolver is required");const{userId:t,emailAddress:s,routePrefix:i="/__unshared",corsOrigins:n,cacheTTL:a=6e4,skipPaths:o,includePathPrefix:d,sessionId:_,deviceId:c,fingerprintSdkBundle:l="",onFlagged:u,onError:p,onFailOpen:h,disableBotFilter:f=!1,checkUserTimeoutMs:m=CHECK_USER_TIMEOUT_MS,blockFlagged:w=!1,flaggedMode:g,autoInterstitial:b=!1,interstitialFlowType:v="email_verification"}=r,I=g??(w?"gate":void 0),y=b||"overlay"===I,A=e=>{if(h)try{h(e)}catch{}},S=Date.now().toString(36),E=void 0!==I||y,R="gate"===I?"flaggedMode 'gate' (blockFlagged)":"overlay"===I?"flaggedMode 'overlay'":"autoInterstitial";if(E&&!l)throw new Error(`[Unshared] ${R} requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the interstitial).`);if(E&&"/__unshared"!==i)throw new Error(`[Unshared] ${R} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const T="gate"===I?(0,gate_page_1.generateGatePage)(i,S):"",k=new verdict_cache_1.VerdictCache(a),C=new rate_limit_backoff_1.RateLimitBackoff,x=new dispatch_dedupe_1.DispatchDedupe,U=(0,fingerprint_script_1.generateFingerprintScript)(i,S,{autoInterstitial:y,interstitialFlowType:v}),O=`${i}/fp.js`,$=`${i}/submit-fp`,F=`${i}/verify-trigger`,P=`${i}/verify`,q=`${i}/status`,L=n?Array.isArray(n)?n:[n]:null;return async function(r,n){let a,h,w;try{const e=new URL(r.url);a=e.pathname,h=e.search}catch{return n(r)}if(a.startsWith(i+"/")){const i=function(e){if(!L)return{};const r=e.headers.get("origin")??"",t=L.includes("*");return t||L.includes(r)?{"Access-Control-Allow-Origin":t?"*":r,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id","Access-Control-Allow-Credentials":"true"}:{}}(r);if("OPTIONS"===r.method)return(0,web_helpers_1.emptyResponse)(204,i);if("GET"===r.method&&a===O)return l?(0,web_helpers_1.bodyResponse)(200,l,{...i,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600"}):(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},i);if("POST"===r.method&&(a===$||a===F||a===P)){let n;try{n=await r.json()}catch{return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"Request body is not valid JSON."}},i)}return a===$?handleSubmitFp(r,n,{client:e,verdictCache:k,rateLimitBackoff:C,dispatchDedupe:x,resolveUserId:t,resolveEmailAddress:s,resolveSessionId:_,resolveDeviceId:c,disableBotFilter:f,onError:p},i):a===F?handleVerifyTriggerWeb(r,n,{client:e,verdictCache:k,resolveEmailAddress:s,resolveDeviceId:c,onError:p},i):handleVerifyWeb(r,n,{client:e,verdictCache:k,resolveEmailAddress:s,resolveDeviceId:c,onError:p},i)}if("GET"===r.method&&a===q){let n;try{n=t(r)}catch{}if(!n)return(0,web_helpers_1.jsonResponse)(200,{status:"anonymous"},i);const a=resolveEmail(r,s);let o=k.get(n);if((!o||k.isStale(n))&&a&&!C.isPaused()&&!k.isRefreshing(n)){k.markRefreshing(n);try{const t=(0,web_helpers_1.extractDeviceIdFromRequest)(r,c),s=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_fingerprint_id")||void 0,i=extractSessionIdFromRequest(r,_),d=t??s??"unknown";await fetchAndCacheVerdict(e,k,n,a,d,s,i,m,(e,r)=>A({operation:"checkUser",reason:e,status:r,userId:n,emailAddress:a})),o=k.get(n)}catch(e){p&&p(e,{operation:"checkUser",userId:n,emailAddress:a}),A({operation:"checkUser",reason:"exception",userId:n,emailAddress:a})}finally{k.clearRefreshing(n)}}return o&&o.isFlagged&&!o.isVerified&&u&&a?(0,web_helpers_1.jsonResponse)(200,{status:"flagged",email:(0,mask_email_1.maskEmail)(a)},i):(0,web_helpers_1.jsonResponse)(200,{status:"ok"},i)}return(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},i)}if((0,skip_paths_1.shouldSkipPath)(a,o))return n(r);if(!(0,include_path_1.shouldIncludePath)(a,d))return injectIntoHtmlResponse(await n(r),U);try{w=t(r)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(w)){const e=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_uid"),t=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_uid_at"),s=t?Number(t):NaN,i=Number.isFinite(s)&&Date.now()-s<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;w=e&&i?e:void 0}if(!w){const e=(0,web_helpers_1.isSecureWebRequest)(r)?"; Secure":"",t=[`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`];return injectIntoHtmlResponse(await n(r),U,t)}const g=resolveEmail(r,s),b=[],v=(0,web_helpers_1.isSecureWebRequest)(r)?"; Secure":"";if(b.push(`__unshared_uid=${encodeURIComponent(w)}; Path=/; SameSite=Lax${v}`),b.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${v}`),g&&b.push(`__unshared_email=${encodeURIComponent(g)}; HttpOnly; Path=/; SameSite=Lax${v}`),!g)return injectIntoHtmlResponse(await n(r),U,b);const y=extractSessionIdFromRequest(r,_),S=(0,web_helpers_1.extractDeviceIdFromRequest)(r,c),E=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_fingerprint_id")||void 0,R=r.headers.get("user-agent")??"",M=(0,web_helpers_1.extractClientIpFromRequest)(r),D=S??E;if(!f&&(0,is_bot_1.isBot)(R))return n(r);let N=k.get(w);if(N)k.isStale(w)&&!k.isRefreshing(w)&&(k.markRefreshing(w),fetchAndCacheVerdict(e,k,w,g,D??"unknown",E,y,m,(e,r)=>A({operation:"checkUser",reason:e,status:r,userId:w,emailAddress:g})).catch(()=>A({operation:"checkUser",reason:"exception",userId:w,emailAddress:g})).finally(()=>k.clearRefreshing(w)));else try{N=await fetchAndCacheVerdict(e,k,w,g,D??"unknown",E,y,m,(e,r)=>A({operation:"checkUser",reason:e,status:r,userId:w,emailAddress:g}))}catch{return A({operation:"checkUser",reason:"exception",userId:w,emailAddress:g}),injectIntoHtmlResponse(await n(r),U,b)}const H=N.isFlagged&&!N.isVerified;if("gate"===I&&H)return(0,content_type_1.isHtmlNavigation)(r.method,r.headers.get("accept")??void 0)?(0,web_helpers_1.bodyResponse)(200,T,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}):(0,web_helpers_1.jsonResponse)(403,(0,flagged_response_1.flaggedResponse)(g));if("overlay"===I&&H)return(0,content_type_1.isHtmlNavigation)(r.method,r.headers.get("accept")??void 0)?injectIntoHtmlResponse(await n(r),U,b):(0,web_helpers_1.jsonResponse)(403,(0,flagged_response_1.flaggedResponse)(g));if(N.isFlagged&&!N.isVerified&&u)try{const e=await u({userId:w,emailAddress:g,verdict:N,request:r});if(e)return injectIntoHtmlResponse(e,U,b)}catch(e){p&&p(e,{operation:"checkUser",userId:w,emailAddress:g})}return N.isFlagged||"unknown"===y||!D||C.isPaused()||dispatchUserEvent(e,k,C,x,{userId:w,emailAddress:g,sessionId:y,deviceId:D,fingerprintId:E,userAgent:R,ipAddress:M,eventType:a+h},p),injectIntoHtmlResponse(await n(r),U,b)}}async function injectIntoHtmlResponse(e,r,t){const s=e.headers.get("content-type");if(!(0,content_type_1.isHtmlContentType)(s??void 0)){if(!t||0===t.length)return e;const r=(0,web_helpers_1.mergeResponseHeaders)(e.headers,void 0,t);return new Response(e.body,{status:e.status,statusText:e.statusText,headers:r})}const i=await e.text(),n=i.lastIndexOf("</body>"),a=-1===n?i+r:i.slice(0,n)+r+i.slice(n),o=(0,web_helpers_1.mergeResponseHeaders)(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(a).length)},t);return o.delete("ETag"),o.delete("Last-Modified"),o.delete("Content-Encoding"),new Response(a,{status:e.status,statusText:e.statusText,headers:o})}function resolveEmail(e,r){if(r)try{const t=r(e);if(t)return t}catch{}const t=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email");if(t)return t}function resolveEmailWithBody(e,r,t){const s=resolveEmail(e,t);if(s)return s;const i=r.email;return"string"==typeof i&&i?i:void 0}function extractSessionIdFromRequest(e,r){if(r)try{const t=r(e);if(t)return t}catch{}return(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid")??"unknown"}function dispatchUserEvent(e,r,t,s,i,n){s.mark(i.userId,i.eventType),e.processUserEvent({eventType:i.eventType,userId:i.userId,emailAddress:i.emailAddress,ipAddress:i.ipAddress,deviceId:i.deviceId,fingerprintId:i.fingerprintId,sessionHash:i.sessionId,userAgent:i.userAgent}).then(e=>{e.success&&e.data?.analysis&&r.update(i.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.pause(1e3*e.error.retryAfter)}).catch(e=>{n&&n(e,{operation:"processUserEvent",userId:i.userId,emailAddress:i.emailAddress})})}async function fetchAndCacheVerdict(e,r,t,s,i,n,a,o=CHECK_USER_TIMEOUT_MS,d){const _={};let c;i&&"unknown"!==i&&(_.deviceId=i),n&&(_.fingerprintId=n);const l=await Promise.race([e.checkUser(s,_),new Promise(e=>{c=setTimeout(()=>e(null),o)})]);if(clearTimeout(c),!l)return d?.("timeout"),{isFlagged:!1,isVerified:!1,emailAddress:s,sessionId:a,cachedAt:0,ttl:0};l.failedOpen&&d?.("http_error",l.failedOpen.status);const u=l.data?.is_user_flagged??!1;return r.set(t,{isFlagged:u,isVerified:!1,emailAddress:s,sessionId:a}),r.get(t)}async function handleSubmitFp(e,r,t,s){try{const i={full_hash:r.hash??"",fingerprint_id:r.stable_hash??"",timestamp:r.collected_at??(new Date).toISOString(),isIncognito:r.is_incognito??!1,components:r.components??{},version:r.version??"inline-1.0.0"};let n,a,o;try{const r=t.resolveUserId(e);r&&!(0,sentinel_user_id_1.isSentinelUserId)(r)&&(n=r)}catch{}if(!n){const e="string"==typeof r.user_id?r.user_id:void 0;e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(n=e)}if(!n){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_uid");r&&!(0,sentinel_user_id_1.isSentinelUserId)(r)&&(n=r)}try{a=t.resolveEmailAddress?t.resolveEmailAddress(e):void 0}catch{}a=a??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")??r.email??void 0;try{o=t.resolveSessionId?t.resolveSessionId(e):void 0}catch{}o=o??r.session_id??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid");const d=(0,web_helpers_1.extractClientIpFromRequest)(e),_=e.headers.get("user-agent")??"";if(!t.disableBotFilter&&(0,is_bot_1.isBot)(_))return(0,web_helpers_1.jsonResponse)(200,{success:!0},s);const c=(i.fingerprint_id&&i.fingerprint_id.length>0?i.fingerprint_id:void 0)??(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),l=i.fingerprint_id||void 0,u=i.full_hash||void 0,p=(0,web_helpers_1.isSecureWebRequest)(e)?"; Secure":"",h=[];if(u&&!(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")&&h.push(`__unshared_fingerprint_id=${encodeURIComponent(u)}; HttpOnly; Path=/; SameSite=Lax${p}`),l){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fp_id");r&&r===l||h.push(`__unshared_fp_id=${encodeURIComponent(l)}; Path=/; SameSite=Lax; Max-Age=31536000${p}`)}let f;if(a&&!(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")&&h.push(`__unshared_email=${encodeURIComponent(a)}; HttpOnly; Path=/; SameSite=Lax${p}`),"string"==typeof r.event_type&&r.event_type)f=r.event_type;else{const r=e.headers.get("referer")??e.headers.get("referrer");let t="unknown";if(r)try{const e=new URL(r);t=(e.pathname||"/")+(e.search||"")}catch{}f=t}const m=e.headers.get("x-idempotency-key")||void 0,w=Date.now(),g=m?`${m}|${w}`:l&&n?`${(0,util_1.sha256Hex)(`${l}|${n}|${f}`)}|${w}`:void 0;n&&t.client.submitFingerprintEvent(i,{userId:n,emailAddress:a,sessionHash:o,eventType:f,ipAddress:d,userAgent:_,idempotencyKey:g}).catch(e=>{t.onError&&t.onError(e,{operation:"submitFingerprintEvent",userId:n,emailAddress:a})}),n&&a&&!t.rateLimitBackoff.isPaused()&&!t.dispatchDedupe.wasRecentlyDispatched(n,f)&&t.client.processUserEvent({eventType:f,userId:n,emailAddress:a,ipAddress:d,deviceId:c,fingerprintId:l,sessionHash:o??"unknown",userAgent:_}).then(e=>{e.success&&e.data?.analysis&&t.verdictCache.update(n,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.rateLimitBackoff.pause(1e3*e.error.retryAfter)}).catch(e=>{t.onError&&t.onError(e,{operation:"processUserEvent",userId:n,emailAddress:a})});const b={...s,"Content-Type":"application/json"},v=new Response(JSON.stringify({success:!0}),{status:200,headers:b});for(const e of h)v.headers.append("Set-Cookie",e);return v}catch{return(0,web_helpers_1.jsonResponse)(200,{success:!0},s)}}async function handleVerifyTriggerWeb(e,r,t,s){try{const i=resolveEmailWithBody(e,r??{},t.resolveEmailAddress);if(!i)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},s);const n=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),a=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,o=await t.client.triggerEmailVerification(i,n,{fingerprintId:a});return o.success?(0,web_helpers_1.jsonResponse)(200,{success:!0,data:o.data},s):(0,web_helpers_1.jsonResponse)(200,{success:!1,error:o.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},s)}catch(e){return t.onError&&t.onError(e,{operation:"verifyTrigger"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},s)}}async function handleVerifyWeb(e,r,t,s){try{const i=resolveEmailWithBody(e,r??{},t.resolveEmailAddress),n=r?.code;if(!i||!n)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}},s);const a=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),o=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,d=await t.client.verify(i,a,n,{fingerprintId:o});if(d.success){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_uid");return r&&t.verdictCache.update(r,{isVerified:!0}),(0,web_helpers_1.jsonResponse)(200,{success:!0,data:{verified:!0}},s)}return(0,web_helpers_1.jsonResponse)(200,{success:!1,error:d.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}},s)}catch(e){return t.onError&&t.onError(e,{operation:"verify"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},s)}}
@@ -57,6 +57,10 @@ export interface WebProtectionConfig {
57
57
  skipPaths?: string[];
58
58
  /** When set, only paths matching one of these prefixes get events dispatched and checkUser called. */
59
59
  includePathPrefix?: string[];
60
+ /** Skip the bot/crawler UA filter. Set to true in test environments so automated browsers (Playwright, Puppeteer, etc.) can observe verdicts. @default false */
61
+ disableBotFilter?: boolean;
62
+ /** Hard timeout (ms) for checkUser on cache miss. Fails open on timeout. @default 500 */
63
+ checkUserTimeoutMs?: number;
60
64
  /** Resolves a custom session ID. Falls back to __unshared_sid cookie. */
61
65
  sessionId?: (req: Request) => string | undefined;
62
66
  /**
@@ -81,6 +85,51 @@ export interface WebProtectionConfig {
81
85
  * gracefully — the cached-fingerprint path still works.
82
86
  */
83
87
  fingerprintSdkBundle?: string;
88
+ /**
89
+ * Server-side enforcement. When `true`, a flagged + unverified request is
90
+ * short-circuited before `next()` runs — the protected content is never served.
91
+ * HTML navigations receive a standalone verification gate page (which renders the
92
+ * interstitial and reloads on success); everything else receives a
93
+ * `403 { error: 'account_flagged' }`. `onFlagged` is ignored in this mode.
94
+ * Requires `fingerprintSdkBundle` to be provided and the default `routePrefix`.
95
+ * @default false
96
+ *
97
+ * `blockFlagged: true` is equivalent to `flaggedMode: 'gate'` (see {@link flaggedMode}).
98
+ */
99
+ blockFlagged?: boolean;
100
+ /**
101
+ * How the SDK enforces a flagged + unverified user (Web/edge equivalent of the Node
102
+ * middleware's `flaggedMode`). Setting it enables enforcement on its own; when both
103
+ * are set, `flaggedMode` wins.
104
+ *
105
+ * - `'gate'` — hard gate (== `blockFlagged: true`): HTML navigations get the standalone
106
+ * verification gate page; everything else gets `403 account_flagged`. Content never sent.
107
+ * - `'overlay'` — modal over blurred live content: HTML navigations render normally and
108
+ * the SDK auto-renders the modal over the blurred page; non-HTML/data requests get
109
+ * `403 account_flagged`. Implies `autoInterstitial`. The HTML is delivered (blur is
110
+ * cosmetic) — the real protection is the `403` on the gated data.
111
+ *
112
+ * Both modes ignore `onFlagged` and require `fingerprintSdkBundle` + the default
113
+ * `routePrefix`. @default undefined
114
+ */
115
+ flaggedMode?: 'gate' | 'overlay';
116
+ /**
117
+ * Auto-render the interstitial modal on SPA/JSON `403 account_flagged` paths
118
+ * (and the deferred `/status` poll) with no app code. When `true`, the injected
119
+ * script boots a proxy-mode browser SDK instance and renders the modal whenever
120
+ * the `unshared:flagged` event fires, reloading the page on successful verify.
121
+ *
122
+ * Content protection still comes from the 403 withholding the data — pair with
123
+ * `blockFlagged` (or an `onFlagged` that 403s your data APIs). Requires
124
+ * `fingerprintSdkBundle` to be provided and the default `routePrefix`.
125
+ * @default false
126
+ */
127
+ autoInterstitial?: boolean;
128
+ /**
129
+ * Flow type requested for the auto-rendered interstitial.
130
+ * Only used when `autoInterstitial` is `true`. @default "email_verification"
131
+ */
132
+ interstitialFlowType?: string;
84
133
  /**
85
134
  * Called when a flagged, unverified user makes a request.
86
135
  *
@@ -107,4 +156,19 @@ export interface WebProtectionConfig {
107
156
  userId?: string;
108
157
  emailAddress?: string;
109
158
  }) => void;
159
+ /**
160
+ * Called whenever a verdict check could NOT be determined and the request
161
+ * therefore passes through (fails open) — see the Node middleware's
162
+ * `onFailOpen`. A non-2xx verdict response is masked into a clean "not flagged"
163
+ * verdict and never surfaces via `onError`; this hook makes that observable.
164
+ * The fail-open behaviour is unchanged; invoked best-effort, never blocks.
165
+ */
166
+ onFailOpen?: (context: WebFailOpenContext) => void;
167
+ }
168
+ export interface WebFailOpenContext {
169
+ operation: 'checkUser';
170
+ reason: 'timeout' | 'http_error' | 'exception';
171
+ status?: number;
172
+ userId?: string;
173
+ emailAddress?: string;
110
174
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unshared-clientjs-sdk",
3
- "version": "2.0.2",
3
+ "version": "2.1.0-rc.10",
4
4
  "description": "Server-side Node.js SDK for the Unshared Labs V2 API",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/esm/index.mjs",
@@ -52,9 +52,10 @@
52
52
  "author": "",
53
53
  "license": "MIT",
54
54
  "dependencies": {
55
- "unshared-frontend-sdk": "2.0.2"
55
+ "unshared-frontend-sdk": "2.1.0-rc.10"
56
56
  },
57
57
  "devDependencies": {
58
+ "@unshared-labs/shared-types": "file:../../../shared/types",
58
59
  "@types/express": "^4.17.21",
59
60
  "@types/node": "^24.10.1",
60
61
  "express": "^4.18.2",