@better-auth/infra 0.1.14 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,292 @@
1
+ import { a as identify, c as dashClient, n as encodePoWSolution, o as bytesToHex, r as solvePoWChallenge, t as decodePoWChallenge } from "./pow-BUuN_EKw.mjs";
2
+ import { env } from "@better-auth/core/env";
3
+ import { Dimensions, InteractionManager, PixelRatio, Platform } from "react-native";
4
+ //#region src/sentinel/native/components.ts
5
+ /**
6
+ * Default first-party component payload for React Native / Expo.
7
+ * Avoids browser-style entropy; sets `clientRuntime` for durable-kv bot scoring.
8
+ */
9
+ async function defaultCollectNativeComponents() {
10
+ const windowDims = Dimensions.get("window");
11
+ const scale = PixelRatio.get();
12
+ let locale = "en";
13
+ let locales = ["en"];
14
+ try {
15
+ locale = Intl.DateTimeFormat().resolvedOptions().locale || locale;
16
+ locales = [locale];
17
+ } catch {}
18
+ const nav = globalThis.navigator;
19
+ if (nav?.languages?.length) locales = [...nav.languages];
20
+ let timezone = "";
21
+ let timezoneOffset = 0;
22
+ try {
23
+ timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "";
24
+ timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
25
+ } catch {
26
+ timezone = "unknown";
27
+ }
28
+ const components = {
29
+ clientRuntime: "react-native",
30
+ platform: Platform.OS,
31
+ screenResolution: `${Math.round(windowDims.width)}x${Math.round(windowDims.height)}`,
32
+ pixelRatio: scale,
33
+ fontScale: windowDims.fontScale,
34
+ language: locales[0] ?? locale,
35
+ languages: locales,
36
+ locale,
37
+ timezone,
38
+ timezoneOffset,
39
+ touchSupport: true,
40
+ maxTouchPoints: Platform.OS === "ios" ? 5 : 10
41
+ };
42
+ if (nav?.userAgent) components.userAgent = nav.userAgent;
43
+ await applyOptionalExpoMetadata(components);
44
+ return components;
45
+ }
46
+ async function applyOptionalExpoMetadata(components) {
47
+ try {
48
+ const c = (await import("expo-constants")).default;
49
+ if (c?.expoConfig?.version) components.appVersion = c.expoConfig.version;
50
+ else if (c?.nativeAppVersion) components.appVersion = c.nativeAppVersion;
51
+ if (c?.expoConfig?.slug) components.expoSlug = c.expoConfig.slug;
52
+ } catch {}
53
+ try {
54
+ const d = (await import("expo-device")).default;
55
+ if (d?.modelName) components.deviceModel = d.modelName;
56
+ if (d?.osVersion) components.osVersion = d.osVersion;
57
+ } catch {}
58
+ }
59
+ //#endregion
60
+ //#region src/sentinel/native/crypto.ts
61
+ /**
62
+ * Cryptographically secure random bytes for React Native, Node (e.g. Expo API routes),
63
+ * and other runtimes
64
+ */
65
+ async function randomBytesAsync(length) {
66
+ const c = globalThis.crypto;
67
+ if (c && typeof c.getRandomValues === "function") {
68
+ const bytes = new Uint8Array(length);
69
+ c.getRandomValues(bytes);
70
+ return bytes;
71
+ }
72
+ try {
73
+ const { getRandomBytesAsync } = await import("expo-crypto");
74
+ return await getRandomBytesAsync(length);
75
+ } catch {
76
+ throw new Error("[@better-auth/infra] No secure RNG available: use Web Crypto / Node, add `import \"react-native-get-random-values\"` at app entry (before other imports), or install `expo-crypto` with a native dev build.");
77
+ }
78
+ }
79
+ //#endregion
80
+ //#region src/sentinel/native/fingerprint.ts
81
+ async function randomVisitorId() {
82
+ return bytesToHex(await randomBytesAsync(10)).slice(0, 20);
83
+ }
84
+ async function generateRequestId() {
85
+ const h = bytesToHex(await randomBytesAsync(16));
86
+ return [
87
+ h.slice(0, 8),
88
+ h.slice(8, 12),
89
+ h.slice(12, 16),
90
+ h.slice(16, 20),
91
+ h.slice(20, 32)
92
+ ].join("-");
93
+ }
94
+ /** Conservative confidence for first-party native payloads (not web-grade signals). */
95
+ const NATIVE_IDENTIFY_CONFIDENCE = .52;
96
+ /** Logical URL recorded with native identify payloads. */
97
+ const NATIVE_IDENTIFY_CONTEXT_URL = "react-native://identify";
98
+ function createNativeFingerprintRuntime(deps) {
99
+ let cached = null;
100
+ let pending = null;
101
+ let identifySent = false;
102
+ let identifyComplete = null;
103
+ let identifyCompleteResolve = null;
104
+ async function getFingerprint() {
105
+ if (cached != null) return cached;
106
+ if (pending != null) return pending;
107
+ pending = (async () => {
108
+ try {
109
+ const visitorId = await deps.getOrCreateVisitorId();
110
+ const components = await defaultCollectNativeComponents();
111
+ const result = {
112
+ visitorId,
113
+ requestId: await generateRequestId(),
114
+ confidence: NATIVE_IDENTIFY_CONFIDENCE,
115
+ components
116
+ };
117
+ cached = result;
118
+ return result;
119
+ } catch (error) {
120
+ console.warn("[Sentinel native] Fingerprint generation failed:", error);
121
+ pending = null;
122
+ return null;
123
+ }
124
+ })();
125
+ return pending;
126
+ }
127
+ async function sendIdentify() {
128
+ if (identifySent) return;
129
+ const fingerprint = await getFingerprint();
130
+ if (!fingerprint) return;
131
+ identifySent = true;
132
+ identifyComplete = new Promise((resolve) => {
133
+ identifyCompleteResolve = resolve;
134
+ });
135
+ const payload = {
136
+ visitorId: fingerprint.visitorId,
137
+ requestId: fingerprint.requestId,
138
+ confidence: fingerprint.confidence,
139
+ components: fingerprint.components,
140
+ url: NATIVE_IDENTIFY_CONTEXT_URL,
141
+ incognito: false
142
+ };
143
+ try {
144
+ await identify(deps.identifyUrl, payload);
145
+ } catch (error) {
146
+ console.warn("[Sentinel native] Identify request failed:", error);
147
+ } finally {
148
+ identifyCompleteResolve?.();
149
+ identifyCompleteResolve = null;
150
+ }
151
+ }
152
+ async function waitForIdentify(timeoutMs = 500) {
153
+ if (!identifyComplete) return;
154
+ await Promise.race([identifyComplete, new Promise((resolve) => setTimeout(resolve, timeoutMs))]);
155
+ }
156
+ return {
157
+ getFingerprint,
158
+ sendIdentify,
159
+ waitForIdentify
160
+ };
161
+ }
162
+ const VISITOR_STORAGE_KEY = "better-auth.infra.sentinel.visitorId";
163
+ let memoryVisitorId = null;
164
+ /**
165
+ * Stable per-install visitor id. Prefer passing `storage` (e.g. secure storage).
166
+ * Otherwise tries `@react-native-async-storage/async-storage`, then falls back to an in-memory id (session only).
167
+ */
168
+ async function getOrCreateVisitorId(storage) {
169
+ if (storage) {
170
+ const existing = await storage.getItem(VISITOR_STORAGE_KEY);
171
+ if (existing) return existing;
172
+ const id = await randomVisitorId();
173
+ await storage.setItem(VISITOR_STORAGE_KEY, id);
174
+ return id;
175
+ }
176
+ try {
177
+ const { default: AsyncStorage } = await import("@react-native-async-storage/async-storage");
178
+ const existing = await AsyncStorage.getItem(VISITOR_STORAGE_KEY);
179
+ if (existing) return existing;
180
+ const id = await randomVisitorId();
181
+ await AsyncStorage.setItem(VISITOR_STORAGE_KEY, id);
182
+ return id;
183
+ } catch {
184
+ memoryVisitorId ??= await randomVisitorId();
185
+ return memoryVisitorId;
186
+ }
187
+ }
188
+ //#endregion
189
+ //#region src/sentinel/native/client.ts
190
+ const DEFAULT_IDENTIFY_URL = "https://kv.better-auth.com";
191
+ function scheduleIdentify(send) {
192
+ const run = () => {
193
+ try {
194
+ send();
195
+ } catch (e) {
196
+ console.warn("[Sentinel native] identify schedule error:", e);
197
+ }
198
+ };
199
+ try {
200
+ const im = InteractionManager;
201
+ if (im && typeof im.runAfterInteractions === "function") {
202
+ im.runAfterInteractions(run);
203
+ return;
204
+ }
205
+ } catch {}
206
+ if (typeof queueMicrotask === "function") queueMicrotask(run);
207
+ else setTimeout(run, 0);
208
+ }
209
+ const sentinelNativeClient = (options) => {
210
+ const autoSolve = options?.autoSolveChallenge !== false;
211
+ const runtime = createNativeFingerprintRuntime({
212
+ identifyUrl: options?.identifyUrl ?? env.BETTER_AUTH_KV_URL ?? DEFAULT_IDENTIFY_URL,
213
+ getOrCreateVisitorId: () => getOrCreateVisitorId(options?.storage)
214
+ });
215
+ scheduleIdentify(() => {
216
+ runtime.sendIdentify();
217
+ });
218
+ return {
219
+ id: "sentinel",
220
+ fetchPlugins: [{
221
+ id: "sentinel-fingerprint",
222
+ name: "sentinel-fingerprint",
223
+ hooks: { async onRequest(context) {
224
+ await runtime.waitForIdentify(500);
225
+ const fingerprint = await runtime.getFingerprint();
226
+ if (!fingerprint) return context;
227
+ const headers = context.headers || new Headers();
228
+ if (headers instanceof Headers) {
229
+ headers.set("X-Visitor-Id", fingerprint.visitorId);
230
+ headers.set("X-Request-Id", fingerprint.requestId);
231
+ }
232
+ return {
233
+ ...context,
234
+ headers
235
+ };
236
+ } }
237
+ }, {
238
+ id: "sentinel-pow-solver",
239
+ name: "sentinel-pow-solver",
240
+ hooks: {
241
+ async onResponse(context) {
242
+ if (context.response.status !== 423 || !autoSolve) return context;
243
+ const challengeHeader = context.response.headers.get("X-PoW-Challenge");
244
+ const reason = context.response.headers.get("X-PoW-Reason") || "";
245
+ if (!challengeHeader) return context;
246
+ options?.onChallengeReceived?.(reason);
247
+ const challenge = decodePoWChallenge(challengeHeader);
248
+ if (!challenge) return context;
249
+ try {
250
+ const startTime = Date.now();
251
+ const solution = await solvePoWChallenge(challenge);
252
+ const solveTime = Date.now() - startTime;
253
+ options?.onChallengeSolved?.(solveTime);
254
+ const originalUrl = context.response.url;
255
+ const fingerprint = await runtime.getFingerprint();
256
+ const retryHeaders = new Headers();
257
+ retryHeaders.set("X-PoW-Solution", encodePoWSolution(solution));
258
+ if (fingerprint) {
259
+ retryHeaders.set("X-Visitor-Id", fingerprint.visitorId);
260
+ retryHeaders.set("X-Request-Id", fingerprint.requestId);
261
+ }
262
+ retryHeaders.set("Content-Type", "application/json");
263
+ let body;
264
+ if (context.request?.body) body = context.request._originalBody;
265
+ const retryResponse = await fetch(originalUrl, {
266
+ method: context.request?.method || "POST",
267
+ headers: retryHeaders,
268
+ body
269
+ });
270
+ return {
271
+ ...context,
272
+ response: retryResponse
273
+ };
274
+ } catch (err) {
275
+ console.error("[Sentinel native] Failed to solve PoW challenge:", err);
276
+ options?.onChallengeFailed?.(err instanceof Error ? err : new Error(String(err)));
277
+ return context;
278
+ }
279
+ },
280
+ async onRequest(context) {
281
+ if (context.body) {
282
+ const ctx = context;
283
+ ctx._originalBody = context.body;
284
+ }
285
+ return context;
286
+ }
287
+ }
288
+ }]
289
+ };
290
+ };
291
+ //#endregion
292
+ export { dashClient, sentinelNativeClient };
@@ -0,0 +1,131 @@
1
+ import "./constants-DdWGfvz1.mjs";
2
+ //#region src/dash-client.ts
3
+ function resolveDashUserId(input, options) {
4
+ return input.userId || options?.resolveUserId?.({
5
+ userId: input.userId,
6
+ user: input.user,
7
+ session: input.session
8
+ }) || input.user?.id || input.session?.user?.id || void 0;
9
+ }
10
+ const dashClient = (options) => {
11
+ return {
12
+ id: "dash",
13
+ getActions: ($fetch) => ({ dash: { getAuditLogs: async (input = {}) => {
14
+ const userId = resolveDashUserId(input, options);
15
+ return $fetch("/events/audit-logs", {
16
+ method: "GET",
17
+ query: {
18
+ limit: input.limit,
19
+ offset: input.offset,
20
+ organizationId: input.organizationId,
21
+ identifier: input.identifier,
22
+ eventType: input.eventType,
23
+ userId
24
+ }
25
+ });
26
+ } } }),
27
+ pathMethods: { "/events/audit-logs": "GET" }
28
+ };
29
+ };
30
+ //#endregion
31
+ //#region src/crypto.ts
32
+ function randomBytes(length) {
33
+ const bytes = new Uint8Array(length);
34
+ crypto.getRandomValues(bytes);
35
+ return bytes;
36
+ }
37
+ function bytesToHex(bytes) {
38
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
39
+ }
40
+ async function hash(message) {
41
+ const msgBuffer = new TextEncoder().encode(message);
42
+ const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
43
+ return bytesToHex(new Uint8Array(hashBuffer));
44
+ }
45
+ //#endregion
46
+ //#region src/abort-signal.ts
47
+ /**
48
+ * Returns an AbortSignal that aborts after `ms`.
49
+ * Uses `AbortSignal.timeout` when present; otherwise `AbortController` + `setTimeout`
50
+ * for React Native / older runtimes where `.timeout` is missing.
51
+ */
52
+ function timeout(ms) {
53
+ if (typeof AbortSignal.timeout === "function") return AbortSignal.timeout(ms);
54
+ const controller = new AbortController();
55
+ setTimeout(() => controller.abort(), ms);
56
+ return controller.signal;
57
+ }
58
+ //#endregion
59
+ //#region src/identification-client.ts
60
+ function generateRequestId() {
61
+ const hex = bytesToHex(randomBytes(16));
62
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
63
+ }
64
+ async function identify(baseURL, payload, signal) {
65
+ const base = baseURL.replace(/\/$/, "");
66
+ await fetch(`${base}/identify`, {
67
+ method: "POST",
68
+ headers: { "Content-Type": "application/json" },
69
+ body: JSON.stringify(payload),
70
+ signal: signal ?? timeout(5e3)
71
+ });
72
+ }
73
+ //#endregion
74
+ //#region src/sentinel/pow.ts
75
+ function hasLeadingZeroBits(hash, bits) {
76
+ const fullHexChars = Math.floor(bits / 4);
77
+ const remainingBits = bits % 4;
78
+ for (let i = 0; i < fullHexChars; i++) if (hash[i] !== "0") return false;
79
+ if (remainingBits > 0 && fullHexChars < hash.length) {
80
+ if (parseInt(hash[fullHexChars], 16) > (1 << 4 - remainingBits) - 1) return false;
81
+ }
82
+ return true;
83
+ }
84
+ async function solvePoWChallenge(challenge) {
85
+ const { nonce, difficulty } = challenge;
86
+ let counter = 0;
87
+ while (true) {
88
+ if (hasLeadingZeroBits(await hash(`${nonce}:${counter}`), difficulty)) return {
89
+ nonce,
90
+ counter
91
+ };
92
+ counter++;
93
+ if (counter % 1e3 === 0) await new Promise((resolve) => setTimeout(resolve, 0));
94
+ if (counter > 1e8) throw new Error("PoW challenge took too long to solve");
95
+ }
96
+ }
97
+ function decodeBase64ToUtf8(encoded) {
98
+ if (typeof globalThis.atob === "function") return globalThis.atob(encoded);
99
+ throw new Error("[Sentinel] Base64 decode requires atob (browser, Hermes, or Bun)");
100
+ }
101
+ function encodeUtf8ToBase64(str) {
102
+ if (typeof globalThis.btoa === "function") return globalThis.btoa(str);
103
+ const bytes = new TextEncoder().encode(str);
104
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
105
+ let out = "";
106
+ for (let i = 0; i < bytes.length; i += 3) {
107
+ const b0 = bytes[i];
108
+ const b1 = bytes[i + 1] ?? 0;
109
+ const b2 = bytes[i + 2] ?? 0;
110
+ const triple = b0 << 16 | b1 << 8 | b2;
111
+ const pad = i + 2 >= bytes.length ? i + 1 >= bytes.length ? 2 : 1 : 0;
112
+ out += chars[triple >> 18 & 63];
113
+ out += chars[triple >> 12 & 63];
114
+ out += pad < 2 ? chars[triple >> 6 & 63] : "=";
115
+ out += pad < 1 ? chars[triple & 63] : "=";
116
+ }
117
+ return out;
118
+ }
119
+ function decodePoWChallenge(encoded) {
120
+ try {
121
+ const decoded = decodeBase64ToUtf8(encoded);
122
+ return JSON.parse(decoded);
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+ function encodePoWSolution(solution) {
128
+ return encodeUtf8ToBase64(JSON.stringify(solution));
129
+ }
130
+ //#endregion
131
+ export { identify as a, dashClient as c, generateRequestId as i, encodePoWSolution as n, bytesToHex as o, solvePoWChallenge as r, hash as s, decodePoWChallenge as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/infra",
3
- "version": "0.1.14",
3
+ "version": "0.2.1",
4
4
  "description": "Dashboard and analytics plugin for Better Auth",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -30,6 +30,11 @@
30
30
  "types": "./dist/email.d.mts",
31
31
  "import": "./dist/email.mjs",
32
32
  "default": "./dist/email.mjs"
33
+ },
34
+ "./native": {
35
+ "types": "./dist/native.d.mts",
36
+ "import": "./dist/native.mjs",
37
+ "default": "./dist/native.mjs"
33
38
  }
34
39
  },
35
40
  "files": [
@@ -62,8 +67,9 @@
62
67
  "@types/bun": "latest",
63
68
  "@types/node": "catalog:",
64
69
  "better-auth": "catalog:",
70
+ "expo-crypto": "^14.0.2",
65
71
  "happy-dom": "^20.8.9",
66
- "msw": "^2.12.14",
72
+ "msw": "^2.13.0",
67
73
  "tsdown": "^0.21.1",
68
74
  "typescript": "catalog:",
69
75
  "zod": "catalog:"
@@ -78,6 +84,25 @@
78
84
  "better-auth": ">=1.4.0",
79
85
  "zod": ">=4.1.12",
80
86
  "@better-auth/core": ">=1.4.0",
81
- "@better-auth/sso": ">=1.4.0"
87
+ "@better-auth/sso": ">=1.4.0",
88
+ "react-native": ">=0.74.0",
89
+ "@react-native-async-storage/async-storage": ">=1.21.0",
90
+ "expo-constants": ">=16.0.0",
91
+ "expo-crypto": ">=13.0.0",
92
+ "expo-device": ">=6.0.0"
93
+ },
94
+ "peerDependenciesMeta": {
95
+ "@react-native-async-storage/async-storage": {
96
+ "optional": true
97
+ },
98
+ "expo-constants": {
99
+ "optional": true
100
+ },
101
+ "expo-crypto": {
102
+ "optional": true
103
+ },
104
+ "expo-device": {
105
+ "optional": true
106
+ }
82
107
  }
83
108
  }
