@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 ADDED
@@ -0,0 +1,5 @@
1
+ # @beignet/provider-auth-better-auth
2
+
3
+ ## 0.0.1
4
+
5
+ - Initial Beignet release under the `@beignet` npm scope.
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
@@ -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
+ }