@better-auth/infra 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.mts CHANGED
@@ -1,5 +1,48 @@
1
- import * as _better_fetch_fetch0 from "@better-fetch/fetch";
1
+ import * as _$_better_fetch_fetch0 from "@better-fetch/fetch";
2
2
 
3
+ //#region src/sentinel/client.d.ts
4
+ interface SentinelClientOptions {
5
+ /**
6
+ * The URL of the identification service
7
+ * @default "https://kv.better-auth.com"
8
+ */
9
+ identifyUrl?: string;
10
+ /**
11
+ * Whether to automatically solve PoW challenges (default: true)
12
+ */
13
+ autoSolveChallenge?: boolean;
14
+ /**
15
+ * Callback when a PoW challenge is received
16
+ */
17
+ onChallengeReceived?: (reason: string) => void;
18
+ /**
19
+ * Callback when a PoW challenge is solved
20
+ */
21
+ onChallengeSolved?: (solveTimeMs: number) => void;
22
+ /**
23
+ * Callback when a PoW challenge fails to solve
24
+ */
25
+ onChallengeFailed?: (error: Error) => void;
26
+ }
27
+ declare const sentinelClient: (options?: SentinelClientOptions) => {
28
+ id: "sentinel";
29
+ fetchPlugins: ({
30
+ id: string;
31
+ name: string;
32
+ hooks: {
33
+ onRequest<T extends Record<string, any>>(context: _$_better_fetch_fetch0.RequestContext<T>): Promise<_$_better_fetch_fetch0.RequestContext<T>>;
34
+ onResponse?: undefined;
35
+ };
36
+ } | {
37
+ id: string;
38
+ name: string;
39
+ hooks: {
40
+ onResponse(context: _$_better_fetch_fetch0.ResponseContext): Promise<_$_better_fetch_fetch0.ResponseContext>;
41
+ onRequest<T extends Record<string, any>>(context: _$_better_fetch_fetch0.RequestContext<T>): Promise<_$_better_fetch_fetch0.RequestContext<T>>;
42
+ };
43
+ })[];
44
+ };
45
+ //#endregion
3
46
  //#region src/client.d.ts
