unshared-clientjs-sdk 2.0.0-rc.9 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +54 -16
  2. package/dist/client.d.ts +43 -35
  3. package/dist/client.js +1 -1
  4. package/dist/esm/client.d.mts +43 -35
  5. package/dist/esm/client.mjs +1 -1
  6. package/dist/esm/index.d.mts +5 -3
  7. package/dist/esm/index.mjs +1 -1
  8. package/dist/esm/middleware/dispatch-dedupe.d.mts +11 -0
  9. package/dist/esm/middleware/dispatch-dedupe.mjs +1 -0
  10. package/dist/esm/middleware/index.d.mts +32 -12
  11. package/dist/esm/middleware/index.mjs +1 -1
  12. package/dist/esm/middleware/injection/fingerprint-script.d.mts +11 -5
  13. package/dist/esm/middleware/injection/fingerprint-script.mjs +1 -1
  14. package/dist/esm/middleware/response-interceptor.d.mts +10 -8
  15. package/dist/esm/middleware/response-interceptor.mjs +1 -1
  16. package/dist/esm/middleware/routes/submit-fp.d.mts +16 -9
  17. package/dist/esm/middleware/routes/submit-fp.mjs +1 -1
  18. package/dist/esm/middleware/routes/verify.d.mts +13 -8
  19. package/dist/esm/middleware/routes/verify.mjs +1 -1
  20. package/dist/esm/middleware/utils/client-ip.d.mts +6 -0
  21. package/dist/esm/middleware/utils/client-ip.mjs +1 -0
  22. package/dist/esm/middleware/utils/cookies.d.mts +6 -0
  23. package/dist/esm/middleware/utils/cookies.mjs +1 -0
  24. package/dist/esm/middleware/utils/device-id.d.mts +19 -0
  25. package/dist/esm/middleware/utils/device-id.mjs +1 -0
  26. package/dist/esm/middleware/utils/flagged-response.d.mts +5 -0
  27. package/dist/esm/middleware/utils/flagged-response.mjs +1 -0
  28. package/dist/esm/middleware/utils/http-helpers.d.mts +21 -0
  29. package/dist/esm/middleware/utils/http-helpers.mjs +1 -0
  30. package/dist/esm/middleware/utils/include-path.d.mts +6 -0
  31. package/dist/esm/middleware/utils/include-path.mjs +1 -0
  32. package/dist/esm/middleware/utils/is-bot.d.mts +5 -0
  33. package/dist/esm/middleware/utils/is-bot.mjs +1 -0
  34. package/dist/esm/middleware/utils/secure.d.mts +3 -0
  35. package/dist/esm/middleware/utils/secure.mjs +1 -0
  36. package/dist/esm/middleware/utils/sentinel-user-id.d.mts +10 -0
  37. package/dist/esm/middleware/utils/sentinel-user-id.mjs +1 -0
  38. package/dist/esm/middleware/utils/skip-paths.mjs +1 -1
  39. package/dist/esm/middleware/verdict-cache.d.mts +12 -1
  40. package/dist/esm/middleware/verdict-cache.mjs +1 -1
  41. package/dist/esm/middleware.d.mts +13 -10
  42. package/dist/esm/middleware.mjs +1 -1
  43. package/dist/esm/types.d.mts +44 -0
  44. package/dist/esm/types.mjs +1 -0
  45. package/dist/esm/web/index.d.mts +17 -0
  46. package/dist/esm/web/index.mjs +1 -0
  47. package/dist/esm/web/protection-handler.d.mts +28 -0
  48. package/dist/esm/web/protection-handler.mjs +1 -0
  49. package/dist/esm/web/submit-handler.d.mts +27 -0
  50. package/dist/esm/web/submit-handler.mjs +1 -0
  51. package/dist/esm/web/types.d.mts +110 -0
  52. package/dist/esm/web/types.mjs +1 -0
  53. package/dist/esm/web/web-helpers.d.mts +55 -0
  54. package/dist/esm/web/web-helpers.mjs +1 -0
  55. package/dist/index.d.ts +5 -3
  56. package/dist/index.js +1 -1
  57. package/dist/middleware/dispatch-dedupe.d.ts +11 -0
  58. package/dist/middleware/dispatch-dedupe.js +1 -0
  59. package/dist/middleware/index.d.ts +32 -12
  60. package/dist/middleware/index.js +1 -1
  61. package/dist/middleware/injection/fingerprint-script.d.ts +11 -5
  62. package/dist/middleware/injection/fingerprint-script.js +1 -1
  63. package/dist/middleware/response-interceptor.d.ts +10 -8
  64. package/dist/middleware/response-interceptor.js +1 -1
  65. package/dist/middleware/routes/submit-fp.d.ts +16 -9
  66. package/dist/middleware/routes/submit-fp.js +1 -1
  67. package/dist/middleware/routes/verify.d.ts +13 -8
  68. package/dist/middleware/routes/verify.js +1 -1
  69. package/dist/middleware/utils/client-ip.d.ts +6 -0
  70. package/dist/middleware/utils/client-ip.js +1 -0
  71. package/dist/middleware/utils/cookies.d.ts +6 -0
  72. package/dist/middleware/utils/cookies.js +1 -0
  73. package/dist/middleware/utils/device-id.d.ts +19 -0
  74. package/dist/middleware/utils/device-id.js +1 -0
  75. package/dist/middleware/utils/flagged-response.d.ts +5 -0
  76. package/dist/middleware/utils/flagged-response.js +1 -0
  77. package/dist/middleware/utils/http-helpers.d.ts +21 -0
  78. package/dist/middleware/utils/http-helpers.js +1 -0
  79. package/dist/middleware/utils/include-path.d.ts +6 -0
  80. package/dist/middleware/utils/include-path.js +1 -0
  81. package/dist/middleware/utils/is-bot.d.ts +5 -0
  82. package/dist/middleware/utils/is-bot.js +1 -0
  83. package/dist/middleware/utils/secure.d.ts +3 -0
  84. package/dist/middleware/utils/secure.js +1 -0
  85. package/dist/middleware/utils/sentinel-user-id.d.ts +10 -0
  86. package/dist/middleware/utils/sentinel-user-id.js +1 -0
  87. package/dist/middleware/utils/skip-paths.js +1 -1
  88. package/dist/middleware/verdict-cache.d.ts +12 -1
  89. package/dist/middleware/verdict-cache.js +1 -1
  90. package/dist/middleware.d.ts +13 -10
  91. package/dist/middleware.js +1 -1
  92. package/dist/types.d.ts +44 -0
  93. package/dist/types.js +1 -0
  94. package/dist/web/index.d.ts +17 -0
  95. package/dist/web/index.js +1 -0
  96. package/dist/web/protection-handler.d.ts +28 -0
  97. package/dist/web/protection-handler.js +1 -0
  98. package/dist/web/submit-handler.d.ts +27 -0
  99. package/dist/web/submit-handler.js +1 -0
  100. package/dist/web/types.d.ts +110 -0
  101. package/dist/web/types.js +1 -0
  102. package/dist/web/web-helpers.d.ts +55 -0
  103. package/dist/web/web-helpers.js +1 -0
  104. package/package.json +7 -10
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # unshared-clientjs-sdk
2
2
 
