@velajs/better-auth 1.22.2 → 1.29.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/CHANGELOG.md +93 -0
- package/README.md +38 -4
- package/dist/index.d.ts +43 -7
- package/dist/index.js +74 -21
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,98 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.29.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2dd809d: `BetterAuthModule.forRoot({ key })` and `forRootAsync({ key })` no longer throw when a root is rebuilt with the same explicit key in one isolate, for example per Worker environment or per Durable Object. Within an application, the same registration still deduplicates.
|
|
8
|
+
|
|
9
|
+
**Behavior change:** an explicit `key` no longer rejects a different registration that reuses it within one application. The key now labels a registration instead of claiming it: the options shape and the auth instance (or `useFactory`) stay part of the module's identity, so each distinct registration under the same key, one with a different auth instance, factory or options, becomes its own module instance, as a registration without a key does. Previously the second registration threw. Use distinct keys, or pass the same auth instance or factory and options, when one application should hold a single registration.
|
|
10
|
+
- 8a3016c: Add `BetterAuthUpgradeAuthenticator` for `@WebSocketGateway({ authenticator: BetterAuthUpgradeAuthenticator })`. It reads the session cookie through the module's `BetterAuthService`, validates the full session, and issues a WebSocket identity under the `BetterAuthModule` issuer, as `AuthGuard` does for HTTP, expiring with the session. The tenant is the session's active organization; provide `BETTER_AUTH_UPGRADE_TENANT` in the module that declares the gateway to choose it per connection (`(session, context, request) => tenantId | undefined`). Without a tenant, the upgrade is refused.
|
|
11
|
+
- 4071cb7: The built-in guards inject the application `Reflector` instead of constructing their own: `AuthGuard` (`@velajs/better-auth`), `FeatureFlagGuard` (`@velajs/feature-flags`), `PermissionGuard` and `RolesGuard` (`@velajs/authz/vela`), `TenantGuard` (`@velajs/tenant/vela`) and `CedarGuard` (`@velajs/authz-cedar/vela`). `PermissionGuard`, `RolesGuard` and `TenantGuard` are now injectable, so `@UseGuards()` builds them in the declaring module through DI, like any provider.
|
|
12
|
+
|
|
13
|
+
**Behavior change:** each guard takes the `Reflector` as a constructor parameter, after any dependencies it already had: `new PermissionGuard(reflector)`, `new RolesGuard(reflector)`, `new TenantGuard(reflector)`, `new CedarGuard(reflector)`, `new AuthGuard(auth, options, reflector)` and `new FeatureFlagGuard(flags, reflector)`. Code that constructs a guard itself, such as `app.useGlobalGuards(new TenantGuard(app.get(Reflector)))` or a unit test, passes the application's `Reflector` or a `new Reflector()`, and a subclass that declares its own constructor passes it to `super()`. A guard class passed to `app.useGlobalGuards()` is built through DI only when a module registers it, including the copy a module registers for a `@UseGuards()` reference; otherwise pass an instance or register it as `{ provide: APP_GUARD, useClass: PermissionGuard }`. Guards resolved through DI need no change.
|
|
14
|
+
- 4071cb7: A factory without parameters may omit `inject` in `BetterAuthModule.forRootAsync()`, `MailModule.forRootAsync()`, `StorageModule.forRootAsync()`, `RpcClientModule.registerAsync()` and `overrideProvider(token).useFactory({ factory })`, as in the `@velajs/vela` factories. A factory with parameters still names their tokens in `inject`, and a dependency tuple given as a type argument still needs a matching `inject`.
|
|
15
|
+
|
|
16
|
+
`StorageModule.forRootAsync()` factories may return `{ driver, multipartGrantSecret }` instead of a bare driver, so the multipart grant secret comes through DI, for example from `ENV`, now that roots are static. The factory still runs once, on first use, or again on the next use until it succeeds; its secret takes precedence over `http.multipartGrantSecret` and is validated with the rest of the factory result on each such use, and multipart endpoints without any secret keep refusing every request. Adds the `StorageAsyncResult` and `StorageControllerOptions` types, and `createStorageController()` takes an optional token for the values it resolves per application.
|
|
17
|
+
|
|
18
|
+
**Behavior change:** a multipart grant secret shorter than 32 bytes is a server configuration error instead of a client error. `StorageModule.forRoot()` throws for such an `http.multipartGrantSecret` when the module is set up, and a `forRootAsync()` factory that returns one fails every storage operation and multipart request until the factory returns a valid result, which the controller answers with a redacted 502 `upstream_error`. Multipart requests no longer answer 400 `invalid_request` with a message that names the setting.
|
|
19
|
+
|
|
20
|
+
**Behavior change:** `MailModule.forRootAsync()` and `StorageModule.forRootAsync()` throw when called with a factory that declares parameters but no `inject`, naming the method, instead of running it with `undefined` arguments. `MailModuleAsyncOptions`, `StorageModuleAsyncOptions` and `RpcClientAsyncOptions` are type aliases instead of interfaces, so an interface can no longer extend them: intersect them instead (`RpcClientAsyncOptions<Inject> & { region: string }`). `MailModuleAsyncOptions.imports` is typed `ModuleImport[]`, so a caller that enables `exactOptionalPropertyTypes` omits it instead of passing `undefined`.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [07d1713]
|
|
25
|
+
- Updated dependencies [db18d3a]
|
|
26
|
+
- Updated dependencies [07d1713]
|
|
27
|
+
- Updated dependencies [4071cb7]
|
|
28
|
+
- Updated dependencies [bacaacd]
|
|
29
|
+
- Updated dependencies [a814199]
|
|
30
|
+
- Updated dependencies [1838474]
|
|
31
|
+
- Updated dependencies [8a3016c]
|
|
32
|
+
- Updated dependencies [d803a49]
|
|
33
|
+
- Updated dependencies [b235935]
|
|
34
|
+
- Updated dependencies [08a81c8]
|
|
35
|
+
- Updated dependencies [5b5b81d]
|
|
36
|
+
- Updated dependencies [7daf4fc]
|
|
37
|
+
- Updated dependencies [35e8e0d]
|
|
38
|
+
- Updated dependencies [4420501]
|
|
39
|
+
- Updated dependencies [ff44b6a]
|
|
40
|
+
- Updated dependencies [6d4f0c0]
|
|
41
|
+
- Updated dependencies [e3bda2a]
|
|
42
|
+
- Updated dependencies [bd7e3c9]
|
|
43
|
+
- Updated dependencies [2b74880]
|
|
44
|
+
- Updated dependencies [5ba8635]
|
|
45
|
+
- Updated dependencies [db0c834]
|
|
46
|
+
- Updated dependencies [d6f6a65]
|
|
47
|
+
- Updated dependencies [8a3016c]
|
|
48
|
+
- Updated dependencies [d5a3ec8]
|
|
49
|
+
- Updated dependencies [0f7e8e7]
|
|
50
|
+
- Updated dependencies [41ec70d]
|
|
51
|
+
- Updated dependencies [b265297]
|
|
52
|
+
- Updated dependencies [bdfff47]
|
|
53
|
+
- Updated dependencies [28c7d07]
|
|
54
|
+
- Updated dependencies [8a3016c]
|
|
55
|
+
- Updated dependencies [44efdde]
|
|
56
|
+
- @velajs/vela@1.29.0
|
|
57
|
+
- @velajs/authz@1.29.0
|
|
58
|
+
|
|
59
|
+
## 1.28.0
|
|
60
|
+
|
|
61
|
+
### Minor Changes
|
|
62
|
+
|
|
63
|
+
- Continue the module-based Workers APIs on the 1.x release line. Vela permits breaking changes in minor releases and does not retain compatibility layers. Upgrade the framework and integrations together for native queue dispatch, cron scheduling, RPC modules and asynchronous roots; see docs/module-workers.md.
|
|
64
|
+
|
|
65
|
+
### Patch Changes
|
|
66
|
+
|
|
67
|
+
- Updated dependencies
|
|
68
|
+
- @velajs/authz@1.28.0
|
|
69
|
+
- @velajs/vela@1.28.0
|
|
70
|
+
|
|
71
|
+
## 3.0.0
|
|
72
|
+
|
|
73
|
+
### Major Changes
|
|
74
|
+
|
|
75
|
+
- Publish module-based Workers on the unused 3.x stable release line. Earlier experimental 2.0.0 registry versions are immutable and do not contain this release. Upgrade the framework and integrations together; see docs/module-workers.md for queue bootstrap, native delivery, RPC modules and async root migration details.
|
|
76
|
+
|
|
77
|
+
### Patch Changes
|
|
78
|
+
|
|
79
|
+
- Updated dependencies
|
|
80
|
+
- @velajs/authz@3.0.0
|
|
81
|
+
- @velajs/vela@3.0.0
|
|
82
|
+
|
|
83
|
+
## 2.0.0
|
|
84
|
+
|
|
85
|
+
### Major Changes
|
|
86
|
+
|
|
87
|
+
- a2d2692: Require the Vela 2 module/runtime peer line. Packages that raise their framework or adapter peer requirement ship a major version so existing 1.x installations are not offered an incompatible patch release. Upgrade the related framework integrations together; native queue/bootstrap migration is documented in docs/module-workers.md.
|
|
88
|
+
|
|
89
|
+
### Patch Changes
|
|
90
|
+
|
|
91
|
+
- Updated dependencies [a2d2692]
|
|
92
|
+
- Updated dependencies [b99d71a]
|
|
93
|
+
- @velajs/authz@2.0.0
|
|
94
|
+
- @velajs/vela@2.0.0
|
|
95
|
+
|
|
3
96
|
## 1.22.2
|
|
4
97
|
|
|
5
98
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -74,12 +74,12 @@ imports: [
|
|
|
74
74
|
|
|
75
75
|
### Pattern B — DI'd plugin construction
|
|
76
76
|
|
|
77
|
-
`forRootAsync`
|
|
77
|
+
`forRootAsync` types the factory's parameters from its `inject` tuple, so they always have matching runtime tokens; a factory without parameters may omit `inject`. It lets Vela services participate in your Better Auth configuration. On Workers, inject the framework `ENV` from `@velajs/vela`: `createCloudflareWorker(AppModule)` seeds the native event environment before DI factories run, and `wrangler types` types its bindings.
|
|
78
78
|
|
|
79
79
|
```ts
|
|
80
80
|
imports: [
|
|
81
81
|
BetterAuthModule.forRootAsync({
|
|
82
|
-
inject: [
|
|
82
|
+
inject: [ENV, EmailService],
|
|
83
83
|
useFactory: (env, email) => betterAuth({
|
|
84
84
|
database: drizzleAdapter(drizzle(env.DB), { provider: 'sqlite' }),
|
|
85
85
|
plugins: [
|
|
@@ -191,6 +191,41 @@ falls back to a platform-attested client address.
|
|
|
191
191
|
|
|
192
192
|
`BETTER_AUTH_OPTIONS` contains runtime configuration; read the auth instance from `BetterAuthService`. Use `createBetterAuthCatchallController()` for a manually mounted catch-all.
|
|
193
193
|
|
|
194
|
+
## WebSocket upgrades
|
|
195
|
+
|
|
196
|
+
`BetterAuthUpgradeAuthenticator` authenticates a WebSocket upgrade from the Better Auth session cookie. The application resolves it from the module that declares the gateway, so that module must see `BetterAuthModule`'s exports:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { defineProvider, Module } from '@velajs/vela';
|
|
200
|
+
import { WebSocketGateway } from '@velajs/vela/websocket';
|
|
201
|
+
import {
|
|
202
|
+
BETTER_AUTH_UPGRADE_TENANT,
|
|
203
|
+
BetterAuthModule,
|
|
204
|
+
BetterAuthUpgradeAuthenticator,
|
|
205
|
+
} from '@velajs/better-auth';
|
|
206
|
+
|
|
207
|
+
@WebSocketGateway({
|
|
208
|
+
path: '/boards/:board/ws',
|
|
209
|
+
roomParam: 'board',
|
|
210
|
+
authenticator: BetterAuthUpgradeAuthenticator,
|
|
211
|
+
})
|
|
212
|
+
class BoardGateway {}
|
|
213
|
+
|
|
214
|
+
@Module({
|
|
215
|
+
imports: [BetterAuthModule.forRoot({ auth, issuer: 'boards' })],
|
|
216
|
+
providers: [
|
|
217
|
+
BoardGateway,
|
|
218
|
+
// Optional: choose the tenant per connection. Returning undefined refuses it.
|
|
219
|
+
defineProvider(BETTER_AUTH_UPGRADE_TENANT, {
|
|
220
|
+
useValue: (_session, context) => (context.room === 'lobby' ? 'public' : undefined),
|
|
221
|
+
}),
|
|
222
|
+
],
|
|
223
|
+
})
|
|
224
|
+
class BoardsModule {}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The principal carries the module's `issuer`, as `AuthGuard` does for HTTP, and the identity expires with the session. Every WebSocket identity needs a tenant: by default it is the session's active organization, and a session without one is refused unless a `BETTER_AUTH_UPGRADE_TENANT` resolver supplies a tenant.
|
|
228
|
+
|
|
194
229
|
## Trusted identity and typing
|
|
195
230
|
|
|
196
231
|
`@CurrentUser()` and `@CurrentSession()` expose only validated Better Auth data tied to the exact current trusted identity. Public routes, missing/rejected sessions, logout, expiry, and another provider replacing the identity invalidate those values. Hono user variables cannot grant roles or permissions.
|
|
@@ -221,12 +256,11 @@ If you deploy to Cloudflare Workers, smoke-test your bundle for `node:` imports.
|
|
|
221
256
|
Set `mountHandler: false` and mount the catch-all yourself if you need a base path other than `/api/auth`:
|
|
222
257
|
|
|
223
258
|
```ts
|
|
224
|
-
import { Controller, All, Req, Inject
|
|
259
|
+
import { Controller, All, Req, Inject } from '@velajs/vela';
|
|
225
260
|
import { BetterAuthService, Public } from '@velajs/better-auth';
|
|
226
261
|
|
|
227
262
|
@Public(true)
|
|
228
263
|
@Controller('/auth')
|
|
229
|
-
@Injectable()
|
|
230
264
|
class CustomCatchallController {
|
|
231
265
|
constructor(@Inject(BetterAuthService) private auth: BetterAuthService) {}
|
|
232
266
|
@All('/*') handle(@Req() c: Context) { return this.auth.handler(c.req.raw); }
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { a as Session, i as BetterAuthRuntimeOptions, n as BetterAuthInstance, o as User, r as BetterAuthModuleOptions, t as BetterAuthService } from "./better-auth.service-CvmboUID.js";
|
|
2
|
-
import { CanActivate, DynamicModule, ExecutionContext, InferTokens, InjectionToken, Token, Type } from "@velajs/vela";
|
|
2
|
+
import { CanActivate, DynamicModule, ExecutionContext, FactoryInject, InferTokens, InjectionToken, Reflector, Token, Type } from "@velajs/vela";
|
|
3
|
+
import { UpgradeAuthenticator, WebSocketUpgradeAuthenticationContext, WebSocketUpgradeIdentity } from "@velajs/vela/websocket";
|
|
3
4
|
import { Identity, PermissionResolver } from "@velajs/authz";
|
|
4
5
|
//#region src/better-auth.module.d.ts
|
|
5
6
|
/**
|
|
@@ -11,14 +12,15 @@ import { Identity, PermissionResolver } from "@velajs/authz";
|
|
|
11
12
|
*
|
|
12
13
|
* ```ts
|
|
13
14
|
* BetterAuthModule.forRootAsync({
|
|
14
|
-
* inject: [
|
|
15
|
+
* inject: [ENV, ConfigService], // captured as readonly tuple
|
|
15
16
|
* useFactory: (env, config) => // inferred from the tokens
|
|
16
17
|
* betterAuth({ database: drizzleAdapter(drizzle(env.DB), ...) }),
|
|
17
18
|
* });
|
|
18
19
|
* ```
|
|
20
|
+
*
|
|
21
|
+
* A factory without parameters may omit `inject`.
|
|
19
22
|
*/
|
|
20
|
-
|
|
21
|
-
inject: Inject;
|
|
23
|
+
type ForRootAsyncOptions<Inject extends readonly Token[] = readonly Token[]> = {
|
|
22
24
|
imports?: DynamicModule['imports'];
|
|
23
25
|
useFactory: (...deps: InferTokens<Inject>) => BetterAuthInstance;
|
|
24
26
|
isGlobal?: boolean;
|
|
@@ -26,7 +28,7 @@ interface ForRootAsyncOptions<Inject extends readonly Token[] = readonly Token[]
|
|
|
26
28
|
basePath?: string;
|
|
27
29
|
issuer?: string;
|
|
28
30
|
key?: string;
|
|
29
|
-
}
|
|
31
|
+
} & FactoryInject<Inject>;
|
|
30
32
|
export declare class BetterAuthModule {
|
|
31
33
|
/**
|
|
32
34
|
* Synchronous registration. The auth instance is constructed by the consumer
|
|
@@ -74,10 +76,44 @@ export declare class AuthGuard implements CanActivate {
|
|
|
74
76
|
private readonly auth;
|
|
75
77
|
private readonly opts;
|
|
76
78
|
private readonly reflector;
|
|
77
|
-
constructor(auth: BetterAuthService, opts: BetterAuthRuntimeOptions);
|
|
79
|
+
constructor(auth: BetterAuthService, opts: BetterAuthRuntimeOptions, reflector: Reflector);
|
|
78
80
|
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
79
81
|
}
|
|
80
82
|
//#endregion
|
|
83
|
+
//#region src/upgrade-authenticator.d.ts
|
|
84
|
+
/** The validated Better Auth session behind a WebSocket upgrade. */
|
|
85
|
+
interface BetterAuthUpgradeSession {
|
|
86
|
+
readonly user: User;
|
|
87
|
+
readonly session: Session;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Chooses the tenant a WebSocket connection joins. Returning no tenant (or
|
|
91
|
+
* throwing) refuses the upgrade.
|
|
92
|
+
*/
|
|
93
|
+
type BetterAuthUpgradeTenantResolver = (session: BetterAuthUpgradeSession, context: WebSocketUpgradeAuthenticationContext, request: Request) => string | undefined | Promise<string | undefined>;
|
|
94
|
+
/**
|
|
95
|
+
* Optional tenant resolver for {@link BetterAuthUpgradeAuthenticator}. Provide it
|
|
96
|
+
* in the module that declares the gateway; without one, the session's active
|
|
97
|
+
* organization is the tenant.
|
|
98
|
+
*/
|
|
99
|
+
export declare const BETTER_AUTH_UPGRADE_TENANT: InjectionToken<BetterAuthUpgradeTenantResolver>;
|
|
100
|
+
/**
|
|
101
|
+
* WebSocket upgrade authenticator backed by the Better Auth session cookie:
|
|
102
|
+
* `@WebSocketGateway({ authenticator: BetterAuthUpgradeAuthenticator })`.
|
|
103
|
+
*
|
|
104
|
+
* The principal uses the `BetterAuthModule` issuer, as `AuthGuard` does for
|
|
105
|
+
* HTTP. The identity expires with the session. Every WebSocket identity carries
|
|
106
|
+
* a tenant, so a session without an active organization is refused unless a
|
|
107
|
+
* {@link BETTER_AUTH_UPGRADE_TENANT} resolver supplies one.
|
|
108
|
+
*/
|
|
109
|
+
export declare class BetterAuthUpgradeAuthenticator implements UpgradeAuthenticator {
|
|
110
|
+
private readonly auth;
|
|
111
|
+
private readonly options;
|
|
112
|
+
private readonly resolveTenant?;
|
|
113
|
+
constructor(auth: BetterAuthService, options: BetterAuthRuntimeOptions, resolveTenant?: BetterAuthUpgradeTenantResolver | undefined);
|
|
114
|
+
authenticate(request: Request, context: WebSocketUpgradeAuthenticationContext): Promise<WebSocketUpgradeIdentity | false>;
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
81
117
|
//#region src/decorators/current-user.decorator.d.ts
|
|
82
118
|
export declare const CurrentUser: (data?: unknown, ...pipes: import("@velajs/vela").PipeType[]) => ParameterDecorator;
|
|
83
119
|
//#endregion
|
|
@@ -147,5 +183,5 @@ export declare const permissionsFromAcRole: (role: BetterAuthAcRole) => string[]
|
|
|
147
183
|
*/
|
|
148
184
|
export declare const betterAuthAcResolver: (roles: Readonly<Record<string, BetterAuthAcRole>>) => PermissionResolver;
|
|
149
185
|
//#endregion
|
|
150
|
-
export { type AuthUser, type BetterAuthAcRole, type BetterAuthInstance, type BetterAuthModuleOptions, type BetterAuthRuntimeOptions, BetterAuthService, type Session, type User };
|
|
186
|
+
export { type AuthUser, type BetterAuthAcRole, type BetterAuthInstance, type BetterAuthModuleOptions, type BetterAuthRuntimeOptions, BetterAuthService, type BetterAuthUpgradeSession, type BetterAuthUpgradeTenantResolver, type Session, type User };
|
|
151
187
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as __decorateMetadata, i as __decorateParam, n as BetterAuthService, r as __decorate, t as BETTER_AUTH_BUILDER } from "./better-auth.service-SNg1YFTK.js";
|
|
2
|
-
import { All, Controller, Inject, Injectable, InjectionToken, Reflector, Req, UnauthorizedException, clearTrustedRequestIdentity, createParamDecorator, createTrustedRequestIdentityStore, defineProvider, getTrustedContextRequest, provideGlobal, setTrustedRequestIdentity, stableHash } from "@velajs/vela";
|
|
2
|
+
import { All, Controller, Inject, Injectable, InjectionToken, Optional, Reflector, Req, UnauthorizedException, clearTrustedRequestIdentity, createParamDecorator, createTrustedRequestIdentityStore, defineProvider, getTrustedContextRequest, lazyProvider, provideGlobal, setTrustedRequestIdentity, stableHash } from "@velajs/vela";
|
|
3
3
|
import { getContextIdentity } from "@velajs/authz/vela";
|
|
4
4
|
//#region src/decorators/public.decorator.ts
|
|
5
5
|
const Public = Reflector.createDecorator({ key: "vela.auth.public" });
|
|
@@ -53,7 +53,6 @@ function createBetterAuthCatchallController(basePath = "/api/auth") {
|
|
|
53
53
|
BetterAuthCatchallController = __decorate([
|
|
54
54
|
Public(true),
|
|
55
55
|
Controller(normalizedBasePath),
|
|
56
|
-
Injectable(),
|
|
57
56
|
__decorateParam(0, Inject(BetterAuthService)),
|
|
58
57
|
__decorateMetadata("design:paramtypes", [typeof BetterAuthService === "undefined" ? Object : BetterAuthService])
|
|
59
58
|
], BetterAuthCatchallController);
|
|
@@ -174,10 +173,11 @@ const OPTIONAL_AUTH_KEY = OptionalAuth.KEY;
|
|
|
174
173
|
let AuthGuard = class AuthGuard {
|
|
175
174
|
auth;
|
|
176
175
|
opts;
|
|
177
|
-
reflector
|
|
178
|
-
constructor(auth, opts) {
|
|
176
|
+
reflector;
|
|
177
|
+
constructor(auth, opts, reflector) {
|
|
179
178
|
this.auth = auth;
|
|
180
179
|
this.opts = opts;
|
|
180
|
+
this.reflector = reflector;
|
|
181
181
|
}
|
|
182
182
|
async canActivate(context) {
|
|
183
183
|
if (context.getType() === "ws") {
|
|
@@ -203,12 +203,16 @@ AuthGuard = __decorate([
|
|
|
203
203
|
Injectable(),
|
|
204
204
|
__decorateParam(0, Inject(BetterAuthService)),
|
|
205
205
|
__decorateParam(1, Inject(BETTER_AUTH_OPTIONS)),
|
|
206
|
-
|
|
206
|
+
__decorateParam(2, Inject(Reflector)),
|
|
207
|
+
__decorateMetadata("design:paramtypes", [
|
|
208
|
+
typeof BetterAuthService === "undefined" ? Object : BetterAuthService,
|
|
209
|
+
Object,
|
|
210
|
+
typeof Reflector === "undefined" ? Object : Reflector
|
|
211
|
+
])
|
|
207
212
|
], AuthGuard);
|
|
208
213
|
//#endregion
|
|
209
214
|
//#region src/better-auth.module.ts
|
|
210
215
|
const referenceIds = /* @__PURE__ */ new WeakMap();
|
|
211
|
-
const explicitKeyClaims = /* @__PURE__ */ new Map();
|
|
212
216
|
let nextReferenceId = 1;
|
|
213
217
|
function referenceId(reference) {
|
|
214
218
|
const existing = referenceIds.get(reference);
|
|
@@ -217,16 +221,16 @@ function referenceId(reference) {
|
|
|
217
221
|
referenceIds.set(reference, id);
|
|
218
222
|
return id;
|
|
219
223
|
}
|
|
220
|
-
|
|
224
|
+
/**
|
|
225
|
+
* An explicit key names a registration without widening its identity: the
|
|
226
|
+
* options shape and auth reference stay in the module key, so the same
|
|
227
|
+
* registration deduplicates within an application while a different one
|
|
228
|
+
* (including a root rebuilt per environment) becomes its own instance. No
|
|
229
|
+
* process-wide state is kept, so repeated bootstraps in one isolate succeed.
|
|
230
|
+
*/
|
|
231
|
+
function explicitKey(key, shape, reference) {
|
|
221
232
|
if (key.length === 0 || key !== key.trim()) throw new Error("@velajs/better-auth: an explicit module key must be a non-empty string");
|
|
222
|
-
|
|
223
|
-
if (existing !== void 0 && (existing.kind !== kind || existing.reference !== reference || existing.shape !== shape)) throw new Error(`@velajs/better-auth: explicit module key "${key}" is already bound to a different auth registration`);
|
|
224
|
-
explicitKeyClaims.set(key, {
|
|
225
|
-
kind,
|
|
226
|
-
reference,
|
|
227
|
-
shape
|
|
228
|
-
});
|
|
229
|
-
return `explicit:${key}:ref:${referenceId(reference)}`;
|
|
233
|
+
return `explicit:${key}:${shape}:ref:${referenceId(reference)}`;
|
|
230
234
|
}
|
|
231
235
|
function normalize(options) {
|
|
232
236
|
const basePath = normalizeBetterAuthBasePath(options.basePath);
|
|
@@ -261,7 +265,7 @@ var BetterAuthModule = class BetterAuthModule {
|
|
|
261
265
|
static forRoot(options) {
|
|
262
266
|
const normalized = normalize(options);
|
|
263
267
|
const shape = stableHash(normalized);
|
|
264
|
-
const key = options.key === void 0 ? `${shape}:auth:${referenceId(options.auth)}` :
|
|
268
|
+
const key = options.key === void 0 ? `${shape}:auth:${referenceId(options.auth)}` : explicitKey(options.key, shape, options.auth);
|
|
265
269
|
const common = commonContributions(normalized);
|
|
266
270
|
return {
|
|
267
271
|
module: BetterAuthModule,
|
|
@@ -292,16 +296,17 @@ var BetterAuthModule = class BetterAuthModule {
|
|
|
292
296
|
...n,
|
|
293
297
|
inject: options.inject
|
|
294
298
|
});
|
|
295
|
-
const key = options.key === void 0 ? `${shape}:factory:${referenceId(options.useFactory)}` :
|
|
299
|
+
const key = options.key === void 0 ? `${shape}:factory:${referenceId(options.useFactory)}` : explicitKey(options.key, shape, options.useFactory);
|
|
296
300
|
return {
|
|
297
301
|
module: BetterAuthModule,
|
|
298
302
|
key,
|
|
299
303
|
imports: options.imports ?? [],
|
|
300
304
|
providers: [
|
|
301
305
|
defineProvider(BETTER_AUTH_OPTIONS, { useValue: n }),
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
306
|
+
lazyProvider({
|
|
307
|
+
...options,
|
|
308
|
+
provide: BETTER_AUTH_BUILDER,
|
|
309
|
+
memoize: false
|
|
305
310
|
}),
|
|
306
311
|
...common.providers,
|
|
307
312
|
...n.isGlobal ? provideGlobal("guard", AuthGuard) : []
|
|
@@ -312,6 +317,54 @@ var BetterAuthModule = class BetterAuthModule {
|
|
|
312
317
|
}
|
|
313
318
|
};
|
|
314
319
|
//#endregion
|
|
320
|
+
//#region src/upgrade-authenticator.ts
|
|
321
|
+
/**
|
|
322
|
+
* Optional tenant resolver for {@link BetterAuthUpgradeAuthenticator}. Provide it
|
|
323
|
+
* in the module that declares the gateway; without one, the session's active
|
|
324
|
+
* organization is the tenant.
|
|
325
|
+
*/
|
|
326
|
+
const BETTER_AUTH_UPGRADE_TENANT = new InjectionToken("vela.better-auth.UpgradeTenant");
|
|
327
|
+
let BetterAuthUpgradeAuthenticator = class BetterAuthUpgradeAuthenticator {
|
|
328
|
+
auth;
|
|
329
|
+
options;
|
|
330
|
+
resolveTenant;
|
|
331
|
+
constructor(auth, options, resolveTenant) {
|
|
332
|
+
this.auth = auth;
|
|
333
|
+
this.options = options;
|
|
334
|
+
this.resolveTenant = resolveTenant;
|
|
335
|
+
}
|
|
336
|
+
async authenticate(request, context) {
|
|
337
|
+
const data = validateSessionData(await this.auth.api.getSession({ headers: request.headers }));
|
|
338
|
+
if (!data) return false;
|
|
339
|
+
const tenantId = this.resolveTenant ? await this.resolveTenant({
|
|
340
|
+
user: data.user,
|
|
341
|
+
session: data.session
|
|
342
|
+
}, context, request) : data.tenantId;
|
|
343
|
+
if (typeof tenantId !== "string" || tenantId.length === 0) return false;
|
|
344
|
+
return {
|
|
345
|
+
principal: {
|
|
346
|
+
issuer: this.options.issuer ?? "better-auth",
|
|
347
|
+
subject: data.user.id,
|
|
348
|
+
principalType: "user"
|
|
349
|
+
},
|
|
350
|
+
tenantId,
|
|
351
|
+
expiresAtMs: data.session.expiresAt.getTime()
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
BetterAuthUpgradeAuthenticator = __decorate([
|
|
356
|
+
Injectable(),
|
|
357
|
+
__decorateParam(0, Inject(BetterAuthService)),
|
|
358
|
+
__decorateParam(1, Inject(BETTER_AUTH_OPTIONS)),
|
|
359
|
+
__decorateParam(2, Optional()),
|
|
360
|
+
__decorateParam(2, Inject(BETTER_AUTH_UPGRADE_TENANT)),
|
|
361
|
+
__decorateMetadata("design:paramtypes", [
|
|
362
|
+
typeof BetterAuthService === "undefined" ? Object : BetterAuthService,
|
|
363
|
+
Object,
|
|
364
|
+
Object
|
|
365
|
+
])
|
|
366
|
+
], BetterAuthUpgradeAuthenticator);
|
|
367
|
+
//#endregion
|
|
315
368
|
//#region src/decorators/current-user.decorator.ts
|
|
316
369
|
const CurrentUser = createParamDecorator((_data, ctx) => {
|
|
317
370
|
return getAuthRequestState(ctx)?.user;
|
|
@@ -386,6 +439,6 @@ const betterAuthAcResolver = (roles) => {
|
|
|
386
439
|
} };
|
|
387
440
|
};
|
|
388
441
|
//#endregion
|
|
389
|
-
export { AuthGuard, BETTER_AUTH_ISSUER, BETTER_AUTH_OPTIONS, BetterAuthModule, BetterAuthService, CurrentSession, CurrentUser, OPTIONAL_AUTH_KEY, OptionalAuth, PUBLIC_KEY, Public, betterAuthAcResolver, createBetterAuthCatchallController, identityFromUser, permissionsFromAcRole };
|
|
442
|
+
export { AuthGuard, BETTER_AUTH_ISSUER, BETTER_AUTH_OPTIONS, BETTER_AUTH_UPGRADE_TENANT, BetterAuthModule, BetterAuthService, BetterAuthUpgradeAuthenticator, CurrentSession, CurrentUser, OPTIONAL_AUTH_KEY, OptionalAuth, PUBLIC_KEY, Public, betterAuthAcResolver, createBetterAuthCatchallController, identityFromUser, permissionsFromAcRole };
|
|
390
443
|
|
|
391
444
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/decorators/public.decorator.ts","../src/base-path.ts","../src/better-auth.controller.ts","../src/better-auth.tokens.ts","../src/auth-request-state.ts","../src/session-data.ts","../src/decorators/optional-auth.decorator.ts","../src/guards/auth.guard.ts","../src/better-auth.module.ts","../src/decorators/current-user.decorator.ts","../src/decorators/current-session.decorator.ts","../src/authz-bridge.ts"],"sourcesContent":["import { Reflector } from '@velajs/vela';\n\nexport const Public = Reflector.createDecorator<boolean>({ key: 'vela.auth.public' });\nexport const PUBLIC_KEY = Public.KEY;\n","export const DEFAULT_BETTER_AUTH_BASE_PATH = '/api/auth';\n\n/** Validate and canonicalize the route prefix used by the public auth controller. */\nexport function normalizeBetterAuthBasePath(value?: string): string {\n const basePath = value ?? DEFAULT_BETTER_AUTH_BASE_PATH;\n if (\n basePath.length === 0 ||\n basePath !== basePath.trim() ||\n !basePath.startsWith('/') ||\n basePath.startsWith('//') ||\n basePath === '/' ||\n basePath.endsWith('/') ||\n /[\\\\?#*]/u.test(basePath) ||\n /%(?:2e|2f|5c)/iu.test(basePath)\n ) {\n throw new Error(\n '@velajs/better-auth: basePath must be a canonical absolute path such as \"/api/auth\"',\n );\n }\n\n let decoded: string;\n try {\n decoded = decodeURIComponent(basePath);\n } catch {\n throw new Error('@velajs/better-auth: basePath contains invalid percent encoding');\n }\n if (decoded.split('/').some((segment) => segment === '.' || segment === '..')) {\n throw new Error('@velajs/better-auth: basePath must not contain dot segments');\n }\n return basePath;\n}\n","import { All, Controller, Inject, Injectable, Req, type Type } from '@velajs/vela';\nimport type { Context } from 'hono';\nimport { BetterAuthService } from './better-auth.service';\nimport { Public } from './decorators/public.decorator';\nimport { normalizeBetterAuthBasePath } from './base-path';\n\n/**\n * Build a catch-all controller that mounts better-auth's handler at `basePath`\n * (default `/api/auth`). This is a factory because vela reads a controller's\n * route off the class at decoration time, so a custom base path needs its own\n * decorated class — the path can't be parametrized on a single shared class.\n *\n * Two base paths to keep consistent:\n * - this `basePath` is RELATIVE to vela's `globalPrefix` (always prepended);\n * - better-auth routes against its OWN absolute `basePath` (the one you pass to\n * `betterAuth({ basePath })`), which must equal `globalPrefix + basePath`.\n *\n * Both default to `/api/auth`, so the no-prefix / no-config case just works.\n */\nexport function createBetterAuthCatchallController(basePath: string = '/api/auth'): Type {\n const normalizedBasePath = normalizeBetterAuthBasePath(basePath);\n @Public(true)\n @Controller(normalizedBasePath)\n @Injectable()\n class BetterAuthCatchallController {\n // Inject the service — its `.handler` getter triggers lazy construction\n // of the underlying betterAuth() instance on first access, AFTER any\n // runtime adapter middleware (Cloudflare env capture) has run.\n constructor(@Inject(BetterAuthService) private readonly auth: BetterAuthService) {}\n\n @All('/*')\n async handle(@Req() c: Context): Promise<Response> {\n return this.auth.handler(c.req.raw);\n }\n }\n return BetterAuthCatchallController;\n}\n","import { InjectionToken } from '@velajs/vela';\nimport type { BetterAuthRuntimeOptions } from './better-auth.types';\n\nexport const BETTER_AUTH_OPTIONS = new InjectionToken<BetterAuthRuntimeOptions>(\n 'vela.BetterAuthOptions',\n);\n","import {\n clearTrustedRequestIdentity,\n createTrustedRequestIdentityStore,\n getTrustedContextRequest,\n setTrustedRequestIdentity,\n type ExecutionContext,\n} from '@velajs/vela';\nimport type { SessionData } from './session-data';\n\n// This map stores provider payload, never a second authentication authority.\n// A clear, expiry, or identity replacement makes the payload unreachable.\nconst sessions = createTrustedRequestIdentityStore<SessionData>();\n\nexport function beginAuthRequest(context: ExecutionContext): void {\n clearTrustedRequestIdentity(context.getRequest());\n}\n\nexport function authenticateRequest(\n context: ExecutionContext,\n data: SessionData,\n issuer: string,\n): void {\n const request = context.getRequest();\n setTrustedRequestIdentity(request, {\n principal: { issuer, subject: data.user.id, principalType: 'user' },\n expiresAtMs: data.session.expiresAt.getTime(),\n roles: data.roles,\n ...(data.tenantId === undefined ? {} : { tenantId: data.tenantId }),\n });\n sessions.set(request, data);\n}\n\nexport function getAuthRequestState(context: ExecutionContext): SessionData | undefined {\n const request = getTrustedContextRequest(context);\n return request ? sessions.get(request) : undefined;\n}\n","import type { Session, User } from './better-auth.types';\n\nexport interface SessionData {\n readonly user: User;\n readonly session: Session;\n readonly roles: readonly string[];\n readonly tenantId?: string;\n}\n\nfunction own(value: unknown, key: string): unknown {\n if (typeof value !== 'object' || value === null) return undefined;\n const descriptor = Object.getOwnPropertyDescriptor(value, key);\n return descriptor && 'value' in descriptor ? descriptor.value : undefined;\n}\n\nfunction requiredString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0;\n}\n\nfunction date(value: unknown): Date | undefined {\n return value instanceof Date && Number.isFinite(value.getTime()) ? new Date(value) : undefined;\n}\n\nfunction optionalString(value: unknown): value is string | null | undefined {\n return value === undefined || value === null || typeof value === 'string';\n}\n\n/** Keep plugin data opaque; base model fields below are validated individually. */\nfunction dataProperties(value: object): Record<string, unknown> {\n return Object.fromEntries(Object.keys(value).map((key) => [key, own(value, key)]));\n}\n\n/** Validate the full Better Auth base models, not merely ids masquerading as them. */\nexport function validateSessionData(value: unknown): SessionData | undefined {\n try {\n const user = own(value, 'user');\n const session = own(value, 'session');\n if (\n typeof user !== 'object' ||\n user === null ||\n typeof session !== 'object' ||\n session === null\n ) {\n return undefined;\n }\n const id = own(user, 'id');\n const email = own(user, 'email');\n const name = own(user, 'name');\n const emailVerified = own(user, 'emailVerified');\n const image = own(user, 'image');\n const createdAt = date(own(user, 'createdAt'));\n const updatedAt = date(own(user, 'updatedAt'));\n const sessionId = own(session, 'id');\n const userId = own(session, 'userId');\n const token = own(session, 'token');\n const sessionCreatedAt = date(own(session, 'createdAt'));\n const sessionUpdatedAt = date(own(session, 'updatedAt'));\n const expiresAt = date(own(session, 'expiresAt'));\n const ipAddress = own(session, 'ipAddress');\n const userAgent = own(session, 'userAgent');\n const organization = own(session, 'activeOrganizationId');\n const role = own(user, 'role');\n let roles: string[] = [];\n if (role !== undefined && role !== null) {\n if (typeof role === 'string')\n roles = role\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean);\n else if (\n Array.isArray(role) &&\n role.every((entry: unknown): entry is string => typeof entry === 'string')\n ) {\n roles = role.map((entry: string) => entry.trim()).filter(Boolean);\n } else return undefined;\n }\n if (\n !requiredString(id) ||\n !requiredString(email) ||\n typeof name !== 'string' ||\n typeof emailVerified !== 'boolean' ||\n !optionalString(image) ||\n !createdAt ||\n !updatedAt ||\n !requiredString(sessionId) ||\n userId !== id ||\n !requiredString(token) ||\n !sessionCreatedAt ||\n !sessionUpdatedAt ||\n !expiresAt ||\n expiresAt.getTime() <= Date.now() ||\n !optionalString(ipAddress) ||\n !optionalString(userAgent) ||\n (organization !== undefined && organization !== null && !requiredString(organization))\n )\n return undefined;\n return {\n user: Object.freeze({\n ...dataProperties(user),\n id,\n email,\n name,\n emailVerified,\n createdAt,\n updatedAt,\n image,\n }),\n session: Object.freeze({\n ...dataProperties(session),\n id: sessionId,\n userId: id,\n token,\n createdAt: sessionCreatedAt,\n updatedAt: sessionUpdatedAt,\n expiresAt,\n ipAddress,\n userAgent,\n }),\n roles: Object.freeze(roles),\n ...(typeof organization === 'string' ? { tenantId: organization } : {}),\n };\n } catch {\n return undefined;\n }\n}\n","import { Reflector } from '@velajs/vela';\n\nexport const OptionalAuth = Reflector.createDecorator<boolean>({ key: 'vela.auth.optional' });\nexport const OPTIONAL_AUTH_KEY = OptionalAuth.KEY;\n","import {\n Inject,\n Injectable,\n Reflector,\n UnauthorizedException,\n getTrustedContextRequest,\n type CanActivate,\n type ExecutionContext,\n} from '@velajs/vela';\nimport { getContextIdentity } from '@velajs/authz/vela';\nimport { BETTER_AUTH_OPTIONS } from '../better-auth.tokens';\nimport { BetterAuthService } from '../better-auth.service';\nimport type { BetterAuthRuntimeOptions } from '../better-auth.types';\nimport { authenticateRequest, beginAuthRequest, getAuthRequestState } from '../auth-request-state';\nimport { validateSessionData } from '../session-data';\nimport { OptionalAuth } from '../decorators/optional-auth.decorator';\nimport { Public } from '../decorators/public.decorator';\n\n@Injectable()\nexport class AuthGuard implements CanActivate {\n private readonly reflector = new Reflector();\n\n constructor(\n @Inject(BetterAuthService) private readonly auth: BetterAuthService,\n @Inject(BETTER_AUTH_OPTIONS) private readonly opts: BetterAuthRuntimeOptions,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n // Frames trust only the authenticated connection attachment; HTTP cookies\n // and request payload never become ambient WebSocket authentication.\n if (context.getType() === 'ws') {\n if (getContextIdentity(context)) return true;\n throw new UnauthorizedException('Authentication required');\n }\n // Custom HTTP-backed dispatchers authenticate once at the outer boundary.\n // Never re-read credentials or clear shared authority across sibling fields.\n if (context.getType() !== 'http') {\n if (\n getTrustedContextRequest(context) &&\n (getAuthRequestState(context) ||\n this.reflector.getAllAndOverride(Public, context) ||\n this.reflector.getAllAndOverride(OptionalAuth, context))\n )\n return true;\n throw new UnauthorizedException('Authentication required');\n }\n beginAuthRequest(context);\n if (this.reflector.getAllAndOverride(Public, context)) return true;\n\n const raw = await this.auth.api.getSession({ headers: context.getRequest().headers });\n const data = validateSessionData(raw);\n if (data) {\n authenticateRequest(context, data, this.opts.issuer ?? 'better-auth');\n return true;\n }\n if (this.reflector.getAllAndOverride(OptionalAuth, context)) return true;\n throw new UnauthorizedException('Authentication required');\n }\n}\n","import {\n defineProvider,\n provideGlobal,\n stableHash,\n type DynamicModule,\n type InferTokens,\n type Token,\n type Type,\n} from '@velajs/vela';\nimport { createBetterAuthCatchallController } from './better-auth.controller';\nimport { BetterAuthService, BETTER_AUTH_BUILDER } from './better-auth.service';\nimport { BETTER_AUTH_OPTIONS } from './better-auth.tokens';\nimport type {\n BetterAuthInstance,\n BetterAuthModuleOptions,\n BetterAuthRuntimeOptions,\n} from './better-auth.types';\nimport { AuthGuard } from './guards/auth.guard';\nimport { normalizeBetterAuthBasePath } from './base-path';\n\nconst referenceIds = new WeakMap<object, number>();\nconst explicitKeyClaims = new Map<\n string,\n { readonly kind: 'auth' | 'factory'; readonly reference: object; readonly shape: string }\n>();\nlet nextReferenceId = 1;\n\nfunction referenceId(reference: object): number {\n const existing = referenceIds.get(reference);\n if (existing !== undefined) return existing;\n const id = nextReferenceId++;\n referenceIds.set(reference, id);\n return id;\n}\n\nfunction claimExplicitKey(\n key: string,\n kind: 'auth' | 'factory',\n reference: object,\n shape: string,\n): string {\n if (key.length === 0 || key !== key.trim()) {\n throw new Error('@velajs/better-auth: an explicit module key must be a non-empty string');\n }\n const existing = explicitKeyClaims.get(key);\n if (\n existing !== undefined &&\n (existing.kind !== kind || existing.reference !== reference || existing.shape !== shape)\n ) {\n throw new Error(\n `@velajs/better-auth: explicit module key \"${key}\" is already bound to a different auth registration`,\n );\n }\n explicitKeyClaims.set(key, { kind, reference, shape });\n return `explicit:${key}:ref:${referenceId(reference)}`;\n}\n\n/** Structural options with defaults applied (everything but the auth instance). */\ninterface NormalizedOptions {\n basePath: string;\n issuer: string;\n isGlobal: boolean;\n mountHandler: boolean;\n}\n\nfunction normalize(options: BetterAuthRuntimeOptions): NormalizedOptions {\n const basePath = normalizeBetterAuthBasePath(options.basePath);\n const issuer = options.issuer ?? `better-auth:${basePath}`;\n if (issuer.length === 0 || issuer !== issuer.trim()) {\n throw new Error('@velajs/better-auth: issuer must be a non-empty stable namespace');\n }\n return {\n basePath,\n issuer,\n isGlobal: options.isGlobal ?? true,\n mountHandler: options.mountHandler ?? true,\n };\n}\n\n/** Providers, controllers, and exports shared by both entry points. */\nfunction commonContributions(n: NormalizedOptions): {\n providers: NonNullable<DynamicModule['providers']>;\n controllers: Type[];\n exports: DynamicModule['exports'];\n} {\n return {\n providers: [BetterAuthService, AuthGuard],\n controllers: n.mountHandler ? [createBetterAuthCatchallController(n.basePath)] : [],\n exports: [BetterAuthService, BETTER_AUTH_OPTIONS, AuthGuard],\n };\n}\n\n/**\n * Options for {@link BetterAuthModule.forRootAsync}.\n *\n * The `Inject` type parameter captures the literal `inject` tuple at the call\n * site (via `const` inference) so `useFactory` parameters are typed from the\n * inject array, position-by-position — no `as const`, no `(...deps: any[])`:\n *\n * ```ts\n * BetterAuthModule.forRootAsync({\n * inject: [WORKER_ENV, ConfigService], // captured as readonly tuple\n * useFactory: (env, config) => // inferred from the tokens\n * betterAuth({ database: drizzleAdapter(drizzle(env.DB), ...) }),\n * });\n * ```\n */\ninterface ForRootAsyncOptions<Inject extends readonly Token[] = readonly Token[]> {\n inject: Inject;\n imports?: DynamicModule['imports'];\n useFactory: (...deps: InferTokens<Inject>) => BetterAuthInstance;\n isGlobal?: boolean;\n mountHandler?: boolean;\n basePath?: string;\n issuer?: string;\n key?: string;\n}\n\nexport class BetterAuthModule {\n /**\n * Synchronous registration. The auth instance is constructed by the consumer\n * at module-load time and passed in directly. Use this when the inputs to\n * `betterAuth({...})` are available at startup (Node apps with a static DB\n * connection, in-memory adapters, etc.).\n */\n static forRoot(\n options: BetterAuthModuleOptions & { isGlobal?: boolean; key?: string },\n ): DynamicModule {\n const normalized = normalize(options);\n const shape = stableHash(normalized);\n const key =\n options.key === undefined\n ? `${shape}:auth:${referenceId(options.auth)}`\n : claimExplicitKey(options.key, 'auth', options.auth, shape);\n const common = commonContributions(normalized);\n return {\n module: BetterAuthModule,\n key,\n providers: [\n defineProvider(BETTER_AUTH_OPTIONS, { useValue: normalized }),\n defineProvider(BETTER_AUTH_BUILDER, { useValue: () => options.auth }),\n ...common.providers,\n ...(normalized.isGlobal ? provideGlobal('guard', AuthGuard) : []),\n ],\n controllers: common.controllers,\n exports: common.exports,\n };\n }\n\n /**\n * Deferred / DI-driven registration. The user factory runs **lazily**, on the\n * first time anything reads `BetterAuthService.auth` (or `.api` / `.handler`).\n * In normal request handling that's `AuthGuard.canActivate` or the catch-all\n * controller's `.handle`. At module load the factory does NOT run — it's only\n * captured in the checked builder provider. The Workers adapter supplies its\n * native environment before DI; the lazily constructed auth instance belongs\n * to that environment's application and never captures another app's bindings.\n */\n static forRootAsync<const Inject extends readonly Token[] = readonly Token[]>(\n options: ForRootAsyncOptions<Inject>,\n ): DynamicModule {\n const n = normalize(options);\n const common = commonContributions(n);\n const shape = stableHash({ ...n, inject: options.inject });\n const key =\n options.key === undefined\n ? `${shape}:factory:${referenceId(options.useFactory)}`\n : claimExplicitKey(options.key, 'factory', options.useFactory, shape);\n return {\n module: BetterAuthModule,\n key,\n imports: options.imports ?? [],\n providers: [\n defineProvider(BETTER_AUTH_OPTIONS, { useValue: n }),\n defineProvider(BETTER_AUTH_BUILDER, {\n inject: options.inject,\n useFactory:\n (...deps: InferTokens<Inject>) =>\n () =>\n options.useFactory(...deps),\n }),\n ...common.providers,\n ...(n.isGlobal ? provideGlobal('guard', AuthGuard) : []),\n ],\n controllers: common.controllers,\n exports: common.exports,\n };\n }\n}\n","import { createParamDecorator, type ExecutionContext } from '@velajs/vela';\nimport { getAuthRequestState } from '../auth-request-state';\nimport type { User } from '../better-auth.types';\n\nexport const CurrentUser = createParamDecorator(\n (_data: unknown, ctx: ExecutionContext): User | undefined => {\n const state = getAuthRequestState(ctx);\n return state?.user;\n },\n);\n","import { createParamDecorator, type ExecutionContext } from '@velajs/vela';\nimport { getAuthRequestState } from '../auth-request-state';\nimport type { Session } from '../better-auth.types';\n\nexport const CurrentSession = createParamDecorator(\n (_data: unknown, ctx: ExecutionContext): Session | undefined => {\n const state = getAuthRequestState(ctx);\n return state?.session;\n },\n);\n","import type { Identity, PermissionResolver } from '@velajs/authz';\nimport type { User } from './better-auth.types';\n\n/**\n * A better-auth user carrying the optional `role` field contributed by the\n * admin plugin. `role` may be a single role, a comma-separated list, or an\n * array — {@link identityFromUser} normalizes all three.\n */\nexport type AuthUser = Pick<User, 'id'> & { role?: string | string[] | null };\n\n/** Stable issuer namespace used for better-auth session principals. */\nexport const BETTER_AUTH_ISSUER = 'better-auth';\n\nconst normalizeRoles = (role: string | string[] | null | undefined): string[] => {\n if (!role) return [];\n // Strip empty entries and return a fresh array (never alias the caller's\n // input), matching the comma-string path below.\n if (Array.isArray(role)) return role.filter(Boolean);\n return role\n .split(',')\n .map((r) => r.trim())\n .filter(Boolean);\n};\n\n/**\n * Pure authorization projection; this does not authenticate or publish trusted state.\n * Adapts an already verified better-auth user into a stable `@velajs/authz` {@link Identity}.\n * The issuer scopes `user.id` as both `subject` and the compatibility `userId`;\n * the admin-plugin `role` field supplies local roles.\n *\n * Fail-closed: a missing user (`null`/`undefined`, i.e. an unauthenticated\n * request) maps to the zero-privilege identity `{ roles: [] }`, so downstream\n * `can()` checks grant nothing.\n */\nexport const identityFromUser = (\n user: AuthUser | null | undefined,\n issuer: string = BETTER_AUTH_ISSUER,\n principalType: 'user' | 'service' = 'user',\n): Identity => {\n if (!user || typeof user.id !== 'string' || user.id.length === 0) return { roles: [] };\n if (issuer.length === 0)\n throw new Error('@velajs/better-auth: identity issuer must be non-empty');\n return {\n issuer,\n subject: user.id,\n principalType,\n userId: user.id,\n roles: normalizeRoles(user.role),\n };\n};\n\n/**\n * The minimal slice of a better-auth access-control role consumed here. Both\n * `createAccessControl(...).newRole(...)` and the standalone `role(...)` return\n * `{ authorize, statements }`; `statements` is the `{ resource: actions[] }`\n * grant map for that role — the only accessor {@link betterAuthAcResolver}\n * reads.\n */\nexport interface BetterAuthAcRole {\n readonly statements: Readonly<Record<string, readonly string[]>>;\n}\n\n/**\n * Flattens a better-auth AC role's `statements` into `resource:action`\n * permission strings — the granted-side format `@velajs/authz` matches\n * (wildcards included).\n */\nexport const permissionsFromAcRole = (role: BetterAuthAcRole): string[] => {\n const permissions: string[] = [];\n for (const [resource, actions] of Object.entries(role.statements ?? {})) {\n for (const action of actions ?? []) permissions.push(`${resource}:${action}`);\n }\n return permissions;\n};\n\n/**\n * Builds a fail-closed `@velajs/authz` {@link PermissionResolver} from a\n * better-auth access-control role table (`{ roleName: acRole }` — the same map\n * shape passed to better-auth's admin/organization plugins). An identity's\n * `roles` are unioned into their granted permission strings; unknown roles\n * contribute nothing.\n *\n * ```ts\n * const ac = createAccessControl({ posts: ['read', 'write'] });\n * const authz = createAuthz({\n * resolver: betterAuthAcResolver({ editor: ac.newRole({ posts: ['write'] }) }),\n * });\n * await authz.can(identityFromUser(user), 'posts:write');\n * ```\n */\nexport const betterAuthAcResolver = (\n roles: Readonly<Record<string, BetterAuthAcRole>>,\n): PermissionResolver => {\n const grantsByRole = new Map<string, string[]>();\n for (const [name, role] of Object.entries(roles)) {\n grantsByRole.set(name, permissionsFromAcRole(role));\n }\n return {\n grants(identity: Identity): Set<string> {\n const out = new Set<string>();\n for (const name of identity.roles ?? []) {\n for (const permission of grantsByRole.get(name) ?? []) out.add(permission);\n }\n return out;\n },\n };\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/decorators/public.decorator.ts","../src/base-path.ts","../src/better-auth.controller.ts","../src/better-auth.tokens.ts","../src/auth-request-state.ts","../src/session-data.ts","../src/decorators/optional-auth.decorator.ts","../src/guards/auth.guard.ts","../src/better-auth.module.ts","../src/upgrade-authenticator.ts","../src/decorators/current-user.decorator.ts","../src/decorators/current-session.decorator.ts","../src/authz-bridge.ts"],"sourcesContent":["import { Reflector } from '@velajs/vela';\n\nexport const Public = Reflector.createDecorator<boolean>({ key: 'vela.auth.public' });\nexport const PUBLIC_KEY = Public.KEY;\n","export const DEFAULT_BETTER_AUTH_BASE_PATH = '/api/auth';\n\n/** Validate and canonicalize the route prefix used by the public auth controller. */\nexport function normalizeBetterAuthBasePath(value?: string): string {\n const basePath = value ?? DEFAULT_BETTER_AUTH_BASE_PATH;\n if (\n basePath.length === 0 ||\n basePath !== basePath.trim() ||\n !basePath.startsWith('/') ||\n basePath.startsWith('//') ||\n basePath === '/' ||\n basePath.endsWith('/') ||\n /[\\\\?#*]/u.test(basePath) ||\n /%(?:2e|2f|5c)/iu.test(basePath)\n ) {\n throw new Error(\n '@velajs/better-auth: basePath must be a canonical absolute path such as \"/api/auth\"',\n );\n }\n\n let decoded: string;\n try {\n decoded = decodeURIComponent(basePath);\n } catch {\n throw new Error('@velajs/better-auth: basePath contains invalid percent encoding');\n }\n if (decoded.split('/').some((segment) => segment === '.' || segment === '..')) {\n throw new Error('@velajs/better-auth: basePath must not contain dot segments');\n }\n return basePath;\n}\n","import { All, Controller, Inject, Req, type Type } from '@velajs/vela';\nimport type { Context } from 'hono';\nimport { BetterAuthService } from './better-auth.service';\nimport { Public } from './decorators/public.decorator';\nimport { normalizeBetterAuthBasePath } from './base-path';\n\n/**\n * Build a catch-all controller that mounts better-auth's handler at `basePath`\n * (default `/api/auth`). This is a factory because vela reads a controller's\n * route off the class at decoration time, so a custom base path needs its own\n * decorated class — the path can't be parametrized on a single shared class.\n *\n * Two base paths to keep consistent:\n * - this `basePath` is RELATIVE to vela's `globalPrefix` (always prepended);\n * - better-auth routes against its OWN absolute `basePath` (the one you pass to\n * `betterAuth({ basePath })`), which must equal `globalPrefix + basePath`.\n *\n * Both default to `/api/auth`, so the no-prefix / no-config case just works.\n */\nexport function createBetterAuthCatchallController(basePath: string = '/api/auth'): Type {\n const normalizedBasePath = normalizeBetterAuthBasePath(basePath);\n @Public(true)\n @Controller(normalizedBasePath)\n class BetterAuthCatchallController {\n // Inject the service — its `.handler` getter triggers lazy construction\n // of the underlying betterAuth() instance on first access, AFTER any\n // runtime adapter middleware (Cloudflare env capture) has run.\n constructor(@Inject(BetterAuthService) private readonly auth: BetterAuthService) {}\n\n @All('/*')\n async handle(@Req() c: Context): Promise<Response> {\n return this.auth.handler(c.req.raw);\n }\n }\n return BetterAuthCatchallController;\n}\n","import { InjectionToken } from '@velajs/vela';\nimport type { BetterAuthRuntimeOptions } from './better-auth.types';\n\nexport const BETTER_AUTH_OPTIONS = new InjectionToken<BetterAuthRuntimeOptions>(\n 'vela.BetterAuthOptions',\n);\n","import {\n clearTrustedRequestIdentity,\n createTrustedRequestIdentityStore,\n getTrustedContextRequest,\n setTrustedRequestIdentity,\n type ExecutionContext,\n} from '@velajs/vela';\nimport type { SessionData } from './session-data';\n\n// This map stores provider payload, never a second authentication authority.\n// A clear, expiry, or identity replacement makes the payload unreachable.\nconst sessions = createTrustedRequestIdentityStore<SessionData>();\n\nexport function beginAuthRequest(context: ExecutionContext): void {\n clearTrustedRequestIdentity(context.getRequest());\n}\n\nexport function authenticateRequest(\n context: ExecutionContext,\n data: SessionData,\n issuer: string,\n): void {\n const request = context.getRequest();\n setTrustedRequestIdentity(request, {\n principal: { issuer, subject: data.user.id, principalType: 'user' },\n expiresAtMs: data.session.expiresAt.getTime(),\n roles: data.roles,\n ...(data.tenantId === undefined ? {} : { tenantId: data.tenantId }),\n });\n sessions.set(request, data);\n}\n\nexport function getAuthRequestState(context: ExecutionContext): SessionData | undefined {\n const request = getTrustedContextRequest(context);\n return request ? sessions.get(request) : undefined;\n}\n","import type { Session, User } from './better-auth.types';\n\nexport interface SessionData {\n readonly user: User;\n readonly session: Session;\n readonly roles: readonly string[];\n readonly tenantId?: string;\n}\n\nfunction own(value: unknown, key: string): unknown {\n if (typeof value !== 'object' || value === null) return undefined;\n const descriptor = Object.getOwnPropertyDescriptor(value, key);\n return descriptor && 'value' in descriptor ? descriptor.value : undefined;\n}\n\nfunction requiredString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0;\n}\n\nfunction date(value: unknown): Date | undefined {\n return value instanceof Date && Number.isFinite(value.getTime()) ? new Date(value) : undefined;\n}\n\nfunction optionalString(value: unknown): value is string | null | undefined {\n return value === undefined || value === null || typeof value === 'string';\n}\n\n/** Keep plugin data opaque; base model fields below are validated individually. */\nfunction dataProperties(value: object): Record<string, unknown> {\n return Object.fromEntries(Object.keys(value).map((key) => [key, own(value, key)]));\n}\n\n/** Validate the full Better Auth base models, not merely ids masquerading as them. */\nexport function validateSessionData(value: unknown): SessionData | undefined {\n try {\n const user = own(value, 'user');\n const session = own(value, 'session');\n if (\n typeof user !== 'object' ||\n user === null ||\n typeof session !== 'object' ||\n session === null\n ) {\n return undefined;\n }\n const id = own(user, 'id');\n const email = own(user, 'email');\n const name = own(user, 'name');\n const emailVerified = own(user, 'emailVerified');\n const image = own(user, 'image');\n const createdAt = date(own(user, 'createdAt'));\n const updatedAt = date(own(user, 'updatedAt'));\n const sessionId = own(session, 'id');\n const userId = own(session, 'userId');\n const token = own(session, 'token');\n const sessionCreatedAt = date(own(session, 'createdAt'));\n const sessionUpdatedAt = date(own(session, 'updatedAt'));\n const expiresAt = date(own(session, 'expiresAt'));\n const ipAddress = own(session, 'ipAddress');\n const userAgent = own(session, 'userAgent');\n const organization = own(session, 'activeOrganizationId');\n const role = own(user, 'role');\n let roles: string[] = [];\n if (role !== undefined && role !== null) {\n if (typeof role === 'string')\n roles = role\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean);\n else if (\n Array.isArray(role) &&\n role.every((entry: unknown): entry is string => typeof entry === 'string')\n ) {\n roles = role.map((entry: string) => entry.trim()).filter(Boolean);\n } else return undefined;\n }\n if (\n !requiredString(id) ||\n !requiredString(email) ||\n typeof name !== 'string' ||\n typeof emailVerified !== 'boolean' ||\n !optionalString(image) ||\n !createdAt ||\n !updatedAt ||\n !requiredString(sessionId) ||\n userId !== id ||\n !requiredString(token) ||\n !sessionCreatedAt ||\n !sessionUpdatedAt ||\n !expiresAt ||\n expiresAt.getTime() <= Date.now() ||\n !optionalString(ipAddress) ||\n !optionalString(userAgent) ||\n (organization !== undefined && organization !== null && !requiredString(organization))\n )\n return undefined;\n return {\n user: Object.freeze({\n ...dataProperties(user),\n id,\n email,\n name,\n emailVerified,\n createdAt,\n updatedAt,\n image,\n }),\n session: Object.freeze({\n ...dataProperties(session),\n id: sessionId,\n userId: id,\n token,\n createdAt: sessionCreatedAt,\n updatedAt: sessionUpdatedAt,\n expiresAt,\n ipAddress,\n userAgent,\n }),\n roles: Object.freeze(roles),\n ...(typeof organization === 'string' ? { tenantId: organization } : {}),\n };\n } catch {\n return undefined;\n }\n}\n","import { Reflector } from '@velajs/vela';\n\nexport const OptionalAuth = Reflector.createDecorator<boolean>({ key: 'vela.auth.optional' });\nexport const OPTIONAL_AUTH_KEY = OptionalAuth.KEY;\n","import {\n Inject,\n Injectable,\n Reflector,\n UnauthorizedException,\n getTrustedContextRequest,\n type CanActivate,\n type ExecutionContext,\n} from '@velajs/vela';\nimport { getContextIdentity } from '@velajs/authz/vela';\nimport { BETTER_AUTH_OPTIONS } from '../better-auth.tokens';\nimport { BetterAuthService } from '../better-auth.service';\nimport type { BetterAuthRuntimeOptions } from '../better-auth.types';\nimport { authenticateRequest, beginAuthRequest, getAuthRequestState } from '../auth-request-state';\nimport { validateSessionData } from '../session-data';\nimport { OptionalAuth } from '../decorators/optional-auth.decorator';\nimport { Public } from '../decorators/public.decorator';\n\n@Injectable()\nexport class AuthGuard implements CanActivate {\n constructor(\n @Inject(BetterAuthService) private readonly auth: BetterAuthService,\n @Inject(BETTER_AUTH_OPTIONS) private readonly opts: BetterAuthRuntimeOptions,\n @Inject(Reflector) private readonly reflector: Reflector,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n // Frames trust only the authenticated connection attachment; HTTP cookies\n // and request payload never become ambient WebSocket authentication.\n if (context.getType() === 'ws') {\n if (getContextIdentity(context)) return true;\n throw new UnauthorizedException('Authentication required');\n }\n // Custom HTTP-backed dispatchers authenticate once at the outer boundary.\n // Never re-read credentials or clear shared authority across sibling fields.\n if (context.getType() !== 'http') {\n if (\n getTrustedContextRequest(context) &&\n (getAuthRequestState(context) ||\n this.reflector.getAllAndOverride(Public, context) ||\n this.reflector.getAllAndOverride(OptionalAuth, context))\n )\n return true;\n throw new UnauthorizedException('Authentication required');\n }\n beginAuthRequest(context);\n if (this.reflector.getAllAndOverride(Public, context)) return true;\n\n const raw = await this.auth.api.getSession({ headers: context.getRequest().headers });\n const data = validateSessionData(raw);\n if (data) {\n authenticateRequest(context, data, this.opts.issuer ?? 'better-auth');\n return true;\n }\n if (this.reflector.getAllAndOverride(OptionalAuth, context)) return true;\n throw new UnauthorizedException('Authentication required');\n }\n}\n","import {\n defineProvider,\n lazyProvider,\n provideGlobal,\n stableHash,\n type DynamicModule,\n type FactoryInject,\n type InferTokens,\n type Token,\n type Type,\n} from '@velajs/vela';\nimport { createBetterAuthCatchallController } from './better-auth.controller';\nimport { BetterAuthService, BETTER_AUTH_BUILDER } from './better-auth.service';\nimport { BETTER_AUTH_OPTIONS } from './better-auth.tokens';\nimport type {\n BetterAuthInstance,\n BetterAuthModuleOptions,\n BetterAuthRuntimeOptions,\n} from './better-auth.types';\nimport { AuthGuard } from './guards/auth.guard';\nimport { normalizeBetterAuthBasePath } from './base-path';\n\nconst referenceIds = new WeakMap<object, number>();\nlet nextReferenceId = 1;\n\nfunction referenceId(reference: object): number {\n const existing = referenceIds.get(reference);\n if (existing !== undefined) return existing;\n const id = nextReferenceId++;\n referenceIds.set(reference, id);\n return id;\n}\n\n/**\n * An explicit key names a registration without widening its identity: the\n * options shape and auth reference stay in the module key, so the same\n * registration deduplicates within an application while a different one\n * (including a root rebuilt per environment) becomes its own instance. No\n * process-wide state is kept, so repeated bootstraps in one isolate succeed.\n */\nfunction explicitKey(key: string, shape: string, reference: object): string {\n if (key.length === 0 || key !== key.trim()) {\n throw new Error('@velajs/better-auth: an explicit module key must be a non-empty string');\n }\n return `explicit:${key}:${shape}:ref:${referenceId(reference)}`;\n}\n\n/** Structural options with defaults applied (everything but the auth instance). */\ninterface NormalizedOptions {\n basePath: string;\n issuer: string;\n isGlobal: boolean;\n mountHandler: boolean;\n}\n\nfunction normalize(options: BetterAuthRuntimeOptions): NormalizedOptions {\n const basePath = normalizeBetterAuthBasePath(options.basePath);\n const issuer = options.issuer ?? `better-auth:${basePath}`;\n if (issuer.length === 0 || issuer !== issuer.trim()) {\n throw new Error('@velajs/better-auth: issuer must be a non-empty stable namespace');\n }\n return {\n basePath,\n issuer,\n isGlobal: options.isGlobal ?? true,\n mountHandler: options.mountHandler ?? true,\n };\n}\n\n/** Providers, controllers, and exports shared by both entry points. */\nfunction commonContributions(n: NormalizedOptions): {\n providers: NonNullable<DynamicModule['providers']>;\n controllers: Type[];\n exports: DynamicModule['exports'];\n} {\n return {\n providers: [BetterAuthService, AuthGuard],\n controllers: n.mountHandler ? [createBetterAuthCatchallController(n.basePath)] : [],\n exports: [BetterAuthService, BETTER_AUTH_OPTIONS, AuthGuard],\n };\n}\n\n/**\n * Options for {@link BetterAuthModule.forRootAsync}.\n *\n * The `Inject` type parameter captures the literal `inject` tuple at the call\n * site (via `const` inference) so `useFactory` parameters are typed from the\n * inject array, position-by-position — no `as const`, no `(...deps: any[])`:\n *\n * ```ts\n * BetterAuthModule.forRootAsync({\n * inject: [ENV, ConfigService], // captured as readonly tuple\n * useFactory: (env, config) => // inferred from the tokens\n * betterAuth({ database: drizzleAdapter(drizzle(env.DB), ...) }),\n * });\n * ```\n *\n * A factory without parameters may omit `inject`.\n */\ntype ForRootAsyncOptions<Inject extends readonly Token[] = readonly Token[]> = {\n imports?: DynamicModule['imports'];\n useFactory: (...deps: InferTokens<Inject>) => BetterAuthInstance;\n isGlobal?: boolean;\n mountHandler?: boolean;\n basePath?: string;\n issuer?: string;\n key?: string;\n} & FactoryInject<Inject>;\n\nexport class BetterAuthModule {\n /**\n * Synchronous registration. The auth instance is constructed by the consumer\n * at module-load time and passed in directly. Use this when the inputs to\n * `betterAuth({...})` are available at startup (Node apps with a static DB\n * connection, in-memory adapters, etc.).\n */\n static forRoot(\n options: BetterAuthModuleOptions & { isGlobal?: boolean; key?: string },\n ): DynamicModule {\n const normalized = normalize(options);\n const shape = stableHash(normalized);\n const key =\n options.key === undefined\n ? `${shape}:auth:${referenceId(options.auth)}`\n : explicitKey(options.key, shape, options.auth);\n const common = commonContributions(normalized);\n return {\n module: BetterAuthModule,\n key,\n providers: [\n defineProvider(BETTER_AUTH_OPTIONS, { useValue: normalized }),\n defineProvider(BETTER_AUTH_BUILDER, { useValue: () => options.auth }),\n ...common.providers,\n ...(normalized.isGlobal ? provideGlobal('guard', AuthGuard) : []),\n ],\n controllers: common.controllers,\n exports: common.exports,\n };\n }\n\n /**\n * Deferred / DI-driven registration. The user factory runs **lazily**, on the\n * first time anything reads `BetterAuthService.auth` (or `.api` / `.handler`).\n * In normal request handling that's `AuthGuard.canActivate` or the catch-all\n * controller's `.handle`. At module load the factory does NOT run — it's only\n * captured in the checked builder provider. The Workers adapter supplies its\n * native environment before DI; the lazily constructed auth instance belongs\n * to that environment's application and never captures another app's bindings.\n */\n static forRootAsync<const Inject extends readonly Token[] = readonly Token[]>(\n options: ForRootAsyncOptions<Inject>,\n ): DynamicModule {\n const n = normalize(options);\n const common = commonContributions(n);\n const shape = stableHash({ ...n, inject: options.inject });\n const key =\n options.key === undefined\n ? `${shape}:factory:${referenceId(options.useFactory)}`\n : explicitKey(options.key, shape, options.useFactory);\n return {\n module: BetterAuthModule,\n key,\n imports: options.imports ?? [],\n providers: [\n defineProvider(BETTER_AUTH_OPTIONS, { useValue: n }),\n // Unmemoized: BetterAuthService caches the instance it builds.\n lazyProvider<BetterAuthInstance, Inject>({\n ...options,\n provide: BETTER_AUTH_BUILDER,\n memoize: false,\n }),\n ...common.providers,\n ...(n.isGlobal ? provideGlobal('guard', AuthGuard) : []),\n ],\n controllers: common.controllers,\n exports: common.exports,\n };\n }\n}\n","import { Inject, Injectable, InjectionToken, Optional } from '@velajs/vela';\nimport type {\n UpgradeAuthenticator,\n WebSocketUpgradeAuthenticationContext,\n WebSocketUpgradeIdentity,\n} from '@velajs/vela/websocket';\nimport { BetterAuthService } from './better-auth.service';\nimport { BETTER_AUTH_OPTIONS } from './better-auth.tokens';\nimport type { BetterAuthRuntimeOptions, Session, User } from './better-auth.types';\nimport { validateSessionData } from './session-data';\n\n/** The validated Better Auth session behind a WebSocket upgrade. */\nexport interface BetterAuthUpgradeSession {\n readonly user: User;\n readonly session: Session;\n}\n\n/**\n * Chooses the tenant a WebSocket connection joins. Returning no tenant (or\n * throwing) refuses the upgrade.\n */\nexport type BetterAuthUpgradeTenantResolver = (\n session: BetterAuthUpgradeSession,\n context: WebSocketUpgradeAuthenticationContext,\n request: Request,\n) => string | undefined | Promise<string | undefined>;\n\n/**\n * Optional tenant resolver for {@link BetterAuthUpgradeAuthenticator}. Provide it\n * in the module that declares the gateway; without one, the session's active\n * organization is the tenant.\n */\nexport const BETTER_AUTH_UPGRADE_TENANT = new InjectionToken<BetterAuthUpgradeTenantResolver>(\n 'vela.better-auth.UpgradeTenant',\n);\n\n/**\n * WebSocket upgrade authenticator backed by the Better Auth session cookie:\n * `@WebSocketGateway({ authenticator: BetterAuthUpgradeAuthenticator })`.\n *\n * The principal uses the `BetterAuthModule` issuer, as `AuthGuard` does for\n * HTTP. The identity expires with the session. Every WebSocket identity carries\n * a tenant, so a session without an active organization is refused unless a\n * {@link BETTER_AUTH_UPGRADE_TENANT} resolver supplies one.\n */\n@Injectable()\nexport class BetterAuthUpgradeAuthenticator implements UpgradeAuthenticator {\n constructor(\n @Inject(BetterAuthService) private readonly auth: BetterAuthService,\n @Inject(BETTER_AUTH_OPTIONS) private readonly options: BetterAuthRuntimeOptions,\n @Optional()\n @Inject(BETTER_AUTH_UPGRADE_TENANT)\n private readonly resolveTenant?: BetterAuthUpgradeTenantResolver,\n ) {}\n\n async authenticate(\n request: Request,\n context: WebSocketUpgradeAuthenticationContext,\n ): Promise<WebSocketUpgradeIdentity | false> {\n const data = validateSessionData(await this.auth.api.getSession({ headers: request.headers }));\n if (!data) return false;\n const tenantId = this.resolveTenant\n ? await this.resolveTenant({ user: data.user, session: data.session }, context, request)\n : data.tenantId;\n if (typeof tenantId !== 'string' || tenantId.length === 0) return false;\n return {\n principal: {\n issuer: this.options.issuer ?? 'better-auth',\n subject: data.user.id,\n principalType: 'user',\n },\n tenantId,\n expiresAtMs: data.session.expiresAt.getTime(),\n };\n }\n}\n","import { createParamDecorator, type ExecutionContext } from '@velajs/vela';\nimport { getAuthRequestState } from '../auth-request-state';\nimport type { User } from '../better-auth.types';\n\nexport const CurrentUser = createParamDecorator(\n (_data: unknown, ctx: ExecutionContext): User | undefined => {\n const state = getAuthRequestState(ctx);\n return state?.user;\n },\n);\n","import { createParamDecorator, type ExecutionContext } from '@velajs/vela';\nimport { getAuthRequestState } from '../auth-request-state';\nimport type { Session } from '../better-auth.types';\n\nexport const CurrentSession = createParamDecorator(\n (_data: unknown, ctx: ExecutionContext): Session | undefined => {\n const state = getAuthRequestState(ctx);\n return state?.session;\n },\n);\n","import type { Identity, PermissionResolver } from '@velajs/authz';\nimport type { User } from './better-auth.types';\n\n/**\n * A better-auth user carrying the optional `role` field contributed by the\n * admin plugin. `role` may be a single role, a comma-separated list, or an\n * array — {@link identityFromUser} normalizes all three.\n */\nexport type AuthUser = Pick<User, 'id'> & { role?: string | string[] | null };\n\n/** Stable issuer namespace used for better-auth session principals. */\nexport const BETTER_AUTH_ISSUER = 'better-auth';\n\nconst normalizeRoles = (role: string | string[] | null | undefined): string[] => {\n if (!role) return [];\n // Strip empty entries and return a fresh array (never alias the caller's\n // input), matching the comma-string path below.\n if (Array.isArray(role)) return role.filter(Boolean);\n return role\n .split(',')\n .map((r) => r.trim())\n .filter(Boolean);\n};\n\n/**\n * Pure authorization projection; this does not authenticate or publish trusted state.\n * Adapts an already verified better-auth user into a stable `@velajs/authz` {@link Identity}.\n * The issuer scopes `user.id` as both `subject` and the compatibility `userId`;\n * the admin-plugin `role` field supplies local roles.\n *\n * Fail-closed: a missing user (`null`/`undefined`, i.e. an unauthenticated\n * request) maps to the zero-privilege identity `{ roles: [] }`, so downstream\n * `can()` checks grant nothing.\n */\nexport const identityFromUser = (\n user: AuthUser | null | undefined,\n issuer: string = BETTER_AUTH_ISSUER,\n principalType: 'user' | 'service' = 'user',\n): Identity => {\n if (!user || typeof user.id !== 'string' || user.id.length === 0) return { roles: [] };\n if (issuer.length === 0)\n throw new Error('@velajs/better-auth: identity issuer must be non-empty');\n return {\n issuer,\n subject: user.id,\n principalType,\n userId: user.id,\n roles: normalizeRoles(user.role),\n };\n};\n\n/**\n * The minimal slice of a better-auth access-control role consumed here. Both\n * `createAccessControl(...).newRole(...)` and the standalone `role(...)` return\n * `{ authorize, statements }`; `statements` is the `{ resource: actions[] }`\n * grant map for that role — the only accessor {@link betterAuthAcResolver}\n * reads.\n */\nexport interface BetterAuthAcRole {\n readonly statements: Readonly<Record<string, readonly string[]>>;\n}\n\n/**\n * Flattens a better-auth AC role's `statements` into `resource:action`\n * permission strings — the granted-side format `@velajs/authz` matches\n * (wildcards included).\n */\nexport const permissionsFromAcRole = (role: BetterAuthAcRole): string[] => {\n const permissions: string[] = [];\n for (const [resource, actions] of Object.entries(role.statements ?? {})) {\n for (const action of actions ?? []) permissions.push(`${resource}:${action}`);\n }\n return permissions;\n};\n\n/**\n * Builds a fail-closed `@velajs/authz` {@link PermissionResolver} from a\n * better-auth access-control role table (`{ roleName: acRole }` — the same map\n * shape passed to better-auth's admin/organization plugins). An identity's\n * `roles` are unioned into their granted permission strings; unknown roles\n * contribute nothing.\n *\n * ```ts\n * const ac = createAccessControl({ posts: ['read', 'write'] });\n * const authz = createAuthz({\n * resolver: betterAuthAcResolver({ editor: ac.newRole({ posts: ['write'] }) }),\n * });\n * await authz.can(identityFromUser(user), 'posts:write');\n * ```\n */\nexport const betterAuthAcResolver = (\n roles: Readonly<Record<string, BetterAuthAcRole>>,\n): PermissionResolver => {\n const grantsByRole = new Map<string, string[]>();\n for (const [name, role] of Object.entries(roles)) {\n grantsByRole.set(name, permissionsFromAcRole(role));\n }\n return {\n grants(identity: Identity): Set<string> {\n const out = new Set<string>();\n for (const name of identity.roles ?? []) {\n for (const permission of grantsByRole.get(name) ?? []) out.add(permission);\n }\n return out;\n },\n };\n};\n"],"mappings":";;;;AAEA,MAAa,SAAS,UAAU,gBAAyB,EAAE,KAAK,mBAAmB,CAAC;AACpF,MAAa,aAAa,OAAO;;ACAjC,SAAgB,4BAA4B,OAAwB;CAClE,MAAM,WAAW,SAAA;CACjB,IACE,SAAS,WAAW,KACpB,aAAa,SAAS,KAAK,KAC3B,CAAC,SAAS,WAAW,GAAG,KACxB,SAAS,WAAW,IAAI,KACxB,aAAa,OACb,SAAS,SAAS,GAAG,KACrB,WAAW,KAAK,QAAQ,KACxB,kBAAkB,KAAK,QAAQ,GAE/B,MAAM,IAAI,MACR,uFACF;CAGF,IAAI;CACJ,IAAI;EACF,UAAU,mBAAmB,QAAQ;CACvC,QAAQ;EACN,MAAM,IAAI,MAAM,iEAAiE;CACnF;CACA,IAAI,QAAQ,MAAM,GAAG,CAAC,CAAC,MAAM,YAAY,YAAY,OAAO,YAAY,IAAI,GAC1E,MAAM,IAAI,MAAM,6DAA6D;CAE/E,OAAO;AACT;;;;;;;;;;;;;;;;ACXA,SAAgB,mCAAmC,WAAmB,aAAmB;CACvF,MAAM,qBAAqB,4BAA4B,QAAQ;CAC/D,IAEM,+BAFN,MAEM,6BAA6B;EAIuB;EAAxD,YAAY,MAAqE;GAAzB,KAAA,OAAA;EAA0B;EAElF,MACM,OAAO,GAAsC;GACjD,OAAO,KAAK,KAAK,QAAQ,EAAE,IAAI,GAAG;EACpC;CACF;;EAJG,IAAI,IAAI;EACK,gBAAA,GAAA,IAAI,CAAA;;;;;;EATnB,OAAO,IAAI;EACX,WAAW,kBAAkB;EAKf,gBAAA,GAAA,OAAO,iBAAiB,CAAA;;;CAOvC,OAAO;AACT;;;AChCA,MAAa,sBAAsB,IAAI,eACrC,wBACF;;;ACMA,MAAM,WAAW,kCAA+C;AAEhE,SAAgB,iBAAiB,SAAiC;CAChE,4BAA4B,QAAQ,WAAW,CAAC;AAClD;AAEA,SAAgB,oBACd,SACA,MACA,QACM;CACN,MAAM,UAAU,QAAQ,WAAW;CACnC,0BAA0B,SAAS;EACjC,WAAW;GAAE;GAAQ,SAAS,KAAK,KAAK;GAAI,eAAe;EAAO;EAClE,aAAa,KAAK,QAAQ,UAAU,QAAQ;EAC5C,OAAO,KAAK;EACZ,GAAI,KAAK,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU,KAAK,SAAS;CACnE,CAAC;CACD,SAAS,IAAI,SAAS,IAAI;AAC5B;AAEA,SAAgB,oBAAoB,SAAoD;CACtF,MAAM,UAAU,yBAAyB,OAAO;CAChD,OAAO,UAAU,SAAS,IAAI,OAAO,IAAI,KAAA;AAC3C;;;AC1BA,SAAS,IAAI,OAAgB,KAAsB;CACjD,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO,KAAA;CACxD,MAAM,aAAa,OAAO,yBAAyB,OAAO,GAAG;CAC7D,OAAO,cAAc,WAAW,aAAa,WAAW,QAAQ,KAAA;AAClE;AAEA,SAAS,eAAe,OAAiC;CACvD,OAAO,OAAO,UAAU,YAAY,MAAM,SAAS;AACrD;AAEA,SAAS,KAAK,OAAkC;CAC9C,OAAO,iBAAiB,QAAQ,OAAO,SAAS,MAAM,QAAQ,CAAC,IAAI,IAAI,KAAK,KAAK,IAAI,KAAA;AACvF;AAEA,SAAS,eAAe,OAAoD;CAC1E,OAAO,UAAU,KAAA,KAAa,UAAU,QAAQ,OAAO,UAAU;AACnE;;AAGA,SAAS,eAAe,OAAwC;CAC9D,OAAO,OAAO,YAAY,OAAO,KAAK,KAAK,CAAC,CAAC,KAAK,QAAQ,CAAC,KAAK,IAAI,OAAO,GAAG,CAAC,CAAC,CAAC;AACnF;;AAGA,SAAgB,oBAAoB,OAAyC;CAC3E,IAAI;EACF,MAAM,OAAO,IAAI,OAAO,MAAM;EAC9B,MAAM,UAAU,IAAI,OAAO,SAAS;EACpC,IACE,OAAO,SAAS,YAChB,SAAS,QACT,OAAO,YAAY,YACnB,YAAY,MAEZ;EAEF,MAAM,KAAK,IAAI,MAAM,IAAI;EACzB,MAAM,QAAQ,IAAI,MAAM,OAAO;EAC/B,MAAM,OAAO,IAAI,MAAM,MAAM;EAC7B,MAAM,gBAAgB,IAAI,MAAM,eAAe;EAC/C,MAAM,QAAQ,IAAI,MAAM,OAAO;EAC/B,MAAM,YAAY,KAAK,IAAI,MAAM,WAAW,CAAC;EAC7C,MAAM,YAAY,KAAK,IAAI,MAAM,WAAW,CAAC;EAC7C,MAAM,YAAY,IAAI,SAAS,IAAI;EACnC,MAAM,SAAS,IAAI,SAAS,QAAQ;EACpC,MAAM,QAAQ,IAAI,SAAS,OAAO;EAClC,MAAM,mBAAmB,KAAK,IAAI,SAAS,WAAW,CAAC;EACvD,MAAM,mBAAmB,KAAK,IAAI,SAAS,WAAW,CAAC;EACvD,MAAM,YAAY,KAAK,IAAI,SAAS,WAAW,CAAC;EAChD,MAAM,YAAY,IAAI,SAAS,WAAW;EAC1C,MAAM,YAAY,IAAI,SAAS,WAAW;EAC1C,MAAM,eAAe,IAAI,SAAS,sBAAsB;EACxD,MAAM,OAAO,IAAI,MAAM,MAAM;EAC7B,IAAI,QAAkB,CAAC;EACvB,IAAI,SAAS,KAAA,KAAa,SAAS,MAAM;GACvC,IAAI,OAAO,SAAS,UAClB,QAAQ,KACL,MAAM,GAAG,CAAC,CACV,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC,CAC1B,OAAO,OAAO;QACd,IACH,MAAM,QAAQ,IAAI,KAClB,KAAK,OAAO,UAAoC,OAAO,UAAU,QAAQ,GAEzE,QAAQ,KAAK,KAAK,UAAkB,MAAM,KAAK,CAAC,CAAC,CAAC,OAAO,OAAO;QAC3D,OAAO,KAAA;EAChB;EACA,IACE,CAAC,eAAe,EAAE,KAClB,CAAC,eAAe,KAAK,KACrB,OAAO,SAAS,YAChB,OAAO,kBAAkB,aACzB,CAAC,eAAe,KAAK,KACrB,CAAC,aACD,CAAC,aACD,CAAC,eAAe,SAAS,KACzB,WAAW,MACX,CAAC,eAAe,KAAK,KACrB,CAAC,oBACD,CAAC,oBACD,CAAC,aACD,UAAU,QAAQ,KAAK,KAAK,IAAI,KAChC,CAAC,eAAe,SAAS,KACzB,CAAC,eAAe,SAAS,KACxB,iBAAiB,KAAA,KAAa,iBAAiB,QAAQ,CAAC,eAAe,YAAY,GAEpF,OAAO,KAAA;EACT,OAAO;GACL,MAAM,OAAO,OAAO;IAClB,GAAG,eAAe,IAAI;IACtB;IACA;IACA;IACA;IACA;IACA;IACA;GACF,CAAC;GACD,SAAS,OAAO,OAAO;IACrB,GAAG,eAAe,OAAO;IACzB,IAAI;IACJ,QAAQ;IACR;IACA,WAAW;IACX,WAAW;IACX;IACA;IACA;GACF,CAAC;GACD,OAAO,OAAO,OAAO,KAAK;GAC1B,GAAI,OAAO,iBAAiB,WAAW,EAAE,UAAU,aAAa,IAAI,CAAC;EACvE;CACF,QAAQ;EACN;CACF;AACF;;;AC1HA,MAAa,eAAe,UAAU,gBAAyB,EAAE,KAAK,qBAAqB,CAAC;AAC5F,MAAa,oBAAoB,aAAa;;;ACgBvC,IAAM,YAAN,MAAM,UAAiC;CAEE;CACE;CACV;CAHtC,YACE,MACA,MACA,WACA;EAH4C,KAAA,OAAA;EACE,KAAA,OAAA;EACV,KAAA,YAAA;CACnC;CAEH,MAAM,YAAY,SAA6C;EAG7D,IAAI,QAAQ,QAAQ,MAAM,MAAM;GAC9B,IAAI,mBAAmB,OAAO,GAAG,OAAO;GACxC,MAAM,IAAI,sBAAsB,yBAAyB;EAC3D;EAGA,IAAI,QAAQ,QAAQ,MAAM,QAAQ;GAChC,IACE,yBAAyB,OAAO,MAC/B,oBAAoB,OAAO,KAC1B,KAAK,UAAU,kBAAkB,QAAQ,OAAO,KAChD,KAAK,UAAU,kBAAkB,cAAc,OAAO,IAExD,OAAO;GACT,MAAM,IAAI,sBAAsB,yBAAyB;EAC3D;EACA,iBAAiB,OAAO;EACxB,IAAI,KAAK,UAAU,kBAAkB,QAAQ,OAAO,GAAG,OAAO;EAG9D,MAAM,OAAO,oBAAoB,MADf,KAAK,KAAK,IAAI,WAAW,EAAE,SAAS,QAAQ,WAAW,CAAC,CAAC,QAAQ,CAAC,CAChD;EACpC,IAAI,MAAM;GACR,oBAAoB,SAAS,MAAM,KAAK,KAAK,UAAU,aAAa;GACpE,OAAO;EACT;EACA,IAAI,KAAK,UAAU,kBAAkB,cAAc,OAAO,GAAG,OAAO;EACpE,MAAM,IAAI,sBAAsB,yBAAyB;CAC3D;AACF;;CAvCC,WAAW;CAGP,gBAAA,GAAA,OAAO,iBAAiB,CAAA;CACxB,gBAAA,GAAA,OAAO,mBAAmB,CAAA;CAC1B,gBAAA,GAAA,OAAO,SAAS,CAAA;;;;;;;;;ACDrB,MAAM,+BAAe,IAAI,QAAwB;AACjD,IAAI,kBAAkB;AAEtB,SAAS,YAAY,WAA2B;CAC9C,MAAM,WAAW,aAAa,IAAI,SAAS;CAC3C,IAAI,aAAa,KAAA,GAAW,OAAO;CACnC,MAAM,KAAK;CACX,aAAa,IAAI,WAAW,EAAE;CAC9B,OAAO;AACT;;;;;;;;AASA,SAAS,YAAY,KAAa,OAAe,WAA2B;CAC1E,IAAI,IAAI,WAAW,KAAK,QAAQ,IAAI,KAAK,GACvC,MAAM,IAAI,MAAM,wEAAwE;CAE1F,OAAO,YAAY,IAAI,GAAG,MAAM,OAAO,YAAY,SAAS;AAC9D;AAUA,SAAS,UAAU,SAAsD;CACvE,MAAM,WAAW,4BAA4B,QAAQ,QAAQ;CAC7D,MAAM,SAAS,QAAQ,UAAU,eAAe;CAChD,IAAI,OAAO,WAAW,KAAK,WAAW,OAAO,KAAK,GAChD,MAAM,IAAI,MAAM,kEAAkE;CAEpF,OAAO;EACL;EACA;EACA,UAAU,QAAQ,YAAY;EAC9B,cAAc,QAAQ,gBAAgB;CACxC;AACF;;AAGA,SAAS,oBAAoB,GAI3B;CACA,OAAO;EACL,WAAW,CAAC,mBAAmB,SAAS;EACxC,aAAa,EAAE,eAAe,CAAC,mCAAmC,EAAE,QAAQ,CAAC,IAAI,CAAC;EAClF,SAAS;GAAC;GAAmB;GAAqB;EAAS;CAC7D;AACF;AA6BA,IAAa,mBAAb,MAAa,iBAAiB;;;;;;;CAO5B,OAAO,QACL,SACe;EACf,MAAM,aAAa,UAAU,OAAO;EACpC,MAAM,QAAQ,WAAW,UAAU;EACnC,MAAM,MACJ,QAAQ,QAAQ,KAAA,IACZ,GAAG,MAAM,QAAQ,YAAY,QAAQ,IAAI,MACzC,YAAY,QAAQ,KAAK,OAAO,QAAQ,IAAI;EAClD,MAAM,SAAS,oBAAoB,UAAU;EAC7C,OAAO;GACL,QAAQ;GACR;GACA,WAAW;IACT,eAAe,qBAAqB,EAAE,UAAU,WAAW,CAAC;IAC5D,eAAe,qBAAqB,EAAE,gBAAgB,QAAQ,KAAK,CAAC;IACpE,GAAG,OAAO;IACV,GAAI,WAAW,WAAW,cAAc,SAAS,SAAS,IAAI,CAAC;GACjE;GACA,aAAa,OAAO;GACpB,SAAS,OAAO;EAClB;CACF;;;;;;;;;;CAWA,OAAO,aACL,SACe;EACf,MAAM,IAAI,UAAU,OAAO;EAC3B,MAAM,SAAS,oBAAoB,CAAC;EACpC,MAAM,QAAQ,WAAW;GAAE,GAAG;GAAG,QAAQ,QAAQ;EAAO,CAAC;EACzD,MAAM,MACJ,QAAQ,QAAQ,KAAA,IACZ,GAAG,MAAM,WAAW,YAAY,QAAQ,UAAU,MAClD,YAAY,QAAQ,KAAK,OAAO,QAAQ,UAAU;EACxD,OAAO;GACL,QAAQ;GACR;GACA,SAAS,QAAQ,WAAW,CAAC;GAC7B,WAAW;IACT,eAAe,qBAAqB,EAAE,UAAU,EAAE,CAAC;IAEnD,aAAyC;KACvC,GAAG;KACH,SAAS;KACT,SAAS;IACX,CAAC;IACD,GAAG,OAAO;IACV,GAAI,EAAE,WAAW,cAAc,SAAS,SAAS,IAAI,CAAC;GACxD;GACA,aAAa,OAAO;GACpB,SAAS,OAAO;EAClB;CACF;AACF;;;;;;;;AClJA,MAAa,6BAA6B,IAAI,eAC5C,gCACF;AAYO,IAAM,iCAAN,MAAM,+BAA+D;CAE5B;CACE;CAG7B;CALnB,YACE,MACA,SACA,eAGA;EAL4C,KAAA,OAAA;EACE,KAAA,UAAA;EAG7B,KAAA,gBAAA;CAChB;CAEH,MAAM,aACJ,SACA,SAC2C;EAC3C,MAAM,OAAO,oBAAoB,MAAM,KAAK,KAAK,IAAI,WAAW,EAAE,SAAS,QAAQ,QAAQ,CAAC,CAAC;EAC7F,IAAI,CAAC,MAAM,OAAO;EAClB,MAAM,WAAW,KAAK,gBAClB,MAAM,KAAK,cAAc;GAAE,MAAM,KAAK;GAAM,SAAS,KAAK;EAAQ,GAAG,SAAS,OAAO,IACrF,KAAK;EACT,IAAI,OAAO,aAAa,YAAY,SAAS,WAAW,GAAG,OAAO;EAClE,OAAO;GACL,WAAW;IACT,QAAQ,KAAK,QAAQ,UAAU;IAC/B,SAAS,KAAK,KAAK;IACnB,eAAe;GACjB;GACA;GACA,aAAa,KAAK,QAAQ,UAAU,QAAQ;EAC9C;CACF;AACF;;CA9BC,WAAW;CAGP,gBAAA,GAAA,OAAO,iBAAiB,CAAA;CACxB,gBAAA,GAAA,OAAO,mBAAmB,CAAA;CAC1B,gBAAA,GAAA,SAAS,CAAA;CACT,gBAAA,GAAA,OAAO,0BAA0B,CAAA;;;;;;;;;AC/CtC,MAAa,cAAc,sBACxB,OAAgB,QAA4C;CAE3D,OADc,oBAAoB,GACvB,CAAC,EAAE;AAChB,CACF;;;ACLA,MAAa,iBAAiB,sBAC3B,OAAgB,QAA+C;CAE9D,OADc,oBAAoB,GACvB,CAAC,EAAE;AAChB,CACF;;;;ACEA,MAAa,qBAAqB;AAElC,MAAM,kBAAkB,SAAyD;CAC/E,IAAI,CAAC,MAAM,OAAO,CAAC;CAGnB,IAAI,MAAM,QAAQ,IAAI,GAAG,OAAO,KAAK,OAAO,OAAO;CACnD,OAAO,KACJ,MAAM,GAAG,CAAC,CACV,KAAK,MAAM,EAAE,KAAK,CAAC,CAAC,CACpB,OAAO,OAAO;AACnB;;;;;;;;;;;AAYA,MAAa,oBACX,MACA,SAAiB,oBACjB,gBAAoC,WACvB;CACb,IAAI,CAAC,QAAQ,OAAO,KAAK,OAAO,YAAY,KAAK,GAAG,WAAW,GAAG,OAAO,EAAE,OAAO,CAAC,EAAE;CACrF,IAAI,OAAO,WAAW,GACpB,MAAM,IAAI,MAAM,wDAAwD;CAC1E,OAAO;EACL;EACA,SAAS,KAAK;EACd;EACA,QAAQ,KAAK;EACb,OAAO,eAAe,KAAK,IAAI;CACjC;AACF;;;;;;AAkBA,MAAa,yBAAyB,SAAqC;CACzE,MAAM,cAAwB,CAAC;CAC/B,KAAK,MAAM,CAAC,UAAU,YAAY,OAAO,QAAQ,KAAK,cAAc,CAAC,CAAC,GACpE,KAAK,MAAM,UAAU,WAAW,CAAC,GAAG,YAAY,KAAK,GAAG,SAAS,GAAG,QAAQ;CAE9E,OAAO;AACT;;;;;;;;;;;;;;;;AAiBA,MAAa,wBACX,UACuB;CACvB,MAAM,+BAAe,IAAI,IAAsB;CAC/C,KAAK,MAAM,CAAC,MAAM,SAAS,OAAO,QAAQ,KAAK,GAC7C,aAAa,IAAI,MAAM,sBAAsB,IAAI,CAAC;CAEpD,OAAO,EACL,OAAO,UAAiC;EACtC,MAAM,sBAAM,IAAI,IAAY;EAC5B,KAAK,MAAM,QAAQ,SAAS,SAAS,CAAC,GACpC,KAAK,MAAM,cAAc,aAAa,IAAI,IAAI,KAAK,CAAC,GAAG,IAAI,IAAI,UAAU;EAE3E,OAAO;CACT,EACF;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velajs/better-auth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.0",
|
|
4
4
|
"description": "better-auth integration for the Vela framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auth",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@velajs/authz": "1.
|
|
50
|
+
"@velajs/authz": "1.29.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@arethetypeswrong/cli": "0.18.5",
|
|
@@ -62,15 +62,15 @@
|
|
|
62
62
|
"typescript": "7.0.2",
|
|
63
63
|
"unplugin-swc": "1.5.9",
|
|
64
64
|
"vite": "^8.0.16",
|
|
65
|
-
"vitest": "4.1.
|
|
66
|
-
"@velajs/
|
|
67
|
-
"@velajs/
|
|
68
|
-
"@velajs/testing": "1.
|
|
65
|
+
"vitest": "4.1.11",
|
|
66
|
+
"@velajs/tenant": "1.29.0",
|
|
67
|
+
"@velajs/vela": "1.29.0",
|
|
68
|
+
"@velajs/testing": "1.29.0"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"better-auth": ">=1.2.0",
|
|
72
72
|
"hono": ">=4",
|
|
73
|
-
"@velajs/vela": "^1.
|
|
73
|
+
"@velajs/vela": "^1.29.0"
|
|
74
74
|
},
|
|
75
75
|
"engines": {
|
|
76
76
|
"node": ">=24"
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"scripts": {
|
|
79
79
|
"build": "tsdown",
|
|
80
80
|
"test": "vitest run",
|
|
81
|
-
"typecheck": "tsc --noEmit",
|
|
81
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
82
82
|
"lint": "oxlint .",
|
|
83
83
|
"format": "oxfmt .",
|
|
84
84
|
"format:check": "oxfmt --check .",
|