unshared-clientjs-sdk 2.0.0-rc.21 → 2.0.0-rc.22

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 (85) hide show
  1. package/dist/client.d.ts +29 -0
  2. package/dist/client.js +1 -1
  3. package/dist/esm/client.d.mts +29 -0
  4. package/dist/esm/client.mjs +1 -1
  5. package/dist/esm/index.d.mts +2 -0
  6. package/dist/esm/index.mjs +1 -1
  7. package/dist/esm/middleware/dispatch-dedupe.d.mts +11 -0
  8. package/dist/esm/middleware/dispatch-dedupe.mjs +1 -0
  9. package/dist/esm/middleware/index.d.mts +26 -11
  10. package/dist/esm/middleware/index.mjs +1 -1
  11. package/dist/esm/middleware/injection/fingerprint-script.mjs +1 -1
  12. package/dist/esm/middleware/response-interceptor.d.mts +2 -2
  13. package/dist/esm/middleware/routes/submit-fp.d.mts +14 -7
  14. package/dist/esm/middleware/routes/submit-fp.mjs +1 -1
  15. package/dist/esm/middleware/routes/verify.d.mts +11 -6
  16. package/dist/esm/middleware/routes/verify.mjs +1 -1
  17. package/dist/esm/middleware/utils/client-ip.d.mts +4 -4
  18. package/dist/esm/middleware/utils/client-ip.mjs +1 -1
  19. package/dist/esm/middleware/utils/cookies.d.mts +2 -2
  20. package/dist/esm/middleware/utils/device-id.d.mts +17 -3
  21. package/dist/esm/middleware/utils/device-id.mjs +1 -1
  22. package/dist/esm/middleware/utils/http-helpers.d.mts +21 -0
  23. package/dist/esm/middleware/utils/http-helpers.mjs +1 -0
  24. package/dist/esm/middleware/utils/include-path.d.mts +6 -0
  25. package/dist/esm/middleware/utils/include-path.mjs +1 -0
  26. package/dist/esm/middleware/utils/is-bot.mjs +1 -1
  27. package/dist/esm/middleware/utils/secure.d.mts +2 -2
  28. package/dist/esm/middleware/utils/secure.mjs +1 -1
  29. package/dist/esm/middleware/utils/sentinel-user-id.d.mts +10 -0
  30. package/dist/esm/middleware/utils/sentinel-user-id.mjs +1 -0
  31. package/dist/esm/middleware.d.mts +11 -8
  32. package/dist/esm/middleware.mjs +1 -1
  33. package/dist/esm/types.d.mts +44 -0
  34. package/dist/esm/types.mjs +1 -0
  35. package/dist/esm/web/index.d.mts +17 -0
  36. package/dist/esm/web/index.mjs +1 -0
  37. package/dist/esm/web/protection-handler.d.mts +28 -0
  38. package/dist/esm/web/protection-handler.mjs +1 -0
  39. package/dist/esm/web/submit-handler.d.mts +27 -0
  40. package/dist/esm/web/submit-handler.mjs +1 -0
  41. package/dist/esm/web/types.d.mts +110 -0
  42. package/dist/esm/web/types.mjs +1 -0
  43. package/dist/esm/web/web-helpers.d.mts +55 -0
  44. package/dist/esm/web/web-helpers.mjs +1 -0
  45. package/dist/index.d.ts +2 -0
  46. package/dist/index.js +1 -1
  47. package/dist/middleware/dispatch-dedupe.d.ts +11 -0
  48. package/dist/middleware/dispatch-dedupe.js +1 -0
  49. package/dist/middleware/index.d.ts +26 -11
  50. package/dist/middleware/index.js +1 -1
  51. package/dist/middleware/injection/fingerprint-script.js +1 -1
  52. package/dist/middleware/response-interceptor.d.ts +2 -2
  53. package/dist/middleware/routes/submit-fp.d.ts +14 -7
  54. package/dist/middleware/routes/submit-fp.js +1 -1
  55. package/dist/middleware/routes/verify.d.ts +11 -6
  56. package/dist/middleware/routes/verify.js +1 -1
  57. package/dist/middleware/utils/client-ip.d.ts +4 -4
  58. package/dist/middleware/utils/client-ip.js +1 -1
  59. package/dist/middleware/utils/cookies.d.ts +2 -2
  60. package/dist/middleware/utils/device-id.d.ts +17 -3
  61. package/dist/middleware/utils/device-id.js +1 -1
  62. package/dist/middleware/utils/http-helpers.d.ts +21 -0
  63. package/dist/middleware/utils/http-helpers.js +1 -0
  64. package/dist/middleware/utils/include-path.d.ts +6 -0
  65. package/dist/middleware/utils/include-path.js +1 -0
  66. package/dist/middleware/utils/is-bot.js +1 -1
  67. package/dist/middleware/utils/secure.d.ts +2 -2
  68. package/dist/middleware/utils/secure.js +1 -1
  69. package/dist/middleware/utils/sentinel-user-id.d.ts +10 -0
  70. package/dist/middleware/utils/sentinel-user-id.js +1 -0
  71. package/dist/middleware.d.ts +11 -8
  72. package/dist/middleware.js +1 -1
  73. package/dist/types.d.ts +44 -0
  74. package/dist/types.js +1 -0
  75. package/dist/web/index.d.ts +17 -0
  76. package/dist/web/index.js +1 -0
  77. package/dist/web/protection-handler.d.ts +28 -0
  78. package/dist/web/protection-handler.js +1 -0
  79. package/dist/web/submit-handler.d.ts +27 -0
  80. package/dist/web/submit-handler.js +1 -0
  81. package/dist/web/types.d.ts +110 -0
  82. package/dist/web/types.js +1 -0
  83. package/dist/web/web-helpers.d.ts +55 -0
  84. package/dist/web/web-helpers.js +1 -0
  85. package/package.json +7 -10
@@ -1,4 +1,4 @@
1
- import type { Response } from 'express';
1
+ import type { UnsharedResponse } from '../types';
2
2
  /**
3
3
  * Intercepts the response body by wrapping res.write() and res.end().
4
4
  *
@@ -10,6 +10,6 @@ import type { Response } from 'express';
10
10
  * callback with the complete body buffer and the Content-Type header.
11
11
  * The transform can return modified content or null to pass through unchanged.
12
12
  */
