unshared-clientjs-sdk 2.0.0-rc.2 → 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.
- package/README.md +100 -102
- package/dist/client.d.ts +57 -12
- package/dist/client.js +1 -1
- package/dist/esm/client.d.mts +57 -12
- package/dist/esm/client.mjs +1 -1
- package/dist/esm/index.d.mts +5 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/middleware/index.d.mts +50 -0
- package/dist/esm/middleware/index.mjs +1 -0
- package/dist/esm/middleware/injection/fingerprint-script.d.mts +16 -0
- package/dist/esm/middleware/injection/fingerprint-script.mjs +1 -0
- package/dist/esm/middleware/rate-limit-backoff.d.mts +14 -0
- package/dist/esm/middleware/rate-limit-backoff.mjs +1 -0
- package/dist/esm/middleware/response-interceptor.d.mts +15 -0
- package/dist/esm/middleware/response-interceptor.mjs +1 -0
- package/dist/esm/middleware/routes/submit-fp.d.mts +24 -0
- package/dist/esm/middleware/routes/submit-fp.mjs +1 -0
- package/dist/esm/middleware/routes/verify.d.mts +28 -0
- package/dist/esm/middleware/routes/verify.mjs +1 -0
- package/dist/esm/middleware/utils/client-ip.d.mts +6 -0
- package/dist/esm/middleware/utils/client-ip.mjs +1 -0
- package/dist/esm/middleware/utils/content-type.d.mts +6 -0
- package/dist/esm/middleware/utils/content-type.mjs +1 -0
- 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/is-bot.d.mts +5 -0
- package/dist/esm/middleware/utils/is-bot.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.d.mts +5 -0
- package/dist/esm/middleware/utils/skip-paths.mjs +1 -0
- package/dist/esm/middleware/verdict-cache.d.mts +47 -0
- package/dist/esm/middleware/verdict-cache.mjs +1 -0
- package/dist/esm/middleware.d.mts +30 -5
- package/dist/esm/middleware.mjs +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +1 -1
- package/dist/middleware/index.d.ts +50 -0
- package/dist/middleware/index.js +1 -0
- package/dist/middleware/injection/fingerprint-script.d.ts +16 -0
- package/dist/middleware/injection/fingerprint-script.js +1 -0
- package/dist/middleware/rate-limit-backoff.d.ts +14 -0
- package/dist/middleware/rate-limit-backoff.js +1 -0
- package/dist/middleware/response-interceptor.d.ts +15 -0
- package/dist/middleware/response-interceptor.js +1 -0
- package/dist/middleware/routes/submit-fp.d.ts +24 -0
- package/dist/middleware/routes/submit-fp.js +1 -0
- package/dist/middleware/routes/verify.d.ts +28 -0
- package/dist/middleware/routes/verify.js +1 -0
- package/dist/middleware/utils/client-ip.d.ts +6 -0
- package/dist/middleware/utils/client-ip.js +1 -0
- package/dist/middleware/utils/content-type.d.ts +6 -0
- package/dist/middleware/utils/content-type.js +1 -0
- 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/is-bot.d.ts +5 -0
- package/dist/middleware/utils/is-bot.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.d.ts +5 -0
- package/dist/middleware/utils/skip-paths.js +1 -0
- package/dist/middleware/verdict-cache.d.ts +47 -0
- package/dist/middleware/verdict-cache.js +1 -0
- package/dist/middleware.d.ts +30 -5
- package/dist/middleware.js +1 -1
- package/package.json +14 -1
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Request } from 'express';
|
|
2
|
+
/**
|
|
3
|
+
* Extract the real client IP from proxy headers, falling back to req.ip.
|
|
4
|
+
* Checked in order: CF-Connecting-IP (Cloudflare) → X-Real-IP (nginx/ALB) → req.ip.
|
|
5
|
+
*/
|
|
6
|
+
export declare function extractClientIp(req: Request): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";function extractClientIp(t){const e=t.headers["cf-connecting-ip"];if("string"==typeof e&&e)return e;const r=t.headers["x-real-ip"];return"string"==typeof r&&r?r:t.ip??""}Object.defineProperty(exports,"t",{value:!0}),exports.extractClientIp=extractClientIp;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Check if a Content-Type header value indicates HTML. */
|
|
2
|
+
export declare function isHtmlContentType(contentType: string | undefined): boolean;
|
|
3
|
+
/** Check if a Content-Type header value indicates JSON. */
|
|
4
|
+
export declare function isJsonContentType(contentType: string | undefined): boolean;
|
|
5
|
+
/** Check if a Content-Type indicates a static asset (images, fonts, etc). */
|
|
6
|
+
export declare function isStaticContentType(contentType: string | undefined): boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";function isHtmlContentType(t){return!!t&&t.includes("text/html")}function isJsonContentType(t){return!!t&&t.includes("application/json")}function isStaticContentType(t){return!!t&&["image/","font/","audio/","video/","application/javascript","text/javascript","text/css","application/wasm"].some(e=>t.includes(e))}Object.defineProperty(exports,"t",{value:!0}),exports.isHtmlContentType=isHtmlContentType,exports.isJsonContentType=isJsonContentType,exports.isStaticContentType=isStaticContentType;
|
|
@@ -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";Object.defineProperty(exports,"t",{value:!0}),exports.isBot=isBot;const BOT_PATTERNS=["googlebot","bingbot","slurp","baiduspider","duckduckbot","yandex","sogou","exabot","ia_archiver","curl","wget","python-requests","python-urllib","axios","node-fetch","go-http-client","java/","libwww-perl","okhttp","apache-httpclient","http_request","httpie","headlesschrome","phantomjs","nessus","nikto","sqlmap","burp","zap","qualys","openvas","nmap","masscan","facebookexternalhit","twitterbot","linkedinbot","whatsapp","telegrambot","slackbot","discordbot","bot","crawl","spider","scrape","fetch","scan"],BOT_RE=new RegExp(BOT_PATTERNS.join("|"),"i");function isBot(t){return!!t&&BOT_RE.test(t)}
|
|
@@ -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;
|
|
@@ -0,0 +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",".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}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface Verdict {
|
|
2
|
+
isFlagged: boolean;
|
|
3
|
+
isVerified: boolean;
|
|
4
|
+
emailAddress: string;
|
|
5
|
+
sessionId: string;
|
|
6
|
+
cachedAt: number;
|
|
7
|
+
ttl: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class VerdictCache {
|
|
10
|
+
private readonly _entries;
|
|
11
|
+
private readonly _activeRefreshes;
|
|
12
|
+
private readonly _defaultTtlMs;
|
|
13
|
+
private readonly _maxSize;
|
|
14
|
+
private _sweepTimer;
|
|
15
|
+
constructor(defaultTTL?: number, maxSize?: number);
|
|
16
|
+
get(userId: string): Verdict | undefined;
|
|
17
|
+
set(userId: string, verdict: Omit<Verdict, 'cachedAt' | 'ttl'>, ttl?: number): void;
|
|
18
|
+
/**
|
|
19
|
+
* Update an existing cache entry (e.g. from webhook).
|
|
20
|
+
* If the user is not in cache, creates a new entry.
|
|
21
|
+
*/
|
|
22
|
+
update(userId: string, partial: Partial<Pick<Verdict, 'isFlagged' | 'isVerified'>>): void;
|
|
23
|
+
delete(userId: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* Returns true if the entry exists but is past its TTL.
|
|
26
|
+
* Stale entries are served while a background refresh happens.
|
|
27
|
+
*/
|
|
28
|
+
isStale(userId: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Returns true if a background refresh is already in flight for this user.
|
|
31
|
+
*/
|
|
32
|
+
isRefreshing(userId: string): boolean;
|
|
33
|
+
markRefreshing(userId: string): void;
|
|
34
|
+
clearRefreshing(userId: string): void;
|
|
35
|
+
/** Number of cached entries. */
|
|
36
|
+
get size(): number;
|
|
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;
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
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;
|
package/dist/middleware.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Request, Response, NextFunction } from 'express';
|
|
1
|
+
import type { Request, Response, NextFunction, Application } from 'express';
|
|
2
2
|
import type { UnsharedLabsClient } from './client';
|
|
3
3
|
export interface MiddlewareOptions {
|
|
4
4
|
/** Override userId extractor. Falls back to req.body.user_id. */
|
|
@@ -7,6 +7,8 @@ export interface MiddlewareOptions {
|
|
|
7
7
|
eventTypeExtractor?: (req: Request) => string | undefined;
|
|
8
8
|
/** Override sessionId extractor. Falls back to X-Session-Id header, then req.body.session_id. */
|
|
9
9
|
sessionIdExtractor?: (req: Request) => string | undefined;
|
|
10
|
+
/** Override IP address extractor. Falls back to req.ip. */
|
|
11
|
+
ipAddressExtractor?: (req: Request) => string | undefined;
|
|
10
12
|
/** Default event type when none is extractable. @default "browser_event" */
|
|
11
13
|
defaultEventType?: string;
|
|
12
14
|
/**
|
|
@@ -14,7 +16,30 @@ export interface MiddlewareOptions {
|
|
|
14
16
|
* @default "/unshared"
|
|
15
17
|
*/
|
|
16
18
|
routePrefix?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Allowed CORS origins for the fingerprint route.
|
|
21
|
+
* Use `"*"` to allow all origins, or pass a specific origin / array of origins.
|
|
22
|
+
* The middleware handles OPTIONS preflight automatically when this is set.
|
|
23
|
+
* @example corsOrigins: "https://app.example.com"
|
|
24
|
+
* @example corsOrigins: ["https://app.example.com", "https://staging.example.com"]
|
|
25
|
+
*/
|
|
26
|
+
corsOrigins?: string | string[];
|
|
17
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Asserts that Express `trust proxy` is configured on the app.
|
|
30
|
+
* Call this once during application startup, before mounting any middleware.
|
|
31
|
+
*
|
|
32
|
+
* Throws synchronously if the setting is missing, killing the process before
|
|
33
|
+
* any requests are served.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* assertTrustProxy(app); // throws at startup if not set
|
|
38
|
+
* app.use(express.json());
|
|
39
|
+
* app.use(createUnsharedMiddleware(client, options));
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare function assertTrustProxy(app: Application): void;
|
|
18
43
|
/**
|
|
19
44
|
* Creates an Express middleware that proxies browser fingerprint events to
|
|
20
45
|
* Unshared Labs. Mount this to handle the browser fingerprint route contract (§4 of spec).
|
|
@@ -25,10 +50,10 @@ export interface MiddlewareOptions {
|
|
|
25
50
|
* **Prerequisites:**
|
|
26
51
|
* - Mount `express.json()` (or equivalent body-parser) **before** this middleware,
|
|
27
52
|
* otherwise `req.body` will be undefined and every request will return 400.
|
|
28
|
-
* -
|
|
29
|
-
*
|
|
30
|
-
* -
|
|
31
|
-
*
|
|
53
|
+
* - For cross-origin frontends, pass `corsOrigins` instead of configuring CORS
|
|
54
|
+
* separately — the middleware handles OPTIONS preflight automatically.
|
|
55
|
+
* - `user_id` is automatically scrubbed from `req.body` after it is read, so
|
|
56
|
+
* downstream logging middleware will not capture plaintext PII.
|
|
32
57
|
*
|
|
33
58
|
* **Error contract:** Never returns 5xx to the browser. Upstream failures are
|
|
34
59
|
* returned as HTTP 200 with { success: false, error: { code: "UPSTREAM_ERROR" } }.
|
package/dist/middleware.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";function createUnsharedMiddleware(e,r){const{userIdExtractor:s,eventTypeExtractor:t,sessionIdExtractor:
|
|
1
|
+
"use strict";function assertTrustProxy(e){if(!e.get("trust proxy"))throw new Error('[unshared-labs] Express "trust proxy" is not set. Add `app.set("trust proxy", 1)` before calling assertTrustProxy, otherwise req.ip will reflect the proxy\'s IP instead of the real client IP.')}function createUnsharedMiddleware(e,r){const{userIdExtractor:s,eventTypeExtractor:t,sessionIdExtractor:o,ipAddressExtractor:i,defaultEventType:n="browser_event",routePrefix:c="/unshared",corsOrigins:d}=r??{},a=`${c}/submit-fingerprint-event`,l=d?Array.isArray(d)?d:[d]:null;let u=!1;return async(r,c,d)=>{if(!u&&(u=!0,r.app&&!r.app.get("trust proxy")))throw new Error('[unshared-labs] Express "trust proxy" is not set. Add `app.set("trust proxy", 1)` before mounting this middleware, otherwise req.ip will reflect the proxy\'s IP instead of the real client IP.');if(l&&r.path===a){const e=r.headers.origin??"",s=l.includes("*");if((s||l.includes(e))&&(c.setHeader("Access-Control-Allow-Origin",s?"*":e),c.setHeader("Access-Control-Allow-Methods","POST, OPTIONS"),c.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id")),"OPTIONS"===r.method)return void c.status(204).end()}if("POST"===r.method&&r.path===a)try{const d=r.body??{};if(!d.hash||!d.stable_hash||!d.collected_at)return void c.status(400).json({success:!1,error:{code:"VALIDATION_ERROR",message:"Missing required fingerprint fields: hash, stable_hash, collected_at"}});if(!r.headers["x-session-id"])return void c.status(400).json({success:!1,error:{code:"VALIDATION_ERROR",message:"Missing required header: X-Session-Id"}});const a={full_hash:d.hash,fingerprint_id:d.stable_hash,timestamp:d.collected_at,isIncognito:d.is_incognito??!1,components:d.components??{},version:d.version??"unknown"};let l,u,p,f;try{l=(s?s(r):void 0)??d.user_id}catch{l=d.user_id}if(r.body&&"object"==typeof r.body&&"user_id"in r.body&&delete r.body.user_id,!l)return void c.status(400).json({success:!1,error:{code:"VALIDATION_ERROR",message:"Missing required field: user_id"}});try{u=(t?t(r):void 0)??d.event_type??n}catch{u=d.event_type??n}try{p=(o?o(r):void 0)??r.headers["x-session-id"]?.toString()??d.session_id}catch{p=r.headers["x-session-id"]?.toString()??d.session_id}try{f=(i?i(r):void 0)??r.ip}catch{f=r.ip}const h=await e.submitFingerprintEvent(a,{userId:l,sessionHash:p,eventType:u,ipAddress:f});if(!h.success)return void c.status(200).json({success:!1,error:{code:"UPSTREAM_ERROR",message:h.error?.message??"Upstream request failed"}});c.status(202).json({success:!0,data:h.data})}catch(e){c.status(200).json({success:!1,error:{code:"MIDDLEWARE_ERROR",message:e instanceof Error?e.message:"Middleware error"}})}else d()}}Object.defineProperty(exports,"t",{value:!0}),exports.assertTrustProxy=assertTrustProxy,exports.createUnsharedMiddleware=createUnsharedMiddleware;
|
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",
|
|
@@ -28,6 +28,16 @@
|
|
|
28
28
|
"import": "./dist/esm/middleware.mjs",
|
|
29
29
|
"require": "./dist/middleware.js",
|
|
30
30
|
"types": "./dist/middleware.d.ts"
|
|
31
|
+
},
|
|
32
|
+
"./auto-middleware": {
|
|
33
|
+
"import": "./dist/esm/middleware/index.mjs",
|
|
34
|
+
"require": "./dist/middleware/index.js",
|
|
35
|
+
"types": "./dist/middleware/index.d.ts"
|
|
36
|
+
},
|
|
37
|
+
"./middleware/auto": {
|
|
38
|
+
"import": "./dist/esm/middleware/index.mjs",
|
|
39
|
+
"require": "./dist/middleware/index.js",
|
|
40
|
+
"types": "./dist/middleware/index.d.ts"
|
|
31
41
|
}
|
|
32
42
|
},
|
|
33
43
|
"engines": {
|
|
@@ -44,6 +54,9 @@
|
|
|
44
54
|
"optional": true
|
|
45
55
|
}
|
|
46
56
|
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"unshared-frontend-sdk": "2.0.0-rc.21"
|
|
59
|
+
},
|
|
47
60
|
"devDependencies": {
|
|
48
61
|
"@types/express": "^4.17.21",
|
|
49
62
|
"@types/node": "^24.10.1",
|