@velajs/better-auth 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,228 @@
1
+ # @velajs/better-auth
2
+
3
+ [better-auth](https://www.better-auth.com/) integration for the [Vela](https://github.com/velajs/vela) framework. Edge-safe, fully DI-driven.
4
+
5
+ ```bash
6
+ pnpm add @velajs/better-auth better-auth
7
+ ```
8
+
9
+ ## Quick start
10
+
11
+ Construct your better-auth instance once, hand it to `BetterAuthModule.forRoot`, then use `AuthGuard` + `@CurrentUser()` like any other vela primitive.
12
+
13
+ ```ts
14
+ import { betterAuth } from 'better-auth';
15
+ import { Module, Controller, Get, UseGuards, VelaFactory } from '@velajs/vela';
16
+ import {
17
+ BetterAuthModule, AuthGuard, CurrentUser, Public,
18
+ } from '@velajs/better-auth';
19
+
20
+ const auth = betterAuth({
21
+ database: /* drizzle / prisma / kysely adapter */,
22
+ emailAndPassword: { enabled: true },
23
+ socialProviders: { github: { clientId, clientSecret } },
24
+ });
25
+
26
+ @Controller('/me')
27
+ @UseGuards(AuthGuard)
28
+ class MeController {
29
+ @Get() me(@CurrentUser() user: { id: string; email: string }) {
30
+ return { id: user.id, email: user.email };
31
+ }
32
+
33
+ @Get('/health') @Public(true)
34
+ health() { return { ok: true }; }
35
+ }
36
+
37
+ @Module({
38
+ imports: [BetterAuthModule.forRoot({ auth, isGlobal: true })],
39
+ controllers: [MeController],
40
+ })
41
+ class AppModule {}
42
+
43
+ const app = await VelaFactory.create(AppModule);
44
+ export default app; // edge-compatible (.fetch)
45
+ ```
46
+
47
+ `POST /api/auth/sign-up/email`, `POST /api/auth/sign-in/email`, `GET /api/auth/sign-in/social/github` and the rest of better-auth's surface are auto-mounted at `/api/auth/*`.
48
+
49
+ ## Module options
50
+
51
+ ```ts
52
+ BetterAuthModule.forRoot({
53
+ auth, // pre-constructed betterAuth({ ... }) instance
54
+ basePath: '/api/auth', // default — must match your better-auth config
55
+ isGlobal: false, // register AuthGuard as APP_GUARD (deny-by-default)
56
+ defaultPolicy: 'deny', // 'deny' | 'allow' for unauthenticated requests
57
+ mountHandler: true, // mount /api/auth/* catch-all controller
58
+ });
59
+ ```
60
+
61
+ ## Three composition patterns
62
+
63
+ ### Pattern A — inline (simplest)
64
+
65
+ ```ts
66
+ imports: [
67
+ BetterAuthModule.forRoot({
68
+ auth: betterAuth({ database, plugins: [magicLink({ sendMagicLink }), apiKey()] }),
69
+ isGlobal: true,
70
+ }),
71
+ ]
72
+ ```
73
+
74
+ ### Pattern B — DI'd plugin construction
75
+
76
+ `forRootAsync` lets vela services participate in your better-auth config. Required for Cloudflare D1 / Hyperdrive bindings, since env bindings only resolve at request time.
77
+
78
+ ```ts
79
+ imports: [
80
+ D1Module.forRoot({ binding: 'DB' }),
81
+ BetterAuthModule.forRootAsync({
82
+ inject: [D1Service, EmailService],
83
+ useFactory: (d1: D1Service, email: EmailService) => ({
84
+ auth: betterAuth({
85
+ database: drizzleAdapter(drizzle(d1.binding), { provider: 'sqlite' }),
86
+ plugins: [
87
+ magicLink({ sendMagicLink: (data) => email.send(data) }), // DI'd EmailService
88
+ apiKey(),
89
+ twoFactor(),
90
+ ],
91
+ }),
92
+ }),
93
+ isGlobal: true,
94
+ }),
95
+ ]
96
+ ```
97
+
98
+ ### Pattern C — modular plugin modules
99
+
100
+ Each feature ships a self-contained vela module that exports a plugin token. AppModule composes them like LEGO. Each module owns its own DI surface; only the public plugin token leaves the boundary.
101
+
102
+ ```ts
103
+ // magic-link-auth.module.ts
104
+ import { Module, InjectionToken } from '@velajs/vela';
105
+ import { magicLink } from 'better-auth/plugins';
106
+ import { EmailService } from './email.service';
107
+
108
+ export const MAGIC_LINK_PLUGIN = new InjectionToken<ReturnType<typeof magicLink>>(
109
+ 'app.MagicLinkPlugin',
110
+ );
111
+
112
+ @Module({
113
+ providers: [
114
+ EmailService,
115
+ {
116
+ provide: MAGIC_LINK_PLUGIN,
117
+ inject: [EmailService],
118
+ useFactory: (email: EmailService) =>
119
+ magicLink({ sendMagicLink: (d) => email.send({ to: d.email, link: d.url }) }),
120
+ },
121
+ ],
122
+ exports: [MAGIC_LINK_PLUGIN],
123
+ })
124
+ export class MagicLinkAuthModule {}
125
+ ```
126
+
127
+ ```ts
128
+ // app.module.ts
129
+ @Module({
130
+ imports: [
131
+ MagicLinkAuthModule,
132
+ OAuthAuthModule,
133
+ BetterAuthModule.forRootAsync({
134
+ imports: [MagicLinkAuthModule, OAuthAuthModule],
135
+ inject: [MAGIC_LINK_PLUGIN, OAUTH_PLUGIN],
136
+ useFactory: (magicLink, oauth) => ({
137
+ auth: betterAuth({ database, plugins: [magicLink, oauth] }),
138
+ }),
139
+ isGlobal: true,
140
+ }),
141
+ ],
142
+ })
143
+ class AppModule {}
144
+ ```
145
+
146
+ ## Calling better-auth from services & controllers
147
+
148
+ Inject the auth instance via the `BETTER_AUTH` token. The full `auth.api.*` surface is available — list sessions, revoke, impersonate, anything better-auth exposes server-side.
149
+
150
+ ```ts
151
+ import { Inject, Injectable } from '@velajs/vela';
152
+ import { BETTER_AUTH, type BetterAuthInstance } from '@velajs/better-auth';
153
+
154
+ @Injectable()
155
+ class AdminUserService {
156
+ constructor(@Inject(BETTER_AUTH) private readonly auth: BetterAuthInstance) {}
157
+
158
+ listSessions(userId: string) {
159
+ return this.auth.api.listUserSessions({ userId });
160
+ }
161
+ revoke(token: string) {
162
+ return this.auth.api.revokeSession({ sessionToken: token });
163
+ }
164
+ }
165
+ ```
166
+
167
+ ## Decorators
168
+
169
+ | Decorator | Purpose |
170
+ | ------------------- | ------------------------------------------------------------------------ |
171
+ | `@CurrentUser()` | Lazy parameter — better-auth `User` from the request (read by AuthGuard) |
172
+ | `@CurrentSession()` | Lazy parameter — better-auth `Session` |
173
+ | `@Public(true)` | Class or method — bypass AuthGuard entirely |
174
+ | `@OptionalAuth(true)` | Class or method — populate user if present, never throw 401 |
175
+ | `@Roles(['admin'])` | Method — read by `RolesGuard`. Compares against `user.role`. |
176
+
177
+ `@CurrentUser()` returns a lazy proxy. It's always object-truthy (because it's a proxy). When auth is optional, probe a property instead of `!!user`:
178
+
179
+ ```ts
180
+ handle(@CurrentUser() user: User | undefined) {
181
+ return { hasUser: user?.id != null }; // ✓ correct
182
+ }
183
+ ```
184
+
185
+ ## Guards
186
+
187
+ - **`AuthGuard`** — singleton. Reads `Authorization` header / cookies via `auth.api.getSession`, populates `REQUEST_CONTEXT`. Honors `@Public()` and `@OptionalAuth()` overrides. Always lets requests under `basePath` through (so the catch-all controller can run unauthenticated).
188
+ - **`RolesGuard`** — singleton. Reads `@Roles([...])` metadata, compares against `user.role`. Use with `@UseGuards(AuthGuard, RolesGuard)` — order matters.
189
+
190
+ Global registration: pass `isGlobal: true` to `forRoot` (binds AuthGuard to `APP_GUARD`). Routes are deny-by-default; mark public ones with `@Public(true)`.
191
+
192
+ ## Edge-safe DB adapters
193
+
194
+ `@velajs/better-auth` itself is `node:`-clean. Edge-safety of your runtime depends on your better-auth DB adapter:
195
+
196
+ | Adapter | Edge-safe |
197
+ | ------------------------------------------ | :-------: |
198
+ | `drizzleAdapter` + `drizzle-orm/d1` | ✅ |
199
+ | `drizzleAdapter` + `@neondatabase/serverless` | ✅ |
200
+ | `kyselyAdapter` + `kysely-d1` | ✅ |
201
+ | `prismaAdapter` + `@prisma/adapter-d1` | ✅ |
202
+ | `drizzleAdapter` + `better-sqlite3` | ❌ |
203
+ | `prismaAdapter` (default, no edge client) | ❌ |
204
+
205
+ If you deploy to Cloudflare Workers, smoke-test your bundle for `node:` imports.
206
+
207
+ ## Custom mount path
208
+
209
+ Set `mountHandler: false` and mount the catch-all yourself if you need a base path other than `/api/auth`:
210
+
211
+ ```ts
212
+ import { Controller, All, Req, Inject, Injectable } from '@velajs/vela';
213
+ import { BETTER_AUTH, Public } from '@velajs/better-auth';
214
+
215
+ @Public(true)
216
+ @Controller('/auth')
217
+ @Injectable()
218
+ class CustomCatchallController {
219
+ constructor(@Inject(BETTER_AUTH) private auth: BetterAuthInstance) {}
220
+ @All('/*') handle(@Req() c: Context) { return this.auth.handler(c.req.raw); }
221
+ }
222
+ ```
223
+
224
+ Pass `basePath: '/auth'` to `BetterAuthModule.forRoot` so `AuthGuard` skips the right paths, and keep your `betterAuth({ basePath: '/auth' })` config in sync.
225
+
226
+ ## License
227
+
228
+ MIT
@@ -0,0 +1,7 @@
1
+ import type { Context } from 'hono';
2
+ import type { BetterAuthInstance } from './better-auth.types';
3
+ export declare class BetterAuthCatchallController {
4
+ private readonly auth;
5
+ constructor(auth: BetterAuthInstance);
6
+ handle(c: Context): Promise<Response>;
7
+ }
@@ -0,0 +1,45 @@
1
+ function _ts_decorate(decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ }
7
+ function _ts_metadata(k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ }
10
+ function _ts_param(paramIndex, decorator) {
11
+ return function(target, key) {
12
+ decorator(target, key, paramIndex);
13
+ };
14
+ }
15
+ import { All, Controller, Inject, Injectable, Req } from "@velajs/vela";
16
+ import { BETTER_AUTH } from "./better-auth.tokens.js";
17
+ import { Public } from "./decorators/public.decorator.js";
18
+ export class BetterAuthCatchallController {
19
+ auth;
20
+ constructor(auth){
21
+ this.auth = auth;
22
+ }
23
+ async handle(c) {
24
+ return this.auth.handler(c.req.raw);
25
+ }
26
+ }
27
+ _ts_decorate([
28
+ All('/*'),
29
+ _ts_param(0, Req()),
30
+ _ts_metadata("design:type", Function),
31
+ _ts_metadata("design:paramtypes", [
32
+ typeof Context === "undefined" ? Object : Context
33
+ ]),
34
+ _ts_metadata("design:returntype", Promise)
35
+ ], BetterAuthCatchallController.prototype, "handle", null);
36
+ BetterAuthCatchallController = _ts_decorate([
37
+ Public(true),
38
+ Controller('/api/auth'),
39
+ Injectable(),
40
+ _ts_param(0, Inject(BETTER_AUTH)),
41
+ _ts_metadata("design:type", Function),
42
+ _ts_metadata("design:paramtypes", [
43
+ typeof BetterAuthInstance === "undefined" ? Object : BetterAuthInstance
44
+ ])
45
+ ], BetterAuthCatchallController);
@@ -0,0 +1,16 @@
1
+ import { type AsyncModuleOptions, type DynamicModule } from '@velajs/vela';
2
+ import type { BetterAuthInstance, BetterAuthModuleOptions } from './better-auth.types';
3
+ type ForRootAsyncOptions = AsyncModuleOptions<{
4
+ auth: BetterAuthInstance;
5
+ basePath?: string;
6
+ defaultPolicy?: 'deny' | 'allow';
7
+ }> & {
8
+ isGlobal?: boolean;
9
+ mountHandler?: boolean;
10
+ key?: string;
11
+ };
12
+ export declare class BetterAuthModule {
13
+ static forRoot(options: BetterAuthModuleOptions): DynamicModule;
14
+ static forRootAsync(options: ForRootAsyncOptions): DynamicModule;
15
+ }
16
+ export {};
@@ -0,0 +1,117 @@
1
+ import { APP_GUARD, stableHash } from "@velajs/vela";
2
+ import { BetterAuthCatchallController } from "./better-auth.controller.js";
3
+ import { BETTER_AUTH, BETTER_AUTH_OPTIONS } from "./better-auth.tokens.js";
4
+ import { AuthGuard } from "./guards/auth.guard.js";
5
+ import { RolesGuard } from "./guards/roles.guard.js";
6
+ const DEFAULT_BASE_PATH = '/api/auth';
7
+ export class BetterAuthModule {
8
+ static forRoot(options) {
9
+ const normalized = normalize(options);
10
+ const providers = baseProviders(normalized, options.auth);
11
+ return {
12
+ module: BetterAuthModule,
13
+ key: stableHash({
14
+ basePath: normalized.basePath,
15
+ defaultPolicy: normalized.defaultPolicy,
16
+ isGlobal: !!options.isGlobal,
17
+ mountHandler: !!normalized.mountHandler
18
+ }),
19
+ providers,
20
+ controllers: normalized.mountHandler ? [
21
+ BetterAuthCatchallController
22
+ ] : [],
23
+ exports: [
24
+ BETTER_AUTH,
25
+ BETTER_AUTH_OPTIONS,
26
+ AuthGuard,
27
+ RolesGuard
28
+ ]
29
+ };
30
+ }
31
+ static forRootAsync(options) {
32
+ const mountHandler = options.mountHandler !== false;
33
+ const isGlobal = options.isGlobal === true;
34
+ const optsProvider = {
35
+ provide: BETTER_AUTH_OPTIONS,
36
+ useFactory: async (...deps)=>{
37
+ const value = await options.useFactory(...deps);
38
+ return {
39
+ auth: value.auth,
40
+ basePath: value.basePath ?? DEFAULT_BASE_PATH,
41
+ defaultPolicy: value.defaultPolicy ?? 'deny',
42
+ isGlobal,
43
+ mountHandler
44
+ };
45
+ },
46
+ inject: options.inject ?? []
47
+ };
48
+ const authProvider = {
49
+ provide: BETTER_AUTH,
50
+ useFactory: (opts)=>opts.auth,
51
+ inject: [
52
+ BETTER_AUTH_OPTIONS
53
+ ]
54
+ };
55
+ const providers = [
56
+ optsProvider,
57
+ authProvider,
58
+ AuthGuard,
59
+ RolesGuard
60
+ ];
61
+ if (isGlobal) {
62
+ providers.push({
63
+ provide: APP_GUARD,
64
+ useExisting: AuthGuard
65
+ });
66
+ }
67
+ return {
68
+ module: BetterAuthModule,
69
+ key: options.key ?? stableHash({
70
+ inject: options.inject,
71
+ isGlobal,
72
+ mountHandler
73
+ }),
74
+ imports: options.imports ?? [],
75
+ providers,
76
+ controllers: mountHandler ? [
77
+ BetterAuthCatchallController
78
+ ] : [],
79
+ exports: [
80
+ BETTER_AUTH,
81
+ BETTER_AUTH_OPTIONS,
82
+ AuthGuard,
83
+ RolesGuard
84
+ ]
85
+ };
86
+ }
87
+ }
88
+ function normalize(options) {
89
+ return {
90
+ auth: options.auth,
91
+ basePath: options.basePath ?? DEFAULT_BASE_PATH,
92
+ isGlobal: options.isGlobal ?? false,
93
+ defaultPolicy: options.defaultPolicy ?? 'deny',
94
+ mountHandler: options.mountHandler ?? true
95
+ };
96
+ }
97
+ function baseProviders(normalized, authInstance) {
98
+ const providers = [
99
+ {
100
+ provide: BETTER_AUTH_OPTIONS,
101
+ useValue: normalized
102
+ },
103
+ {
104
+ provide: BETTER_AUTH,
105
+ useValue: authInstance
106
+ },
107
+ AuthGuard,
108
+ RolesGuard
109
+ ];
110
+ if (normalized.isGlobal) {
111
+ providers.push({
112
+ provide: APP_GUARD,
113
+ useExisting: AuthGuard
114
+ });
115
+ }
116
+ return providers;
117
+ }
@@ -0,0 +1,6 @@
1
+ import { InjectionToken } from '@velajs/vela';
2
+ import type { BetterAuthInstance, BetterAuthModuleOptions } from './better-auth.types';
3
+ export declare const BETTER_AUTH: InjectionToken<BetterAuthInstance>;
4
+ export declare const BETTER_AUTH_OPTIONS: InjectionToken<BetterAuthModuleOptions>;
5
+ export declare const AUTH_USER_KEY: unique symbol;
6
+ export declare const AUTH_SESSION_KEY: unique symbol;
@@ -0,0 +1,5 @@
1
+ import { InjectionToken } from "@velajs/vela";
2
+ export const BETTER_AUTH = new InjectionToken('vela.BetterAuth');
3
+ export const BETTER_AUTH_OPTIONS = new InjectionToken('vela.BetterAuthOptions');
4
+ export const AUTH_USER_KEY = Symbol.for('vela.better-auth.user');
5
+ export const AUTH_SESSION_KEY = Symbol.for('vela.better-auth.session');
@@ -0,0 +1,11 @@
1
+ import type { Auth, Session as BASession, User as BAUser } from 'better-auth';
2
+ export type BetterAuthInstance = Auth<any>;
3
+ export interface BetterAuthModuleOptions {
4
+ auth: BetterAuthInstance;
5
+ basePath?: string;
6
+ isGlobal?: boolean;
7
+ defaultPolicy?: 'deny' | 'allow';
8
+ mountHandler?: boolean;
9
+ }
10
+ export type User = BAUser;
11
+ export type Session = BASession;
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1 @@
1
+ export declare const CurrentSession: (data?: unknown, ...pipes: import("@velajs/vela").PipeType[]) => ParameterDecorator;
@@ -0,0 +1,7 @@
1
+ import { createLazyParamDecorator, REQUEST_CONTEXT } from "@velajs/vela";
2
+ import { AUTH_SESSION_KEY } from "../better-auth.tokens.js";
3
+ export const CurrentSession = createLazyParamDecorator((_data, ctx)=>{
4
+ const honoCtx = ctx.getContext();
5
+ const reqCtx = honoCtx.get('container').resolve(REQUEST_CONTEXT);
6
+ return reqCtx.get(AUTH_SESSION_KEY);
7
+ });
@@ -0,0 +1 @@
1
+ export declare const CurrentUser: (data?: unknown, ...pipes: import("@velajs/vela").PipeType[]) => ParameterDecorator;
@@ -0,0 +1,7 @@
1
+ import { createLazyParamDecorator, REQUEST_CONTEXT } from "@velajs/vela";
2
+ import { AUTH_USER_KEY } from "../better-auth.tokens.js";
3
+ export const CurrentUser = createLazyParamDecorator((_data, ctx)=>{
4
+ const honoCtx = ctx.getContext();
5
+ const reqCtx = honoCtx.get('container').resolve(REQUEST_CONTEXT);
6
+ return reqCtx.get(AUTH_USER_KEY);
7
+ });
@@ -0,0 +1,2 @@
1
+ export declare const OptionalAuth: import("@velajs/vela").ReflectableDecorator<boolean>;
2
+ export declare const OPTIONAL_AUTH_KEY: string;
@@ -0,0 +1,5 @@
1
+ import { Reflector } from "@velajs/vela";
2
+ export const OptionalAuth = Reflector.createDecorator({
3
+ key: 'vela.auth.optional'
4
+ });
5
+ export const OPTIONAL_AUTH_KEY = OptionalAuth.KEY;
@@ -0,0 +1,2 @@
1
+ export declare const Public: import("@velajs/vela").ReflectableDecorator<boolean>;
2
+ export declare const PUBLIC_KEY: string;
@@ -0,0 +1,5 @@
1
+ import { Reflector } from "@velajs/vela";
2
+ export const Public = Reflector.createDecorator({
3
+ key: 'vela.auth.public'
4
+ });
5
+ export const PUBLIC_KEY = Public.KEY;
@@ -0,0 +1,2 @@
1
+ export declare const Roles: import("@velajs/vela").ReflectableDecorator<string[]>;
2
+ export declare const ROLES_KEY: string;
@@ -0,0 +1,5 @@
1
+ import { Reflector } from "@velajs/vela";
2
+ export const Roles = Reflector.createDecorator({
3
+ key: 'vela.auth.roles'
4
+ });
5
+ export const ROLES_KEY = Roles.KEY;
@@ -0,0 +1,9 @@
1
+ import { type CanActivate, type ExecutionContext } from '@velajs/vela';
2
+ import type { BetterAuthInstance, BetterAuthModuleOptions } from '../better-auth.types';
3
+ export declare class AuthGuard implements CanActivate {
4
+ private readonly auth;
5
+ private readonly opts;
6
+ private readonly reflector;
7
+ constructor(auth: BetterAuthInstance, opts: BetterAuthModuleOptions);
8
+ canActivate(context: ExecutionContext): Promise<boolean>;
9
+ }
@@ -0,0 +1,62 @@
1
+ function _ts_decorate(decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ }
7
+ function _ts_metadata(k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ }
10
+ function _ts_param(paramIndex, decorator) {
11
+ return function(target, key) {
12
+ decorator(target, key, paramIndex);
13
+ };
14
+ }
15
+ import { Inject, Injectable, REQUEST_CONTEXT, Reflector, UnauthorizedException } from "@velajs/vela";
16
+ import { AUTH_SESSION_KEY, AUTH_USER_KEY, BETTER_AUTH, BETTER_AUTH_OPTIONS } from "../better-auth.tokens.js";
17
+ import { OptionalAuth } from "../decorators/optional-auth.decorator.js";
18
+ import { Public } from "../decorators/public.decorator.js";
19
+ export class AuthGuard {
20
+ auth;
21
+ opts;
22
+ reflector = new Reflector();
23
+ constructor(auth, opts){
24
+ this.auth = auth;
25
+ this.opts = opts;
26
+ }
27
+ async canActivate(context) {
28
+ if (this.reflector.getAllAndOverride(Public, context)) return true;
29
+ const request = context.getRequest();
30
+ const path = new URL(request.url).pathname;
31
+ const basePath = this.opts.basePath ?? '/api/auth';
32
+ if (path === basePath || path.startsWith(`${basePath}/`)) return true;
33
+ const data = await this.auth.api.getSession({
34
+ headers: request.headers
35
+ });
36
+ if (data) {
37
+ const reqCtx = resolveRequestContext(context);
38
+ reqCtx.set(AUTH_USER_KEY, data.user);
39
+ reqCtx.set(AUTH_SESSION_KEY, data.session);
40
+ return true;
41
+ }
42
+ if (this.opts.defaultPolicy === 'allow' || this.reflector.getAllAndOverride(OptionalAuth, context)) {
43
+ return true;
44
+ }
45
+ throw new UnauthorizedException('Authentication required');
46
+ }
47
+ }
48
+ AuthGuard = _ts_decorate([
49
+ Injectable(),
50
+ _ts_param(0, Inject(BETTER_AUTH)),
51
+ _ts_param(1, Inject(BETTER_AUTH_OPTIONS)),
52
+ _ts_metadata("design:type", Function),
53
+ _ts_metadata("design:paramtypes", [
54
+ typeof BetterAuthInstance === "undefined" ? Object : BetterAuthInstance,
55
+ typeof BetterAuthModuleOptions === "undefined" ? Object : BetterAuthModuleOptions
56
+ ])
57
+ ], AuthGuard);
58
+ function resolveRequestContext(context) {
59
+ const honoCtx = context.getContext();
60
+ const container = honoCtx.get('container');
61
+ return container.resolve(REQUEST_CONTEXT);
62
+ }
@@ -0,0 +1,5 @@
1
+ import { type CanActivate, type ExecutionContext } from '@velajs/vela';
2
+ export declare class RolesGuard implements CanActivate {
3
+ private readonly reflector;
4
+ canActivate(context: ExecutionContext): boolean;
5
+ }
@@ -0,0 +1,36 @@
1
+ function _ts_decorate(decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ }
7
+ import { ForbiddenException, Injectable, REQUEST_CONTEXT, Reflector } from "@velajs/vela";
8
+ import { AUTH_USER_KEY } from "../better-auth.tokens.js";
9
+ import { Roles } from "../decorators/roles.decorator.js";
10
+ export class RolesGuard {
11
+ reflector = new Reflector();
12
+ canActivate(context) {
13
+ const required = this.reflector.getAllAndOverride(Roles, context);
14
+ if (!required || required.length === 0) return true;
15
+ const honoCtx = context.getContext();
16
+ const reqCtx = honoCtx.get('container').resolve(REQUEST_CONTEXT);
17
+ const user = reqCtx.get(AUTH_USER_KEY);
18
+ if (!user) {
19
+ throw new ForbiddenException('Role check requires authentication');
20
+ }
21
+ const userRoles = normalizeRoles(user.role);
22
+ const ok = required.some((r)=>userRoles.includes(r));
23
+ if (!ok) {
24
+ throw new ForbiddenException(`Insufficient role; one of [${required.join(', ')}] required`);
25
+ }
26
+ return true;
27
+ }
28
+ }
29
+ RolesGuard = _ts_decorate([
30
+ Injectable()
31
+ ], RolesGuard);
32
+ function normalizeRoles(role) {
33
+ if (!role) return [];
34
+ if (Array.isArray(role)) return role;
35
+ return role.split(',').map((r)=>r.trim()).filter(Boolean);
36
+ }
@@ -0,0 +1,11 @@
1
+ export { BetterAuthModule } from './better-auth.module';
2
+ export { BetterAuthCatchallController } from './better-auth.controller';
3
+ export { BETTER_AUTH, BETTER_AUTH_OPTIONS, AUTH_USER_KEY, AUTH_SESSION_KEY, } from './better-auth.tokens';
4
+ export { AuthGuard } from './guards/auth.guard';
5
+ export { RolesGuard } from './guards/roles.guard';
6
+ export { CurrentUser } from './decorators/current-user.decorator';
7
+ export { CurrentSession } from './decorators/current-session.decorator';
8
+ export { Public, PUBLIC_KEY } from './decorators/public.decorator';
9
+ export { OptionalAuth, OPTIONAL_AUTH_KEY } from './decorators/optional-auth.decorator';
10
+ export { Roles, ROLES_KEY } from './decorators/roles.decorator';
11
+ export type { BetterAuthInstance, BetterAuthModuleOptions, Session, User, } from './better-auth.types';
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ // Module
2
+ export { BetterAuthModule } from "./better-auth.module.js";
3
+ export { BetterAuthCatchallController } from "./better-auth.controller.js";
4
+ // Tokens & symbols
5
+ export { BETTER_AUTH, BETTER_AUTH_OPTIONS, AUTH_USER_KEY, AUTH_SESSION_KEY } from "./better-auth.tokens.js";
6
+ // Guards
7
+ export { AuthGuard } from "./guards/auth.guard.js";
8
+ export { RolesGuard } from "./guards/roles.guard.js";
9
+ // Decorators
10
+ export { CurrentUser } from "./decorators/current-user.decorator.js";
11
+ export { CurrentSession } from "./decorators/current-session.decorator.js";
12
+ export { Public, PUBLIC_KEY } from "./decorators/public.decorator.js";
13
+ export { OptionalAuth, OPTIONAL_AUTH_KEY } from "./decorators/optional-auth.decorator.js";
14
+ export { Roles, ROLES_KEY } from "./decorators/roles.decorator.js";
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@velajs/better-auth",
3
+ "version": "0.1.0",
4
+ "description": "better-auth integration for the Vela framework",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE",
18
+ "CHANGELOG.md"
19
+ ],
20
+ "sideEffects": false,
21
+ "keywords": [
22
+ "vela",
23
+ "better-auth",
24
+ "auth",
25
+ "authentication",
26
+ "session",
27
+ "oauth",
28
+ "edge",
29
+ "framework"
30
+ ],
31
+ "author": "ksh",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/velajs/better-auth.git"
36
+ },
37
+ "homepage": "https://github.com/velajs/better-auth#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/velajs/better-auth/issues"
40
+ },
41
+ "engines": {
42
+ "node": ">=20"
43
+ },
44
+ "peerDependencies": {
45
+ "@velajs/vela": ">=1.6.0",
46
+ "better-auth": ">=1.2.0",
47
+ "hono": ">=4"
48
+ },
49
+ "devDependencies": {
50
+ "@swc/cli": "^0.8.0",
51
+ "@swc/core": "^1.15.11",
52
+ "@velajs/vela": "^1.6.0",
53
+ "better-auth": "^1.2.0",
54
+ "hono": "^4",
55
+ "typescript": "^5",
56
+ "unplugin-swc": "^1.5.9",
57
+ "vitest": "^4.0.18"
58
+ },
59
+ "scripts": {
60
+ "build": "rm -rf dist && swc src -d dist --strip-leading-paths && tsc --emitDeclarationOnly",
61
+ "test": "vitest run",
62
+ "typecheck": "tsc --noEmit"
63
+ }
64
+ }