@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 +44 -0
- package/dist/client.d.mts +7 -73
- package/dist/client.mjs +9 -95
- package/dist/dash-client-hJHp7l_X.d.mts +72 -0
- package/dist/index.d.mts +867 -1090
- package/dist/index.mjs +1174 -1094
- package/dist/native.d.mts +18 -0
- package/dist/native.mjs +292 -0
- package/dist/pow-BUuN_EKw.mjs +131 -0
- package/package.json +34 -7
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
|
|
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/
|
|
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
|
|
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
|
|
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
|
-
|
|
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 };
|