unshared-clientjs-sdk 2.0.0-rc.15 → 2.0.0-rc.21
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.
|
@@ -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";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:
|
|
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:u}=r,l=new VerdictCache(s),f=new RateLimitBackoff,p=Date.now().toString(36),m=generateFingerprintScript(n,p);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:l,rateLimitBackoff:f,resolveUserId:t,resolveEmailAddress:i,resolveSessionId:d,resolveDeviceId:a}),C=handleVerifyTrigger({client:e,verdictCache:l,resolveEmailAddress:i,resolveDeviceId:a}),I=handleVerify({client:e,verdictCache:l,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`,A=`${n}/status`;return function(r,o,s){const p=r.path;if(p.startsWith(n+"/")){if(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)return void o.status(204).end();if("GET"===r.method&&p===g)return o.setHeader("Content-Type","application/javascript"),o.setHeader("Cache-Control","public, max-age=3600"),void o.status(200).end(h);if("POST"===r.method&&p===k)return void v(r,o);if("POST"===r.method&&p===y)return void C(r,o);if("POST"===r.method&&p===_)return void I(r,o);if("GET"===r.method&&p===A){let e;try{e=t(r)}catch{}if(!e)return void o.status(200).json({status:"anonymous"});const n=resolveEmail(r,i),s=l.get(e);return void(s&&s.isFlagged&&!s.isVerified&&u&&n?o.status(403).json({error:"account_flagged",email:n}):o.status(200).json({status:"ok"}))}return void o.status(404).json({success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if(shouldSkipPath(p,c))return void s();let T;try{T=t(r)}catch{}if(!T)return clearUserIdCookieIfPresent(r,o),clearEmailCookieIfPresent(r,o),interceptForInjection(r,o,m),void s();const x=resolveEmail(r,i);if(setUserIdCookie(r,o,T),x&&setEmailCookie(r,o,x),!x)return interceptForInjection(r,o,m),void s();const P=extractSessionId(r,d),E=extractDeviceId(r,a),F=extractFingerprintId(r),w=r.headers["user-agent"]??"",U=extractClientIp(r);if(isBot(w))return void s();if("unknown"===P)return interceptForInjection(r,o,m),void s();f.isPaused()||dispatchUserEvent(e,l,f,{userId:T,emailAddress:x,sessionId:P,deviceId:E,fingerprintId:F,userAgent:w,ipAddress:U,eventType:`${r.method} ${r.path}`});const O=l.get(T);O?(l.isStale(T)&&!l.isRefreshing(T)&&(l.markRefreshing(T),fetchAndCacheVerdict(e,l,T,x,E,F,P).finally(()=>l.clearRefreshing(T))),applyVerdict(O,T,x,r,o,s,m,u)):fetchAndCacheVerdict(e,l,T,x,E,F,P).then(e=>{applyVerdict(e,T,x,r,o,s,m,u)}).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 u=a.data?.is_user_flagged??!1;return r.set(t,{isFlagged:u,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 +1 @@
|
|
|
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,"\\'")}
|
|
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","speech"]}).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;deferredCheck()}\nvar s=document.createElement("script");\ns.src=pfx+"/fp.js${t}";\ns.onload=function(){fpReady=true;if(!pageLoadSubmitted){collectAndSubmit("page_load");deferredCheck()}};\ndocument.head.appendChild(s);\n\n// --- Deferred verdict check ---\n// After fingerprint submission, the backend processes the event async.\n// If the user was just flagged, the initial page load may have beaten\n// the verdict update. Re-check after a delay so newly flagged sessions\n// get caught without waiting for user interaction.\n// Uses the patched fetch so the 403 interceptor picks up the response.\nfunction deferredCheck(){\n var uid=gC("__unshared_uid");\n if(!uid)return;\n setTimeout(function(){\n try{fetch(pfx+"/status",{method:"GET",credentials:"same-origin"}).catch(function(){})}catch(e){}\n },5000);\n}\n\n// --- SPA route change tracking (History API + popstate) ---\nvar oPush=history.pushState,oReplace=history.replaceState;\nhistory.pushState=function(){oPush.apply(this,arguments);try{collectAndSubmit("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,"\\'")}
|
package/dist/middleware/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
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`,
|
|
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`,y=`${n}/verify-trigger`,S=`${n}/verify`,A=`${n}/status`;return function(r,s,o){const f=r.path;if(f.startsWith(n+"/")){if(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)return void s.status(204).end();if("GET"===r.method&&f===C)return s.setHeader("Content-Type","application/javascript"),s.setHeader("Cache-Control","public, max-age=3600"),void s.status(200).end(v);if("POST"===r.method&&f===g)return void h(r,s);if("POST"===r.method&&f===y)return void m(r,s);if("POST"===r.method&&f===S)return void I(r,s);if("GET"===r.method&&f===A){let e;try{e=i(r)}catch{}if(!e)return void s.status(200).json({status:"anonymous"});const n=resolveEmail(r,t),o=l.get(e);return void(o&&o.isFlagged&&!o.isVerified&&u&&n?s.status(403).json({error:"account_flagged",email:n}):s.status(200).json({status:"ok"}))}return 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 T;try{T=i(r)}catch{}if(!T)return clearUserIdCookieIfPresent(r,s),clearEmailCookieIfPresent(r,s),interceptForInjection(r,s,p),void o();const E=resolveEmail(r,t);if(setUserIdCookie(r,s,T),E&&setEmailCookie(r,s,E),!E)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"]??"",O=(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:T,emailAddress:E,sessionId:q,deviceId:x,fingerprintId:P,userAgent:b,ipAddress:O,eventType:`${r.method} ${r.path}`});const U=l.get(T);U?(l.isStale(T)&&!l.isRefreshing(T)&&(l.markRefreshing(T),fetchAndCacheVerdict(e,l,T,E,x,P,q).finally(()=>l.clearRefreshing(T))),applyVerdict(U,T,E,r,s,o,p,u)):fetchAndCacheVerdict(e,l,T,E,x,P,q).then(e=>{applyVerdict(e,T,E,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 +1 @@
|
|
|
1
|
-
"use strict";function generateFingerprintScript(e,
|
|
1
|
+
"use strict";function generateFingerprintScript(e,t){const n=t?`?v=${escapeJavaScript(t)}`:"";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","speech"]}).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;deferredCheck()}\nvar s=document.createElement("script");\ns.src=pfx+"/fp.js${n}";\ns.onload=function(){fpReady=true;if(!pageLoadSubmitted){collectAndSubmit("page_load");deferredCheck()}};\ndocument.head.appendChild(s);\n\n// --- Deferred verdict check ---\n// After fingerprint submission, the backend processes the event async.\n// If the user was just flagged, the initial page load may have beaten\n// the verdict update. Re-check after a delay so newly flagged sessions\n// get caught without waiting for user interaction.\n// Uses the patched fetch so the 403 interceptor picks up the response.\nfunction deferredCheck(){\n var uid=gC("__unshared_uid");\n if(!uid)return;\n setTimeout(function(){\n try{fetch(pfx+"/status",{method:"GET",credentials:"same-origin"}).catch(function(){})}catch(e){}\n },5000);\n}\n\n// --- SPA route change tracking (History API + popstate) ---\nvar oPush=history.pushState,oReplace=history.replaceState;\nhistory.pushState=function(){oPush.apply(this,arguments);try{collectAndSubmit("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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unshared-clientjs-sdk",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.21",
|
|
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",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"unshared-frontend-sdk": "
|
|
58
|
+
"unshared-frontend-sdk": "2.0.0-rc.21"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/express": "^4.17.21",
|