13
- export declare function interceptResponse(res: Response, transform: (body: Buffer, contentType: string | undefined) => Buffer | string | null, options?: {
13
+ export declare function interceptResponse(res: UnsharedResponse, transform: (body: Buffer, contentType: string | undefined) => Buffer | string | null, options?: {
14
14
  preventCaching?: boolean;
15
15
  }): void;
@@ -1,15 +1,22 @@
1
- import type { Request, Response } from 'express';
1
+ import type { UnsharedRequest, UnsharedResponse } from '../../types';
2
2
  import type { UnsharedLabsClient } from '../../client';
3
3
  import type { VerdictCache } from '../verdict-cache';
4
4
  import type { RateLimitBackoff } from '../rate-limit-backoff';
5
- export interface SubmitFingerprintDependencies {
5
+ import type { DispatchDedupe } from '../dispatch-dedupe';
6
+ export interface SubmitFingerprintDependencies<TReq extends UnsharedRequest = UnsharedRequest> {
6
7
  client: UnsharedLabsClient;
7
8
  verdictCache: VerdictCache;
8
9
  rateLimitBackoff: RateLimitBackoff;
9
- resolveUserId?: (req: Request) => string | undefined;
10
- resolveEmailAddress?: (req: Request) => string | undefined;
11
- resolveSessionId?: (req: Request) => string | undefined;
12
- resolveDeviceId?: (req: Request) => string | undefined;
10
+ dispatchDedupe: DispatchDedupe;
11
+ resolveUserId?: (req: TReq) => string | undefined;
12
+ resolveEmailAddress?: (req: TReq) => string | undefined;
13
+ resolveSessionId?: (req: TReq) => string | undefined;
14
+ resolveDeviceId?: (req: TReq) => string | undefined;
15
+ onError?: (error: unknown, context: {
16
+ operation: 'processUserEvent' | 'submitFingerprintEvent' | 'checkUser' | 'verifyTrigger' | 'verify';
17
+ userId?: string;
18
+ emailAddress?: string;
19
+ }) => void;
13
20
  }
14
21
  /**
15
22
  * Handles POST /__unshared/submit-fp
@@ -21,4 +28,4 @@ export interface SubmitFingerprintDependencies {
21
28
  *
22
29
  * Always returns 200 (fire-and-forget from browser's perspective).
23
30
  */
24
- export declare function handleSubmitFingerprint(dependencies: SubmitFingerprintDependencies): (req: Request, res: Response) => Promise<void>;
31
+ export declare function handleSubmitFingerprint<TReq extends UnsharedRequest = UnsharedRequest>(dependencies: SubmitFingerprintDependencies<TReq>): (req: TReq, res: UnsharedResponse) => Promise<void>;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"i",{value:!0}),exports.handleSubmitFingerprint=handleSubmitFingerprint;const is_bot_1=require("../utils/is-bot"),client_ip_1=require("../utils/client-ip"),cookies_1=require("../utils/cookies"),device_id_1=require("../utils/device-id"),secure_1=require("../utils/secure");function handleSubmitFingerprint(e){return async(i,s)=>{try{const t=i.body??{},n={full_hash:t.hash??"",fingerprint_id:t.stable_hash??"",timestamp:t.collected_at??(new Date).toISOString(),isIncognito:t.is_incognito??!1,components:t.components??{},version:t.version??"inline-1.0.0"};let o,r,c;try{o=e.resolveUserId?e.resolveUserId(i):void 0}catch{}o=o??t.user_id??void 0;try{r=e.resolveEmailAddress?e.resolveEmailAddress(i):void 0}catch{}r=r??(0,cookies_1.parseCookie)(i,"__unshared_email")??t.email??void 0;try{c=e.resolveSessionId?e.resolveSessionId(i):void 0}catch{}c=c??t.session_id??(0,cookies_1.parseCookie)(i,"__unshared_sid");const _=(0,client_ip_1.extractClientIp)(i),d=i.headers["user-agent"]??"";if((0,is_bot_1.isBot)(d))return void s.status(200).json({success:!0});const u=(0,device_id_1.extractDeviceId)(i,e.resolveDeviceId),a=n.fingerprint_id||void 0,l=(0,secure_1.isSecureRequest)(i)?"; Secure":"",p=[];if(a&&!(0,cookies_1.parseCookie)(i,"__unshared_fingerprint_id")&&p.push(`__unshared_fingerprint_id=${encodeURIComponent(a)}; HttpOnly; Path=/; SameSite=Lax${l}`),r&&!(0,cookies_1.parseCookie)(i,"__unshared_email")&&p.push(`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${l}`),p.length>0){const e=s.getHeader("Set-Cookie");if(e){const i=Array.isArray(e)?[...e]:[String(e)];i.push(...p),s.setHeader("Set-Cookie",i)}else s.setHeader("Set-Cookie",p)}o&&e.client.submitFingerprintEvent(n,{userId:o,emailAddress:r,sessionHash:c,eventType:"auto_collect",ipAddress:_,userAgent:d}).catch(()=>{}),o&&r&&!e.rateLimitBackoff.isPaused()&&e.client.processUserEvent({eventType:"auto_collect",userId:o,emailAddress:r,ipAddress:_,deviceId:u,fingerprintId:a,sessionHash:c??"unknown",userAgent:d}).then(i=>{i.success&&i.data?.analysis&&e.verdictCache.update(o,{isFlagged:i.data.analysis.is_user_flagged}),!i.success&&i.error?.retryAfter&&e.rateLimitBackoff.pause(1e3*i.error.retryAfter)}).catch(()=>{}),s.status(200).json({success:!0})}catch{s.status(200).json({success:!0})}}}
1
+ "use strict";Object.defineProperty(exports,"i",{value:!0}),exports.handleSubmitFingerprint=handleSubmitFingerprint;const is_bot_1=require("../utils/is-bot"),client_ip_1=require("../utils/client-ip"),cookies_1=require("../utils/cookies"),device_id_1=require("../utils/device-id"),secure_1=require("../utils/secure"),sentinel_user_id_1=require("../utils/sentinel-user-id"),http_helpers_1=require("../utils/http-helpers");function handleSubmitFingerprint(e){return async(i,s)=>{try{const t=i.body??{},n={full_hash:t.hash??"",fingerprint_id:t.stable_hash??"",timestamp:t.collected_at??(new Date).toISOString(),isIncognito:t.is_incognito??!1,components:t.components??{},version:t.version??"inline-1.0.0"};let r,o,_;try{const s=e.resolveUserId?e.resolveUserId(i):void 0;s&&!(0,sentinel_user_id_1.isSentinelUserId)(s)&&(r=s)}catch{}if(!r){const e="string"==typeof t.user_id?t.user_id:void 0;e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(r=e)}if(!r){const e=(0,cookies_1.parseCookie)(i,"__unshared_uid");e&&!(0,sentinel_user_id_1.isSentinelUserId)(e)&&(r=e)}try{o=e.resolveEmailAddress?e.resolveEmailAddress(i):void 0}catch{}o=o??(0,cookies_1.parseCookie)(i,"__unshared_email")??t.email??void 0;try{_=e.resolveSessionId?e.resolveSessionId(i):void 0}catch{}_=_??t.session_id??(0,cookies_1.parseCookie)(i,"__unshared_sid");const d=(0,client_ip_1.extractClientIp)(i),c=i.headers["user-agent"]??"";if((0,is_bot_1.isBot)(c))return void(0,http_helpers_1.sendJson)(s,200,{success:!0});const u=(n.fingerprint_id&&n.fingerprint_id.length>0?n.fingerprint_id:void 0)??(0,device_id_1.extractDeviceId)(i,e.resolveDeviceId),a=n.fingerprint_id||void 0,p=(0,secure_1.isSecureRequest)(i)?"; Secure":"",l=[];if(a&&!(0,cookies_1.parseCookie)(i,"__unshared_fingerprint_id")&&l.push(`__unshared_fingerprint_id=${encodeURIComponent(a)}; HttpOnly; Path=/; SameSite=Lax${p}`),a){const e=(0,cookies_1.parseCookie)(i,"__unshared_fp_id");e&&e===a||l.push(`__unshared_fp_id=${encodeURIComponent(a)}; Path=/; SameSite=Lax; Max-Age=31536000${p}`)}if(o&&!(0,cookies_1.parseCookie)(i,"__unshared_email")&&l.push(`__unshared_email=${encodeURIComponent(o)}; HttpOnly; Path=/; SameSite=Lax${p}`),l.length>0){const e=s.getHeader("Set-Cookie");if(e){const i=Array.isArray(e)?[...e]:[String(e)];i.push(...l),s.setHeader("Set-Cookie",i)}else s.setHeader("Set-Cookie",l)}let h;if("string"==typeof t.event_type&&t.event_type)h=t.event_type;else{const e=i.headers.referer??i.headers.referrer;let s="unknown";if("string"==typeof e&&e.length>0)try{const i=new URL(e);s=(i.pathname||"/")+(i.search||"")}catch{}h=s}const f=i.headers["x-idempotency-key"],m=("string"==typeof f&&f.length>0?f:void 0)??(a&&r?`${a}|${r}|${h}`:void 0);if(r&&e.client.submitFingerprintEvent(n,{userId:r,emailAddress:o,sessionHash:_,eventType:h,ipAddress:d,userAgent:c,idempotencyKey:m}).catch(i=>{e.onError&&e.onError(i,{operation:"submitFingerprintEvent",userId:r,emailAddress:o})}),r&&o&&!e.rateLimitBackoff.isPaused()&&!e.dispatchDedupe.wasRecentlyDispatched(r,h)){e.dispatchDedupe.mark(r,h);try{const i=await e.client.processUserEvent({eventType:h,userId:r,emailAddress:o,ipAddress:d,deviceId:u,fingerprintId:a,sessionHash:_??"unknown",userAgent:c});i.success&&i.data?.analysis&&e.verdictCache.update(r,{isFlagged:i.data.analysis.is_user_flagged}),!i.success&&i.error?.retryAfter&&e.rateLimitBackoff.pause(1e3*i.error.retryAfter)}catch(i){e.onError&&e.onError(i,{operation:"processUserEvent",userId:r,emailAddress:o})}}(0,http_helpers_1.sendJson)(s,200,{success:!0})}catch{(0,http_helpers_1.sendJson)(s,200,{success:!0})}}}
@@ -1,11 +1,16 @@
1
- import type { Request, Response } from 'express';
1
+ import type { UnsharedRequest, UnsharedResponse } from '../../types';
2
2
  import type { UnsharedLabsClient } from '../../client';
3
3
  import type { VerdictCache } from '../verdict-cache';
4
- export interface VerificationDependencies {
4
+ export interface VerificationDependencies<TReq extends UnsharedRequest = UnsharedRequest> {
5
5
  client: UnsharedLabsClient;
6
6
  verdictCache: VerdictCache;
7
- resolveEmailAddress?: (req: Request) => string | undefined;
8
- resolveDeviceId?: (req: Request) => string | undefined;
7
+ resolveEmailAddress?: (req: TReq) => string | undefined;
8
+ resolveDeviceId?: (req: TReq) => string | undefined;
9
+ onError?: (error: unknown, context: {
10
+ operation: 'processUserEvent' | 'submitFingerprintEvent' | 'checkUser' | 'verifyTrigger' | 'verify';
11
+ userId?: string;
12
+ emailAddress?: string;
13
+ }) => void;
9
14
  }
10
15
  /**
11
16
  * POST /__unshared/verify-trigger
@@ -15,7 +20,7 @@ export interface VerificationDependencies {
15
20
  *
16
21
  * The deviceId is resolved via extractDeviceId (same as the middleware).
17
22
  */
18
- export declare function handleVerifyTrigger(dependencies: VerificationDependencies): (req: Request, res: Response) => Promise<void>;
23
+ export declare function handleVerifyTrigger<TReq extends UnsharedRequest = UnsharedRequest>(dependencies: VerificationDependencies<TReq>): (req: TReq, res: UnsharedResponse) => Promise<void>;
19
24
  /**
20
25
  * POST /__unshared/verify
21
26
  * Validates OTP code. Called by the blocker overlay UI.
@@ -25,4 +30,4 @@ export declare function handleVerifyTrigger(dependencies: VerificationDependenci
25
30
  * On successful verification, updates the verdict cache to mark
26
31
  * the user as verified so subsequent requests pass through.
27
32
  */
28
- export declare function handleVerify(dependencies: VerificationDependencies): (req: Request, res: Response) => Promise<void>;
33
+ export declare function handleVerify<TReq extends UnsharedRequest = UnsharedRequest>(dependencies: VerificationDependencies<TReq>): (req: TReq, res: UnsharedResponse) => Promise<void>;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"i",{value:!0}),exports.handleVerifyTrigger=handleVerifyTrigger,exports.handleVerify=handleVerify;const cookies_1=require("../utils/cookies"),device_id_1=require("../utils/device-id");function handleVerifyTrigger(e){return async(i,r)=>{try{const s=resolveEmail(i,i.body??{},e.resolveEmailAddress);if(!s)return void r.status(400).json({success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}});const c=(0,device_id_1.extractDeviceId)(i,e.resolveDeviceId),o=(0,cookies_1.parseCookie)(i,"__unshared_fingerprint_id")||void 0,t=await e.client.triggerEmailVerification(s,c,{fingerprintId:o});t.success?r.status(200).json({success:!0,data:t.data}):r.status(200).json({success:!1,error:t.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}})}catch{r.status(200).json({success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}})}}}function handleVerify(e){return async(i,r)=>{try{const s=i.body??{},c=resolveEmail(i,s,e.resolveEmailAddress),o=s.code;if(!c||!o)return void r.status(400).json({success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}});const t=(0,device_id_1.extractDeviceId)(i,e.resolveDeviceId),n=(0,cookies_1.parseCookie)(i,"__unshared_fingerprint_id")||void 0,d=await e.client.verify(c,t,o,{fingerprintId:n});if(d.success){const s=(0,cookies_1.parseCookie)(i,"__unshared_uid");s&&e.verdictCache.update(s,{isVerified:!0}),r.status(200).json({success:!0,data:{verified:!0}})}else r.status(200).json({success:!1,error:d.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}})}catch{r.status(200).json({success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}})}}}function resolveEmail(e,i,r){if(r)try{const i=r(e);if(i)return i}catch{}const s=(0,cookies_1.parseCookie)(e,"__unshared_email");if(s)return s;const c=i.email;return"string"==typeof c&&c?c:void 0}
1
+ "use strict";Object.defineProperty(exports,"i",{value:!0}),exports.handleVerifyTrigger=handleVerifyTrigger,exports.handleVerify=handleVerify;const cookies_1=require("../utils/cookies"),device_id_1=require("../utils/device-id"),http_helpers_1=require("../utils/http-helpers");function handleVerifyTrigger(e){return async(r,i)=>{try{const s=resolveEmail(r,r.body??{},e.resolveEmailAddress);if(!s)return void(0,http_helpers_1.sendJson)(i,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}});const t=(0,device_id_1.extractDeviceId)(r,e.resolveDeviceId),o=(0,cookies_1.parseCookie)(r,"__unshared_fingerprint_id")||void 0,c=await e.client.triggerEmailVerification(s,t,{fingerprintId:o});c.success?(0,http_helpers_1.sendJson)(i,200,{success:!0,data:c.data}):(0,http_helpers_1.sendJson)(i,200,{success:!1,error:c.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}})}catch(r){e.onError&&e.onError(r,{operation:"verifyTrigger"}),(0,http_helpers_1.sendJson)(i,200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}})}}}function handleVerify(e){return async(r,i)=>{try{const s=r.body??{},t=resolveEmail(r,s,e.resolveEmailAddress),o=s.code;if(!t||!o)return void(0,http_helpers_1.sendJson)(i,400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}});const c=(0,device_id_1.extractDeviceId)(r,e.resolveDeviceId),_=(0,cookies_1.parseCookie)(r,"__unshared_fingerprint_id")||void 0,n=await e.client.verify(t,c,o,{fingerprintId:_});if(n.success){const s=(0,cookies_1.parseCookie)(r,"__unshared_uid");s&&e.verdictCache.update(s,{isVerified:!0}),(0,http_helpers_1.sendJson)(i,200,{success:!0,data:{verified:!0}})}else(0,http_helpers_1.sendJson)(i,200,{success:!1,error:n.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}})}catch(r){e.onError&&e.onError(r,{operation:"verify"}),(0,http_helpers_1.sendJson)(i,200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}})}}}function resolveEmail(e,r,i){if(i)try{const r=i(e);if(r)return r}catch{}const s=(0,cookies_1.parseCookie)(e,"__unshared_email");if(s)return s;const t=r.email;return"string"==typeof t&&t?t:void 0}
@@ -1,6 +1,6 @@
1
- import type { Request } from 'express';
1
+ import type { UnsharedRequest } from '../../types';
2
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.
3
+ * Extract the real client IP from proxy headers, falling back to req.ip or socket address.
4
+ * Checked in order: CF-Connecting-IP (Cloudflare) → X-Real-IP (nginx/ALB) → req.ip → socket.
5
5
  */
