@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 +280 -0
- package/dist/client.d.mts +113 -0
- package/dist/client.mjs +595 -0
- package/dist/email-BGxJ96Ky.mjs +128 -0
- package/dist/email.d.mts +202 -0
- package/dist/email.mjs +3 -0
- package/dist/index.d.mts +2743 -0
- package/dist/index.mjs +7509 -0
- package/package.json +74 -0
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 };
|