unshared-clientjs-sdk 2.2.1 → 3.0.0-rc.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +176 -151
  2. package/dist/client.d.ts +58 -24
  3. package/dist/client.js +1 -1
  4. package/dist/esm/client.d.mts +58 -24
  5. package/dist/esm/client.mjs +1 -1
  6. package/dist/esm/index.d.mts +8 -8
  7. package/dist/esm/index.mjs +1 -1
  8. package/dist/esm/middleware/dispatch-dedupe.d.mts +1 -0
  9. package/dist/esm/middleware/dispatch-dedupe.mjs +1 -1
  10. package/dist/esm/middleware/dispatch-processing.d.mts +25 -0
  11. package/dist/esm/middleware/dispatch-processing.mjs +1 -0
  12. package/dist/esm/middleware/index.d.mts +19 -5
  13. package/dist/esm/middleware/index.mjs +1 -1
  14. package/dist/esm/middleware/injection/fingerprint-script.d.mts +7 -0
  15. package/dist/esm/middleware/injection/fingerprint-script.mjs +1 -1
  16. package/dist/esm/middleware/injection/fp-bundle-source.d.mts +5 -4
  17. package/dist/esm/middleware/injection/fp-bundle-source.mjs +1 -1
  18. package/dist/esm/middleware/rate-limit-backoff.d.mts +11 -8
  19. package/dist/esm/middleware/rate-limit-backoff.mjs +1 -1
  20. package/dist/esm/middleware/response-interceptor.d.mts +2 -1
  21. package/dist/esm/middleware/response-interceptor.mjs +1 -1
  22. package/dist/esm/middleware/routes/interstitial.d.mts +2 -2
  23. package/dist/esm/middleware/routes/interstitial.mjs +1 -1
  24. package/dist/esm/middleware/routes/submit-fp.d.mts +5 -5
  25. package/dist/esm/middleware/routes/submit-fp.mjs +1 -1
  26. package/dist/esm/middleware/routes/verify.d.mts +3 -3
  27. package/dist/esm/middleware/routes/verify.mjs +1 -1
  28. package/dist/esm/middleware/utils/client-ip.d.mts +1 -1
  29. package/dist/esm/middleware/utils/cookies.d.mts +1 -1
  30. package/dist/esm/middleware/utils/device-id.d.mts +1 -1
  31. package/dist/esm/middleware/utils/device-id.mjs +1 -1
  32. package/dist/esm/middleware/utils/flagged-response.mjs +1 -1
  33. package/dist/esm/middleware/utils/http-helpers.d.mts +1 -1
  34. package/dist/esm/middleware/utils/permanent-device-id.d.mts +7 -0
  35. package/dist/esm/middleware/utils/permanent-device-id.mjs +1 -0
  36. package/dist/esm/middleware/utils/read-json-body.d.mts +3 -0
  37. package/dist/esm/middleware/utils/read-json-body.mjs +1 -0
  38. package/dist/esm/middleware/utils/secure.d.mts +1 -1
  39. package/dist/esm/middleware/utils/skip-paths.d.mts +5 -1
  40. package/dist/esm/middleware/utils/skip-paths.mjs +1 -1
  41. package/dist/esm/middleware/verdict-cache.d.mts +8 -1
  42. package/dist/esm/middleware/verdict-cache.mjs +1 -1
  43. package/dist/esm/middleware.d.mts +5 -6
  44. package/dist/esm/middleware.mjs +1 -1
  45. package/dist/esm/shared-types.d.mts +148 -0
  46. package/dist/esm/web/index.d.mts +6 -6
  47. package/dist/esm/web/index.mjs +1 -1
  48. package/dist/esm/web/protection-handler.d.mts +31 -3
  49. package/dist/esm/web/protection-handler.mjs +1 -1
  50. package/dist/esm/web/submit-handler.d.mts +2 -2
  51. package/dist/esm/web/submit-handler.mjs +1 -1
  52. package/dist/esm/web/types.d.mts +12 -2
  53. package/dist/esm/web/web-helpers.d.mts +11 -0
  54. package/dist/esm/web/web-helpers.mjs +1 -1
  55. package/dist/esm/webcrypto.d.mts +8 -0
  56. package/dist/esm/webcrypto.mjs +1 -0
  57. package/dist/middleware/dispatch-dedupe.d.ts +1 -0
  58. package/dist/middleware/dispatch-dedupe.js +1 -1
  59. package/dist/middleware/dispatch-processing.d.ts +25 -0
  60. package/dist/middleware/dispatch-processing.js +1 -0
  61. package/dist/middleware/index.d.ts +14 -0
  62. package/dist/middleware/index.js +1 -1
  63. package/dist/middleware/injection/fingerprint-script.d.ts +7 -0
  64. package/dist/middleware/injection/fingerprint-script.js +1 -1
  65. package/dist/middleware/injection/fp-bundle-source.d.ts +5 -4
  66. package/dist/middleware/injection/fp-bundle-source.js +1 -1
  67. package/dist/middleware/rate-limit-backoff.d.ts +11 -8
  68. package/dist/middleware/rate-limit-backoff.js +1 -1
  69. package/dist/middleware/response-interceptor.d.ts +1 -0
  70. package/dist/middleware/response-interceptor.js +1 -1
  71. package/dist/middleware/routes/submit-fp.js +1 -1
  72. package/dist/middleware/routes/verify.js +1 -1
  73. package/dist/middleware/utils/permanent-device-id.d.ts +7 -0
  74. package/dist/middleware/utils/permanent-device-id.js +1 -0
  75. package/dist/middleware/utils/read-json-body.d.ts +3 -0
  76. package/dist/middleware/utils/read-json-body.js +1 -0
  77. package/dist/middleware/utils/skip-paths.d.ts +5 -1
  78. package/dist/middleware/utils/skip-paths.js +1 -1
  79. package/dist/middleware/verdict-cache.d.ts +8 -1
  80. package/dist/middleware/verdict-cache.js +1 -1
  81. package/dist/middleware.d.ts +3 -4
  82. package/dist/middleware.js +1 -1
  83. package/dist/shared-types.d.ts +148 -0
  84. package/dist/web/protection-handler.d.ts +29 -1
  85. package/dist/web/protection-handler.js +1 -1
  86. package/dist/web/submit-handler.js +1 -1
  87. package/dist/web/types.d.ts +11 -1
  88. package/dist/web/web-helpers.d.ts +11 -0
  89. package/dist/web/web-helpers.js +1 -1
  90. package/dist/webcrypto.d.ts +8 -0
  91. package/dist/webcrypto.js +1 -0
  92. package/package.json +5 -5
@@ -0,0 +1,7 @@
1
+ import type { UnsharedRequest } from '../../types';
2
+ export declare function isPermanentDeviceId(value: unknown): value is string;
3
+ export declare function extractPermanentDeviceId(req: UnsharedRequest): string | undefined;
4
+ export declare function resolveOrCreatePermanentDeviceId(req: UnsharedRequest, candidate?: unknown): {
5
+ id: string;
6
+ shouldSetCookie: boolean;
7
+ };
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.isPermanentDeviceId=isPermanentDeviceId,exports.extractPermanentDeviceId=extractPermanentDeviceId,exports.resolveOrCreatePermanentDeviceId=resolveOrCreatePermanentDeviceId;const crypto_1=require("crypto"),cookies_1=require("./cookies"),PERMANENT_DEVICE_ID_PATTERN=/^upid_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;function isPermanentDeviceId(e){return"string"==typeof e&&PERMANENT_DEVICE_ID_PATTERN.test(e)}function extractPermanentDeviceId(e){const r=(0,cookies_1.parseCookie)(e,"__unshared_device_id");if(isPermanentDeviceId(r))return r;const t=e.headers["x-permanent-device-id"];return isPermanentDeviceId(t)?t:void 0}function resolveOrCreatePermanentDeviceId(e,r){const t=(0,cookies_1.parseCookie)(e,"__unshared_device_id");if(isPermanentDeviceId(t))return{id:t,shouldSetCookie:!1};const i=e.headers["x-permanent-device-id"];return{id:isPermanentDeviceId(i)?i:isPermanentDeviceId(r)?r:`upid_${(0,crypto_1.randomUUID)()}`,shouldSetCookie:!0}}
@@ -0,0 +1,3 @@
1
+ import type { UnsharedRequest, UnsharedResponse } from '../../types';
2
+ /** Only call for SDK-owned routes, never for application requests. */
3
+ export declare function readJsonBody(req: UnsharedRequest, res: UnsharedResponse): Promise<boolean>;
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.readJsonBody=readJsonBody;const http_helpers_1=require("./http-helpers"),MAX_REQUEST_BYTES=1<<20;async function readJsonBody(e,t){const r=e,o=(e,o,s)=>{if(!r.destroyed&&!r.readableEnded&&r.resume&&r.once&&r.removeListener&&r.destroy){const e=setTimeout(()=>r.destroy(),3e4);e.unref();const t=()=>{clearTimeout(e),r.removeListener("end",t),r.removeListener("close",t),r.removeListener("error",t)};r.once("end",t),r.once("close",t),r.once("error",t),r.resume()}return(0,http_helpers_1.sendJson)(t,e,{success:!1,error:{code:o,message:s}}),!1},s=()=>o(413,"REQUEST_TOO_LARGE","Request body exceeds 1 MiB");if(void 0!==e.body)try{return!(Buffer.byteLength(JSON.stringify(e.body),"utf8")>MAX_REQUEST_BYTES)||s()}catch{return o(400,"INVALID_JSON","Request body is not valid JSON.")}return r.on&&r.removeListener&&r.pause?r.destroyed||r.readableEnded?o(400,"INVALID_JSON","Request body is unavailable or incomplete."):Number(e.headers["content-length"])>MAX_REQUEST_BYTES?s():e.headers["content-encoding"]&&"identity"!==e.headers["content-encoding"]?o(415,"UNSUPPORTED_ENCODING","Request body must use identity encoding."):new Promise(t=>{const s=[];let d=0,u=!1;const n=(e,d,n)=>{u||(u=!0,clearTimeout(_),r.removeListener("data",a),r.removeListener("end",c),r.removeListener("aborted",i),r.removeListener("error",i),r.removeListener("close",i),s.length=0,t(!e||o(e,d,n)))},i=()=>n(400,"INVALID_JSON","Request body is unavailable or incomplete."),a=e=>{const t=Buffer.isBuffer(e)?e:Buffer.from(e,"utf8");d+=t.length,d>MAX_REQUEST_BYTES?n(413,"REQUEST_TOO_LARGE","Request body exceeds 1 MiB"):s.push(t)},c=()=>{try{e.body=JSON.parse(Buffer.concat(s,d).toString("utf8"))}catch{return void n(400,"INVALID_JSON","Request body is not valid JSON.")}n()},_=setTimeout(()=>n(408,"REQUEST_TIMEOUT","Timed out reading request body."),3e4);_.unref(),r.on("data",a),r.on("end",c),r.on("aborted",i),r.on("error",i),r.on("close",i)}):o(400,"BODY_PARSER_MISSING","Provide a readable Node request stream or a parsed JSON body.")}
@@ -1,5 +1,9 @@
1
1
  /**
2
2
  * Returns true if the request path looks like a static asset and should be
3
3
  * skipped by the middleware (no fingerprint injection, no verdict checks).
4
+ *
5
+ * Content-bearing extensions (pdf/txt/xml/media) are only skipped when no
6
+ * enforcement mode is active — otherwise a flagged user could pull protected
7
+ * downloads/media straight through a `blockFlagged`/`flaggedMode` gate.
4
8
  */
