unshared-clientjs-sdk 2.0.0 → 2.0.1

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/README.md CHANGED
@@ -112,35 +112,73 @@ await client.submitFingerprintEvent(fingerprint, {
112
112
 
113
113
  ---
114
114
 
115
- ## Express Middleware
115
+ ## Protection Middleware (Recommended)
116
116
 
117
- The middleware adds a proxy route (`POST /unshared/submit-fingerprint-event`) that the browser SDK calls. It handles forwarding fingerprints to Unshared and attaching your API key.
117
+ `unsharedBoundToUser` is the full-featured middleware: auto-injects the fingerprint script, enforces verdicts, handles email verification flows, and dispatches events.
118
118
 
119
119
  ```typescript
120
- import { createUnsharedMiddleware } from 'unshared-clientjs-sdk/middleware';
120
+ import { UnsharedClient, unsharedBoundToUser, flaggedResponse } from 'unshared-clientjs-sdk';
121
121
 
122
- // express.json() must come before this middleware
122
+ app.set('trust proxy', 1);
123
123
  app.use(express.json());
124
124
 
125
- app.use(createUnsharedMiddleware(client, {
126
- userIdExtractor: (req) => req.user?.id, // attach logged-in user
125
+ const client = new UnsharedClient({ apiKey: process.env.UNSHARED_API_KEY! });
126
+
127
+ app.use(unsharedBoundToUser(client, {
128
+ userId: (req) => req.cookies?.userId,
129
+ emailAddress: (req) => req.cookies?.email,
130
+ includePathPrefix: ['/api/'],
131
+ onFlagged: ({ emailAddress, res }) => {
132
+ res.status(403).json(flaggedResponse(emailAddress));
133
+ },
127
134
  }));
128
135
  ```
129
136
 
130
- **Prerequisites:**
131
- - Mount `express.json()` before the middleware
132
- - `user_id` is automatically removed from `req.body` after being read — downstream logging won't capture it
137
+ **Smoke test:** `curl http://localhost:3000/__unshared/status` — returns `{ "status": "anonymous" | "ok" | "flagged" }`.
133
138
 
134
- **Options:**
139
+ **Key options:**
140
+
141
+ | Option | Type | Default | Description |
142
+ |--------|------|---------|-------------|
143
+ | `userId` | `(req) => string \| undefined` | — | **Required.** Resolve the current user's ID |
144
+ | `emailAddress` | `(req) => string \| undefined` | — | Resolve the current user's email |
145
+ | `routePrefix` | `string` | `"/__unshared"` | Route mount prefix |
146
+ | `includePathPrefix` | `string[]` | — | Only these path prefixes trigger verdicts and events |
147
+ | `onFlagged` | `(ctx) => void` | — | Called when a flagged user makes a request |
148
+ | `disableBotFilter` | `boolean` | `false` | Skip bot UA filter (enable for E2E testing) |
149
+ | `checkUserTimeoutMs` | `number` | `500` | Timeout (ms) for checkUser API calls; fails open on timeout |
150
+ | `skipPaths` | `string[]` | — | Paths to skip entirely (static assets, health checks) |
151
+ | `corsOrigins` | `string \| string[]` | — | Allowed CORS origins; handles OPTIONS preflight |
152
+ | `onError` | `(error, ctx) => void` | — | Called on background SDK errors for observability |
153
+
154
+ See [quickstart](./docs/quickstart.md) and [flag semantics](./docs/flag-semantics.md) for full setup and testing details.
155
+
156
+ ---
157
+
158
+ ## Simple Fingerprint Middleware
159
+
160
+ `createUnsharedMiddleware` is a lightweight alternative that only proxies fingerprint events — no verdicts, no script injection, no verification flows. Use `unsharedBoundToUser` unless you have a specific reason not to.
161
+
162
+ ```typescript
163
+ import { createUnsharedMiddleware } from 'unshared-clientjs-sdk';
164
+
165
+ app.use(express.json());
166
+ app.use(createUnsharedMiddleware(client, {
167
+ userIdExtractor: (req) => req.user?.id,
168
+ }));
169
+ ```
135
170
 
136
171
  | Option | Type | Default | Description |
137
172
  |--------|------|---------|-------------|
138
173
  | `userIdExtractor` | `(req) => string \| undefined` | — | Pull user ID from your auth session |
139
174
  | `eventTypeExtractor` | `(req) => string \| undefined` | — | Override event type |
140
175
  | `sessionIdExtractor` | `(req) => string \| undefined` | — | Override session ID |
176
+ | `ipAddressExtractor` | `(req) => string \| undefined` | — | Override IP address |
141
177
  | `defaultEventType` | `string` | `"browser_event"` | Fallback event type |
142
178
  | `routePrefix` | `string` | `"/unshared"` | Route mount prefix |
143
- | `corsOrigins` | `string \| string[]` | — | Allowed CORS origins; handles OPTIONS preflight automatically |
179
+ | `corsOrigins` | `string \| string[]` | — | Allowed CORS origins |
180
+
181
+ > **Note:** This middleware uses a different default prefix (`/unshared`) and route (`submit-fingerprint-event`) than `unsharedBoundToUser` (`/__unshared`, `submit-fp`).
144
182
 
145
183
  ---
146
184
 
@@ -1,7 +1,7 @@
1
1
  export { UnsharedClient } from './client';
2
2
  export { createUnsharedMiddleware, assertTrustProxy } from './middleware';
3
3
  export type { MiddlewareOptions } from './middleware';
4
- export { unsharedBoundToUser, VerdictCache, } from './middleware/index';
4
+ export { unsharedBoundToUser, VerdictCache, flaggedResponse, ACCOUNT_FLAGGED_ERROR, } from './middleware/index';
5
5
  export type { ProtectionConfig, Verdict } from './middleware/index';
6
6
  export type { UnsharedClientConfig, ApiResult, UnsharedError, SubmitFingerprintOptions, SubmitFingerprintResult, ProcessUserEventParams, ProcessUserEventResult, CheckUserResult, TriggerEmailVerificationResult, VerifyResult, } from './client';
7
7
  export type { UnsharedRequest, UnsharedResponse, UnsharedNextFunction } from './types';
@@ -1 +1 @@
1
- export{UnsharedClient}from"./client";export{createUnsharedMiddleware,assertTrustProxy}from"./middleware";export{unsharedBoundToUser,VerdictCache}from"./middleware/index";export{sendJson}from"./middleware/utils/http-helpers";
1
+ export{UnsharedClient}from"./client";export{createUnsharedMiddleware,assertTrustProxy}from"./middleware";export{unsharedBoundToUser,VerdictCache,flaggedResponse,ACCOUNT_FLAGGED_ERROR}from"./middleware/index";export{sendJson}from"./middleware/utils/http-helpers";
@@ -2,6 +2,7 @@ import type { UnsharedRequest, UnsharedResponse, UnsharedNextFunction } from '..
2
2
  import type { UnsharedClient } from '../client';
3
3
  import { VerdictCache } from './verdict-cache';
4
4
  import type { Verdict } from './verdict-cache';
5
+ export { flaggedResponse, ACCOUNT_FLAGGED_ERROR } from './utils/flagged-response';
5
6
  export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest> {
6
7
  /**
7
8
  * Required. Resolves the current user's ID from the request.
@@ -24,6 +25,10 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
24
25
  skipPaths?: string[];
25
26
  /** When set, only paths matching one of these prefixes get events dispatched and checkUser called. */
26
27
  includePathPrefix?: string[];
28
+ /** Skip the bot/crawler UA filter. Set to true in test environments so automated browsers (Playwright, Puppeteer, etc.) can observe verdicts. @default false */
29
+ disableBotFilter?: boolean;
30
+ /** Hard timeout (ms) for checkUser on cache miss. Fails open on timeout. @default 500 */
31
+ checkUserTimeoutMs?: number;
27
32
  /** Resolves a custom session ID. Falls back to __unshared_sid cookie. */
28
33
  sessionId?: (req: TReq) => string | undefined;
29
34
  /**
@@ -1 +1 @@
1
- import{readFileSync}from"fs";import{VerdictCache}from"./verdict-cache";import{RateLimitBackoff}from"./rate-limit-backoff";import{DispatchDedupe}from"./dispatch-dedupe";import{interceptResponse}from"./response-interceptor";import{generateFingerprintScript}from"./injection/fingerprint-script";import{handleSubmitFingerprint}from"./routes/submit-fp";import{handleVerifyTrigger,handleVerify}from"./routes/verify";import{sendJson,sendEmpty,sendBody,getRequestPath}from"./utils/http-helpers";import{isHtmlContentType}from"./utils/content-type";import{shouldSkipPath}from"./utils/skip-paths";import{shouldIncludePath}from"./utils/include-path";import{isBot}from"./utils/is-bot";import{extractClientIp}from"./utils/client-ip";import{parseCookie}from"./utils/cookies";import{extractDeviceIdOrUndefined}from"./utils/device-id";import{isSecureRequest}from"./utils/secure";import{isSentinelUserId,SENTINEL_STICKINESS_TTL_MS}from"./utils/sentinel-user-id";export{VerdictCache};const CHECK_USER_TIMEOUT_MS=500;export function unsharedBoundToUser(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");if(!t.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:r,emailAddress:i,routePrefix:n="/__unshared",corsOrigins:s,cacheTTL:o=6e4,skipPaths:d,includePathPrefix:a,sessionId:c,deviceId:u,onFlagged:l,onError:p}=t,f=new VerdictCache(o),m=new RateLimitBackoff,h=new DispatchDedupe,S=Date.now().toString(36),I=generateFingerprintScript(n,S);let v="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");v=readFileSync(e,"utf8")}catch{}const _=handleSubmitFingerprint({client:e,verdictCache:f,rateLimitBackoff:m,dispatchDedupe:h,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:c,resolveDeviceId:u,onError:p}),C=handleVerifyTrigger({client:e,verdictCache:f,resolveEmailAddress:i,resolveDeviceId:u,onError:p}),g=handleVerify({client:e,verdictCache:f,resolveEmailAddress:i,resolveDeviceId:u,onError:p}),y=s?Array.isArray(s)?s:[s]:null,k=`${n}/fp.js`,A=`${n}/submit-fp`,T=`${n}/verify-trigger`,E=`${n}/verify`,x=`${n}/status`;return function(t,s,o){const S=getRequestPath(t.url),P=t.url||S;if(S.startsWith(n+"/")){if(function(e,t){if(!y)return;const r=e.headers.origin??"",i=y.includes("*");(i||y.includes(r))&&(t.setHeader("Access-Control-Allow-Origin",i?"*":r),t.setHeader("Access-Control-Allow-Methods","POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,s),"OPTIONS"===t.method)return void sendEmpty(s,204);if("GET"===t.method&&S===k)return s.setHeader("Content-Type","application/javascript"),s.setHeader("Cache-Control","public, max-age=3600"),void sendBody(s,200,v);if("POST"===t.method&&(S===A||S===T||S===E))return void 0===t.body?void sendJson(s,400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"req.body is undefined. Mount a JSON body-parsing middleware (e.g., express.json()) before the Unshared middleware."}}):S===A?void _(t,s):S===T?void C(t,s):void g(t,s);if("GET"===t.method&&S===x){let n;try{n=r(t)}catch{}if(!n)return void sendJson(s,200,{status:"anonymous"});const o=resolveEmail(t,i);return void(async()=>{let r=f.get(n);if((!r||f.isStale(n))&&o&&!m.isPaused()&&!f.isRefreshing(n)){f.markRefreshing(n);try{const i=extractDeviceIdOrUndefined(t,u),s=extractFingerprintId(t),d=extractSessionId(t,c),a=i??s??"unknown";await fetchAndCacheVerdict(e,f,n,o,a,s,d),r=f.get(n)}catch(e){p&&p(e,{operation:"checkUser",userId:n,emailAddress:o})}finally{f.clearRefreshing(n)}}r&&r.isFlagged&&!r.isVerified&&l&&o?sendJson(s,200,{status:"flagged",email:o}):sendJson(s,200,{status:"ok"})})()}return void sendJson(s,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if(shouldSkipPath(S,d))return void o();if(!shouldIncludePath(S,a))return interceptForInjection(t,s,I),void o();let w;try{w=r(t)}catch{}if(isSentinelUserId(w)){const e=parseCookie(t,"__unshared_uid"),r=parseCookie(t,"__unshared_uid_at"),i=r?Number(r):NaN,n=Number.isFinite(i)&&Date.now()-i<=SENTINEL_STICKINESS_TTL_MS;w=e&&n?e:void 0}if(!w){const e=isSecureRequest(t)?"; Secure":"";return appendSetCookie(s,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(t,s,I),void o()}const U=resolveEmail(t,i);if(setUserIdCookie(t,s,w),U&&setEmailCookie(t,s,U),!U)return interceptForInjection(t,s,I),void o();const F=extractSessionId(t,c),D=extractDeviceIdOrUndefined(t,u),N=extractFingerprintId(t),O=t.headers["user-agent"]??"",V=extractClientIp(t),b=D??N;if(isBot(O))return void o();const L=f.get(w);function R(){"unknown"!==F&&b&&(m.isPaused()||dispatchUserEvent(e,f,m,h,{userId:w,emailAddress:U,sessionId:F,deviceId:b,fingerprintId:N,userAgent:O,ipAddress:V,eventType:P},p))}L?(f.isStale(w)&&!f.isRefreshing(w)&&(f.markRefreshing(w),fetchAndCacheVerdict(e,f,w,U,b??"unknown",N,F).finally(()=>f.clearRefreshing(w))),L.isFlagged||R(),applyVerdict(L,w,U,t,s,o,I,l)):fetchAndCacheVerdict(e,f,w,U,b??"unknown",N,F).then(e=>{e.isFlagged||R(),applyVerdict(e,w,U,t,s,o,I,l)}).catch(()=>{R(),interceptForInjection(t,s,I),o()})}}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=parseCookie(e,"__unshared_email");if(r)return r;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,t,r,i,n,s,o,d){if(interceptForInjection(i,n,o),e.isFlagged&&!e.isVerified&&d)try{d({userId:t,emailAddress:r,verdict:e,req:i,res:n,next:s})}catch{s()}else s()}function interceptForInjection(e,t,r){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],interceptResponse(t,(e,t)=>{if(!isHtmlContentType(t))return null;const i=e.toString("utf8"),n=i.lastIndexOf("</body>");return-1===n?i+r:i.slice(0,n)+r+i.slice(n)},{preventCaching:!0})}function dispatchUserEvent(e,t,r,i,n,s){i.mark(n.userId,n.eventType),e.processUserEvent({eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,sessionHash:n.sessionId,userAgent:n.userAgent}).then(e=>{e.success&&e.data?.analysis&&t.update(n.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&r.pause(1e3*e.error.retryAfter)}).catch(e=>{s&&s(e,{operation:"processUserEvent",userId:n.userId,emailAddress:n.emailAddress})})}async function fetchAndCacheVerdict(e,t,r,i,n,s,o){const d={};let a;n&&"unknown"!==n&&(d.deviceId=n),s&&(d.fingerprintId=s);const c=await Promise.race([e.checkUser(i,d),new Promise(e=>{a=setTimeout(()=>e(null),500)})]);if(clearTimeout(a),!c)return{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:o,cachedAt:0,ttl:0};const u=c.data?.is_user_flagged??!1;return t.set(r,{isFlagged:u,isVerified:!1,emailAddress:i,sessionId:o}),t.get(r)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return parseCookie(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return parseCookie(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
1
+ import{readFileSync}from"fs";import{VerdictCache}from"./verdict-cache";import{RateLimitBackoff}from"./rate-limit-backoff";import{DispatchDedupe}from"./dispatch-dedupe";import{interceptResponse}from"./response-interceptor";import{generateFingerprintScript}from"./injection/fingerprint-script";import{handleSubmitFingerprint}from"./routes/submit-fp";import{handleVerifyTrigger,handleVerify}from"./routes/verify";import{sendJson,sendEmpty,sendBody,getRequestPath}from"./utils/http-helpers";import{isHtmlContentType}from"./utils/content-type";import{shouldSkipPath}from"./utils/skip-paths";import{shouldIncludePath}from"./utils/include-path";import{isBot}from"./utils/is-bot";import{extractClientIp}from"./utils/client-ip";import{parseCookie}from"./utils/cookies";import{extractDeviceIdOrUndefined}from"./utils/device-id";import{isSecureRequest}from"./utils/secure";export{flaggedResponse,ACCOUNT_FLAGGED_ERROR}from"./utils/flagged-response";import{isSentinelUserId,SENTINEL_STICKINESS_TTL_MS}from"./utils/sentinel-user-id";export{VerdictCache};const CHECK_USER_TIMEOUT_MS=500;export function unsharedBoundToUser(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");if(!t.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:r,emailAddress:i,routePrefix:n="/__unshared",corsOrigins:s,cacheTTL:o=6e4,skipPaths:d,includePathPrefix:a,disableBotFilter:c=!1,checkUserTimeoutMs:u=CHECK_USER_TIMEOUT_MS,sessionId:l,deviceId:p,onFlagged:f,onError:m}=t,h=new VerdictCache(o),S=new RateLimitBackoff,I=new DispatchDedupe,_=Date.now().toString(36),v=generateFingerprintScript(n,_);let C="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");C=readFileSync(e,"utf8")}catch{}const g=handleSubmitFingerprint({client:e,verdictCache:h,rateLimitBackoff:S,dispatchDedupe:I,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:l,resolveDeviceId:p,onError:m}),y=handleVerifyTrigger({client:e,verdictCache:h,resolveEmailAddress:i,resolveDeviceId:p,onError:m}),k=handleVerify({client:e,verdictCache:h,resolveEmailAddress:i,resolveDeviceId:p,onError:m}),A=s?Array.isArray(s)?s:[s]:null,E=`${n}/fp.js`,T=`${n}/submit-fp`,x=`${n}/verify-trigger`,U=`${n}/verify`,P=`${n}/status`;return function(t,s,o){const _=getRequestPath(t.url),w=t.url||_;if(_.startsWith(n+"/")){if(function(e,t){if(!A)return;const r=e.headers.origin??"",i=A.includes("*");(i||A.includes(r))&&(t.setHeader("Access-Control-Allow-Origin",i?"*":r),t.setHeader("Access-Control-Allow-Methods","POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,s),"OPTIONS"===t.method)return void sendEmpty(s,204);if("GET"===t.method&&_===E)return s.setHeader("Content-Type","application/javascript"),s.setHeader("Cache-Control","public, max-age=3600"),void sendBody(s,200,C);if("POST"===t.method&&(_===T||_===x||_===U))return void 0===t.body?void sendJson(s,400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"req.body is undefined. Mount a JSON body-parsing middleware (e.g., express.json()) before the Unshared middleware."}}):_===T?void g(t,s):_===x?void y(t,s):void k(t,s);if("GET"===t.method&&_===P){let n;try{n=r(t)}catch{}if(!n)return void sendJson(s,200,{status:"anonymous"});const o=resolveEmail(t,i);return void(async()=>{let r=h.get(n);if((!r||h.isStale(n))&&o&&!S.isPaused()&&!h.isRefreshing(n)){h.markRefreshing(n);try{const i=extractDeviceIdOrUndefined(t,p),s=extractFingerprintId(t),d=extractSessionId(t,l),a=i??s??"unknown";await fetchAndCacheVerdict(e,h,n,o,a,s,d,u),r=h.get(n)}catch(e){m&&m(e,{operation:"checkUser",userId:n,emailAddress:o})}finally{h.clearRefreshing(n)}}r&&r.isFlagged&&!r.isVerified&&f&&o?sendJson(s,200,{status:"flagged",email:o}):sendJson(s,200,{status:"ok"})})()}return void sendJson(s,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if(shouldSkipPath(_,d))return void o();if(!shouldIncludePath(_,a))return interceptForInjection(t,s,v),void o();let F;try{F=r(t)}catch{}if(isSentinelUserId(F)){const e=parseCookie(t,"__unshared_uid"),r=parseCookie(t,"__unshared_uid_at"),i=r?Number(r):NaN,n=Number.isFinite(i)&&Date.now()-i<=SENTINEL_STICKINESS_TTL_MS;F=e&&n?e:void 0}if(!F){const e=isSecureRequest(t)?"; Secure":"";return appendSetCookie(s,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(t,s,v),void o()}const O=resolveEmail(t,i);if(setUserIdCookie(t,s,F),O&&setEmailCookie(t,s,O),!O)return interceptForInjection(t,s,v),void o();const R=extractSessionId(t,l),D=extractDeviceIdOrUndefined(t,p),N=extractFingerprintId(t),b=t.headers["user-agent"]??"",L=extractClientIp(t),M=D??N;if(!c&&isBot(b))return void o();const V=h.get(F);function $(){"unknown"!==R&&M&&(S.isPaused()||dispatchUserEvent(e,h,S,I,{userId:F,emailAddress:O,sessionId:R,deviceId:M,fingerprintId:N,userAgent:b,ipAddress:L,eventType:w},m))}V?(h.isStale(F)&&!h.isRefreshing(F)&&(h.markRefreshing(F),fetchAndCacheVerdict(e,h,F,O,M??"unknown",N,R,u).finally(()=>h.clearRefreshing(F))),V.isFlagged||$(),applyVerdict(V,F,O,t,s,o,v,f)):fetchAndCacheVerdict(e,h,F,O,M??"unknown",N,R,u).then(e=>{e.isFlagged||$(),applyVerdict(e,F,O,t,s,o,v,f)}).catch(()=>{$(),interceptForInjection(t,s,v),o()})}}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=parseCookie(e,"__unshared_email");if(r)return r;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,t,r,i,n,s,o,d){if(interceptForInjection(i,n,o),e.isFlagged&&!e.isVerified&&d)try{d({userId:t,emailAddress:r,verdict:e,req:i,res:n,next:s})}catch{s()}else s()}function interceptForInjection(e,t,r){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],interceptResponse(t,(e,t)=>{if(!isHtmlContentType(t))return null;const i=e.toString("utf8"),n=i.lastIndexOf("</body>");return-1===n?i+r:i.slice(0,n)+r+i.slice(n)},{preventCaching:!0})}function dispatchUserEvent(e,t,r,i,n,s){i.mark(n.userId,n.eventType),e.processUserEvent({eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,sessionHash:n.sessionId,userAgent:n.userAgent}).then(e=>{e.success&&e.data?.analysis&&t.update(n.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&r.pause(1e3*e.error.retryAfter)}).catch(e=>{s&&s(e,{operation:"processUserEvent",userId:n.userId,emailAddress:n.emailAddress})})}async function fetchAndCacheVerdict(e,t,r,i,n,s,o,d=CHECK_USER_TIMEOUT_MS){const a={};let c;n&&"unknown"!==n&&(a.deviceId=n),s&&(a.fingerprintId=s);const u=await Promise.race([e.checkUser(i,a),new Promise(e=>{c=setTimeout(()=>e(null),d)})]);if(clearTimeout(c),!u)return{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:o,cachedAt:0,ttl:0};const l=u.data?.is_user_flagged??!1;return t.set(r,{isFlagged:l,isVerified:!1,emailAddress:i,sessionId:o}),t.get(r)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return parseCookie(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return parseCookie(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
@@ -0,0 +1,5 @@
1
+ export declare const ACCOUNT_FLAGGED_ERROR: "account_flagged";
2
+ export declare function flaggedResponse(email: string): {
3
+ readonly error: "account_flagged";
4
+ readonly email: string;
5
+ };
@@ -0,0 +1 @@
1
+ export const ACCOUNT_FLAGGED_ERROR="account_flagged";export function flaggedResponse(e){return{error:"account_flagged",email:e}}
@@ -65,7 +65,7 @@ export declare function assertTrustProxy(app: any): void;
65
65
  *
66
66
  * @example
67
67
  * ```typescript
68
- * import { createUnsharedMiddleware } from "@unshared-labs/sdk/middleware";
68
+ * import { createUnsharedMiddleware } from "unshared-clientjs-sdk";
69
69
  *
70
70
  * app.use(express.json()); // must come first
71
71
  * app.use(createUnsharedMiddleware(client, {
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { UnsharedClient } from './client';
2
2
  export { createUnsharedMiddleware, assertTrustProxy } from './middleware';
3
3
  export type { MiddlewareOptions } from './middleware';
4
- export { unsharedBoundToUser, VerdictCache, } from './middleware/index';
4
+ export { unsharedBoundToUser, VerdictCache, flaggedResponse, ACCOUNT_FLAGGED_ERROR, } from './middleware/index';
5
5
  export type { ProtectionConfig, Verdict } from './middleware/index';
6
6
  export type { UnsharedClientConfig, ApiResult, UnsharedError, SubmitFingerprintOptions, SubmitFingerprintResult, ProcessUserEventParams, ProcessUserEventResult, CheckUserResult, TriggerEmailVerificationResult, VerifyResult, } from './client';
7
7
  export type { UnsharedRequest, UnsharedResponse, UnsharedNextFunction } from './types';
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.sendJson=exports.VerdictCache=exports.unsharedBoundToUser=exports.assertTrustProxy=exports.createUnsharedMiddleware=exports.UnsharedClient=void 0;var client_1=require("./client");Object.defineProperty(exports,"UnsharedClient",{enumerable:!0,get:function(){return client_1.UnsharedClient}});var middleware_1=require("./middleware");Object.defineProperty(exports,"createUnsharedMiddleware",{enumerable:!0,get:function(){return middleware_1.createUnsharedMiddleware}}),Object.defineProperty(exports,"assertTrustProxy",{enumerable:!0,get:function(){return middleware_1.assertTrustProxy}});var index_1=require("./middleware/index");Object.defineProperty(exports,"unsharedBoundToUser",{enumerable:!0,get:function(){return index_1.unsharedBoundToUser}}),Object.defineProperty(exports,"VerdictCache",{enumerable:!0,get:function(){return index_1.VerdictCache}});var http_helpers_1=require("./middleware/utils/http-helpers");Object.defineProperty(exports,"sendJson",{enumerable:!0,get:function(){return http_helpers_1.sendJson}});
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.sendJson=exports.ACCOUNT_FLAGGED_ERROR=exports.flaggedResponse=exports.VerdictCache=exports.unsharedBoundToUser=exports.assertTrustProxy=exports.createUnsharedMiddleware=exports.UnsharedClient=void 0;var client_1=require("./client");Object.defineProperty(exports,"UnsharedClient",{enumerable:!0,get:function(){return client_1.UnsharedClient}});var middleware_1=require("./middleware");Object.defineProperty(exports,"createUnsharedMiddleware",{enumerable:!0,get:function(){return middleware_1.createUnsharedMiddleware}}),Object.defineProperty(exports,"assertTrustProxy",{enumerable:!0,get:function(){return middleware_1.assertTrustProxy}});var index_1=require("./middleware/index");Object.defineProperty(exports,"unsharedBoundToUser",{enumerable:!0,get:function(){return index_1.unsharedBoundToUser}}),Object.defineProperty(exports,"VerdictCache",{enumerable:!0,get:function(){return index_1.VerdictCache}}),Object.defineProperty(exports,"flaggedResponse",{enumerable:!0,get:function(){return index_1.flaggedResponse}}),Object.defineProperty(exports,"ACCOUNT_FLAGGED_ERROR",{enumerable:!0,get:function(){return index_1.ACCOUNT_FLAGGED_ERROR}});var http_helpers_1=require("./middleware/utils/http-helpers");Object.defineProperty(exports,"sendJson",{enumerable:!0,get:function(){return http_helpers_1.sendJson}});
@@ -2,6 +2,7 @@ import type { UnsharedRequest, UnsharedResponse, UnsharedNextFunction } from '..
2
2
  import type { UnsharedClient } from '../client';
3
3
  import { VerdictCache } from './verdict-cache';
4
4
  import type { Verdict } from './verdict-cache';
5
+ export { flaggedResponse, ACCOUNT_FLAGGED_ERROR } from './utils/flagged-response';
5
6
  export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest> {
6
7
  /**
7
8
  * Required. Resolves the current user's ID from the request.
@@ -24,6 +25,10 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
24
25
  skipPaths?: string[];
25
26
  /** When set, only paths matching one of these prefixes get events dispatched and checkUser called. */
26
27
  includePathPrefix?: string[];
28
+ /** Skip the bot/crawler UA filter. Set to true in test environments so automated browsers (Playwright, Puppeteer, etc.) can observe verdicts. @default false */
29
+ disableBotFilter?: boolean;
30
+ /** Hard timeout (ms) for checkUser on cache miss. Fails open on timeout. @default 500 */
31
+ checkUserTimeoutMs?: number;
27
32
  /** Resolves a custom session ID. Falls back to __unshared_sid cookie. */
28
33
  sessionId?: (req: TReq) => string | undefined;
29
34
  /**
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{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"),dispatch_dedupe_1=require("./dispatch-dedupe"),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"),http_helpers_1=require("./utils/http-helpers"),content_type_1=require("./utils/content-type"),skip_paths_1=require("./utils/skip-paths"),include_path_1=require("./utils/include-path"),is_bot_1=require("./utils/is-bot"),client_ip_1=require("./utils/client-ip"),cookies_1=require("./utils/cookies"),device_id_1=require("./utils/device-id"),secure_1=require("./utils/secure"),sentinel_user_id_1=require("./utils/sentinel-user-id"),CHECK_USER_TIMEOUT_MS=500;function unsharedBoundToUser(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");if(!t.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:r,emailAddress:i,routePrefix:n="/__unshared",corsOrigins:s,cacheTTL:o=6e4,skipPaths:d,includePathPrefix:c,sessionId:a,deviceId:u,onFlagged:_,onError:l}=t,p=new verdict_cache_1.VerdictCache(o),h=new rate_limit_backoff_1.RateLimitBackoff,f=new dispatch_dedupe_1.DispatchDedupe,v=Date.now().toString(36),m=(0,fingerprint_script_1.generateFingerprintScript)(n,v);let I="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");I=(0,fs_1.readFileSync)(e,"utf8")}catch{}const S=(0,submit_fp_1.handleSubmitFingerprint)({client:e,verdictCache:p,rateLimitBackoff:h,dispatchDedupe:f,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:a,resolveDeviceId:u,onError:l}),k=(0,verify_1.handleVerifyTrigger)({client:e,verdictCache:p,resolveEmailAddress:i,resolveDeviceId:u,onError:l}),g=(0,verify_1.handleVerify)({client:e,verdictCache:p,resolveEmailAddress:i,resolveDeviceId:u,onError:l}),y=s?Array.isArray(s)?s:[s]:null,A=`${n}/fp.js`,C=`${n}/submit-fp`,x=`${n}/verify-trigger`,q=`${n}/verify`,w=`${n}/status`;return function(t,s,o){const v=(0,http_helpers_1.getRequestPath)(t.url),E=t.url||v;if(v.startsWith(n+"/")){if(function(e,t){if(!y)return;const r=e.headers.origin??"",i=y.includes("*");(i||y.includes(r))&&(t.setHeader("Access-Control-Allow-Origin",i?"*":r),t.setHeader("Access-Control-Allow-Methods","POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,s),"OPTIONS"===t.method)return void(0,http_helpers_1.sendEmpty)(s,204);if("GET"===t.method&&v===A)return s.setHeader("Content-Type","application/javascript"),s.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBody)(s,200,I);if("POST"===t.method&&(v===C||v===x||v===q))return void 0===t.body?void(0,http_helpers_1.sendJson)(s,400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"req.body is undefined. Mount a JSON body-parsing middleware (e.g., express.json()) before the Unshared middleware."}}):v===C?void S(t,s):v===x?void k(t,s):void g(t,s);if("GET"===t.method&&v===w){let n;try{n=r(t)}catch{}if(!n)return void(0,http_helpers_1.sendJson)(s,200,{status:"anonymous"});const o=resolveEmail(t,i);return void(async()=>{let r=p.get(n);if((!r||p.isStale(n))&&o&&!h.isPaused()&&!p.isRefreshing(n)){p.markRefreshing(n);try{const i=(0,device_id_1.extractDeviceIdOrUndefined)(t,u),s=extractFingerprintId(t),d=extractSessionId(t,a),c=i??s??"unknown";await fetchAndCacheVerdict(e,p,n,o,c,s,d),r=p.get(n)}catch(e){l&&l(e,{operation:"checkUser",userId:n,emailAddress:o})}finally{p.clearRefreshing(n)}}r&&r.isFlagged&&!r.isVerified&&_&&o?(0,http_helpers_1.sendJson)(s,200,{status:"flagged",email:o}):(0,http_helpers_1.sendJson)(s,200,{status:"ok"})})()}return void(0,http_helpers_1.sendJson)(s,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if((0,skip_paths_1.shouldSkipPath)(v,d))return void o();if(!(0,include_path_1.shouldIncludePath)(v,c))return interceptForInjection(t,s,m),void o();let b;try{b=r(t)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(b)){const e=(0,cookies_1.parseCookie)(t,"__unshared_uid"),r=(0,cookies_1.parseCookie)(t,"__unshared_uid_at"),i=r?Number(r):NaN,n=Number.isFinite(i)&&Date.now()-i<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;b=e&&n?e:void 0}if(!b){const e=(0,secure_1.isSecureRequest)(t)?"; Secure":"";return appendSetCookie(s,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(t,s,m),void o()}const T=resolveEmail(t,i);if(setUserIdCookie(t,s,b),T&&setEmailCookie(t,s,T),!T)return interceptForInjection(t,s,m),void o();const P=extractSessionId(t,a),U=(0,device_id_1.extractDeviceIdOrUndefined)(t,u),O=extractFingerprintId(t),$=t.headers["user-agent"]??"",F=(0,client_ip_1.extractClientIp)(t),j=U??O;if((0,is_bot_1.isBot)($))return void o();const N=p.get(b);function D(){"unknown"!==P&&j&&(h.isPaused()||dispatchUserEvent(e,p,h,f,{userId:b,emailAddress:T,sessionId:P,deviceId:j,fingerprintId:O,userAgent:$,ipAddress:F,eventType:E},l))}N?(p.isStale(b)&&!p.isRefreshing(b)&&(p.markRefreshing(b),fetchAndCacheVerdict(e,p,b,T,j??"unknown",O,P).finally(()=>p.clearRefreshing(b))),N.isFlagged||D(),applyVerdict(N,b,T,t,s,o,m,_)):fetchAndCacheVerdict(e,p,b,T,j??"unknown",O,P).then(e=>{e.isFlagged||D(),applyVerdict(e,b,T,t,s,o,m,_)}).catch(()=>{D(),interceptForInjection(t,s,m),o()})}}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=(0,cookies_1.parseCookie)(e,"__unshared_email");if(r)return r;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,t,r,i,n,s,o,d){if(interceptForInjection(i,n,o),e.isFlagged&&!e.isVerified&&d)try{d({userId:t,emailAddress:r,verdict:e,req:i,res:n,next:s})}catch{s()}else s()}function interceptForInjection(e,t,r){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],(0,response_interceptor_1.interceptResponse)(t,(e,t)=>{if(!(0,content_type_1.isHtmlContentType)(t))return null;const i=e.toString("utf8"),n=i.lastIndexOf("</body>");return-1===n?i+r:i.slice(0,n)+r+i.slice(n)},{preventCaching:!0})}function dispatchUserEvent(e,t,r,i,n,s){i.mark(n.userId,n.eventType),e.processUserEvent({eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,sessionHash:n.sessionId,userAgent:n.userAgent}).then(e=>{e.success&&e.data?.analysis&&t.update(n.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&r.pause(1e3*e.error.retryAfter)}).catch(e=>{s&&s(e,{operation:"processUserEvent",userId:n.userId,emailAddress:n.emailAddress})})}async function fetchAndCacheVerdict(e,t,r,i,n,s,o){const d={};let c;n&&"unknown"!==n&&(d.deviceId=n),s&&(d.fingerprintId=s);const a=await Promise.race([e.checkUser(i,d),new Promise(e=>{c=setTimeout(()=>e(null),500)})]);if(clearTimeout(c),!a)return{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:o,cachedAt:0,ttl:0};const u=a.data?.is_user_flagged??!1;return t.set(r,{isFlagged:u,isVerified:!1,emailAddress:i,sessionId:o}),t.get(r)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return(0,cookies_1.parseCookie)(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return(0,cookies_1.parseCookie)(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.VerdictCache=exports.ACCOUNT_FLAGGED_ERROR=exports.flaggedResponse=void 0,exports.unsharedBoundToUser=unsharedBoundToUser;const fs_1=require("fs"),verdict_cache_1=require("./verdict-cache");Object.defineProperty(exports,"VerdictCache",{enumerable:!0,get:function(){return verdict_cache_1.VerdictCache}});const rate_limit_backoff_1=require("./rate-limit-backoff"),dispatch_dedupe_1=require("./dispatch-dedupe"),response_interceptor_1=require("./response-interceptor"),fingerprint_script_1=require("./injection/fingerprint-script"),submit_fp_1=require("./routes/submit-fp"),verify_1=require("./routes/verify"),http_helpers_1=require("./utils/http-helpers"),content_type_1=require("./utils/content-type"),skip_paths_1=require("./utils/skip-paths"),include_path_1=require("./utils/include-path"),is_bot_1=require("./utils/is-bot"),client_ip_1=require("./utils/client-ip"),cookies_1=require("./utils/cookies"),device_id_1=require("./utils/device-id"),secure_1=require("./utils/secure");var flagged_response_1=require("./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}});const sentinel_user_id_1=require("./utils/sentinel-user-id"),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:t,emailAddress:i,routePrefix:s="/__unshared",corsOrigins:n,cacheTTL:o=6e4,skipPaths:d,includePathPrefix:c,disableBotFilter:a=!1,checkUserTimeoutMs:u=CHECK_USER_TIMEOUT_MS,sessionId:_,deviceId:l,onFlagged:p,onError:h}=r,f=new verdict_cache_1.VerdictCache(o),v=new rate_limit_backoff_1.RateLimitBackoff,m=new dispatch_dedupe_1.DispatchDedupe,g=Date.now().toString(36),S=(0,fingerprint_script_1.generateFingerprintScript)(s,g);let I="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");I=(0,fs_1.readFileSync)(e,"utf8")}catch{}const k=(0,submit_fp_1.handleSubmitFingerprint)({client:e,verdictCache:f,rateLimitBackoff:v,dispatchDedupe:m,resolveUserId:t,resolveEmailAddress:i,resolveSessionId:_,resolveDeviceId:l,onError:h}),C=(0,verify_1.handleVerifyTrigger)({client:e,verdictCache:f,resolveEmailAddress:i,resolveDeviceId:l,onError:h}),A=(0,verify_1.handleVerify)({client:e,verdictCache:f,resolveEmailAddress:i,resolveDeviceId:l,onError:h}),y=n?Array.isArray(n)?n:[n]:null,x=`${s}/fp.js`,E=`${s}/submit-fp`,b=`${s}/verify-trigger`,T=`${s}/verify`,q=`${s}/status`;return function(r,n,o){const g=(0,http_helpers_1.getRequestPath)(r.url),U=r.url||g;if(g.startsWith(s+"/")){if(function(e,r){if(!y)return;const t=e.headers.origin??"",i=y.includes("*");(i||y.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,n),"OPTIONS"===r.method)return void(0,http_helpers_1.sendEmpty)(n,204);if("GET"===r.method&&g===x)return n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBody)(n,200,I);if("POST"===r.method&&(g===E||g===b||g===T))return void 0===r.body?void(0,http_helpers_1.sendJson)(n,400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"req.body is undefined. Mount a JSON body-parsing middleware (e.g., express.json()) before the Unshared middleware."}}):g===E?void k(r,n):g===b?void C(r,n):void A(r,n);if("GET"===r.method&&g===q){let s;try{s=t(r)}catch{}if(!s)return void(0,http_helpers_1.sendJson)(n,200,{status:"anonymous"});const o=resolveEmail(r,i);return void(async()=>{let t=f.get(s);if((!t||f.isStale(s))&&o&&!v.isPaused()&&!f.isRefreshing(s)){f.markRefreshing(s);try{const i=(0,device_id_1.extractDeviceIdOrUndefined)(r,l),n=extractFingerprintId(r),d=extractSessionId(r,_),c=i??n??"unknown";await fetchAndCacheVerdict(e,f,s,o,c,n,d,u),t=f.get(s)}catch(e){h&&h(e,{operation:"checkUser",userId:s,emailAddress:o})}finally{f.clearRefreshing(s)}}t&&t.isFlagged&&!t.isVerified&&p&&o?(0,http_helpers_1.sendJson)(n,200,{status:"flagged",email:o}):(0,http_helpers_1.sendJson)(n,200,{status:"ok"})})()}return void(0,http_helpers_1.sendJson)(n,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if((0,skip_paths_1.shouldSkipPath)(g,d))return void o();if(!(0,include_path_1.shouldIncludePath)(g,c))return interceptForInjection(r,n,S),void o();let w;try{w=t(r)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(w)){const e=(0,cookies_1.parseCookie)(r,"__unshared_uid"),t=(0,cookies_1.parseCookie)(r,"__unshared_uid_at"),i=t?Number(t):NaN,s=Number.isFinite(i)&&Date.now()-i<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;w=e&&s?e:void 0}if(!w){const e=(0,secure_1.isSecureRequest)(r)?"; Secure":"";return appendSetCookie(n,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(r,n,S),void o()}const O=resolveEmail(r,i);if(setUserIdCookie(r,n,w),O&&setEmailCookie(r,n,O),!O)return interceptForInjection(r,n,S),void o();const P=extractSessionId(r,_),F=(0,device_id_1.extractDeviceIdOrUndefined)(r,l),j=extractFingerprintId(r),M=r.headers["user-agent"]??"",$=(0,client_ip_1.extractClientIp)(r),N=F??j;if(!a&&(0,is_bot_1.isBot)(M))return void o();const D=f.get(w);function R(){"unknown"!==P&&N&&(v.isPaused()||dispatchUserEvent(e,f,v,m,{userId:w,emailAddress:O,sessionId:P,deviceId:N,fingerprintId:j,userAgent:M,ipAddress:$,eventType:U},h))}D?(f.isStale(w)&&!f.isRefreshing(w)&&(f.markRefreshing(w),fetchAndCacheVerdict(e,f,w,O,N??"unknown",j,P,u).finally(()=>f.clearRefreshing(w))),D.isFlagged||R(),applyVerdict(D,w,O,r,n,o,S,p)):fetchAndCacheVerdict(e,f,w,O,N??"unknown",j,P,u).then(e=>{e.isFlagged||R(),applyVerdict(e,w,O,r,n,o,S,p)}).catch(()=>{R(),interceptForInjection(r,n,S),o()})}}function resolveEmail(e,r){if(r)try{const t=r(e);if(t)return t}catch{}const t=(0,cookies_1.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,s,n,o,d){if(interceptForInjection(i,s,o),e.isFlagged&&!e.isVerified&&d)try{d({userId:r,emailAddress:t,verdict:e,req:i,res:s,next:n})}catch{n()}else n()}function interceptForInjection(e,r,t){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 i=e.toString("utf8"),s=i.lastIndexOf("</body>");return-1===s?i+t:i.slice(0,s)+t+i.slice(s)},{preventCaching:!0})}function dispatchUserEvent(e,r,t,i,s,n){i.mark(s.userId,s.eventType),e.processUserEvent({eventType:s.eventType,userId:s.userId,emailAddress:s.emailAddress,ipAddress:s.ipAddress,deviceId:s.deviceId,fingerprintId:s.fingerprintId,sessionHash:s.sessionId,userAgent:s.userAgent}).then(e=>{e.success&&e.data?.analysis&&r.update(s.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:s.userId,emailAddress:s.emailAddress})})}async function fetchAndCacheVerdict(e,r,t,i,s,n,o,d=CHECK_USER_TIMEOUT_MS){const c={};let a;s&&"unknown"!==s&&(c.deviceId=s),n&&(c.fingerprintId=n);const u=await Promise.race([e.checkUser(i,c),new Promise(e=>{a=setTimeout(()=>e(null),d)})]);if(clearTimeout(a),!u)return{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:o,cachedAt:0,ttl:0};const _=u.data?.is_user_flagged??!1;return r.set(t,{isFlagged:_,isVerified:!1,emailAddress:i,sessionId:o}),r.get(t)}function extractSessionId(e,r){if(r)try{const t=r(e);if(t)return t}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 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=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(r,`__unshared_uid=${encodeURIComponent(t)}; Path=/; SameSite=Lax${i}`),appendSetCookie(r,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,r,t){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(r,`__unshared_email=${encodeURIComponent(t)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
@@ -0,0 +1,5 @@
1
+ export declare const ACCOUNT_FLAGGED_ERROR: "account_flagged";
2
+ export declare function flaggedResponse(email: string): {
3
+ readonly error: "account_flagged";
4
+ readonly email: string;
5
+ };
@@ -0,0 +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";
@@ -65,7 +65,7 @@ export declare function assertTrustProxy(app: any): void;
65
65
  *
66
66
  * @example
67
67
  * ```typescript
68
- * import { createUnsharedMiddleware } from "@unshared-labs/sdk/middleware";
68
+ * import { createUnsharedMiddleware } from "unshared-clientjs-sdk";
69
69
  *
70
70
  * app.use(express.json()); // must come first
71
71
  * app.use(createUnsharedMiddleware(client, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unshared-clientjs-sdk",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
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,7 +52,7 @@
52
52
  "author": "",
53
53
  "license": "MIT",
54
54
  "dependencies": {
55
- "unshared-frontend-sdk": "2.0.0"
55
+ "unshared-frontend-sdk": "2.0.1"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/express": "^4.17.21",