@better-auth/infra 0.1.6

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 ADDED
@@ -0,0 +1,280 @@
1
+ # Better Auth Infrastructure
2
+
3
+ Infra plugins for Better Auth:
4
+
5
+ - `dash()` for dashboard/admin APIs, analytics tracking, and infra endpoints.
6
+ - `dashClient()` for dashboard client actions (including audit log queries).
7
+ - `sentinel()` for security checks and abuse protection.
8
+ - `sentinelClient()` for browser fingerprint headers + optional PoW auto-solving.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install @better-auth/infra
14
+ # or
15
+ pnpm add @better-auth/infra
16
+ # or
17
+ bun add @better-auth/infra
18
+ ```
19
+
20
+ ## Server Usage
21
+
22
+ ```ts
23
+ import { betterAuth } from "better-auth";
24
+ import { dash, sentinel } from "@better-auth/infra";
25
+
26
+ export const auth = betterAuth({
27
+ // ...your Better Auth config
28
+ plugins: [
29
+ dash({
30
+ apiUrl: process.env.BETTER_AUTH_API_URL,
31
+ kvUrl: process.env.BETTER_AUTH_KV_URL,
32
+ apiKey: process.env.BETTER_AUTH_API_KEY,
33
+ }),
34
+ sentinel({
35
+ apiUrl: process.env.BETTER_AUTH_API_URL,
36
+ kvUrl: process.env.BETTER_AUTH_KV_URL,
37
+ apiKey: process.env.BETTER_AUTH_API_KEY,
38
+ security: {
39
+ credentialStuffing: {
40
+ enabled: true,
41
+ thresholds: { challenge: 3, block: 5 },
42
+ },
43
+ },
44
+ }),
45
+ ],
46
+ });
47
+ ```
48
+
49
+ ## Client Usage
50
+
51
+ ```ts
52
+ import { createAuthClient } from "better-auth/client";
53
+ import { dashClient, sentinelClient } from "@better-auth/infra/client";
54
+
55
+ export const authClient = createAuthClient({
56
+ plugins: [
57
+ dashClient(),
58
+ sentinelClient({
59
+ autoSolveChallenge: true,
60
+ }),
61
+ ],
62
+ });
63
+
64
+ // Resolve user from session or pass explicit user/org context
65
+ const auditLogs = await authClient.dash.getAuditLogs({
66
+ session: await authClient.getSession().then((r) => r.data),
67
+ organizationId: "org_123",
68
+ limit: 20,
69
+ });
70
+ ```
71
+
72
+ ## Audit Log APIs
73
+
74
+ ### `dashClient()` API
75
+
76
+ `dashClient()` adds:
77
+
78
+ - `authClient.dash.getAuditLogs(input)`
79
+
80
+ `getAuditLogs(input)` accepts:
81
+
82
+ - `limit?: number` (default `50`, max `100`)
83
+ - `offset?: number` (default `0`)
84
+ - `organizationId?: string`
85
+ - `identifier?: string`
86
+ - `eventType?: string`
87
+ - `userId?: string`
88
+ - `user?: { id?: string | null }`
89
+ - `session?: { user?: { id?: string | null } }`
90
+
91
+ The resolved user ID is determined in this order:
92
+
93
+ - `input.userId`
94
+ - `dashClient({ resolveUserId })`
95
+ - `input.user?.id`
96
+ - `input.session?.user?.id`
97
+
98
+ Response shape:
99
+
100
+ - `events: DashAuditLog[]`
101
+ - `total: number`
102
+ - `limit: number`
103
+ - `offset: number`
104
+
105
+ Example:
106
+
107
+ ```ts
108
+ const session = await authClient.getSession().then((r) => r.data);
109
+
110
+ const logs = await authClient.dash.getAuditLogs({
111
+ session,
112
+ organizationId: "org_123",
113
+ limit: 50,
114
+ offset: 0,
115
+ });
116
+ ```
117
+
118
+ To fetch all events, keep paginating with `offset` until `events.length < limit`.
119
+
120
+ ### Filtering
121
+
122
+ Use `getAuditLogs` filters directly in the query:
123
+
124
+ - `eventType`: only return a specific event type (for example `user_signed_in`)
125
+ - `organizationId`: scope logs to one organization
126
+ - `identifier`: narrow organization logs to a specific identifier
127
+ - `userId` / `user` / `session`: resolve which user the logs should be scoped to
128
+
129
+ Examples:
130
+
131
+ ```ts
132
+ // 1) Filter by event type
133
+ const signIns = await authClient.dash.getAuditLogs({
134
+ session,
135
+ eventType: "user_signed_in",
136
+ limit: 20,
137
+ });
138
+
139
+ // 2) Filter by org + identifier
140
+ const orgMemberEvents = await authClient.dash.getAuditLogs({
141
+ session,
142
+ organizationId: "org_123",
143
+ identifier: "user@example.com",
144
+ limit: 50,
145
+ });
146
+
147
+ // 3) Combine filters
148
+ const orgSignIns = await authClient.dash.getAuditLogs({
149
+ session,
150
+ organizationId: "org_123",
151
+ eventType: "user_signed_in",
152
+ limit: 50,
153
+ });
154
+ ```
155
+
156
+ ### Search Patterns
157
+
158
+ `getAuditLogs` does not currently expose a dedicated full-text `search` query param.
159
+ For text search, fetch pages and filter client-side.
160
+
161
+ ```ts
162
+ const matchesText = (event: { eventType: string; eventKey: string; eventData: Record<string, unknown> }, query: string) => {
163
+ const q = query.toLowerCase().trim();
164
+ if (!q) return true;
165
+
166
+ const haystack = [
167
+ event.eventType,
168
+ event.eventKey,
169
+ JSON.stringify(event.eventData ?? {}),
170
+ ]
171
+ .join(" ")
172
+ .toLowerCase();
173
+
174
+ return haystack.includes(q);
175
+ };
176
+
177
+ const page = await authClient.dash.getAuditLogs({ session, limit: 100, offset: 0 });
178
+ const filtered = page.data?.events.filter((event) => matchesText(event, "password")) ?? [];
179
+ ```
180
+
181
+ You can apply the same pattern for:
182
+
183
+ - date range filtering (by `createdAt`)
184
+ - location filtering (by `location.country`, `location.city`, etc.)
185
+ - multi-field compound filters
186
+
187
+ ### Pagination Helper
188
+
189
+ ```ts
190
+ async function getAllAuditLogs(session: unknown) {
191
+ const limit = 100;
192
+ let offset = 0;
193
+ const all: Array<{
194
+ eventType: string;
195
+ createdAt: string;
196
+ eventData: Record<string, unknown>;
197
+ }> = [];
198
+
199
+ while (true) {
200
+ const result = await authClient.dash.getAuditLogs({ session, limit, offset });
201
+ const events = result.data?.events ?? [];
202
+ all.push(...events);
203
+
204
+ if (events.length < limit) break;
205
+ offset += limit;
206
+ }
207
+
208
+ return all;
209
+ }
210
+ ```
211
+
212
+ ### `dash()` Event Endpoints
213
+
214
+ The `dash()` plugin registers these event endpoints:
215
+
216
+ - `getUserEvents` on `GET /events/list`
217
+ - `getAuditLogs` on `GET /events/audit-logs`
218
+ - `getEventTypes` on `GET /events/types`
219
+
220
+ `getAuditLogs` supports query params:
221
+
222
+ - `limit`, `offset`, `eventType`
223
+ - `organizationId`, `identifier`
224
+ - `userId` (must match the authenticated session user)
225
+
226
+ ## Option Types
227
+
228
+ ### `DashOptions`
229
+
230
+ - `apiUrl?: string`
231
+ - `kvUrl?: string`
232
+ - `apiKey?: string`
233
+ - `activityTracking?: { enabled?: boolean; updateInterval?: number }`
234
+
235
+ `dash()` no longer accepts sentinel security config.
236
+
237
+ ### `SentinelOptions`
238
+
239
+ - `apiUrl?: string`
240
+ - `kvUrl?: string`
241
+ - `apiKey?: string`
242
+ - `security?: SecurityOptions`
243
+
244
+ All security configuration now belongs in `sentinel()`.
245
+
246
+ ### `DashClientOptions`
247
+
248
+ - `resolveUserId?: ({ userId, user, session }) => string | undefined`
249
+
250
+ See `Audit Log APIs` above for full method details.
251
+
252
+ ## Migration
253
+
254
+ If you previously passed security config to `dash()`, move it to `sentinel()`:
255
+
256
+ ```ts
257
+ // before
258
+ dash({
259
+ apiKey: process.env.BETTER_AUTH_API_KEY,
260
+ // no longer supported in dash
261
+ security: {
262
+ credentialStuffing: { enabled: true },
263
+ },
264
+ });
265
+
266
+ // after
267
+ dash({ apiKey: process.env.BETTER_AUTH_API_KEY });
268
+ sentinel({
269
+ apiKey: process.env.BETTER_AUTH_API_KEY,
270
+ security: {
271
+ credentialStuffing: { enabled: true },
272
+ },
273
+ });
274
+ ```
275
+
276
+ ## Security Notes
277
+
278
+ - Use `sentinel()` to enforce security checks. `dash()` is focused on telemetry/admin behavior.
279
+ - Provide `BETTER_AUTH_API_KEY`; without it, sentinel cannot securely call infra APIs and will warn at startup.
280
+ - If you run behind a proxy/CDN, validate your upstream header trust model (`x-forwarded-for`, etc.) to avoid spoofed client IP attribution.
@@ -0,0 +1,113 @@
1
+ import * as _better_fetch_fetch0 from "@better-fetch/fetch";
2
+
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;
14
+ city?: string;
15
+ country?: string;
16
+ countryCode?: string;
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: DashAuditLogsResponse;
56
+ error: null;
57
+ } | {
58
+ data: null;
59
+ error: {
60
+ message?: string | undefined;
61
+ status: number;
62
+ statusText: string;
63
+ };
64
+ }>;
65
+ };
66
+ };
67
+ pathMethods: {
68
+ "/events/audit-logs": "GET";
69
+ };
70
+ };
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
+ //#endregion
113
+ export { DashAuditLog, DashAuditLogsResponse, DashClientOptions, DashGetAuditLogsInput, SentinelClientOptions, dashClient, sentinelClient };