@@ -1,178 +0,0 @@
1
- import { n as INFRA_KV_URL, r as KV_TIMEOUT_MS } from "./constants-DdWGfvz1.mjs";
2
- import { logger } from "better-auth";
3
- import { createAuthMiddleware } from "better-auth/api";
4
- //#region src/crypto.ts
5
- function randomBytes(length) {
6
- const bytes = new Uint8Array(length);
7
- crypto.getRandomValues(bytes);
8
- return bytes;
9
- }
10
- function bytesToHex(bytes) {
11
- return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
12
- }
13
- async function hash(message) {
14
- const msgBuffer = new TextEncoder().encode(message);
15
- const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
16
- return bytesToHex(new Uint8Array(hashBuffer));
17
- }
18
- //#endregion
19
- //#region src/identification.ts
20
- /**
21
- * Identification Service
22
- *
23
- * Fetches identification data from the durable-kv service
24
- * when a request includes an X-Request-Id header.
25
- */
26
- const IDENTIFICATION_COOKIE_NAME = "__infra-rid";
27
- const identificationCache = /* @__PURE__ */ new Map();
28
- const CACHE_TTL_MS = 6e4;
29
- const CACHE_MAX_SIZE = 1e3;
30
- let lastCleanup = Date.now();
31
- function cleanupCache() {
32
- const now = Date.now();
33
- for (const [key, value] of identificationCache.entries()) if (now - value.timestamp > CACHE_TTL_MS) identificationCache.delete(key);
34
- lastCleanup = now;
35
- }
36
- function maybeCleanup() {
37
- if (Date.now() - lastCleanup > CACHE_TTL_MS || identificationCache.size > CACHE_MAX_SIZE) cleanupCache();
38
- }
39
- /**
40
- * Fetch identification data from durable-kv by requestId
41
- */
42
- async function getIdentification(requestId, apiKey, kvUrl) {
43
- maybeCleanup();
44
- const cached = identificationCache.get(requestId);
45
- if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) return cached.data;
46
- const baseUrl = kvUrl || INFRA_KV_URL;
47
- const maxRetries = 3;
48
- const retryDelays = [
49
- 50,
50
- 100,
51
- 200
52
- ];
53
- for (let attempt = 0; attempt <= maxRetries; attempt++) try {
54
- const response = await fetch(`${baseUrl}/identify/${requestId}`, {
55
- method: "GET",
56
- headers: { "x-api-key": apiKey },
57
- signal: AbortSignal.timeout(KV_TIMEOUT_MS)
58
- });
59
- if (response.ok) {
60
- const data = await response.json();
61
- identificationCache.set(requestId, {
62
- data,
63
- timestamp: Date.now()
64
- });
65
- return data;
66
- }
67
- if (response.status === 404 && attempt < maxRetries) {
68
- await new Promise((resolve) => setTimeout(resolve, retryDelays[attempt]));
69
- continue;
70
- }
71
- if (response.status !== 404) identificationCache.set(requestId, {
72
- data: null,
73
- timestamp: Date.now()
74
- });
75
- return null;
76
- } catch (error) {
77
- if (attempt === maxRetries) {
78
- logger.error("[Dash] Failed to fetch identification:", error);
79
- return null;
80
- }
81
- await new Promise((resolve) => setTimeout(resolve, retryDelays[attempt] || 50));
82
- }
83
- return null;
84
- }
85
- function generateRequestId() {
86
- const hex = bytesToHex(randomBytes(16));
87
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
88
- }
89
- async function identify(baseURL, payload, signal) {
90
- const base = baseURL.replace(/\/$/, "");
91
- await fetch(`${base}/identify`, {
92
- method: "POST",
93
- headers: { "Content-Type": "application/json" },
94
- body: JSON.stringify(payload),
95
- signal: signal ?? AbortSignal.timeout(5e3)
96
- });
97
- }
98
- /**
99
- * Extract identification headers from a request
100
- */
101
- function extractIdentificationHeaders(request) {
102
- if (!request) return {
103
- visitorId: null,
104
- requestId: null
105
- };
106
- return {
107
- visitorId: request.headers.get("X-Visitor-Id"),
108
- requestId: request.headers.get("X-Request-Id")
109
- };
110
- }
111
- /**
112
- * Early middleware that loads identification data
113
- */
114
- function createIdentificationMiddleware(options) {
115
- return createAuthMiddleware(async (ctx) => {
116
- const { visitorId, requestId: headerRequestId } = extractIdentificationHeaders(ctx.request);
117
- const requestId = headerRequestId ?? ctx.getCookie("__infra-rid") ?? null;
118
- ctx.context.visitorId = visitorId;
119
- ctx.context.requestId = requestId;
120
- if (requestId) ctx.context.identification = ctx.context.identification ?? await getIdentification(requestId, options.apiKey, options.kvUrl) ?? null;
121
- else ctx.context.identification = null;
122
- const ipConfig = ctx.context.options?.advanced?.ipAddress;
123
- if (ipConfig?.disableIpTracking === true) {
124
- ctx.context.location = void 0;
125
- return;
126
- }
127
- const identification = ctx.context.identification;
128
- if (requestId && identification) {
129
- const loc = getLocation(identification);
130
- ctx.context.location = {
131
- ipAddress: identification.ip || void 0,
132
- city: loc?.city || void 0,
133
- country: loc?.country?.name || void 0,
134
- countryCode: loc?.country?.code || void 0
135
- };
136
- return;
137
- }
138
- const ipAddress = getClientIpFromRequest(ctx.request, ipConfig?.ipAddressHeaders || null);
139
- const countryCode = getCountryCodeFromRequest(ctx.request);
140
- if (ipAddress || countryCode) {
141
- ctx.context.location = {
142
- ipAddress,
143
- countryCode
144
- };
145
- return;
146
- }
147
- ctx.context.location = void 0;
148
- });
149
- }
150
- /**
151
- * Get the visitor's location
152
- */
153
- function getLocation(identification) {
154
- if (!identification) return null;
155
- return identification.location;
156
- }
157
- function getClientIpFromRequest(request, ipAddressHeaders) {
158
- if (!request) return void 0;
159
- const headers = ipAddressHeaders?.length ? ipAddressHeaders : [
160
- "cf-connecting-ip",
161
- "x-forwarded-for",
162
- "x-real-ip",
163
- "x-vercel-forwarded-for"
164
- ];
165
- for (const headerName of headers) {
166
- const value = request.headers.get(headerName);
167
- if (!value) continue;
168
- const ip = value.split(",")[0]?.trim();
169
- if (ip) return ip;
170
- }
171
- }
172
- function getCountryCodeFromRequest(request) {
173
- if (!request) return void 0;
174
- const cc = request.headers.get("cf-ipcountry") ?? request.headers.get("x-vercel-ip-country");
175
- return cc ? cc.toUpperCase() : void 0;
176
- }
177
- //#endregion
178
- export { hash as a, identify as i, createIdentificationMiddleware as n, generateRequestId as r, IDENTIFICATION_COOKIE_NAME as t };