6
- export declare function extractClientIp(req: Request): string;
6
+ export declare function extractClientIp(req: UnsharedRequest): string;
@@ -1 +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;
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??t.socket?.remoteAddress??""}Object.defineProperty(exports,"t",{value:!0}),exports.extractClientIp=extractClientIp;
@@ -1,6 +1,6 @@
1
- import type { Request } from 'express';
1
+ import type { UnsharedRequest } from '../../types';
2
2
  /**
3
3
  * Reads a single cookie value from the raw Cookie header.
4
4
  * Works without cookie-parser middleware.
5
5
  */
6
- export declare function parseCookie(req: Request, name: string): string | undefined;
6
+ export declare function parseCookie(req: UnsharedRequest, name: string): string | undefined;
@@ -1,5 +1,19 @@
1
- import type { Request } from 'express';
1
+ import type { UnsharedRequest } from '../../types';
2
2
  /**
3
- * Resolves device ID from: custom resolver → __unshared_fp_id cookie → X-Device-Id header.
3
+ * Resolves device ID from: custom resolver → X-Device-Id header → __unshared_fp_id cookie.
4
+ *
5
+ * Returns the literal string `"unknown"` when no source provides a value.
6
+ * Callers that can tolerate a tri-state should use `extractDeviceIdOrUndefined`
7
+ * instead — the "unknown" sentinel polluted 19% of FP rows in production
8
+ * because it was being dispatched as-if-real during the first request of a
9
+ * session, before the inline script had a chance to set `__unshared_fp_id`.
10
+ * Kept for API compatibility.
4
11
  */
