unshared-clientjs-sdk 2.0.0-rc.13 → 2.0.0-rc.15
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.
- package/dist/esm/middleware/index.mjs +1 -1
- package/dist/esm/middleware/injection/fingerprint-script.d.mts +11 -5
- package/dist/esm/middleware/injection/fingerprint-script.mjs +1 -1
- package/dist/esm/middleware/response-interceptor.d.mts +9 -7
- package/dist/esm/middleware/response-interceptor.mjs +1 -1
- package/dist/esm/middleware/routes/submit-fp.mjs +1 -1
- package/dist/esm/middleware/routes/verify.mjs +1 -1
- package/dist/esm/middleware/utils/cookies.d.mts +6 -0
- package/dist/esm/middleware/utils/cookies.mjs +1 -0
- package/dist/esm/middleware/utils/device-id.d.mts +5 -0
- package/dist/esm/middleware/utils/device-id.mjs +1 -0
- package/dist/esm/middleware/utils/secure.d.mts +3 -0
- package/dist/esm/middleware/utils/secure.mjs +1 -0
- package/dist/esm/middleware/utils/skip-paths.mjs +1 -1
- package/dist/esm/middleware/verdict-cache.d.mts +12 -1
- package/dist/esm/middleware/verdict-cache.mjs +1 -1
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/injection/fingerprint-script.d.ts +11 -5
- package/dist/middleware/injection/fingerprint-script.js +1 -1
- package/dist/middleware/response-interceptor.d.ts +9 -7
- package/dist/middleware/response-interceptor.js +1 -1
- package/dist/middleware/routes/submit-fp.js +1 -1
- package/dist/middleware/routes/verify.js +1 -1
- package/dist/middleware/utils/cookies.d.ts +6 -0
- package/dist/middleware/utils/cookies.js +1 -0
- package/dist/middleware/utils/device-id.d.ts +5 -0
- package/dist/middleware/utils/device-id.js +1 -0
- package/dist/middleware/utils/secure.d.ts +3 -0
- package/dist/middleware/utils/secure.js +1 -0
- package/dist/middleware/utils/skip-paths.js +1 -1
- package/dist/middleware/verdict-cache.d.ts +12 -1
- package/dist/middleware/verdict-cache.js +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFileSync}from"fs";import{VerdictCache}from"./verdict-cache";import{RateLimitBackoff}from"./rate-limit-backoff";import{interceptResponse}from"./response-interceptor";import{generateFingerprintScript}from"./injection/fingerprint-script";import{handleSubmitFingerprint}from"./routes/submit-fp";import{handleVerifyTrigger,handleVerify}from"./routes/verify";import{isHtmlContentType}from"./utils/content-type";import{shouldSkipPath}from"./utils/skip-paths";import{isBot}from"./utils/is-bot";import{extractClientIp}from"./utils/client-ip";export{VerdictCache};const CHECK_USER_TIMEOUT_MS=500;export function unsharedBoundToUser(e,
|
|
1
|
+
import{readFileSync}from"fs";import{VerdictCache}from"./verdict-cache";import{RateLimitBackoff}from"./rate-limit-backoff";import{interceptResponse}from"./response-interceptor";import{generateFingerprintScript}from"./injection/fingerprint-script";import{handleSubmitFingerprint}from"./routes/submit-fp";import{handleVerifyTrigger,handleVerify}from"./routes/verify";import{isHtmlContentType}from"./utils/content-type";import{shouldSkipPath}from"./utils/skip-paths";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";export{VerdictCache};const CHECK_USER_TIMEOUT_MS=500;export function unsharedBoundToUser(e,r){if(!r.userId)throw new Error("[Unshared] userId resolver is required");if(!r.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:t,emailAddress:i,routePrefix:n="/__unshared",corsOrigins:o,cacheTTL:s=6e4,skipPaths:c,sessionId:d,deviceId:a,onFlagged:l}=r,u=new VerdictCache(s),p=new RateLimitBackoff,f=Date.now().toString(36),m=generateFingerprintScript(n,f);let h="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");h=readFileSync(e,"utf8")}catch{}const v=handleSubmitFingerprint({client:e,verdictCache:u,rateLimitBackoff:p,resolveUserId:t,resolveEmailAddress:i,resolveSessionId:d,resolveDeviceId:a}),C=handleVerifyTrigger({client:e,verdictCache:u,resolveEmailAddress:i,resolveDeviceId:a}),I=handleVerify({client:e,verdictCache:u,resolveEmailAddress:i,resolveDeviceId:a}),S=o?Array.isArray(o)?o:[o]:null,g=`${n}/fp.js`,k=`${n}/submit-fp`,y=`${n}/verify-trigger`,_=`${n}/verify`;return function(r,o,s){const f=r.path;if(f.startsWith(n+"/"))return function(e,r){if(!S)return;const t=e.headers.origin??"",i=S.includes("*");(i||S.includes(t))&&(r.setHeader("Access-Control-Allow-Origin",i?"*":t),r.setHeader("Access-Control-Allow-Methods","POST, OPTIONS"),r.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),r.setHeader("Access-Control-Allow-Credentials","true"))}(r,o),"OPTIONS"===r.method?void o.status(204).end():"GET"===r.method&&f===g?(o.setHeader("Content-Type","application/javascript"),o.setHeader("Cache-Control","public, max-age=3600"),void o.status(200).end(h)):"POST"===r.method&&f===k?void v(r,o):"POST"===r.method&&f===y?void C(r,o):"POST"===r.method&&f===_?void I(r,o):void o.status(404).json({success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}});if(shouldSkipPath(f,c))return void s();let A;try{A=t(r)}catch{}if(!A)return clearUserIdCookieIfPresent(r,o),clearEmailCookieIfPresent(r,o),interceptForInjection(r,o,m),void s();const T=resolveEmail(r,i);if(setUserIdCookie(r,o,A),T&&setEmailCookie(r,o,T),!T)return interceptForInjection(r,o,m),void s();const x=extractSessionId(r,d),P=extractDeviceId(r,a),F=extractFingerprintId(r),E=r.headers["user-agent"]??"",w=extractClientIp(r);if(isBot(E))return void s();if("unknown"===x)return interceptForInjection(r,o,m),void s();p.isPaused()||dispatchUserEvent(e,u,p,{userId:A,emailAddress:T,sessionId:x,deviceId:P,fingerprintId:F,userAgent:E,ipAddress:w,eventType:`${r.method} ${r.path}`});const U=u.get(A);U?(u.isStale(A)&&!u.isRefreshing(A)&&(u.markRefreshing(A),fetchAndCacheVerdict(e,u,A,T,P,F,x).finally(()=>u.clearRefreshing(A))),applyVerdict(U,A,T,r,o,s,m,l)):fetchAndCacheVerdict(e,u,A,T,P,F,x).then(e=>{applyVerdict(e,A,T,r,o,s,m,l)}).catch(()=>{interceptForInjection(r,o,m),s()})}}function resolveEmail(e,r){if(r)try{const t=r(e);if(t)return t}catch{}const t=parseCookie(e,"__unshared_email");if(t)return t;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,r,t,i,n,o,s,c){if(interceptForInjection(i,n,s),e.isFlagged&&!e.isVerified&&c)try{c({userId:r,emailAddress:t,verdict:e,req:i,res:n,next:o})}catch{o()}else o()}function interceptForInjection(e,r,t){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],interceptResponse(r,(e,r)=>{if(!isHtmlContentType(r))return null;const i=e.toString("utf8"),n=i.lastIndexOf("</body>");return-1===n?i+t:i.slice(0,n)+t+i.slice(n)},{preventCaching:!0})}function dispatchUserEvent(e,r,t,i){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(()=>{})}async function fetchAndCacheVerdict(e,r,t,i,n,o,s){const c={};let d;n&&"unknown"!==n&&(c.deviceId=n),o&&(c.fingerprintId=o);const a=await Promise.race([e.checkUser(i,c),new Promise(e=>{d=setTimeout(()=>e(null),500)})]);if(clearTimeout(d),!a)return{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:s,cachedAt:0,ttl:0};const l=a.data?.is_user_flagged??!1;return r.set(t,{isFlagged:l,isVerified:!1,emailAddress:i,sessionId:s}),r.get(t)}function extractSessionId(e,r){if(r)try{const t=r(e);if(t)return t}catch{}return parseCookie(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return parseCookie(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,r){const t=e.getHeader("Set-Cookie");if(t){const i=Array.isArray(t)?[...t]:[String(t)];i.push(r),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",r)}function setUserIdCookie(e,r,t){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(r,`__unshared_uid=${encodeURIComponent(t)}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,r,t){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(r,`__unshared_email=${encodeURIComponent(t)}; HttpOnly; Path=/; SameSite=Lax${i}`)}function clearUserIdCookieIfPresent(e,r){parseCookie(e,"__unshared_uid")&&appendSetCookie(r,"__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0"+(isSecureRequest(e)?"; Secure":""))}function clearEmailCookieIfPresent(e,r){parseCookie(e,"__unshared_email")&&appendSetCookie(r,"__unshared_email=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0"+(isSecureRequest(e)?"; Secure":""))}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Generates
|
|
3
|
-
* 1. Loads the
|
|
4
|
-
* 2. Collects a
|
|
5
|
-
* 3.
|
|
2
|
+
* Generates an inline loader script that:
|
|
3
|
+
* 1. Loads the fingerprint SDK from /__unshared/fp.js
|
|
4
|
+
* 2. Collects a fingerprint and POSTs to /__unshared/submit-fp
|
|
5
|
+
* 3. Caches fingerprint in sessionStorage for reuse on SPA navigations
|
|
6
|
+
* 4. Patches History API to detect SPA route changes → re-submits fingerprint
|
|
7
|
+
* 5. Patches fetch/XHR to detect 403 account_flagged → dispatches "unshared:flagged" event
|
|
6
8
|
*
|
|
7
9
|
* The actual SDK UMD bundle is served by the middleware at /__unshared/fp.js.
|
|
8
|
-
*
|
|
10
|
+
*
|
|
11
|
+
* Event contract:
|
|
12
|
+
* window.addEventListener("unshared:flagged", (e) => {
|
|
13
|
+
* e.detail.email — the flagged user's email (from 403 response body)
|
|
14
|
+
* });
|
|
9
15
|
*/
|
|
10
16
|
export declare function generateFingerprintScript(routePrefix: string, version?: string): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function generateFingerprintScript(e,n){const t=n?`?v=${escapeJavaScript(n)}`:"";return`<script>\n(function(){\ntry{\nvar pfx="${escapeJavaScript(e)}";\n\n//
|
|
1
|
+
export function generateFingerprintScript(e,n){const t=n?`?v=${escapeJavaScript(n)}`:"";return`<script>\n(function(){\ntry{\nvar pfx="${escapeJavaScript(e)}";\nvar SS_FP="__unshared_fp";\n\n// --- Helpers ---\nfunction gC(n){var m=document.cookie.match(new RegExp("(?:^|; )"+n+"=([^;]*)"));return m?decodeURIComponent(m[1]):null}\nfunction sC(n,v,d){var e="";if(d){var dt=new Date();dt.setTime(dt.getTime()+d*864e5);e="; expires="+dt.toUTCString()}document.cookie=n+"="+encodeURIComponent(v)+e+"; path=/; SameSite=Lax"}\nfunction uuid(){return(typeof crypto!=="undefined"&&crypto.randomUUID)?crypto.randomUUID():("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(c){var r=Math.random()*16|0;return(c==="x"?r:r&0x3|0x8).toString(16)}))}\n\n// --- Session + device IDs ---\nvar sid=gC("__unshared_sid");\nif(!sid){sid=uuid();sC("__unshared_sid",sid,365)}\nvar did="";\ntry{did=localStorage.getItem("__unshared_device_id")||"";if(!did){did=uuid();localStorage.setItem("__unshared_device_id",did)}}catch(e){did=did||uuid()}\nsC("__unshared_fp_id",did,365);\n\n// --- Fingerprint cache (sessionStorage) ---\nfunction getFP(){try{var r=sessionStorage.getItem(SS_FP);return r?JSON.parse(r):null}catch(e){return null}}\nfunction setFP(fp){try{sessionStorage.setItem(SS_FP,JSON.stringify(fp))}catch(e){}}\n\n// --- Submit fingerprint to backend ---\nfunction submitFP(fp,evType){\n var uid=gC("__unshared_uid");\n if(!uid)return;\n var body={hash:fp.full_hash,stable_hash:fp.fingerprint_id,collected_at:fp.timestamp,is_incognito:fp.isIncognito,components:fp.components,version:fp.version,session_id:sid,user_id:uid,event_type:evType||"page_load"};\n var xhr=new XMLHttpRequest();\n xhr.open("POST",pfx+"/submit-fp",true);\n xhr.setRequestHeader("Content-Type","application/json");\n xhr.setRequestHeader("X-Session-Id",sid);\n xhr.setRequestHeader("X-Device-Id",did);\n xhr.send(JSON.stringify(body));\n}\n\n// --- Collect fingerprint (loads fp.js if needed) then submit ---\nvar fpReady=false;\nfunction collectAndSubmit(evType){\n var uid=gC("__unshared_uid");\n if(!uid)return;\n var cached=getFP();\n if(cached){submitFP(cached,evType);return}\n if(!fpReady)return;\n try{\n var c=new UnsharedLabsBrowser.UnsharedLabsBrowser({baseUrl:""});\n c.collect({exclude:["timing","navigatorConnection"]}).then(function(fp){setFP(fp);submitFP(fp,evType)});\n }catch(e){}\n}\n\n// --- Load fp.js (always — browser caches it for 1h) ---\n// Submit cached FP immediately if available; load fp.js for fresh collection\nvar pageLoadSubmitted=false;\nif(getFP()&&gC("__unshared_uid")){submitFP(getFP(),"page_load");pageLoadSubmitted=true}\nvar s=document.createElement("script");\ns.src=pfx+"/fp.js${t}";\ns.onload=function(){fpReady=true;if(!pageLoadSubmitted)collectAndSubmit("page_load")};\ndocument.head.appendChild(s);\n\n// --- SPA route change tracking (History API + popstate) ---\nvar oPush=history.pushState,oReplace=history.replaceState;\nhistory.pushState=function(){oPush.apply(this,arguments);try{collectAndSubmit("route_change")}catch(e){}};\nhistory.replaceState=function(){oReplace.apply(this,arguments);try{collectAndSubmit("route_change")}catch(e){}};\nwindow.addEventListener("popstate",function(){try{collectAndSubmit("route_change")}catch(e){}});\n\n// --- 403 interception: dispatch "unshared:flagged" event ---\nfunction emitFlagged(body){\n try{window.dispatchEvent(new CustomEvent("unshared:flagged",{detail:{email:body.email||""}}))}catch(e){}\n}\n\n// Patch fetch\nvar oFetch=window.fetch;\nif(oFetch){window.fetch=function(){return oFetch.apply(this,arguments).then(function(r){if(r.status===403){try{var cl=r.clone();cl.json().then(function(b){if(b&&b.error==="account_flagged")emitFlagged(b)}).catch(function(){})}catch(e){}}return r})}}\n\n// Patch XMLHttpRequest\nvar oXSend=XMLHttpRequest.prototype.send;\nXMLHttpRequest.prototype.send=function(){var x=this;x.addEventListener("load",function(){if(x.status===403){try{var b=JSON.parse(x.responseText);if(b&&b.error==="account_flagged")emitFlagged(b)}catch(e){}}});return oXSend.apply(this,arguments)};\n\n}catch(e){}\n})();\n<\/script>`}function escapeJavaScript(e){return e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/'/g,"\\'")}
|
|
@@ -2,12 +2,14 @@ import type { Response } from 'express';
|
|
|
2
2
|
/**
|
|
3
3
|
* Intercepts the response body by wrapping res.write() and res.end().
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* to pass through unchanged.
|
|
5
|
+
* Only buffers HTML responses (text/html). Non-HTML responses (JSON, images,
|
|
6
|
+
* CSS, JS, etc.) pass through to the original write/end without buffering,
|
|
7
|
+
* avoiding unnecessary memory usage on large payloads.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* When res.end() is called on an HTML response, invokes the `transform`
|
|
10
|
+
* callback with the complete body buffer and the Content-Type header.
|
|
11
|
+
* The transform can return modified content or null to pass through unchanged.
|
|
12
12
|
*/
|
|
13
|
-
export declare function interceptResponse(res: Response, transform: (body: Buffer, contentType: string | undefined) => Buffer | string | null
|
|
13
|
+
export declare function interceptResponse(res: Response, transform: (body: Buffer, contentType: string | undefined) => Buffer | string | null, options?: {
|
|
14
|
+
preventCaching?: boolean;
|
|
15
|
+
}): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function interceptResponse(n,t){const f
|
|
1
|
+
export function interceptResponse(n,t,f){const e=f?.preventCaching??!1,o=n.write.bind(n),u=n.end.bind(n),r=n.writeHead.bind(n),c=[];let i=!1,l=null;function s(){if(null!==l)return;const t=n.getHeader("content-type");null!=t&&(l=String(t).includes("text/html"),l||function(){n.write=o,n.end=u;for(const n of c)o(n);c.length=0}())}n.writeHead=function(t,...f){return s(),e&&l&&(n.setHeader("Cache-Control","no-store"),n.removeHeader("ETag"),n.removeHeader("Last-Modified")),r(t,...f)},n.write=function(n,t,f){if(s(),!1===l)return o(n,t,f);if(null!=n){const f=Buffer.isBuffer(n)?n:Buffer.from(n,"string"==typeof t?t:"utf8");c.push(f)}return"function"==typeof t&&t(null),"function"==typeof f&&f(null),!0},n.end=function(f,e,r){if(i)return n;if(i=!0,s(),!1===l)return u(f,e,r);if(null!=f){const n=Buffer.isBuffer(f)?f:Buffer.from(f,"string"==typeof e?e:"utf8");c.push(n)}const p=Buffer.concat(c),y=n.getHeader("content-type");let B;try{B=t(p,y)}catch{B=null}if(null!=B){const t=Buffer.isBuffer(B)?B:Buffer.from(B,"utf8");n.setHeader("Content-Length",t.length),n.removeHeader("Content-Encoding"),o(t)}else p.length>0&&o(p);const g="function"==typeof e?e:r;return g?u(g):u(),n}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isBot}from"../utils/is-bot";import{extractClientIp}from"../utils/client-ip";export function handleSubmitFingerprint(e){return async(t
|
|
1
|
+
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";export function handleSubmitFingerprint(e){return async(i,t)=>{try{const s=i.body??{},o={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"};let r,n,c;try{r=e.resolveUserId?e.resolveUserId(i):void 0}catch{}r=r??s.user_id??void 0;try{n=e.resolveEmailAddress?e.resolveEmailAddress(i):void 0}catch{}n=n??parseCookie(i,"__unshared_email")??s.email??void 0;try{c=e.resolveSessionId?e.resolveSessionId(i):void 0}catch{}c=c??s.session_id??parseCookie(i,"__unshared_sid");const a=extractClientIp(i),d=i.headers["user-agent"]??"";if(isBot(d))return void t.status(200).json({success:!0});const u=extractDeviceId(i,e.resolveDeviceId),p=o.fingerprint_id||void 0,l=isSecureRequest(i)?"; Secure":"",_=[];if(p&&!parseCookie(i,"__unshared_fingerprint_id")&&_.push(`__unshared_fingerprint_id=${encodeURIComponent(p)}; HttpOnly; Path=/; SameSite=Lax${l}`),n&&!parseCookie(i,"__unshared_email")&&_.push(`__unshared_email=${encodeURIComponent(n)}; HttpOnly; Path=/; SameSite=Lax${l}`),_.length>0){const e=t.getHeader("Set-Cookie");if(e){const i=Array.isArray(e)?[...e]:[String(e)];i.push(..._),t.setHeader("Set-Cookie",i)}else t.setHeader("Set-Cookie",_)}r&&e.client.submitFingerprintEvent(o,{userId:r,emailAddress:n,sessionHash:c,eventType:"auto_collect",ipAddress:a,userAgent:d}).catch(()=>{}),r&&n&&!e.rateLimitBackoff.isPaused()&&e.client.processUserEvent({eventType:"auto_collect",userId:r,emailAddress:n,ipAddress:a,deviceId:u,fingerprintId:p,sessionHash:c??"unknown",userAgent:d}).then(i=>{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(()=>{}),t.status(200).json({success:!0})}catch{t.status(200).json({success:!0})}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function handleVerifyTrigger(e){return async(r,i)=>{try{const s=resolveEmail(r,r.body??{},e.resolveEmailAddress);if(!s)return void i.status(400).json({success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}});const
|
|
1
|
+
import{parseCookie}from"../utils/cookies";import{extractDeviceId}from"../utils/device-id";export function handleVerifyTrigger(e){return async(r,i)=>{try{const s=resolveEmail(r,r.body??{},e.resolveEmailAddress);if(!s)return void i.status(400).json({success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}});const o=extractDeviceId(r,e.resolveDeviceId),c=parseCookie(r,"__unshared_fingerprint_id")||void 0,t=await e.client.triggerEmailVerification(s,o,{fingerprintId:c});t.success?i.status(200).json({success:!0,data:t.data}):i.status(200).json({success:!1,error:t.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}})}catch{i.status(200).json({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),c=s.code;if(!o||!c)return void i.status(400).json({success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}});const t=extractDeviceId(r,e.resolveDeviceId),a=parseCookie(r,"__unshared_fingerprint_id")||void 0,n=await e.client.verify(o,t,c,{fingerprintId:a});if(n.success){const s=parseCookie(r,"__unshared_uid");s&&e.verdictCache.update(s,{isVerified:!0}),i.status(200).json({success:!0,data:{verified:!0}})}else i.status(200).json({success:!1,error:n.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}})}catch{i.status(200).json({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}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function parseCookie(e,o){const n=e.headers.cookie;if(!n)return;const t=n.match(new RegExp(`(?:^|; )${o}=([^;]*)`));return t?decodeURIComponent(t[1]):void 0}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{parseCookie}from"./cookies";export function extractDeviceId(o,r){if(r)try{const t=r(o);if(t)return t}catch{}const t=parseCookie(o,"__unshared_fp_id");if(t)return t;const e=o.headers["x-device-id"];return"string"==typeof e&&e?e:"unknown"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function isSecureRequest(e){return e.secure||"https"===e.headers["x-forwarded-proto"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const STATIC_EXTENSIONS=new Set([".js",".mjs",".cjs",".css",".map",".png",".jpg",".jpeg",".gif",".svg",".ico",".webp",".avif",".woff",".woff2",".ttf",".otf",".eot",".mp3",".mp4",".webm",".ogg",".wasm",".
|
|
1
|
+
const STATIC_EXTENSIONS=new Set([".js",".mjs",".cjs",".css",".map",".png",".jpg",".jpeg",".gif",".svg",".ico",".webp",".avif",".woff",".woff2",".ttf",".otf",".eot",".mp3",".mp4",".webm",".ogg",".wasm",".xml",".txt",".pdf"]),STATIC_PATH_PREFIXES=["/static/","/assets/","/public/","/_next/","/__vite/","/favicon"];export function shouldSkipPath(t,f){if(f)for(const o of f)if(t.startsWith(o))return!0;const o=t.lastIndexOf(".");if(-1!==o){const f=t.slice(o).toLowerCase().split("?")[0];if(STATIC_EXTENSIONS.has(f))return!0}for(const f of STATIC_PATH_PREFIXES)if(t.startsWith(f))return!0;return!1}
|
|
@@ -10,7 +10,9 @@ export declare class VerdictCache {
|
|
|
10
10
|
private readonly _entries;
|
|
11
11
|
private readonly _activeRefreshes;
|
|
12
12
|
private readonly _defaultTtlMs;
|
|
13
|
-
|
|
13
|
+
private readonly _maxSize;
|
|
14
|
+
private _sweepTimer;
|
|
15
|
+
constructor(defaultTTL?: number, maxSize?: number);
|
|
14
16
|
get(userId: string): Verdict | undefined;
|
|
15
17
|
set(userId: string, verdict: Omit<Verdict, 'cachedAt' | 'ttl'>, ttl?: number): void;
|
|
16
18
|
/**
|
|
@@ -33,4 +35,13 @@ export declare class VerdictCache {
|
|
|
33
35
|
/** Number of cached entries. */
|
|
34
36
|
get size(): number;
|
|
35
37
|
clear(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Stops the periodic sweep timer. Call this when shutting down
|
|
40
|
+
* or when the middleware is no longer needed (e.g., in tests).
|
|
41
|
+
*/
|
|
42
|
+
destroy(): void;
|
|
43
|
+
/** Remove all entries that are past their TTL + a 2x grace period. */
|
|
44
|
+
private _sweep;
|
|
45
|
+
/** Evict the oldest entry by cachedAt to make room. */
|
|
46
|
+
private _evictOldest;
|
|
36
47
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export class VerdictCache{constructor(t=6e4){this.t=new Map,this.i=new Set,this.h=t}get(t){return this.t.get(t)}set(t,
|
|
1
|
+
const DEFAULT_TTL_MS=6e4,DEFAULT_MAX_SIZE=1e4,SWEEP_INTERVAL_MS=3e5;export class VerdictCache{constructor(t=6e4,s=1e4){this.t=new Map,this.i=new Set,this.h=null,this.l=t,this.o=s,this.h=setInterval(()=>this.u(),3e5),this.h&&"function"==typeof this.h.unref&&this.h.unref()}get(t){return this.t.get(t)}set(t,s,i){!this.t.has(t)&&this.t.size>=this.o&&this._(),this.t.set(t,{...s,cachedAt:Date.now(),ttl:i??this.l})}update(t,s){const i=this.t.get(t);i?(void 0!==s.isFlagged&&(i.isFlagged=s.isFlagged),void 0!==s.isVerified&&(i.isVerified=s.isVerified),i.cachedAt=Date.now()):(this.t.size>=this.o&&this._(),this.t.set(t,{isFlagged:s.isFlagged??!1,isVerified:s.isVerified??!1,emailAddress:"",sessionId:"",cachedAt:Date.now(),ttl:this.l}))}delete(t){this.t.delete(t),this.i.delete(t)}isStale(t){const s=this.t.get(t);return!!s&&Date.now()-s.cachedAt>s.ttl}isRefreshing(t){return this.i.has(t)}markRefreshing(t){this.i.add(t)}clearRefreshing(t){this.i.delete(t)}get size(){return this.t.size}clear(){this.t.clear(),this.i.clear()}destroy(){this.h&&(clearInterval(this.h),this.h=null),this.clear()}u(){const t=Date.now();for(const[s,i]of this.t)t-i.cachedAt>2*i.ttl&&(this.t.delete(s),this.i.delete(s))}_(){let t=null,s=1/0;for(const[i,e]of this.t)e.cachedAt<s&&(s=e.cachedAt,t=i);t&&(this.t.delete(t),this.i.delete(t))}}
|
package/dist/middleware/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"
|
|
1
|
+
"use strict";Object.defineProperty(exports,"i",{value:!0}),exports.VerdictCache=void 0,exports.unsharedBoundToUser=unsharedBoundToUser;const fs_1=require("fs"),verdict_cache_1=require("./verdict-cache");Object.defineProperty(exports,"VerdictCache",{enumerable:!0,get:function(){return verdict_cache_1.VerdictCache}});const rate_limit_backoff_1=require("./rate-limit-backoff"),response_interceptor_1=require("./response-interceptor"),fingerprint_script_1=require("./injection/fingerprint-script"),submit_fp_1=require("./routes/submit-fp"),verify_1=require("./routes/verify"),content_type_1=require("./utils/content-type"),skip_paths_1=require("./utils/skip-paths"),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"),CHECK_USER_TIMEOUT_MS=500;function unsharedBoundToUser(e,r){if(!r.userId)throw new Error("[Unshared] userId resolver is required");if(!r.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:i,emailAddress:t,routePrefix:n="/__unshared",corsOrigins:s,cacheTTL:o=6e4,skipPaths:c,sessionId:d,deviceId:a,onFlagged:u}=r,l=new verdict_cache_1.VerdictCache(o),_=new rate_limit_backoff_1.RateLimitBackoff,f=Date.now().toString(36),p=(0,fingerprint_script_1.generateFingerprintScript)(n,f);let v="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");v=(0,fs_1.readFileSync)(e,"utf8")}catch{}const h=(0,submit_fp_1.handleSubmitFingerprint)({client:e,verdictCache:l,rateLimitBackoff:_,resolveUserId:i,resolveEmailAddress:t,resolveSessionId:d,resolveDeviceId:a}),m=(0,verify_1.handleVerifyTrigger)({client:e,verdictCache:l,resolveEmailAddress:t,resolveDeviceId:a}),I=(0,verify_1.handleVerify)({client:e,verdictCache:l,resolveEmailAddress:t,resolveDeviceId:a}),k=s?Array.isArray(s)?s:[s]:null,C=`${n}/fp.js`,g=`${n}/submit-fp`,S=`${n}/verify-trigger`,y=`${n}/verify`;return function(r,s,o){const f=r.path;if(f.startsWith(n+"/"))return function(e,r){if(!k)return;const i=e.headers.origin??"",t=k.includes("*");(t||k.includes(i))&&(r.setHeader("Access-Control-Allow-Origin",t?"*":i),r.setHeader("Access-Control-Allow-Methods","POST, OPTIONS"),r.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),r.setHeader("Access-Control-Allow-Credentials","true"))}(r,s),"OPTIONS"===r.method?void s.status(204).end():"GET"===r.method&&f===C?(s.setHeader("Content-Type","application/javascript"),s.setHeader("Cache-Control","public, max-age=3600"),void s.status(200).end(v)):"POST"===r.method&&f===g?void h(r,s):"POST"===r.method&&f===S?void m(r,s):"POST"===r.method&&f===y?void I(r,s):void s.status(404).json({success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}});if((0,skip_paths_1.shouldSkipPath)(f,c))return void o();let A;try{A=i(r)}catch{}if(!A)return clearUserIdCookieIfPresent(r,s),clearEmailCookieIfPresent(r,s),interceptForInjection(r,s,p),void o();const T=resolveEmail(r,t);if(setUserIdCookie(r,s,A),T&&setEmailCookie(r,s,T),!T)return interceptForInjection(r,s,p),void o();const q=extractSessionId(r,d),x=(0,device_id_1.extractDeviceId)(r,a),P=extractFingerprintId(r),b=r.headers["user-agent"]??"",E=(0,client_ip_1.extractClientIp)(r);if((0,is_bot_1.isBot)(b))return void o();if("unknown"===q)return interceptForInjection(r,s,p),void o();_.isPaused()||dispatchUserEvent(e,l,_,{userId:A,emailAddress:T,sessionId:q,deviceId:x,fingerprintId:P,userAgent:b,ipAddress:E,eventType:`${r.method} ${r.path}`});const O=l.get(A);O?(l.isStale(A)&&!l.isRefreshing(A)&&(l.markRefreshing(A),fetchAndCacheVerdict(e,l,A,T,x,P,q).finally(()=>l.clearRefreshing(A))),applyVerdict(O,A,T,r,s,o,p,u)):fetchAndCacheVerdict(e,l,A,T,x,P,q).then(e=>{applyVerdict(e,A,T,r,s,o,p,u)}).catch(()=>{interceptForInjection(r,s,p),o()})}}function resolveEmail(e,r){if(r)try{const i=r(e);if(i)return i}catch{}const i=(0,cookies_1.parseCookie)(e,"__unshared_email");if(i)return i;const t=e.body?.email;return"string"==typeof t&&t?t:void 0}function applyVerdict(e,r,i,t,n,s,o,c){if(interceptForInjection(t,n,o),e.isFlagged&&!e.isVerified&&c)try{c({userId:r,emailAddress:i,verdict:e,req:t,res:n,next:s})}catch{s()}else s()}function interceptForInjection(e,r,i){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],(0,response_interceptor_1.interceptResponse)(r,(e,r)=>{if(!(0,content_type_1.isHtmlContentType)(r))return null;const t=e.toString("utf8"),n=t.lastIndexOf("</body>");return-1===n?t+i:t.slice(0,n)+i+t.slice(n)},{preventCaching:!0})}function dispatchUserEvent(e,r,i,t){e.processUserEvent({eventType:t.eventType,userId:t.userId,emailAddress:t.emailAddress,ipAddress:t.ipAddress,deviceId:t.deviceId,fingerprintId:t.fingerprintId,sessionHash:t.sessionId,userAgent:t.userAgent}).then(e=>{e.success&&e.data?.analysis&&r.update(t.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&i.pause(1e3*e.error.retryAfter)}).catch(()=>{})}async function fetchAndCacheVerdict(e,r,i,t,n,s,o){const c={};let d;n&&"unknown"!==n&&(c.deviceId=n),s&&(c.fingerprintId=s);const a=await Promise.race([e.checkUser(t,c),new Promise(e=>{d=setTimeout(()=>e(null),500)})]);if(clearTimeout(d),!a)return{isFlagged:!1,isVerified:!1,emailAddress:t,sessionId:o,cachedAt:0,ttl:0};const u=a.data?.is_user_flagged??!1;return r.set(i,{isFlagged:u,isVerified:!1,emailAddress:t,sessionId:o}),r.get(i)}function extractSessionId(e,r){if(r)try{const i=r(e);if(i)return i}catch{}return(0,cookies_1.parseCookie)(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return(0,cookies_1.parseCookie)(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,r){const i=e.getHeader("Set-Cookie");if(i){const t=Array.isArray(i)?[...i]:[String(i)];t.push(r),e.setHeader("Set-Cookie",t)}else e.setHeader("Set-Cookie",r)}function setUserIdCookie(e,r,i){const t=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(r,`__unshared_uid=${encodeURIComponent(i)}; Path=/; SameSite=Lax${t}`)}function setEmailCookie(e,r,i){const t=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(r,`__unshared_email=${encodeURIComponent(i)}; HttpOnly; Path=/; SameSite=Lax${t}`)}function clearUserIdCookieIfPresent(e,r){(0,cookies_1.parseCookie)(e,"__unshared_uid")&&appendSetCookie(r,"__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0"+((0,secure_1.isSecureRequest)(e)?"; Secure":""))}function clearEmailCookieIfPresent(e,r){(0,cookies_1.parseCookie)(e,"__unshared_email")&&appendSetCookie(r,"__unshared_email=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0"+((0,secure_1.isSecureRequest)(e)?"; Secure":""))}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Generates
|
|
3
|
-
* 1. Loads the
|
|
4
|
-
* 2. Collects a
|
|
5
|
-
* 3.
|
|
2
|
+
* Generates an inline loader script that:
|
|
3
|
+
* 1. Loads the fingerprint SDK from /__unshared/fp.js
|
|
4
|
+
* 2. Collects a fingerprint and POSTs to /__unshared/submit-fp
|
|
5
|
+
* 3. Caches fingerprint in sessionStorage for reuse on SPA navigations
|
|
6
|
+
* 4. Patches History API to detect SPA route changes → re-submits fingerprint
|
|
7
|
+
* 5. Patches fetch/XHR to detect 403 account_flagged → dispatches "unshared:flagged" event
|
|
6
8
|
*
|
|
7
9
|
* The actual SDK UMD bundle is served by the middleware at /__unshared/fp.js.
|
|
8
|
-
*
|
|
10
|
+
*
|
|
11
|
+
* Event contract:
|
|
12
|
+
* window.addEventListener("unshared:flagged", (e) => {
|
|
13
|
+
* e.detail.email — the flagged user's email (from 403 response body)
|
|
14
|
+
* });
|
|
9
15
|
*/
|
|
10
16
|
export declare function generateFingerprintScript(routePrefix: string, version?: string): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";function generateFingerprintScript(e,n){const t=n?`?v=${escapeJavaScript(n)}`:"";return`<script>\n(function(){\ntry{\nvar pfx="${escapeJavaScript(e)}";\n\n//
|
|
1
|
+
"use strict";function generateFingerprintScript(e,n){const t=n?`?v=${escapeJavaScript(n)}`:"";return`<script>\n(function(){\ntry{\nvar pfx="${escapeJavaScript(e)}";\nvar SS_FP="__unshared_fp";\n\n// --- Helpers ---\nfunction gC(n){var m=document.cookie.match(new RegExp("(?:^|; )"+n+"=([^;]*)"));return m?decodeURIComponent(m[1]):null}\nfunction sC(n,v,d){var e="";if(d){var dt=new Date();dt.setTime(dt.getTime()+d*864e5);e="; expires="+dt.toUTCString()}document.cookie=n+"="+encodeURIComponent(v)+e+"; path=/; SameSite=Lax"}\nfunction uuid(){return(typeof crypto!=="undefined"&&crypto.randomUUID)?crypto.randomUUID():("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(c){var r=Math.random()*16|0;return(c==="x"?r:r&0x3|0x8).toString(16)}))}\n\n// --- Session + device IDs ---\nvar sid=gC("__unshared_sid");\nif(!sid){sid=uuid();sC("__unshared_sid",sid,365)}\nvar did="";\ntry{did=localStorage.getItem("__unshared_device_id")||"";if(!did){did=uuid();localStorage.setItem("__unshared_device_id",did)}}catch(e){did=did||uuid()}\nsC("__unshared_fp_id",did,365);\n\n// --- Fingerprint cache (sessionStorage) ---\nfunction getFP(){try{var r=sessionStorage.getItem(SS_FP);return r?JSON.parse(r):null}catch(e){return null}}\nfunction setFP(fp){try{sessionStorage.setItem(SS_FP,JSON.stringify(fp))}catch(e){}}\n\n// --- Submit fingerprint to backend ---\nfunction submitFP(fp,evType){\n var uid=gC("__unshared_uid");\n if(!uid)return;\n var body={hash:fp.full_hash,stable_hash:fp.fingerprint_id,collected_at:fp.timestamp,is_incognito:fp.isIncognito,components:fp.components,version:fp.version,session_id:sid,user_id:uid,event_type:evType||"page_load"};\n var xhr=new XMLHttpRequest();\n xhr.open("POST",pfx+"/submit-fp",true);\n xhr.setRequestHeader("Content-Type","application/json");\n xhr.setRequestHeader("X-Session-Id",sid);\n xhr.setRequestHeader("X-Device-Id",did);\n xhr.send(JSON.stringify(body));\n}\n\n// --- Collect fingerprint (loads fp.js if needed) then submit ---\nvar fpReady=false;\nfunction collectAndSubmit(evType){\n var uid=gC("__unshared_uid");\n if(!uid)return;\n var cached=getFP();\n if(cached){submitFP(cached,evType);return}\n if(!fpReady)return;\n try{\n var c=new UnsharedLabsBrowser.UnsharedLabsBrowser({baseUrl:""});\n c.collect({exclude:["timing","navigatorConnection"]}).then(function(fp){setFP(fp);submitFP(fp,evType)});\n }catch(e){}\n}\n\n// --- Load fp.js (always — browser caches it for 1h) ---\n// Submit cached FP immediately if available; load fp.js for fresh collection\nvar pageLoadSubmitted=false;\nif(getFP()&&gC("__unshared_uid")){submitFP(getFP(),"page_load");pageLoadSubmitted=true}\nvar s=document.createElement("script");\ns.src=pfx+"/fp.js${t}";\ns.onload=function(){fpReady=true;if(!pageLoadSubmitted)collectAndSubmit("page_load")};\ndocument.head.appendChild(s);\n\n// --- SPA route change tracking (History API + popstate) ---\nvar oPush=history.pushState,oReplace=history.replaceState;\nhistory.pushState=function(){oPush.apply(this,arguments);try{collectAndSubmit("route_change")}catch(e){}};\nhistory.replaceState=function(){oReplace.apply(this,arguments);try{collectAndSubmit("route_change")}catch(e){}};\nwindow.addEventListener("popstate",function(){try{collectAndSubmit("route_change")}catch(e){}});\n\n// --- 403 interception: dispatch "unshared:flagged" event ---\nfunction emitFlagged(body){\n try{window.dispatchEvent(new CustomEvent("unshared:flagged",{detail:{email:body.email||""}}))}catch(e){}\n}\n\n// Patch fetch\nvar oFetch=window.fetch;\nif(oFetch){window.fetch=function(){return oFetch.apply(this,arguments).then(function(r){if(r.status===403){try{var cl=r.clone();cl.json().then(function(b){if(b&&b.error==="account_flagged")emitFlagged(b)}).catch(function(){})}catch(e){}}return r})}}\n\n// Patch XMLHttpRequest\nvar oXSend=XMLHttpRequest.prototype.send;\nXMLHttpRequest.prototype.send=function(){var x=this;x.addEventListener("load",function(){if(x.status===403){try{var b=JSON.parse(x.responseText);if(b&&b.error==="account_flagged")emitFlagged(b)}catch(e){}}});return oXSend.apply(this,arguments)};\n\n}catch(e){}\n})();\n<\/script>`}function escapeJavaScript(e){return e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/'/g,"\\'")}Object.defineProperty(exports,"t",{value:!0}),exports.generateFingerprintScript=generateFingerprintScript;
|
|
@@ -2,12 +2,14 @@ import type { Response } from 'express';
|
|
|
2
2
|
/**
|
|
3
3
|
* Intercepts the response body by wrapping res.write() and res.end().
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* to pass through unchanged.
|
|
5
|
+
* Only buffers HTML responses (text/html). Non-HTML responses (JSON, images,
|
|
6
|
+
* CSS, JS, etc.) pass through to the original write/end without buffering,
|
|
7
|
+
* avoiding unnecessary memory usage on large payloads.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* When res.end() is called on an HTML response, invokes the `transform`
|
|
10
|
+
* callback with the complete body buffer and the Content-Type header.
|
|
11
|
+
* The transform can return modified content or null to pass through unchanged.
|
|
12
12
|
*/
|
|
13
|
-
export declare function interceptResponse(res: Response, transform: (body: Buffer, contentType: string | undefined) => Buffer | string | null
|
|
13
|
+
export declare function interceptResponse(res: Response, transform: (body: Buffer, contentType: string | undefined) => Buffer | string | null, options?: {
|
|
14
|
+
preventCaching?: boolean;
|
|
15
|
+
}): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";function interceptResponse(t,n){const e
|
|
1
|
+
"use strict";function interceptResponse(t,n,e){const f=e?.preventCaching??!1,o=t.write.bind(t),u=t.end.bind(t),r=t.writeHead.bind(t),c=[];let i=!1,l=null;function s(){if(null!==l)return;const n=t.getHeader("content-type");null!=n&&(l=String(n).includes("text/html"),l||function(){t.write=o,t.end=u;for(const t of c)o(t);c.length=0}())}t.writeHead=function(n,...e){return s(),f&&l&&(t.setHeader("Cache-Control","no-store"),t.removeHeader("ETag"),t.removeHeader("Last-Modified")),r(n,...e)},t.write=function(t,n,e){if(s(),!1===l)return o(t,n,e);if(null!=t){const e=Buffer.isBuffer(t)?t:Buffer.from(t,"string"==typeof n?n:"utf8");c.push(e)}return"function"==typeof n&&n(null),"function"==typeof e&&e(null),!0},t.end=function(e,f,r){if(i)return t;if(i=!0,s(),!1===l)return u(e,f,r);if(null!=e){const t=Buffer.isBuffer(e)?e:Buffer.from(e,"string"==typeof f?f:"utf8");c.push(t)}const p=Buffer.concat(c),y=t.getHeader("content-type");let B;try{B=n(p,y)}catch{B=null}if(null!=B){const n=Buffer.isBuffer(B)?B:Buffer.from(B,"utf8");t.setHeader("Content-Length",n.length),t.removeHeader("Content-Encoding"),o(n)}else p.length>0&&o(p);const g="function"==typeof f?f:r;return g?u(g):u(),t}}Object.defineProperty(exports,"t",{value:!0}),exports.interceptResponse=interceptResponse;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"
|
|
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");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 o,r,c;try{o=e.resolveUserId?e.resolveUserId(i):void 0}catch{}o=o??t.user_id??void 0;try{r=e.resolveEmailAddress?e.resolveEmailAddress(i):void 0}catch{}r=r??(0,cookies_1.parseCookie)(i,"__unshared_email")??t.email??void 0;try{c=e.resolveSessionId?e.resolveSessionId(i):void 0}catch{}c=c??t.session_id??(0,cookies_1.parseCookie)(i,"__unshared_sid");const _=(0,client_ip_1.extractClientIp)(i),d=i.headers["user-agent"]??"";if((0,is_bot_1.isBot)(d))return void s.status(200).json({success:!0});const u=(0,device_id_1.extractDeviceId)(i,e.resolveDeviceId),a=n.fingerprint_id||void 0,l=(0,secure_1.isSecureRequest)(i)?"; Secure":"",p=[];if(a&&!(0,cookies_1.parseCookie)(i,"__unshared_fingerprint_id")&&p.push(`__unshared_fingerprint_id=${encodeURIComponent(a)}; HttpOnly; Path=/; SameSite=Lax${l}`),r&&!(0,cookies_1.parseCookie)(i,"__unshared_email")&&p.push(`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${l}`),p.length>0){const e=s.getHeader("Set-Cookie");if(e){const i=Array.isArray(e)?[...e]:[String(e)];i.push(...p),s.setHeader("Set-Cookie",i)}else s.setHeader("Set-Cookie",p)}o&&e.client.submitFingerprintEvent(n,{userId:o,emailAddress:r,sessionHash:c,eventType:"auto_collect",ipAddress:_,userAgent:d}).catch(()=>{}),o&&r&&!e.rateLimitBackoff.isPaused()&&e.client.processUserEvent({eventType:"auto_collect",userId:o,emailAddress:r,ipAddress:_,deviceId:u,fingerprintId:a,sessionHash:c??"unknown",userAgent:d}).then(i=>{i.success&&i.data?.analysis&&e.verdictCache.update(o,{isFlagged:i.data.analysis.is_user_flagged}),!i.success&&i.error?.retryAfter&&e.rateLimitBackoff.pause(1e3*i.error.retryAfter)}).catch(()=>{}),s.status(200).json({success:!0})}catch{s.status(200).json({success:!0})}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";function handleVerifyTrigger(e){return async(r
|
|
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");function handleVerifyTrigger(e){return async(i,r)=>{try{const s=resolveEmail(i,i.body??{},e.resolveEmailAddress);if(!s)return void r.status(400).json({success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}});const c=(0,device_id_1.extractDeviceId)(i,e.resolveDeviceId),o=(0,cookies_1.parseCookie)(i,"__unshared_fingerprint_id")||void 0,t=await e.client.triggerEmailVerification(s,c,{fingerprintId:o});t.success?r.status(200).json({success:!0,data:t.data}):r.status(200).json({success:!1,error:t.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}})}catch{r.status(200).json({success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}})}}}function handleVerify(e){return async(i,r)=>{try{const s=i.body??{},c=resolveEmail(i,s,e.resolveEmailAddress),o=s.code;if(!c||!o)return void r.status(400).json({success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}});const t=(0,device_id_1.extractDeviceId)(i,e.resolveDeviceId),n=(0,cookies_1.parseCookie)(i,"__unshared_fingerprint_id")||void 0,d=await e.client.verify(c,t,o,{fingerprintId:n});if(d.success){const s=(0,cookies_1.parseCookie)(i,"__unshared_uid");s&&e.verdictCache.update(s,{isVerified:!0}),r.status(200).json({success:!0,data:{verified:!0}})}else r.status(200).json({success:!1,error:d.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}})}catch{r.status(200).json({success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}})}}}function resolveEmail(e,i,r){if(r)try{const i=r(e);if(i)return i}catch{}const s=(0,cookies_1.parseCookie)(e,"__unshared_email");if(s)return s;const c=i.email;return"string"==typeof c&&c?c:void 0}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";function parseCookie(e,o){const t=e.headers.cookie;if(!t)return;const n=t.match(new RegExp(`(?:^|; )${o}=([^;]*)`));return n?decodeURIComponent(n[1]):void 0}Object.defineProperty(exports,"o",{value:!0}),exports.parseCookie=parseCookie;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"t",{value:!0}),exports.extractDeviceId=extractDeviceId;const cookies_1=require("./cookies");function extractDeviceId(e,t){if(t)try{const c=t(e);if(c)return c}catch{}const c=(0,cookies_1.parseCookie)(e,"__unshared_fp_id");if(c)return c;const o=e.headers["x-device-id"];return"string"==typeof o&&o?o:"unknown"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";function isSecureRequest(e){return e.secure||"https"===e.headers["x-forwarded-proto"]}Object.defineProperty(exports,"t",{value:!0}),exports.isSecureRequest=isSecureRequest;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"t",{value:!0}),exports.shouldSkipPath=shouldSkipPath;const STATIC_EXTENSIONS=new Set([".js",".mjs",".cjs",".css",".map",".png",".jpg",".jpeg",".gif",".svg",".ico",".webp",".avif",".woff",".woff2",".ttf",".otf",".eot",".mp3",".mp4",".webm",".ogg",".wasm",".
|
|
1
|
+
"use strict";Object.defineProperty(exports,"t",{value:!0}),exports.shouldSkipPath=shouldSkipPath;const STATIC_EXTENSIONS=new Set([".js",".mjs",".cjs",".css",".map",".png",".jpg",".jpeg",".gif",".svg",".ico",".webp",".avif",".woff",".woff2",".ttf",".otf",".eot",".mp3",".mp4",".webm",".ogg",".wasm",".xml",".txt",".pdf"]),STATIC_PATH_PREFIXES=["/static/","/assets/","/public/","/_next/","/__vite/","/favicon"];function shouldSkipPath(t,s){if(s)for(const o of s)if(t.startsWith(o))return!0;const o=t.lastIndexOf(".");if(-1!==o){const s=t.slice(o).toLowerCase().split("?")[0];if(STATIC_EXTENSIONS.has(s))return!0}for(const s of STATIC_PATH_PREFIXES)if(t.startsWith(s))return!0;return!1}
|
|
@@ -10,7 +10,9 @@ export declare class VerdictCache {
|
|
|
10
10
|
private readonly _entries;
|
|
11
11
|
private readonly _activeRefreshes;
|
|
12
12
|
private readonly _defaultTtlMs;
|
|
13
|
-
|
|
13
|
+
private readonly _maxSize;
|
|
14
|
+
private _sweepTimer;
|
|
15
|
+
constructor(defaultTTL?: number, maxSize?: number);
|
|
14
16
|
get(userId: string): Verdict | undefined;
|
|
15
17
|
set(userId: string, verdict: Omit<Verdict, 'cachedAt' | 'ttl'>, ttl?: number): void;
|
|
16
18
|
/**
|
|
@@ -33,4 +35,13 @@ export declare class VerdictCache {
|
|
|
33
35
|
/** Number of cached entries. */
|
|
34
36
|
get size(): number;
|
|
35
37
|
clear(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Stops the periodic sweep timer. Call this when shutting down
|
|
40
|
+
* or when the middleware is no longer needed (e.g., in tests).
|
|
41
|
+
*/
|
|
42
|
+
destroy(): void;
|
|
43
|
+
/** Remove all entries that are past their TTL + a 2x grace period. */
|
|
44
|
+
private _sweep;
|
|
45
|
+
/** Evict the oldest entry by cachedAt to make room. */
|
|
46
|
+
private _evictOldest;
|
|
36
47
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"t",{value:!0}),exports.VerdictCache=void 0;class VerdictCache{constructor(t=6e4){this.i=new Map,this.h=new Set,this.o=t}get(t){return this.i.get(t)}set(t,e
|
|
1
|
+
"use strict";Object.defineProperty(exports,"t",{value:!0}),exports.VerdictCache=void 0;const DEFAULT_TTL_MS=6e4,DEFAULT_MAX_SIZE=1e4,SWEEP_INTERVAL_MS=3e5;class VerdictCache{constructor(t=6e4,s=1e4){this.i=new Map,this.h=new Set,this.o=null,this.l=t,this.u=s,this.o=setInterval(()=>this._(),3e5),this.o&&"function"==typeof this.o.unref&&this.o.unref()}get(t){return this.i.get(t)}set(t,s,e){!this.i.has(t)&&this.i.size>=this.u&&this.p(),this.i.set(t,{...s,cachedAt:Date.now(),ttl:e??this.l})}update(t,s){const e=this.i.get(t);e?(void 0!==s.isFlagged&&(e.isFlagged=s.isFlagged),void 0!==s.isVerified&&(e.isVerified=s.isVerified),e.cachedAt=Date.now()):(this.i.size>=this.u&&this.p(),this.i.set(t,{isFlagged:s.isFlagged??!1,isVerified:s.isVerified??!1,emailAddress:"",sessionId:"",cachedAt:Date.now(),ttl:this.l}))}delete(t){this.i.delete(t),this.h.delete(t)}isStale(t){const s=this.i.get(t);return!!s&&Date.now()-s.cachedAt>s.ttl}isRefreshing(t){return this.h.has(t)}markRefreshing(t){this.h.add(t)}clearRefreshing(t){this.h.delete(t)}get size(){return this.i.size}clear(){this.i.clear(),this.h.clear()}destroy(){this.o&&(clearInterval(this.o),this.o=null),this.clear()}_(){const t=Date.now();for(const[s,e]of this.i)t-e.cachedAt>2*e.ttl&&(this.i.delete(s),this.h.delete(s))}p(){let t=null,s=1/0;for(const[e,i]of this.i)i.cachedAt<s&&(s=i.cachedAt,t=e);t&&(this.i.delete(t),this.h.delete(t))}}exports.VerdictCache=VerdictCache;
|