3
- Server-side Node.js SDK for [Unshared Labs](https://unsharedlabs.com) — detect account sharing, analyze user events for fraud, and run email verification flows.
3
+ Server-side Node.js SDK for [Unshared](https://unshared.ai) — detect account sharing, analyze user events for fraud, and run email verification flows.
4
4
 
5
5
  ---
6
6
 
@@ -17,9 +17,9 @@ npm install unshared-clientjs-sdk
17
17
  ## Quick Start
18
18
 
19
19
  ```typescript
20
- import { UnsharedLabsClient } from 'unshared-clientjs-sdk';
20
+ import { UnsharedClient } from 'unshared-clientjs-sdk';
21
21
 
22
- const client = new UnsharedLabsClient({
22
+ const client = new UnsharedClient({
23
23
  apiKey: process.env.UNSHARED_API_KEY, // usk_…
24
24
  });
25
25
  ```
@@ -112,44 +112,82 @@ await client.submitFingerprintEvent(fingerprint, {
112
112
 
113
113
  ---
114
114
 
115
- ## Express Middleware
115
+ ## Protection Middleware (Recommended)
116
116
 
117
- The middleware adds a proxy route (`POST /unshared/submit-fingerprint-event`) that the browser SDK calls. It handles forwarding fingerprints to Unshared Labs and attaching your API key.
117
+ `unsharedBoundToUser` is the full-featured middleware: auto-injects the fingerprint script, enforces verdicts, handles email verification flows, and dispatches events.
118
118
 
119
119
  ```typescript
120
- import { createUnsharedMiddleware } from 'unshared-clientjs-sdk/middleware';
120
+ import { UnsharedClient, unsharedBoundToUser, flaggedResponse } from 'unshared-clientjs-sdk';
121
121
 
122
- // express.json() must come before this middleware
122
+ app.set('trust proxy', 1);
123
123
  app.use(express.json());
124
124
 
125
- app.use(createUnsharedMiddleware(client, {
126
- userIdExtractor: (req) => req.user?.id, // attach logged-in user
125
+ const client = new UnsharedClient({ apiKey: process.env.UNSHARED_API_KEY! });
126
+
127
+ app.use(unsharedBoundToUser(client, {
128
+ userId: (req) => req.cookies?.userId,
129
+ emailAddress: (req) => req.cookies?.email,
130
+ includePathPrefix: ['/api/'],
131
+ onFlagged: ({ emailAddress, res }) => {
132
+ res.status(403).json(flaggedResponse(emailAddress));
133
+ },
127
134
  }));
128
135
  ```
129
136
 
130
- **Prerequisites:**
131
- - Mount `express.json()` before the middleware
132
- - `user_id` is automatically removed from `req.body` after being read — downstream logging won't capture it
137
+ **Smoke test:** `curl http://localhost:3000/__unshared/status` — returns `{ "status": "anonymous" | "ok" | "flagged" }`.
133
138
 
134
- **Options:**
139
+ **Key options:**
140
+
141
+ | Option | Type | Default | Description |
142
+ |--------|------|---------|-------------|
143
+ | `userId` | `(req) => string \| undefined` | — | **Required.** Resolve the current user's ID |
144
+ | `emailAddress` | `(req) => string \| undefined` | — | Resolve the current user's email |
145
+ | `routePrefix` | `string` | `"/__unshared"` | Route mount prefix |
146
+ | `includePathPrefix` | `string[]` | — | Only these path prefixes trigger verdicts and events |
147
+ | `onFlagged` | `(ctx) => void` | — | Called when a flagged user makes a request |
148
+ | `disableBotFilter` | `boolean` | `false` | Skip bot UA filter (enable for E2E testing) |
149
+ | `checkUserTimeoutMs` | `number` | `500` | Timeout (ms) for checkUser API calls; fails open on timeout |
150
+ | `skipPaths` | `string[]` | — | Paths to skip entirely (static assets, health checks) |
151
+ | `corsOrigins` | `string \| string[]` | — | Allowed CORS origins; handles OPTIONS preflight |
152
+ | `onError` | `(error, ctx) => void` | — | Called on background SDK errors for observability |
153
+
154
+ See [quickstart](./docs/quickstart.md) and [flag semantics](./docs/flag-semantics.md) for full setup and testing details.
155
+
156
+ ---
157
+
158
+ ## Simple Fingerprint Middleware
159
+
160
+ `createUnsharedMiddleware` is a lightweight alternative that only proxies fingerprint events — no verdicts, no script injection, no verification flows. Use `unsharedBoundToUser` unless you have a specific reason not to.
161
+
162
+ ```typescript
163
+ import { createUnsharedMiddleware } from 'unshared-clientjs-sdk';
164
+
165
+ app.use(express.json());
166
+ app.use(createUnsharedMiddleware(client, {
167
+ userIdExtractor: (req) => req.user?.id,
168
+ }));
169
+ ```
135
170
 
136
171
  | Option | Type | Default | Description |
137
172
  |--------|------|---------|-------------|
138
173
  | `userIdExtractor` | `(req) => string \| undefined` | — | Pull user ID from your auth session |
139
174
  | `eventTypeExtractor` | `(req) => string \| undefined` | — | Override event type |
140
175
  | `sessionIdExtractor` | `(req) => string \| undefined` | — | Override session ID |
176
+ | `ipAddressExtractor` | `(req) => string \| undefined` | — | Override IP address |
141
177
  | `defaultEventType` | `string` | `"browser_event"` | Fallback event type |
142
178
  | `routePrefix` | `string` | `"/unshared"` | Route mount prefix |
143
- | `corsOrigins` | `string \| string[]` | — | Allowed CORS origins; handles OPTIONS preflight automatically |
179
+ | `corsOrigins` | `string \| string[]` | — | Allowed CORS origins |
180
+
181
+ > **Note:** This middleware uses a different default prefix (`/unshared`) and route (`submit-fingerprint-event`) than `unsharedBoundToUser` (`/__unshared`, `submit-fp`).
144
182
 
145
183
  ---
146
184
 
147
185
  ## Configuration
148
186
 
149
187
  ```typescript
150
- new UnsharedLabsClient({
188
+ new UnsharedClient({
151
189
  apiKey: 'usk_…', // required
152
- baseUrl: 'https://api-ingress.unsharedlabs.com', // optional
190
+ baseUrl: 'https://api.unshared.ai', // optional
153
191
  timeout: 10_000, // optional, ms
154
192
  maxRetries: 3, // optional
155
193
  });
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { FingerprintWireFormat } from '@unshared-labs/shared-types';
2
- export interface UnsharedLabsClientConfig {
2
+ export interface UnsharedClientConfig {
3
3
  /**
4
4
  * Secret API key. Must be kept server-side.
5
5
  * Format: usk_…
@@ -7,7 +7,7 @@ export interface UnsharedLabsClientConfig {
7
7
  apiKey: string;
8
8
  /**
9
9
  * Base URL of the Unshared Labs V2 ingress.
10
- * @default "https://api-ingress.unsharedlabs.com"
10
+ * @default "https://api.unshared.ai"
11
11
  */
12
12
  baseUrl?: string;
13
13
  /**
@@ -20,8 +20,28 @@ export interface UnsharedLabsClientConfig {
20
20
  * @default 3
21
21
  */
22
22
  maxRetries?: number;
23
+ /**
24
+ * Custom fetch implementation. Use this to configure connection pooling,
25
+ * custom HTTP agents, or proxies. Defaults to the global `fetch`.
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * import { Agent, fetch as undiciFetch } from 'undici';
30
+ *
31
+ * const agent = new Agent({
32
+ * keepAliveTimeout: 30_000,
33
+ * connections: 50,
34
+ * });
35
+ *
36
+ * const client = new UnsharedClient({
37
+ * apiKey: 'usk_...',
38
+ * fetch: (url, init) => undiciFetch(url, { ...init, dispatcher: agent }),
39
+ * });
40
+ * ```
41
+ */
42
+ fetch?: typeof globalThis.fetch;
23
43
  }
24
- export interface UnsharedLabsError {
44
+ export interface UnsharedError {
25
45
  code: string;
26
46
  message: string;
27
47
  details?: Record<string, unknown>;
@@ -35,14 +55,27 @@ export interface UnsharedLabsError {
35
55
  export interface ApiResult<T = unknown> {
36
56
  success: boolean;
37
57
  data?: T;
38
- error?: UnsharedLabsError;
58
+ error?: UnsharedError;
39
59
  status: number;
40
60
  }
41
61
  export interface SubmitFingerprintOptions {
42
62
  userId?: string;
63
+ /** SDK encrypts before sending. */
64
+ emailAddress?: string;
43
65
  sessionHash?: string;
44
66
  eventType?: string;
67
+ /** SDK encrypts before sending. */
45
68
  ipAddress?: string;
69
+ /** SDK encrypts before sending. */
70
+ userAgent?: string;
71
+ /**
72
+ * Client-supplied idempotency key. Forwarded verbatim as X-Idempotency-Key
73
+ * so the backend's ON CONFLICT (idempotency_key) catches duplicates across
74
+ * reloads, tabs, and concurrent SDK instances. When omitted, a fresh UUID
75
+ * is generated (best-effort dedup only within a single submitFingerprintEvent
76
+ * call's retries).
77
+ */
78
+ idempotencyKey?: string;
46
79
  }
47
80
  export interface SubmitFingerprintResult {
48
81
  hash: string;
@@ -53,12 +86,14 @@ export interface SubmitFingerprintResult {
53
86
  }
54
87
  export interface ProcessUserEventParams {
55
88
  eventType: string;
89
+ /** SDK encrypts before sending. */
56
90
  userId: string;
57
- /** Plaintext — SDK does not encrypt this field. */
91
+ /** SDK encrypts before sending. */
58
92
  ipAddress: string;
59
93
  /** SDK encrypts before sending. */
60
94
  deviceId: string;
61
95
  sessionHash: string;
96
+ /** SDK encrypts before sending. */
62
97
  userAgent: string;
63
98
  /** SDK encrypts before sending. */
64
99
  emailAddress: string;
@@ -95,29 +130,14 @@ export interface VerifyResult {
95
130
  verified: boolean;
96
131
  reason?: 'not_found' | 'code_mismatch' | 'code_expired';
97
132
  }
98
- export interface VerificationFlowStep {
99
- type: 'message' | 'email_input' | 'otp_input' | 'support_link';
100
- title: string;
101
- body: string;
102
- buttonText?: string;
103
- url?: string;
104
- }
105
- export interface VerificationFlowConfigResult {
106
- steps: VerificationFlowStep[];
107
- branding?: {
108
- companyName?: string;
109
- logoUrl?: string;
110
- primaryColor?: string;
111
- supportEmail?: string;
112
- };
113
- }
114
- export declare class UnsharedLabsClient {
133
+ export declare class UnsharedClient {
115
134
  private readonly _apiKey;
116
135
  private readonly _baseUrl;
117
136
  private readonly _timeout;
118
137
  private readonly _maxRetries;
119
138
  private readonly _encryptionKey;
120
- constructor(config: UnsharedLabsClientConfig);
139
+ private readonly _customFetch;
140
+ constructor(config: UnsharedClientConfig);
121
141
  private _encrypt;
122
142
  /**
123
143
  * Core HTTP method with retry logic.
@@ -181,16 +201,4 @@ export declare class UnsharedLabsClient {
181
201
  verify(emailAddress: string, deviceId: string, code: string, opts?: {
182
202
  fingerprintId?: string;
183
203
  }): Promise<ApiResult<VerifyResult>>;
184
- /**
185
- * Fetch the verification flow configuration for this company.
186
- * Maps to: GET /v2/verification-flow-config
187
- *
188
- * Returns the flow steps and branding configured by the Unshared Labs
189
- * team for this company. The middleware uses this to render the
190
- * verification overlay.
191
- *
192
- * Returns `null` on any failure (network error, 4xx, 5xx) so the
193
- * middleware can fall back to the default flow.
194
- */
195
- getVerificationFlowConfig(): Promise<VerificationFlowConfigResult | null>;
196
204
  }
package/dist/client.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.UnsharedLabsClient=void 0;const crypto_1=require("crypto"),util_1=require("./util"),DEFAULT_BASE_URL="https://api-ingress.unsharedlabs.com",DEFAULT_TIMEOUT_MS=1e4,DEFAULT_MAX_RETRIES=3,MAX_DELAY_MS=3e4,BASE_DELAY_MS=1e3;function sleep(e){return new Promise(s=>setTimeout(s,e))}function retryDelay(e){const s=Math.min(1e3*Math.pow(2,e-1),3e4),t=s*(.5*Math.random()-.25);return Math.max(0,s+t)}async function parseErrorBody(e){const s=await e.text().catch(()=>"");try{const t=JSON.parse(s);return t?.error?.code?{code:t.error.code,message:t.error.message??"Unknown error",details:t.error.details}:{code:"UNKNOWN_ERROR",message:s||e.statusText}}catch{return{code:"UNKNOWN_ERROR",message:s||e.statusText}}}class UnsharedLabsClient{constructor(e){if(!e.apiKey||""===e.apiKey.trim())throw new Error("apiKey is required");this.i=e.apiKey,this.o=e.baseUrl?e.baseUrl.replace(/\/$/,""):DEFAULT_BASE_URL,this.h=e.timeout??1e4,this.u=e.maxRetries??3,this.l=(0,crypto_1.createHash)("sha256").update(e.apiKey).digest()}_(e){return(0,util_1.encryptData)(e,this.l)}async p(e,s){const t=this.u+1;let r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let i=1;i<=t;i++){i>1&&await sleep(retryDelay(i-1));const t=new AbortController,n=setTimeout(()=>t.abort(),this.h);try{const i=await fetch(e,{method:s.method,headers:{"X-API-Key":this.i,...s.headers},body:s.body,signal:t.signal});if(clearTimeout(n),i.ok){const e=await i.text().catch(()=>"{}");let s;try{s=JSON.parse(e)}catch{s={}}const t="data"in s?s.data:s;return{success:!0,status:i.status,data:t}}const a=await parseErrorBody(i);if(i.status>=400&&i.status<500){if(429===i.status){const e=i.headers.get("Retry-After");if(null!=e){const s=parseInt(e,10);isNaN(s)||(a.retryAfter=s)}}return{success:!1,status:i.status,error:a}}r={success:!1,status:i.status,error:a}}catch(e){clearTimeout(n),r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return r}async submitFingerprintEvent(e,s){const t={hash:e.full_hash,stable_hash:e.fingerprint_id,collected_at:e.timestamp,is_incognito:e.isIncognito,components:e.components,version:e.version};return null!=s?.userId&&(t.user_id=this._(s.userId)),null!=s?.sessionHash&&(t.session_hash=s.sessionHash),null!=s?.eventType&&(t.event_type=s.eventType),null!=s?.ipAddress&&(t.ip_address=s.ipAddress),this.p(`${this.o}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":(0,crypto_1.randomUUID)()},body:JSON.stringify(t)})}async processUserEvent(e){const s={event_type:e.eventType,user_id:e.userId,ip_address:e.ipAddress,device_id:this._(e.deviceId),session_hash:e.sessionHash,user_agent:e.userAgent,email_address:this._(e.emailAddress)};return null!=e.fingerprintId&&(s.fingerprint_id=this._(e.fingerprintId)),null!=e.subscriptionStatus&&(s.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(s.event_details=e.eventDetails),this.p(`${this.o}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})}async checkUser(e,s){const t="string"==typeof s?{deviceId:s}:s;if(!t.deviceId&&!t.fingerprintId)return{success:!0,status:200,data:{is_user_flagged:!1}};const r=new URLSearchParams;r.set("email_address",this._(e)),t.deviceId&&r.set("device_id",this._(t.deviceId)),t.fingerprintId&&r.set("fingerprint_id",this._(t.fingerprintId));const i=await this.p(`${this.o}/v2/check-user?${r}`,{method:"GET"});return i.success?i:{success:!0,status:200,data:{is_user_flagged:!1}}}async triggerEmailVerification(e,s,t){const r={email_address:this._(e),device_id:this._(s)};t?.fingerprintId&&(r.fingerprint_id=this._(t.fingerprintId));const i=await this.p(`${this.o}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});return!i.success&&(0===i.status||i.status>=500)?{success:!1,status:i.status,error:{code:"DELIVERY_FAILED",message:i.error?.message??"Delivery failed"}}:i}async verify(e,s,t,r){const i={email_address:this._(e),device_id:this._(s),code:this._(t)};r?.fingerprintId&&(i.fingerprint_id=this._(r.fingerprintId));const n=await this.p(`${this.o}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});return!n.success&&(0===n.status||n.status>=500)?{success:!1,status:n.status,error:{code:"DELIVERY_FAILED",message:n.error?.message??"Delivery failed"}}:n.success&&!1===n.data?.verified?{success:!1,status:n.status,error:{code:"VERIFICATION_FAILED",message:"Code is incorrect or expired",details:n.data.reason?{reason:n.data.reason}:void 0}}:n}async getVerificationFlowConfig(){const e=await this.p(`${this.o}/v2/verification-flow-config`,{method:"GET"});return e.success&&e.data?e.data:null}}exports.UnsharedLabsClient=UnsharedLabsClient;
1
+ "use strict";Object.defineProperty(exports,"t",{value:!0}),exports.UnsharedClient=void 0;const crypto_1=require("crypto"),util_1=require("./util"),DEFAULT_BASE_URL="https://api.unshared.ai",DEFAULT_TIMEOUT_MS=1e4,DEFAULT_MAX_RETRIES=3,MAX_DELAY_MS=3e4,BASE_DELAY_MS=1e3;function sleep(e){return new Promise(s=>setTimeout(s,e))}function retryDelay(e){const s=Math.min(1e3*Math.pow(2,e-1),3e4),t=s*(.5*Math.random()-.25);return Math.max(0,s+t)}async function parseErrorBody(e){const s=await e.text().catch(()=>"");try{const t=JSON.parse(s);return t?.error?.code?{code:t.error.code,message:t.error.message??"Unknown error",details:t.error.details}:{code:"UNKNOWN_ERROR",message:s||e.statusText}}catch{return{code:"UNKNOWN_ERROR",message:s||e.statusText}}}class UnsharedClient{constructor(e){if(!e.apiKey||""===e.apiKey.trim())throw new Error("apiKey is required");this.i=e.apiKey,this.o=e.baseUrl?e.baseUrl.replace(/\/$/,""):DEFAULT_BASE_URL,this.h=e.timeout??1e4,this.u=e.maxRetries??3,this.l=(0,crypto_1.createHash)("sha256").update(e.apiKey).digest(),this._=e.fetch}p(e){return(0,util_1.encryptData)(e,this.l)}async m(e,s){const t=this.u+1;let r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let i=1;i<=t;i++){i>1&&await sleep(retryDelay(i-1));const t=new AbortController,a=setTimeout(()=>t.abort(),this.h);try{const i=this._??globalThis.fetch,n=await i(e,{method:s.method,headers:{"X-API-Key":this.i,...s.headers},body:s.body,signal:t.signal});if(clearTimeout(a),n.ok){const e=await n.text().catch(()=>"{}");let s;try{s=JSON.parse(e)}catch{s={}}const t="data"in s?s.data:s;return{success:!0,status:n.status,data:t}}const o=await parseErrorBody(n);if(n.status>=400&&n.status<500){if(429===n.status){const e=n.headers.get("Retry-After");if(null!=e){const s=parseInt(e,10);isNaN(s)||(o.retryAfter=s)}}return{success:!1,status:n.status,error:o}}r={success:!1,status:n.status,error:o}}catch(e){clearTimeout(a),r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return r}async submitFingerprintEvent(e,s){const t={hash:e.full_hash,stable_hash:e.fingerprint_id,collected_at:e.timestamp,is_incognito:e.isIncognito,components:e.components,version:e.version};return null!=s?.userId&&(t.user_id=this.p(s.userId)),null!=s?.emailAddress&&(t.email_address=this.p(s.emailAddress)),null!=s?.sessionHash&&(t.session_hash=s.sessionHash),null!=s?.eventType&&(t.event_type=s.eventType),null!=s?.ipAddress&&(t.ip_address=this.p(s.ipAddress)),null!=s?.userAgent&&(t.user_agent=this.p(s.userAgent)),this.m(`${this.o}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":s?.idempotencyKey??(0,crypto_1.randomUUID)()},body:JSON.stringify(t)})}async processUserEvent(e){const s={event_type:e.eventType,user_id:this.p(e.userId),ip_address:this.p(e.ipAddress),device_id:this.p(e.deviceId),session_hash:e.sessionHash,user_agent:this.p(e.userAgent),email_address:this.p(e.emailAddress)};return null!=e.fingerprintId&&(s.fingerprint_id=this.p(e.fingerprintId)),null!=e.subscriptionStatus&&(s.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(s.event_details=e.eventDetails),this.m(`${this.o}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})}async checkUser(e,s){const t="string"==typeof s?{deviceId:s}:s;if(!t.deviceId&&!t.fingerprintId)return{success:!0,status:200,data:{is_user_flagged:!1}};const r=new URLSearchParams;r.set("email_address",this.p(e)),t.deviceId&&r.set("device_id",this.p(t.deviceId)),t.fingerprintId&&r.set("fingerprint_id",this.p(t.fingerprintId));const i=await this.m(`${this.o}/v2/check-user?${r}`,{method:"GET"});return i.success?i:{success:!0,status:200,data:{is_user_flagged:!1}}}async triggerEmailVerification(e,s,t){const r={email_address:this.p(e),device_id:this.p(s)};t?.fingerprintId&&(r.fingerprint_id=this.p(t.fingerprintId));const i=await this.m(`${this.o}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});return!i.success&&(0===i.status||i.status>=500)?{success:!1,status:i.status,error:{code:"DELIVERY_FAILED",message:i.error?.message??"Delivery failed"}}:i}async verify(e,s,t,r){const i={email_address:this.p(e),device_id:this.p(s),code:this.p(t)};r?.fingerprintId&&(i.fingerprint_id=this.p(r.fingerprintId));const a=await this.m(`${this.o}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});return!a.success&&(0===a.status||a.status>=500)?{success:!1,status:a.status,error:{code:"DELIVERY_FAILED",message:a.error?.message??"Delivery failed"}}:a.success&&!1===a.data?.verified?{success:!1,status:a.status,error:{code:"VERIFICATION_FAILED",message:"Code is incorrect or expired",details:a.data.reason?{reason:a.data.reason}:void 0}}:a}}exports.UnsharedClient=UnsharedClient;
@@ -1,5 +1,5 @@
1
1
  import type { FingerprintWireFormat } from '@unshared-labs/shared-types';
2
- export interface UnsharedLabsClientConfig {
2
+ export interface UnsharedClientConfig {
3
3
  /**
4
4
  * Secret API key. Must be kept server-side.
5
5
  * Format: usk_…
@@ -7,7 +7,7 @@ export interface UnsharedLabsClientConfig {
7
7
  apiKey: string;
8
8
  /**
9
9
  * Base URL of the Unshared Labs V2 ingress.
10
- * @default "https://api-ingress.unsharedlabs.com"
10
+ * @default "https://api.unshared.ai"
11
11
  */
12
12
  baseUrl?: string;
13
13
  /**
@@ -20,8 +20,28 @@ export interface UnsharedLabsClientConfig {
20
20
  * @default 3
21
21
  */
22
22
  maxRetries?: number;
23
+ /**
24
+ * Custom fetch implementation. Use this to configure connection pooling,
25
+ * custom HTTP agents, or proxies. Defaults to the global `fetch`.
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * import { Agent, fetch as undiciFetch } from 'undici';
30
+ *
31
+ * const agent = new Agent({
32
+ * keepAliveTimeout: 30_000,
33
+ * connections: 50,
34
+ * });
35
+ *
36
+ * const client = new UnsharedClient({
37
+ * apiKey: 'usk_...',
38
+ * fetch: (url, init) => undiciFetch(url, { ...init, dispatcher: agent }),
39
+ * });
40
+ * ```
41
+ */
42
+ fetch?: typeof globalThis.fetch;
23
43
  }
24
- export interface UnsharedLabsError {
44
+ export interface UnsharedError {
25
45
  code: string;
26
46
  message: string;
27
47
  details?: Record<string, unknown>;
@@ -35,14 +55,27 @@ export interface UnsharedLabsError {
35
55
  export interface ApiResult<T = unknown> {
36
56
  success: boolean;
37
57
  data?: T;
38
- error?: UnsharedLabsError;
58
+ error?: UnsharedError;
39
59
  status: number;
40
60
  }
41
61
  export interface SubmitFingerprintOptions {
42
62
  userId?: string;
63
+ /** SDK encrypts before sending. */
64
+ emailAddress?: string;
43
65
  sessionHash?: string;
44
66
  eventType?: string;
67
+ /** SDK encrypts before sending. */
45
68
  ipAddress?: string;
69
+ /** SDK encrypts before sending. */
70
+ userAgent?: string;
71
+ /**
72
+ * Client-supplied idempotency key. Forwarded verbatim as X-Idempotency-Key
73
+ * so the backend's ON CONFLICT (idempotency_key) catches duplicates across
74
+ * reloads, tabs, and concurrent SDK instances. When omitted, a fresh UUID
75
+ * is generated (best-effort dedup only within a single submitFingerprintEvent
76
+ * call's retries).
77
+ */
78
+ idempotencyKey?: string;
46
79
  }
47
80
  export interface SubmitFingerprintResult {
48
81
  hash: string;
@@ -53,12 +86,14 @@ export interface SubmitFingerprintResult {
53
86
  }
54
87
  export interface ProcessUserEventParams {
55
88
  eventType: string;
89
+ /** SDK encrypts before sending. */
56
90
  userId: string;
57
- /** Plaintext — SDK does not encrypt this field. */
91
+ /** SDK encrypts before sending. */
58
92
  ipAddress: string;
59
93
  /** SDK encrypts before sending. */
60
94
  deviceId: string;
61
95
  sessionHash: string;
96
+ /** SDK encrypts before sending. */
62
97
  userAgent: string;
63
98
  /** SDK encrypts before sending. */
64
99
  emailAddress: string;
@@ -95,29 +130,14 @@ export interface VerifyResult {
95
130
  verified: boolean;
96
131
  reason?: 'not_found' | 'code_mismatch' | 'code_expired';
97
132
  }
98
- export interface VerificationFlowStep {
99
- type: 'message' | 'email_input' | 'otp_input' | 'support_link';
100
- title: string;
101
- body: string;
102
- buttonText?: string;
103
- url?: string;
104
- }
105
- export interface VerificationFlowConfigResult {
106
- steps: VerificationFlowStep[];
107
- branding?: {
108
- companyName?: string;
109
- logoUrl?: string;
110
- primaryColor?: string;
111
- supportEmail?: string;
112
- };
113
- }
114
- export declare class UnsharedLabsClient {
133
+ export declare class UnsharedClient {
115
134
  private readonly _apiKey;
116
135
  private readonly _baseUrl;
117
136
  private readonly _timeout;
118
137
  private readonly _maxRetries;
119
138
  private readonly _encryptionKey;
120
- constructor(config: UnsharedLabsClientConfig);
139
+ private readonly _customFetch;
140
+ constructor(config: UnsharedClientConfig);
121
141
  private _encrypt;
122
142
  /**
123
143
  * Core HTTP method with retry logic.
@@ -181,16 +201,4 @@ export declare class UnsharedLabsClient {
181
201
  verify(emailAddress: string, deviceId: string, code: string, opts?: {
182
202
  fingerprintId?: string;
183
203
  }): Promise<ApiResult<VerifyResult>>;
184
- /**
185
- * Fetch the verification flow configuration for this company.
186
- * Maps to: GET /v2/verification-flow-config
187
- *
188
- * Returns the flow steps and branding configured by the Unshared Labs
189
- * team for this company. The middleware uses this to render the
190
- * verification overlay.
191
- *
192
- * Returns `null` on any failure (network error, 4xx, 5xx) so the
193
- * middleware can fall back to the default flow.
194
- */
195
- getVerificationFlowConfig(): Promise<VerificationFlowConfigResult | null>;
196
204
  }
@@ -1 +1 @@
1
- import{createHash,randomUUID}from"crypto";import{encryptData}from"./util";const DEFAULT_BASE_URL="https://api-ingress.unsharedlabs.com",DEFAULT_TIMEOUT_MS=1e4,DEFAULT_MAX_RETRIES=3,MAX_DELAY_MS=3e4,BASE_DELAY_MS=1e3;function sleep(e){return new Promise(s=>setTimeout(s,e))}function retryDelay(e){const s=Math.min(1e3*Math.pow(2,e-1),3e4),t=s*(.5*Math.random()-.25);return Math.max(0,s+t)}async function parseErrorBody(e){const s=await e.text().catch(()=>"");try{const t=JSON.parse(s);return t?.error?.code?{code:t.error.code,message:t.error.message??"Unknown error",details:t.error.details}:{code:"UNKNOWN_ERROR",message:s||e.statusText}}catch{return{code:"UNKNOWN_ERROR",message:s||e.statusText}}}export class UnsharedLabsClient{constructor(e){if(!e.apiKey||""===e.apiKey.trim())throw new Error("apiKey is required");this.t=e.apiKey,this.i=e.baseUrl?e.baseUrl.replace(/\/$/,""):DEFAULT_BASE_URL,this.o=e.timeout??1e4,this.h=e.maxRetries??3,this.u=createHash("sha256").update(e.apiKey).digest()}l(e){return encryptData(e,this.u)}async _(e,s){const t=this.h+1;let r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let i=1;i<=t;i++){i>1&&await sleep(retryDelay(i-1));const t=new AbortController,a=setTimeout(()=>t.abort(),this.o);try{const i=await fetch(e,{method:s.method,headers:{"X-API-Key":this.t,...s.headers},body:s.body,signal:t.signal});if(clearTimeout(a),i.ok){const e=await i.text().catch(()=>"{}");let s;try{s=JSON.parse(e)}catch{s={}}const t="data"in s?s.data:s;return{success:!0,status:i.status,data:t}}const n=await parseErrorBody(i);if(i.status>=400&&i.status<500){if(429===i.status){const e=i.headers.get("Retry-After");if(null!=e){const s=parseInt(e,10);isNaN(s)||(n.retryAfter=s)}}return{success:!1,status:i.status,error:n}}r={success:!1,status:i.status,error:n}}catch(e){clearTimeout(a),r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return r}async submitFingerprintEvent(e,s){const t={hash:e.full_hash,stable_hash:e.fingerprint_id,collected_at:e.timestamp,is_incognito:e.isIncognito,components:e.components,version:e.version};return null!=s?.userId&&(t.user_id=this.l(s.userId)),null!=s?.sessionHash&&(t.session_hash=s.sessionHash),null!=s?.eventType&&(t.event_type=s.eventType),null!=s?.ipAddress&&(t.ip_address=s.ipAddress),this._(`${this.i}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":randomUUID()},body:JSON.stringify(t)})}async processUserEvent(e){const s={event_type:e.eventType,user_id:e.userId,ip_address:e.ipAddress,device_id:this.l(e.deviceId),session_hash:e.sessionHash,user_agent:e.userAgent,email_address:this.l(e.emailAddress)};return null!=e.fingerprintId&&(s.fingerprint_id=this.l(e.fingerprintId)),null!=e.subscriptionStatus&&(s.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(s.event_details=e.eventDetails),this._(`${this.i}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})}async checkUser(e,s){const t="string"==typeof s?{deviceId:s}:s;if(!t.deviceId&&!t.fingerprintId)return{success:!0,status:200,data:{is_user_flagged:!1}};const r=new URLSearchParams;r.set("email_address",this.l(e)),t.deviceId&&r.set("device_id",this.l(t.deviceId)),t.fingerprintId&&r.set("fingerprint_id",this.l(t.fingerprintId));const i=await this._(`${this.i}/v2/check-user?${r}`,{method:"GET"});return i.success?i:{success:!0,status:200,data:{is_user_flagged:!1}}}async triggerEmailVerification(e,s,t){const r={email_address:this.l(e),device_id:this.l(s)};t?.fingerprintId&&(r.fingerprint_id=this.l(t.fingerprintId));const i=await this._(`${this.i}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});return!i.success&&(0===i.status||i.status>=500)?{success:!1,status:i.status,error:{code:"DELIVERY_FAILED",message:i.error?.message??"Delivery failed"}}:i}async verify(e,s,t,r){const i={email_address:this.l(e),device_id:this.l(s),code:this.l(t)};r?.fingerprintId&&(i.fingerprint_id=this.l(r.fingerprintId));const a=await this._(`${this.i}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});return!a.success&&(0===a.status||a.status>=500)?{success:!1,status:a.status,error:{code:"DELIVERY_FAILED",message:a.error?.message??"Delivery failed"}}:a.success&&!1===a.data?.verified?{success:!1,status:a.status,error:{code:"VERIFICATION_FAILED",message:"Code is incorrect or expired",details:a.data.reason?{reason:a.data.reason}:void 0}}:a}async getVerificationFlowConfig(){const e=await this._(`${this.i}/v2/verification-flow-config`,{method:"GET"});return e.success&&e.data?e.data:null}}
1
+ import{createHash,randomUUID}from"crypto";import{encryptData}from"./util";const DEFAULT_BASE_URL="https://api.unshared.ai",DEFAULT_TIMEOUT_MS=1e4,DEFAULT_MAX_RETRIES=3,MAX_DELAY_MS=3e4,BASE_DELAY_MS=1e3;function sleep(e){return new Promise(s=>setTimeout(s,e))}function retryDelay(e){const s=Math.min(1e3*Math.pow(2,e-1),3e4),t=s*(.5*Math.random()-.25);return Math.max(0,s+t)}async function parseErrorBody(e){const s=await e.text().catch(()=>"");try{const t=JSON.parse(s);return t?.error?.code?{code:t.error.code,message:t.error.message??"Unknown error",details:t.error.details}:{code:"UNKNOWN_ERROR",message:s||e.statusText}}catch{return{code:"UNKNOWN_ERROR",message:s||e.statusText}}}export class UnsharedClient{constructor(e){if(!e.apiKey||""===e.apiKey.trim())throw new Error("apiKey is required");this.t=e.apiKey,this.i=e.baseUrl?e.baseUrl.replace(/\/$/,""):DEFAULT_BASE_URL,this.o=e.timeout??1e4,this.h=e.maxRetries??3,this.u=createHash("sha256").update(e.apiKey).digest(),this.l=e.fetch}_(e){return encryptData(e,this.u)}async p(e,s){const t=this.h+1;let r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:"Request failed"}};for(let i=1;i<=t;i++){i>1&&await sleep(retryDelay(i-1));const t=new AbortController,a=setTimeout(()=>t.abort(),this.o);try{const i=this.l??globalThis.fetch,n=await i(e,{method:s.method,headers:{"X-API-Key":this.t,...s.headers},body:s.body,signal:t.signal});if(clearTimeout(a),n.ok){const e=await n.text().catch(()=>"{}");let s;try{s=JSON.parse(e)}catch{s={}}const t="data"in s?s.data:s;return{success:!0,status:n.status,data:t}}const o=await parseErrorBody(n);if(n.status>=400&&n.status<500){if(429===n.status){const e=n.headers.get("Retry-After");if(null!=e){const s=parseInt(e,10);isNaN(s)||(o.retryAfter=s)}}return{success:!1,status:n.status,error:o}}r={success:!1,status:n.status,error:o}}catch(e){clearTimeout(a),r={success:!1,status:0,error:{code:"NETWORK_ERROR",message:e instanceof Error?e.message:String(e)}}}}return r}async submitFingerprintEvent(e,s){const t={hash:e.full_hash,stable_hash:e.fingerprint_id,collected_at:e.timestamp,is_incognito:e.isIncognito,components:e.components,version:e.version};return null!=s?.userId&&(t.user_id=this._(s.userId)),null!=s?.emailAddress&&(t.email_address=this._(s.emailAddress)),null!=s?.sessionHash&&(t.session_hash=s.sessionHash),null!=s?.eventType&&(t.event_type=s.eventType),null!=s?.ipAddress&&(t.ip_address=this._(s.ipAddress)),null!=s?.userAgent&&(t.user_agent=this._(s.userAgent)),this.p(`${this.i}/v2/submit-fingerprint-event`,{method:"POST",headers:{"Content-Type":"application/json","X-Idempotency-Key":s?.idempotencyKey??randomUUID()},body:JSON.stringify(t)})}async processUserEvent(e){const s={event_type:e.eventType,user_id:this._(e.userId),ip_address:this._(e.ipAddress),device_id:this._(e.deviceId),session_hash:e.sessionHash,user_agent:this._(e.userAgent),email_address:this._(e.emailAddress)};return null!=e.fingerprintId&&(s.fingerprint_id=this._(e.fingerprintId)),null!=e.subscriptionStatus&&(s.subscription_status=e.subscriptionStatus),null!=e.eventDetails&&(s.event_details=e.eventDetails),this.p(`${this.i}/v2/process-user-event`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})}async checkUser(e,s){const t="string"==typeof s?{deviceId:s}:s;if(!t.deviceId&&!t.fingerprintId)return{success:!0,status:200,data:{is_user_flagged:!1}};const r=new URLSearchParams;r.set("email_address",this._(e)),t.deviceId&&r.set("device_id",this._(t.deviceId)),t.fingerprintId&&r.set("fingerprint_id",this._(t.fingerprintId));const i=await this.p(`${this.i}/v2/check-user?${r}`,{method:"GET"});return i.success?i:{success:!0,status:200,data:{is_user_flagged:!1}}}async triggerEmailVerification(e,s,t){const r={email_address:this._(e),device_id:this._(s)};t?.fingerprintId&&(r.fingerprint_id=this._(t.fingerprintId));const i=await this.p(`${this.i}/v2/trigger-email-verification`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});return!i.success&&(0===i.status||i.status>=500)?{success:!1,status:i.status,error:{code:"DELIVERY_FAILED",message:i.error?.message??"Delivery failed"}}:i}async verify(e,s,t,r){const i={email_address:this._(e),device_id:this._(s),code:this._(t)};r?.fingerprintId&&(i.fingerprint_id=this._(r.fingerprintId));const a=await this.p(`${this.i}/v2/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});return!a.success&&(0===a.status||a.status>=500)?{success:!1,status:a.status,error:{code:"DELIVERY_FAILED",message:a.error?.message??"Delivery failed"}}:a.success&&!1===a.data?.verified?{success:!1,status:a.status,error:{code:"VERIFICATION_FAILED",message:"Code is incorrect or expired",details:a.data.reason?{reason:a.data.reason}:void 0}}:a}}
@@ -1,6 +1,8 @@
1
- export { UnsharedLabsClient } from './client';
1
+ export { UnsharedClient } from './client';
2
2
  export { createUnsharedMiddleware, assertTrustProxy } from './middleware';
3
3
  export type { MiddlewareOptions } from './middleware';
4
- export { unsharedBoundToUser, VerdictCache, } from './middleware/index';
4
+ export { unsharedBoundToUser, VerdictCache, flaggedResponse, ACCOUNT_FLAGGED_ERROR, } from './middleware/index';
5
5
  export type { ProtectionConfig, Verdict } from './middleware/index';
6
- export type { UnsharedLabsClientConfig, ApiResult, UnsharedLabsError, SubmitFingerprintOptions, SubmitFingerprintResult, ProcessUserEventParams, ProcessUserEventResult, CheckUserResult, TriggerEmailVerificationResult, VerifyResult, VerificationFlowStep, VerificationFlowConfigResult, } from './client';
6
+ export type { UnsharedClientConfig, ApiResult, UnsharedError, SubmitFingerprintOptions, SubmitFingerprintResult, ProcessUserEventParams, ProcessUserEventResult, CheckUserResult, TriggerEmailVerificationResult, VerifyResult, } from './client';
7
+ export type { UnsharedRequest, UnsharedResponse, UnsharedNextFunction } from './types';
8
+ export { sendJson } from './middleware/utils/http-helpers';
@@ -1 +1 @@
1
- export{UnsharedLabsClient}from"./client";export{createUnsharedMiddleware,assertTrustProxy}from"./middleware";export{unsharedBoundToUser,VerdictCache}from"./middleware/index";
1
+ export{UnsharedClient}from"./client";export{createUnsharedMiddleware,assertTrustProxy}from"./middleware";export{unsharedBoundToUser,VerdictCache,flaggedResponse,ACCOUNT_FLAGGED_ERROR}from"./middleware/index";export{sendJson}from"./middleware/utils/http-helpers";
@@ -0,0 +1,11 @@
1
+ export declare class DispatchDedupe {
2
+ private readonly _entries;
3
+ private readonly _ttlMs;
4
+ constructor(ttlMs?: number);
5
+ private _key;
6
+ mark(userId: string, eventType: string): void;
7
+ wasRecentlyDispatched(userId: string, eventType: string): boolean;
8
+ clear(): void;
9
+ get size(): number;
10
+ private _sweep;
11
+ }
@@ -0,0 +1 @@
1
+ const DEFAULT_TTL_MS=1e4,SWEEP_THRESHOLD=1e3;export class DispatchDedupe{constructor(t=1e4){this.t=new Map,this.i=t}h(t,s){return`${t}|${s}`}mark(t,s){this.t.set(this.h(t,s),Date.now()),this.t.size>1e3&&this.o()}wasRecentlyDispatched(t,s){const e=this.h(t,s),i=this.t.get(e);return!(void 0===i||Date.now()-i>this.i&&(this.t.delete(e),1))}clear(){this.t.clear()}get size(){return this.t.size}o(){const t=Date.now()-this.i;for(const[s,e]of this.t)e<t&&this.t.delete(s)}}
@@ -1,19 +1,20 @@
1
- import type { Request, Response, NextFunction } from 'express';
2
- import type { UnsharedLabsClient } from '../client';
1
+ import type { UnsharedRequest, UnsharedResponse, UnsharedNextFunction } from '../types';
2
+ import type { UnsharedClient } from '../client';
3
3
  import { VerdictCache } from './verdict-cache';
4
4
  import type { Verdict } from './verdict-cache';
5
- export interface ProtectionConfig {
5
+ export { flaggedResponse, ACCOUNT_FLAGGED_ERROR } from './utils/flagged-response';
6
+ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest> {
6
7
  /**
7
8
  * Required. Resolves the current user's ID from the request.
8
9
  * Return undefined for anonymous/logged-out visitors.
9
10
  */
10
- userId: (req: Request) => string | undefined;
11
+ userId: (req: TReq) => string | undefined;
11
12
  /**
12
13
  * Resolves the current user's email address from the request.
13
14
  * Required in Tier 2 (backend-only). Recommended in Tier 1.
14
15
  * Falls back to HttpOnly cookie → req.body.email when not configured.
15
16
  */
16
- emailAddress?: (req: Request) => string | undefined;
17
+ emailAddress?: (req: TReq) => string | undefined;
17
18
  /** Route prefix for internal routes. @default "/__unshared" */
18
19
  routePrefix?: string;
19
20
  /** Allowed CORS origins for /__unshared/* routes. */
@@ -22,13 +23,19 @@ export interface ProtectionConfig {
22
23
  cacheTTL?: number;
23
24
  /** Paths to skip entirely (static assets, health checks). */
24
25
  skipPaths?: string[];
26
+ /** When set, only paths matching one of these prefixes get events dispatched and checkUser called. */
27
+ includePathPrefix?: string[];
28
+ /** Skip the bot/crawler UA filter. Set to true in test environments so automated browsers (Playwright, Puppeteer, etc.) can observe verdicts. @default false */
29
+ disableBotFilter?: boolean;
30
+ /** Hard timeout (ms) for checkUser on cache miss. Fails open on timeout. @default 500 */
31
+ checkUserTimeoutMs?: number;
25
32
  /** Resolves a custom session ID. Falls back to __unshared_sid cookie. */
26
- sessionId?: (req: Request) => string | undefined;
33
+ sessionId?: (req: TReq) => string | undefined;
27
34
  /**
28
35
  * Resolves a device ID from the request.
29
- * Falls back to __unshared_fp_id cookie → X-Device-Id header.
36
+ * Falls back to X-Device-Id header → __unshared_fp_id cookie.
30
37
  */
31
- deviceId?: (req: Request) => string | undefined;
38
+ deviceId?: (req: TReq) => string | undefined;
32
39
  /**
33
40
  * Called when a flagged, unverified user makes a request.
34
41
  * You own the response — block, redirect, or call next() to let it through.
@@ -40,11 +47,24 @@ export interface ProtectionConfig {
40
47
  userId: string;
41
48
  emailAddress: string;
42
49
  verdict: Verdict;
43
- req: Request;
44
- res: Response;
45
- next: NextFunction;
50
+ req: TReq;
51
+ res: UnsharedResponse;
52
+ next: UnsharedNextFunction;
53
+ }) => void;
54
+ /**
55
+ * Called when a background SDK operation fails (fire-and-forget API calls,
56
+ * verdict refreshes, etc.). Use this to pipe errors to your logging or
57
+ * monitoring system for observability.
58
+ *
59
+ * Without this callback, background errors are silently swallowed (fail-open).
60
+ * The middleware never blocks requests due to these errors regardless.
61
+ */
62
+ onError?: (error: unknown, context: {
63
+ operation: 'processUserEvent' | 'submitFingerprintEvent' | 'checkUser' | 'verifyTrigger' | 'verify';
64
+ userId?: string;
65
+ emailAddress?: string;
46
66
  }) => void;
47
67
  }
48
68
  export type { Verdict };
49
69
  export { VerdictCache };
50
- export declare function unsharedBoundToUser(client: UnsharedLabsClient, config: ProtectionConfig): (req: Request, res: Response, next: NextFunction) => void;
70
+ export declare function unsharedBoundToUser<TReq extends UnsharedRequest = UnsharedRequest>(client: UnsharedClient, config: ProtectionConfig<TReq>): (req: TReq, res: UnsharedResponse, next: UnsharedNextFunction) => void;
@@ -1 +1 @@
1
- import{readFileSync}from"fs";import{VerdictCache}from"./verdict-cache";import{RateLimitBackoff}from"./rate-limit-backoff";import{interceptResponse}from"./response-interceptor";import{generateFingerprintScript}from"./injection/fingerprint-script";import{handleSubmitFingerprint}from"./routes/submit-fp";import{handleVerifyTrigger,handleVerify}from"./routes/verify";import{isHtmlContentType}from"./utils/content-type";import{shouldSkipPath}from"./utils/skip-paths";export{VerdictCache};const CHECK_USER_TIMEOUT_MS=500;export function unsharedBoundToUser(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");if(!t.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:r,emailAddress:n,routePrefix:i="/__unshared",corsOrigins:o,cacheTTL:s=6e4,skipPaths:c,sessionId:d,deviceId:a,onFlagged:l}=t,u=new VerdictCache(s),p=new RateLimitBackoff,f=Date.now().toString(36),m=generateFingerprintScript(i,f);let h="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");h=readFileSync(e,"utf8")}catch{}const v=handleSubmitFingerprint({client:e,verdictCache:u,rateLimitBackoff:p,resolveUserId:r,resolveEmailAddress:n,resolveSessionId:d,resolveDeviceId:a}),C=handleVerifyTrigger({client:e,verdictCache:u,resolveEmailAddress:n,resolveDeviceId:a}),g=handleVerify({client:e,verdictCache:u,resolveEmailAddress:n,resolveDeviceId:a}),I=o?Array.isArray(o)?o:[o]:null,y=`${i}/fp.js`,k=`${i}/submit-fp`,S=`${i}/verify-trigger`,_=`${i}/verify`;return function(t,o,s){const f=t.path;if(f.startsWith(i+"/"))return function(e,t){if(!I)return;const r=e.headers.origin??"",n=I.includes("*");(n||I.includes(r))&&(t.setHeader("Access-Control-Allow-Origin",n?"*":r),t.setHeader("Access-Control-Allow-Methods","POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,o),"OPTIONS"===t.method?void o.status(204).end():"GET"===t.method&&f===y?(o.setHeader("Content-Type","application/javascript"),o.setHeader("Cache-Control","public, max-age=3600"),void o.status(200).end(h)):"POST"===t.method&&f===k?void v(t,o):"POST"===t.method&&f===S?void C(t,o):"POST"===t.method&&f===_?void g(t,o):void o.status(404).json({success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}});if(shouldSkipPath(f,c))return void s();let A;try{A=r(t)}catch{}if(!A)return clearEmailCookieIfPresent(t,o),interceptForInjection(t,o,m),void s();const T=resolveEmail(t,n);if(setUserIdCookie(o,A),T&&setEmailCookie(o,T),!T)return interceptForInjection(t,o,m),void s();const E=extractSessionId(t,d),x=extractDeviceId(t,a),F=extractFingerprintId(t),w=t.headers["user-agent"]??"",P=t.ip??"";p.isPaused()||dispatchUserEvent(e,u,p,{userId:A,emailAddress:T,sessionId:E,deviceId:x,fingerprintId:F,userAgent:w,ipAddress:P,eventType:`${t.method} ${t.path}`});const O=u.get(A);O?(u.isStale(A)&&!u.isRefreshing(A)&&(u.markRefreshing(A),fetchAndCacheVerdict(e,u,A,T,x,F,E).finally(()=>u.clearRefreshing(A))),applyVerdict(O,A,T,t,o,s,m,l)):fetchAndCacheVerdict(e,u,A,T,x,F,E).then(e=>{applyVerdict(e,A,T,t,o,s,m,l)}).catch(()=>{interceptForInjection(t,o,m),s()})}}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=parseCookie(e,"__unshared_email");if(r)return r;const n=e.body?.email;return"string"==typeof n&&n?n:void 0}function applyVerdict(e,t,r,n,i,o,s,c){if(interceptForInjection(n,i,s),e.isFlagged&&!e.isVerified&&c)try{c({userId:t,emailAddress:r,verdict:e,req:n,res:i,next:o})}catch{o()}else o()}function preventHtmlCaching(e,t){delete e.headers["if-none-match"],delete e.headers["if-modified-since"];const r=t.writeHead.bind(t);t.writeHead=function(e,...n){const i=t.getHeader("content-type");return i&&String(i).includes("text/html")&&(t.setHeader("Cache-Control","no-store"),t.removeHeader("ETag"),t.removeHeader("Last-Modified")),r(e,...n)}}function interceptForInjection(e,t,r){preventHtmlCaching(e,t),interceptResponse(t,(e,t)=>{if(!isHtmlContentType(t))return null;const n=e.toString("utf8"),i=n.lastIndexOf("</body>");return-1===i?n+r:n.slice(0,i)+r+n.slice(i)})}function dispatchUserEvent(e,t,r,n){e.processUserEvent({eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,sessionHash:n.sessionId,userAgent:n.userAgent}).then(e=>{e.success&&e.data?.analysis&&t.update(n.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&r.pause(1e3*e.error.retryAfter)}).catch(()=>{})}async function fetchAndCacheVerdict(e,t,r,n,i,o,s){const c={};i&&"unknown"!==i&&(c.deviceId=i),o&&(c.fingerprintId=o);const d=await Promise.race([e.checkUser(n,c),new Promise(e=>setTimeout(()=>e(null),500))]);if(!d)return{isFlagged:!1,isVerified:!1,emailAddress:n,sessionId:s,cachedAt:0,ttl:0};const a=d.data?.is_user_flagged??!1;return t.set(r,{isFlagged:a,isVerified:!1,emailAddress:n,sessionId:s}),t.get(r)}function parseCookie(e,t){const r=e.headers.cookie;if(!r)return;const n=r.match(new RegExp(`(?:^|; )${t}=([^;]*)`));return n?decodeURIComponent(n[1]):void 0}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return parseCookie(e,"__unshared_sid")??"unknown"}function extractDeviceId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=parseCookie(e,"__unshared_fp_id");if(r)return r;const n=e.headers["x-device-id"];return"string"==typeof n&&n?n:"unknown"}function extractFingerprintId(e){return parseCookie(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const n=Array.isArray(r)?[...r]:[String(r)];n.push(t),e.setHeader("Set-Cookie",n)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t){appendSetCookie(e,`__unshared_uid=${encodeURIComponent(t)}; Path=/; SameSite=Lax`)}function setEmailCookie(e,t){appendSetCookie(e,`__unshared_email=${encodeURIComponent(t)}; HttpOnly; Path=/; SameSite=Lax`)}function clearEmailCookieIfPresent(e,t){parseCookie(e,"__unshared_email")&&appendSetCookie(t,"__unshared_email=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0")}
1
+ import{readFileSync}from"fs";import{VerdictCache}from"./verdict-cache";import{RateLimitBackoff}from"./rate-limit-backoff";import{DispatchDedupe}from"./dispatch-dedupe";import{interceptResponse}from"./response-interceptor";import{generateFingerprintScript}from"./injection/fingerprint-script";import{handleSubmitFingerprint}from"./routes/submit-fp";import{handleVerifyTrigger,handleVerify}from"./routes/verify";import{sendJson,sendEmpty,sendBody,getRequestPath}from"./utils/http-helpers";import{isHtmlContentType}from"./utils/content-type";import{shouldSkipPath}from"./utils/skip-paths";import{shouldIncludePath}from"./utils/include-path";import{isBot}from"./utils/is-bot";import{extractClientIp}from"./utils/client-ip";import{parseCookie}from"./utils/cookies";import{extractDeviceIdOrUndefined}from"./utils/device-id";import{isSecureRequest}from"./utils/secure";export{flaggedResponse,ACCOUNT_FLAGGED_ERROR}from"./utils/flagged-response";import{isSentinelUserId,SENTINEL_STICKINESS_TTL_MS}from"./utils/sentinel-user-id";export{VerdictCache};const CHECK_USER_TIMEOUT_MS=500;export function unsharedBoundToUser(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");if(!t.emailAddress){let e=!1;try{require.resolve("unshared-frontend-sdk"),e=!0}catch{}e||console.warn("[Unshared] Warning: emailAddress resolver is not configured and unshared-frontend-sdk is not installed.\nNo user events will be submitted. Either install unshared-frontend-sdk (Tier 1) or\nprovide emailAddress in your middleware config (Tier 2).")}const{userId:r,emailAddress:i,routePrefix:n="/__unshared",corsOrigins:s,cacheTTL:o=6e4,skipPaths:d,includePathPrefix:a,disableBotFilter:c=!1,checkUserTimeoutMs:u=CHECK_USER_TIMEOUT_MS,sessionId:l,deviceId:p,onFlagged:f,onError:m}=t,h=new VerdictCache(o),S=new RateLimitBackoff,I=new DispatchDedupe,_=Date.now().toString(36),v=generateFingerprintScript(n,_);let C="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");C=readFileSync(e,"utf8")}catch{}const g=handleSubmitFingerprint({client:e,verdictCache:h,rateLimitBackoff:S,dispatchDedupe:I,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:l,resolveDeviceId:p,onError:m}),y=handleVerifyTrigger({client:e,verdictCache:h,resolveEmailAddress:i,resolveDeviceId:p,onError:m}),k=handleVerify({client:e,verdictCache:h,resolveEmailAddress:i,resolveDeviceId:p,onError:m}),A=s?Array.isArray(s)?s:[s]:null,E=`${n}/fp.js`,T=`${n}/submit-fp`,x=`${n}/verify-trigger`,U=`${n}/verify`,P=`${n}/status`;return function(t,s,o){const _=getRequestPath(t.url),w=t.url||_;if(_.startsWith(n+"/")){if(function(e,t){if(!A)return;const r=e.headers.origin??"",i=A.includes("*");(i||A.includes(r))&&(t.setHeader("Access-Control-Allow-Origin",i?"*":r),t.setHeader("Access-Control-Allow-Methods","POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,s),"OPTIONS"===t.method)return void sendEmpty(s,204);if("GET"===t.method&&_===E)return s.setHeader("Content-Type","application/javascript"),s.setHeader("Cache-Control","public, max-age=3600"),void sendBody(s,200,C);if("POST"===t.method&&(_===T||_===x||_===U))return void 0===t.body?void sendJson(s,400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"req.body is undefined. Mount a JSON body-parsing middleware (e.g., express.json()) before the Unshared middleware."}}):_===T?void g(t,s):_===x?void y(t,s):void k(t,s);if("GET"===t.method&&_===P){let n;try{n=r(t)}catch{}if(!n)return void sendJson(s,200,{status:"anonymous"});const o=resolveEmail(t,i);return void(async()=>{let r=h.get(n);if((!r||h.isStale(n))&&o&&!S.isPaused()&&!h.isRefreshing(n)){h.markRefreshing(n);try{const i=extractDeviceIdOrUndefined(t,p),s=extractFingerprintId(t),d=extractSessionId(t,l),a=i??s??"unknown";await fetchAndCacheVerdict(e,h,n,o,a,s,d,u),r=h.get(n)}catch(e){m&&m(e,{operation:"checkUser",userId:n,emailAddress:o})}finally{h.clearRefreshing(n)}}r&&r.isFlagged&&!r.isVerified&&f&&o?sendJson(s,200,{status:"flagged",email:o}):sendJson(s,200,{status:"ok"})})()}return void sendJson(s,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}if(shouldSkipPath(_,d))return void o();if(!shouldIncludePath(_,a))return interceptForInjection(t,s,v),void o();let F;try{F=r(t)}catch{}if(isSentinelUserId(F)){const e=parseCookie(t,"__unshared_uid"),r=parseCookie(t,"__unshared_uid_at"),i=r?Number(r):NaN,n=Number.isFinite(i)&&Date.now()-i<=SENTINEL_STICKINESS_TTL_MS;F=e&&n?e:void 0}if(!F){const e=isSecureRequest(t)?"; Secure":"";return appendSetCookie(s,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(t,s,v),void o()}const O=resolveEmail(t,i);if(setUserIdCookie(t,s,F),O&&setEmailCookie(t,s,O),!O)return interceptForInjection(t,s,v),void o();const R=extractSessionId(t,l),D=extractDeviceIdOrUndefined(t,p),N=extractFingerprintId(t),b=t.headers["user-agent"]??"",L=extractClientIp(t),M=D??N;if(!c&&isBot(b))return void o();const V=h.get(F);function $(){"unknown"!==R&&M&&(S.isPaused()||dispatchUserEvent(e,h,S,I,{userId:F,emailAddress:O,sessionId:R,deviceId:M,fingerprintId:N,userAgent:b,ipAddress:L,eventType:w},m))}V?(h.isStale(F)&&!h.isRefreshing(F)&&(h.markRefreshing(F),fetchAndCacheVerdict(e,h,F,O,M??"unknown",N,R,u).finally(()=>h.clearRefreshing(F))),V.isFlagged||$(),applyVerdict(V,F,O,t,s,o,v,f)):fetchAndCacheVerdict(e,h,F,O,M??"unknown",N,R,u).then(e=>{e.isFlagged||$(),applyVerdict(e,F,O,t,s,o,v,f)}).catch(()=>{$(),interceptForInjection(t,s,v),o()})}}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=parseCookie(e,"__unshared_email");if(r)return r;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,t,r,i,n,s,o,d){if(interceptForInjection(i,n,o),e.isFlagged&&!e.isVerified&&d)try{d({userId:t,emailAddress:r,verdict:e,req:i,res:n,next:s})}catch{s()}else s()}function interceptForInjection(e,t,r){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],interceptResponse(t,(e,t)=>{if(!isHtmlContentType(t))return null;const i=e.toString("utf8"),n=i.lastIndexOf("</body>");return-1===n?i+r:i.slice(0,n)+r+i.slice(n)},{preventCaching:!0})}function dispatchUserEvent(e,t,r,i,n,s){i.mark(n.userId,n.eventType),e.processUserEvent({eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,sessionHash:n.sessionId,userAgent:n.userAgent}).then(e=>{e.success&&e.data?.analysis&&t.update(n.userId,{isFlagged:e.data.analysis.is_user_flagged}),!e.success&&e.error?.retryAfter&&r.pause(1e3*e.error.retryAfter)}).catch(e=>{s&&s(e,{operation:"processUserEvent",userId:n.userId,emailAddress:n.emailAddress})})}async function fetchAndCacheVerdict(e,t,r,i,n,s,o,d=CHECK_USER_TIMEOUT_MS){const a={};let c;n&&"unknown"!==n&&(a.deviceId=n),s&&(a.fingerprintId=s);const u=await Promise.race([e.checkUser(i,a),new Promise(e=>{c=setTimeout(()=>e(null),d)})]);if(clearTimeout(c),!u)return{isFlagged:!1,isVerified:!1,emailAddress:i,sessionId:o,cachedAt:0,ttl:0};const l=u.data?.is_user_flagged??!1;return t.set(r,{isFlagged:l,isVerified:!1,emailAddress:i,sessionId:o}),t.get(r)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return parseCookie(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return parseCookie(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,t,r){const i=isSecureRequest(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
@@ -1,10 +1,16 @@
1
1
  /**
2
- * Generates a small inline loader script that:
3
- * 1. Loads the real fingerprint SDK from /__unshared/fp.js
4
- * 2. Collects a full fingerprint (31+ signals, MurmurHash3 Merkle tree)
5
- * 3. POSTs the result to /__unshared/submit-fp
2
+ * Generates an inline loader script that:
3
+ * 1. Loads the fingerprint SDK from /__unshared/fp.js
4
+ * 2. Collects a fingerprint and POSTs to /__unshared/submit-fp
5
+ * 3. Caches fingerprint in sessionStorage for reuse on SPA navigations
6
+ * 4. Patches History API to detect SPA route changes → re-submits fingerprint
7
+ * 5. Patches fetch/XHR to detect 403 account_flagged → dispatches "unshared:flagged" event
6
8
  *
7
9
  * The actual SDK UMD bundle is served by the middleware at /__unshared/fp.js.
8
- * This keeps the injected HTML small (~500 bytes) while using the full library.
10
+ *
11
+ * Event contract:
12
+ * window.addEventListener("unshared:flagged", (e) => {
13
+ * e.detail.email — the flagged user's email (from 403 response body)
14
+ * });
9
15
  */
10
16
  export declare function generateFingerprintScript(routePrefix: string, version?: string): string;