@better-auth/infra 0.1.13 → 0.2.0

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 CHANGED
@@ -6,6 +6,7 @@ Infra plugins for Better Auth:
6
6
  - `dashClient()` for dashboard client actions (including audit log queries).
7
7
  - `sentinel()` for security checks and abuse protection.
8
8
  - `sentinelClient()` for browser fingerprint headers + optional PoW auto-solving.
9
+ - `sentinelNativeClient()` (from `@better-auth/infra/native`) for React Native / Expo.
9
10
 
10
11
  ## Installation
11
12
 
@@ -69,6 +70,45 @@ const auditLogs = await authClient.dash.getAuditLogs({
69
70
  });
70
71
  ```
71
72
 
73
+ ## Native client
74
+
75
+ Use this plugin for your React Native / Expo app. This plugin is designed to work along with the server side sentinel and dash plugins and will ensure your app is protected from abuse and bot attacks.
76
+
77
+ ### Install peers
78
+
79
+ ```bash
80
+ pnpm add react-native @react-native-async-storage/async-storage
81
+ # Optional, for richer device metadata in the identify payload:
82
+ pnpm add expo-constants expo-device
83
+ ```
84
+
85
+ `@react-native-async-storage/async-storage` is optional; if it is not installed, a session-only in-memory visitor id is used. For production, install it or pass `storage` (for example secure storage).
86
+
87
+ ### Example
88
+
89
+ ```ts
90
+ import { createAuthClient } from "better-auth/client";
91
+ import { dashClient, sentinelNativeClient } from "@better-auth/infra/native";
92
+
93
+ export const authClient = createAuthClient({
94
+ baseURL: "https://your-api.example.com",
95
+ plugins: [
96
+ dashClient(),
97
+ sentinelNativeClient({
98
+ identifyUrl: process.env.EXPO_PUBLIC_BETTER_AUTH_KV_URL,
99
+ autoSolveChallenge: true,
100
+ }),
101
+ ],
102
+ });
103
+ ```
104
+
105
+ ### Options (`sentinelNativeClient`)
106
+
107
+ - `identifyUrl?: string` — KV identify endpoint base (defaults to `BETTER_AUTH_KV_URL` from env, then `https://kv.better-auth.com`).
108
+ - `autoSolveChallenge?: boolean` — When `true` (default), `423` responses that include `X-PoW-Challenge` are solved and the request is retried once with `X-PoW-Solution`.
109
+ - `onChallengeReceived?` / `onChallengeSolved?` / `onChallengeFailed?` — PoW lifecycle hooks (reason string, solve time in ms, or error).
110
+ - `storage?: { getItem, setItem }` — Async key/value storage for a stable per-install visitor id (recommended for production).
111
+
72
112
  ## Audit Log APIs
73
113
 
74
114
  ### `dashClient()` API
@@ -249,6 +289,10 @@ All security configuration now belongs in `sentinel()`.
249
289
 
250
290
  See `Audit Log APIs` above for full method details.
251
291
 
