@beignet/provider-auth-better-auth 0.0.1
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/CHANGELOG.md +5 -0
- package/README.md +426 -0
- package/dist/index.d.ts +130 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +225 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
- package/src/index.ts +274 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
# @beignet/provider-auth-better-auth
|
|
2
|
+
|
|
3
|
+
Better Auth provider for Beignet applications.
|
|
4
|
+
|
|
5
|
+
The provider wraps an already-configured [Better Auth](https://better-auth.com)
|
|
6
|
+
server instance and exposes the shared `AuthPort` from `@beignet/core/ports` on
|
|
7
|
+
`ctx.ports.auth`. Your app still owns Better Auth configuration, database
|
|
8
|
+
schema, and auth routes.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
**What this provider does:**
|
|
13
|
+
|
|
14
|
+
- Wraps a Better Auth instance with a simple, stable API
|
|
15
|
+
- Extends `ports.auth` with `getSession`, `getUser`, and `requireUser` methods
|
|
16
|
+
- Maintains type safety for your custom User type
|
|
17
|
+
- Records auth checks in devtools when devtools is installed
|
|
18
|
+
|
|
19
|
+
**What this provider does NOT do:**
|
|
20
|
+
|
|
21
|
+
- Define database schema (you own your user table)
|
|
22
|
+
- Define the User type (you define it in your app)
|
|
23
|
+
- Configure Better Auth (secrets, session strategy, etc. stay in your app)
|
|
24
|
+
- Implement login/signup routes (use Better Auth's routes directly)
|
|
25
|
+
- Manage RBAC/permissions (that's application logic)
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bun add @beignet/core @beignet/provider-auth-better-auth better-auth
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Setup
|
|
34
|
+
|
|
35
|
+
### 1. Configure Better Auth in your app
|
|
36
|
+
|
|
37
|
+
First, set up Better Auth with your database and configuration:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// lib/better-auth.ts
|
|
41
|
+
import { betterAuth } from "better-auth";
|
|
42
|
+
import { db } from "./db"; // Your Drizzle/Prisma/etc. client
|
|
43
|
+
|
|
44
|
+
export const auth = betterAuth({
|
|
45
|
+
database: db,
|
|
46
|
+
emailAndPassword: {
|
|
47
|
+
enabled: true,
|
|
48
|
+
},
|
|
49
|
+
// ...other Better Auth configuration
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. Define your ports type
|
|
54
|
+
|
|
55
|
+
Own the public session shape in your app, then add the `auth` port to your
|
|
56
|
+
application's ports type:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
// ports/auth.ts
|
|
60
|
+
import type { AuthPort as BeignetAuthPort } from "@beignet/core/ports";
|
|
61
|
+
|
|
62
|
+
export type AuthUser = {
|
|
63
|
+
id: string;
|
|
64
|
+
name?: string | null;
|
|
65
|
+
email?: string | null;
|
|
66
|
+
image?: string | null;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type AuthSessionMetadata = unknown;
|
|
70
|
+
|
|
71
|
+
export type AuthPort = BeignetAuthPort<AuthUser, AuthSessionMetadata>;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// ports/index.ts
|
|
76
|
+
import type { AuthPort } from "./auth";
|
|
77
|
+
|
|
78
|
+
export type AppPorts = {
|
|
79
|
+
auth: AuthPort;
|
|
80
|
+
// ...other ports (db, mailer, eventBus, etc.)
|
|
81
|
+
};
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3. Wire the provider into Beignet
|
|
85
|
+
|
|
86
|
+
Register the provider when creating your server:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// server/providers.ts
|
|
90
|
+
import { createAuthBetterAuthProvider } from "@beignet/provider-auth-better-auth";
|
|
91
|
+
import { auth } from "@/lib/better-auth";
|
|
92
|
+
|
|
93
|
+
export const providers = [
|
|
94
|
+
createAuthBetterAuthProvider(auth),
|
|
95
|
+
// ...other providers
|
|
96
|
+
];
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// server/index.ts
|
|
101
|
+
import { createNextServer } from "@beignet/next";
|
|
102
|
+
import { definePorts } from "@beignet/core/ports";
|
|
103
|
+
import { routes } from "@/server/routes";
|
|
104
|
+
import { providers } from "./providers";
|
|
105
|
+
|
|
106
|
+
const appPorts = definePorts({
|
|
107
|
+
// add your app's other ports here
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
export const server = await createNextServer({
|
|
111
|
+
ports: appPorts,
|
|
112
|
+
providers,
|
|
113
|
+
createContext: async ({ ports }) => ({ ports }),
|
|
114
|
+
routes,
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 4. Use the auth port in auth hooks
|
|
119
|
+
|
|
120
|
+
Use `createAuthHooks(...)` to protect routes that declare
|
|
121
|
+
`.meta({ auth: "required" })`:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
// server/auth-hooks.ts
|
|
125
|
+
import { createAuthHooks } from "@beignet/core/server";
|
|
126
|
+
|
|
127
|
+
export const authHooks = createAuthHooks<AppContext>({
|
|
128
|
+
assign: ({ ctx, session }) => ({
|
|
129
|
+
...ctx,
|
|
130
|
+
auth: session,
|
|
131
|
+
user: session?.user ?? null,
|
|
132
|
+
}),
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Then register it on your server:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const server = await createNextServer({
|
|
140
|
+
ports: appPorts,
|
|
141
|
+
providers: [createAuthBetterAuthProvider(auth)],
|
|
142
|
+
hooks: [authHooks],
|
|
143
|
+
createContext: async ({ ports }) => ({
|
|
144
|
+
ports,
|
|
145
|
+
auth: null,
|
|
146
|
+
user: null,
|
|
147
|
+
}),
|
|
148
|
+
routes,
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 5. Optional: check auth in use cases
|
|
153
|
+
|
|
154
|
+
You can also check authentication in use cases:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
// features/users/use-cases/get-profile.ts
|
|
158
|
+
import { createUseCase } from "@beignet/core/application";
|
|
159
|
+
import { z } from "zod";
|
|
160
|
+
import { requireUser } from "@/lib/auth";
|
|
161
|
+
|
|
162
|
+
const UserProfileSchema = z.object({
|
|
163
|
+
id: z.string(),
|
|
164
|
+
email: z.string().email(),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const useCase = createUseCase<AppCtx>();
|
|
168
|
+
|
|
169
|
+
export const getUserProfile = useCase
|
|
170
|
+
.query("users.profile")
|
|
171
|
+
.input(z.object({ userId: z.string() }))
|
|
172
|
+
.output(UserProfileSchema)
|
|
173
|
+
.run(async ({ ctx, input }) => {
|
|
174
|
+
requireUser(ctx);
|
|
175
|
+
|
|
176
|
+
return ctx.ports.db.users.getProfile(input.userId);
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
In the standard app shape, `createContext` reads the request once with
|
|
181
|
+
`ctx.ports.auth.getSession(req)` and stores the result on `ctx.auth`. Use cases
|
|
182
|
+
then call an app-owned helper such as `requireUser(ctx)` instead of depending
|
|
183
|
+
on the raw request.
|
|
184
|
+
|
|
185
|
+
## Devtools
|
|
186
|
+
|
|
187
|
+
When `@beignet/devtools` is installed before this provider, auth checks
|
|
188
|
+
appear under the dashboard's Auth watcher.
|
|
189
|
+
|
|
190
|
+
The provider records `auth.getSession`, `auth.getUser`, and
|
|
191
|
+
`auth.requireUser` events with the operation, authenticated status, and
|
|
192
|
+
duration. User and session objects are not recorded. Provider failures are
|
|
193
|
+
recorded with `.failed` event names and the original error is rethrown.
|
|
194
|
+
|
|
195
|
+
## API reference
|
|
196
|
+
|
|
197
|
+
### `AuthPort<User, Session>`
|
|
198
|
+
|
|
199
|
+
The provider implements the auth port interface exported by
|
|
200
|
+
`@beignet/core/ports`:
|
|
201
|
+
|
|
202
|
+
#### `getSession(req: Request): Promise<AuthSession<User, Session> | null>`
|
|
203
|
+
|
|
204
|
+
Get the current session from a Request. Returns `null` if not authenticated.
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
const session = await ctx.ports.auth.getSession(req);
|
|
208
|
+
if (session) {
|
|
209
|
+
console.log(session.user);
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
#### `getUser(req: Request): Promise<User | null>`
|
|
214
|
+
|
|
215
|
+
Get the current user from a Request. Returns `null` if not authenticated.
|
|
216
|
+
|
|
217
|
+
This is a convenience method that extracts the user from the session.
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
const user = await ctx.ports.auth.getUser(req);
|
|
221
|
+
if (user) {
|
|
222
|
+
console.log(user.email);
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### `requireUser(req: Request): Promise<User>`
|
|
227
|
+
|
|
228
|
+
Require an authenticated user. Throws an error if not authenticated.
|
|
229
|
+
|
|
230
|
+
Use this in lifecycle hooks or use cases that require authentication.
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
const user = await ctx.ports.auth.requireUser(req);
|
|
234
|
+
// user is guaranteed to exist here
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Throws:** `AuthUnauthorizedError` from `@beignet/core/ports` if not
|
|
238
|
+
authenticated. When this error reaches Beignet's server runtime, it is
|
|
239
|
+
returned as a framework-owned `401` response with the standard error envelope.
|
|
240
|
+
|
|
241
|
+
### `AuthSession<User, Session>`
|
|
242
|
+
|
|
243
|
+
Represents an authenticated session:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
interface AuthSession<User = unknown, Session = unknown> {
|
|
247
|
+
user: User;
|
|
248
|
+
session?: Session;
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### `createAuthBetterAuthProvider(auth)`
|
|
253
|
+
|
|
254
|
+
Factory function that creates the provider:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
function createAuthBetterAuthProvider<User = unknown, Session = unknown>(
|
|
258
|
+
auth: BetterAuthServer<User, Session>
|
|
259
|
+
): ServiceProvider
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Parameters:**
|
|
263
|
+
- `auth`: A Better Auth server instance configured in your application
|
|
264
|
+
|
|
265
|
+
**Returns:** A Beignet provider that can be registered with the server
|
|
266
|
+
|
|
267
|
+
## Advanced usage
|
|
268
|
+
|
|
269
|
+
### Metadata-driven authentication
|
|
270
|
+
|
|
271
|
+
Use `createAuthHooks(...)` from `@beignet/core/server` to enforce
|
|
272
|
+
contract authentication metadata through the shared auth port:
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
// Define a contract with auth metadata
|
|
276
|
+
const users = createContractGroup();
|
|
277
|
+
|
|
278
|
+
const getProfile = users
|
|
279
|
+
.get("/api/profile")
|
|
280
|
+
.responses({
|
|
281
|
+
200: z.object({ name: z.string() }),
|
|
282
|
+
})
|
|
283
|
+
.meta({ auth: "required" });
|
|
284
|
+
|
|
285
|
+
const authHooks = createAuthHooks<AppContext>({
|
|
286
|
+
assign: ({ ctx, session }) => ({
|
|
287
|
+
...ctx,
|
|
288
|
+
auth: session,
|
|
289
|
+
user: session?.user ?? null,
|
|
290
|
+
}),
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Custom error types
|
|
295
|
+
|
|
296
|
+
You can wrap `requireUser` to throw custom error types:
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
class UnauthorizedError extends Error {
|
|
300
|
+
constructor() {
|
|
301
|
+
super("Unauthorized");
|
|
302
|
+
this.name = "UnauthorizedError";
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export const requireAuth = async (
|
|
307
|
+
req: Request,
|
|
308
|
+
auth: { requireUser: (req: Request) => Promise<unknown> },
|
|
309
|
+
) => {
|
|
310
|
+
try {
|
|
311
|
+
return await auth.requireUser(req);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
throw new UnauthorizedError();
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Multiple authentication strategies
|
|
319
|
+
|
|
320
|
+
If you need multiple auth strategies (e.g., JWT + session), you can:
|
|
321
|
+
|
|
322
|
+
1. Configure Better Auth with multiple strategies
|
|
323
|
+
2. Or create multiple providers (e.g., `createAuthBetterAuthProvider(sessionAuth)` + `createAuthJWTProvider(jwtAuth)`)
|
|
324
|
+
|
|
325
|
+
Better Auth supports multiple strategies out of the box, so the first approach is recommended.
|
|
326
|
+
|
|
327
|
+
## Type safety
|
|
328
|
+
|
|
329
|
+
The provider maintains full type safety for your custom User type:
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
type MyUser = {
|
|
333
|
+
id: string;
|
|
334
|
+
email: string;
|
|
335
|
+
role: "admin" | "user";
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const authProvider = createAuthBetterAuthProvider<MyUser>(auth);
|
|
339
|
+
|
|
340
|
+
// Later, in your routes:
|
|
341
|
+
const user = await ctx.ports.auth.requireUser(req);
|
|
342
|
+
// user.role is typed as "admin" | "user"
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Integration with Better Auth routes
|
|
346
|
+
|
|
347
|
+
Better Auth provides its own route handlers for login, signup, etc. You can mount these alongside your Beignet routes:
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
// Next.js App Router example
|
|
351
|
+
import { auth } from "@/lib/better-auth";
|
|
352
|
+
|
|
353
|
+
// Better Auth handles /api/auth/*
|
|
354
|
+
export const { GET, POST } = auth.handler;
|
|
355
|
+
|
|
356
|
+
// Your Beignet routes handle /api/app/*
|
|
357
|
+
// (mounted separately)
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
See the [Better Auth documentation](https://better-auth.com) for details on route configuration.
|
|
361
|
+
|
|
362
|
+
## Examples
|
|
363
|
+
|
|
364
|
+
### Basic setup
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
import { betterAuth } from "better-auth";
|
|
368
|
+
import { createNextServer } from "@beignet/next";
|
|
369
|
+
import { definePorts } from "@beignet/core/ports";
|
|
370
|
+
import { createAuthBetterAuthProvider } from "@beignet/provider-auth-better-auth";
|
|
371
|
+
import { routes } from "@/server/routes";
|
|
372
|
+
|
|
373
|
+
const auth = betterAuth({ database: db });
|
|
374
|
+
const appPorts = definePorts({});
|
|
375
|
+
|
|
376
|
+
const server = await createNextServer({
|
|
377
|
+
ports: appPorts,
|
|
378
|
+
providers: [createAuthBetterAuthProvider(auth)],
|
|
379
|
+
createContext: async ({ ports }) => ({ ports }),
|
|
380
|
+
routes,
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Protecting routes
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
const authHook = {
|
|
388
|
+
name: "auth",
|
|
389
|
+
beforeHandle: async ({ req, ctx }) => {
|
|
390
|
+
const user = await ctx.ports.auth.requireUser(req);
|
|
391
|
+
return { ctx: { ...ctx, user } };
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const server = await createNextServer({
|
|
396
|
+
ports: appPorts,
|
|
397
|
+
providers: [createAuthBetterAuthProvider(auth)],
|
|
398
|
+
hooks: [authHook],
|
|
399
|
+
createContext: async ({ ports }) => ({ ports }),
|
|
400
|
+
routes,
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Optional authentication
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
const listData = async ({ req, ctx }) => {
|
|
408
|
+
const user = await ctx.ports.auth.getUser(req);
|
|
409
|
+
|
|
410
|
+
if (user) {
|
|
411
|
+
return {
|
|
412
|
+
status: 200,
|
|
413
|
+
body: { data: getPersonalizedData(user) },
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
status: 200,
|
|
419
|
+
body: { data: getPublicData() },
|
|
420
|
+
};
|
|
421
|
+
};
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
## License
|
|
425
|
+
|
|
426
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @beignet/provider-auth-better-auth
|
|
3
|
+
*
|
|
4
|
+
* Better Auth provider that extends ports with authentication capabilities.
|
|
5
|
+
* This provider wraps an already-configured Better Auth server instance
|
|
6
|
+
* and exposes the shared Beignet AuthPort on ctx.ports.auth.
|
|
7
|
+
*
|
|
8
|
+
* The provider does NOT own:
|
|
9
|
+
* - Database schema
|
|
10
|
+
* - User type definition
|
|
11
|
+
* - Better Auth configuration (secrets, session strategy, etc.)
|
|
12
|
+
*
|
|
13
|
+
* Those stay in the application layer. This provider simply wires
|
|
14
|
+
* an existing Better Auth instance into the Beignet framework.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // 1. Configure Better Auth in your app
|
|
19
|
+
* import { betterAuth } from "better-auth";
|
|
20
|
+
* import { db } from "./db";
|
|
21
|
+
*
|
|
22
|
+
* export const auth = betterAuth({
|
|
23
|
+
* database: db,
|
|
24
|
+
* // ...other Better Auth config
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // Own this shape in your app instead of exporting provider-inferred types.
|
|
28
|
+
* export type AuthUser = {
|
|
29
|
+
* id: string;
|
|
30
|
+
* name?: string | null;
|
|
31
|
+
* email?: string | null;
|
|
32
|
+
* image?: string | null;
|
|
33
|
+
* };
|
|
34
|
+
*
|
|
35
|
+
* // 2. Define your ports type
|
|
36
|
+
* import type { AuthPort } from "@beignet/core/ports";
|
|
37
|
+
*
|
|
38
|
+
* export type AppPorts = {
|
|
39
|
+
* auth: AuthPort<AuthUser>;
|
|
40
|
+
* // ...other ports
|
|
41
|
+
* };
|
|
42
|
+
*
|
|
43
|
+
* // 3. Wire the provider into Beignet
|
|
44
|
+
* import { createNextServer } from "@beignet/next";
|
|
45
|
+
* import { createAuthBetterAuthProvider } from "@beignet/provider-auth-better-auth";
|
|
46
|
+
*
|
|
47
|
+
* const server = await createNextServer({
|
|
48
|
+
* ports: basePorts,
|
|
49
|
+
* providers: [
|
|
50
|
+
* createAuthBetterAuthProvider(auth),
|
|
51
|
+
* // ...other providers
|
|
52
|
+
* ],
|
|
53
|
+
* });
|
|
54
|
+
*
|
|
55
|
+
* // 4. Use the auth port through createAuthHooks(...)
|
|
56
|
+
* import { createAuthHooks } from "@beignet/core/server";
|
|
57
|
+
*
|
|
58
|
+
* const authHooks = createAuthHooks<AppContext>({
|
|
59
|
+
* assign: ({ ctx, session }) => ({
|
|
60
|
+
* ...ctx,
|
|
61
|
+
* auth: session,
|
|
62
|
+
* user: session?.user ?? null,
|
|
63
|
+
* }),
|
|
64
|
+
* });
|
|
65
|
+
*
|
|
66
|
+
* const server = await createNextServer({
|
|
67
|
+
* ports: basePorts,
|
|
68
|
+
* providers: [createAuthBetterAuthProvider(auth)],
|
|
69
|
+
* hooks: [authHooks],
|
|
70
|
+
* createContext: async ({ ports }) => ({ ports, auth: null, user: null }),
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
import { type AuthPort, type AuthRequestLike } from "@beignet/core/ports";
|
|
75
|
+
/**
|
|
76
|
+
* Minimal type representing a Better Auth server instance.
|
|
77
|
+
* This is the interface we expect from the Better Auth library.
|
|
78
|
+
*
|
|
79
|
+
* Applications pass their configured Better Auth instance to
|
|
80
|
+
* createAuthBetterAuthProvider, which must have an api.getSession method.
|
|
81
|
+
*/
|
|
82
|
+
export type BetterAuthServer<User = unknown, Session = unknown> = {
|
|
83
|
+
/**
|
|
84
|
+
* Better Auth's session API.
|
|
85
|
+
* The exact shape depends on Better Auth's API version.
|
|
86
|
+
*/
|
|
87
|
+
api: {
|
|
88
|
+
/**
|
|
89
|
+
* Get the current session from a request.
|
|
90
|
+
* Better Auth returns null if not authenticated, or a session object
|
|
91
|
+
* containing user and session data.
|
|
92
|
+
*/
|
|
93
|
+
getSession(context: {
|
|
94
|
+
headers: Headers;
|
|
95
|
+
}): Promise<{
|
|
96
|
+
user: User;
|
|
97
|
+
session: Session;
|
|
98
|
+
} | null>;
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Create a Beignet provider for Better Auth.
|
|
103
|
+
*
|
|
104
|
+
* This factory accepts an already-configured Better Auth server instance
|
|
105
|
+
* and returns a provider that can be registered with Beignet.
|
|
106
|
+
*
|
|
107
|
+
* The provider extends ports.auth with an AuthPort that wraps the
|
|
108
|
+
* Better Auth instance with a simple, stable API.
|
|
109
|
+
*
|
|
110
|
+
* @param auth - A Better Auth server instance configured in the application
|
|
111
|
+
* @returns A Beignet provider that extends ports.auth
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* import { betterAuth } from "better-auth";
|
|
116
|
+
* import { createAuthBetterAuthProvider } from "@beignet/provider-auth-better-auth";
|
|
117
|
+
* import { createNextServer } from "@beignet/next";
|
|
118
|
+
*
|
|
119
|
+
* const auth = betterAuth({ database: db });
|
|
120
|
+
*
|
|
121
|
+
* const server = await createNextServer({
|
|
122
|
+
* ports: basePorts,
|
|
123
|
+
* providers: [createAuthBetterAuthProvider(auth)],
|
|
124
|
+
* });
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export declare function createAuthBetterAuthProvider<User = unknown, Session = unknown>(auth: BetterAuthServer<User, Session>): import("@beignet/core/providers").ServiceProvider<unknown, import("@standard-schema/spec").StandardSchemaV1<void, void>, {
|
|
128
|
+
auth: AuthPort<User, Session, AuthRequestLike>;
|
|
129
|
+
}>;
|
|
130
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AAEH,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,eAAe,EAErB,MAAM,qBAAqB,CAAC;AAM7B;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,CAAC,IAAI,GAAG,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI;IAChE;;;OAGG;IACH,GAAG,EAAE;QACH;;;;WAIG;QACH,UAAU,CAAC,OAAO,EAAE;YAClB,OAAO,EAAE,OAAO,CAAC;SAClB,GAAG,OAAO,CAAC;YAAE,IAAI,EAAE,IAAI,CAAC;YAAC,OAAO,EAAE,OAAO,CAAA;SAAE,GAAG,IAAI,CAAC,CAAC;KACtD,CAAC;CACH,CAAC;AAMF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,GAAG,OAAO,EAAE,OAAO,GAAG,OAAO,EAC5E,IAAI,EAAE,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC;;GAsItC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @beignet/provider-auth-better-auth
|
|
3
|
+
*
|
|
4
|
+
* Better Auth provider that extends ports with authentication capabilities.
|
|
5
|
+
* This provider wraps an already-configured Better Auth server instance
|
|
6
|
+
* and exposes the shared Beignet AuthPort on ctx.ports.auth.
|
|
7
|
+
*
|
|
8
|
+
* The provider does NOT own:
|
|
9
|
+
* - Database schema
|
|
10
|
+
* - User type definition
|
|
11
|
+
* - Better Auth configuration (secrets, session strategy, etc.)
|
|
12
|
+
*
|
|
13
|
+
* Those stay in the application layer. This provider simply wires
|
|
14
|
+
* an existing Better Auth instance into the Beignet framework.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // 1. Configure Better Auth in your app
|
|
19
|
+
* import { betterAuth } from "better-auth";
|
|
20
|
+
* import { db } from "./db";
|
|
21
|
+
*
|
|
22
|
+
* export const auth = betterAuth({
|
|
23
|
+
* database: db,
|
|
24
|
+
* // ...other Better Auth config
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // Own this shape in your app instead of exporting provider-inferred types.
|
|
28
|
+
* export type AuthUser = {
|
|
29
|
+
* id: string;
|
|
30
|
+
* name?: string | null;
|
|
31
|
+
* email?: string | null;
|
|
32
|
+
* image?: string | null;
|
|
33
|
+
* };
|
|
34
|
+
*
|
|
35
|
+
* // 2. Define your ports type
|
|
36
|
+
* import type { AuthPort } from "@beignet/core/ports";
|
|
37
|
+
*
|
|
38
|
+
* export type AppPorts = {
|
|
39
|
+
* auth: AuthPort<AuthUser>;
|
|
40
|
+
* // ...other ports
|
|
41
|
+
* };
|
|
42
|
+
*
|
|
43
|
+
* // 3. Wire the provider into Beignet
|
|
44
|
+
* import { createNextServer } from "@beignet/next";
|
|
45
|
+
* import { createAuthBetterAuthProvider } from "@beignet/provider-auth-better-auth";
|
|
46
|
+
*
|
|
47
|
+
* const server = await createNextServer({
|
|
48
|
+
* ports: basePorts,
|
|
49
|
+
* providers: [
|
|
50
|
+
* createAuthBetterAuthProvider(auth),
|
|
51
|
+
* // ...other providers
|
|
52
|
+
* ],
|
|
53
|
+
* });
|
|
54
|
+
*
|
|
55
|
+
* // 4. Use the auth port through createAuthHooks(...)
|
|
56
|
+
* import { createAuthHooks } from "@beignet/core/server";
|
|
57
|
+
*
|
|
58
|
+
* const authHooks = createAuthHooks<AppContext>({
|
|
59
|
+
* assign: ({ ctx, session }) => ({
|
|
60
|
+
* ...ctx,
|
|
61
|
+
* auth: session,
|
|
62
|
+
* user: session?.user ?? null,
|
|
63
|
+
* }),
|
|
64
|
+
* });
|
|
65
|
+
*
|
|
66
|
+
* const server = await createNextServer({
|
|
67
|
+
* ports: basePorts,
|
|
68
|
+
* providers: [createAuthBetterAuthProvider(auth)],
|
|
69
|
+
* hooks: [authHooks],
|
|
70
|
+
* createContext: async ({ ports }) => ({ ports, auth: null, user: null }),
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
import { AuthUnauthorizedError, } from "@beignet/core/ports";
|
|
75
|
+
import { createProvider, createProviderInstrumentation, } from "@beignet/core/providers";
|
|
76
|
+
function errorMessage(error) {
|
|
77
|
+
return error instanceof Error ? error.message : String(error);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Create a Beignet provider for Better Auth.
|
|
81
|
+
*
|
|
82
|
+
* This factory accepts an already-configured Better Auth server instance
|
|
83
|
+
* and returns a provider that can be registered with Beignet.
|
|
84
|
+
*
|
|
85
|
+
* The provider extends ports.auth with an AuthPort that wraps the
|
|
86
|
+
* Better Auth instance with a simple, stable API.
|
|
87
|
+
*
|
|
88
|
+
* @param auth - A Better Auth server instance configured in the application
|
|
89
|
+
* @returns A Beignet provider that extends ports.auth
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* import { betterAuth } from "better-auth";
|
|
94
|
+
* import { createAuthBetterAuthProvider } from "@beignet/provider-auth-better-auth";
|
|
95
|
+
* import { createNextServer } from "@beignet/next";
|
|
96
|
+
*
|
|
97
|
+
* const auth = betterAuth({ database: db });
|
|
98
|
+
*
|
|
99
|
+
* const server = await createNextServer({
|
|
100
|
+
* ports: basePorts,
|
|
101
|
+
* providers: [createAuthBetterAuthProvider(auth)],
|
|
102
|
+
* });
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export function createAuthBetterAuthProvider(auth) {
|
|
106
|
+
return createProvider({
|
|
107
|
+
name: "auth-better-auth",
|
|
108
|
+
async setup({ ports }) {
|
|
109
|
+
const instrumentation = createProviderInstrumentation(ports, {
|
|
110
|
+
providerName: "auth-better-auth",
|
|
111
|
+
watcher: "auth",
|
|
112
|
+
});
|
|
113
|
+
async function resolveSession(req) {
|
|
114
|
+
const session = await auth.api.getSession({
|
|
115
|
+
headers: req.headers,
|
|
116
|
+
});
|
|
117
|
+
if (!session) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
user: session.user,
|
|
122
|
+
session: session.session,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function recordAuthEvent(event) {
|
|
126
|
+
instrumentation.custom({
|
|
127
|
+
name: `auth.${event.operation}`,
|
|
128
|
+
label: `Auth ${event.operation}`,
|
|
129
|
+
summary: event.summary,
|
|
130
|
+
details: {
|
|
131
|
+
operation: event.operation,
|
|
132
|
+
authenticated: event.authenticated,
|
|
133
|
+
durationMs: event.durationMs,
|
|
134
|
+
...(event.error ? { error: event.error } : {}),
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const authPort = {
|
|
139
|
+
async getSession(req) {
|
|
140
|
+
const startedAt = Date.now();
|
|
141
|
+
try {
|
|
142
|
+
const session = await resolveSession(req);
|
|
143
|
+
recordAuthEvent({
|
|
144
|
+
operation: "getSession",
|
|
145
|
+
authenticated: session != null,
|
|
146
|
+
summary: session ? "Session found" : "No session",
|
|
147
|
+
durationMs: Date.now() - startedAt,
|
|
148
|
+
});
|
|
149
|
+
return session;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
recordAuthEvent({
|
|
153
|
+
operation: "getSession.failed",
|
|
154
|
+
authenticated: false,
|
|
155
|
+
summary: "Session lookup failed",
|
|
156
|
+
durationMs: Date.now() - startedAt,
|
|
157
|
+
error: errorMessage(error),
|
|
158
|
+
});
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
async getUser(req) {
|
|
163
|
+
const startedAt = Date.now();
|
|
164
|
+
try {
|
|
165
|
+
const session = await resolveSession(req);
|
|
166
|
+
recordAuthEvent({
|
|
167
|
+
operation: "getUser",
|
|
168
|
+
authenticated: session != null,
|
|
169
|
+
summary: session ? "User found" : "No user",
|
|
170
|
+
durationMs: Date.now() - startedAt,
|
|
171
|
+
});
|
|
172
|
+
return session?.user ?? null;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
recordAuthEvent({
|
|
176
|
+
operation: "getUser.failed",
|
|
177
|
+
authenticated: false,
|
|
178
|
+
summary: "User lookup failed",
|
|
179
|
+
durationMs: Date.now() - startedAt,
|
|
180
|
+
error: errorMessage(error),
|
|
181
|
+
});
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
async requireUser(req) {
|
|
186
|
+
const startedAt = Date.now();
|
|
187
|
+
try {
|
|
188
|
+
const session = await resolveSession(req);
|
|
189
|
+
if (!session) {
|
|
190
|
+
recordAuthEvent({
|
|
191
|
+
operation: "requireUser",
|
|
192
|
+
authenticated: false,
|
|
193
|
+
summary: "Unauthorized",
|
|
194
|
+
durationMs: Date.now() - startedAt,
|
|
195
|
+
});
|
|
196
|
+
throw new AuthUnauthorizedError();
|
|
197
|
+
}
|
|
198
|
+
recordAuthEvent({
|
|
199
|
+
operation: "requireUser",
|
|
200
|
+
authenticated: true,
|
|
201
|
+
summary: "Authorized",
|
|
202
|
+
durationMs: Date.now() - startedAt,
|
|
203
|
+
});
|
|
204
|
+
return session.user;
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (error instanceof AuthUnauthorizedError) {
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
recordAuthEvent({
|
|
211
|
+
operation: "requireUser.failed",
|
|
212
|
+
authenticated: false,
|
|
213
|
+
summary: "Required user lookup failed",
|
|
214
|
+
durationMs: Date.now() - startedAt,
|
|
215
|
+
error: errorMessage(error),
|
|
216
|
+
});
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
return { ports: { auth: authPort } };
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@beignet/provider-auth-better-auth",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Better Auth provider for Beignet - adds auth port for authentication and session management",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src",
|
|
17
|
+
"!src/**/*.test.ts",
|
|
18
|
+
"!src/**/*.test.tsx",
|
|
19
|
+
"!src/**/*.test-d.ts",
|
|
20
|
+
"README.md",
|
|
21
|
+
"CHANGELOG.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"dev": "tsc --watch",
|
|
26
|
+
"clean": "rm -rf dist coverage .turbo",
|
|
27
|
+
"test": "bun test",
|
|
28
|
+
"test:coverage": "bun test --coverage",
|
|
29
|
+
"lint": "biome check ."
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"beignet",
|
|
33
|
+
"auth",
|
|
34
|
+
"better-auth",
|
|
35
|
+
"provider",
|
|
36
|
+
"authentication",
|
|
37
|
+
"session",
|
|
38
|
+
"ports"
|
|
39
|
+
],
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/taylorbryant/beignet.git",
|
|
44
|
+
"directory": "packages/provider-auth-better-auth"
|
|
45
|
+
},
|
|
46
|
+
"author": "Taylor Bryant",
|
|
47
|
+
"homepage": "https://github.com/taylorbryant/beignet#readme",
|
|
48
|
+
"bugs": "https://github.com/taylorbryant/beignet/issues",
|
|
49
|
+
"sideEffects": false,
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=18.0.0"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"better-auth": "^1.3.26"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@beignet/core": "*"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@beignet/devtools": "*",
|
|
64
|
+
"@types/bun": "^1.3.13",
|
|
65
|
+
"@types/node": "^20.10.0",
|
|
66
|
+
"better-auth": "^1.4.6",
|
|
67
|
+
"typescript": "^5.3.0"
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @beignet/provider-auth-better-auth
|
|
3
|
+
*
|
|
4
|
+
* Better Auth provider that extends ports with authentication capabilities.
|
|
5
|
+
* This provider wraps an already-configured Better Auth server instance
|
|
6
|
+
* and exposes the shared Beignet AuthPort on ctx.ports.auth.
|
|
7
|
+
*
|
|
8
|
+
* The provider does NOT own:
|
|
9
|
+
* - Database schema
|
|
10
|
+
* - User type definition
|
|
11
|
+
* - Better Auth configuration (secrets, session strategy, etc.)
|
|
12
|
+
*
|
|
13
|
+
* Those stay in the application layer. This provider simply wires
|
|
14
|
+
* an existing Better Auth instance into the Beignet framework.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // 1. Configure Better Auth in your app
|
|
19
|
+
* import { betterAuth } from "better-auth";
|
|
20
|
+
* import { db } from "./db";
|
|
21
|
+
*
|
|
22
|
+
* export const auth = betterAuth({
|
|
23
|
+
* database: db,
|
|
24
|
+
* // ...other Better Auth config
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // Own this shape in your app instead of exporting provider-inferred types.
|
|
28
|
+
* export type AuthUser = {
|
|
29
|
+
* id: string;
|
|
30
|
+
* name?: string | null;
|
|
31
|
+
* email?: string | null;
|
|
32
|
+
* image?: string | null;
|
|
33
|
+
* };
|
|
34
|
+
*
|
|
35
|
+
* // 2. Define your ports type
|
|
36
|
+
* import type { AuthPort } from "@beignet/core/ports";
|
|
37
|
+
*
|
|
38
|
+
* export type AppPorts = {
|
|
39
|
+
* auth: AuthPort<AuthUser>;
|
|
40
|
+
* // ...other ports
|
|
41
|
+
* };
|
|
42
|
+
*
|
|
43
|
+
* // 3. Wire the provider into Beignet
|
|
44
|
+
* import { createNextServer } from "@beignet/next";
|
|
45
|
+
* import { createAuthBetterAuthProvider } from "@beignet/provider-auth-better-auth";
|
|
46
|
+
*
|
|
47
|
+
* const server = await createNextServer({
|
|
48
|
+
* ports: basePorts,
|
|
49
|
+
* providers: [
|
|
50
|
+
* createAuthBetterAuthProvider(auth),
|
|
51
|
+
* // ...other providers
|
|
52
|
+
* ],
|
|
53
|
+
* });
|
|
54
|
+
*
|
|
55
|
+
* // 4. Use the auth port through createAuthHooks(...)
|
|
56
|
+
* import { createAuthHooks } from "@beignet/core/server";
|
|
57
|
+
*
|
|
58
|
+
* const authHooks = createAuthHooks<AppContext>({
|
|
59
|
+
* assign: ({ ctx, session }) => ({
|
|
60
|
+
* ...ctx,
|
|
61
|
+
* auth: session,
|
|
62
|
+
* user: session?.user ?? null,
|
|
63
|
+
* }),
|
|
64
|
+
* });
|
|
65
|
+
*
|
|
66
|
+
* const server = await createNextServer({
|
|
67
|
+
* ports: basePorts,
|
|
68
|
+
* providers: [createAuthBetterAuthProvider(auth)],
|
|
69
|
+
* hooks: [authHooks],
|
|
70
|
+
* createContext: async ({ ports }) => ({ ports, auth: null, user: null }),
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
import {
|
|
76
|
+
type AuthPort,
|
|
77
|
+
type AuthRequestLike,
|
|
78
|
+
AuthUnauthorizedError,
|
|
79
|
+
} from "@beignet/core/ports";
|
|
80
|
+
import {
|
|
81
|
+
createProvider,
|
|
82
|
+
createProviderInstrumentation,
|
|
83
|
+
} from "@beignet/core/providers";
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Minimal type representing a Better Auth server instance.
|
|
87
|
+
* This is the interface we expect from the Better Auth library.
|
|
88
|
+
*
|
|
89
|
+
* Applications pass their configured Better Auth instance to
|
|
90
|
+
* createAuthBetterAuthProvider, which must have an api.getSession method.
|
|
91
|
+
*/
|
|
92
|
+
export type BetterAuthServer<User = unknown, Session = unknown> = {
|
|
93
|
+
/**
|
|
94
|
+
* Better Auth's session API.
|
|
95
|
+
* The exact shape depends on Better Auth's API version.
|
|
96
|
+
*/
|
|
97
|
+
api: {
|
|
98
|
+
/**
|
|
99
|
+
* Get the current session from a request.
|
|
100
|
+
* Better Auth returns null if not authenticated, or a session object
|
|
101
|
+
* containing user and session data.
|
|
102
|
+
*/
|
|
103
|
+
getSession(context: {
|
|
104
|
+
headers: Headers;
|
|
105
|
+
}): Promise<{ user: User; session: Session } | null>;
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
function errorMessage(error: unknown): string {
|
|
110
|
+
return error instanceof Error ? error.message : String(error);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create a Beignet provider for Better Auth.
|
|
115
|
+
*
|
|
116
|
+
* This factory accepts an already-configured Better Auth server instance
|
|
117
|
+
* and returns a provider that can be registered with Beignet.
|
|
118
|
+
*
|
|
119
|
+
* The provider extends ports.auth with an AuthPort that wraps the
|
|
120
|
+
* Better Auth instance with a simple, stable API.
|
|
121
|
+
*
|
|
122
|
+
* @param auth - A Better Auth server instance configured in the application
|
|
123
|
+
* @returns A Beignet provider that extends ports.auth
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* import { betterAuth } from "better-auth";
|
|
128
|
+
* import { createAuthBetterAuthProvider } from "@beignet/provider-auth-better-auth";
|
|
129
|
+
* import { createNextServer } from "@beignet/next";
|
|
130
|
+
*
|
|
131
|
+
* const auth = betterAuth({ database: db });
|
|
132
|
+
*
|
|
133
|
+
* const server = await createNextServer({
|
|
134
|
+
* ports: basePorts,
|
|
135
|
+
* providers: [createAuthBetterAuthProvider(auth)],
|
|
136
|
+
* });
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
export function createAuthBetterAuthProvider<User = unknown, Session = unknown>(
|
|
140
|
+
auth: BetterAuthServer<User, Session>,
|
|
141
|
+
) {
|
|
142
|
+
return createProvider({
|
|
143
|
+
name: "auth-better-auth",
|
|
144
|
+
|
|
145
|
+
async setup({ ports }) {
|
|
146
|
+
const instrumentation = createProviderInstrumentation(ports, {
|
|
147
|
+
providerName: "auth-better-auth",
|
|
148
|
+
watcher: "auth",
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
async function resolveSession(req: AuthRequestLike) {
|
|
152
|
+
const session = await auth.api.getSession({
|
|
153
|
+
headers: req.headers,
|
|
154
|
+
});
|
|
155
|
+
if (!session) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
user: session.user,
|
|
161
|
+
session: session.session,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function recordAuthEvent(event: {
|
|
166
|
+
operation: string;
|
|
167
|
+
authenticated: boolean;
|
|
168
|
+
summary: string;
|
|
169
|
+
durationMs: number;
|
|
170
|
+
error?: string;
|
|
171
|
+
}) {
|
|
172
|
+
instrumentation.custom({
|
|
173
|
+
name: `auth.${event.operation}`,
|
|
174
|
+
label: `Auth ${event.operation}`,
|
|
175
|
+
summary: event.summary,
|
|
176
|
+
details: {
|
|
177
|
+
operation: event.operation,
|
|
178
|
+
authenticated: event.authenticated,
|
|
179
|
+
durationMs: event.durationMs,
|
|
180
|
+
...(event.error ? { error: event.error } : {}),
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const authPort: AuthPort<User, Session> = {
|
|
186
|
+
async getSession(req) {
|
|
187
|
+
const startedAt = Date.now();
|
|
188
|
+
try {
|
|
189
|
+
const session = await resolveSession(req);
|
|
190
|
+
recordAuthEvent({
|
|
191
|
+
operation: "getSession",
|
|
192
|
+
authenticated: session != null,
|
|
193
|
+
summary: session ? "Session found" : "No session",
|
|
194
|
+
durationMs: Date.now() - startedAt,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return session;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
recordAuthEvent({
|
|
200
|
+
operation: "getSession.failed",
|
|
201
|
+
authenticated: false,
|
|
202
|
+
summary: "Session lookup failed",
|
|
203
|
+
durationMs: Date.now() - startedAt,
|
|
204
|
+
error: errorMessage(error),
|
|
205
|
+
});
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
async getUser(req) {
|
|
211
|
+
const startedAt = Date.now();
|
|
212
|
+
try {
|
|
213
|
+
const session = await resolveSession(req);
|
|
214
|
+
recordAuthEvent({
|
|
215
|
+
operation: "getUser",
|
|
216
|
+
authenticated: session != null,
|
|
217
|
+
summary: session ? "User found" : "No user",
|
|
218
|
+
durationMs: Date.now() - startedAt,
|
|
219
|
+
});
|
|
220
|
+
return session?.user ?? null;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
recordAuthEvent({
|
|
223
|
+
operation: "getUser.failed",
|
|
224
|
+
authenticated: false,
|
|
225
|
+
summary: "User lookup failed",
|
|
226
|
+
durationMs: Date.now() - startedAt,
|
|
227
|
+
error: errorMessage(error),
|
|
228
|
+
});
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
async requireUser(req) {
|
|
234
|
+
const startedAt = Date.now();
|
|
235
|
+
try {
|
|
236
|
+
const session = await resolveSession(req);
|
|
237
|
+
if (!session) {
|
|
238
|
+
recordAuthEvent({
|
|
239
|
+
operation: "requireUser",
|
|
240
|
+
authenticated: false,
|
|
241
|
+
summary: "Unauthorized",
|
|
242
|
+
durationMs: Date.now() - startedAt,
|
|
243
|
+
});
|
|
244
|
+
throw new AuthUnauthorizedError();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
recordAuthEvent({
|
|
248
|
+
operation: "requireUser",
|
|
249
|
+
authenticated: true,
|
|
250
|
+
summary: "Authorized",
|
|
251
|
+
durationMs: Date.now() - startedAt,
|
|
252
|
+
});
|
|
253
|
+
return session.user;
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (error instanceof AuthUnauthorizedError) {
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
recordAuthEvent({
|
|
260
|
+
operation: "requireUser.failed",
|
|
261
|
+
authenticated: false,
|
|
262
|
+
summary: "Required user lookup failed",
|
|
263
|
+
durationMs: Date.now() - startedAt,
|
|
264
|
+
error: errorMessage(error),
|
|
265
|
+
});
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
return { ports: { auth: authPort } };
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
}
|