5
- export declare function shouldSkipPath(path: string, customSkipPaths?: string[]): boolean;
9
+ export declare function shouldSkipPath(path: string, customSkipPaths?: string[], enforcementActive?: boolean): boolean;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.shouldSkipPath=shouldSkipPath;const STATIC_EXTENSIONS=new Set([".js",".mjs",".cjs",".css",".map",".png",".jpg",".jpeg",".gif",".svg",".ico",".webp",".avif",".woff",".woff2",".ttf",".otf",".eot",".mp3",".mp4",".webm",".ogg",".wasm",".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}
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.shouldSkipPath=shouldSkipPath;const STATIC_ASSET_EXTENSIONS=new Set([".js",".mjs",".cjs",".css",".map",".png",".jpg",".jpeg",".gif",".svg",".ico",".webp",".avif",".woff",".woff2",".ttf",".otf",".eot",".wasm"]),CONTENT_EXTENSIONS=new Set([".mp3",".mp4",".webm",".ogg",".xml",".txt",".pdf"]),STATIC_PATH_PREFIXES=["/static/","/assets/","/public/","/_next/","/__vite/","/favicon"];function shouldSkipPath(t,e,s=!1){if(e)for(const s of e)if(t.startsWith(s))return!0;const o=t.lastIndexOf(".");if(-1!==o){const e=t.slice(o).toLowerCase().split("?")[0];if(STATIC_ASSET_EXTENSIONS.has(e))return!0;if(!s&&CONTENT_EXTENSIONS.has(e))return!0}for(const e of STATIC_PATH_PREFIXES)if(t.startsWith(e))return!0;return!1}
@@ -13,6 +13,7 @@ export declare class VerdictCache {
13
13
  private readonly _maxSize;
14
14
  private _sweepTimer;
15
15
  constructor(defaultTTL?: number, maxSize?: number);
16
+ private _ensureSweepTimer;
16
17
  get(userId: string): Verdict | undefined;
17
18
  set(userId: string, verdict: Omit<Verdict, 'cachedAt' | 'ttl'>, ttl?: number): void;
18
19
  /**
@@ -42,6 +43,12 @@ export declare class VerdictCache {
42
43
  destroy(): void;
43
44
  /** Remove all entries that are past their TTL + a 2x grace period. */
44
45
  private _sweep;
45
- /** Evict the oldest entry by cachedAt to make room. */
46
+ /**
47
+ * Evict the oldest entry by last WRITE (set/update) — O(1). Map preserves
48
+ * insertion order, and both set() and update() re-append an existing key on
49
+ * write, so the first key in iteration order is the least-recently-written.
50
+ * Reads (get/isStale/isRefreshing) don't refresh recency — this is LRU-by-write,
51
+ * not LRU-by-read.
52
+ */
46
53
  private _evictOldest;
47
54
  }
@@ -1 +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;
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}_(){if(!this.o)try{this.o=setInterval(()=>this.p(),3e5),this.o&&"function"==typeof this.o.unref&&this.o.unref()}catch{this.o=null}}get(t){return this.i.get(t)}set(t,s,i){this._(),!this.i.has(t)&&this.i.size>=this.u?this.v():this.i.has(t)&&this.i.delete(t),this.i.set(t,{...s,cachedAt:Date.now(),ttl:i??this.l})}update(t,s){this._();const i=this.i.get(t);i?(void 0!==s.isFlagged&&(i.isFlagged=s.isFlagged),void 0!==s.isVerified&&(i.isVerified=s.isVerified),i.cachedAt=Date.now(),this.i.delete(t),this.i.set(t,i)):(this.i.size>=this.u&&this.v(),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()}p(){const t=Date.now();for(const[s,i]of this.i)t-i.cachedAt>2*i.ttl&&(this.i.delete(s),this.h.delete(s))}v(){const t=this.i.keys().next().value;void 0!==t&&(this.i.delete(t),this.h.delete(t))}}exports.VerdictCache=VerdictCache;
@@ -38,7 +38,6 @@ export interface MiddlewareOptions<TReq extends UnsharedRequest = UnsharedReques
38
38
  * @example
39
39
  * ```typescript
40
40
  * assertTrustProxy(app); // throws at startup if not set
41
- * app.use(express.json());
42
41
  * app.use(createUnsharedMiddleware(client, options));
43
42
  * ```
44
43
  */
@@ -51,8 +50,8 @@ export declare function assertTrustProxy(app: any): void;
51
50
  * Passes all other requests to next().
52
51
  *
53
52
  * **Prerequisites:**
54
- * - Mount `express.json()` (or equivalent body-parser) **before** this middleware,
55
- * otherwise `req.body` will be undefined and every request will return 400.
53
+ * - Existing default JSON parser order is supported by the browser's 100 KiB
54
+ * proxy limit. Unparsed SDK routes are also read automatically up to 1 MiB.
56
55
  * - For cross-origin frontends, pass `corsOrigins` instead of configuring CORS
57
56
  * separately — the middleware handles OPTIONS preflight automatically.
58
57
  * - `user_id` is automatically scrubbed from `req.body` after it is read, so
@@ -67,10 +66,10 @@ export declare function assertTrustProxy(app: any): void;
67
66
  * ```typescript
68
67
  * import { createUnsharedMiddleware } from "unshared-clientjs-sdk";
69
68
  *
70
- * app.use(express.json()); // must come first
71
69
  * app.use(createUnsharedMiddleware(client, {
72
70
  * userIdExtractor: (req) => req.user?.id,
73
71
  * }));
72
+ * app.use(express.json()); // application routes keep their own default limit
74
73
  * ```
75
74
  */
76
75
  export declare function createUnsharedMiddleware<TReq extends UnsharedRequest = UnsharedRequest>(client: UnsharedClient, options?: MiddlewareOptions<TReq>): (req: TReq, res: UnsharedResponse, next: UnsharedNextFunction) => Promise<void>;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.assertTrustProxy=assertTrustProxy,exports.createUnsharedMiddleware=createUnsharedMiddleware;const http_helpers_1=require("./middleware/utils/http-helpers");function assertTrustProxy(e){if(!e?.get?.("trust proxy"))throw new Error('[unshared-labs] Express "trust proxy" is not set.\n\n Fix: add this line before mounting any middleware:\n app.set("trust proxy", 1);\n\n Why: without it, req.ip returns the proxy/load-balancer IP instead of\n the real client IP, which degrades account-sharing detection accuracy.\n Set to 1 if behind a single reverse proxy (Nginx, ALB, CloudFront),\n or the number of trusted proxies in your chain.')}function createUnsharedMiddleware(e,r){const{userIdExtractor:s,eventTypeExtractor:t,sessionIdExtractor:o,ipAddressExtractor:n,defaultEventType:i="browser_event",routePrefix:d="/unshared",corsOrigins:c}=r??{},a=`${d}/submit-fingerprint-event`,h=c?Array.isArray(c)?c:[c]:null;return async(r,d,c)=>{const u=(0,http_helpers_1.getRequestPath)(r.url);if(h&&u===a){const e=r.headers.origin??"",s=h.includes("*");if((s||h.includes(e))&&(d.setHeader("Access-Control-Allow-Origin",s?"*":e),d.setHeader("Access-Control-Allow-Methods","POST, OPTIONS"),d.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id")),"OPTIONS"===r.method)return void(0,http_helpers_1.sendEmpty)(d,204)}if("POST"===r.method&&u===a)try{if(void 0===r.body)return void(0,http_helpers_1.sendJson)(d,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."}});const c=r.body??{};if(!c.hash||!c.stable_hash||!c.collected_at)return void(0,http_helpers_1.sendJson)(d,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Missing required fingerprint fields: hash, stable_hash, collected_at"}});if(!r.headers["x-session-id"])return void(0,http_helpers_1.sendJson)(d,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Missing required header: X-Session-Id"}});const a={full_hash:c.hash,fingerprint_id:c.stable_hash,timestamp:c.collected_at,isIncognito:c.is_incognito??!1,components:c.components??{},version:c.version??"unknown"};let h,u,l,p;try{h=(s?s(r):void 0)??c.user_id}catch{h=c.user_id}if(r.body&&"object"==typeof r.body&&"user_id"in r.body&&delete r.body.user_id,!h)return void(0,http_helpers_1.sendJson)(d,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Missing required field: user_id"}});try{u=(t?t(r):void 0)??c.event_type??i}catch{u=c.event_type??i}try{l=(o?o(r):void 0)??r.headers["x-session-id"]?.toString()??c.session_id}catch{l=r.headers["x-session-id"]?.toString()??c.session_id}const _=r.ip??r.socket?.remoteAddress??"";try{p=(n?n(r):void 0)??_}catch{p=_}const f=await e.submitFingerprintEvent(a,{userId:h,sessionHash:l,eventType:u,ipAddress:p});if(!f.success)return void(0,http_helpers_1.sendJson)(d,200,{success:!1,error:{code:"UPSTREAM_ERROR",message:f.error?.message??"Upstream request failed"}});(0,http_helpers_1.sendJson)(d,202,{success:!0,data:f.data})}catch(e){(0,http_helpers_1.sendJson)(d,200,{success:!1,error:{code:"MIDDLEWARE_ERROR",message:e instanceof Error?e.message:"Middleware error"}})}else c()}}
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.assertTrustProxy=assertTrustProxy,exports.createUnsharedMiddleware=createUnsharedMiddleware;const http_helpers_1=require("./middleware/utils/http-helpers"),read_json_body_1=require("./middleware/utils/read-json-body");function assertTrustProxy(e){if(!e?.get?.("trust proxy"))throw new Error('[unshared-labs] Express "trust proxy" is not set.\n\n Fix: add this line before mounting any middleware:\n app.set("trust proxy", 1);\n\n Why: without it, req.ip returns the proxy/load-balancer IP instead of\n the real client IP, which degrades account-sharing detection accuracy.\n Set to 1 if behind a single reverse proxy (Nginx, ALB, CloudFront),\n or the number of trusted proxies in your chain.')}function createUnsharedMiddleware(e,t){const{userIdExtractor:r,eventTypeExtractor:o,sessionIdExtractor:s,ipAddressExtractor:n,defaultEventType:i="browser_event",routePrefix:c="/unshared",corsOrigins:a}=t??{},d=`${c}/submit-fingerprint-event`,p=a?Array.isArray(a)?a:[a]:null;return async(t,c,a)=>{const y=(0,http_helpers_1.getRequestPath)(t.url);if(p&&y===d){const e=t.headers.origin??"",r=p.includes("*");if((r||p.includes(e))&&(c.setHeader("Access-Control-Allow-Origin",r?"*":e),c.setHeader("Access-Control-Allow-Methods","POST, OPTIONS"),c.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Permanent-Device-Id")),"OPTIONS"===t.method)return void(0,http_helpers_1.sendEmpty)(c,204)}if("POST"===t.method&&y===d)try{if(!await(0,read_json_body_1.readJsonBody)(t,c))return;const a=t.body??{};if(!a||"object"!=typeof a||Array.isArray(a))return void(0,http_helpers_1.sendJson)(c,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Request body must be a JSON object"}});const d=a.context&&"object"==typeof a.context?a.context:{},p=d.identity&&"object"==typeof d.identity?d.identity:{},y=d.identifiers&&"object"==typeof d.identifiers?d.identifiers:{},u=d.event&&"object"==typeof d.event?d.event:{};let h,l,f,_;try{h=(r?r(t):void 0)??p.user_id}catch{h=p.user_id}try{l=(o?o(t):void 0)??u.type??i}catch{l=u.type??i}try{f=(s?s(t):void 0)??t.headers["x-session-id"]?.toString()??y.session_hash}catch{f=t.headers["x-session-id"]?.toString()??y.session_hash}const b=t.ip??t.socket?.remoteAddress??"";try{_=(n?n(t):void 0)??b}catch{_=b}const x=t.headers["x-permanent-device-id"]?.toString()??y.unshared_device_id,A=d.sdk&&"object"==typeof d.sdk?d.sdk.name:void 0,j="string"==typeof A&&A.startsWith("unshared-")&&a.fingerprint&&"object"==typeof a.fingerprint&&a.context&&"object"==typeof a.context&&!Array.isArray(a.context)&&a.context.identity&&"object"==typeof a.context.identity&&!Array.isArray(a.context.identity)&&a.context.identifiers&&"object"==typeof a.context.identifiers&&!Array.isArray(a.context.identifiers)&&a.context.event&&"object"==typeof a.context.event&&!Array.isArray(a.context.event),v=!Object.prototype.hasOwnProperty.call(a,"context")&&"string"==typeof a.hash&&"string"==typeof a.stable_hash&&"string"==typeof a.collected_at&&a.components&&"object"==typeof a.components&&!Array.isArray(a.components),g=j||v?{...a,context:{...d,identity:{...p,...h?{user_id:h}:{}},identifiers:{...y,...f?{session_hash:f}:{},...x?{unshared_device_id:x}:{}},event:{...u,type:l,..._?{ip_address:_}:{},user_agent:t.headers["user-agent"]?.toString()??""}}}:a,w=await e.submitFingerprint(g,{idempotencyKey:t.headers["x-idempotency-key"]?.toString()});if(!w.success)return void(0,http_helpers_1.sendJson)(c,200,{success:!1,error:w.error??{code:"UPSTREAM_ERROR",message:"Upstream request failed"}});(0,http_helpers_1.sendJson)(c,202,{success:!0,data:w.data})}catch(e){(0,http_helpers_1.sendJson)(c,200,{success:!1,error:{code:"MIDDLEWARE_ERROR",message:e instanceof Error?e.message:"Middleware error"}})}else a()}}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Canonical wire-format for fingerprint data transmitted from the browser SDK to the backend.
3
+ *
4
+ * Single source of truth shared by:
5
+ * - @unshared-labs/frontend-fingerprint (serializer: _buildBody in browser.ts)
6
+ * - @unshared-labs/sdk (submitFingerprintEvent in client.ts)
7
+ *
8
+ * SCHEMA AUTHORITY: The browser SDK serializer is the writer. The Node.js SDK is the
9
+ * reader. Any field added here must be produced by the browser SDK and consumed by the
10
+ * server SDK. Do not add fields that only one side uses.
11
+ */
12
+ export type JSONPrimitive = string | number | boolean | null;
13
+ export type JSONValue = JSONPrimitive | JSONObject | JSONValue[];
14
+ export interface JSONObject {
15
+ [key: string]: JSONValue;
16
+ }
17
+
18
+ export interface BrowserStorageEntry extends JSONObject {
19
+ name: string;
20
+ value: string;
21
+ }
22
+
23
+ export interface BrowserStorageSnapshot extends JSONObject {
24
+ cookies: BrowserStorageEntry[];
25
+ local_storage: BrowserStorageEntry[];
26
+ session_storage: BrowserStorageEntry[];
27
+ }
28
+
29
+ export interface EncryptedEventEnvelope {
30
+ encrypted_data: string;
31
+ }
32
+
33
+ export interface FingerprintEventContext extends JSONObject {
34
+ sdk: JSONObject;
35
+ identity: JSONObject;
36
+ identifiers: JSONObject;
37
+ event: JSONObject;
38
+ browser_storage?: BrowserStorageSnapshot;
39
+ }
40
+
41
+ export type V3Identifiers = Record<string, string>;
42
+
43
+ export interface V3Success<T> {
44
+ success: true;
45
+ data: T;
46
+ }
47
+
48
+ export interface V3Error {
49
+ success: false;
50
+ error: {
51
+ code: string;
52
+ message: string;
53
+ retryAfter?: number;
54
+ nextAllowedAt?: string;
55
+ };
56
+ }
57
+
58
+ export type V3Result<T> = V3Success<T> | V3Error;
59
+
60
+ export interface IngestResult {
61
+ event_id: string;
62
+ collected_at: string;
63
+ endpoint_version: 'v3';
64
+ }
65
+
66
+ export interface TriggerEmailRequest {
67
+ email_address: string;
68
+ identifiers: V3Identifiers;
69
+ }
70
+
71
+ export interface TriggerEmailResult {
72
+ verification_id: string;
73
+ next_allowed_at: string;
74
+ retry_after_seconds: number;
75
+ }
76
+
77
+ export interface VerifyEmailRequest {
78
+ verification_id: string;
79
+ code: string;
80
+ }
81
+
82
+ export type VerifyEmailResult =
83
+ | { verified: true; verification_scope: 'email' }
84
+ | { verified: false; verification_scope: 'email'; reason: 'invalid_code' };
85
+
86
+ export interface CheckUserRequest {
87
+ user_id?: string;
88
+ email_address: string;
89
+ identifiers: V3Identifiers;
90
+ }
91
+
92
+ export type CheckDecisionCode =
93
+ | 'FLAGGED_RISK_MATCH'
94
+ | 'NO_ENFORCING_RISK'
95
+ | 'NO_IDENTITY_MATCH'
96
+ | 'IDENTITY_UNCERTAIN'
97
+ | 'CHECK_UNAVAILABLE';
98
+
99
+ export interface CheckUserResult {
100
+ is_user_flagged: boolean;
101
+ decision_id: string;
102
+ decision_code: CheckDecisionCode;
103
+ }
104
+
105
+ export interface FingerprintEventPayload extends JSONObject {
106
+ fingerprint: JSONObject;
107
+ context: FingerprintEventContext;
108
+ }
109
+
110
+ export interface FingerprintWireFormat {
111
+ [key: string]: unknown;
112
+ /**
113
+ * Primary device identifier — stable hash excluding volatile signals (audio, timing).
114
+ * Consistent across normal and incognito sessions on the same device.
115
+ * 32-character hex string.
116
+ */
117
+ fingerprint_id: string;
118
+ /**
119
+ * Full hash including all active signals (volatile signals included).
120
+ * Maximum entropy; may vary slightly between sessions on the same device.
121
+ * 32-character hex string.
122
+ */
123
+ full_hash: string;
124
+ /**
125
+ * Raw browser signal data keyed by signal name (e.g. `webgl`, `hardware`, `audio`).
126
+ *
127
+ * This blob is forwarded as-is in the `components` JSON field and stored verbatim
128
+ * in a `components JSONB` column on `fingerprint_events`. No server-side decomposition
129
+ * is performed; the full component tree is preserved for querying.
130
+ */
131
+ components: Record<string, unknown>;
132
+ /** ISO timestamp when the fingerprint was collected. */
133
+ timestamp: string;
134
+ /** Whether private/incognito browsing was detected at collection time. */
135
+ isIncognito: boolean;
136
+ /** Fingerprint library semver, e.g. "1.0.0". */
137
+ version: string;
138
+ /**
139
+ * Where the fingerprint agent that produced this result was loaded from:
140
+ * - `'remote'` — the runtime-loaded UMD (S3 directly in browser direct mode,
141
+ * or the Node SDK's same-origin proxy of S3).
142
+ * - `'bundled'` — the copy compiled into the SDK, used as the fallback when
143
+ * the remote load failed/timed out.
144
+ * Optional: older SDK artifacts and the mobile SDKs omit it, so the reader
145
+ * must tolerate its absence. Observability for S3 fast-path vs fallback rates.
146
+ */
147
+ source?: 'remote' | 'bundled';
148
+ }
@@ -1,4 +1,6 @@
1
1
  import type { UnsharedClient } from '../client';
2
+ import { VerdictCache } from '../middleware/verdict-cache';
3
+ import type { Verdict } from '../middleware/verdict-cache';
2
4
  import type { WebMiddleware, WebProtectionConfig } from './types';
3
5
  /**
4
6
  * Web Standard equivalent of `unsharedBoundToUser` from src/middleware/index.ts.
@@ -11,7 +13,15 @@ import type { WebMiddleware, WebProtectionConfig } from './types';
11
13
  * handler calls `next(request)` to get the downstream Response, transforms
12
14
  * it (injects the fingerprint script into HTML), and returns a new Response.
13
15
  *
14
- * @example Next.js App Router (in middleware.ts)
16
+ * NOTE: In Next.js middleware, `next(request)` is typically `() => NextResponse.next()`,
17
+ * which is a passthrough with a null body and no Content-Type — there is no HTML to
18
+ * inject a script into. This middleware still resolves the verdict and enforces it
19
+ * (gate/overlay/403), but fingerprint COLLECTION must be done by the browser/React SDK
20
+ * mounted in your app, not by injection here. See the Next.js section of the
21
+ * integration guide.
22
+ *
23
+ * @example Next.js App Router (in middleware.ts) — verdict + enforcement only;
24
+ * mount the browser/React SDK client-side to collect fingerprints.
15
25
  * ```typescript
16
26
  * import { createWebProtectionMiddleware } from 'unshared-clientjs-sdk/web';
17
27
  *
@@ -26,3 +36,21 @@ import type { WebMiddleware, WebProtectionConfig } from './types';
26
36
  * ```
27
37
  */
28
38
  export declare function createWebProtectionMiddleware(client: UnsharedClient, config: WebProtectionConfig): WebMiddleware;
39
+ /** Mutable holder for the one-time passthrough-injection warning (see below). */
40
+ interface InjectWarnState {
41
+ emitted: boolean;
42
+ }
43
+ /**
44
+ * Read the response body, inject the fingerprint script into HTML responses,
45
+ * merge Set-Cookie headers, and return a new Response. For non-HTML responses,
46
+ * only the cookies and cache-control override are applied.
47
+ *
48
+ * `warnState` scopes the one-time "passthrough, nothing injected" warning to a
49
+ * single middleware instance; when omitted, the module-level default is used.
50
+ */
51
+ declare function injectIntoHtmlResponse(response: Response, fingerprintScript: string, extraCookies?: string[], warnState?: InjectWarnState): Promise<Response>;
52
+ declare function fetchAndCacheVerdict(client: UnsharedClient, cache: VerdictCache, userId: string, emailAddress: string, deviceId: string, fingerprintId: string | undefined, permanentDeviceId: string | undefined, sessionId: string, timeoutMs?: number, reportFailOpen?: (reason: 'timeout' | 'http_error' | 'no_device_id', status?: number) => void): Promise<Verdict>;
53
+ export declare const __test_fetchAndCacheVerdict: typeof fetchAndCacheVerdict;
54
+ export declare const __test_injectIntoHtmlResponse: typeof injectIntoHtmlResponse;
55
+ export declare function __test_resetInjectWarning(): void;
56
+ export {};
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.createWebProtectionMiddleware=createWebProtectionMiddleware;const util_1=require("../util"),verdict_cache_1=require("../middleware/verdict-cache"),rate_limit_backoff_1=require("../middleware/rate-limit-backoff"),dispatch_dedupe_1=require("../middleware/dispatch-dedupe"),fingerprint_script_1=require("../middleware/injection/fingerprint-script"),gate_page_1=require("../middleware/injection/gate-page"),flagged_response_1=require("../middleware/utils/flagged-response"),mask_email_1=require("../middleware/utils/mask-email"),content_type_1=require("../middleware/utils/content-type"),skip_paths_1=require("../middleware/utils/skip-paths"),include_path_1=require("../middleware/utils/include-path"),is_bot_1=require("../middleware/utils/is-bot"),sentinel_user_id_1=require("../middleware/utils/sentinel-user-id"),web_helpers_1=require("./web-helpers"),CHECK_USER_TIMEOUT_MS=1500,FAIL_OPEN_TTL_MS=5e3;function createWebProtectionMiddleware(e,r){if(!r.userId)throw new Error("[Unshared] userId resolver is required");const{userId:t,emailAddress:s,routePrefix:i="/__unshared",corsOrigins:n,cacheTTL:a=6e4,skipPaths:o,includePathPrefix:d,sessionId:_,deviceId:c,fingerprintSdkBundle:l="",onFlagged:u,onError:p,onFailOpen:h,disableBotFilter:f=!1,checkUserTimeoutMs:m=CHECK_USER_TIMEOUT_MS,blockFlagged:w=!1,flaggedMode:g,autoInterstitial:v=!1,interstitialFlowType:I="email_verification"}=r,b=g??(w?"gate":void 0),y=v||"overlay"===b,A=e=>{if(h)try{h(e)}catch{}},S=Date.now().toString(36),E=void 0!==b||y,R="gate"===b?"flaggedMode 'gate' (blockFlagged)":"overlay"===b?"flaggedMode 'overlay'":"autoInterstitial";if(E&&!l)throw new Error(`[Unshared] ${R} requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the interstitial).`);if(E&&"/__unshared"!==i)throw new Error(`[Unshared] ${R} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const T="gate"===b?(0,gate_page_1.generateGatePage)(i,S):"",k=new verdict_cache_1.VerdictCache(a),x=new rate_limit_backoff_1.RateLimitBackoff,C=new dispatch_dedupe_1.DispatchDedupe,U=(0,fingerprint_script_1.generateFingerprintScript)(i,S,{autoInterstitial:y,interstitialFlowType:I}),O=`${i}/fp.js`,$=`${i}/submit-fp`,F=`${i}/verify-trigger`,P=`${i}/verify`,L=`${i}/status`,q=n?Array.isArray(n)?n:[n]:null;return async function(r,n){let a,h,w;try{const e=new URL(r.url);a=e.pathname,h=e.search}catch{return n(r)}if(a.startsWith(i+"/")){const i=function(e){if(!q)return{};const r=e.headers.get("origin")??"",t=q.includes("*");if(!t&&!q.includes(r))return{};const s={"Access-Control-Allow-Origin":t?"*":r,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"};return t||(s["Access-Control-Allow-Credentials"]="true"),s}(r);if("OPTIONS"===r.method)return(0,web_helpers_1.emptyResponse)(204,i);if(("GET"===r.method||"HEAD"===r.method)&&a===O){if(!l)return(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},i);const e={...i,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600","Content-Length":String((new TextEncoder).encode(l).byteLength)};return"HEAD"===r.method?new Response(null,{status:200,headers:new Headers(e)}):(0,web_helpers_1.bodyResponse)(200,l,e)}if("POST"===r.method&&(a===$||a===F||a===P)){let n;try{n=await r.json()}catch{return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"Request body is not valid JSON."}},i)}return a===$?handleSubmitFp(r,n,{client:e,verdictCache:k,rateLimitBackoff:x,dispatchDedupe:C,resolveUserId:t,resolveEmailAddress:s,resolveSessionId:_,resolveDeviceId:c,disableBotFilter:f,onError:p},i):a===F?handleVerifyTriggerWeb(r,n,{client:e,verdictCache:k,resolveUserId:t,resolveEmailAddress:s,resolveDeviceId:c,onError:p},i):handleVerifyWeb(r,n,{client:e,verdictCache:k,resolveUserId:t,resolveEmailAddress:s,resolveDeviceId:c,onError:p},i)}if("GET"===r.method&&a===L){let n;try{n=t(r)}catch{}if(!n)return(0,web_helpers_1.jsonResponse)(200,{status:"anonymous"},i);const a=resolveEmail(r,s);let o=k.get(n);if((!o||k.isStale(n))&&a&&!x.isPaused()&&!k.isRefreshing(n)){k.markRefreshing(n);try{const t=(0,web_helpers_1.extractDeviceIdFromRequest)(r,c),s=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_fingerprint_id")||void 0,i=extractSessionIdFromRequest(r,_),d=t??s??"unknown";await fetchAndCacheVerdict(e,k,n,a,d,s,i,m,(e,r)=>A({operation:"checkUser",reason:e,status:r,userId:n,emailAddress:a})),o=k.get(n)}catch(e){p&&p(e,{operation:"checkUser",userId:n,emailAddress:a}),A({operation:"checkUser",reason:"exception",userId:n,emailAddress:a})}finally{k.clearRefreshing(n)}}return o&&o.isFlagged&&!o.isVerified&&(void 0!==b||u)&&a?(0,web_helpers_1.jsonResponse)(200,{status:"flagged",email:(0,mask_email_1.maskEmail)(a)},i):(0,web_helpers_1.jsonResponse)(200,{status:"ok"},i)}return(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},i)}if((0,skip_paths_1.shouldSkipPath)(a,o))return n(r);if(!(0,include_path_1.shouldIncludePath)(a,d))return injectIntoHtmlResponse(await n(r),U);try{w=t(r)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(w)){const e=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_uid"),t=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_uid_at"),s=t?Number(t):NaN,i=Number.isFinite(s)&&Date.now()-s<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;w=e&&i?e:void 0}if(!w){const e=(0,web_helpers_1.isSecureWebRequest)(r)?"; Secure":"",t=[`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`];return injectIntoHtmlResponse(await n(r),U,t)}const g=resolveEmail(r,s),v=[],I=(0,web_helpers_1.isSecureWebRequest)(r)?"; Secure":"";if(v.push(`__unshared_uid=${encodeURIComponent(w)}; Path=/; SameSite=Lax${I}`),v.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${I}`),g&&v.push(`__unshared_email=${encodeURIComponent(g)}; HttpOnly; Path=/; SameSite=Lax${I}`),!g)return injectIntoHtmlResponse(await n(r),U,v);const y=extractSessionIdFromRequest(r,_),S=(0,web_helpers_1.extractDeviceIdFromRequest)(r,c),E=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_fingerprint_id")||void 0,R=r.headers.get("user-agent")??"",D=(0,web_helpers_1.extractClientIpFromRequest)(r),M=S??E;if(!f&&(0,is_bot_1.isBot)(R))return n(r);let H=k.get(w);if(H)k.isStale(w)&&!k.isRefreshing(w)&&(k.markRefreshing(w),fetchAndCacheVerdict(e,k,w,g,M??"unknown",E,y,m,(e,r)=>A({operation:"checkUser",reason:e,status:r,userId:w,emailAddress:g})).catch(()=>A({operation:"checkUser",reason:"exception",userId:w,emailAddress:g})).finally(()=>k.clearRefreshing(w)));else try{H=await fetchAndCacheVerdict(e,k,w,g,M??"unknown",E,y,m,(e,r)=>A({operation:"checkUser",reason:e,status:r,userId:w,emailAddress:g}))}catch{return A({operation:"checkUser",reason:"exception",userId:w,emailAddress:g}),injectIntoHtmlResponse(await n(r),U,v)}const N=H.isFlagged&&!H.isVerified;if("gate"===b&&N)return(0,content_type_1.isHtmlNavigation)(r.method,r.headers.get("accept")??void 0)?(0,web_helpers_1.bodyResponse)(200,T,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}):(0,web_helpers_1.jsonResponse)(403,(0,flagged_response_1.flaggedResponse)(g));if("overlay"===b&&N)return(0,content_type_1.isHtmlNavigation)(r.method,r.headers.get("accept")??void 0)?injectIntoHtmlResponse(await n(r),U,v):(0,web_helpers_1.jsonResponse)(403,(0,flagged_response_1.flaggedResponse)(g));if(H.isFlagged&&!H.isVerified&&u)try{const e=await u({userId:w,emailAddress:g,verdict:H,request:r});if(e)return injectIntoHtmlResponse(e,U,v)}catch(e){p&&p(e,{operation:"checkUser",userId:w,emailAddress:g})}return H.isFlagged||"unknown"===y||!M||x.isPaused()||dispatchUserEvent(e,k,x,C,{userId:w,emailAddress:g,sessionId:y,deviceId:M,fingerprintId:E,userAgent:R,ipAddress:D,eventType:a+h},p),injectIntoHtmlResponse(await n(r),U,v)}}async function injectIntoHtmlResponse(e,r,t){const s=e.headers.get("content-type");if(!(0,content_type_1.isHtmlContentType)(s??void 0)){if(!t||0===t.length)return e;const r=(0,web_helpers_1.mergeResponseHeaders)(e.headers,void 0,t);return new Response(e.body,{status:e.status,statusText:e.statusText,headers:r})}const i=await e.text(),n=i.lastIndexOf("</body>"),a=-1===n?i+r:i.slice(0,n)+r+i.slice(n),o=(0,web_helpers_1.mergeResponseHeaders)(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(a).length)},t);return o.delete("ETag"),o.delete("Last-Modified"),o.delete("Content-Encoding"),new Response(a,{status:e.status,statusText:e.statusText,headers:o})}function resolveEmail(e,r){if(r)try{const t=r(e);if(t)return t}catch{}const t=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email");if(t)return t}function resolveEmailWithBody(e,r,t){const s=resolveEmail(e,t);if(s)return s;const i=r.email;return"string"==typeof i&&i?i:void 0}function extractSessionIdFromRequest(e,r){if(r)try{const t=r(e);if(t)return t}catch{}return(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid")??"unknown"}function dispatchUserEvent(e,r,t,s,i,n){s.mark(i.userId,i.eventType),e.processUserEvent({eventType:i.eventType,userId:i.userId,emailAddress:i.emailAddress,ipAddress:i.ipAddress,deviceId:i.deviceId,fingerprintId:i.fingerprintId,sessionHash:i.sessionId,userAgent:i.userAgent}).then(e=>{e.success&&e.data?.analysis&&r.update(i.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.pause(1e3*e.error.retryAfter)}).catch(e=>{n&&n(e,{operation:"processUserEvent",userId:i.userId,emailAddress:i.emailAddress})})}async function fetchAndCacheVerdict(e,r,t,s,i,n,a,o=CHECK_USER_TIMEOUT_MS,d){const _=()=>{const e=r.get(t);return r.set(t,{isFlagged:e?.isFlagged??!1,isVerified:e?.isVerified??!1,emailAddress:s,sessionId:a},5e3),r.get(t)},c={};let l;i&&"unknown"!==i&&(c.deviceId=i),n&&(c.fingerprintId=n);const u=await Promise.race([e.checkUser(s,c),new Promise(e=>{l=setTimeout(()=>e(null),o)})]);return clearTimeout(l),u?u.failedOpen?(d?.(u.failedOpen.reason??"http_error",u.failedOpen.status),_()):(r.set(t,{isFlagged:u.data?.is_user_flagged??!1,isVerified:!1,emailAddress:s,sessionId:a}),r.get(t)):(d?.("timeout"),_())}async function handleSubmitFp(e,r,t,s){try{const i={full_hash:r.hash??"",fingerprint_id:r.stable_hash??"",timestamp:r.collected_at??(new Date).toISOString(),isIncognito:r.is_incognito??!1,components:r.components??{},version:r.version??"inline-1.0.0"};let n,a,o;try{const r=t.resolveUserId(e);r&&!(0,sentinel_user_id_1.isSentinelUserId)(r)&&(n=r)}catch{}if(!n){const e="string"==typeof r.user_id?r.user_id:void 0;e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(n=e)}if(!n){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_uid");r&&!(0,sentinel_user_id_1.isSentinelUserId)(r)&&(n=r)}try{a=t.resolveEmailAddress?t.resolveEmailAddress(e):void 0}catch{}a=a??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")??r.email??void 0;try{o=t.resolveSessionId?t.resolveSessionId(e):void 0}catch{}o=o??r.session_id??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid");const d=(0,web_helpers_1.extractClientIpFromRequest)(e),_=e.headers.get("user-agent")??"";if(!t.disableBotFilter&&(0,is_bot_1.isBot)(_))return(0,web_helpers_1.jsonResponse)(200,{success:!0},s);const c=(i.fingerprint_id&&i.fingerprint_id.length>0?i.fingerprint_id:void 0)??(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),l=i.fingerprint_id||void 0,u=i.full_hash||void 0,p=(0,web_helpers_1.isSecureWebRequest)(e)?"; Secure":"",h=[];if(u&&!(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")&&h.push(`__unshared_fingerprint_id=${encodeURIComponent(u)}; HttpOnly; Path=/; SameSite=Lax${p}`),l){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fp_id");r&&r===l||h.push(`__unshared_fp_id=${encodeURIComponent(l)}; Path=/; SameSite=Lax; Max-Age=31536000${p}`)}let f;if(a&&!(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")&&h.push(`__unshared_email=${encodeURIComponent(a)}; HttpOnly; Path=/; SameSite=Lax${p}`),"string"==typeof r.event_type&&r.event_type)f=r.event_type;else{const r=e.headers.get("referer")??e.headers.get("referrer");let t="unknown";if(r)try{const e=new URL(r);t=(e.pathname||"/")+(e.search||"")}catch{}f=t}const m=e.headers.get("x-idempotency-key")||void 0,w=Date.now(),g=m?`${m}|${w}`:l&&n?`${(0,util_1.sha256Hex)(`${l}|${n}|${f}`)}|${w}`:void 0;n&&t.client.submitFingerprintEvent(i,{userId:n,emailAddress:a,sessionHash:o,eventType:f,ipAddress:d,userAgent:_,idempotencyKey:g}).catch(e=>{t.onError&&t.onError(e,{operation:"submitFingerprintEvent",userId:n,emailAddress:a})}),n&&a&&!t.rateLimitBackoff.isPaused()&&!t.dispatchDedupe.wasRecentlyDispatched(n,f)&&t.client.processUserEvent({eventType:f,userId:n,emailAddress:a,ipAddress:d,deviceId:c,fingerprintId:l,sessionHash:o??"unknown",userAgent:_}).then(e=>{e.success&&e.data?.analysis&&t.verdictCache.update(n,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&t.rateLimitBackoff.pause(1e3*e.error.retryAfter)}).catch(e=>{t.onError&&t.onError(e,{operation:"processUserEvent",userId:n,emailAddress:a})});const v={...s,"Content-Type":"application/json"},I=new Response(JSON.stringify({success:!0}),{status:200,headers:v});for(const e of h)I.headers.append("Set-Cookie",e);return I}catch{return(0,web_helpers_1.jsonResponse)(200,{success:!0},s)}}async function handleVerifyTriggerWeb(e,r,t,s){try{const i=resolveEmailWithBody(e,r??{},t.resolveEmailAddress);if(!i)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},s);const n=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),a=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,o=await t.client.triggerEmailVerification(i,n,{fingerprintId:a});return o.success?(0,web_helpers_1.jsonResponse)(200,{success:!0,data:o.data},s):(0,web_helpers_1.jsonResponse)(200,{success:!1,error:o.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},s)}catch(e){return t.onError&&t.onError(e,{operation:"verifyTrigger"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},s)}}async function handleVerifyWeb(e,r,t,s){try{const i=resolveEmailWithBody(e,r??{},t.resolveEmailAddress),n=r?.code;if(!i||!n)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}},s);const a=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,t.resolveDeviceId),o=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,d=await t.client.verify(i,a,n,{fingerprintId:o});if(d.success){let r;try{r=t.resolveUserId(e)}catch{}return r&&t.verdictCache.update(r,{isVerified:!0}),(0,web_helpers_1.jsonResponse)(200,{success:!0,data:{verified:!0}},s)}return(0,web_helpers_1.jsonResponse)(200,{success:!1,error:d.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}},s)}catch(e){return t.onError&&t.onError(e,{operation:"verify"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},s)}}
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.__test_injectIntoHtmlResponse=exports.__test_fetchAndCacheVerdict=void 0,exports.createWebProtectionMiddleware=createWebProtectionMiddleware,exports.__test_resetInjectWarning=__test_resetInjectWarning;const webcrypto_1=require("../webcrypto"),verdict_cache_1=require("../middleware/verdict-cache"),rate_limit_backoff_1=require("../middleware/rate-limit-backoff"),dispatch_dedupe_1=require("../middleware/dispatch-dedupe"),fingerprint_script_1=require("../middleware/injection/fingerprint-script"),gate_page_1=require("../middleware/injection/gate-page"),flagged_response_1=require("../middleware/utils/flagged-response"),mask_email_1=require("../middleware/utils/mask-email"),content_type_1=require("../middleware/utils/content-type"),skip_paths_1=require("../middleware/utils/skip-paths"),include_path_1=require("../middleware/utils/include-path"),is_bot_1=require("../middleware/utils/is-bot"),sentinel_user_id_1=require("../middleware/utils/sentinel-user-id"),dispatch_processing_1=require("../middleware/dispatch-processing"),web_helpers_1=require("./web-helpers"),CHECK_USER_TIMEOUT_MS=1500,FAIL_OPEN_TTL_MS=5e3;function createWebProtectionMiddleware(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");const{userId:r,emailAddress:s,routePrefix:n="/__unshared",corsOrigins:i,cacheTTL:a=6e4,skipPaths:o,includePathPrefix:d,sessionId:c,deviceId:_,fingerprintSdkBundle:l="",onFlagged:u,onError:p,onFailOpen:h,disableBotFilter:f=!1,checkUserTimeoutMs:m=CHECK_USER_TIMEOUT_MS,blockFlagged:g=!1,flaggedMode:w,autoInterstitial:b=!1,interstitialFlowType:v="email_verification"}=t,y=w??(g?"gate":void 0),I=b||"overlay"===y,x=e=>{if(h)try{h(e)}catch{}},S=Date.now().toString(36),A=void 0!==y||I,R="gate"===y?"flaggedMode 'gate' (blockFlagged)":"overlay"===y?"flaggedMode 'overlay'":"autoInterstitial";if(A&&!l)throw new Error(`[Unshared] ${R} requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the interstitial).`);if(A&&"/__unshared"!==n)throw new Error(`[Unshared] ${R} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const E="gate"===y?(0,gate_page_1.generateGatePage)(n,S):"",T=new verdict_cache_1.VerdictCache(a),k=new Map,C=new rate_limit_backoff_1.RateLimitBackoff,P=new dispatch_dedupe_1.DispatchDedupe,U=(0,fingerprint_script_1.generateFingerprintScript)(n,S,{autoInterstitial:I,interstitialFlowType:v}),$={emitted:!1},j=`${n}/fp.js`,D=`${n}/submit-fp`,O=`${n}/verify-trigger`,M=`${n}/verify`,F=`${n}/status`,q=i?Array.isArray(i)?i:[i]:null;return async function(t,i,a){const h=e=>{a?.waitUntil&&a.waitUntil(e)};let g,w;try{const e=new URL(t.url);g=e.pathname,w=e.search}catch{return i(t)}if(g.startsWith(n+"/")){const n=function(e){if(!q)return{};const t=e.headers.get("origin")??"",r=q.includes("*");if(!r&&!q.includes(t))return{};const s={"Access-Control-Allow-Origin":r?"*":t,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id, X-Permanent-Device-Id","Access-Control-Expose-Headers":"X-Unshared-Enforcement-Reconciled"};return r||(s["Access-Control-Allow-Credentials"]="true"),s}(t);if("OPTIONS"===t.method)return(0,web_helpers_1.emptyResponse)(204,n);if(("GET"===t.method||"HEAD"===t.method)&&g===j){if(!l)return(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},n);const e={...n,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600","Content-Length":String((new TextEncoder).encode(l).byteLength)};return"HEAD"===t.method?new Response(null,{status:200,headers:new Headers(e)}):(0,web_helpers_1.bodyResponse)(200,l,e)}if("POST"===t.method&&(g===D||g===O||g===M)){const i=await(0,web_helpers_1.readJsonRequest)(t);if(!i.success)return(0,web_helpers_1.jsonResponse)(i.status,{success:!1,error:i.error},n);const a=i.data;return g===D?handleSubmitFp(t,a,{client:e,verdictCache:T,rateLimitBackoff:C,dispatchDedupe:P,resolveUserId:r,resolveEmailAddress:s,resolveSessionId:c,resolveDeviceId:_,disableBotFilter:f,onError:p,registerBackground:h},n):g===O?handleVerifyTriggerWeb(t,a,{client:e,verdictCache:T,resolveUserId:r,resolveEmailAddress:s,resolveDeviceId:_,onError:p},n):handleVerifyWeb(t,a,{client:e,verdictCache:T,resolveUserId:r,resolveEmailAddress:s,resolveDeviceId:_,onError:p},n)}if("GET"===t.method&&g===F){let i;try{i=r(t)}catch{}if(!i)return(0,web_helpers_1.jsonResponse)(200,{status:"anonymous"},n);const a=resolveEmail(t,s);let o=T.get(i);if((!o||T.isStale(i))&&a&&!C.isPaused()&&!T.isRefreshing(i)){T.markRefreshing(i);try{const r=(0,web_helpers_1.extractDeviceIdFromRequest)(t,_),s=(0,web_helpers_1.parseCookieFromRequest)(t,"__unshared_fingerprint_id")||void 0,n=extractPermanentDeviceIdFromRequest(t),d=extractSessionIdFromRequest(t,c),l=r??s??"unknown";await fetchAndCacheVerdict(e,T,i,a,l,s,n,d,m,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:i,emailAddress:a})),o=T.get(i)}catch(e){p&&p(e,{operation:"checkUser",userId:i,emailAddress:a}),x({operation:"checkUser",reason:"exception",userId:i,emailAddress:a})}finally{T.clearRefreshing(i)}}return o&&o.isFlagged&&!o.isVerified&&(void 0!==y||u)&&a?(0,web_helpers_1.jsonResponse)(200,{status:"flagged",email:(0,mask_email_1.maskEmail)(a)},n):(0,web_helpers_1.jsonResponse)(200,{status:"ok"},n)}return(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},n)}const b=void 0!==y;if((0,skip_paths_1.shouldSkipPath)(g,o,b))return i(t);if(!(0,include_path_1.shouldIncludePath)(g,d))return injectIntoHtmlResponse(await i(t),U,void 0,$);let v;try{v=r(t)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(v)){const e=(0,web_helpers_1.parseCookieFromRequest)(t,"__unshared_uid"),r=(0,web_helpers_1.parseCookieFromRequest)(t,"__unshared_uid_at"),s=r?Number(r):NaN,n=Number.isFinite(s)&&Date.now()-s<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;v=e&&n?e:void 0}if(!v){const e=(0,web_helpers_1.isSecureWebRequest)(t)?"; Secure":"",r=[`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_verification_id=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0${e}`];return injectIntoHtmlResponse(await i(t),U,r,$)}const I=resolveEmail(t,s),S=[],A=(0,web_helpers_1.isSecureWebRequest)(t)?"; Secure":"";if(S.push(`__unshared_uid=${encodeURIComponent(v)}; Path=/; SameSite=Lax; Max-Age=31536000${A}`),S.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax; Max-Age=31536000${A}`),I&&S.push(`__unshared_email=${encodeURIComponent(I)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=31536000${A}`),!I)return injectIntoHtmlResponse(await i(t),U,S,$);const R=extractSessionIdFromRequest(t,c),L=(0,web_helpers_1.extractDeviceIdFromRequest)(t,_),H=(0,web_helpers_1.parseCookieFromRequest)(t,"__unshared_fingerprint_id")||void 0,N=extractPermanentDeviceIdFromRequest(t),V=t.headers.get("user-agent")??"",W=(0,web_helpers_1.extractClientIpFromRequest)(t),B=L??H;if(!f&&(0,is_bot_1.isBot)(V))return i(t);let K=T.get(v);if(K)T.isStale(v)&&!T.isRefreshing(v)&&(T.markRefreshing(v),h(fetchAndCacheVerdict(e,T,v,I,B??"unknown",H,N,R,m,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:I})).catch(()=>x({operation:"checkUser",reason:"exception",userId:v,emailAddress:I})).finally(()=>T.clearRefreshing(v))));else try{let t=k.get(v);t||(t=fetchAndCacheVerdict(e,T,v,I,B??"unknown",H,N,R,m,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:v,emailAddress:I})).finally(()=>k.delete(v)),k.set(v,t)),K=await t}catch{return x({operation:"checkUser",reason:"exception",userId:v,emailAddress:I}),injectIntoHtmlResponse(await i(t),U,S,$)}const X=K.isFlagged&&!K.isVerified;if("gate"===y&&X)return(0,content_type_1.isHtmlNavigation)(t.method,t.headers.get("accept")??void 0)?(0,web_helpers_1.bodyResponse)(200,E,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}):(0,web_helpers_1.jsonResponse)(403,(0,flagged_response_1.flaggedResponse)(I));if("overlay"===y&&X)return(0,content_type_1.isHtmlNavigation)(t.method,t.headers.get("accept")??void 0)?injectIntoHtmlResponse(await i(t),U,S,$):(0,web_helpers_1.jsonResponse)(403,(0,flagged_response_1.flaggedResponse)(I));if(K.isFlagged&&!K.isVerified&&u)try{const e=await u({userId:v,emailAddress:I,verdict:K,request:t});if(e)return injectIntoHtmlResponse(e,U,S,$)}catch(e){p&&p(e,{operation:"checkUser",userId:v,emailAddress:I})}return K.isFlagged||"unknown"===R||!B||C.isPaused()||h(dispatchUserEvent(e,T,C,P,{userId:v,emailAddress:I,sessionId:R,deviceId:B,fingerprintId:H,permanentDeviceId:N,userAgent:V,ipAddress:W,eventType:g+w},p)),injectIntoHtmlResponse(await i(t),U,S,$)}}const defaultInjectWarnState={emitted:!1};function passthroughResponse(e,t,r){if(!t||0===t.length)return e;const s=(0,web_helpers_1.mergeResponseHeaders)(e.headers,void 0,t);return new Response(r,{status:e.status,statusText:e.statusText,headers:s})}async function injectIntoHtmlResponse(e,t,r,s=defaultInjectWarnState){const n=e.headers.get("content-type");if(204===e.status||304===e.status)return passthroughResponse(e,r,null);if(null===e.body)return n||s.emitted||(s.emitted=!0,console.warn("[Unshared] The response returned by your handler has no body (e.g. NextResponse.next()), so fingerprint injection did not run. In Next.js middleware, fingerprints must be collected by the browser/React SDK, while this middleware handles verdict + enforcement. See the Next.js section of the integration guide.")),passthroughResponse(e,r,null);if(!(0,content_type_1.isHtmlContentType)(n??void 0))return passthroughResponse(e,r,e.body);const i=await e.text(),a=i.lastIndexOf("</body>"),o=-1===a?i+t:i.slice(0,a)+t+i.slice(a),d=(0,web_helpers_1.mergeResponseHeaders)(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(o).length)},r);return d.delete("ETag"),d.delete("Last-Modified"),d.delete("Content-Encoding"),new Response(o,{status:e.status,statusText:e.statusText,headers:d})}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email");if(r)return r}function resolveEmailWithBody(e,t,r){const s=resolveEmail(e,r);if(s)return s;const n=t.email;return"string"==typeof n&&n?n:void 0}function extractSessionIdFromRequest(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid")??"unknown"}function isPermanentDeviceId(e){return"string"==typeof e&&/^upid_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(e)}function extractPermanentDeviceIdFromRequest(e){const t=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_device_id");if(isPermanentDeviceId(t))return t;const r=e.headers.get("x-permanent-device-id");return isPermanentDeviceId(r)?r:void 0}function dispatchUserEvent(e,t,r,s,n,i){return s.mark(n.userId,n.eventType),(0,dispatch_processing_1.runProcessUserEvent)({client:e,cache:t,backoff:r,onError:i},{eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,permanentDeviceId:n.permanentDeviceId,sessionHash:n.sessionId,userAgent:n.userAgent},dispatch_processing_1.DISPATCH_TIMEOUT_MS)}async function fetchAndCacheVerdict(e,t,r,s,n,i,a,o,d=CHECK_USER_TIMEOUT_MS,c){const _=()=>{const e=t.get(r);return t.set(r,{isFlagged:e?.isFlagged??!1,isVerified:e?.isVerified??!1,emailAddress:s,sessionId:o},5e3),t.get(r)},l={timeoutMs:d,maxRetries:0};let u;n&&"unknown"!==n&&(l.deviceId=n),i&&(l.fullHash=i),a&&(l.permanentDeviceId=a);const p=await Promise.race([e.checkUser(s,l),new Promise(e=>{u=setTimeout(()=>e(null),d)})]);return clearTimeout(u),p?p.failedOpen?(c?.(p.failedOpen.reason??"http_error",p.failedOpen.status),_()):(t.set(r,{isFlagged:p.data?.is_user_flagged??!1,isVerified:!1,emailAddress:s,sessionId:o}),t.get(r)):(c?.("timeout"),_())}async function handleSubmitFp(e,t,r,s){try{const n=t?.fingerprint&&"object"==typeof t.fingerprint?t.fingerprint:t??{},i=t?.context&&"object"==typeof t.context?t.context:{},a=i.identity&&"object"==typeof i.identity?i.identity:{},o=i.identifiers&&"object"==typeof i.identifiers?i.identifiers:{},d=i.event&&"object"==typeof i.event?i.event:{},c={full_hash:n.hash??n.full_hash??"",fingerprint_id:n.stableHash??n.stable_hash??n.fingerprint_id??""};let _,l,u;try{const t=r.resolveUserId(e);t&&!(0,sentinel_user_id_1.isSentinelUserId)(t)&&(_=t)}catch{}if(!_){const e="string"==typeof a.user_id?a.user_id:"string"==typeof t.user_id?t.user_id:void 0;e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(_=e)}if(!_){const t=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_uid");t&&!(0,sentinel_user_id_1.isSentinelUserId)(t)&&(_=t)}try{l=r.resolveEmailAddress?r.resolveEmailAddress(e):void 0}catch{}l=l??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")??a.email_address??t.email??void 0;try{u=r.resolveSessionId?r.resolveSessionId(e):void 0}catch{}u=u??o.session_hash??t.session_id??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid");const p=(0,web_helpers_1.extractClientIpFromRequest)(e),h=e.headers.get("user-agent")??"";if(!r.disableBotFilter&&(0,is_bot_1.isBot)(h))return(0,web_helpers_1.jsonResponse)(200,{success:!0,data:{skipped:!0}},s);const f=(c.fingerprint_id&&c.fingerprint_id.length>0?c.fingerprint_id:void 0)??(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,r.resolveDeviceId),m=c.fingerprint_id||void 0,g=c.full_hash||void 0,w=extractPermanentDeviceIdFromRequest(e),b=e.headers.get("x-permanent-device-id")??o.unshared_device_id??t.permanent_device_id,v=w??(isPermanentDeviceId(b)?b:`upid_${crypto.randomUUID()}`),y=(0,web_helpers_1.isSecureWebRequest)(e)?"; Secure":"",I=[];I.push(`__unshared_device_id=${encodeURIComponent(v)}; Path=/; SameSite=Lax; Max-Age=34560000${y}`);const x=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||g;let S;if(x&&I.push(`__unshared_fingerprint_id=${encodeURIComponent(x)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=31536000${y}`),m&&I.push(`__unshared_fp_id=${encodeURIComponent(m)}; Path=/; SameSite=Lax; Max-Age=31536000${y}`),l&&I.push(`__unshared_email=${encodeURIComponent(l)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=31536000${y}`),"string"==typeof d.type&&d.type)S=d.type;else if("string"==typeof t.event_type&&t.event_type)S=t.event_type;else{const t=e.headers.get("referer")??e.headers.get("referrer");let r="unknown";if(t)try{const e=new URL(t);r=(e.pathname||"/")+(e.search||"")}catch{}S=r}const A=e.headers.get("x-idempotency-key")||void 0,R=Date.now();let E;A?E=`${A}|${R}`:m&&_&&(E=`${await(0,webcrypto_1.sha256HexWeb)(`${m}|${_}|${S}`)}|${R}`);const T={...o,unshared_device_id:v,...f&&"unknown"!==f?{device_id:f}:{},...m?{stable_hash:m}:{},...g?{full_hash:g}:{},...u?{session_hash:u}:{}},k=i.sdk&&"object"==typeof i.sdk?i.sdk.name:void 0,C="string"==typeof k&&k.startsWith("unshared-")&&Object.prototype.hasOwnProperty.call(t,"fingerprint")&&t.context&&"object"==typeof t.context&&!Array.isArray(t.context)&&i.identity&&"object"==typeof i.identity&&!Array.isArray(i.identity)&&i.identifiers&&"object"==typeof i.identifiers&&!Array.isArray(i.identifiers)&&i.event&&"object"==typeof i.event&&!Array.isArray(i.event),P=!Object.prototype.hasOwnProperty.call(t,"context")&&"string"==typeof t.hash&&"string"==typeof t.stable_hash&&"string"==typeof t.collected_at&&t.components&&"object"==typeof t.components&&!Array.isArray(t.components),U=C||P?{...t,fingerprint:n,context:{...i,identity:{...a,..._?{user_id:_}:{},...l?{email_address:l}:{}},identifiers:T,event:{...d,type:S,ip_address:p,user_agent:h}}}:t,$=await r.client.submitFingerprint(U,{idempotencyKey:E});if($.success||r.onError?.($.error,{operation:"submitFingerprintEvent",userId:_,emailAddress:l}),_&&l&&!r.rateLimitBackoff.isPaused()&&!r.dispatchDedupe.wasRecentlyDispatched(_,S)){const e=(0,dispatch_processing_1.runCheckUser)({client:r.client,cache:r.verdictCache,backoff:r.rateLimitBackoff,onError:r.onError},{user_id:_,email_address:l,identifiers:T},_,l,dispatch_processing_1.SUBMIT_FP_TIMEOUT_MS);r.registerBackground?.(e)}const j={...s,"Content-Type":"application/json"},D=new Response(JSON.stringify($.success?{success:!0,data:$.data}:{success:!1,error:$.error}),{status:200,headers:j});for(const e of I)D.headers.append("Set-Cookie",e);return D}catch{return(0,web_helpers_1.jsonResponse)(200,{success:!0,data:{accepted:!1}},s)}}async function handleVerifyTriggerWeb(e,t,r,s){try{const n=resolveEmailWithBody(e,t??{},r.resolveEmailAddress);if(!n)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},s);const i=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,r.resolveDeviceId),a=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fp_id")||void 0,o=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,d=extractPermanentDeviceIdFromRequest(e),c=await r.client.triggerEmailVerification(n,i,{fingerprintId:a,fullHash:o,permanentDeviceId:d});if(c.success){const t=(0,web_helpers_1.jsonResponse)(200,{success:!0,data:c.data},s);if(c.data?.verification_id){const r=(0,web_helpers_1.isSecureWebRequest)(e)?"; Secure":"";t.headers.append("Set-Cookie",`__unshared_verification_id=${encodeURIComponent(c.data.verification_id)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=600${r}`)}return t}return(0,web_helpers_1.jsonResponse)(200,{success:!1,error:c.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},s)}catch(e){return r.onError&&r.onError(e,{operation:"verifyTrigger"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},s)}}async function handleVerifyWeb(e,t,r,s){try{const n=t?.code,i=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_verification_id");if(!i||!n)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Verification ID and code are required"}},s);const a=await r.client.verifyEmail({verification_id:i,code:n});if(a.success&&a.data?.verified){let t,n=!1;try{t=r.resolveUserId(e)}catch{}const i=resolveEmail(e,r.resolveEmailAddress),o=(0,web_helpers_1.extractDeviceIdFromRequest)(e,r.resolveDeviceId),d=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fp_id")||void 0,c=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,_=extractPermanentDeviceIdFromRequest(e);if(t&&i){const e={...o?{device_id:o}:{},...d?{stable_hash:d}:{},...c?{full_hash:c}:{},..._?{unshared_device_id:_}:{}},s=await r.client.checkUser({user_id:t,email_address:i,identifiers:e},{maxRetries:0});s.success&&s.data&&!s.failedOpen&&!s.data.is_user_flagged&&(r.verdictCache.update(t,{isVerified:!0}),n=!0)}const l=(0,web_helpers_1.jsonResponse)(200,{success:!0,data:a.data},s);n&&l.headers.set("X-Unshared-Enforcement-Reconciled","clean");const u=(0,web_helpers_1.isSecureWebRequest)(e)?"; Secure":"";return l.headers.append("Set-Cookie",`__unshared_verification_id=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0${u}`),l}return a.success?(0,web_helpers_1.jsonResponse)(200,{success:!0,data:a.data},s):(0,web_helpers_1.jsonResponse)(200,{success:!1,error:a.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}},s)}catch(e){return r.onError&&r.onError(e,{operation:"verify"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},s)}}function __test_resetInjectWarning(){defaultInjectWarnState.emitted=!1}exports.__test_fetchAndCacheVerdict=fetchAndCacheVerdict,exports.__test_injectIntoHtmlResponse=injectIntoHtmlResponse;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.createWebSubmitHandler=createWebSubmitHandler;const web_helpers_1=require("./web-helpers");function createWebSubmitHandler(e,r){const{userIdExtractor:s,eventTypeExtractor:t,sessionIdExtractor:n,ipAddressExtractor:o,defaultEventType:c="browser_event",routePrefix:i="/unshared",corsOrigins:a}=r??{},d=`${i}/submit-fingerprint-event`,u=a?Array.isArray(a)?a:[a]:null;return async r=>{let i;try{i=new URL(r.url).pathname}catch{return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"INVALID_URL",message:"Unable to parse request URL"}})}const a=u&&i===d?function(e){if(!u)return{};const r=e.headers.get("origin")??"",s=u.includes("*");return s||u.includes(r)?{"Access-Control-Allow-Origin":s?"*":r,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id"}:{}}(r):{};if("OPTIONS"===r.method&&i===d)return(0,web_helpers_1.emptyResponse)(204,a);if("POST"!==r.method||i!==d)return(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},a);try{let i;try{i=await r.json()}catch{return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"Request body is not valid JSON."}},a)}if(!(i&&i.hash&&i.stable_hash&&i.collected_at))return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Missing required fingerprint fields: hash, stable_hash, collected_at"}},a);if(!r.headers.get("x-session-id"))return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Missing required header: X-Session-Id"}},a);const d={full_hash:i.hash,fingerprint_id:i.stable_hash,timestamp:i.collected_at,isIncognito:i.is_incognito??!1,components:i.components??{},version:i.version??"unknown"};let u,l,_,h;try{u=(s?s(r):void 0)??i.user_id}catch{u=i.user_id}if(!u)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Missing required field: user_id"}},a);try{l=(t?t(r):void 0)??i.event_type??c}catch{l=i.event_type??c}try{_=(n?n(r):void 0)??r.headers.get("x-session-id")??i.session_id}catch{_=r.headers.get("x-session-id")??i.session_id}const p=(0,web_helpers_1.extractClientIpFromRequest)(r);try{h=(o?o(r):void 0)??p}catch{h=p}const b=await e.submitFingerprintEvent(d,{userId:u,sessionHash:_,eventType:l,ipAddress:h});return b.success?(0,web_helpers_1.jsonResponse)(202,{success:!0,data:b.data},a):(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"UPSTREAM_ERROR",message:b.error?.message??"Upstream request failed"}},a)}catch(e){return(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"MIDDLEWARE_ERROR",message:e instanceof Error?e.message:"Handler error"}},a)}}}
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.createWebSubmitHandler=createWebSubmitHandler;const web_helpers_1=require("./web-helpers");function createWebSubmitHandler(e,r){const{userIdExtractor:t,eventTypeExtractor:s,sessionIdExtractor:o,ipAddressExtractor:c,defaultEventType:n="browser_event",routePrefix:i="/unshared",corsOrigins:a}=r??{},d=`${i}/submit-fingerprint-event`,p=a?Array.isArray(a)?a:[a]:null;return async r=>{let i;try{i=new URL(r.url).pathname}catch{return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"INVALID_URL",message:"Unable to parse request URL"}})}const a=p&&i===d?function(e){if(!p)return{};const r=e.headers.get("origin")??"",t=p.includes("*");return t||p.includes(r)?{"Access-Control-Allow-Origin":t?"*":r,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Permanent-Device-Id"}:{}}(r):{};if("OPTIONS"===r.method&&i===d)return(0,web_helpers_1.emptyResponse)(204,a);if("POST"!==r.method||i!==d)return(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},a);try{const i=await(0,web_helpers_1.readJsonRequest)(r);if(!i.success)return(0,web_helpers_1.jsonResponse)(i.status,{success:!1,error:i.error},a);const d=i.data;if(!d||"object"!=typeof d||Array.isArray(d))return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Request body must be a JSON object"}},a);const p=d.context&&"object"==typeof d.context?d.context:{},y=p.identity&&"object"==typeof p.identity?p.identity:{},u=p.identifiers&&"object"==typeof p.identifiers?p.identifiers:{},b=p.event&&"object"==typeof p.event?p.event:{};let _,l,f,h;try{_=(t?t(r):void 0)??y.user_id}catch{_=y.user_id}try{l=(s?s(r):void 0)??b.type??n}catch{l=b.type??n}try{f=(o?o(r):void 0)??r.headers.get("x-session-id")??u.session_hash}catch{f=r.headers.get("x-session-id")??u.session_hash}const w=(0,web_helpers_1.extractClientIpFromRequest)(r);try{h=(c?c(r):void 0)??w}catch{h=w}const A=r.headers.get("x-permanent-device-id")??u.unshared_device_id,O=p.sdk&&"object"==typeof p.sdk?p.sdk.name:void 0,g="string"==typeof O&&O.startsWith("unshared-")&&d.fingerprint&&"object"==typeof d.fingerprint&&d.context&&"object"==typeof d.context&&!Array.isArray(d.context)&&d.context.identity&&"object"==typeof d.context.identity&&!Array.isArray(d.context.identity)&&d.context.identifiers&&"object"==typeof d.context.identifiers&&!Array.isArray(d.context.identifiers)&&d.context.event&&"object"==typeof d.context.event&&!Array.isArray(d.context.event),j=!Object.prototype.hasOwnProperty.call(d,"context")&&"string"==typeof d.hash&&"string"==typeof d.stable_hash&&"string"==typeof d.collected_at&&d.components&&"object"==typeof d.components&&!Array.isArray(d.components),m=g||j?{...d,context:{...p,identity:{...y,..._?{user_id:_}:{}},identifiers:{...u,...f?{session_hash:f}:{},...A?{unshared_device_id:A}:{}},event:{...b,type:l,...h?{ip_address:h}:{},user_agent:r.headers.get("user-agent")??""}}}:d,v=await e.submitFingerprint(m,{idempotencyKey:r.headers.get("x-idempotency-key")??void 0});return v.success?(0,web_helpers_1.jsonResponse)(202,{success:!0,data:v.data},a):(0,web_helpers_1.jsonResponse)(200,{success:!1,error:v.error??{code:"UPSTREAM_ERROR",message:"Upstream request failed"}},a)}catch(e){return(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"MIDDLEWARE_ERROR",message:e instanceof Error?e.message:"Handler error"}},a)}}}
@@ -12,7 +12,17 @@ import type { Verdict } from '../middleware/verdict-cache';
12
12
  /** A downstream handler that returns a Response. */