292
+ ### `SentinelNativeClientOptions`
293
+
294
+ Exported from `@better-auth/infra/native`. See [React Native client](#native-client) for the full option list and usage.
295
+
252
296
  ## Migration
253
297
 
254
298
  If you previously passed security config to `dash()`, move it to `sentinel()`:
package/dist/client.d.mts CHANGED
@@ -1,73 +1,7 @@
1
- import * as _better_fetch_fetch0 from "@better-fetch/fetch";
1
+ import { a as dashClient, i as DashGetAuditLogsInput, n as DashAuditLogsResponse, r as DashClientOptions, t as DashAuditLog } from "./dash-client-hJHp7l_X.mjs";
2
+ import * as _$_better_fetch_fetch0 from "@better-fetch/fetch";
2
3
 
3
- //#region src/client.d.ts
4
- interface DashAuditLog {
5
- eventType: string;
6
- eventData: Record<string, unknown>;
7
- eventKey: string;
8
- projectId: string;
9
- createdAt: string;
10
- updatedAt: string;
11
- ageInMinutes?: number;
12
- location?: {
13
- ipAddress?: string | null;
14
- city?: string | null;
15
- country?: string | null;
16
- countryCode?: string | null;
17
- };
18
- }
19
- interface DashAuditLogsResponse {
20
- events: DashAuditLog[];
21
- total: number;
22
- limit: number;
23
- offset: number;
24
- }
25
- type SessionLike = {
26
- user?: {
27
- id?: string | null;
28
- };
29
- };
30
- type UserLike = {
31
- id?: string | null;
32
- };
33
- interface DashGetAuditLogsInput {
34
- limit?: number;
35
- offset?: number;
36
- organizationId?: string;
37
- identifier?: string;
38
- eventType?: string;
39
- userId?: string;
40
- user?: UserLike | null;
41
- session?: SessionLike | null;
42
- }
43
- interface DashClientOptions {
44
- resolveUserId?: (input: {
45
- userId?: string;
46
- user?: UserLike | null;
47
- session?: SessionLike | null;
48
- }) => string | undefined;
49
- }
50
- declare const dashClient: (options?: DashClientOptions) => {
51
- id: "dash";
52
- getActions: ($fetch: _better_fetch_fetch0.BetterFetch) => {
53
- dash: {
54
- getAuditLogs: (input?: DashGetAuditLogsInput) => Promise<{
55
- data: null;
56
- error: {
57
- message?: string | undefined;
58
- status: number;
59
- statusText: string;
60
- };
61
- } | {
62
- data: DashAuditLogsResponse;
63
- error: null;
64
- }>;
65
- };
66
- };
67
- pathMethods: {
68
- "/events/audit-logs": "GET";
69
- };
70
- };
4
+ //#region src/sentinel/client.d.ts
71
5
  interface SentinelClientOptions {
72
6
  /**
73
7
  * The URL of the identification service
@@ -97,17 +31,17 @@ declare const sentinelClient: (options?: SentinelClientOptions) => {
97
31
  id: string;
98
32
  name: string;
99
33
  hooks: {
100
- onRequest<T extends Record<string, any>>(context: _better_fetch_fetch0.RequestContext<T>): Promise<_better_fetch_fetch0.RequestContext<T>>;
34
+ onRequest<T extends Record<string, any>>(context: _$_better_fetch_fetch0.RequestContext<T>): Promise<_$_better_fetch_fetch0.RequestContext<T>>;
101
35
  onResponse?: undefined;
102
36
  };
103
37
  } | {
104
38
  id: string;
105
39
  name: string;
106
40
  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>>;
41
+ onResponse(context: _$_better_fetch_fetch0.ResponseContext): Promise<_$_better_fetch_fetch0.ResponseContext>;
42
+ onRequest<T extends Record<string, any>>(context: _$_better_fetch_fetch0.RequestContext<T>): Promise<_$_better_fetch_fetch0.RequestContext<T>>;
109
43
  };
110
44
  })[];
111
45
  };
112
46
  //#endregion
113
- export { DashAuditLog, DashAuditLogsResponse, DashClientOptions, DashGetAuditLogsInput, SentinelClientOptions, dashClient, sentinelClient };
47
+ export { type DashAuditLog, type DashAuditLogsResponse, type DashClientOptions, type 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 identify, c as dashClient, i as generateRequestId, n as encodePoWSolution, r as solvePoWChallenge, s as hash, t as decodePoWChallenge } from "./pow-BUuN_EKw.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,79 +416,9 @@ 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
- */
438
- function hasLeadingZeroBits(hash, bits) {
439
- const fullHexChars = Math.floor(bits / 4);
440
- const remainingBits = bits % 4;
441
- for (let i = 0; i < fullHexChars; i++) if (hash[i] !== "0") return false;
442
- if (remainingBits > 0 && fullHexChars < hash.length) {
443
- if (parseInt(hash[fullHexChars], 16) > (1 << 4 - remainingBits) - 1) return false;
444
- }
445
- return true;
446
- }
447
- /**
448
- * Solve a PoW challenge
449
- * @returns solution or null if challenge couldn't be solved
450
- */
451
- async function solvePoWChallenge(challenge) {
452
- const { nonce, difficulty } = challenge;
453
- let counter = 0;
454
- while (true) {
455
- if (hasLeadingZeroBits(await sha256(`${nonce}:${counter}`), difficulty)) return {
456
- nonce,
457
- counter
458
- };
459
- counter++;
460
- if (counter % 1e3 === 0) await new Promise((resolve) => setTimeout(resolve, 0));
461
- if (counter > 1e8) throw new Error("PoW challenge took too long to solve");
462
- }
463
- }
464
- /**
465
- * Decode a base64-encoded challenge string
466
- */
467
- function decodePoWChallenge(encoded) {
468
- try {
469
- const decoded = atob(encoded);
470
- return JSON.parse(decoded);
471
- } catch {
472
- return null;
473
- }
474
- }
475
- /**
476
- * Encode a solution to base64
477
- */
478
- 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;
487
- }
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
- };
419
+ //#endregion
420
+ //#region src/sentinel/client.ts
421
+ const DEFAULT_IDENTIFY_URL = "https://kv.better-auth.com";
508
422
  const sentinelClient = (options) => {
509
423
  const autoSolve = options?.autoSolveChallenge !== false;
510
424
  const identifyUrl = options?.identifyUrl ?? env.BETTER_AUTH_KV_URL ?? DEFAULT_IDENTIFY_URL;
@@ -0,0 +1,72 @@
1
+ import * as _$_better_fetch_fetch0 from "@better-fetch/fetch";
2
+
3
+ //#region src/dash-client.d.ts
4
+ interface DashAuditLog {
5
+ eventType: string;
6
+ eventData: Record<string, unknown>;
7
+ eventKey: string;
8
+ projectId: string;
9
+ createdAt: string;
10
+ updatedAt: string;
11
+ ageInMinutes?: number;
12
+ location?: {
13
+ ipAddress?: string | null;
14
+ city?: string | null;
15
+ country?: string | null;
16
+ countryCode?: string | null;
17
+ };
18
+ }
19
+ interface DashAuditLogsResponse {
20
+ events: DashAuditLog[];
21
+ total: number;
22
+ limit: number;
23
+ offset: number;
24
+ }
25
+ type SessionLike = {
26
+ user?: {
27
+ id?: string | null;
28
+ };
29
+ };
30
+ type UserLike = {
31
+ id?: string | null;
32
+ };
33
+ interface DashGetAuditLogsInput {
34
+ limit?: number;
35
+ offset?: number;
36
+ organizationId?: string;
37
+ identifier?: string;
38
+ eventType?: string;
39
+ userId?: string;
40
+ user?: UserLike | null;
41
+ session?: SessionLike | null;
42
+ }
43
+ interface DashClientOptions {
44
+ resolveUserId?: (input: {
45
+ userId?: string;
46
+ user?: UserLike | null;
47
+ session?: SessionLike | null;
48
+ }) => string | undefined;
49
+ }
50
+ declare const dashClient: (options?: DashClientOptions) => {
51
+ id: "dash";
52
+ getActions: ($fetch: _$_better_fetch_fetch0.BetterFetch) => {
53
+ dash: {
54
+ getAuditLogs: (input?: DashGetAuditLogsInput) => Promise<{
55
+ data: null;
56
+ error: {
57
+ message?: string | undefined;
58
+ status: number;
59
+ statusText: string;
60
+ };
61
+ } | {
62
+ data: DashAuditLogsResponse;
63
+ error: null;
64
+ }>;
65
+ };
66
+ };
67
+ pathMethods: {
68
+ "/events/audit-logs": "GET";
69
+ };
70
+ };
71
+ //#endregion
72
+ export { dashClient as a, DashGetAuditLogsInput as i, DashAuditLogsResponse as n, DashClientOptions as r, DashAuditLog as t };