unshared-clientjs-sdk 2.3.0 → 3.0.0-rc.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +349 -152
- package/dist/client.d.ts +54 -25
- package/dist/client.js +1 -1
- package/dist/esm/client.d.mts +54 -25
- package/dist/esm/client.mjs +1 -1
- package/dist/esm/index.d.mts +8 -8
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/middleware/dispatch-processing.d.mts +11 -5
- package/dist/esm/middleware/dispatch-processing.mjs +1 -1
- package/dist/esm/middleware/index.d.mts +10 -6
- package/dist/esm/middleware/index.mjs +1 -1
- package/dist/esm/middleware/injection/fingerprint-script.d.mts +9 -0
- package/dist/esm/middleware/injection/fingerprint-script.mjs +1 -1
- package/dist/esm/middleware/injection/fp-bundle-source.d.mts +5 -4
- package/dist/esm/middleware/injection/fp-bundle-source.mjs +1 -1
- package/dist/esm/middleware/response-interceptor.d.mts +1 -1
- package/dist/esm/middleware/routes/interstitial.d.mts +2 -2
- package/dist/esm/middleware/routes/interstitial.mjs +1 -1
- package/dist/esm/middleware/routes/submit-fp.d.mts +5 -5
- package/dist/esm/middleware/routes/submit-fp.mjs +1 -1
- package/dist/esm/middleware/routes/verify.d.mts +5 -4
- package/dist/esm/middleware/routes/verify.mjs +1 -1
- package/dist/esm/middleware/utils/client-ip.d.mts +1 -1
- package/dist/esm/middleware/utils/cookies.d.mts +1 -1
- package/dist/esm/middleware/utils/device-id.d.mts +5 -3
- package/dist/esm/middleware/utils/device-id.mjs +1 -1
- package/dist/esm/middleware/utils/flagged-response.mjs +1 -1
- package/dist/esm/middleware/utils/http-helpers.d.mts +1 -1
- package/dist/esm/middleware/utils/permanent-device-id.d.mts +7 -0
- package/dist/esm/middleware/utils/permanent-device-id.mjs +1 -0
- package/dist/esm/middleware/utils/read-json-body.d.mts +3 -0
- package/dist/esm/middleware/utils/read-json-body.mjs +1 -0
- package/dist/esm/middleware/utils/secure.d.mts +1 -1
- package/dist/esm/middleware.d.mts +5 -6
- package/dist/esm/middleware.mjs +1 -1
- package/dist/esm/shared-types.d.mts +148 -0
- package/dist/esm/web/index.d.mts +6 -6
- package/dist/esm/web/index.mjs +1 -1
- package/dist/esm/web/protection-handler.d.mts +5 -5
- package/dist/esm/web/protection-handler.mjs +1 -1
- package/dist/esm/web/submit-handler.d.mts +2 -2
- package/dist/esm/web/submit-handler.mjs +1 -1
- package/dist/esm/web/types.d.mts +5 -3
- package/dist/esm/web/web-helpers.d.mts +13 -1
- package/dist/esm/web/web-helpers.mjs +1 -1
- package/dist/middleware/dispatch-processing.d.ts +8 -2
- package/dist/middleware/dispatch-processing.js +1 -1
- package/dist/middleware/index.d.ts +5 -1
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/injection/fingerprint-script.d.ts +9 -0
- package/dist/middleware/injection/fingerprint-script.js +1 -1
- package/dist/middleware/injection/fp-bundle-source.d.ts +5 -4
- package/dist/middleware/injection/fp-bundle-source.js +1 -1
- package/dist/middleware/routes/submit-fp.js +1 -1
- package/dist/middleware/routes/verify.d.ts +2 -1
- package/dist/middleware/routes/verify.js +1 -1
- package/dist/middleware/utils/device-id.d.ts +4 -2
- package/dist/middleware/utils/device-id.js +1 -1
- package/dist/middleware/utils/permanent-device-id.d.ts +7 -0
- package/dist/middleware/utils/permanent-device-id.js +1 -0
- package/dist/middleware/utils/read-json-body.d.ts +3 -0
- package/dist/middleware/utils/read-json-body.js +1 -0
- package/dist/middleware.d.ts +3 -4
- package/dist/middleware.js +1 -1
- package/dist/shared-types.d.ts +148 -0
- package/dist/web/protection-handler.d.ts +1 -1
- package/dist/web/protection-handler.js +1 -1
- package/dist/web/submit-handler.js +1 -1
- package/dist/web/types.d.ts +4 -2
- package/dist/web/web-helpers.d.ts +13 -1
- package/dist/web/web-helpers.js +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical wire-format for fingerprint data transmitted from the browser SDK to the backend.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth shared by:
|
|
5
|
+
* - @unshared-labs/frontend-fingerprint (serializer: _buildBody in browser.ts)
|
|
6
|
+
* - @unshared-labs/sdk (submitFingerprintEvent in client.ts)
|
|
7
|
+
*
|
|
8
|
+
* SCHEMA AUTHORITY: The browser SDK serializer is the writer. The Node.js SDK is the
|
|
9
|
+
* reader. Any field added here must be produced by the browser SDK and consumed by the
|
|
10
|
+
* server SDK. Do not add fields that only one side uses.
|
|
11
|
+
*/
|
|
12
|
+
export type JSONPrimitive = string | number | boolean | null;
|
|
13
|
+
export type JSONValue = JSONPrimitive | JSONObject | JSONValue[];
|
|
14
|
+
export interface JSONObject {
|
|
15
|
+
[key: string]: JSONValue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface BrowserStorageEntry extends JSONObject {
|
|
19
|
+
name: string;
|
|
20
|
+
value: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface BrowserStorageSnapshot extends JSONObject {
|
|
24
|
+
cookies: BrowserStorageEntry[];
|
|
25
|
+
local_storage: BrowserStorageEntry[];
|
|
26
|
+
session_storage: BrowserStorageEntry[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface EncryptedEventEnvelope {
|
|
30
|
+
encrypted_data: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FingerprintEventContext extends JSONObject {
|
|
34
|
+
sdk: JSONObject;
|
|
35
|
+
identity: JSONObject;
|
|
36
|
+
identifiers: JSONObject;
|
|
37
|
+
event: JSONObject;
|
|
38
|
+
browser_storage?: BrowserStorageSnapshot;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type V3Identifiers = Record<string, string>;
|
|
42
|
+
|
|
43
|
+
export interface V3Success<T> {
|
|
44
|
+
success: true;
|
|
45
|
+
data: T;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface V3Error {
|
|
49
|
+
success: false;
|
|
50
|
+
error: {
|
|
51
|
+
code: string;
|
|
52
|
+
message: string;
|
|
53
|
+
retryAfter?: number;
|
|
54
|
+
nextAllowedAt?: string;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type V3Result<T> = V3Success<T> | V3Error;
|
|
59
|
+
|
|
60
|
+
export interface IngestResult {
|
|
61
|
+
event_id: string;
|
|
62
|
+
collected_at: string;
|
|
63
|
+
endpoint_version: 'v3';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface TriggerEmailRequest {
|
|
67
|
+
email_address: string;
|
|
68
|
+
identifiers: V3Identifiers;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface TriggerEmailResult {
|
|
72
|
+
verification_id: string;
|
|
73
|
+
next_allowed_at: string;
|
|
74
|
+
retry_after_seconds: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface VerifyEmailRequest {
|
|
78
|
+
verification_id: string;
|
|
79
|
+
code: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type VerifyEmailResult =
|
|
83
|
+
| { verified: true; verification_scope: 'email' }
|
|
84
|
+
| { verified: false; verification_scope: 'email'; reason: 'invalid_code' };
|
|
85
|
+
|
|
86
|
+
export interface CheckUserRequest {
|
|
87
|
+
user_id?: string;
|
|
88
|
+
email_address: string;
|
|
89
|
+
identifiers: V3Identifiers;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type CheckDecisionCode =
|
|
93
|
+
| 'FLAGGED_RISK_MATCH'
|
|
94
|
+
| 'NO_ENFORCING_RISK'
|
|
95
|
+
| 'NO_IDENTITY_MATCH'
|
|
96
|
+
| 'IDENTITY_UNCERTAIN'
|
|
97
|
+
| 'CHECK_UNAVAILABLE';
|
|
98
|
+
|
|
99
|
+
export interface CheckUserResult {
|
|
100
|
+
is_user_flagged: boolean;
|
|
101
|
+
decision_id: string;
|
|
102
|
+
decision_code: CheckDecisionCode;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface FingerprintEventPayload extends JSONObject {
|
|
106
|
+
fingerprint: JSONObject;
|
|
107
|
+
context: FingerprintEventContext;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface FingerprintWireFormat {
|
|
111
|
+
[key: string]: unknown;
|
|
112
|
+
/**
|
|
113
|
+
* Primary device identifier — stable hash excluding volatile signals (audio, timing).
|
|
114
|
+
* Consistent across normal and incognito sessions on the same device.
|
|
115
|
+
* 32-character hex string.
|
|
116
|
+
*/
|
|
117
|
+
fingerprint_id: string;
|
|
118
|
+
/**
|
|
119
|
+
* Full hash including all active signals (volatile signals included).
|
|
120
|
+
* Maximum entropy; may vary slightly between sessions on the same device.
|
|
121
|
+
* 32-character hex string.
|
|
122
|
+
*/
|
|
123
|
+
full_hash: string;
|
|
124
|
+
/**
|
|
125
|
+
* Raw browser signal data keyed by signal name (e.g. `webgl`, `hardware`, `audio`).
|
|
126
|
+
*
|
|
127
|
+
* This blob is forwarded as-is in the `components` JSON field and stored verbatim
|
|
128
|
+
* in a `components JSONB` column on `fingerprint_events`. No server-side decomposition
|
|
129
|
+
* is performed; the full component tree is preserved for querying.
|
|
130
|
+
*/
|
|
131
|
+
components: Record<string, unknown>;
|
|
132
|
+
/** ISO timestamp when the fingerprint was collected. */
|
|
133
|
+
timestamp: string;
|
|
134
|
+
/** Whether private/incognito browsing was detected at collection time. */
|
|
135
|
+
isIncognito: boolean;
|
|
136
|
+
/** Fingerprint library semver, e.g. "1.0.0". */
|
|
137
|
+
version: string;
|
|
138
|
+
/**
|
|
139
|
+
* Where the fingerprint agent that produced this result was loaded from:
|
|
140
|
+
* - `'remote'` — the runtime-loaded UMD (S3 directly in browser direct mode,
|
|
141
|
+
* or the Node SDK's same-origin proxy of S3).
|
|
142
|
+
* - `'bundled'` — the copy compiled into the SDK, used as the fallback when
|
|
143
|
+
* the remote load failed/timed out.
|
|
144
|
+
* Optional: older SDK artifacts and the mobile SDKs omit it, so the reader
|
|
145
|
+
* must tolerate its absence. Observability for S3 fast-path vs fallback rates.
|
|
146
|
+
*/
|
|
147
|
+
source?: 'remote' | 'bundled';
|
|
148
|
+
}
|
package/dist/esm/web/index.d.mts
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* is compatible with all frameworks that expose `http.IncomingMessage`/
|
|
11
11
|
* `http.ServerResponse` objects.
|
|
12
12
|
*/
|
|
13
|
-
export { createWebSubmitHandler } from './submit-handler';
|
|
14
|
-
export { createWebProtectionMiddleware } from './protection-handler';
|
|
15
|
-
export { flaggedResponse, ACCOUNT_FLAGGED_ERROR } from '../middleware/utils/flagged-response';
|
|
16
|
-
export type { WebHandler, WebMiddleware, WebSubmitOptions, WebProtectionConfig, } from './types';
|
|
17
|
-
export { VerdictCache } from '../middleware/verdict-cache';
|
|
18
|
-
export type { Verdict } from '../middleware/verdict-cache';
|
|
13
|
+
export { createWebSubmitHandler } from './submit-handler.mjs';
|
|
14
|
+
export { createWebProtectionMiddleware } from './protection-handler.mjs';
|
|
15
|
+
export { flaggedResponse, ACCOUNT_FLAGGED_ERROR } from '../middleware/utils/flagged-response.mjs';
|
|
16
|
+
export type { WebHandler, WebMiddleware, WebSubmitOptions, WebProtectionConfig, } from './types.mjs';
|
|
17
|
+
export { VerdictCache } from '../middleware/verdict-cache.mjs';
|
|
18
|
+
export type { Verdict } from '../middleware/verdict-cache.mjs';
|
package/dist/esm/web/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{createWebSubmitHandler}from"./submit-handler";export{createWebProtectionMiddleware}from"./protection-handler";export{flaggedResponse,ACCOUNT_FLAGGED_ERROR}from"../middleware/utils/flagged-response";export{VerdictCache}from"../middleware/verdict-cache";
|
|
1
|
+
export{createWebSubmitHandler}from"./submit-handler.mjs";export{createWebProtectionMiddleware}from"./protection-handler.mjs";export{flaggedResponse,ACCOUNT_FLAGGED_ERROR}from"../middleware/utils/flagged-response.mjs";export{VerdictCache}from"../middleware/verdict-cache.mjs";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { UnsharedClient } from '../client';
|
|
2
|
-
import { VerdictCache } from '../middleware/verdict-cache';
|
|
3
|
-
import type { Verdict } from '../middleware/verdict-cache';
|
|
4
|
-
import type { WebMiddleware, WebProtectionConfig } from './types';
|
|
1
|
+
import type { UnsharedClient } from '../client.mjs';
|
|
2
|
+
import { VerdictCache } from '../middleware/verdict-cache.mjs';
|
|
3
|
+
import type { Verdict } from '../middleware/verdict-cache.mjs';
|
|
4
|
+
import type { WebMiddleware, WebProtectionConfig } from './types.mjs';
|
|
5
5
|
/**
|
|
6
6
|
* Web Standard equivalent of `unsharedBoundToUser` from src/middleware/index.ts.
|
|
7
7
|
*
|
|
@@ -49,7 +49,7 @@ interface InjectWarnState {
|
|
|
49
49
|
* single middleware instance; when omitted, the module-level default is used.
|
|
50
50
|
*/
|
|
51
51
|
declare function injectIntoHtmlResponse(response: Response, fingerprintScript: string, extraCookies?: string[], warnState?: InjectWarnState): Promise<Response>;
|
|
52
|
-
declare function fetchAndCacheVerdict(client: UnsharedClient, cache: VerdictCache, userId: string, emailAddress: string, deviceId: string, fingerprintId: string | undefined, sessionId: string, timeoutMs?: number, reportFailOpen?: (reason: 'timeout' | 'http_error' | 'no_device_id', status?: number) => void): Promise<Verdict>;
|
|
52
|
+
declare function fetchAndCacheVerdict(client: UnsharedClient, cache: VerdictCache, userId: string, emailAddress: string, deviceId: string | undefined, fingerprintId: string | undefined, fullHash: string | undefined, permanentDeviceId: string | undefined, sessionId: string, timeoutMs?: number, reportFailOpen?: (reason: 'timeout' | 'http_error' | 'no_device_id', status?: number) => void): Promise<Verdict>;
|
|
53
53
|
export declare const __test_fetchAndCacheVerdict: typeof fetchAndCacheVerdict;
|
|
54
54
|
export declare const __test_injectIntoHtmlResponse: typeof injectIntoHtmlResponse;
|
|
55
55
|
export declare function __test_resetInjectWarning(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{sha256HexWeb}from"../webcrypto";import{VerdictCache}from"../middleware/verdict-cache";import{RateLimitBackoff}from"../middleware/rate-limit-backoff";import{DispatchDedupe}from"../middleware/dispatch-dedupe";import{generateFingerprintScript}from"../middleware/injection/fingerprint-script";import{generateGatePage}from"../middleware/injection/gate-page";import{flaggedResponse}from"../middleware/utils/flagged-response";import{maskEmail}from"../middleware/utils/mask-email";import{isHtmlContentType,isHtmlNavigation}from"../middleware/utils/content-type";import{shouldSkipPath}from"../middleware/utils/skip-paths";import{shouldIncludePath}from"../middleware/utils/include-path";import{isBot}from"../middleware/utils/is-bot";import{isSentinelUserId,SENTINEL_STICKINESS_TTL_MS}from"../middleware/utils/sentinel-user-id";import{runProcessUserEvent,DISPATCH_TIMEOUT_MS,SUBMIT_FP_TIMEOUT_MS}from"../middleware/dispatch-processing";import{parseCookieFromRequest,extractClientIpFromRequest,extractDeviceIdFromRequest,extractDeviceIdFromRequestOrUnknown,isSecureWebRequest,jsonResponse,emptyResponse,bodyResponse,mergeResponseHeaders}from"./web-helpers";const CHECK_USER_TIMEOUT_MS=1500,FAIL_OPEN_TTL_MS=5e3;export function createWebProtectionMiddleware(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");const{userId:r,emailAddress:s,routePrefix:n="/__unshared",corsOrigins:i,cacheTTL:o=6e4,skipPaths:a,includePathPrefix:d,sessionId:c,deviceId:u,fingerprintSdkBundle:l="",onFlagged:p,onError:m,onFailOpen:f,disableBotFilter:h=!1,checkUserTimeoutMs:_=CHECK_USER_TIMEOUT_MS,blockFlagged:g=!1,flaggedMode:I,autoInterstitial:R=!1,interstitialFlowType:v="email_verification"}=t,S=I??(g?"gate":void 0),y=R||"overlay"===S,w=e=>{if(f)try{f(e)}catch{}},E=Date.now().toString(36),T=void 0!==S||y,C="gate"===S?"flaggedMode 'gate' (blockFlagged)":"overlay"===S?"flaggedMode 'overlay'":"autoInterstitial";if(T&&!l)throw new Error(`[Unshared] ${C} requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the interstitial).`);if(T&&"/__unshared"!==n)throw new Error(`[Unshared] ${C} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const A="gate"===S?generateGatePage(n,E):"",k=new VerdictCache(o),x=new Map,F=new RateLimitBackoff,U=new DispatchDedupe,j=generateFingerprintScript(n,E,{autoInterstitial:y,interstitialFlowType:v}),b={emitted:!1},q=`${n}/fp.js`,P=`${n}/submit-fp`,O=`${n}/verify-trigger`,D=`${n}/verify`,H=`${n}/status`,M=i?Array.isArray(i)?i:[i]:null;return async function(t,i,o){const f=e=>{o?.waitUntil&&o.waitUntil(e)};let g,I,R;try{const e=new URL(t.url);g=e.pathname,I=e.search}catch{return i(t)}if(g.startsWith(n+"/")){const n=function(e){if(!M)return{};const t=e.headers.get("origin")??"",r=M.includes("*");if(!r&&!M.includes(t))return{};const s={"Access-Control-Allow-Origin":r?"*":t,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id"};return r||(s["Access-Control-Allow-Credentials"]="true"),s}(t);if("OPTIONS"===t.method)return emptyResponse(204,n);if(("GET"===t.method||"HEAD"===t.method)&&g===q){if(!l)return jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Fingerprint SDK bundle not configured. Pass fingerprintSdkBundle in config."}},n);const e={...n,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600","Content-Length":String((new TextEncoder).encode(l).byteLength)};return"HEAD"===t.method?new Response(null,{status:200,headers:new Headers(e)}):bodyResponse(200,l,e)}if("POST"===t.method&&(g===P||g===O||g===D)){let i;try{i=await t.json()}catch{return jsonResponse(400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"Request body is not valid JSON."}},n)}return g===P?handleSubmitFp(t,i,{client:e,verdictCache:k,rateLimitBackoff:F,dispatchDedupe:U,resolveUserId:r,resolveEmailAddress:s,resolveSessionId:c,resolveDeviceId:u,disableBotFilter:h,onError:m,registerBackground:f},n):g===O?handleVerifyTriggerWeb(t,i,{client:e,verdictCache:k,resolveUserId:r,resolveEmailAddress:s,resolveDeviceId:u,onError:m},n):handleVerifyWeb(t,i,{client:e,verdictCache:k,resolveUserId:r,resolveEmailAddress:s,resolveDeviceId:u,onError:m},n)}if("GET"===t.method&&g===H){let i;try{i=r(t)}catch{}if(!i)return jsonResponse(200,{status:"anonymous"},n);const o=resolveEmail(t,s);let a=k.get(i);if((!a||k.isStale(i))&&o&&!F.isPaused()&&!k.isRefreshing(i)){k.markRefreshing(i);try{const r=extractDeviceIdFromRequest(t,u),s=parseCookieFromRequest(t,"__unshared_fingerprint_id")||void 0,n=extractSessionIdFromRequest(t,c),d=r??s??"unknown";await fetchAndCacheVerdict(e,k,i,o,d,s,n,_,(e,t)=>w({operation:"checkUser",reason:e,status:t,userId:i,emailAddress:o})),a=k.get(i)}catch(e){m&&m(e,{operation:"checkUser",userId:i,emailAddress:o}),w({operation:"checkUser",reason:"exception",userId:i,emailAddress:o})}finally{k.clearRefreshing(i)}}return a&&a.isFlagged&&!a.isVerified&&(void 0!==S||p)&&o?jsonResponse(200,{status:"flagged",email:maskEmail(o)},n):jsonResponse(200,{status:"ok"},n)}return jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},n)}if(shouldSkipPath(g,a,void 0!==S))return i(t);if(!shouldIncludePath(g,d))return injectIntoHtmlResponse(await i(t),j,void 0,b);try{R=r(t)}catch{}if(isSentinelUserId(R)){const e=parseCookieFromRequest(t,"__unshared_uid"),r=parseCookieFromRequest(t,"__unshared_uid_at"),s=r?Number(r):NaN,n=Number.isFinite(s)&&Date.now()-s<=SENTINEL_STICKINESS_TTL_MS;R=e&&n?e:void 0}if(!R){const e=isSecureWebRequest(t)?"; Secure":"",r=[`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`];return injectIntoHtmlResponse(await i(t),j,r,b)}const v=resolveEmail(t,s),y=[],E=isSecureWebRequest(t)?"; Secure":"";if(y.push(`__unshared_uid=${encodeURIComponent(R)}; Path=/; SameSite=Lax${E}`),y.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${E}`),v&&y.push(`__unshared_email=${encodeURIComponent(v)}; HttpOnly; Path=/; SameSite=Lax${E}`),!v)return injectIntoHtmlResponse(await i(t),j,y,b);const T=extractSessionIdFromRequest(t,c),C=extractDeviceIdFromRequest(t,u),N=parseCookieFromRequest(t,"__unshared_fingerprint_id")||void 0,L=t.headers.get("user-agent")??"",$=extractClientIpFromRequest(t),V=C??N;if(!h&&isBot(L))return i(t);let B=k.get(R);if(B)k.isStale(R)&&!k.isRefreshing(R)&&(k.markRefreshing(R),f(fetchAndCacheVerdict(e,k,R,v,V??"unknown",N,T,_,(e,t)=>w({operation:"checkUser",reason:e,status:t,userId:R,emailAddress:v})).catch(()=>w({operation:"checkUser",reason:"exception",userId:R,emailAddress:v})).finally(()=>k.clearRefreshing(R))));else try{let t=x.get(R);t||(t=fetchAndCacheVerdict(e,k,R,v,V??"unknown",N,T,_,(e,t)=>w({operation:"checkUser",reason:e,status:t,userId:R,emailAddress:v})).finally(()=>x.delete(R)),x.set(R,t)),B=await t}catch{return w({operation:"checkUser",reason:"exception",userId:R,emailAddress:v}),injectIntoHtmlResponse(await i(t),j,y,b)}const W=B.isFlagged&&!B.isVerified;if("gate"===S&&W)return isHtmlNavigation(t.method,t.headers.get("accept")??void 0)?bodyResponse(200,A,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"}):jsonResponse(403,flaggedResponse(v));if("overlay"===S&&W)return isHtmlNavigation(t.method,t.headers.get("accept")??void 0)?injectIntoHtmlResponse(await i(t),j,y,b):jsonResponse(403,flaggedResponse(v));if(B.isFlagged&&!B.isVerified&&p)try{const e=await p({userId:R,emailAddress:v,verdict:B,request:t});if(e)return injectIntoHtmlResponse(e,j,y,b)}catch(e){m&&m(e,{operation:"checkUser",userId:R,emailAddress:v})}return B.isFlagged||"unknown"===T||!V||F.isPaused()||f(dispatchUserEvent(e,k,F,U,{userId:R,emailAddress:v,sessionId:T,deviceId:V,fingerprintId:N,userAgent:L,ipAddress:$,eventType:g+I},m)),injectIntoHtmlResponse(await i(t),j,y,b)}}const defaultInjectWarnState={emitted:!1};function passthroughResponse(e,t,r){if(!t||0===t.length)return e;const s=mergeResponseHeaders(e.headers,void 0,t);return new Response(r,{status:e.status,statusText:e.statusText,headers:s})}async function injectIntoHtmlResponse(e,t,r,s=defaultInjectWarnState){const n=e.headers.get("content-type");if(204===e.status||304===e.status)return passthroughResponse(e,r,null);if(null===e.body)return n||s.emitted||(s.emitted=!0,console.warn("[Unshared] The response returned by your handler has no body (e.g. NextResponse.next()), so fingerprint injection did not run. In Next.js middleware, fingerprints must be collected by the browser/React SDK, while this middleware handles verdict + enforcement. See the Next.js section of the integration guide.")),passthroughResponse(e,r,null);if(!isHtmlContentType(n??void 0))return passthroughResponse(e,r,e.body);const i=await e.text(),o=i.lastIndexOf("</body>"),a=-1===o?i+t:i.slice(0,o)+t+i.slice(o),d=mergeResponseHeaders(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(a).length)},r);return d.delete("ETag"),d.delete("Last-Modified"),d.delete("Content-Encoding"),new Response(a,{status:e.status,statusText:e.statusText,headers:d})}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=parseCookieFromRequest(e,"__unshared_email");if(r)return r}function resolveEmailWithBody(e,t,r){const s=resolveEmail(e,r);if(s)return s;const n=t.email;return"string"==typeof n&&n?n:void 0}function extractSessionIdFromRequest(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return parseCookieFromRequest(e,"__unshared_sid")??"unknown"}function dispatchUserEvent(e,t,r,s,n,i){return s.mark(n.userId,n.eventType),runProcessUserEvent({client:e,cache:t,backoff:r,onError:i},{eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,sessionHash:n.sessionId,userAgent:n.userAgent},DISPATCH_TIMEOUT_MS)}async function fetchAndCacheVerdict(e,t,r,s,n,i,o,a=CHECK_USER_TIMEOUT_MS,d){const c=()=>{const e=t.get(r);return t.set(r,{isFlagged:e?.isFlagged??!1,isVerified:e?.isVerified??!1,emailAddress:s,sessionId:o},5e3),t.get(r)},u={timeoutMs:a,maxRetries:0};let l;n&&"unknown"!==n&&(u.deviceId=n),i&&(u.fingerprintId=i);const p=await Promise.race([e.checkUser(s,u),new Promise(e=>{l=setTimeout(()=>e(null),a)})]);return clearTimeout(l),p?p.failedOpen?(d?.(p.failedOpen.reason??"http_error",p.failedOpen.status),c()):(t.set(r,{isFlagged:p.data?.is_user_flagged??!1,isVerified:!1,emailAddress:s,sessionId:o}),t.get(r)):(d?.("timeout"),c())}async function handleSubmitFp(e,t,r,s){try{const 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 i,o,a;try{const t=r.resolveUserId(e);t&&!isSentinelUserId(t)&&(i=t)}catch{}if(!i){const e="string"==typeof t.user_id?t.user_id:void 0;e&&!isSentinelUserId(e)&&(i=e)}if(!i){const t=parseCookieFromRequest(e,"__unshared_uid");t&&!isSentinelUserId(t)&&(i=t)}try{o=r.resolveEmailAddress?r.resolveEmailAddress(e):void 0}catch{}o=o??parseCookieFromRequest(e,"__unshared_email")??t.email??void 0;try{a=r.resolveSessionId?r.resolveSessionId(e):void 0}catch{}a=a??t.session_id??parseCookieFromRequest(e,"__unshared_sid");const d=extractClientIpFromRequest(e),c=e.headers.get("user-agent")??"";if(!r.disableBotFilter&&isBot(c))return jsonResponse(200,{success:!0},s);const u=(n.fingerprint_id&&n.fingerprint_id.length>0?n.fingerprint_id:void 0)??extractDeviceIdFromRequestOrUnknown(e,r.resolveDeviceId),l=n.fingerprint_id||void 0,p=n.full_hash||void 0,m=isSecureWebRequest(e)?"; Secure":"",f=[];if(p&&!parseCookieFromRequest(e,"__unshared_fingerprint_id")&&f.push(`__unshared_fingerprint_id=${encodeURIComponent(p)}; HttpOnly; Path=/; SameSite=Lax${m}`),l){const t=parseCookieFromRequest(e,"__unshared_fp_id");t&&t===l||f.push(`__unshared_fp_id=${encodeURIComponent(l)}; Path=/; SameSite=Lax; Max-Age=31536000${m}`)}let h;if(o&&!parseCookieFromRequest(e,"__unshared_email")&&f.push(`__unshared_email=${encodeURIComponent(o)}; HttpOnly; Path=/; SameSite=Lax${m}`),"string"==typeof t.event_type&&t.event_type)h=t.event_type;else{const t=e.headers.get("referer")??e.headers.get("referrer");let r="unknown";if(t)try{const e=new URL(t);r=(e.pathname||"/")+(e.search||"")}catch{}h=r}const _=e.headers.get("x-idempotency-key")||void 0,g=Date.now();let I;if(_?I=`${_}|${g}`:l&&i&&(I=`${await sha256HexWeb(`${l}|${i}|${h}`)}|${g}`),i){const e=r.client.submitFingerprintEvent(n,{userId:i,emailAddress:o,sessionHash:a,eventType:h,ipAddress:d,userAgent:c,idempotencyKey:I}).catch(e=>{r.onError&&r.onError(e,{operation:"submitFingerprintEvent",userId:i,emailAddress:o})});r.registerBackground?.(e)}if(i&&o&&!r.rateLimitBackoff.isPaused()&&!r.dispatchDedupe.wasRecentlyDispatched(i,h)){const e=runProcessUserEvent({client:r.client,cache:r.verdictCache,backoff:r.rateLimitBackoff,onError:r.onError},{eventType:h,userId:i,emailAddress:o,ipAddress:d,deviceId:u,fingerprintId:l,sessionHash:a??"unknown",userAgent:c},SUBMIT_FP_TIMEOUT_MS);r.registerBackground?.(e)}const R={...s,"Content-Type":"application/json"},v=new Response(JSON.stringify({success:!0}),{status:200,headers:R});for(const e of f)v.headers.append("Set-Cookie",e);return v}catch{return jsonResponse(200,{success:!0},s)}}async function handleVerifyTriggerWeb(e,t,r,s){try{const n=resolveEmailWithBody(e,t??{},r.resolveEmailAddress);if(!n)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},s);const i=extractDeviceIdFromRequestOrUnknown(e,r.resolveDeviceId),o=parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,a=await r.client.triggerEmailVerification(n,i,{fingerprintId:o});return a.success?jsonResponse(200,{success:!0,data:a.data},s):jsonResponse(200,{success:!1,error:a.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},s)}catch(e){return r.onError&&r.onError(e,{operation:"verifyTrigger"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},s)}}async function handleVerifyWeb(e,t,r,s){try{const n=resolveEmailWithBody(e,t??{},r.resolveEmailAddress),i=t?.code;if(!n||!i)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email and code are required"}},s);const o=extractDeviceIdFromRequestOrUnknown(e,r.resolveDeviceId),a=parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,d=await r.client.verify(n,o,i,{fingerprintId:a});if(d.success){let t;try{t=r.resolveUserId(e)}catch{}return t&&r.verdictCache.update(t,{isVerified:!0}),jsonResponse(200,{success:!0,data:{verified:!0}},s)}return jsonResponse(200,{success:!1,error:d.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}},s)}catch(e){return r.onError&&r.onError(e,{operation:"verify"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},s)}}export const __test_fetchAndCacheVerdict=fetchAndCacheVerdict;export const __test_injectIntoHtmlResponse=injectIntoHtmlResponse;export function __test_resetInjectWarning(){defaultInjectWarnState.emitted=!1}
|
|
1
|
+
import{sha256HexWeb}from"../webcrypto.mjs";import{VerdictCache}from"../middleware/verdict-cache.mjs";import{RateLimitBackoff}from"../middleware/rate-limit-backoff.mjs";import{DispatchDedupe}from"../middleware/dispatch-dedupe.mjs";import{generateFingerprintScript}from"../middleware/injection/fingerprint-script.mjs";import{generateGatePage}from"../middleware/injection/gate-page.mjs";import{flaggedResponse}from"../middleware/utils/flagged-response.mjs";import{maskEmail}from"../middleware/utils/mask-email.mjs";import{isHtmlContentType,isHtmlNavigation}from"../middleware/utils/content-type.mjs";import{shouldSkipPath}from"../middleware/utils/skip-paths.mjs";import{shouldIncludePath}from"../middleware/utils/include-path.mjs";import{isBot}from"../middleware/utils/is-bot.mjs";import{isSentinelUserId,SENTINEL_STICKINESS_TTL_MS}from"../middleware/utils/sentinel-user-id.mjs";import{runCheckUser,runProcessUserEvent,DISPATCH_TIMEOUT_MS,SUBMIT_FP_TIMEOUT_MS}from"../middleware/dispatch-processing.mjs";import{parseCookieFromRequest,extractClientIpFromRequest,extractDeviceIdFromRequest,isSecureWebRequest,jsonResponse,emptyResponse,bodyResponse,mergeResponseHeaders,readJsonRequest}from"./web-helpers.mjs";const CHECK_USER_TIMEOUT_MS=1500,FAIL_OPEN_TTL_MS=5e3;export function createWebProtectionMiddleware(e,t){if(!t.userId)throw new Error("[Unshared] userId resolver is required");const{userId:s,emailAddress:r,routePrefix:n="/__unshared",corsOrigins:o,cacheTTL:i=6e4,skipPaths:a,includePathPrefix:d,sessionId:c,deviceId:u,fingerprintSdkBundle:p="",onFlagged:l,onError:m,onFailOpen:h,disableBotFilter:_=!1,checkUserTimeoutMs:f=CHECK_USER_TIMEOUT_MS,blockFlagged:R=!1,flaggedMode:g,autoInterstitial:I=!1,interstitialFlowType:v="email_verification"}=t,y=g??(R?"gate":void 0),S=I||"overlay"===y,C=e=>{if(h)try{h(e)}catch{}},x=Date.now().toString(36),w=void 0!==y||S,k="gate"===y?"flaggedMode 'gate' (blockFlagged)":"overlay"===y?"flaggedMode 'overlay'":"autoInterstitial";if(w&&!p)throw new Error(`[Unshared] ${k} requires fingerprintSdkBundle (the browser SDK UMD served at {routePrefix}/fp.js renders the interstitial).`);if(w&&"/__unshared"!==n)throw new Error(`[Unshared] ${k} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const F="gate"===y?generateGatePage(n,x):"",A=new VerdictCache(i),j=new Map,E=new RateLimitBackoff,b=new DispatchDedupe,T=generateFingerprintScript(n,x,{autoInterstitial:S,interstitialFlowType:v,debug:t.debug,disableBotFilter:t.disableBotFilter}),q={emitted:!1},U=`${n}/fp.js`,P=`${n}/submit-fp`,D=`${n}/verify-trigger`,M=`${n}/verify`,H=`${n}/status`,$=o?Array.isArray(o)?o:[o]:null;return async function(t,o,i){const h=e=>{i?.waitUntil&&i.waitUntil(e)};let R,g,I;try{const e=new URL(t.url);R=e.pathname,g=e.search}catch{return o(t)}if(R.startsWith(n+"/")){const n=function(e){if(!$)return{};const t=e.headers.get("origin")??"",s=$.includes("*");if(!s&&!$.includes(t))return{};const r={"Access-Control-Allow-Origin":s?"*":t,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id, X-Permanent-Device-Id","Access-Control-Expose-Headers":"X-Unshared-Enforcement-Reconciled"};return s||(r["Access-Control-Allow-Credentials"]="true"),r}(t);if("OPTIONS"===t.method)return emptyResponse(204,n);if(("GET"===t.method||"HEAD"===t.method)&&R===U){if(!p)return new Response("HEAD"===t.method?null:"/* fingerprint SDK unavailable */",{status:503,headers:{...n,"Content-Type":"application/javascript","Cache-Control":"no-store"}});const e={...n,"Content-Type":"application/javascript","Cache-Control":"public, max-age=3600","Content-Length":String((new TextEncoder).encode(p).byteLength)};return"HEAD"===t.method?new Response(null,{status:200,headers:new Headers(e)}):bodyResponse(200,p,e)}if("POST"===t.method&&(R===P||R===D||R===M)){const o=await readJsonRequest(t);if(!o.success)return jsonResponse(o.status,{success:!1,error:o.error},n);const i=o.data;return R===P?handleSubmitFp(t,i,{client:e,verdictCache:A,rateLimitBackoff:E,dispatchDedupe:b,resolveUserId:s,resolveEmailAddress:r,resolveSessionId:c,resolveDeviceId:u,disableBotFilter:_,onError:m,registerBackground:h},n):R===D?handleVerifyTriggerWeb(t,i,{client:e,verdictCache:A,resolveUserId:s,resolveEmailAddress:r,resolveDeviceId:u,resolveSessionId:c,onError:m},n):handleVerifyWeb(t,i,{client:e,verdictCache:A,resolveUserId:s,resolveEmailAddress:r,resolveDeviceId:u,resolveSessionId:c,onError:m},n)}if("GET"===t.method&&R===H){let o;try{o=s(t)}catch{}if(!o)return jsonResponse(200,{status:"anonymous"},n);const i=resolveEmail(t,r);let a=A.get(o);if((!a||A.isStale(o))&&i&&!E.isPaused()&&!A.isRefreshing(o)){A.markRefreshing(o);try{const s=extractDeviceIdFromRequest(t,u),r=parseCookieFromRequest(t,"__unshared_stable_hash")||parseCookieFromRequest(t,"__unshared_fp_id")||void 0,n=parseCookieFromRequest(t,"__unshared_full_hash")||parseCookieFromRequest(t,"__unshared_fingerprint_id")||void 0,d=extractPermanentDeviceIdFromRequest(t),p=extractSessionIdFromRequest(t,c);await fetchAndCacheVerdict(e,A,o,i,s,r,n,d,p,f,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:o,emailAddress:i})),a=A.get(o)}catch(e){m&&m(e,{operation:"checkUser",userId:o,emailAddress:i}),C({operation:"checkUser",reason:"exception",userId:o,emailAddress:i})}finally{A.clearRefreshing(o)}}return a&&a.isFlagged&&!a.isVerified&&(void 0!==y||l)&&i?jsonResponse(200,{status:"flagged",email:maskEmail(i)},n):jsonResponse(200,{status:"ok"},n)}return jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},n)}if(shouldSkipPath(R,a,void 0!==y))return o(t);try{I=s(t)}catch{}if(isSentinelUserId(I)){const e=parseCookieFromRequest(t,"__unshared_uid"),s=parseCookieFromRequest(t,"__unshared_uid_at"),r=s?Number(s):NaN,n=Number.isFinite(r)&&Date.now()-r<=SENTINEL_STICKINESS_TTL_MS;I=e&&n?e:void 0}if(!I){const e=isSecureWebRequest(t)?"; Secure":"",s=[`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`,`__unshared_verification_id=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0${e}`];return injectIntoHtmlResponse(await o(t),T,s,q)}const v=resolveEmail(t,r),S=[],x=isSecureWebRequest(t)?"; Secure":"";if(S.push(`__unshared_uid=${encodeURIComponent(I)}; Path=/; SameSite=Lax; Max-Age=31536000${x}`),S.push(`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax; Max-Age=31536000${x}`),v&&S.push(`__unshared_email=${encodeURIComponent(v)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=31536000${x}`),!shouldIncludePath(R,d))return injectIntoHtmlResponse(await o(t),T,S,q);if(!v)return injectIntoHtmlResponse(await o(t),T,S,q);const w=extractSessionIdFromRequest(t,c),k=extractDeviceIdFromRequest(t,u),L=parseCookieFromRequest(t,"__unshared_stable_hash")||parseCookieFromRequest(t,"__unshared_fp_id")||void 0,O=parseCookieFromRequest(t,"__unshared_full_hash")||parseCookieFromRequest(t,"__unshared_fingerprint_id")||void 0,N=extractPermanentDeviceIdFromRequest(t),V=t.headers.get("user-agent")??"",W=extractClientIpFromRequest(t);if(!_&&isBot(V)){const e=await o(t);return passthroughResponse(e,S,e.body)}let B=A.get(I);if(B)A.isStale(I)&&!A.isRefreshing(I)&&(A.markRefreshing(I),h(fetchAndCacheVerdict(e,A,I,v,k,L,O,N,w,f,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:I,emailAddress:v})).catch(()=>C({operation:"checkUser",reason:"exception",userId:I,emailAddress:v})).finally(()=>A.clearRefreshing(I))));else try{let t=j.get(I);t||(t=fetchAndCacheVerdict(e,A,I,v,k,L,O,N,w,f,(e,t)=>C({operation:"checkUser",reason:e,status:t,userId:I,emailAddress:v})).finally(()=>j.delete(I)),j.set(I,t)),B=await t}catch{return C({operation:"checkUser",reason:"exception",userId:I,emailAddress:v}),injectIntoHtmlResponse(await o(t),T,S,q)}const K=B.isFlagged&&!B.isVerified;if("gate"===y&&K){if(isHtmlNavigation(t.method,t.headers.get("accept")??void 0)){const e=bodyResponse(200,F,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store"});return passthroughResponse(e,S,e.body)}const e=jsonResponse(403,flaggedResponse(v));return passthroughResponse(e,S,e.body)}if("overlay"===y&&K){if(isHtmlNavigation(t.method,t.headers.get("accept")??void 0))return injectIntoHtmlResponse(await o(t),T,S,q);const e=jsonResponse(403,flaggedResponse(v));return passthroughResponse(e,S,e.body)}if(B.isFlagged&&!B.isVerified&&l)try{const e=await l({userId:I,emailAddress:v,verdict:B,request:t});if(e)return injectIntoHtmlResponse(e,T,S,q)}catch(e){m&&m(e,{operation:"checkUser",userId:I,emailAddress:v})}return B.isFlagged||!w||"unknown"===w||E.isPaused()||h(dispatchUserEvent(e,A,E,b,{userId:I,emailAddress:v,sessionId:w,deviceId:k,fingerprintId:L,fullHash:O,permanentDeviceId:N,userAgent:V,ipAddress:W,eventType:R+g},m)),injectIntoHtmlResponse(await o(t),T,S,q)}}const defaultInjectWarnState={emitted:!1};function passthroughResponse(e,t,s){if(!t||0===t.length)return e;const r=mergeResponseHeaders(e.headers,void 0,t);return new Response(s,{status:e.status,statusText:e.statusText,headers:r})}async function injectIntoHtmlResponse(e,t,s,r=defaultInjectWarnState){const n=e.headers.get("content-type");if(204===e.status||304===e.status)return passthroughResponse(e,s,null);if(null===e.body)return n||r.emitted||(r.emitted=!0,console.warn("[Unshared] The response returned by your handler has no body (e.g. NextResponse.next()), so fingerprint injection did not run. In Next.js middleware, fingerprints must be collected by the browser/React SDK, while this middleware handles verdict + enforcement. See the Next.js section of the integration guide.")),passthroughResponse(e,s,null);if(!isHtmlContentType(n??void 0))return passthroughResponse(e,s,e.body);const o=await e.text(),i=o.lastIndexOf("</body>"),a=-1===i?o+t:o.slice(0,i)+t+o.slice(i),d=mergeResponseHeaders(e.headers,{"Cache-Control":"no-store","Content-Length":String((new TextEncoder).encode(a).length)},s);return d.delete("ETag"),d.delete("Last-Modified"),d.delete("Content-Encoding"),new Response(a,{status:e.status,statusText:e.statusText,headers:d})}function resolveEmail(e,t){if(t)try{const s=t(e);if(s)return s}catch{}const s=parseCookieFromRequest(e,"__unshared_email");if(s)return s}function resolveEmailWithBody(e,t,s){const r=resolveEmail(e,s);if(r)return r;const n=t.email;return"string"==typeof n&&n?n:void 0}function extractSessionIdFromRequest(e,t){if(t)try{const s=t(e);if(s)return s}catch{}return parseCookieFromRequest(e,"__unshared_sid")??"unknown"}function isPermanentDeviceId(e){return"string"==typeof e&&/^upid_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(e)}function extractPermanentDeviceIdFromRequest(e){const t=parseCookieFromRequest(e,"__unshared_device_id");if(isPermanentDeviceId(t))return t;const s=e.headers.get("x-permanent-device-id");return isPermanentDeviceId(s)?s:void 0}function dispatchUserEvent(e,t,s,r,n,o){return r.mark(n.userId,n.eventType),runProcessUserEvent({client:e,cache:t,backoff:s,onError:o},{eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,fullHash:n.fullHash,permanentDeviceId:n.permanentDeviceId,sessionHash:n.sessionId,userAgent:n.userAgent},DISPATCH_TIMEOUT_MS)}async function fetchAndCacheVerdict(e,t,s,r,n,o,i,a,d,c=CHECK_USER_TIMEOUT_MS,u){const p=()=>{const e=t.get(s);return t.set(s,{isFlagged:e?.isFlagged??!1,isVerified:e?.isVerified??!1,emailAddress:r,sessionId:d},5e3),t.get(s)},l={timeoutMs:c,maxRetries:0};let m;n&&"unknown"!==n&&(l.deviceId=n),o&&"unknown"!==o&&(l.fingerprintId=o),i&&"unknown"!==i&&(l.fullHash=i),a&&(l.permanentDeviceId=a),d&&"unknown"!==d&&(l.sessionHash=d);const h=await Promise.race([e.checkUser(r,l),new Promise(e=>{m=setTimeout(()=>e(null),c)})]);return clearTimeout(m),h?h.failedOpen?(u?.(h.failedOpen.reason??"http_error",h.failedOpen.status),p()):(t.set(s,{isFlagged:h.data?.is_user_flagged??!1,isVerified:!1,emailAddress:r,sessionId:d}),t.get(s)):(u?.("timeout"),p())}async function handleSubmitFp(e,t,s,r){try{const n=t?.fingerprint&&"object"==typeof t.fingerprint?t.fingerprint:t??{},o=t?.context&&"object"==typeof t.context?t.context:{},i=o.identity&&"object"==typeof o.identity?o.identity:{},a=o.identifiers&&"object"==typeof o.identifiers?o.identifiers:{},d=o.event&&"object"==typeof o.event?o.event:{},c={full_hash:n.hash??n.full_hash??"",fingerprint_id:n.stableHash??n.stable_hash??n.fingerprint_id??""};let u,p,l;try{const t=s.resolveUserId(e);t&&!isSentinelUserId(t)&&(u=t)}catch{}if(!u){const e="string"==typeof i.user_id?i.user_id:"string"==typeof t.user_id?t.user_id:void 0;e&&!isSentinelUserId(e)&&(u=e)}if(!u){const t=parseCookieFromRequest(e,"__unshared_uid");t&&!isSentinelUserId(t)&&(u=t)}try{p=s.resolveEmailAddress?s.resolveEmailAddress(e):void 0}catch{}p=p??parseCookieFromRequest(e,"__unshared_email")??i.email_address??t.email??void 0;try{l=s.resolveSessionId?s.resolveSessionId(e):void 0}catch{}l=l??a.session_hash??t.session_id??parseCookieFromRequest(e,"__unshared_sid");const m=extractClientIpFromRequest(e),h=e.headers.get("user-agent")??"";if(!s.disableBotFilter&&isBot(h))return jsonResponse(200,{success:!0,data:{skipped:!0}},r);let _;try{_=s.resolveDeviceId?.(e)}catch{}const f=[_,e.headers.get("x-device-id"),a.device_id,c.fingerprint_id,parseCookieFromRequest(e,"__unshared_stable_hash"),parseCookieFromRequest(e,"__unshared_fp_id")].find(e=>"string"==typeof e&&""!==e.trim()&&"unknown"!==e.trim()),R=c.fingerprint_id||void 0,g=c.full_hash||void 0,I=extractPermanentDeviceIdFromRequest(e),v=e.headers.get("x-permanent-device-id")??a.unshared_device_id??t.permanent_device_id,y=I??(isPermanentDeviceId(v)?v:`upid_${crypto.randomUUID()}`),S=isSecureWebRequest(e)?"; Secure":"",C=[];C.push(`__unshared_device_id=${encodeURIComponent(y)}; Path=/; SameSite=Lax; Max-Age=34560000${S}`);const x=parseCookieFromRequest(e,"__unshared_fingerprint_id")||g;let w;if(x&&C.push(`__unshared_fingerprint_id=${encodeURIComponent(x)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=34560000${S}`),g&&C.push(`__unshared_full_hash=${encodeURIComponent(g)}; Path=/; SameSite=Lax; Max-Age=34560000${S}`),R&&(C.push(`__unshared_fp_id=${encodeURIComponent(R)}; Path=/; SameSite=Lax; Max-Age=34560000${S}`),C.push(`__unshared_stable_hash=${encodeURIComponent(R)}; Path=/; SameSite=Lax; Max-Age=34560000${S}`)),p&&C.push(`__unshared_email=${encodeURIComponent(p)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=31536000${S}`),"string"==typeof d.type&&d.type)w=d.type;else if("string"==typeof t.event_type&&t.event_type)w=t.event_type;else{const t=e.headers.get("referer")??e.headers.get("referrer");let s="unknown";if(t)try{const e=new URL(t);s=(e.pathname||"/")+(e.search||"")}catch{}w=s}const k=e.headers.get("x-idempotency-key")||void 0,F=Date.now();let A;k?A=`${k}|${F}`:R&&u&&(A=`${await sha256HexWeb(`${R}|${u}|${w}`)}|${F}`);const j={...a,unshared_device_id:y,...f&&"unknown"!==f?{device_id:f}:{},...R?{stable_hash:R}:{},...g?{full_hash:g}:{},...l?{session_hash:l}:{}};f||delete j.device_id;const E=o.sdk&&"object"==typeof o.sdk?o.sdk.name:void 0,b="string"==typeof E&&E.startsWith("unshared-")&&Object.prototype.hasOwnProperty.call(t,"fingerprint")&&t.context&&"object"==typeof t.context&&!Array.isArray(t.context)&&o.identity&&"object"==typeof o.identity&&!Array.isArray(o.identity)&&o.identifiers&&"object"==typeof o.identifiers&&!Array.isArray(o.identifiers)&&o.event&&"object"==typeof o.event&&!Array.isArray(o.event),T=!Object.prototype.hasOwnProperty.call(t,"context")&&"string"==typeof t.hash&&"string"==typeof t.stable_hash&&"string"==typeof t.collected_at&&t.components&&"object"==typeof t.components&&!Array.isArray(t.components),q=b||T?{...t,fingerprint:n,context:{...o,identity:{...i,...u?{user_id:u}:{},...p?{email_address:p}:{}},identifiers:j,event:{...d,type:w,ip_address:m,user_agent:h}}}:t,U=await s.client.submitFingerprint(q,{idempotencyKey:A});if(U.success||s.onError?.(U.error,{operation:"submitFingerprintEvent",userId:u,emailAddress:p}),u&&p&&!s.rateLimitBackoff.isPaused()&&!s.dispatchDedupe.wasRecentlyDispatched(u,w)){const e=runCheckUser({client:s.client,cache:s.verdictCache,backoff:s.rateLimitBackoff,onError:s.onError},{user_id:u,email_address:p,identifiers:j},u,p,SUBMIT_FP_TIMEOUT_MS);s.registerBackground?.(e)}const P={...r,"Content-Type":"application/json"},D=new Response(JSON.stringify(U.success?{success:!0,data:U.data}:{success:!1,error:U.error}),{status:200,headers:P});for(const e of C)D.headers.append("Set-Cookie",e);return D}catch{return jsonResponse(200,{success:!0,data:{accepted:!1}},r)}}async function handleVerifyTriggerWeb(e,t,s,r){try{const n=resolveEmailWithBody(e,t??{},s.resolveEmailAddress);if(!n)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Email is required"}},r);const o=extractDeviceIdFromRequest(e,s.resolveDeviceId),i=parseCookieFromRequest(e,"__unshared_stable_hash")||parseCookieFromRequest(e,"__unshared_fp_id")||void 0,a=parseCookieFromRequest(e,"__unshared_full_hash")||parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,d=extractPermanentDeviceIdFromRequest(e),c=extractSessionIdFromRequest(e,s.resolveSessionId),u=await s.client.triggerEmailVerification(n,o&&"unknown"!==o?o:"",{fingerprintId:"unknown"!==i?i:void 0,fullHash:"unknown"!==a?a:void 0,permanentDeviceId:d,sessionHash:"unknown"!==c?c:void 0});if(u.success){const t=jsonResponse(200,{success:!0,data:u.data},r);if(u.data?.verification_id){const s=isSecureWebRequest(e)?"; Secure":"";t.headers.append("Set-Cookie",`__unshared_verification_id=${encodeURIComponent(u.data.verification_id)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=600${s}`)}return t}return jsonResponse(200,{success:!1,error:u.error??{code:"TRIGGER_FAILED",message:"Failed to send verification email"}},r)}catch(e){return s.onError&&s.onError(e,{operation:"verifyTrigger"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Failed to trigger verification"}},r)}}async function handleVerifyWeb(e,t,s,r){try{const n=t?.code,o=parseCookieFromRequest(e,"__unshared_verification_id");if(!o||!n)return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Verification ID and code are required"}},r);const i=await s.client.verifyEmail({verification_id:o,code:n});if(i.success&&i.data?.verified){let t,n=!1;try{t=s.resolveUserId(e)}catch{}const o=resolveEmail(e,s.resolveEmailAddress),a=extractDeviceIdFromRequest(e,s.resolveDeviceId),d=parseCookieFromRequest(e,"__unshared_stable_hash")||parseCookieFromRequest(e,"__unshared_fp_id")||void 0,c=parseCookieFromRequest(e,"__unshared_full_hash")||parseCookieFromRequest(e,"__unshared_fingerprint_id")||void 0,u=extractPermanentDeviceIdFromRequest(e),p=extractSessionIdFromRequest(e,s.resolveSessionId);if(t&&o){const e={...a&&"unknown"!==a?{device_id:a}:{},...d&&"unknown"!==d?{stable_hash:d}:{},...c&&"unknown"!==c?{full_hash:c}:{},...u?{unshared_device_id:u}:{},...p&&"unknown"!==p?{session_hash:p}:{}},r=await s.client.checkUser({user_id:t,email_address:o,identifiers:e},{maxRetries:0});r.success&&r.data&&!r.failedOpen&&!r.data.is_user_flagged&&(s.verdictCache.update(t,{isVerified:!0}),n=!0)}const l=jsonResponse(200,{success:!0,data:i.data},r);n&&l.headers.set("X-Unshared-Enforcement-Reconciled","clean");const m=isSecureWebRequest(e)?"; Secure":"";return l.headers.append("Set-Cookie",`__unshared_verification_id=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0${m}`),l}return i.success?jsonResponse(200,{success:!0,data:i.data},r):jsonResponse(200,{success:!1,error:i.error??{code:"VERIFICATION_FAILED",message:"Verification failed"}},r)}catch(e){return s.onError&&s.onError(e,{operation:"verify"}),jsonResponse(200,{success:!1,error:{code:"INTERNAL_ERROR",message:"Verification failed"}},r)}}export const __test_fetchAndCacheVerdict=fetchAndCacheVerdict;export const __test_injectIntoHtmlResponse=injectIntoHtmlResponse;export function __test_resetInjectWarning(){defaultInjectWarnState.emitted=!1}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { UnsharedClient } from '../client';
|
|
2
|
-
import type { WebSubmitOptions } from './types';
|
|
1
|
+
import type { UnsharedClient } from '../client.mjs';
|
|
2
|
+
import type { WebSubmitOptions } from './types.mjs';
|
|
3
3
|
/**
|
|
4
4
|
* Web Standard equivalent of `createUnsharedMiddleware` from src/middleware.ts.
|
|
5
5
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{jsonResponse,emptyResponse,extractClientIpFromRequest}from"./web-helpers";export function createWebSubmitHandler(e,
|
|
1
|
+
import{jsonResponse,emptyResponse,extractClientIpFromRequest,readJsonRequest}from"./web-helpers.mjs";export function createWebSubmitHandler(e,t){const{userIdExtractor:s,eventTypeExtractor:r,sessionIdExtractor:o,ipAddressExtractor:n,defaultEventType:c="browser_event",routePrefix:i="/unshared",corsOrigins:a}=t??{},p=`${i}/submit-fingerprint-event`,d=a?Array.isArray(a)?a:[a]:null;return async t=>{let i;try{i=new URL(t.url).pathname}catch{return jsonResponse(400,{success:!1,error:{code:"INVALID_URL",message:"Unable to parse request URL"}})}const a=d&&i===p?function(e){if(!d)return{};const t=e.headers.get("origin")??"",s=d.includes("*");return s||d.includes(t)?{"Access-Control-Allow-Origin":s?"*":t,"Access-Control-Allow-Methods":"POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, X-Idempotency-Key, X-Session-Id, X-Permanent-Device-Id"}:{}}(t):{};if("OPTIONS"===t.method&&i===p)return emptyResponse(204,a);if("POST"!==t.method||i!==p)return jsonResponse(404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}},a);try{const i=await readJsonRequest(t);if(!i.success)return jsonResponse(i.status,{success:!1,error:i.error},a);const p=i.data;if(!p||"object"!=typeof p||Array.isArray(p))return jsonResponse(400,{success:!1,error:{code:"VALIDATION_ERROR",message:"Request body must be a JSON object"}},a);const d=p.context&&"object"==typeof p.context?p.context:{},y=d.identity&&"object"==typeof d.identity?d.identity:{},u=d.identifiers&&"object"==typeof d.identifiers?d.identifiers:{},f=d.event&&"object"==typeof d.event?d.event:{};let R,j,b,l;try{R=(s?s(t):void 0)??y.user_id}catch{R=y.user_id}try{j=(r?r(t):void 0)??f.type??c}catch{j=f.type??c}try{b=(o?o(t):void 0)??t.headers.get("x-session-id")??u.session_hash}catch{b=t.headers.get("x-session-id")??u.session_hash}const m=extractClientIpFromRequest(t);try{l=(n?n(t):void 0)??m}catch{l=m}const A=t.headers.get("x-permanent-device-id")??u.unshared_device_id,O=d.sdk&&"object"==typeof d.sdk?d.sdk.name:void 0,g="string"==typeof O&&O.startsWith("unshared-")&&p.fingerprint&&"object"==typeof p.fingerprint&&p.context&&"object"==typeof p.context&&!Array.isArray(p.context)&&p.context.identity&&"object"==typeof p.context.identity&&!Array.isArray(p.context.identity)&&p.context.identifiers&&"object"==typeof p.context.identifiers&&!Array.isArray(p.context.identifiers)&&p.context.event&&"object"==typeof p.context.event&&!Array.isArray(p.context.event),v=!Object.prototype.hasOwnProperty.call(p,"context")&&"string"==typeof p.hash&&"string"==typeof p.stable_hash&&"string"==typeof p.collected_at&&p.components&&"object"==typeof p.components&&!Array.isArray(p.components),x=g||v?{...p,context:{...d,identity:{...y,...R?{user_id:R}:{}},identifiers:{...u,...b?{session_hash:b}:{},...A?{unshared_device_id:A}:{}},event:{...f,type:j,...l?{ip_address:l}:{},user_agent:t.headers.get("user-agent")??""}}}:p,I=await e.submitFingerprint(x,{idempotencyKey:t.headers.get("x-idempotency-key")??void 0});return I.success?jsonResponse(202,{success:!0,data:I.data},a):jsonResponse(200,{success:!1,error:I.error??{code:"UPSTREAM_ERROR",message:"Upstream request failed"}},a)}catch(e){return jsonResponse(200,{success:!1,error:{code:"MIDDLEWARE_ERROR",message:e instanceof Error?e.message:"Handler error"}},a)}}}
|
package/dist/esm/web/types.d.mts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* The DOM lib reference above scopes the Fetch API globals to this file only,
|
|
9
9
|
* avoiding `window`/`document` pollution in the Node.js middleware code.
|
|
10
10
|
*/
|
|
11
|
-
import type { Verdict } from '../middleware/verdict-cache';
|
|
11
|
+
import type { Verdict } from '../middleware/verdict-cache.mjs';
|
|
12
12
|
/** A downstream handler that returns a Response. */
|
|
13
13
|
export type WebHandler = (request: Request) => Response | Promise<Response>;
|
|
14
14
|
/** Middleware that wraps a downstream handler, optionally intercepting the response. */
|
|
@@ -69,13 +69,15 @@ export interface WebProtectionConfig {
|
|
|
69
69
|
includePathPrefix?: string[];
|
|
70
70
|
/** Skip the bot/crawler UA filter. Set to true in test environments so automated browsers (Playwright, Puppeteer, etc.) can observe verdicts. @default false */
|
|
71
71
|
disableBotFilter?: boolean;
|
|
72
|
+
/** Enable diagnostic logging in the injected fingerprint script. @default false */
|
|
73
|
+
debug?: boolean;
|
|
72
74
|
/** Hard timeout (ms) for checkUser on cache miss. Fails open on timeout. @default 1500 */
|
|
73
75
|
checkUserTimeoutMs?: number;
|
|
74
76
|
/** Resolves a custom session ID. Falls back to __unshared_sid cookie. */
|
|
75
77
|
sessionId?: (req: Request) => string | undefined;
|
|
76
78
|
/**
|
|
77
79
|
* Resolves a device ID from the request.
|
|
78
|
-
* Falls back to X-Device-Id header
|
|
80
|
+
* Falls back to X-Device-Id header, then the canonical or legacy stable-hash cookie.
|
|
79
81
|
*/
|
|
80
82
|
deviceId?: (req: Request) => string | undefined;
|
|
81
83
|
/**
|
|
@@ -91,7 +93,7 @@ export interface WebProtectionConfig {
|
|
|
91
93
|
* createWebProtectionMiddleware(client, { ..., fingerprintSdkBundle });
|
|
92
94
|
* ```
|
|
93
95
|
*
|
|
94
|
-
* If omitted, the fp.js route returns
|
|
96
|
+
* If omitted, the fp.js route returns a noncacheable 503. The inline script degrades
|
|
95
97
|
* gracefully — the cached-fingerprint path still works.
|
|
96
98
|
*/
|
|
97
99
|
fingerprintSdkBundle?: string;
|
|
@@ -18,7 +18,8 @@ export declare function parseCookieFromRequest(request: Request, name: string):
|
|
|
18
18
|
*/
|
|
19
19
|
export declare function extractClientIpFromRequest(request: Request): string;
|
|
20
20
|
/**
|
|
21
|
-
* Resolves device ID from
|
|
21
|
+
* Resolves device ID from the custom resolver, X-Device-Id header, then the
|
|
22
|
+
* canonical __unshared_stable_hash cookie or legacy __unshared_fp_id cookie.
|
|
22
23
|
* Mirrors `extractDeviceIdOrUndefined` in src/middleware/utils/device-id.ts.
|
|
23
24
|
*/
|
|
24
25
|
export declare function extractDeviceIdFromRequest(request: Request, resolveDeviceId?: (req: Request) => string | undefined): string | undefined;
|
|
@@ -32,6 +33,17 @@ export declare function extractDeviceIdFromRequestOrUnknown(request: Request, re
|
|
|
32
33
|
* 2. The URL protocol (for direct HTTPS requests)
|
|
33
34
|
*/
|
|
34
35
|
export declare function isSecureWebRequest(request: Request): boolean;
|
|
36
|
+
export declare function readJsonRequest(request: Request, maxBytes?: number): Promise<{
|
|
37
|
+
success: true;
|
|
38
|
+
data: unknown;
|
|
39
|
+
} | {
|
|
40
|
+
success: false;
|
|
41
|
+
status: 400 | 413;
|
|
42
|
+
error: {
|
|
43
|
+
code: string;
|
|
44
|
+
message: string;
|
|
45
|
+
};
|
|
46
|
+
}>;
|
|
35
47
|
/**
|
|
36
48
|
* Build a JSON response. Mirrors `sendJson(res, status, data)` in
|
|
37
49
|
* src/middleware/utils/http-helpers.ts, but returns a new Response instead
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function parseCookieFromRequest(e,t){const
|
|
1
|
+
export function parseCookieFromRequest(e,t){const o=e.headers.get("cookie");if(!o)return;const r=o.match(new RegExp(`(?:^|; )${t}=([^;]*)`));if(r)try{return decodeURIComponent(r[1])}catch{return r[1]}}export function extractClientIpFromRequest(e){const t=e.headers.get("cf-connecting-ip");if(t)return t;const o=e.headers.get("x-real-ip");if(o)return o;const r=e.headers.get("x-forwarded-for");if(r){const e=r.split(",")[0]?.trim();if(e)return e}return""}export function extractDeviceIdFromRequest(e,t){if(t)try{const o=t(e);if(o)return o}catch{}const o=e.headers.get("x-device-id");if(o)return o;return parseCookieFromRequest(e,"__unshared_stable_hash")||parseCookieFromRequest(e,"__unshared_fp_id")||void 0}export function extractDeviceIdFromRequestOrUnknown(e,t){return extractDeviceIdFromRequest(e,t)??"unknown"}export function isSecureWebRequest(e){if("https"===e.headers.get("x-forwarded-proto"))return!0;try{return"https:"===new URL(e.url).protocol}catch{return!1}}export async function readJsonRequest(e,t=1<<20){const o=Number(e.headers.get("content-length"));if(Number.isFinite(o)&&o>t)return{success:!1,status:413,error:{code:"REQUEST_TOO_LARGE",message:"Request body exceeds 1 MiB"}};try{if(!e.body)throw new Error("Missing body");const o=e.body.getReader(),r=[];let n=0;for(;;){const{done:e,value:s}=await o.read();if(e)break;if(n+=s.byteLength,n>t)return o.cancel().catch(()=>{}),{success:!1,status:413,error:{code:"REQUEST_TOO_LARGE",message:"Request body exceeds 1 MiB"}};r.push(s)}const s=new Uint8Array(n);let c=0;for(const e of r)s.set(e,c),c+=e.byteLength;return{success:!0,data:JSON.parse((new TextDecoder).decode(s))}}catch{return{success:!1,status:400,error:{code:"INVALID_JSON",message:"Request body is not valid JSON."}}}}export function jsonResponse(e,t,o){const r=new Headers(o);return r.set("Content-Type","application/json"),new Response(JSON.stringify(t),{status:e,headers:r})}export function emptyResponse(e,t){return new Response(null,{status:e,headers:new Headers(t)})}export function bodyResponse(e,t,o){return new Response(t,{status:e,headers:new Headers(o)})}export function appendCookieToHeaders(e,t){e.append("Set-Cookie",t)}export function mergeResponseHeaders(e,t,o){const r=new Headers;e.forEach((e,t)=>{"set-cookie"!==t.toLowerCase()&&r.set(t,e)});const n="function"==typeof e.getSetCookie?e.getSetCookie():[];for(const e of n)r.append("Set-Cookie",e);if(o)for(const e of o)r.append("Set-Cookie",e);if(t)for(const[e,o]of Object.entries(t))r.set(e,o);return r}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { UnsharedClient, ProcessUserEventParams } from '../client';
|
|
2
2
|
import type { VerdictCache } from './verdict-cache';
|
|
3
3
|
import type { RateLimitBackoff } from './rate-limit-backoff';
|
|
4
|
+
import type { CheckUserRequest } from '../shared-types';
|
|
4
5
|
/** Single policy point for background processUserEvent budgets. */
|
|
5
6
|
export declare const DISPATCH_TIMEOUT_MS = 2000;
|
|
6
7
|
export declare const SUBMIT_FP_TIMEOUT_MS = 3000;
|
|
@@ -14,9 +15,14 @@ export interface DispatchIo {
|
|
|
14
15
|
emailAddress: string;
|
|
15
16
|
}) => void;
|
|
16
17
|
}
|
|
18
|
+
/** Warm the verdict cache after fingerprint ingestion without creating a duplicate event. */
|
|
19
|
+
export declare function runCheckUser(io: DispatchIo, request: CheckUserRequest, userId: string, emailAddress: string, timeoutMs: number): Promise<void>;
|
|
17
20
|
/**
|
|
18
21
|
* Run processUserEvent with the standard bounded budget and backoff wiring.
|
|
19
|
-
* Never rejects.
|
|
22
|
+
* Never rejects. Both ordinary dispatch sites (classic/web)
|
|
20
23
|
* MUST go through this helper so the cap and backoff can't drift per-site.
|
|
21
24
|
*/
|
|
22
|
-
export declare function runProcessUserEvent(io: DispatchIo, params: ProcessUserEventParams,
|
|
25
|
+
export declare function runProcessUserEvent(io: DispatchIo, params: Omit<ProcessUserEventParams, 'deviceId'> & {
|
|
26
|
+
deviceId?: string;
|
|
27
|
+
fullHash?: string;
|
|
28
|
+
}, timeoutMs: number): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";async function
|
|
1
|
+
"use strict";async function runCheckUser(e,s,t,r,i){try{const r=await e.client.checkUser(s,{timeoutMs:i,maxRetries:0});if(r.success&&r.data&&!r.failedOpen)return e.cache.update(t,{isFlagged:r.data.is_user_flagged}),void e.backoff.noteSuccess();if(r.failedOpen)return void e.backoff.noteFailure();r.error?.retryAfter?e.backoff.pause(1e3*r.error.retryAfter):e.backoff.noteFailure()}catch(s){e.backoff.noteFailure();try{e.onError&&e.onError(s,{operation:"processUserEvent",userId:t,emailAddress:r})}catch{}}}async function runProcessUserEvent(e,s,t){try{const r=Object.fromEntries(Object.entries({device_id:s.deviceId,stable_hash:s.fingerprintId,full_hash:s.fullHash,unshared_device_id:s.permanentDeviceId,session_hash:s.sessionHash}).filter(e=>!!e[1]&&"unknown"!==e[1])),i=await e.client.processUserEvent({event_type:s.eventType,identity:{user_id:s.userId,email_address:s.emailAddress},identifiers:r,context:{ip_address:s.ipAddress,user_agent:s.userAgent}},{timeoutMs:t,maxRetries:0});if(i.failedOpen)return void e.backoff.noteFailure();i.success?await runCheckUser(e,{user_id:s.userId,email_address:s.emailAddress,identifiers:r},s.userId,s.emailAddress,t):i.error?.retryAfter?e.backoff.pause(1e3*i.error.retryAfter):e.backoff.noteFailure()}catch(t){e.backoff.noteFailure();try{e.onError&&e.onError(t,{operation:"processUserEvent",userId:s.userId,emailAddress:s.emailAddress})}catch{}}}Object.defineProperty(exports,"t",{value:!0}),exports.SUBMIT_FP_TIMEOUT_MS=exports.DISPATCH_TIMEOUT_MS=void 0,exports.runCheckUser=runCheckUser,exports.runProcessUserEvent=runProcessUserEvent,exports.DISPATCH_TIMEOUT_MS=2e3,exports.SUBMIT_FP_TIMEOUT_MS=3e3;
|
|
@@ -38,6 +38,10 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
|
|
|
38
38
|
includePathPrefix?: string[];
|
|
39
39
|
/** Skip the bot/crawler UA filter. Set to true in test environments so automated browsers (Playwright, Puppeteer, etc.) can observe verdicts. @default false */
|
|
40
40
|
disableBotFilter?: boolean;
|
|
41
|
+
/** Enable diagnostic logging in the injected fingerprint script. @default false */
|
|
42
|
+
debug?: boolean;
|
|
43
|
+
/** @internal Serve only the bundled fingerprint library, without S3 fetches. @default false */
|
|
44
|
+
disableS3Fingerprint?: boolean;
|
|
41
45
|
/** Hard timeout (ms) for checkUser on cache miss. Fails open on timeout. @default 1500 */
|
|
42
46
|
checkUserTimeoutMs?: number;
|
|
43
47
|
/**
|
|
@@ -56,7 +60,7 @@ export interface ProtectionConfig<TReq extends UnsharedRequest = UnsharedRequest
|
|
|
56
60
|
sessionId?: (req: TReq) => string | undefined;
|
|
57
61
|
/**
|
|
58
62
|
* Resolves a device ID from the request.
|
|
59
|
-
* Falls back to X-Device-Id header
|
|
63
|
+
* Falls back to X-Device-Id header, then the canonical or legacy stable-hash cookie.
|
|
60
64
|
*/
|
|
61
65
|
deviceId?: (req: TReq) => string | undefined;
|
|
62
66
|
/**
|
package/dist/middleware/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"t",{value:!0}),exports.ACCOUNT_FLAGGED_ERROR=exports.flaggedResponse=exports.VerdictCache=void 0,exports.unsharedBoundToUser=unsharedBoundToUser;const fs_1=require("fs"),verdict_cache_1=require("./verdict-cache");Object.defineProperty(exports,"VerdictCache",{enumerable:!0,get:function(){return verdict_cache_1.VerdictCache}});const rate_limit_backoff_1=require("./rate-limit-backoff"),dispatch_dedupe_1=require("./dispatch-dedupe"),response_interceptor_1=require("./response-interceptor"),fingerprint_script_1=require("./injection/fingerprint-script"),fp_bundle_source_1=require("./injection/fp-bundle-source"),submit_fp_1=require("./routes/submit-fp"),verify_1=require("./routes/verify"),interstitial_1=require("./routes/interstitial"),gate_page_1=require("./injection/gate-page"),dispatch_processing_1=require("./dispatch-processing"),http_helpers_1=require("./utils/http-helpers"),content_type_1=require("./utils/content-type"),flagged_response_1=require("./utils/flagged-response"),mask_email_1=require("./utils/mask-email"),skip_paths_1=require("./utils/skip-paths"),include_path_1=require("./utils/include-path"),is_bot_1=require("./utils/is-bot"),client_ip_1=require("./utils/client-ip"),cookies_1=require("./utils/cookies"),device_id_1=require("./utils/device-id"),secure_1=require("./utils/secure"),sentinel_user_id_1=require("./utils/sentinel-user-id");var flagged_response_2=require("./utils/flagged-response");Object.defineProperty(exports,"flaggedResponse",{enumerable:!0,get:function(){return flagged_response_2.flaggedResponse}}),Object.defineProperty(exports,"ACCOUNT_FLAGGED_ERROR",{enumerable:!0,get:function(){return flagged_response_2.ACCOUNT_FLAGGED_ERROR}});const CHECK_USER_TIMEOUT_MS=1500,FAIL_OPEN_TTL_MS=5e3,_lastFailOpenWarn={};function warnFailOpenThrottled(e){const t=Date.now();t-(_lastFailOpenWarn[e.reason]??0)<6e4||(_lastFailOpenWarn[e.reason]=t,console.warn(`[Unshared] Failing open (reason=${e.reason}${null!=e.status?`, status=${e.status}`:""}). Detection is not enforced for this request. Configure onFailOpen to observe this directly.`))}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:s="/__unshared",corsOrigins:n,cacheTTL:o=6e4,maxCacheSize:a,streamingIdleMs:d=50,skipPaths:c,includePathPrefix:l,disableBotFilter:u=!1,checkUserTimeoutMs:_=CHECK_USER_TIMEOUT_MS,cacheMissStrategy:p="block",sessionId:h,deviceId:f,onFlagged:g,onError:v,onFailOpen:m,blockFlagged:I=!1,flaggedMode:k,autoInterstitial:y=!1,interstitialFlowType:A="email_verification"}=t,C=k??(I?"gate":void 0),S=y||"overlay"===C,x=e=>{if(m)try{m(e)}catch{}else"async_miss"!==e.reason&&warnFailOpenThrottled(e)},T=new verdict_cache_1.VerdictCache(o,a),b=new rate_limit_backoff_1.RateLimitBackoff,E=new Map,w=new dispatch_dedupe_1.DispatchDedupe,O=Date.now().toString(36),U=(0,fingerprint_script_1.generateFingerprintScript)(s,O,{autoInterstitial:S,interstitialFlowType:A});let F="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");F=(0,fs_1.readFileSync)(e,"utf8")}catch{}const q=(0,fp_bundle_source_1.createFingerprintBundleSource)(),M=void 0!==C||S,P="gate"===C?"flaggedMode 'gate' (blockFlagged)":"overlay"===C?"flaggedMode 'overlay'":"autoInterstitial";if(M&&!F)throw new Error(`[Unshared] ${P} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(M&&"/__unshared"!==s)throw new Error(`[Unshared] ${P} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const $="gate"===C?(0,gate_page_1.generateGatePage)(s,O):"",j=(0,submit_fp_1.handleSubmitFingerprint)({client:e,verdictCache:T,rateLimitBackoff:b,dispatchDedupe:w,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:h,resolveDeviceId:f,disableBotFilter:u,onError:v}),D=(0,verify_1.handleVerifyTrigger)({client:e,verdictCache:T,resolveEmailAddress:i,resolveDeviceId:f,onError:v}),V=(0,verify_1.handleVerify)({client:e,verdictCache:T,resolveUserId:r,resolveEmailAddress:i,resolveDeviceId:f,onError:v}),N=(0,interstitial_1.handleGetInterstitialFlow)({client:e}),L=n?Array.isArray(n)?n:[n]:null,R=`${s}/fp.js`,G=`${s}/fingerprint.js`,H=`${s}/submit-fp`,B=`${s}/verify-trigger`,K=`${s}/verify`,X=`${s}/status`,W=`${s}/interstitial-flow`,z=function(t,n,o){const a=(0,http_helpers_1.getRequestPath)(t.url),m=t.url||a;if(a.startsWith(s+"/")){if(function(e,t){if(!L)return;const r=e.headers.origin??"";L.includes("*")?(t.setHeader("Access-Control-Allow-Origin","*"),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id")):L.includes(r)&&(t.setHeader("Access-Control-Allow-Origin",r),t.setHeader("Vary","Origin"),t.setHeader("Access-Control-Allow-Methods","GET, 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,n),"OPTIONS"===t.method)return void(0,http_helpers_1.sendEmpty)(n,204);if(("GET"===t.method||"HEAD"===t.method)&&a===R)return n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBodyForMethod)(n,t.method,200,F);if(("GET"===t.method||"HEAD"===t.method)&&a===G){const e=q.get();return e?(n.setHeader("Content-Type","application/javascript"),n.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBodyForMethod)(n,t.method,200,e)):void(0,http_helpers_1.sendBodyForMethod)(n,t.method,503,"/* fingerprint agent unavailable */")}if("POST"===t.method&&(a===H||a===B||a===K))return void 0===t.body?void(0,http_helpers_1.sendJson)(n,400,{success:!1,error:{code:"BODY_PARSER_MISSING",message:"req.body is undefined. Mount a JSON body-parsing middleware (e.g., express.json()) before the Unshared middleware."}}):a===H?void j(t,n):a===B?void D(t,n):void V(t,n);if("GET"===t.method&&a===W)return void N(t,n);if("GET"===t.method&&a===X){let s;try{s=r(t)}catch{}if(!s)return void(0,http_helpers_1.sendJson)(n,200,{status:"anonymous"});const o=resolveEmail(t,i);return void(async()=>{let r=T.get(s);if((!r||T.isStale(s))&&o&&!b.isPaused()&&!T.isRefreshing(s)){T.markRefreshing(s);try{const i=(0,device_id_1.extractDeviceIdOrUndefined)(t,f),n=extractFingerprintId(t),a=extractSessionId(t,h),d=i??n??"unknown";await fetchAndCacheVerdict(e,T,s,o,d,n,a,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:s,emailAddress:o})),r=T.get(s)}catch(e){v&&v(e,{operation:"checkUser",userId:s,emailAddress:o}),x({operation:"checkUser",reason:"exception",userId:s,emailAddress:o})}finally{T.clearRefreshing(s)}}r&&r.isFlagged&&!r.isVerified&&(void 0!==C||g)&&o?(0,http_helpers_1.sendJson)(n,200,{status:"flagged",email:(0,mask_email_1.maskEmail)(o)}):(0,http_helpers_1.sendJson)(n,200,{status:"ok"})})()}return void(0,http_helpers_1.sendJson)(n,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}const I=void 0!==C;if((0,skip_paths_1.shouldSkipPath)(a,c,I))return void o();if(!(0,include_path_1.shouldIncludePath)(a,l))return interceptForInjection(t,n,U,d),void o();let k;try{k=r(t)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(k)){const e=(0,cookies_1.parseCookie)(t,"__unshared_uid"),r=(0,cookies_1.parseCookie)(t,"__unshared_uid_at"),i=r?Number(r):NaN,s=Number.isFinite(i)&&Date.now()-i<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;k=e&&s?e:void 0}if(!k){const e=(0,secure_1.isSecureRequest)(t)?"; Secure":"";return appendSetCookie(n,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(n,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(t,n,U,d),void o()}const y=resolveEmail(t,i);if(setUserIdCookie(t,n,k),y&&setEmailCookie(t,n,y),!y)return interceptForInjection(t,n,U,d),void o();const A=extractSessionId(t,h),S=(0,device_id_1.extractDeviceIdOrUndefined)(t,f),O=extractFingerprintId(t),M=t.headers["user-agent"]??"",P=(0,client_ip_1.extractClientIp)(t),z=S??O;if(!u&&(0,is_bot_1.isBot)(M))return void o();const J=T.get(k);if(!J){if("async"===p)return T.isRefreshing(k)||(T.markRefreshing(k),fetchAndCacheVerdict(e,T,k,y,z??"unknown",O,A,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:k,emailAddress:y})).catch(()=>x({operation:"checkUser",reason:"exception",userId:k,emailAddress:y})).finally(()=>T.clearRefreshing(k))),x({operation:"checkUser",reason:"async_miss",userId:k,emailAddress:y}),Y(),interceptForInjection(t,n,U,d),void o();let r=E.get(k);return r||(r=fetchAndCacheVerdict(e,T,k,y,z??"unknown",O,A,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:k,emailAddress:y})).finally(()=>E.delete(k)),E.set(k,r)),void r.then(e=>{e.isFlagged||Y(),applyVerdict(e,k,y,t,n,o,U,g,C,$,d)}).catch(()=>{x({operation:"checkUser",reason:"exception",userId:k,emailAddress:y}),Y(),interceptForInjection(t,n,U,d),o()})}function Y(){"unknown"!==A&&z&&(b.isPaused()||dispatchUserEvent(e,T,b,w,{userId:k,emailAddress:y,sessionId:A,deviceId:z,fingerprintId:O,userAgent:M,ipAddress:P,eventType:m},v))}T.isStale(k)&&!T.isRefreshing(k)&&(T.markRefreshing(k),fetchAndCacheVerdict(e,T,k,y,z??"unknown",O,A,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:k,emailAddress:y})).catch(()=>x({operation:"checkUser",reason:"exception",userId:k,emailAddress:y})).finally(()=>T.clearRefreshing(k))),J.isFlagged||Y(),applyVerdict(J,k,y,t,n,o,U,g,C,$,d)};return z.destroy=()=>{T.destroy(),q.stop()},z}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=(0,cookies_1.parseCookie)(e,"__unshared_email");if(r)return r;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,t,r,i,s,n,o,a,d,c,l){const u=e.isFlagged&&!e.isVerified;if("gate"===d&&u)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(s.statusCode=200,s.setHeader("Content-Type","text/html; charset=utf-8"),s.setHeader("Cache-Control","no-store"),s.end(c)):(0,http_helpers_1.sendJson)(s,403,(0,flagged_response_1.flaggedResponse)(r));else if("overlay"===d&&u)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(interceptForInjection(i,s,o,l),n()):(0,http_helpers_1.sendJson)(s,403,(0,flagged_response_1.flaggedResponse)(r));else if(interceptForInjection(i,s,o,l),e.isFlagged&&!e.isVerified&&a)try{a({userId:t,emailAddress:r,verdict:e,req:i,res:s,next:n})}catch{n()}else n()}function interceptForInjection(e,t,r,i){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],(0,response_interceptor_1.interceptResponse)(t,(e,t)=>{if(!(0,content_type_1.isHtmlContentType)(t))return null;const i=e.toString("utf8"),s=i.lastIndexOf("</body>");return-1===s?i+r:i.slice(0,s)+r+i.slice(s)},{preventCaching:!0,streamingIdleMs:i})}function dispatchUserEvent(e,t,r,i,s,n){i.mark(s.userId,s.eventType),(0,dispatch_processing_1.runProcessUserEvent)({client:e,cache:t,backoff:r,onError:n},{eventType:s.eventType,userId:s.userId,emailAddress:s.emailAddress,ipAddress:s.ipAddress,deviceId:s.deviceId,fingerprintId:s.fingerprintId,sessionHash:s.sessionId,userAgent:s.userAgent},dispatch_processing_1.DISPATCH_TIMEOUT_MS)}async function fetchAndCacheVerdict(e,t,r,i,s,n,o,a=CHECK_USER_TIMEOUT_MS,d){const c={timeoutMs:a,maxRetries:0},l=s&&"unknown"!==s?s:void 0,u=l??n,_=n??l;let p;u&&(c.deviceId=u),_&&(c.fingerprintId=_);const h=await Promise.race([e.checkUser(i,c),new Promise(e=>{p=setTimeout(()=>e(null),a)})]);return clearTimeout(p),h?h.failedOpen?(d?.(h.failedOpen.reason??"http_error",h.failedOpen.status),cacheFailOpenVerdict(t,r,i,o)):(t.set(r,{isFlagged:h.data?.is_user_flagged??!1,isVerified:!1,emailAddress:i,sessionId:o}),t.get(r)):(d?.("timeout"),cacheFailOpenVerdict(t,r,i,o))}function cacheFailOpenVerdict(e,t,r,i){const s=e.get(t);return e.set(t,{isFlagged:s?.isFlagged??!1,isVerified:s?.isVerified??!1,emailAddress:r,sessionId:i},5e3),e.get(t)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return(0,cookies_1.parseCookie)(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return(0,cookies_1.parseCookie)(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax${i}`)}function setEmailCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax${i}`)}
|
|
1
|
+
"use strict";Object.defineProperty(exports,"t",{value:!0}),exports.ACCOUNT_FLAGGED_ERROR=exports.flaggedResponse=exports.VerdictCache=void 0,exports.unsharedBoundToUser=unsharedBoundToUser;const fs_1=require("fs"),read_json_body_1=require("./utils/read-json-body"),verdict_cache_1=require("./verdict-cache");Object.defineProperty(exports,"VerdictCache",{enumerable:!0,get:function(){return verdict_cache_1.VerdictCache}});const rate_limit_backoff_1=require("./rate-limit-backoff"),dispatch_dedupe_1=require("./dispatch-dedupe"),response_interceptor_1=require("./response-interceptor"),fingerprint_script_1=require("./injection/fingerprint-script"),fp_bundle_source_1=require("./injection/fp-bundle-source"),submit_fp_1=require("./routes/submit-fp"),verify_1=require("./routes/verify"),interstitial_1=require("./routes/interstitial"),gate_page_1=require("./injection/gate-page"),dispatch_processing_1=require("./dispatch-processing"),http_helpers_1=require("./utils/http-helpers"),content_type_1=require("./utils/content-type"),flagged_response_1=require("./utils/flagged-response"),mask_email_1=require("./utils/mask-email"),skip_paths_1=require("./utils/skip-paths"),include_path_1=require("./utils/include-path"),is_bot_1=require("./utils/is-bot"),client_ip_1=require("./utils/client-ip"),cookies_1=require("./utils/cookies"),device_id_1=require("./utils/device-id"),permanent_device_id_1=require("./utils/permanent-device-id"),secure_1=require("./utils/secure"),sentinel_user_id_1=require("./utils/sentinel-user-id");var flagged_response_2=require("./utils/flagged-response");Object.defineProperty(exports,"flaggedResponse",{enumerable:!0,get:function(){return flagged_response_2.flaggedResponse}}),Object.defineProperty(exports,"ACCOUNT_FLAGGED_ERROR",{enumerable:!0,get:function(){return flagged_response_2.ACCOUNT_FLAGGED_ERROR}});const CHECK_USER_TIMEOUT_MS=1500,FAIL_OPEN_TTL_MS=5e3,_lastFailOpenWarn={};function warnFailOpenThrottled(e){const t=Date.now();t-(_lastFailOpenWarn[e.reason]??0)<6e4||(_lastFailOpenWarn[e.reason]=t,console.warn(`[Unshared] Failing open (reason=${e.reason}${null!=e.status?`, status=${e.status}`:""}). Detection is not enforced for this request. Configure onFailOpen to observe this directly.`))}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,maxCacheSize:a,streamingIdleMs:d=50,skipPaths:c,includePathPrefix:l,disableBotFilter:u=!1,checkUserTimeoutMs:_=CHECK_USER_TIMEOUT_MS,cacheMissStrategy:p="block",sessionId:h,deviceId:f,onFlagged:g,onError:v,onFailOpen:m,blockFlagged:I=!1,flaggedMode:k,autoInterstitial:C=!1,interstitialFlowType:y="email_verification"}=t,A=k??(I?"gate":void 0),S=C||"overlay"===A,x=e=>{if(m)try{m(e)}catch{}else"async_miss"!==e.reason&&warnFailOpenThrottled(e)},b=new verdict_cache_1.VerdictCache(o,a),E=new rate_limit_backoff_1.RateLimitBackoff,T=new Map,F=new dispatch_dedupe_1.DispatchDedupe,U=Date.now().toString(36),O=(0,fingerprint_script_1.generateFingerprintScript)(n,U,{autoInterstitial:S,interstitialFlowType:y,debug:t.debug,disableBotFilter:t.disableBotFilter});let w="";try{const e=require.resolve("unshared-frontend-sdk/dist/index.umd.js");w=(0,fs_1.readFileSync)(e,"utf8")}catch{}const q=(0,fp_bundle_source_1.createFingerprintBundleSource)({disabled:t.disableS3Fingerprint}),M=void 0!==A||S,P="gate"===A?"flaggedMode 'gate' (blockFlagged)":"overlay"===A?"flaggedMode 'overlay'":"autoInterstitial";if(M&&!w)throw new Error(`[Unshared] ${P} requires unshared-frontend-sdk to be installed (its UMD bundle renders the interstitial).`);if(M&&"/__unshared"!==n)throw new Error(`[Unshared] ${P} requires the default routePrefix ("/__unshared"); the browser SDK proxy routes are fixed to that prefix.`);const j="gate"===A?(0,gate_page_1.generateGatePage)(n,U):"",$=(0,submit_fp_1.handleSubmitFingerprint)({client:e,verdictCache:b,rateLimitBackoff:E,dispatchDedupe:F,resolveUserId:r,resolveEmailAddress:i,resolveSessionId:h,resolveDeviceId:f,disableBotFilter:u,onError:v}),D=(0,verify_1.handleVerifyTrigger)({client:e,verdictCache:b,resolveEmailAddress:i,resolveDeviceId:f,resolveSessionId:h,onError:v}),H=(0,verify_1.handleVerify)({client:e,verdictCache:b,resolveUserId:r,resolveEmailAddress:i,resolveDeviceId:f,resolveSessionId:h,onError:v}),V=(0,interstitial_1.handleGetInterstitialFlow)({client:e}),L=s?Array.isArray(s)?s:[s]:null,N=`${n}/fp.js`,R=`${n}/fingerprint.js`,X=`${n}/submit-fp`,G=`${n}/verify-trigger`,K=`${n}/verify`,B=`${n}/status`,W=`${n}/interstitial-flow`,z=function(t,s,o){const a=(0,http_helpers_1.getRequestPath)(t.url),m=t.url||a;if(a.startsWith(n+"/")){if(function(e,t){if(!L)return;const r=e.headers.origin??"";L.includes("*")?(t.setHeader("Access-Control-Allow-Origin","*"),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id, X-Permanent-Device-Id"),t.setHeader("Access-Control-Expose-Headers","X-Unshared-Enforcement-Reconciled")):L.includes(r)&&(t.setHeader("Access-Control-Allow-Origin",r),t.setHeader("Vary","Origin"),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type, X-Idempotency-Key, X-Session-Id, X-Device-Id, X-Permanent-Device-Id"),t.setHeader("Access-Control-Expose-Headers","X-Unshared-Enforcement-Reconciled"),t.setHeader("Access-Control-Allow-Credentials","true"))}(t,s),"OPTIONS"===t.method)return void(0,http_helpers_1.sendEmpty)(s,204);if(("GET"===t.method||"HEAD"===t.method)&&a===N)return s.setHeader("Content-Type","application/javascript"),w?(s.setHeader("Cache-Control","public, max-age=3600"),void(0,http_helpers_1.sendBodyForMethod)(s,t.method,200,w)):(s.setHeader("Cache-Control","no-store"),void(0,http_helpers_1.sendBodyForMethod)(s,t.method,503,"/* fingerprint SDK unavailable */"));if(("GET"===t.method||"HEAD"===t.method)&&a===R)return void q.get().then(e=>{if(!e)return s.setHeader("Cache-Control","no-store"),void(0,http_helpers_1.sendBodyForMethod)(s,t.method,503,"/* fingerprint agent unavailable */");s.setHeader("Content-Type","application/javascript"),s.setHeader("Cache-Control","public, max-age=3600"),(0,http_helpers_1.sendBodyForMethod)(s,t.method,200,e)});if("POST"===t.method&&(a===X||a===G||a===K))return void(0,read_json_body_1.readJsonBody)(t,s).then(e=>{if(e)return a===X?$(t,s):a===G?D(t,s):H(t,s)}).catch(o);if("GET"===t.method&&a===W)return void V(t,s);if("GET"===t.method&&a===B){let n;try{n=r(t)}catch{}if(!n)return void(0,http_helpers_1.sendJson)(s,200,{status:"anonymous"});const o=resolveEmail(t,i);return void(async()=>{let r=b.get(n);if((!r||b.isStale(n))&&o&&!E.isPaused()&&!b.isRefreshing(n)){b.markRefreshing(n);try{const i=(0,device_id_1.extractDeviceIdOrUndefined)(t,f),s=extractFingerprintId(t),a=extractFullHash(t),d=(0,permanent_device_id_1.extractPermanentDeviceId)(t),c=extractSessionId(t,h);await fetchAndCacheVerdict(e,b,n,o,i,s,a,d,c,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:n,emailAddress:o})),r=b.get(n)}catch(e){v&&v(e,{operation:"checkUser",userId:n,emailAddress:o}),x({operation:"checkUser",reason:"exception",userId:n,emailAddress:o})}finally{b.clearRefreshing(n)}}r&&r.isFlagged&&!r.isVerified&&(void 0!==A||g)&&o?(0,http_helpers_1.sendJson)(s,200,{status:"flagged",email:(0,mask_email_1.maskEmail)(o)}):(0,http_helpers_1.sendJson)(s,200,{status:"ok"})})()}return void(0,http_helpers_1.sendJson)(s,404,{success:!1,error:{code:"NOT_FOUND",message:"Unknown route"}})}const I=void 0!==A;if((0,skip_paths_1.shouldSkipPath)(a,c,I))return void o();let k;try{k=r(t)}catch{}if((0,sentinel_user_id_1.isSentinelUserId)(k)){const e=(0,cookies_1.parseCookie)(t,"__unshared_uid"),r=(0,cookies_1.parseCookie)(t,"__unshared_uid_at"),i=r?Number(r):NaN,n=Number.isFinite(i)&&Date.now()-i<=sentinel_user_id_1.SENTINEL_STICKINESS_TTL_MS;k=e&&n?e:void 0}if(!k){const e=(0,secure_1.isSecureRequest)(t)?"; Secure":"";return appendSetCookie(s,`__unshared_uid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_uid_at=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_sid=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_email=; Path=/; SameSite=Lax; Max-Age=0${e}`),appendSetCookie(s,`__unshared_verification_id=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0${e}`),interceptForInjection(t,s,O,d),void o()}const C=resolveEmail(t,i);if(setUserIdCookie(t,s,k),C&&setEmailCookie(t,s,C),!(0,include_path_1.shouldIncludePath)(a,l))return interceptForInjection(t,s,O,d),void o();if(!C)return interceptForInjection(t,s,O,d),void o();const y=extractSessionId(t,h),S=(0,device_id_1.extractDeviceIdOrUndefined)(t,f),U=extractFingerprintId(t),M=extractFullHash(t),P=(0,permanent_device_id_1.extractPermanentDeviceId)(t),z=t.headers["user-agent"]??"",J=(0,client_ip_1.extractClientIp)(t);if(!u&&(0,is_bot_1.isBot)(z))return void o();const Q=b.get(k);if(!Q){if("async"===p)return b.isRefreshing(k)||(b.markRefreshing(k),fetchAndCacheVerdict(e,b,k,C,S,U,M,P,y,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:k,emailAddress:C})).catch(()=>x({operation:"checkUser",reason:"exception",userId:k,emailAddress:C})).finally(()=>b.clearRefreshing(k))),x({operation:"checkUser",reason:"async_miss",userId:k,emailAddress:C}),Y(),interceptForInjection(t,s,O,d),void o();let r=T.get(k);return r||(r=fetchAndCacheVerdict(e,b,k,C,S,U,M,P,y,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:k,emailAddress:C})).finally(()=>T.delete(k)),T.set(k,r)),void r.then(e=>{e.isFlagged||Y(),applyVerdict(e,k,C,t,s,o,O,g,A,j,d)}).catch(()=>{x({operation:"checkUser",reason:"exception",userId:k,emailAddress:C}),Y(),interceptForInjection(t,s,O,d),o()})}function Y(){y&&"unknown"!==y&&(E.isPaused()||dispatchUserEvent(e,b,E,F,{userId:k,emailAddress:C,sessionId:y,deviceId:S,fingerprintId:U,fullHash:M,permanentDeviceId:P,userAgent:z,ipAddress:J,eventType:m},v))}b.isStale(k)&&!b.isRefreshing(k)&&(b.markRefreshing(k),fetchAndCacheVerdict(e,b,k,C,S,U,M,P,y,_,(e,t)=>x({operation:"checkUser",reason:e,status:t,userId:k,emailAddress:C})).catch(()=>x({operation:"checkUser",reason:"exception",userId:k,emailAddress:C})).finally(()=>b.clearRefreshing(k))),Q.isFlagged||Y(),applyVerdict(Q,k,C,t,s,o,O,g,A,j,d)};return z.destroy=()=>{b.destroy(),q.stop()},z}function resolveEmail(e,t){if(t)try{const r=t(e);if(r)return r}catch{}const r=(0,cookies_1.parseCookie)(e,"__unshared_email");if(r)return r;const i=e.body?.email;return"string"==typeof i&&i?i:void 0}function applyVerdict(e,t,r,i,n,s,o,a,d,c,l){const u=e.isFlagged&&!e.isVerified;if("gate"===d&&u)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(n.statusCode=200,n.setHeader("Content-Type","text/html; charset=utf-8"),n.setHeader("Cache-Control","no-store"),n.end(c)):(0,http_helpers_1.sendJson)(n,403,(0,flagged_response_1.flaggedResponse)(r));else if("overlay"===d&&u)(0,content_type_1.isHtmlNavigation)(i.method,i.headers.accept)?(interceptForInjection(i,n,o,l),s()):(0,http_helpers_1.sendJson)(n,403,(0,flagged_response_1.flaggedResponse)(r));else if(interceptForInjection(i,n,o,l),e.isFlagged&&!e.isVerified&&a)try{a({userId:t,emailAddress:r,verdict:e,req:i,res:n,next:s})}catch{s()}else s()}function interceptForInjection(e,t,r,i){delete e.headers["if-none-match"],delete e.headers["if-modified-since"],(0,response_interceptor_1.interceptResponse)(t,(e,t)=>{if(!(0,content_type_1.isHtmlContentType)(t))return null;const i=e.toString("utf8"),n=i.lastIndexOf("</body>");return-1===n?i+r:i.slice(0,n)+r+i.slice(n)},{preventCaching:!0,streamingIdleMs:i})}function dispatchUserEvent(e,t,r,i,n,s){i.mark(n.userId,n.eventType),(0,dispatch_processing_1.runProcessUserEvent)({client:e,cache:t,backoff:r,onError:s},{eventType:n.eventType,userId:n.userId,emailAddress:n.emailAddress,ipAddress:n.ipAddress,deviceId:n.deviceId,fingerprintId:n.fingerprintId,fullHash:n.fullHash,permanentDeviceId:n.permanentDeviceId,sessionHash:n.sessionId,userAgent:n.userAgent},dispatch_processing_1.DISPATCH_TIMEOUT_MS)}async function fetchAndCacheVerdict(e,t,r,i,n,s,o,a,d,c=CHECK_USER_TIMEOUT_MS,l){const u={timeoutMs:c,maxRetries:0};let _;n&&"unknown"!==n&&(u.deviceId=n),s&&"unknown"!==s&&(u.fingerprintId=s),o&&"unknown"!==o&&(u.fullHash=o),a&&(u.permanentDeviceId=a),d&&"unknown"!==d&&(u.sessionHash=d);const p=await Promise.race([e.checkUser(i,u),new Promise(e=>{_=setTimeout(()=>e(null),c)})]);return clearTimeout(_),p?p.failedOpen?(l?.(p.failedOpen.reason??"http_error",p.failedOpen.status),cacheFailOpenVerdict(t,r,i,d)):(t.set(r,{isFlagged:p.data?.is_user_flagged??!1,isVerified:!1,emailAddress:i,sessionId:d}),t.get(r)):(l?.("timeout"),cacheFailOpenVerdict(t,r,i,d))}function cacheFailOpenVerdict(e,t,r,i){const n=e.get(t);return e.set(t,{isFlagged:n?.isFlagged??!1,isVerified:n?.isVerified??!1,emailAddress:r,sessionId:i},5e3),e.get(t)}function extractSessionId(e,t){if(t)try{const r=t(e);if(r)return r}catch{}return(0,cookies_1.parseCookie)(e,"__unshared_sid")??"unknown"}function extractFingerprintId(e){return(0,cookies_1.parseCookie)(e,"__unshared_stable_hash")||(0,cookies_1.parseCookie)(e,"__unshared_fp_id")||void 0}function extractFullHash(e){return(0,cookies_1.parseCookie)(e,"__unshared_full_hash")||(0,cookies_1.parseCookie)(e,"__unshared_fingerprint_id")||void 0}function appendSetCookie(e,t){const r=e.getHeader("Set-Cookie");if(r){const i=Array.isArray(r)?[...r]:[String(r)];i.push(t),e.setHeader("Set-Cookie",i)}else e.setHeader("Set-Cookie",t)}function setUserIdCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_uid=${encodeURIComponent(r)}; Path=/; SameSite=Lax; Max-Age=31536000${i}`),appendSetCookie(t,`__unshared_uid_at=${Date.now()}; Path=/; SameSite=Lax; Max-Age=31536000${i}`)}function setEmailCookie(e,t,r){const i=(0,secure_1.isSecureRequest)(e)?"; Secure":"";appendSetCookie(t,`__unshared_email=${encodeURIComponent(r)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=31536000${i}`)}
|
|
@@ -12,8 +12,17 @@
|
|
|
12
12
|
* window.addEventListener("unshared:flagged", (e) => {
|
|
13
13
|
* e.detail.email — the flagged user's email (from 403 response body)
|
|
14
14
|
* });
|
|
15
|
+
*
|
|
16
|
+
* Sticky-signal contract (ordering-proof alternative to the event): the signal
|
|
17
|
+
* is also recorded on the shared namespace, so listeners that attach AFTER the
|
|
18
|
+
* CustomEvent fired (late hydration, app crash-and-remount) can recover it:
|
|
19
|
+
* window.__unshared.flagged — { email } once flagged, else undefined
|
|
20
|
+
* window.__unshared.whenFlagged(cb) — subscribes cb AND replays immediately
|
|
21
|
+
* if the signal already fired
|
|
15
22
|
*/
|
|
16
23
|
export declare function generateFingerprintScript(routePrefix: string, version?: string, opts?: {
|
|
17
24
|
autoInterstitial?: boolean;
|
|
18
25
|
interstitialFlowType?: string;
|
|
26
|
+
debug?: boolean;
|
|
27
|
+
disableBotFilter?: boolean;
|
|
19
28
|
}): string;
|