@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 +228 -0
- package/dist/better-auth.controller.d.ts +7 -0
- package/dist/better-auth.controller.js +45 -0
- package/dist/better-auth.module.d.ts +16 -0
- package/dist/better-auth.module.js +117 -0
- package/dist/better-auth.tokens.d.ts +6 -0
- package/dist/better-auth.tokens.js +5 -0
- package/dist/better-auth.types.d.ts +11 -0
- package/dist/better-auth.types.js +1 -0
- package/dist/decorators/current-session.decorator.d.ts +1 -0
- package/dist/decorators/current-session.decorator.js +7 -0
- package/dist/decorators/current-user.decorator.d.ts +1 -0
- package/dist/decorators/current-user.decorator.js +7 -0
- package/dist/decorators/optional-auth.decorator.d.ts +2 -0
- package/dist/decorators/optional-auth.decorator.js +5 -0
- package/dist/decorators/public.decorator.d.ts +2 -0
- package/dist/decorators/public.decorator.js +5 -0
- package/dist/decorators/roles.decorator.d.ts +2 -0
- package/dist/decorators/roles.decorator.js +5 -0
- package/dist/guards/auth.guard.d.ts +9 -0
- package/dist/guards/auth.guard.js +62 -0
- package/dist/guards/roles.guard.d.ts +5 -0
- package/dist/guards/roles.guard.js +36 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +14 -0
- package/package.json +64 -0
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,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,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,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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|