@better-auth/infra 0.1.12 → 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;
@@ -10,10 +53,10 @@ interface DashAuditLog {
10
53
  updatedAt: string;
11
54
  ageInMinutes?: number;
12
55
  location?: {
13
- ipAddress?: string;
14
- city?: string;
15
- country?: string;
16
- countryCode?: string;
56
+ ipAddress?: string | null;
57
+ city?: string | null;
58
+ country?: string | null;
59
+ countryCode?: string | null;
17
60
  };
18
61
  }
19
62
  interface DashAuditLogsResponse {
@@ -49,7 +92,7 @@ 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<{
55
98
  data: DashAuditLogsResponse;
@@ -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,19 +1,7 @@
1
- import { r as KV_TIMEOUT_MS } from "./constants-B-e0_Nsv.mjs";
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
-
4
- //#region src/client.ts
5
- const DEFAULT_IDENTIFY_URL = "https://kv.better-auth.com";
6
- async function sha256(message) {
7
- const msgBuffer = new TextEncoder().encode(message);
8
- const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
9
- return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
10
- }
11
- function generateRequestId() {
12
- const array = new Uint8Array(16);
13
- crypto.getRandomValues(array);
14
- const hex = Array.from(array).map((b) => b.toString(16).padStart(2, "0")).join("");
15
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
16
- }
4
+ //#region src/sentinel/fingerprint.ts
17
5
  function murmurhash3(str, seed = 0) {
18
6
  let h1 = seed;
19
7
  const c1 = 3432918353;
@@ -337,7 +325,7 @@ async function generateVisitorId(components) {
337
325
  fonts: components.fonts,
338
326
  maxTouchPoints: components.maxTouchPoints
339
327
  };
340
- return (await sha256(JSON.stringify(stableData))).slice(0, 20);
328
+ return (await hash(JSON.stringify(stableData))).slice(0, 20);
341
329
  }
342
330
  function calculateConfidence(components) {
343
331
  const weights = {
@@ -370,6 +358,8 @@ function calculateConfidence(components) {
370
358
  let cachedFingerprint = null;
371
359
  let fingerprintPromise = null;
372
360
  let identifySent = false;
361
+ let identifyCompletePromise = null;
362
+ let identifyCompleteResolve = null;
373
363
  async function getFingerprint() {
374
364
  if (typeof window === "undefined") return null;
375
365
  if (await cachedFingerprint) return cachedFingerprint;
@@ -394,8 +384,6 @@ async function getFingerprint() {
394
384
  return null;
395
385
  }
396
386
  }
397
- let identifyCompletePromise = null;
398
- let identifyCompleteResolve = null;
399
387
  async function sendIdentify(identifyUrl) {
400
388
  if (identifySent || typeof window === "undefined") return;
401
389
  const fingerprint = await getFingerprint();
@@ -413,12 +401,7 @@ async function sendIdentify(identifyUrl) {
413
401
  incognito: detectIncognito()
414
402
  };
415
403
  try {
416
- await fetch(`${identifyUrl}/identify`, {
417
- method: "POST",
418
- headers: { "Content-Type": "application/json" },
419
- body: JSON.stringify(payload),
420
- signal: AbortSignal.timeout(KV_TIMEOUT_MS)
421
- });
404
+ await identify(identifyUrl, payload, AbortSignal.timeout(KV_TIMEOUT_MS));
422
405
  } catch (error) {
423
406
  console.warn("[Dash] Identify request failed:", error);
424
407
  } finally {
@@ -433,9 +416,8 @@ async function waitForIdentify(timeoutMs = 500) {
433
416
  if (!identifyCompletePromise) return;
434
417
  await Promise.race([identifyCompletePromise, new Promise((resolve) => setTimeout(resolve, timeoutMs))]);
435
418
  }
436
- /**
437
- * Check if a hash has the required number of leading zero bits
438
- */
419
+ //#endregion
420
+ //#region src/sentinel/pow.ts
439
421
  function hasLeadingZeroBits(hash, bits) {
440
422
  const fullHexChars = Math.floor(bits / 4);
441
423
  const remainingBits = bits % 4;
@@ -445,15 +427,11 @@ function hasLeadingZeroBits(hash, bits) {
445
427
  }
446
428
  return true;
447
429
  }
448
- /**
449
- * Solve a PoW challenge
450
- * @returns solution or null if challenge couldn't be solved
451
- */
452
430
  async function solvePoWChallenge(challenge) {
453
431
  const { nonce, difficulty } = challenge;
454
432
  let counter = 0;
455
433
  while (true) {
456
- if (hasLeadingZeroBits(await sha256(`${nonce}:${counter}`), difficulty)) return {
434
+ if (hasLeadingZeroBits(await hash(`${nonce}:${counter}`), difficulty)) return {
457
435
  nonce,
458
436
  counter
459
437
  };
@@ -462,50 +440,42 @@ async function solvePoWChallenge(challenge) {
462
440
  if (counter > 1e8) throw new Error("PoW challenge took too long to solve");
463
441
  }
464
442
  }
465
- /**
466
- * Decode a base64-encoded challenge string
467
- */
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
+ }
468
465
  function decodePoWChallenge(encoded) {
469
466
  try {
470
- const decoded = atob(encoded);
467
+ const decoded = decodeBase64ToUtf8(encoded);
471
468
  return JSON.parse(decoded);
472
469
  } catch {
473
470
  return null;
474
471
  }
475
472
  }
476
- /**
477
- * Encode a solution to base64
478
- */
479
473
  function encodePoWSolution(solution) {
480
- return btoa(JSON.stringify(solution));
481
- }
482
- function resolveDashUserId(input, options) {
483
- return input.userId || options?.resolveUserId?.({
484
- userId: input.userId,
485
- user: input.user,
486
- session: input.session
487
- }) || input.user?.id || input.session?.user?.id || void 0;
474
+ return encodeUtf8ToBase64(JSON.stringify(solution));
488
475
  }
489
- const dashClient = (options) => {
490
- return {
491
- id: "dash",
492
- getActions: ($fetch) => ({ dash: { getAuditLogs: async (input = {}) => {
493
- const userId = resolveDashUserId(input, options);
494
- return $fetch("/events/audit-logs", {
495
- method: "GET",
496
- query: {
497
- limit: input.limit,
498
- offset: input.offset,
499
- organizationId: input.organizationId,
500
- identifier: input.identifier,
501
- eventType: input.eventType,
502
- userId
503
- }
504
- });
505
- } } }),
506
- pathMethods: { "/events/audit-logs": "GET" }
507
- };
508
- };
476
+ //#endregion
477
+ //#region src/sentinel/client.ts
478
+ const DEFAULT_IDENTIFY_URL = "https://kv.better-auth.com";
509
479
  const sentinelClient = (options) => {
510
480
  const autoSolve = options?.autoSolveChallenge !== false;
511
481
  const identifyUrl = options?.identifyUrl ?? env.BETTER_AUTH_KV_URL ?? DEFAULT_IDENTIFY_URL;
@@ -594,6 +564,34 @@ const sentinelClient = (options) => {
594
564
  }]
595
565
  };
596
566
  };
597
-
598
567
  //#endregion
599
- export { dashClient, sentinelClient };
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
+ export { dashClient, sentinelClient };
@@ -1,5 +1,4 @@
1
1
  import { env } from "@better-auth/core/env";
2
-
3
2
  //#region src/constants.ts
4
3
  /**
5
4
  * Infrastructure API URL
@@ -13,6 +12,5 @@ const INFRA_API_URL = env.BETTER_AUTH_API_URL || "https://dash.better-auth.com";
13
12
  const INFRA_KV_URL = env.BETTER_AUTH_KV_URL || "https://kv.better-auth.com";
14
13
  /** Timeout for KV HTTP operations (ms) */
15
14
  const KV_TIMEOUT_MS = 5e3;
16
-
17
15
  //#endregion
18
- export { INFRA_KV_URL as n, KV_TIMEOUT_MS as r, INFRA_API_URL as t };
16
+ export { INFRA_KV_URL as n, KV_TIMEOUT_MS as r, INFRA_API_URL as t };
package/dist/email.mjs CHANGED
@@ -1,7 +1,6 @@
1
- import { t as INFRA_API_URL } from "./constants-B-e0_Nsv.mjs";
1
+ import { t as INFRA_API_URL } from "./constants-DdWGfvz1.mjs";
2
2
  import { logger } from "better-auth";
3
3
  import { env } from "@better-auth/core/env";
4
-
5
4
  //#region src/email.ts
6
5
  /**
7
6
  * Email sending module for @better-auth/infra
@@ -178,6 +177,5 @@ async function sendEmail(options, config) {
178
177
  async function sendBulkEmails(options, config) {
179
178
  return createEmailSender(config).sendBulk(options);
180
179
  }
181
-
182
180
  //#endregion
183
- export { EMAIL_TEMPLATES, createEmailSender, sendBulkEmails, sendEmail };
181
+ export { EMAIL_TEMPLATES, createEmailSender, sendBulkEmails, sendEmail };
@@ -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 };