13
13
  export type WebHandler = (request: Request) => Response | Promise<Response>;
14
14
  /** Middleware that wraps a downstream handler, optionally intercepting the response. */
15
- export type WebMiddleware = (request: Request, next: WebHandler) => Response | Promise<Response>;
15
+ export type WebMiddleware = (request: Request, next: WebHandler,
16
+ /**
17
+ * Platform background-work hook (e.g. Cloudflare Workers / Vercel Edge
18
+ * `ExecutionContext.waitUntil`). When provided, background promises (SWR
19
+ * verdict refresh, event dispatch) are registered through it so the
20
+ * runtime keeps them alive past the response — otherwise they are killed
21
+ * at response end on edge runtimes.
22
+ */
23
+ ctx?: {
24
+ waitUntil?: (p: Promise<unknown>) => void;
25
+ }) => Response | Promise<Response>;
16
26
  export interface WebSubmitOptions {
17
27
  /** Override userId extractor. Falls back to body.user_id. */
18
28
  userIdExtractor?: (req: Request) => string | undefined;
@@ -32,6 +32,17 @@ export declare function extractDeviceIdFromRequestOrUnknown(request: Request, re
32
32
  * 2. The URL protocol (for direct HTTPS requests)
33
33
  */
34
34
  export declare function isSecureWebRequest(request: Request): boolean;
35
+ export declare function readJsonRequest(request: Request, maxBytes?: number): Promise<{
36
+ success: true;
37
+ data: unknown;
38
+ } | {
39
+ success: false;
40
+ status: 400 | 413;
41
+ error: {
42
+ code: string;
43
+ message: string;
44
+ };
45
+ }>;
35
46
  /**
36
47
  * Build a JSON response. Mirrors `sendJson(res, status, data)` in
37
48
  * src/middleware/utils/http-helpers.ts, but returns a new Response instead
@@ -1 +1 @@
1
- "use strict";function parseCookieFromRequest(e,t){const o=e.headers.get("cookie");if(!o)return;const n=o.match(new RegExp(`(?:^|; )${t}=([^;]*)`));return n?decodeURIComponent(n[1]):void 0}function extractClientIpFromRequest(e){const t=e.headers.get("cf-connecting-ip");if(t)return t;const o=e.headers.get("x-real-ip");if(o)return o;const n=e.headers.get("x-forwarded-for");if(n){const e=n.split(",")[0]?.trim();if(e)return e}return""}function extractDeviceIdFromRequest(e,t){if(t)try{const o=t(e);if(o)return o}catch{}const o=e.headers.get("x-device-id");if(o)return o;return parseCookieFromRequest(e,"__unshared_fp_id")||void 0}function extractDeviceIdFromRequestOrUnknown(e,t){return extractDeviceIdFromRequest(e,t)??"unknown"}function isSecureWebRequest(e){if("https"===e.headers.get("x-forwarded-proto"))return!0;try{return"https:"===new URL(e.url).protocol}catch{return!1}}function jsonResponse(e,t,o){const n=new Headers(o);return n.set("Content-Type","application/json"),new Response(JSON.stringify(t),{status:e,headers:n})}function emptyResponse(e,t){return new Response(null,{status:e,headers:new Headers(t)})}function bodyResponse(e,t,o){return new Response(t,{status:e,headers:new Headers(o)})}function appendCookieToHeaders(e,t){e.append("Set-Cookie",t)}function mergeResponseHeaders(e,t,o){const n=new Headers;e.forEach((e,t)=>{"set-cookie"!==t.toLowerCase()&&n.set(t,e)});const r="function"==typeof e.getSetCookie?e.getSetCookie():[];for(const e of r)n.append("Set-Cookie",e);if(o)for(const e of o)n.append("Set-Cookie",e);if(t)for(const[e,o]of Object.entries(t))n.set(e,o);return n}Object.defineProperty(exports,"t",{value:!0}),exports.parseCookieFromRequest=parseCookieFromRequest,exports.extractClientIpFromRequest=extractClientIpFromRequest,exports.extractDeviceIdFromRequest=extractDeviceIdFromRequest,exports.extractDeviceIdFromRequestOrUnknown=extractDeviceIdFromRequestOrUnknown,exports.isSecureWebRequest=isSecureWebRequest,exports.jsonResponse=jsonResponse,exports.emptyResponse=emptyResponse,exports.bodyResponse=bodyResponse,exports.appendCookieToHeaders=appendCookieToHeaders,exports.mergeResponseHeaders=mergeResponseHeaders;
1
+ "use strict";function parseCookieFromRequest(e,t){const o=e.headers.get("cookie");if(!o)return;const s=o.match(new RegExp(`(?:^|; )${t}=([^;]*)`));if(s)try{return decodeURIComponent(s[1])}catch{return s[1]}}function extractClientIpFromRequest(e){const t=e.headers.get("cf-connecting-ip");if(t)return t;const o=e.headers.get("x-real-ip");if(o)return o;const s=e.headers.get("x-forwarded-for");if(s){const e=s.split(",")[0]?.trim();if(e)return e}return""}function extractDeviceIdFromRequest(e,t){if(t)try{const o=t(e);if(o)return o}catch{}const o=e.headers.get("x-device-id");if(o)return o;return parseCookieFromRequest(e,"__unshared_fp_id")||void 0}function extractDeviceIdFromRequestOrUnknown(e,t){return extractDeviceIdFromRequest(e,t)??"unknown"}function isSecureWebRequest(e){if("https"===e.headers.get("x-forwarded-proto"))return!0;try{return"https:"===new URL(e.url).protocol}catch{return!1}}async function readJsonRequest(e,t=1<<20){const o=Number(e.headers.get("content-length"));if(Number.isFinite(o)&&o>t)return{success:!1,status:413,error:{code:"REQUEST_TOO_LARGE",message:"Request body exceeds 1 MiB"}};try{if(!e.body)throw new Error("Missing body");const o=e.body.getReader(),s=[];let r=0;for(;;){const{done:e,value:n}=await o.read();if(e)break;if(r+=n.byteLength,r>t)return o.cancel().catch(()=>{}),{success:!1,status:413,error:{code:"REQUEST_TOO_LARGE",message:"Request body exceeds 1 MiB"}};s.push(n)}const n=new Uint8Array(r);let c=0;for(const e of s)n.set(e,c),c+=e.byteLength;return{success:!0,data:JSON.parse((new TextDecoder).decode(n))}}catch{return{success:!1,status:400,error:{code:"INVALID_JSON",message:"Request body is not valid JSON."}}}}function jsonResponse(e,t,o){const s=new Headers(o);return s.set("Content-Type","application/json"),new Response(JSON.stringify(t),{status:e,headers:s})}function emptyResponse(e,t){return new Response(null,{status:e,headers:new Headers(t)})}function bodyResponse(e,t,o){return new Response(t,{status:e,headers:new Headers(o)})}function appendCookieToHeaders(e,t){e.append("Set-Cookie",t)}function mergeResponseHeaders(e,t,o){const s=new Headers;e.forEach((e,t)=>{"set-cookie"!==t.toLowerCase()&&s.set(t,e)});const r="function"==typeof e.getSetCookie?e.getSetCookie():[];for(const e of r)s.append("Set-Cookie",e);if(o)for(const e of o)s.append("Set-Cookie",e);if(t)for(const[e,o]of Object.entries(t))s.set(e,o);return s}Object.defineProperty(exports,"t",{value:!0}),exports.parseCookieFromRequest=parseCookieFromRequest,exports.extractClientIpFromRequest=extractClientIpFromRequest,exports.extractDeviceIdFromRequest=extractDeviceIdFromRequest,exports.extractDeviceIdFromRequestOrUnknown=extractDeviceIdFromRequestOrUnknown,exports.isSecureWebRequest=isSecureWebRequest,exports.readJsonRequest=readJsonRequest,exports.jsonResponse=jsonResponse,exports.emptyResponse=emptyResponse,exports.bodyResponse=bodyResponse,exports.appendCookieToHeaders=appendCookieToHeaders,exports.mergeResponseHeaders=mergeResponseHeaders;
@@ -0,0 +1,8 @@
1
+ export declare function sha256Bytes(input: string): Promise<Uint8Array>;
2
+ export declare function sha256HexWeb(input: string): Promise<string>;
3
+ /**
4
+ * Import raw key bytes as an AES-GCM CryptoKey, once, so callers can reuse it
5
+ * across many encryptions instead of paying importKey's cost per call.
6
+ */
7
+ export declare function importAesGcmKey(keyBytes: Uint8Array): Promise<CryptoKey>;
8
+ export declare function encryptDataWeb(plaintext: string, key: CryptoKey): Promise<string>;
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.sha256Bytes=sha256Bytes,exports.sha256HexWeb=sha256HexWeb,exports.importAesGcmKey=importAesGcmKey,exports.encryptDataWeb=encryptDataWeb;const subtle=globalThis.crypto.subtle;function toBase64(t){let e="";for(let n=0;n<t.length;n++)e+=String.fromCharCode(t[n]);return btoa(e)}async function sha256Bytes(t){const e=await subtle.digest("SHA-256",(new TextEncoder).encode(t));return new Uint8Array(e)}async function sha256HexWeb(t){const e=await sha256Bytes(t);return Array.from(e).map(t=>t.toString(16).padStart(2,"0")).join("")}async function importAesGcmKey(t){return subtle.importKey("raw",new Uint8Array(t),{name:"AES-GCM"},!1,["encrypt"])}async function encryptDataWeb(t,e){const n=globalThis.crypto.getRandomValues(new Uint8Array(12)),a=new Uint8Array(await subtle.encrypt({name:"AES-GCM",iv:n,tagLength:128},e,(new TextEncoder).encode(t))),r=a.slice(0,a.length-16),s=a.slice(a.length-16);return`${toBase64(n)}:${toBase64(s)}:${toBase64(r)}`}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "unshared-clientjs-sdk",
3
- "version": "2.2.1",
4
- "description": "Server-side Node.js SDK for the Unshared Labs V2 API",
3
+ "version": "3.0.0-rc.14",
4
+ "description": "Server-side Node.js SDK for the Unshared Labs API",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/esm/index.mjs",
7
7
  "types": "dist/index.d.ts",
@@ -46,18 +46,18 @@
46
46
  }
47
47
  },
48
48
  "engines": {
49
- "node": ">=18"
49
+ "node": ">=20"
50
50
  },
51
51
  "keywords": [],
52
52
  "author": "",
53
53
  "license": "MIT",
54
54
  "dependencies": {
55
- "unshared-frontend-sdk": "2.2.1"
55
+ "unshared-frontend-sdk": "3.0.0-rc.14"
56
56
  },
57
57
  "devDependencies": {
58
- "@unshared-labs/shared-types": "file:../../../shared/types",
59
58
  "@types/express": "^4.17.21",
60
59
  "@types/node": "^24.10.1",
60
+ "@unshared-labs/shared-types": "file:../../../shared/types",
61
61
  "express": "^4.18.2",
62
62
  "terser": "^5.44.1",
63
63
  "ts-node": "^10.9.2",