5
- export declare function extractDeviceId(req: Request, resolveDeviceId?: (req: Request) => string | undefined): string;
12
+ export declare function extractDeviceId<TReq extends UnsharedRequest = UnsharedRequest>(req: TReq, resolveDeviceId?: (req: TReq) => string | undefined): string;
13
+ /**
14
+ * Resolves device ID from: custom resolver → X-Device-Id header → __unshared_fp_id cookie.
15
+ * Returns `undefined` when nothing is available. Use this at any call site
16
+ * that can take a "skip dispatch" branch during the bootstrap window, so we
17
+ * stop writing `device_id="unknown"` rows to the analytics table on first request.
18
+ */
19
+ export declare function extractDeviceIdOrUndefined<TReq extends UnsharedRequest = UnsharedRequest>(req: TReq, resolveDeviceId?: (req: TReq) => string | undefined): string | undefined;
@@ -1 +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"}
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.extractDeviceId=extractDeviceId,exports.extractDeviceIdOrUndefined=extractDeviceIdOrUndefined;const cookies_1=require("./cookies");function extractDeviceId(e,t){return extractDeviceIdOrUndefined(e,t)??"unknown"}function extractDeviceIdOrUndefined(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=e.headers["x-device-id"];if("string"==typeof r&&r)return r;return(0,cookies_1.parseCookie)(e,"__unshared_fp_id")||void 0}
@@ -0,0 +1,21 @@
1
+ import type { UnsharedResponse } from '../../types';
2
+ /**
3
+ * Send a JSON response. Framework-agnostic replacement for Express's
4
+ * `res.status(code).json(data)`.
5
+ */
6
+ export declare function sendJson(res: UnsharedResponse, statusCode: number, data: unknown): void;
7
+ /**
8
+ * Send an empty response with a status code. Replacement for Express's
9
+ * `res.status(code).end()`.
10
+ */
11
+ export declare function sendEmpty(res: UnsharedResponse, statusCode: number): void;
12
+ /**
13
+ * Send a response with a specific body. Replacement for Express's
14
+ * `res.status(code).end(body)`.
15
+ */
16
+ export declare function sendBody(res: UnsharedResponse, statusCode: number, body: string | Buffer): void;
17
+ /**
18
+ * Extract the pathname from a request URL, stripping query string.
19
+ * Framework-agnostic replacement for Express's `req.path`.
20
+ */
21
+ export declare function getRequestPath(url: string | undefined): string;
@@ -0,0 +1 @@
1
+ "use strict";function sendJson(t,e,n){const s=JSON.stringify(n);t.statusCode=e,t.setHeader("Content-Type","application/json"),t.end(s)}function sendEmpty(t,e){t.statusCode=e,t.end()}function sendBody(t,e,n){t.statusCode=e,t.end(n)}function getRequestPath(t){if(!t)return"/";const e=t.indexOf("?");return-1===e?t:t.slice(0,e)}Object.defineProperty(exports,"t",{value:!0}),exports.sendJson=sendJson,exports.sendEmpty=sendEmpty,exports.sendBody=sendBody,exports.getRequestPath=getRequestPath;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Returns true if the path matches at least one of the includePathPrefix entries.
3
+ * When includePathPrefix is undefined, returns true (all paths match — preserves default behavior).
4
+ * When includePathPrefix is an empty array, returns false (no paths match).
5
+ */
6
+ export declare function shouldIncludePath(path: string, includePathPrefix?: string[]): boolean;
@@ -0,0 +1 @@
1
+ "use strict";function shouldIncludePath(e,t){return!t||t.some(t=>e.startsWith(t))}Object.defineProperty(exports,"t",{value:!0}),exports.shouldIncludePath=shouldIncludePath;
@@ -1 +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)}
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","puppeteer","playwright","cypress","selenium","webdriver","electron","jsdom","vercel-screenshot","screenshot","prerender","lighthouse","chrome-lighthouse","pagespeed","gtmetrix","pingdom","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(e){return!!e&&BOT_RE.test(e)}
@@ -1,3 +1,3 @@
1
- import type { Request } from 'express';
1
+ import type { UnsharedRequest } from '../../types';
2
2
  /** Returns true if the request arrived over HTTPS (direct or via reverse proxy). */