4
47
  interface DashAuditLog {
5
48
  eventType: string;
@@ -49,18 +92,18 @@ interface DashClientOptions {
49
92
  }
50
93
  declare const dashClient: (options?: DashClientOptions) => {
51
94
  id: "dash";
52
- getActions: ($fetch: _better_fetch_fetch0.BetterFetch) => {
95
+ getActions: ($fetch: _$_better_fetch_fetch0.BetterFetch) => {
53
96
  dash: {
54
97
  getAuditLogs: (input?: DashGetAuditLogsInput) => Promise<{
98
+ data: DashAuditLogsResponse;
99
+ error: null;
100
+ } | {
55
101
  data: null;
56
102
  error: {
57
103
  message?: string | undefined;
58
104
  status: number;
59
105
  statusText: string;
60
106
  };
61
- } | {
62
- data: DashAuditLogsResponse;
63
- error: null;
64
107
  }>;
65
108
  };
66
109
  };
@@ -68,46 +111,5 @@ declare const dashClient: (options?: DashClientOptions) => {
68
111
  "/events/audit-logs": "GET";
69
112
  };
70
113
  };
71
- interface SentinelClientOptions {
72
- /**
73
- * The URL of the identification service
74
- * @default "https://kv.better-auth.com"
75
- */
76
- identifyUrl?: string;
77
- /**
78
- * Whether to automatically solve PoW challenges (default: true)
79
- */
80
- autoSolveChallenge?: boolean;
81
- /**
82
- * Callback when a PoW challenge is received
83
- */
84
- onChallengeReceived?: (reason: string) => void;
85
- /**
86
- * Callback when a PoW challenge is solved
87
- */
88
- onChallengeSolved?: (solveTimeMs: number) => void;
89
- /**
90
- * Callback when a PoW challenge fails to solve
91
- */
92
- onChallengeFailed?: (error: Error) => void;
93
- }
94
- declare const sentinelClient: (options?: SentinelClientOptions) => {
95
- id: "sentinel";
96
- fetchPlugins: ({
97
- id: string;
98
- name: string;
99
- hooks: {
100
- onRequest<T extends Record<string, any>>(context: _better_fetch_fetch0.RequestContext<T>): Promise<_better_fetch_fetch0.RequestContext<T>>;
101
- onResponse?: undefined;
102
- };
103
- } | {
104
- id: string;
105
- name: string;
106
- hooks: {
107
- onResponse(context: _better_fetch_fetch0.ResponseContext): Promise<_better_fetch_fetch0.ResponseContext>;
108
- onRequest<T extends Record<string, any>>(context: _better_fetch_fetch0.RequestContext<T>): Promise<_better_fetch_fetch0.RequestContext<T>>;
109
- };
110
- })[];
111
- };
112
114
  //#endregion
113
- export { DashAuditLog, DashAuditLogsResponse, DashClientOptions, DashGetAuditLogsInput, SentinelClientOptions, dashClient, sentinelClient };
115
+ export { DashAuditLog, DashAuditLogsResponse, DashClientOptions, DashGetAuditLogsInput, type SentinelClientOptions, dashClient, sentinelClient };
package/dist/client.mjs CHANGED
@@ -1,18 +1,7 @@
1
1
  import { r as KV_TIMEOUT_MS } from "./constants-DdWGfvz1.mjs";
2
+ import { a as hash, i as identify, r as generateRequestId } from "./identification-DF2nvmng.mjs";
2
3
  import { env } from "@better-auth/core/env";
3
- //#region src/client.ts
4
- const DEFAULT_IDENTIFY_URL = "https://kv.better-auth.com";
5
- async function sha256(message) {
6
- const msgBuffer = new TextEncoder().encode(message);
7
- const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
8
- return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
9
- }
10
- function generateRequestId() {
11
- const array = new Uint8Array(16);
12
- crypto.getRandomValues(array);
13
- const hex = Array.from(array).map((b) => b.toString(16).padStart(2, "0")).join("");
14
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
15
- }
4
+ //#region src/sentinel/fingerprint.ts
16
5
  function murmurhash3(str, seed = 0) {
17
6
  let h1 = seed;
18
7
  const c1 = 3432918353;
@@ -336,7 +325,7 @@ async function generateVisitorId(components) {
336
325
  fonts: components.fonts,
337
326
  maxTouchPoints: components.maxTouchPoints
338
327
  };
339
- return (await sha256(JSON.stringify(stableData))).slice(0, 20);
328
+ return (await hash(JSON.stringify(stableData))).slice(0, 20);
340
329
  }
341
330
  function calculateConfidence(components) {
342
331
  const weights = {
@@ -369,6 +358,8 @@ function calculateConfidence(components) {
369
358
  let cachedFingerprint = null;
370
359
  let fingerprintPromise = null;
371
360
  let identifySent = false;
361
+ let identifyCompletePromise = null;
362
+ let identifyCompleteResolve = null;
372
363
  async function getFingerprint() {
373
364
  if (typeof window === "undefined") return null;
374
365
  if (await cachedFingerprint) return cachedFingerprint;
@@ -393,8 +384,6 @@ async function getFingerprint() {
393
384
  return null;
394
385
  }
395
386
  }
396
- let identifyCompletePromise = null;
397
- let identifyCompleteResolve = null;
398
387
  async function sendIdentify(identifyUrl) {
399
388
  if (identifySent || typeof window === "undefined") return;
400
389
  const fingerprint = await getFingerprint();
@@ -412,12 +401,7 @@ async function sendIdentify(identifyUrl) {
412
401
  incognito: detectIncognito()
413
402
  };
414
403
  try {
415
- await fetch(`${identifyUrl}/identify`, {
416
- method: "POST",
417
- headers: { "Content-Type": "application/json" },
418
- body: JSON.stringify(payload),
419
- signal: AbortSignal.timeout(KV_TIMEOUT_MS)
420
- });
404
+ await identify(identifyUrl, payload, AbortSignal.timeout(KV_TIMEOUT_MS));
421
405
  } catch (error) {
422
406
  console.warn("[Dash] Identify request failed:", error);
423
407
  } finally {
@@ -432,9 +416,8 @@ async function waitForIdentify(timeoutMs = 500) {
432
416
  if (!identifyCompletePromise) return;
433
417
  await Promise.race([identifyCompletePromise, new Promise((resolve) => setTimeout(resolve, timeoutMs))]);
434
418
  }
435
- /**
436
- * Check if a hash has the required number of leading zero bits
437
- */
419
+ //#endregion
420
+ //#region src/sentinel/pow.ts
438
421
  function hasLeadingZeroBits(hash, bits) {
439
422
  const fullHexChars = Math.floor(bits / 4);
440
423
  const remainingBits = bits % 4;
@@ -444,15 +427,11 @@ function hasLeadingZeroBits(hash, bits) {
444
427
  }
445
428
  return true;
446
429
  }
447
- /**
448
- * Solve a PoW challenge
449
- * @returns solution or null if challenge couldn't be solved
450
- */
451
430
  async function solvePoWChallenge(challenge) {
452
431
  const { nonce, difficulty } = challenge;
453
432
  let counter = 0;
454
433
  while (true) {
455
- if (hasLeadingZeroBits(await sha256(`${nonce}:${counter}`), difficulty)) return {
434
+ if (hasLeadingZeroBits(await hash(`${nonce}:${counter}`), difficulty)) return {
456
435
  nonce,
457
436
  counter
458
437
  };
@@ -461,50 +440,42 @@ async function solvePoWChallenge(challenge) {
461
440
  if (counter > 1e8) throw new Error("PoW challenge took too long to solve");
462
441
  }
463
442
  }
464
- /**
465
- * Decode a base64-encoded challenge string
466
- */
443
+ function decodeBase64ToUtf8(encoded) {
444
+ if (typeof globalThis.atob === "function") return globalThis.atob(encoded);
445
+ throw new Error("[Sentinel] Base64 decode requires atob (browser, Hermes, or Bun)");
446
+ }
447
+ function encodeUtf8ToBase64(str) {
448
+ if (typeof globalThis.btoa === "function") return globalThis.btoa(str);
449
+ const bytes = new TextEncoder().encode(str);
450
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
451
+ let out = "";
452
+ for (let i = 0; i < bytes.length; i += 3) {
453
+ const b0 = bytes[i];
454
+ const b1 = bytes[i + 1] ?? 0;
455
+ const b2 = bytes[i + 2] ?? 0;
456
+ const triple = b0 << 16 | b1 << 8 | b2;
457
+ const pad = i + 2 >= bytes.length ? i + 1 >= bytes.length ? 2 : 1 : 0;
458
+ out += chars[triple >> 18 & 63];
459
+ out += chars[triple >> 12 & 63];
460
+ out += pad < 2 ? chars[triple >> 6 & 63] : "=";
461
+ out += pad < 1 ? chars[triple & 63] : "=";
462
+ }
463
+ return out;
464
+ }
467
465
  function decodePoWChallenge(encoded) {
468
466
  try {
469
- const decoded = atob(encoded);
467
+ const decoded = decodeBase64ToUtf8(encoded);
470
468
  return JSON.parse(decoded);
471
469
  } catch {
472
470
  return null;
473
471
  }
474
472
  }
475
- /**
476
- * Encode a solution to base64
477
- */
478
473
  function encodePoWSolution(solution) {
479
- return btoa(JSON.stringify(solution));
480
- }
481
- function resolveDashUserId(input, options) {
482
- return input.userId || options?.resolveUserId?.({
483
- userId: input.userId,
484
- user: input.user,
485
- session: input.session
486
- }) || input.user?.id || input.session?.user?.id || void 0;
474
+ return encodeUtf8ToBase64(JSON.stringify(solution));
487
475
  }
488
- const dashClient = (options) => {
489
- return {
490
- id: "dash",
491
- getActions: ($fetch) => ({ dash: { getAuditLogs: async (input = {}) => {
492
- const userId = resolveDashUserId(input, options);
493
- return $fetch("/events/audit-logs", {
494
- method: "GET",
495
- query: {
496
- limit: input.limit,
497
- offset: input.offset,
498
- organizationId: input.organizationId,
499
- identifier: input.identifier,
500
- eventType: input.eventType,
501
- userId
502
- }
503
- });
504
- } } }),
505
- pathMethods: { "/events/audit-logs": "GET" }
506
- };
507
- };
476
+ //#endregion
477
+ //#region src/sentinel/client.ts
478
+ const DEFAULT_IDENTIFY_URL = "https://kv.better-auth.com";
508
479
  const sentinelClient = (options) => {
509
480
  const autoSolve = options?.autoSolveChallenge !== false;
510
481
  const identifyUrl = options?.identifyUrl ?? env.BETTER_AUTH_KV_URL ?? DEFAULT_IDENTIFY_URL;
@@ -594,4 +565,33 @@ const sentinelClient = (options) => {
594
565
  };
595
566
  };
596
567
  //#endregion
568
+ //#region src/client.ts
569
+ function resolveDashUserId(input, options) {
570
+ return input.userId || options?.resolveUserId?.({
571
+ userId: input.userId,
572
+ user: input.user,
573
+ session: input.session
574
+ }) || input.user?.id || input.session?.user?.id || void 0;
575
+ }
576
+ const dashClient = (options) => {
577
+ return {
578
+ id: "dash",
579
+ getActions: ($fetch) => ({ dash: { getAuditLogs: async (input = {}) => {
580
+ const userId = resolveDashUserId(input, options);
581
+ return $fetch("/events/audit-logs", {
582
+ method: "GET",
583
+ query: {
584
+ limit: input.limit,
585
+ offset: input.offset,
586
+ organizationId: input.organizationId,
587
+ identifier: input.identifier,
588
+ eventType: input.eventType,
589
+ userId
590
+ }
591
+ });
592
+ } } }),
593
+ pathMethods: { "/events/audit-logs": "GET" }
594
+ };
595
+ };
596
+ //#endregion
597
597
  export { dashClient, sentinelClient };
@@ -0,0 +1,178 @@
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 };