@better-auth/infra 0.1.14 → 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,3 +1,4 @@
1
+ import { a as dashClient, i as DashGetAuditLogsInput, n as DashAuditLogsResponse, r as DashClientOptions, t as DashAuditLog } from "./dash-client-hJHp7l_X.mjs";
1
2
  import * as _$_better_fetch_fetch0 from "@better-fetch/fetch";
2
3
 
3
4
  //#region src/sentinel/client.d.ts
@@ -43,73 +44,4 @@ declare const sentinelClient: (options?: SentinelClientOptions) => {
43
44
  })[];
44
45
  };
45
46
  //#endregion
46
- //#region src/client.d.ts
47
- interface DashAuditLog {
48
- eventType: string;
49
- eventData: Record<string, unknown>;
50
- eventKey: string;
51
- projectId: string;
52
- createdAt: string;
53
- updatedAt: string;
54
- ageInMinutes?: number;
55
- location?: {
56
- ipAddress?: string | null;
57
- city?: string | null;
58
- country?: string | null;
59
- countryCode?: string | null;
60
- };
61
- }
62
- interface DashAuditLogsResponse {
63
- events: DashAuditLog[];
64
- total: number;
65
- limit: number;
66
- offset: number;
67
- }
68
- type SessionLike = {
69
- user?: {
70
- id?: string | null;
71
- };
72
- };
73
- type UserLike = {
74
- id?: string | null;
75
- };
76
- interface DashGetAuditLogsInput {
77
- limit?: number;
78
- offset?: number;
79
- organizationId?: string;
80
- identifier?: string;
81
- eventType?: string;
82
- userId?: string;
83
- user?: UserLike | null;
84
- session?: SessionLike | null;
85
- }
86
- interface DashClientOptions {
87
- resolveUserId?: (input: {
88
- userId?: string;
89
- user?: UserLike | null;
90
- session?: SessionLike | null;
91
- }) => string | undefined;
92
- }
93
- declare const dashClient: (options?: DashClientOptions) => {
94
- id: "dash";
95
- getActions: ($fetch: _$_better_fetch_fetch0.BetterFetch) => {
96
- dash: {
97
- getAuditLogs: (input?: DashGetAuditLogsInput) => Promise<{
98
- data: DashAuditLogsResponse;
99
- error: null;
100
- } | {
101
- data: null;
102
- error: {
103
- message?: string | undefined;
104
- status: number;
105
- statusText: string;
106
- };
107
- }>;
108
- };
109
- };
110
- pathMethods: {
111
- "/events/audit-logs": "GET";
112
- };
113
- };
114
- //#endregion
115
- export { DashAuditLog, DashAuditLogsResponse, DashClientOptions, DashGetAuditLogsInput, type SentinelClientOptions, dashClient, sentinelClient };
47
+ export { type DashAuditLog, type DashAuditLogsResponse, type DashClientOptions, type DashGetAuditLogsInput, type SentinelClientOptions, dashClient, sentinelClient };
package/dist/client.mjs CHANGED
@@ -1,5 +1,5 @@
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
+ 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";
3
3
  import { env } from "@better-auth/core/env";
4
4
  //#region src/sentinel/fingerprint.ts
5
5
  function murmurhash3(str, seed = 0) {
@@ -417,63 +417,6 @@ async function waitForIdentify(timeoutMs = 500) {
417
417
  await Promise.race([identifyCompletePromise, new Promise((resolve) => setTimeout(resolve, timeoutMs))]);
418
418
  }
419
419
  //#endregion
420
- //#region src/sentinel/pow.ts
421
- function hasLeadingZeroBits(hash, bits) {
422
- const fullHexChars = Math.floor(bits / 4);
423
- const remainingBits = bits % 4;
424
- for (let i = 0; i < fullHexChars; i++) if (hash[i] !== "0") return false;
425
- if (remainingBits > 0 && fullHexChars < hash.length) {
426
- if (parseInt(hash[fullHexChars], 16) > (1 << 4 - remainingBits) - 1) return false;
427
- }
428
- return true;
429
- }
430
- async function solvePoWChallenge(challenge) {
431
- const { nonce, difficulty } = challenge;
432
- let counter = 0;
433
- while (true) {
434
- if (hasLeadingZeroBits(await hash(`${nonce}:${counter}`), difficulty)) return {
435
- nonce,
436
- counter
437
- };
438
- counter++;
439
- if (counter % 1e3 === 0) await new Promise((resolve) => setTimeout(resolve, 0));
440
- if (counter > 1e8) throw new Error("PoW challenge took too long to solve");
441
- }
442
- }
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
- }
465
- function decodePoWChallenge(encoded) {
466
- try {
467
- const decoded = decodeBase64ToUtf8(encoded);
468
- return JSON.parse(decoded);
469
- } catch {
470
- return null;
471
- }
472
- }
473
- function encodePoWSolution(solution) {
474
- return encodeUtf8ToBase64(JSON.stringify(solution));
475
- }
476
- //#endregion
477
420
  //#region src/sentinel/client.ts
478
421
  const DEFAULT_IDENTIFY_URL = "https://kv.better-auth.com";
479
422
  const sentinelClient = (options) => {
@@ -565,33 +508,4 @@ const sentinelClient = (options) => {
565
508
  };
566
509
  };
567
510
  //#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
511
  export { dashClient, sentinelClient };
@@ -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 };