3
- export declare function isSecureRequest(req: Request): boolean;
3
+ export declare function isSecureRequest(req: UnsharedRequest): boolean;
@@ -1 +1 @@
1
- "use strict";function isSecureRequest(e){return e.secure||"https"===e.headers["x-forwarded-proto"]}Object.defineProperty(exports,"t",{value:!0}),exports.isSecureRequest=isSecureRequest;
1
+ "use strict";function isSecureRequest(e){return!!e.socket?.encrypted||"https"===e.headers["x-forwarded-proto"]}Object.defineProperty(exports,"t",{value:!0}),exports.isSecureRequest=isSecureRequest;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Stickiness window: if the resolver returns a sentinel but the
3
+ * `__unshared_uid` cookie was set within this many milliseconds, we prefer
4
+ * the cookie value (assuming it's a hydration race on the same tab).
5
+ * Outside the window, the cookie is considered stale and we fall through
6
+ * to the "no userId" branch — without clearing the cookie, because clearing
7
+ * on every hydration blip is exactly the bug we're fixing.
8
+ */
9
+ export declare const SENTINEL_STICKINESS_TTL_MS = 30000;
10
+ export declare function isSentinelUserId(value: string | undefined | null): boolean;
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.SENTINEL_STICKINESS_TTL_MS=void 0,exports.isSentinelUserId=isSentinelUserId;const SENTINEL_USER_IDS=new Set(["__pre_auth__","anonymous","guest","undefined","null"]);function isSentinelUserId(e){return"string"==typeof e&&SENTINEL_USER_IDS.has(e)}exports.SENTINEL_STICKINESS_TTL_MS=3e4;
@@ -1,14 +1,14 @@
1
- import type { Request, Response, NextFunction, Application } from 'express';
1
+ import type { UnsharedRequest, UnsharedResponse, UnsharedNextFunction } from './types';
2
2
  import type { UnsharedLabsClient } from './client';
3
- export interface MiddlewareOptions {
3
+ export interface MiddlewareOptions<TReq extends UnsharedRequest = UnsharedRequest> {
4
4
  /** Override userId extractor. Falls back to req.body.user_id. */
5
- userIdExtractor?: (req: Request) => string | undefined;
5
+ userIdExtractor?: (req: TReq) => string | undefined;
6
6
  /** Override eventType extractor. Falls back to req.body.event_type. */
7
- eventTypeExtractor?: (req: Request) => string | undefined;
7
+ eventTypeExtractor?: (req: TReq) => string | undefined;
8
8
  /** Override sessionId extractor. Falls back to X-Session-Id header, then req.body.session_id. */
9
- sessionIdExtractor?: (req: Request) => string | undefined;
9
+ sessionIdExtractor?: (req: TReq) => string | undefined;
10
10
  /** Override IP address extractor. Falls back to req.ip. */
11
- ipAddressExtractor?: (req: Request) => string | undefined;
11
+ ipAddressExtractor?: (req: TReq) => string | undefined;
12
12
  /** Default event type when none is extractable. @default "browser_event" */
13
13
  defaultEventType?: string;
14
14
  /**
@@ -29,6 +29,9 @@ export interface MiddlewareOptions {
29
29
  * Asserts that Express `trust proxy` is configured on the app.
30
30
  * Call this once during application startup, before mounting any middleware.
31
31
  *
32
+ * **Express-specific utility.** Other frameworks handle proxy trust differently;
33
+ * consult your framework's documentation for equivalent configuration.
34
+ *
32
35
  * Throws synchronously if the setting is missing, killing the process before
33
36
  * any requests are served.
34
37
  *
@@ -39,7 +42,7 @@ export interface MiddlewareOptions {
39
42
  * app.use(createUnsharedMiddleware(client, options));
40
43
  * ```
41
44
  */
42
- export declare function assertTrustProxy(app: Application): void;
45
+ export declare function assertTrustProxy(app: any): void;
43
46
  /**
44
47
  * Creates an Express middleware that proxies browser fingerprint events to
45
48
  * Unshared Labs. Mount this to handle the browser fingerprint route contract (§4 of spec).
@@ -70,4 +73,4 @@ export declare function assertTrustProxy(app: Application): void;
70
73
  * }));
71
74
  * ```
72
75
  */
73
- export declare function createUnsharedMiddleware(client: UnsharedLabsClient, options?: MiddlewareOptions): (req: Request, res: Response, next: NextFunction) => Promise<void>;
76
+ export declare function createUnsharedMiddleware<TReq extends UnsharedRequest = UnsharedRequest>(client: UnsharedLabsClient, options?: MiddlewareOptions<TReq>): (req: TReq, res: UnsharedResponse, next: UnsharedNextFunction) => Promise<void>;
@@ -1 +1 @@
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;
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()}}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Framework-agnostic HTTP types for the Unshared Labs middleware.
3
+ *
4
+ * These interfaces are structurally compatible with Express, Fastify (via raw),
5
+ * Koa (via ctx.req/ctx.res), and raw Node.js http.IncomingMessage/ServerResponse.
6
+ */
7
+ import type { IncomingHttpHeaders } from 'http';
8
+ /**
9
+ * Minimal request interface for framework-agnostic middleware.
10
+ *
11
+ * Express, Fastify's `request.raw`, Koa's `ctx.req`, and Node.js
12
+ * `http.IncomingMessage` all satisfy this interface via structural typing.
13
+ */
14
+ export interface UnsharedRequest {
15
+ method?: string;
16
+ url?: string;
17
+ headers: IncomingHttpHeaders;
18
+ body?: any;
19
+ /** Client IP address, if the framework provides it (e.g. Express `req.ip`). */
20
+ ip?: string;
21
+ socket?: {
22
+ remoteAddress?: string;
23
+ encrypted?: boolean;
24
+ };
25
+ }
26
+ /**
27
+ * Minimal response interface for framework-agnostic middleware.
28
+ *
29
+ * Express `Response`, Fastify's `reply.raw`, Koa's `ctx.res`, and Node.js
30
+ * `http.ServerResponse` all satisfy this interface via structural typing.
31
+ */
32
+ export interface UnsharedResponse {
33
+ statusCode: number;
34
+ setHeader(name: string, value: string | number | string[]): any;
35
+ getHeader(name: string): string | number | string[] | undefined;
36
+ removeHeader(name: string): void;
37
+ write(chunk: any, encodingOrCallback?: any, callback?: any): boolean;
38
+ end(chunk?: any, encodingOrCallback?: any, callback?: any): any;
39
+ writeHead(statusCode: number, ...args: any[]): any;
40
+ }
41
+ /**
42
+ * Middleware next function. Compatible with Express, Connect, and Koa.
43
+ */
44
+ export type UnsharedNextFunction = (err?: any) => void;
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0});
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Web Standard Request/Response handlers for serverless/edge environments.
3
+ *
4
+ * Use this entry point in Next.js App Router, Vercel Edge Functions,
5
+ * Cloudflare Workers, Deno Deploy, Bun, or any other runtime that uses
6
+ * the Web Standard Fetch API (`Request`/`Response` globals).
7
+ *
8
+ * For Node.js HTTP frameworks (Express, Fastify, Koa, Next.js Pages Router),
9
+ * use the default `unshared-clientjs-sdk` entry point instead — its middleware
10
+ * is compatible with all frameworks that expose `http.IncomingMessage`/
11
+ * `http.ServerResponse` objects.
12
+ */
13
+ export { createWebSubmitHandler } from './submit-handler';
14
+ export { createWebProtectionMiddleware } from './protection-handler';
15
+ export type { WebHandler, WebMiddleware, WebSubmitOptions, WebProtectionConfig, } from './types';
16
+ export { VerdictCache } from '../middleware/verdict-cache';
17
+ export type { Verdict } from '../middleware/verdict-cache';
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.VerdictCache=exports.createWebProtectionMiddleware=exports.createWebSubmitHandler=void 0;var submit_handler_1=require("./submit-handler");Object.defineProperty(exports,"createWebSubmitHandler",{enumerable:!0,get:function(){return submit_handler_1.createWebSubmitHandler}});var protection_handler_1=require("./protection-handler");Object.defineProperty(exports,"createWebProtectionMiddleware",{enumerable:!0,get:function(){return protection_handler_1.createWebProtectionMiddleware}});var verdict_cache_1=require("../middleware/verdict-cache");Object.defineProperty(exports,"VerdictCache",{enumerable:!0,get:function(){return verdict_cache_1.VerdictCache}});
@@ -0,0 +1,28 @@
1
+ import type { UnsharedLabsClient } from '../client';
2
+ import type { WebMiddleware, WebProtectionConfig } from './types';
3
+ /**
4
+ * Web Standard equivalent of `unsharedBoundToUser` from src/middleware/index.ts.
5
+ *
6
+ * Returns a middleware `(request, next) => Promise<Response>` suitable for
7
+ * Next.js App Router, Vercel Edge Functions, Cloudflare Workers, Deno Deploy,
8
+ * and other Web Standard runtimes.
9
+ *
10
+ * Unlike the Node.js middleware which mutates the response object, this
11
+ * handler calls `next(request)` to get the downstream Response, transforms
12
+ * it (injects the fingerprint script into HTML), and returns a new Response.
13
+ *
14
+ * @example Next.js App Router (in middleware.ts)
15
+ * ```typescript
16
+ * import { createWebProtectionMiddleware } from 'unshared-clientjs-sdk/web';
17
+ *
18
+ * const protect = createWebProtectionMiddleware(client, {
19
+ * userId: (req) => parseJwt(req.headers.get('authorization'))?.sub,
20
+ * emailAddress: (req) => parseJwt(req.headers.get('authorization'))?.email,
21
+ * });
22
+ *
23
+ * export async function middleware(request: Request) {
24
+ * return protect(request, async () => NextResponse.next());
25
+ * }
26
+ * ```
27
+ */
28
+ export declare function createWebProtectionMiddleware(client: UnsharedLabsClient, config: WebProtectionConfig): WebMiddleware;
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.createWebProtectionMiddleware=createWebProtectionMiddleware;const 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"),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=500;function createWebProtectionMiddleware(e,r){if(!r.userId)throw new Error("[Unshared] userId resolver is required");const{userId:s,emailAddress:t,routePrefix:i="/__unshared",corsOrigins:n,cacheTTL:o=6e4,skipPaths:a,includePathPrefix:d,sessionId:c,deviceId:_,fingerprintSdkBundle:l="",onFlagged:u,onError:p}=r,h=new verdict_cache_1.VerdictCache(o),f=new rate_limit_backoff_1.RateLimitBackoff,m=new dispatch_dedupe_1.DispatchDedupe,w=Date.now().toString(36),g=(0,fingerprint_script_1.generateFingerprintScript)(i,w),b=`${i}/fp.js`,v=`${i}/submit-fp`,I=`${i}/verify-trigger`,y=`${i}/verify`,A=`${i}/status`,R=n?Array.isArray(n)?n:[n]:null;return async function(r,n){let o,w,E;try{const e=new URL(r.url);o=e.pathname,w=e.search}catch{return n(r)}if(o.startsWith(i+"/")){const i=function(e){if(!R)return{};const r=e.headers.get("origin")??"",s=R.includes("*");return s||R.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, X-Device-Id","Access-Control-Allow-Credentials":"true"}:{}}(r);if("OPTIONS"===r.method)return(0,web_helpers_1.emptyResponse)(204,i);if("GET"===r.method&&o===b)return l?(0,web_helpers_1.bodyResponse)(200,l,{...i,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600"}):(0,web_helpers_1.jsonResponse)(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},i);if("POST"===r.method&&(o===v||o===I||o===y)){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 o===v?handleSubmitFp(r,n,{client:e,verdictCache:h,rateLimitBackoff:f,dispatchDedupe:m,resolveUserId:s,resolveEmailAddress:t,resolveSessionId:c,resolveDeviceId:_,onError:p},i):o===I?handleVerifyTriggerWeb(r,n,{client:e,verdictCache:h,resolveEmailAddress:t,resolveDeviceId:_,onError:p},i):handleVerifyWeb(r,n,{client:e,verdictCache:h,resolveEmailAddress:t,resolveDeviceId:_,onError:p},i)}if("GET"===r.method&&o===A){let e;try{e=s(r)}catch{}if(!e)return(0,web_helpers_1.jsonResponse)(200,{status:"anonymous"},i);const n=resolveEmail(r,t),o=h.get(e);return o&&o.isFlagged&&!o.isVerified&&u&&n?(0,web_helpers_1.jsonResponse)(403,{error:"account_flagged",email:n},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)(o,a))return n(r);if(!(0,include_path_1.shouldIncludePath)(o,d))return injectIntoHtmlResponse(await n(r),g);try{E=s(r)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(E)){const e=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_uid"),s=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_uid_at"),t=s?Number(s):NaN,i=Number.isFinite(t)&&Date.now()-t<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;E=e&&i?e:void 0}if(!E)return injectIntoHtmlResponse(await n(r),g);const S=resolveEmail(r,t),T=[],C=(0,web_helpers_1.isSecureWebRequest)(r)?"; Secure":"";if(T.push(`__unshared_uid=${encodeURIComponent(E)}; Path=/; SameSite=Lax${C}`),T.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${C}`),S&&T.push(`__unshared_email=${encodeURIComponent(S)}; HttpOnly; Path=/; SameSite=Lax${C}`),!S)return injectIntoHtmlResponse(await n(r),g,T);const O=extractSessionIdFromRequest(r,c),k=(0,web_helpers_1.extractDeviceIdFromRequest)(r,_),N=(0,web_helpers_1.parseCookieFromRequest)(r,"__unshared_fingerprint_id")||void 0,P=r.headers.get("user-agent")??"",$=(0,web_helpers_1.extractClientIpFromRequest)(r);if((0,is_bot_1.isBot)(P))return n(r);if("unknown"===O)return injectIntoHtmlResponse(await n(r),g,T);const U=k??N;if(!U)return injectIntoHtmlResponse(await n(r),g,T);f.isPaused()||dispatchUserEvent(e,h,f,m,{userId:E,emailAddress:S,sessionId:O,deviceId:U,fingerprintId:N,userAgent:P,ipAddress:$,eventType:o+w},p);let x=h.get(E);if(x)h.isStale(E)&&!h.isRefreshing(E)&&(h.markRefreshing(E),fetchAndCacheVerdict(e,h,E,S,U,N,O).finally(()=>h.clearRefreshing(E)));else try{x=await fetchAndCacheVerdict(e,h,E,S,U,N,O)}catch{return injectIntoHtmlResponse(await n(r),g,T)}if(x.isFlagged&&!x.isVerified&&u)try{const e=await u({userId:E,emailAddress:S,verdict:x,request:r});if(e)return injectIntoHtmlResponse(e,g,T)}catch(e){p&&p(e,{operation:"checkUser",userId:E,emailAddress:S})}return injectIntoHtmlResponse(await n(r),g,T)}}async function injectIntoHtmlResponse(e,r,s){const t=e.headers.get("content-type");if(!(0,content_type_1.isHtmlContentType)(t??void 0)){if(!s||0===s.length)return e;const r=(0,web_helpers_1.mergeResponseHeaders)(e.headers,void 0,s);return new Response(e.body,{status:e.status,statusText:e.statusText,headers:r})}const i=await e.text(),n=i.lastIndexOf("</body>"),o=-1===n?i+r:i.slice(0,n)+r+i.slice(n),a=(0,web_helpers_1.mergeResponseHeaders)(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(o).length)},s);return a.delete("ETag"),a.delete("Last-Modified"),a.delete("Content-Encoding"),new Response(o,{status:e.status,statusText:e.statusText,headers:a})}function resolveEmail(e,r){if(r)try{const s=r(e);if(s)return s}catch{}const s=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email");if(s)return s}function resolveEmailWithBody(e,r,s){const t=resolveEmail(e,s);if(t)return t;const i=r.email;return"string"==typeof i&&i?i:void 0}function extractSessionIdFromRequest(e,r){if(r)try{const s=r(e);if(s)return s}catch{}return(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid")??"unknown"}function dispatchUserEvent(e,r,s,t,i,n){t.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&&s.pause(1e3*e.error.retryAfter)}).catch(e=>{n&&n(e,{operation:"processUserEvent",userId:i.userId,emailAddress:i.emailAddress})})}async function fetchAndCacheVerdict(e,r,s,t,i,n,o){const a={};let d;i&&"unknown"!==i&&(a.deviceId=i),n&&(a.fingerprintId=n);const c=await Promise.race([e.checkUser(t,a),new Promise(e=>{d=setTimeout(()=>e(null),500)})]);if(clearTimeout(d),!c)return{isFlagged:!1,isVerified:!1,emailAddress:t,sessionId:o,cachedAt:0,ttl:0};const _=c.data?.is_user_flagged??!1;return r.set(s,{isFlagged:_,isVerified:!1,emailAddress:t,sessionId:o}),r.get(s)}async function handleSubmitFp(e,r,s,t){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,o,a;try{const r=s.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{o=s.resolveEmailAddress?s.resolveEmailAddress(e):void 0}catch{}o=o??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")??r.email??void 0;try{a=s.resolveSessionId?s.resolveSessionId(e):void 0}catch{}a=a??r.session_id??(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_sid");const d=(0,web_helpers_1.extractClientIpFromRequest)(e),c=e.headers.get("user-agent")??"";if((0,is_bot_1.isBot)(c))return(0,web_helpers_1.jsonResponse)(200,{success:!0},t);const _=(i.fingerprint_id&&i.fingerprint_id.length>0?i.fingerprint_id:void 0)??(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,s.resolveDeviceId),l=i.fingerprint_id||void 0,u=(0,web_helpers_1.isSecureWebRequest)(e)?"; Secure":"",p=[];if(l&&!(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")&&p.push(`__unshared_fingerprint_id=${encodeURIComponent(l)}; HttpOnly; Path=/; SameSite=Lax${u}`),l){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fp_id");r&&r===l||p.push(`__unshared_fp_id=${encodeURIComponent(l)}; Path=/; SameSite=Lax; Max-Age=31536000${u}`)}let h;if(o&&!(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_email")&&p.push(`__unshared_email=${encodeURIComponent(o)}; HttpOnly; Path=/; SameSite=Lax${u}`),"string"==typeof r.event_type&&r.event_type)h=r.event_type;else{const r=e.headers.get("referer")??e.headers.get("referrer");let s="unknown";if(r)try{const e=new URL(r);s=(e.pathname||"/")+(e.search||"")}catch{}h=s}const f=(e.headers.get("x-idempotency-key")||void 0)??(l&&n?`${l}|${n}|${h}`:void 0);n&&s.client.submitFingerprintEvent(i,{userId:n,emailAddress:o,sessionHash:a,eventType:h,ipAddress:d,userAgent:c,idempotencyKey:f}).catch(e=>{s.onError&&s.onError(e,{operation:"submitFingerprintEvent",userId:n,emailAddress:o})}),n&&o&&!s.rateLimitBackoff.isPaused()&&!s.dispatchDedupe.wasRecentlyDispatched(n,h)&&(s.dispatchDedupe.mark(n,h),s.client.processUserEvent({eventType:h,userId:n,emailAddress:o,ipAddress:d,deviceId:_,fingerprintId:l,sessionHash:a??"unknown",userAgent:c}).then(e=>{e.success&&e.data?.analysis&&s.verdictCache.update(n,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&s.rateLimitBackoff.pause(1e3*e.error.retryAfter)}).catch(e=>{s.onError&&s.onError(e,{operation:"processUserEvent",userId:n,emailAddress:o})}));const m={...t,"Content-Type":"application/json"},w=new Response(JSON.stringify({success:!0}),{status:200,headers:m});for(const e of p)w.headers.append("Set-Cookie",e);return w}catch{return(0,web_helpers_1.jsonResponse)(200,{success:!0},t)}}async function handleVerifyTriggerWeb(e,r,s,t){try{const i=resolveEmailWithBody(e,r??{},s.resolveEmailAddress);if(!i)return(0,web_helpers_1.jsonResponse)(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},t);const n=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,s.resolveDeviceId),o=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,a=await s.client.triggerEmailVerification(i,n,{fingerprintId:o});return a.success?(0,web_helpers_1.jsonResponse)(200,{success:!0,data:a.data},t):(0,web_helpers_1.jsonResponse)(200,{success:!1,error:a.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},t)}catch(e){return s.onError&&s.onError(e,{operation:"verifyTrigger"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},t)}}async function handleVerifyWeb(e,r,s,t){try{const i=resolveEmailWithBody(e,r??{},s.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"}},t);const o=(0,web_helpers_1.extractDeviceIdFromRequestOrUnknown)(e,s.resolveDeviceId),a=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_fingerprint_id")||void 0,d=await s.client.verify(i,o,n,{fingerprintId:a});if(d.success){const r=(0,web_helpers_1.parseCookieFromRequest)(e,"__unshared_uid");return r&&s.verdictCache.update(r,{isVerified:!0}),(0,web_helpers_1.jsonResponse)(200,{success:!0,data:{verified:!0}},t)}return(0,web_helpers_1.jsonResponse)(200,{success:!1,error:d.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}},t)}catch(e){return s.onError&&s.onError(e,{operation:"verify"}),(0,web_helpers_1.jsonResponse)(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},t)}}
@@ -0,0 +1,27 @@
1
+ import type { UnsharedLabsClient } from '../client';
2
+ import type { WebSubmitOptions } from './types';
3
+ /**
4
+ * Web Standard equivalent of `createUnsharedMiddleware` from src/middleware.ts.
5
+ *
6
+ * Returns a handler `(request: Request) => Promise<Response>` suitable for
7
+ * Next.js App Router Route Handlers, Vercel Edge Functions, Cloudflare
8
+ * Workers, Deno Deploy, and any other Web Standard runtime.
9
+ *
10
+ * @example Next.js App Router
11
+ * ```typescript
12
+ * // app/unshared/submit-fingerprint-event/route.ts
13
+ * import { createWebSubmitHandler } from 'unshared-clientjs-sdk/web';
14
+ * import { UnsharedLabsClient } from 'unshared-clientjs-sdk';
15
+ *
16
+ * const client = new UnsharedLabsClient({ apiKey: '...', clientId: '...' });
17
+ * const handler = createWebSubmitHandler(client);
18
+ *
19
+ * export const POST = handler;
20
+ * export const OPTIONS = handler;
21
+ * ```
22
+ *
23
+ * **Error contract:** Never returns 5xx. Upstream failures return HTTP 200
24
+ * with `{ success: false, error: { code: "UPSTREAM_ERROR" } }`. Same contract
25
+ * as the Node.js middleware.
26
+ */
27
+ export declare function createWebSubmitHandler(client: UnsharedLabsClient, options?: WebSubmitOptions): (request: Request) => Promise<Response>;
@@ -0,0 +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)}}}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Web Standard Request/Response handler types for serverless/edge environments.
3
+ *
4
+ * These types use the global `Request` and `Response` from the Fetch API,
5
+ * available in: Next.js App Router Route Handlers, Vercel Edge Functions,
6
+ * Cloudflare Workers, Deno Deploy, Bun, and Node.js 18+.
7
+ *
8
+ * The DOM lib reference above scopes the Fetch API globals to this file only,
9
+ * avoiding `window`/`document` pollution in the Node.js middleware code.
10
+ */
11
+ import type { Verdict } from '../middleware/verdict-cache';
12
+ /** A downstream handler that returns a Response. */
13
+ export type WebHandler = (request: Request) => Response | Promise<Response>;
14
+ /** Middleware that wraps a downstream handler, optionally intercepting the response. */
15
+ export type WebMiddleware = (request: Request, next: WebHandler) => Response | Promise<Response>;
16
+ export interface WebSubmitOptions {
17
+ /** Override userId extractor. Falls back to body.user_id. */
18
+ userIdExtractor?: (req: Request) => string | undefined;
19
+ /** Override eventType extractor. Falls back to body.event_type. */
20
+ eventTypeExtractor?: (req: Request) => string | undefined;
21
+ /** Override sessionId extractor. Falls back to X-Session-Id header, then body.session_id. */
22
+ sessionIdExtractor?: (req: Request) => string | undefined;
23
+ /** Override IP address extractor. Falls back to CF-Connecting-IP / X-Real-IP / X-Forwarded-For headers. */
24
+ ipAddressExtractor?: (req: Request) => string | undefined;
25
+ /** Default event type when none is extractable. @default "browser_event" */
26
+ defaultEventType?: string;
27
+ /**
28
+ * Route prefix the handler responds under.
29
+ * @default "/unshared"
30
+ */
31
+ routePrefix?: string;
32
+ /**
33
+ * Allowed CORS origins for the fingerprint route.
34
+ * Use `"*"` to allow all origins, or pass a specific origin / array of origins.
35
+ * The handler handles OPTIONS preflight automatically when this is set.
36
+ */
37
+ corsOrigins?: string | string[];
38
+ }
39
+ export interface WebProtectionConfig {
40
+ /**
41
+ * Required. Resolves the current user's ID from the request.
42
+ * Return undefined for anonymous/logged-out visitors.
43
+ */
44
+ userId: (req: Request) => string | undefined;
45
+ /**
46
+ * Resolves the current user's email address from the request.
47
+ * Falls back to HttpOnly cookie → request body when not configured.
48
+ */
49
+ emailAddress?: (req: Request) => string | undefined;
50
+ /** Route prefix for internal routes. @default "/__unshared" */
51
+ routePrefix?: string;
52
+ /** Allowed CORS origins for /__unshared/* routes. */
53
+ corsOrigins?: string | string[];
54
+ /** Verdict cache TTL in ms. @default 60000 */
55
+ cacheTTL?: number;
56
+ /** Paths to skip entirely (static assets, health checks). */
57
+ skipPaths?: string[];
58
+ /** When set, only paths matching one of these prefixes get events dispatched and checkUser called. */
59
+ includePathPrefix?: string[];
60
+ /** Resolves a custom session ID. Falls back to __unshared_sid cookie. */
61
+ sessionId?: (req: Request) => string | undefined;
62
+ /**
63
+ * Resolves a device ID from the request.
64
+ * Falls back to X-Device-Id header → __unshared_fp_id cookie.
65
+ */
66
+ deviceId?: (req: Request) => string | undefined;
67
+ /**
68
+ * Inline JavaScript bundle for the frontend SDK, served at /__unshared/fp.js.
69
+ *
70
+ * In Node.js environments the middleware reads this from node_modules via
71
+ * `require.resolve('unshared-frontend-sdk/dist/index.umd.js')`. Edge runtimes
72
+ * do not have filesystem access, so the bundle must be passed as a string
73
+ * (typically imported via a bundler):
74
+ *
75
+ * ```typescript
76
+ * import fingerprintSdkBundle from 'unshared-frontend-sdk/dist/index.umd.js?raw';
77
+ * createWebProtectionMiddleware(client, { ..., fingerprintSdkBundle });
78
+ * ```
79
+ *
80
+ * If omitted, the fp.js route returns 404. The inline script degrades
81
+ * gracefully — the cached-fingerprint path still works.
82
+ */
83
+ fingerprintSdkBundle?: string;
84
+ /**
85
+ * Called when a flagged, unverified user makes a request.
86
+ *
87
+ * Return a `Response` to block/redirect, or `null` to pass through (injection
88
+ * still happens). Exceptions are caught — the request passes through on error.
89
+ *
90
+ * This differs from the Node.js middleware: in Web Standard environments
91
+ * Response objects are immutable, so the callback returns a new Response
92
+ * rather than mutating an existing one.
93
+ */
94
+ onFlagged?: (context: {
95
+ userId: string;
96
+ emailAddress: string;
97
+ verdict: Verdict;
98
+ request: Request;
99
+ }) => Response | Promise<Response> | null;
100
+ /**
101
+ * Called when a background SDK operation fails (fire-and-forget API calls,
102
+ * verdict refreshes, etc.). Use this to pipe errors to your logging or
103
+ * monitoring system for observability.
104
+ */
105
+ onError?: (error: unknown, context: {
106
+ operation: 'processUserEvent' | 'submitFingerprintEvent' | 'checkUser' | 'verifyTrigger' | 'verify';
107
+ userId?: string;
108
+ emailAddress?: string;
109
+ }) => void;
110
+ }
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0});