@velajs/better-auth 0.6.1 → 1.0.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - a468b57: Run guards before ordinary identity parameters, install authentication globally and deny application routes by default, remove the insecure `defaultPolicy: 'allow'` mode and implicit auth-path bypass, enforce canonical auth mount paths, reject ambiguous authorization engines, and key module instances by the actual auth/factory reference. Anonymous routes must now use explicit `@Public()` or `@OptionalAuth()` metadata. Verified sessions now publish Vela's framework-owned principal identity and, when present, the Better Auth organization plugin's `activeOrganizationId` so downstream throttling can partition by principal and tenant before IP fallback.
8
+
3
9
  ## 0.6.1
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -8,13 +8,13 @@ pnpm add @velajs/better-auth better-auth
8
8
 
9
9
  ## Quick start
10
10
 
11
- Construct your better-auth instance once, hand it to `BetterAuthModule.forRoot`, then use `AuthGuard` + `@CurrentUser()` like any other vela primitive.
11
+ Construct your better-auth instance once and hand it to `BetterAuthModule.forRoot`. The module installs `AuthGuard` application-wide by default; use `@CurrentUser()` on authenticated routes and mark the small anonymous surface explicitly.
12
12
 
13
13
  ```ts
14
14
  import { betterAuth } from 'better-auth';
15
- import { Module, Controller, Get, UseGuards, VelaFactory } from '@velajs/vela';
15
+ import { Module, Controller, Get, VelaFactory } from '@velajs/vela';
16
16
  import {
17
- BetterAuthModule, AuthGuard, CurrentUser, Public,
17
+ BetterAuthModule, CurrentUser, Public,
18
18
  } from '@velajs/better-auth';
19
19
 
20
20
  const auth = betterAuth({
@@ -24,7 +24,6 @@ const auth = betterAuth({
24
24
  });
25
25
 
26
26
  @Controller('/me')
27
- @UseGuards(AuthGuard)
28
27
  class MeController {
29
28
  @Get() me(@CurrentUser() user: { id: string; email: string }) {
30
29
  return { id: user.id, email: user.email };
@@ -35,7 +34,7 @@ class MeController {
35
34
  }
36
35
 
37
36
  @Module({
38
- imports: [BetterAuthModule.forRoot({ auth, isGlobal: true })],
37
+ imports: [BetterAuthModule.forRoot({ auth })],
39
38
  controllers: [MeController],
40
39
  })
41
40
  class AppModule {}
@@ -51,13 +50,15 @@ export default app; // edge-compatible (.fetch)
51
50
  ```ts
52
51
  BetterAuthModule.forRoot({
53
52
  auth, // pre-constructed betterAuth({ ... }) instance
53
+ issuer: 'my-app:better-auth', // stable namespace paired with user ids
54
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
55
+ isGlobal: true, // default — register AuthGuard as APP_GUARD
57
56
  mountHandler: true, // mount /api/auth/* catch-all controller
58
57
  });
59
58
  ```
60
59
 
60
+ Authentication has no allow-by-default compatibility mode. Use `@Public(true)` for routes that intentionally skip authentication, or `@OptionalAuth(true)` when the route accepts an anonymous identity. `isGlobal: false` is intended only for applications that install an equivalent global authentication guard themselves.
61
+
61
62
  ## Three composition patterns
62
63
 
63
64
  ### Pattern A — inline (simplest)
@@ -170,26 +171,30 @@ Under `forRootAsync`, the underlying `betterAuth({...})` instance is constructed
170
171
 
171
172
  | Decorator | Purpose |
172
173
  | ------------------- | ------------------------------------------------------------------------ |
173
- | `@CurrentUser()` | Lazy parameter — better-auth `User` from the request (read by AuthGuard) |
174
- | `@CurrentSession()` | Lazy parameter — better-auth `Session` |
174
+ | `@CurrentUser()` | Better-auth `User` from the request after guards run |
175
+ | `@CurrentSession()` | Better-auth `Session` after guards run |
175
176
  | `@Public(true)` | Class or method — bypass AuthGuard entirely |
176
177
  | `@OptionalAuth(true)` | Class or method — populate user if present, never throw 401 |
177
178
  | `@Roles(['admin'])` | Method — read by `RolesGuard`. Compares against `user.role`. |
178
179
 
179
- `@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`:
180
+ Optional identities are ordinary values: an anonymous caller receives the actual `undefined`, so normal truthiness checks are safe.
180
181
 
181
182
  ```ts
182
183
  handle(@CurrentUser() user: User | undefined) {
183
- return { hasUser: user?.id != null }; // ✓ correct
184
+ return { hasUser: Boolean(user) };
184
185
  }
185
186
  ```
186
187
 
187
188
  ## Guards
188
189
 
189
- - **`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).
190
+ - **`AuthGuard`** — singleton. Reads `Authorization` header / cookies via `auth.api.getSession`, populates `REQUEST_CONTEXT`, and publishes Vela's trusted principal/tenant identity for downstream security components. The Better Auth organization plugin's verified `activeOrganizationId` becomes the tenant partition when present. The guard honors only explicit `@Public()` / `@OptionalAuth()` metadata. The generated Better Auth catch-all controller is explicitly public; sharing its URL prefix never makes an application controller public.
190
191
  - **`RolesGuard`** — singleton. Reads `@Roles([...])` metadata, compares against `user.role`. Use with `@UseGuards(AuthGuard, RolesGuard)` — order matters.
191
192
 
192
- Global registration: pass `isGlobal: true` to `forRoot` (binds AuthGuard to `APP_GUARD`). Routes are deny-by-default; mark public ones with `@Public(true)`.
193
+ Global registration is the default: installing the module binds `AuthGuard` to `APP_GUARD`. Routes are deny-by-default; mark public ones with `@Public(true)`. The generated Better Auth controller is already marked public.
194
+
195
+ When using `ThrottlerModule`, import Better Auth first. Vela then rate-limits by
196
+ the verified issuer, subject, principal type, and active organization before it
197
+ falls back to a platform-attested client address.
193
198
 
194
199
  ## Edge-safe DB adapters
195
200
 
@@ -223,7 +228,7 @@ class CustomCatchallController {
223
228
  }
224
229
  ```
225
230
 
226
- Pass `basePath: '/auth'` to `BetterAuthModule.forRoot` so `AuthGuard` skips the right paths, and keep your `betterAuth({ basePath: '/auth' })` config in sync.
231
+ Pass `basePath: '/auth'` to `BetterAuthModule.forRoot` to mount the generated controller there, and keep your `betterAuth({ basePath: '/auth' })` config in sync. A custom catch-all must carry `@Public(true)` itself. Base paths must be canonical absolute paths: no root mount, trailing slash, wildcards, query/fragment, backslashes, or dot segments.
227
232
 
228
233
  ## License
229
234
 
package/dist/index.d.ts CHANGED
@@ -5,9 +5,16 @@ import { Identity, PermissionResolver } from "@velajs/authz";
5
5
  type BetterAuthInstance = Auth<any>;
6
6
  interface BetterAuthModuleOptions {
7
7
  auth: BetterAuthInstance;
8
+ /** Stable namespace paired with user ids in authorization identities. */
9
+ issuer?: string;
8
10
  basePath?: string;
11
+ /**
12
+ * Register AuthGuard application-wide. Defaults to `true`; opt out only when
13
+ * the application installs an equivalent global authentication guard itself.
14
+ */
9
15
  isGlobal?: boolean;
10
- defaultPolicy?: 'deny' | 'allow';
16
+ /** @deprecated Authentication is deny-by-default. Only `'deny'` is accepted. */
17
+ defaultPolicy?: 'deny';
11
18
  mountHandler?: boolean;
12
19
  }
13
20
  type User = User$1;
@@ -36,7 +43,9 @@ interface ForRootAsyncOptions<Inject extends readonly Token<unknown>[] = readonl
36
43
  isGlobal?: boolean;
37
44
  mountHandler?: boolean;
38
45
  basePath?: string;
39
- defaultPolicy?: 'deny' | 'allow';
46
+ issuer?: string;
47
+ /** @deprecated Authentication is deny-by-default. Only `'deny'` is accepted. */
48
+ defaultPolicy?: 'deny';
40
49
  key?: string;
41
50
  }
42
51
  declare class BetterAuthModule {
@@ -124,6 +133,8 @@ declare const BetterAuthCatchallController: Type;
124
133
  declare const BETTER_AUTH_OPTIONS: InjectionToken<BetterAuthModuleOptions>;
125
134
  declare const AUTH_USER_KEY: unique symbol;
126
135
  declare const AUTH_SESSION_KEY: unique symbol;
136
+ declare const AUTH_ISSUER_KEY: unique symbol;
137
+ declare const AUTH_PRINCIPAL_TYPE_KEY: unique symbol;
127
138
  //#endregion
128
139
  //#region src/guards/auth.guard.d.ts
129
140
  declare class AuthGuard implements CanActivate {
@@ -146,22 +157,18 @@ declare class RolesGuard implements CanActivate {
146
157
  * engine. For each required permission it calls `authz.can(identity, perm)`,
147
158
  * requiring **all** of them (AND semantics — contrast {@link RolesGuard}, which
148
159
  * is OR over roles). The caller's `Identity` is derived from the better-auth
149
- * user that {@link AuthGuard} placed in the request context, so this guard must
160
+ * user that {@link AuthGuard} placed in canonical request-local auth state, so this guard must
150
161
  * run *after* `AuthGuard` (e.g. `@UseGuards(AuthGuard, PermissionGuard)`).
151
162
  *
152
- * `AUTHZ` is resolved at **request time** from the per-request container (the
153
- * same container `REQUEST_CONTEXT` is resolved from), not constructor-injected.
154
- * This deliberately avoids DI visibility coupling: the guard works whether or
155
- * not `AuthzModule` is registered as global — a present-but-non-global
156
- * `AuthzModule` resolves fine and, crucially, never crashes bootstrap. If
157
- * `AuthzModule` is not registered at all the resolve fails and the guard fails
158
- * closed (403) rather than granting access.
163
+ * `AUTHZ` is resolved at request time from the per-request container. Exactly
164
+ * one reachable engine is required; zero or multiple registrations deny rather
165
+ * than selecting one by import order.
159
166
  *
160
167
  * Fail-closed on every abnormal path — no branch grants access on missing
161
168
  * wiring or a missing caller:
162
169
  * - no required permissions → allow (nothing to enforce);
163
170
  * - `AUTHZ` unresolvable (`AuthzModule` not registered) → deny (`ForbiddenException`);
164
- * - no authenticated user in the request context → deny;
171
+ * - no authenticated user in request-local auth state → deny;
165
172
  * - any single required permission not granted → deny.
166
173
  *
167
174
  * The guard is stateless (no injected dependencies), so it is safe to register
@@ -224,15 +231,18 @@ declare const REQUIRE_PERMISSION_KEY: string;
224
231
  type AuthUser = User & {
225
232
  role?: string | string[] | null;
226
233
  };
234
+ /** Stable issuer namespace used for better-auth session principals. */
235
+ declare const BETTER_AUTH_ISSUER = "better-auth";
227
236
  /**
228
- * Adapts a better-auth user into a `@velajs/authz` {@link Identity}. Maps
229
- * `user.id` `userId` and the admin-plugin `role` field → `roles`.
237
+ * Adapts a better-auth user into a stable `@velajs/authz` {@link Identity}.
238
+ * The issuer scopes `user.id` as both `subject` and the compatibility `userId`;
239
+ * the admin-plugin `role` field supplies local roles.
230
240
  *
231
241
  * Fail-closed: a missing user (`null`/`undefined`, i.e. an unauthenticated
232
242
  * request) maps to the zero-privilege identity `{ roles: [] }`, so downstream
233
243
  * `can()` checks grant nothing.
234
244
  */
235
- declare const identityFromUser: (user: AuthUser | null | undefined) => Identity;
245
+ declare const identityFromUser: (user: AuthUser | null | undefined, issuer?: string, principalType?: 'user' | 'service') => Identity;
236
246
  /**
237
247
  * The minimal slice of a better-auth access-control role consumed here. Both
238
248
  * `createAccessControl(...).newRole(...)` and the standalone `role(...)` return
@@ -266,5 +276,5 @@ declare const permissionsFromAcRole: (role: BetterAuthAcRole) => string[];
266
276
  */
267
277
  declare const betterAuthAcResolver: (roles: Readonly<Record<string, BetterAuthAcRole>>) => PermissionResolver;
268
278
  //#endregion
269
- export { AUTH_SESSION_KEY, AUTH_USER_KEY, AuthGuard, type AuthUser, BETTER_AUTH_OPTIONS, type BetterAuthAcRole, BetterAuthCatchallController, type BetterAuthInstance, BetterAuthModule, type BetterAuthModuleOptions, BetterAuthService, CurrentSession, CurrentUser, OPTIONAL_AUTH_KEY, OptionalAuth, PUBLIC_KEY, PermissionGuard, Public, REQUIRE_PERMISSION_KEY, ROLES_KEY, RequirePermission, Roles, RolesGuard, type Session, type User, betterAuthAcResolver, createBetterAuthCatchallController, identityFromUser, permissionsFromAcRole };
279
+ export { AUTH_ISSUER_KEY, AUTH_PRINCIPAL_TYPE_KEY, AUTH_SESSION_KEY, AUTH_USER_KEY, AuthGuard, type AuthUser, BETTER_AUTH_ISSUER, BETTER_AUTH_OPTIONS, type BetterAuthAcRole, BetterAuthCatchallController, type BetterAuthInstance, BetterAuthModule, type BetterAuthModuleOptions, BetterAuthService, CurrentSession, CurrentUser, OPTIONAL_AUTH_KEY, OptionalAuth, PUBLIC_KEY, PermissionGuard, Public, REQUIRE_PERMISSION_KEY, ROLES_KEY, RequirePermission, Roles, RolesGuard, type Session, type User, betterAuthAcResolver, createBetterAuthCatchallController, identityFromUser, permissionsFromAcRole };
270
280
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,9 +1,22 @@
1
1
  import { a as __decorateMetadata, i as __decorateParam, n as BetterAuthService, r as __decorate, t as BETTER_AUTH_BUILDER } from "./better-auth.service-BMkyFX-w.js";
2
- import { All, Controller, ForbiddenException, Inject, Injectable, InjectionToken, REQUEST_CONTEXT, Reflector, Req, UnauthorizedException, createLazyParamDecorator, defineModule, lazyProvider, provideGlobal, stableHash } from "@velajs/vela";
2
+ import { All, Controller, ForbiddenException, Inject, Injectable, InjectionToken, REQUEST_CONTEXT, Reflector, Req, UnauthorizedException, clearTrustedRequestIdentity, createParamDecorator, defineModule, lazyProvider, provideGlobal, setTrustedRequestIdentity, stableHash } from "@velajs/vela";
3
3
  import { AUTHZ } from "@velajs/authz/vela";
4
4
  //#region src/decorators/public.decorator.ts
5
5
  const Public = Reflector.createDecorator({ key: "vela.auth.public" });
6
6
  const PUBLIC_KEY = Public.KEY;
7
+ /** Validate and canonicalize the route prefix used by the public auth controller. */
8
+ function normalizeBetterAuthBasePath(value) {
9
+ const basePath = value ?? "/api/auth";
10
+ if (basePath.length === 0 || basePath !== basePath.trim() || !basePath.startsWith("/") || basePath.startsWith("//") || basePath === "/" || basePath.endsWith("/") || /[\\?#*]/u.test(basePath) || /%(?:2e|2f|5c)/iu.test(basePath)) throw new Error("@velajs/better-auth: basePath must be a canonical absolute path such as \"/api/auth\"");
11
+ let decoded;
12
+ try {
13
+ decoded = decodeURIComponent(basePath);
14
+ } catch {
15
+ throw new Error("@velajs/better-auth: basePath contains invalid percent encoding");
16
+ }
17
+ if (decoded.split("/").some((segment) => segment === "." || segment === "..")) throw new Error("@velajs/better-auth: basePath must not contain dot segments");
18
+ return basePath;
19
+ }
7
20
  //#endregion
8
21
  //#region src/better-auth.controller.ts
9
22
  /**
@@ -20,6 +33,7 @@ const PUBLIC_KEY = Public.KEY;
20
33
  * Both default to `/api/auth`, so the no-prefix / no-config case just works.
21
34
  */
22
35
  function createBetterAuthCatchallController(basePath = "/api/auth") {
36
+ const normalizedBasePath = normalizeBetterAuthBasePath(basePath);
23
37
  let BetterAuthCatchallController = class BetterAuthCatchallController {
24
38
  auth;
25
39
  constructor(auth) {
@@ -38,7 +52,7 @@ function createBetterAuthCatchallController(basePath = "/api/auth") {
38
52
  ], BetterAuthCatchallController.prototype, "handle", null);
39
53
  BetterAuthCatchallController = __decorate([
40
54
  Public(true),
41
- Controller(basePath),
55
+ Controller(normalizedBasePath),
42
56
  Injectable(),
43
57
  __decorateParam(0, Inject(BetterAuthService)),
44
58
  __decorateMetadata("design:paramtypes", [typeof BetterAuthService === "undefined" ? Object : BetterAuthService])
@@ -56,6 +70,27 @@ const BetterAuthCatchallController = createBetterAuthCatchallController();
56
70
  const BETTER_AUTH_OPTIONS = new InjectionToken("vela.BetterAuthOptions");
57
71
  const AUTH_USER_KEY = Symbol.for("vela.better-auth.user");
58
72
  const AUTH_SESSION_KEY = Symbol.for("vela.better-auth.session");
73
+ const AUTH_ISSUER_KEY = Symbol.for("vela.better-auth.issuer");
74
+ const AUTH_PRINCIPAL_TYPE_KEY = Symbol.for("vela.better-auth.principal-type");
75
+ //#endregion
76
+ //#region src/auth-request-state.ts
77
+ const ANONYMOUS = Object.freeze({ authenticated: false });
78
+ const stateByRequest = /* @__PURE__ */ new WeakMap();
79
+ /** Clear any state before a guard evaluates a request. */
80
+ const beginAuthRequest = (context) => {
81
+ stateByRequest.set(context.getRequest(), ANONYMOUS);
82
+ };
83
+ /** Publish a fully verified session for downstream guards and parameters. */
84
+ const authenticateRequest = (context, state) => {
85
+ const authenticated = Object.freeze({
86
+ authenticated: true,
87
+ ...state
88
+ });
89
+ stateByRequest.set(context.getRequest(), authenticated);
90
+ return authenticated;
91
+ };
92
+ /** Missing state is anonymous: no guard means no ambient identity. */
93
+ const getAuthRequestState = (context) => context.getType() === "http" ? stateByRequest.get(context.getRequest()) ?? ANONYMOUS : ANONYMOUS;
59
94
  //#endregion
60
95
  //#region src/decorators/optional-auth.decorator.ts
61
96
  const OptionalAuth = Reflector.createDecorator({ key: "vela.auth.optional" });
@@ -71,20 +106,26 @@ let AuthGuard = class AuthGuard {
71
106
  this.opts = opts;
72
107
  }
73
108
  async canActivate(context) {
109
+ if (context.getType() === "ws") {
110
+ if (hasValidWebSocketIdentity(context)) return true;
111
+ throw new AuthenticationRequiredException();
112
+ }
113
+ beginAuthRequest(context);
114
+ mirrorRequestContext(context, { authenticated: false });
74
115
  if (this.reflector.getAllAndOverride(Public, context)) return true;
75
116
  const request = context.getRequest();
76
- const path = new URL(request.url).pathname;
77
- const basePath = this.opts.basePath ?? "/api/auth";
78
- if (path === basePath || path.startsWith(`${basePath}/`)) return true;
79
- const data = await this.auth.api.getSession({ headers: request.headers });
117
+ const data = validateSessionData(await this.auth.api.getSession({ headers: request.headers }));
80
118
  if (data) {
81
- const reqCtx = resolveRequestContext(context);
82
- reqCtx.set(AUTH_USER_KEY, data.user);
83
- reqCtx.set(AUTH_SESSION_KEY, data.session);
119
+ mirrorRequestContext(context, authenticateRequest(context, {
120
+ user: data.user,
121
+ session: data.session,
122
+ issuer: this.opts.issuer ?? "better-auth",
123
+ principalType: "user"
124
+ }));
84
125
  return true;
85
126
  }
86
- if (this.opts.defaultPolicy === "allow" || this.reflector.getAllAndOverride(OptionalAuth, context)) return true;
87
- throw new UnauthorizedException("Authentication required");
127
+ if (this.reflector.getAllAndOverride(OptionalAuth, context)) return true;
128
+ throw new AuthenticationRequiredException();
88
129
  }
89
130
  };
90
131
  AuthGuard = __decorate([
@@ -93,24 +134,120 @@ AuthGuard = __decorate([
93
134
  __decorateParam(1, Inject(BETTER_AUTH_OPTIONS)),
94
135
  __decorateMetadata("design:paramtypes", [typeof BetterAuthService === "undefined" ? Object : BetterAuthService, Object])
95
136
  ], AuthGuard);
96
- function resolveRequestContext(context) {
97
- return context.getContext().get("container").resolve(REQUEST_CONTEXT);
137
+ function hasValidWebSocketIdentity(context) {
138
+ try {
139
+ const data = context.switchToWs().getClient()?.data;
140
+ if (!data || typeof data !== "object") return false;
141
+ const record = data;
142
+ const principal = record.principal;
143
+ if (!principal || typeof principal !== "object") return false;
144
+ const fields = principal;
145
+ return typeof fields.issuer === "string" && fields.issuer.length > 0 && typeof fields.subject === "string" && fields.subject.length > 0 && (fields.principalType === "user" || fields.principalType === "service") && typeof record.tenantId === "string" && record.tenantId.length > 0 && typeof record.expiresAtMs === "number" && Number.isSafeInteger(record.expiresAtMs) && record.expiresAtMs > Date.now();
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+ /**
151
+ * Preserve the public REQUEST_CONTEXT symbols for applications that consume
152
+ * them directly. The private Request-keyed state above remains canonical: a
153
+ * missing/duplicated framework token must not prevent a verified guard from
154
+ * publishing identity to its own downstream decorators and guards.
155
+ */
156
+ function mirrorRequestContext(context, state) {
157
+ const request = context.getRequest();
158
+ if (!state.authenticated) clearTrustedRequestIdentity(request);
159
+ else {
160
+ const tenantId = readActiveOrganizationId(state.session);
161
+ setTrustedRequestIdentity(request, {
162
+ principal: {
163
+ issuer: state.issuer,
164
+ subject: state.user.id,
165
+ principalType: state.principalType
166
+ },
167
+ ...tenantId === void 0 ? {} : { tenantId }
168
+ });
169
+ }
170
+ const container = context.getContext().get("container");
171
+ if (!container) return;
172
+ let reqCtx;
173
+ try {
174
+ reqCtx = container.resolve(REQUEST_CONTEXT);
175
+ } catch {
176
+ return;
177
+ }
178
+ if (!state.authenticated) {
179
+ reqCtx.set(AUTH_USER_KEY, void 0);
180
+ reqCtx.set(AUTH_SESSION_KEY, void 0);
181
+ reqCtx.set(AUTH_ISSUER_KEY, void 0);
182
+ reqCtx.set(AUTH_PRINCIPAL_TYPE_KEY, void 0);
183
+ return;
184
+ }
185
+ reqCtx.set(AUTH_USER_KEY, state.user);
186
+ reqCtx.set(AUTH_SESSION_KEY, state.session);
187
+ reqCtx.set(AUTH_ISSUER_KEY, state.issuer);
188
+ reqCtx.set(AUTH_PRINCIPAL_TYPE_KEY, state.principalType);
189
+ }
190
+ function readActiveOrganizationId(session) {
191
+ const descriptor = Object.getOwnPropertyDescriptor(session, "activeOrganizationId");
192
+ if (descriptor === void 0 || !("value" in descriptor)) return void 0;
193
+ const value = descriptor.value;
194
+ return typeof value === "string" && value.length > 0 ? value : void 0;
195
+ }
196
+ /** A test/provider override is trusted code, but its runtime result is not. */
197
+ function validateSessionData(value) {
198
+ if (value === null || typeof value !== "object") return void 0;
199
+ try {
200
+ const userDescriptor = Object.getOwnPropertyDescriptor(value, "user");
201
+ const sessionDescriptor = Object.getOwnPropertyDescriptor(value, "session");
202
+ if (userDescriptor === void 0 || !("value" in userDescriptor) || sessionDescriptor === void 0 || !("value" in sessionDescriptor)) return;
203
+ const user = userDescriptor.value;
204
+ const session = sessionDescriptor.value;
205
+ if (user === null || typeof user !== "object" || session === null || typeof session !== "object") return;
206
+ const userIdDescriptor = Object.getOwnPropertyDescriptor(user, "id");
207
+ const sessionIdDescriptor = Object.getOwnPropertyDescriptor(session, "id");
208
+ const sessionUserIdDescriptor = Object.getOwnPropertyDescriptor(session, "userId");
209
+ const userId = userIdDescriptor !== void 0 && "value" in userIdDescriptor ? userIdDescriptor.value : void 0;
210
+ const sessionId = sessionIdDescriptor !== void 0 && "value" in sessionIdDescriptor ? sessionIdDescriptor.value : void 0;
211
+ const sessionUserId = sessionUserIdDescriptor !== void 0 && "value" in sessionUserIdDescriptor ? sessionUserIdDescriptor.value : void 0;
212
+ if (typeof userId !== "string" || userId.length === 0 || typeof sessionId !== "string" || sessionId.length === 0 || sessionUserId !== userId) return;
213
+ return {
214
+ user,
215
+ session
216
+ };
217
+ } catch {
218
+ return;
219
+ }
98
220
  }
221
+ /**
222
+ * `UnauthorizedException` preserves Nest-style direct behavior. The structural
223
+ * VelaError brand also survives test/runtime package duplication, so the
224
+ * central renderer still maps this to 401 rather than treating it as a foreign
225
+ * 500 error.
226
+ */
227
+ var AuthenticationRequiredException = class extends UnauthorizedException {
228
+ type = "VelaError";
229
+ code = "unauthorized";
230
+ status = 401;
231
+ constructor() {
232
+ super("Authentication required");
233
+ }
234
+ };
99
235
  //#endregion
100
236
  //#region src/decorators/roles.decorator.ts
101
237
  const Roles = Reflector.createDecorator({ key: "vela.auth.roles" });
102
238
  const ROLES_KEY = Roles.KEY;
103
239
  //#endregion
104
240
  //#region src/guards/roles.guard.ts
241
+ const ACCESS_DENIED$1 = "Access denied";
105
242
  let RolesGuard = class RolesGuard {
106
243
  reflector = new Reflector();
107
244
  canActivate(context) {
108
245
  const required = this.reflector.getAllAndOverride(Roles, context);
109
246
  if (!required || required.length === 0) return true;
110
- const user = context.getContext().get("container").resolve(REQUEST_CONTEXT).get(AUTH_USER_KEY);
111
- if (!user) throw new ForbiddenException("Role check requires authentication");
112
- const userRoles = normalizeRoles$1(user.role);
113
- if (!required.some((r) => userRoles.includes(r))) throw new ForbiddenException(`Insufficient role; one of [${required.join(", ")}] required`);
247
+ const state = getAuthRequestState(context);
248
+ if (!state.authenticated) throw new ForbiddenException(ACCESS_DENIED$1);
249
+ const userRoles = normalizeRoles$1(state.user.role);
250
+ if (!required.some((r) => userRoles.includes(r))) throw new ForbiddenException(ACCESS_DENIED$1);
114
251
  return true;
115
252
  }
116
253
  };
@@ -122,22 +259,29 @@ function normalizeRoles$1(role) {
122
259
  }
123
260
  //#endregion
124
261
  //#region src/authz-bridge.ts
262
+ /** Stable issuer namespace used for better-auth session principals. */
263
+ const BETTER_AUTH_ISSUER = "better-auth";
125
264
  const normalizeRoles = (role) => {
126
265
  if (!role) return [];
127
266
  if (Array.isArray(role)) return role.filter(Boolean);
128
267
  return role.split(",").map((r) => r.trim()).filter(Boolean);
129
268
  };
130
269
  /**
131
- * Adapts a better-auth user into a `@velajs/authz` {@link Identity}. Maps
132
- * `user.id` `userId` and the admin-plugin `role` field → `roles`.
270
+ * Adapts a better-auth user into a stable `@velajs/authz` {@link Identity}.
271
+ * The issuer scopes `user.id` as both `subject` and the compatibility `userId`;
272
+ * the admin-plugin `role` field supplies local roles.
133
273
  *
134
274
  * Fail-closed: a missing user (`null`/`undefined`, i.e. an unauthenticated
135
275
  * request) maps to the zero-privilege identity `{ roles: [] }`, so downstream
136
276
  * `can()` checks grant nothing.
137
277
  */
138
- const identityFromUser = (user) => {
139
- if (!user) return { roles: [] };
278
+ const identityFromUser = (user, issuer = BETTER_AUTH_ISSUER, principalType = "user") => {
279
+ if (!user || typeof user.id !== "string" || user.id.length === 0) return { roles: [] };
280
+ if (issuer.length === 0) throw new Error("@velajs/better-auth: identity issuer must be non-empty");
140
281
  return {
282
+ issuer,
283
+ subject: user.id,
284
+ principalType,
141
285
  userId: user.id,
142
286
  roles: normalizeRoles(user.role)
143
287
  };
@@ -204,35 +348,99 @@ const REQUIRE_PERMISSION_KEY = RequirePermission.KEY;
204
348
  //#endregion
205
349
  //#region src/guards/permission.guard.ts
206
350
  const AUTHZ_TOKEN = AUTHZ;
351
+ const ACCESS_DENIED = "Access denied";
352
+ function resolveSingleAuthz(container, moduleId) {
353
+ try {
354
+ if (typeof container.resolveAll === "function") {
355
+ const candidates = container.resolveAll(AUTHZ_TOKEN, moduleId);
356
+ return candidates.length === 1 ? candidates[0] : void 0;
357
+ }
358
+ return container.resolve(AUTHZ_TOKEN, moduleId);
359
+ } catch {
360
+ return;
361
+ }
362
+ }
207
363
  let PermissionGuard = class PermissionGuard {
208
364
  reflector = new Reflector();
209
365
  async canActivate(context) {
210
366
  const required = this.reflector.getAllAndOverride(RequirePermission, context);
211
367
  if (!required || required.length === 0) return true;
212
- const container = context.getContext().get("container");
213
- let authz;
214
- try {
215
- authz = container.resolve(AUTHZ_TOKEN);
216
- } catch {
217
- authz = void 0;
218
- }
219
- if (!authz) throw new ForbiddenException("Authorization is not configured");
220
- const user = container.resolve(REQUEST_CONTEXT).get(AUTH_USER_KEY);
221
- if (!user) throw new ForbiddenException("Permission check requires authentication");
222
- const identity = identityFromUser(user);
223
- for (const permission of required) if (!await authz.can(identity, permission)) throw new ForbiddenException(`Missing permission: ${permission}`);
368
+ const container = resolveContextContainer(context);
369
+ const moduleId = context.getModuleId();
370
+ const authz = container === void 0 || moduleId === void 0 ? void 0 : resolveSingleAuthz(container, moduleId);
371
+ if (!authz) throw new ForbiddenException(ACCESS_DENIED);
372
+ const identity = resolveContextIdentity(context);
373
+ if (identity === void 0) throw new ForbiddenException(ACCESS_DENIED);
374
+ for (const permission of required) if (!await authz.can(identity, permission)) throw new ForbiddenException(ACCESS_DENIED);
224
375
  return true;
225
376
  }
226
377
  };
227
378
  PermissionGuard = __decorate([Injectable()], PermissionGuard);
379
+ function resolveContextContainer(context) {
380
+ const direct = context.getContainer?.();
381
+ if (direct !== void 0) return direct;
382
+ if (context.getType() !== "http") return void 0;
383
+ try {
384
+ return context.getContext().get("container");
385
+ } catch {
386
+ return;
387
+ }
388
+ }
389
+ function resolveContextIdentity(context) {
390
+ if (context.getType() === "ws") try {
391
+ const data = context.switchToWs().getClient()?.data;
392
+ if (!data || typeof data !== "object") return void 0;
393
+ const record = data;
394
+ const principal = record.principal;
395
+ if (!principal || typeof principal !== "object") return void 0;
396
+ const fields = principal;
397
+ if (typeof fields.issuer !== "string" || fields.issuer.length === 0 || typeof fields.subject !== "string" || fields.subject.length === 0 || fields.principalType !== "user" && fields.principalType !== "service" || typeof record.tenantId !== "string" || record.tenantId.length === 0 || typeof record.expiresAtMs !== "number" || !Number.isSafeInteger(record.expiresAtMs) || record.expiresAtMs <= Date.now()) return;
398
+ return {
399
+ issuer: fields.issuer,
400
+ subject: fields.subject,
401
+ principalType: fields.principalType,
402
+ userId: fields.subject,
403
+ roles: []
404
+ };
405
+ } catch {
406
+ return;
407
+ }
408
+ const state = getAuthRequestState(context);
409
+ return state.authenticated ? identityFromUser(state.user, state.issuer, state.principalType) : void 0;
410
+ }
228
411
  //#endregion
229
412
  //#region src/better-auth.module.ts
230
- const DEFAULT_BASE_PATH = "/api/auth";
413
+ const referenceIds = /* @__PURE__ */ new WeakMap();
414
+ const explicitKeyClaims = /* @__PURE__ */ new Map();
415
+ let nextReferenceId = 1;
416
+ function referenceId(reference) {
417
+ const existing = referenceIds.get(reference);
418
+ if (existing !== void 0) return existing;
419
+ const id = nextReferenceId++;
420
+ referenceIds.set(reference, id);
421
+ return id;
422
+ }
423
+ function claimExplicitKey(key, kind, reference, shape) {
424
+ if (key.length === 0 || key !== key.trim()) throw new Error("@velajs/better-auth: an explicit module key must be a non-empty string");
425
+ const existing = explicitKeyClaims.get(key);
426
+ 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`);
427
+ explicitKeyClaims.set(key, {
428
+ kind,
429
+ reference,
430
+ shape
431
+ });
432
+ return `explicit:${key}:ref:${referenceId(reference)}`;
433
+ }
231
434
  function normalize(options) {
435
+ const basePath = normalizeBetterAuthBasePath(options.basePath);
436
+ const issuer = options.issuer ?? `better-auth:${basePath}`;
437
+ if (issuer.length === 0 || issuer !== issuer.trim()) throw new Error("@velajs/better-auth: issuer must be a non-empty stable namespace");
438
+ if (options.defaultPolicy !== void 0 && options.defaultPolicy !== "deny") throw new Error("@velajs/better-auth: defaultPolicy is deny-only; mark anonymous routes with @Public() or @OptionalAuth()");
232
439
  return {
233
- basePath: options.basePath ?? DEFAULT_BASE_PATH,
234
- isGlobal: options.isGlobal ?? false,
235
- defaultPolicy: options.defaultPolicy ?? "deny",
440
+ basePath,
441
+ issuer,
442
+ isGlobal: options.isGlobal ?? true,
443
+ defaultPolicy: "deny",
236
444
  mountHandler: options.mountHandler ?? true
237
445
  };
238
446
  }
@@ -304,8 +512,13 @@ var BetterAuthModule = class BetterAuthModule {
304
512
  * connection, in-memory adapters, etc.).
305
513
  */
306
514
  static forRoot(options) {
515
+ const shape = stableHash(normalize(options));
516
+ const key = options.key === void 0 ? `${shape}:auth:${referenceId(options.auth)}` : claimExplicitKey(options.key, "auth", options.auth, shape);
307
517
  return {
308
- ...authModuleHost.ConfigurableModuleClass.forRoot(options),
518
+ ...authModuleHost.ConfigurableModuleClass.forRoot({
519
+ ...options,
520
+ key
521
+ }),
309
522
  module: BetterAuthModule
310
523
  };
311
524
  }
@@ -323,12 +536,14 @@ var BetterAuthModule = class BetterAuthModule {
323
536
  static forRootAsync(options) {
324
537
  const n = normalize(options);
325
538
  const common = commonContributions(n);
539
+ const shape = stableHash({
540
+ ...n,
541
+ inject: options.inject
542
+ });
543
+ const key = options.key === void 0 ? `${shape}:factory:${referenceId(options.useFactory)}` : claimExplicitKey(options.key, "factory", options.useFactory, shape);
326
544
  return {
327
545
  module: BetterAuthModule,
328
- key: options.key ?? stableHash({
329
- ...n,
330
- inject: options.inject
331
- }),
546
+ key,
332
547
  imports: options.imports ?? [],
333
548
  providers: [
334
549
  {
@@ -350,15 +565,17 @@ var BetterAuthModule = class BetterAuthModule {
350
565
  };
351
566
  //#endregion
352
567
  //#region src/decorators/current-user.decorator.ts
353
- const CurrentUser = createLazyParamDecorator((_data, ctx) => {
354
- return ctx.getContext().get("container").resolve(REQUEST_CONTEXT).get(AUTH_USER_KEY);
568
+ const CurrentUser = createParamDecorator((_data, ctx) => {
569
+ const state = getAuthRequestState(ctx);
570
+ return state.authenticated ? state.user : void 0;
355
571
  });
356
572
  //#endregion
357
573
  //#region src/decorators/current-session.decorator.ts
358
- const CurrentSession = createLazyParamDecorator((_data, ctx) => {
359
- return ctx.getContext().get("container").resolve(REQUEST_CONTEXT).get(AUTH_SESSION_KEY);
574
+ const CurrentSession = createParamDecorator((_data, ctx) => {
575
+ const state = getAuthRequestState(ctx);
576
+ return state.authenticated ? state.session : void 0;
360
577
  });
361
578
  //#endregion
362
- export { AUTH_SESSION_KEY, AUTH_USER_KEY, AuthGuard, BETTER_AUTH_OPTIONS, BetterAuthCatchallController, BetterAuthModule, BetterAuthService, CurrentSession, CurrentUser, OPTIONAL_AUTH_KEY, OptionalAuth, PUBLIC_KEY, PermissionGuard, Public, REQUIRE_PERMISSION_KEY, ROLES_KEY, RequirePermission, Roles, RolesGuard, betterAuthAcResolver, createBetterAuthCatchallController, identityFromUser, permissionsFromAcRole };
579
+ export { AUTH_ISSUER_KEY, AUTH_PRINCIPAL_TYPE_KEY, AUTH_SESSION_KEY, AUTH_USER_KEY, AuthGuard, BETTER_AUTH_ISSUER, BETTER_AUTH_OPTIONS, BetterAuthCatchallController, BetterAuthModule, BetterAuthService, CurrentSession, CurrentUser, OPTIONAL_AUTH_KEY, OptionalAuth, PUBLIC_KEY, PermissionGuard, Public, REQUIRE_PERMISSION_KEY, ROLES_KEY, RequirePermission, Roles, RolesGuard, betterAuthAcResolver, createBetterAuthCatchallController, identityFromUser, permissionsFromAcRole };
363
580
 
364
581
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["normalizeRoles"],"sources":["../src/decorators/public.decorator.ts","../src/better-auth.controller.ts","../src/better-auth.tokens.ts","../src/decorators/optional-auth.decorator.ts","../src/guards/auth.guard.ts","../src/decorators/roles.decorator.ts","../src/guards/roles.guard.ts","../src/authz-bridge.ts","../src/decorators/require-permission.decorator.ts","../src/guards/permission.guard.ts","../src/better-auth.module.ts","../src/decorators/current-user.decorator.ts","../src/decorators/current-session.decorator.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","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';\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 @Public(true)\n @Controller(basePath)\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\n/**\n * Default-path (`/api/auth`) catch-all controller. Retained for back-compat;\n * `BetterAuthModule` now mounts {@link createBetterAuthCatchallController} with\n * the configured `basePath`. Prefer the factory for a custom base path.\n */\nexport const BetterAuthCatchallController = createBetterAuthCatchallController();\n","import { InjectionToken } from '@velajs/vela';\nimport type { BetterAuthModuleOptions } from './better-auth.types';\n\nexport const BETTER_AUTH_OPTIONS = new InjectionToken<BetterAuthModuleOptions>(\n 'vela.BetterAuthOptions',\n);\n\nexport const AUTH_USER_KEY = Symbol.for('vela.better-auth.user');\nexport const AUTH_SESSION_KEY = Symbol.for('vela.better-auth.session');\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 REQUEST_CONTEXT,\n Reflector,\n UnauthorizedException,\n type CanActivate,\n type ExecutionContext,\n type RequestContext,\n} from '@velajs/vela';\nimport { AUTH_SESSION_KEY, AUTH_USER_KEY, BETTER_AUTH_OPTIONS } from '../better-auth.tokens';\nimport { BetterAuthService } from '../better-auth.service';\nimport type { BetterAuthModuleOptions } from '../better-auth.types';\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 rather than the raw better-auth instance.\n // The service's lazy `.auth` getter defers construction to first use, so\n // forRootAsync factories that depend on values only available at\n // request time (Cloudflare D1/KV bindings, etc.) build safely on the\n // first canActivate — not at module-load bootstrap.\n @Inject(BetterAuthService) private readonly auth: BetterAuthService,\n @Inject(BETTER_AUTH_OPTIONS) private readonly opts: BetterAuthModuleOptions,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n if (this.reflector.getAllAndOverride(Public, context)) return true;\n\n const request = context.getRequest();\n const path = new URL(request.url).pathname;\n const basePath = this.opts.basePath ?? '/api/auth';\n if (path === basePath || path.startsWith(`${basePath}/`)) return true;\n\n const data = await this.auth.api.getSession({ headers: request.headers });\n\n if (data) {\n const reqCtx = resolveRequestContext(context);\n reqCtx.set(AUTH_USER_KEY, data.user);\n reqCtx.set(AUTH_SESSION_KEY, data.session);\n return true;\n }\n\n if (\n this.opts.defaultPolicy === 'allow' ||\n this.reflector.getAllAndOverride(OptionalAuth, context)\n ) {\n return true;\n }\n\n throw new UnauthorizedException('Authentication required');\n }\n}\n\ninterface ContainerLike {\n resolve<T>(token: unknown): T;\n}\n\nfunction resolveRequestContext(context: ExecutionContext): RequestContext {\n const honoCtx = context.getContext() as { get: (k: string) => ContainerLike };\n const container = honoCtx.get('container');\n return container.resolve<RequestContext>(REQUEST_CONTEXT);\n}\n","import { Reflector } from '@velajs/vela';\n\nexport const Roles = Reflector.createDecorator<string[]>({ key: 'vela.auth.roles' });\nexport const ROLES_KEY = Roles.KEY;\n","import {\n ForbiddenException,\n Injectable,\n REQUEST_CONTEXT,\n Reflector,\n type CanActivate,\n type ExecutionContext,\n type RequestContext,\n} from '@velajs/vela';\nimport { AUTH_USER_KEY } from '../better-auth.tokens';\nimport type { User } from '../better-auth.types';\nimport { Roles } from '../decorators/roles.decorator';\n\n@Injectable()\nexport class RolesGuard implements CanActivate {\n private readonly reflector = new Reflector();\n\n canActivate(context: ExecutionContext): boolean {\n const required = this.reflector.getAllAndOverride(Roles, context);\n if (!required || required.length === 0) return true;\n\n const honoCtx = context.getContext() as {\n get: (k: string) => { resolve<T>(t: unknown): T };\n };\n const reqCtx = honoCtx.get('container').resolve<RequestContext>(REQUEST_CONTEXT);\n const user = reqCtx.get<User & { role?: string | string[] }>(AUTH_USER_KEY);\n if (!user) {\n throw new ForbiddenException('Role check requires authentication');\n }\n\n const userRoles = normalizeRoles(user.role);\n const ok = required.some((r) => userRoles.includes(r));\n if (!ok) {\n throw new ForbiddenException(`Insufficient role; one of [${required.join(', ')}] required`);\n }\n return true;\n }\n}\n\nfunction normalizeRoles(role: string | string[] | undefined): string[] {\n if (!role) return [];\n if (Array.isArray(role)) return role;\n return role\n .split(',')\n .map((r) => r.trim())\n .filter(Boolean);\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 = User & { role?: string | string[] | null };\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 * Adapts a better-auth user into a `@velajs/authz` {@link Identity}. Maps\n * `user.id` → `userId` and the admin-plugin `role` field → `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 = (user: AuthUser | null | undefined): Identity => {\n if (!user) return { roles: [] };\n return {\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","import { Reflector } from '@velajs/vela';\n\n/**\n * Declares the `@velajs/authz` permission(s) required to reach a controller or\n * route handler. Read via `Reflector` in an authorization guard, then checked\n * against the caller's `Identity` with `authz.can(...)`.\n *\n * ```ts\n * @RequirePermission(['posts:write'])\n * @Post()\n * create() { ... }\n * ```\n *\n * The metadata is a plain `string[]` of permission strings in the granted-side\n * format `@velajs/authz` matches (`resource:action`, or wildcards like\n * `posts:*`). Handler-level metadata overrides class-level (standard\n * `Reflector.getAllAndOverride` precedence).\n *\n * Semantics are **require-ALL** (AND): every listed permission must be granted\n * for access — the `PermissionGuard` denies if any one is missing. This\n * contrasts with `@Roles`, which is **OR** (any one of the listed roles\n * suffices).\n */\nexport const RequirePermission = Reflector.createDecorator<string[]>({\n key: 'vela.authz.permissions',\n});\n\nexport const REQUIRE_PERMISSION_KEY = RequirePermission.KEY;\n","import {\n ForbiddenException,\n Injectable,\n InjectionToken,\n REQUEST_CONTEXT,\n Reflector,\n type CanActivate,\n type ExecutionContext,\n type RequestContext,\n} from '@velajs/vela';\nimport { AUTHZ } from '@velajs/authz/vela';\nimport type { Authz } from '@velajs/authz';\nimport { AUTH_USER_KEY } from '../better-auth.tokens';\nimport type { User } from '../better-auth.types';\nimport { identityFromUser } from '../authz-bridge';\nimport { RequirePermission } from '../decorators/require-permission.decorator';\n\n// The linked `@velajs/authz` is built against its own (newer) `@velajs/vela`\n// copy, so the `AUTHZ` token's `InjectionToken` type is nominally distinct from\n// this package's `InjectionToken` — even though it is the very same runtime\n// token object (the DI container matches tokens by object identity). Re-type it\n// to the local `InjectionToken` so the request-time `container.resolve(...)`\n// accepts it without a structural clash. This is purely a compile-time alias;\n// it changes nothing at runtime. (Version-skew workaround until both publish.)\nconst AUTHZ_TOKEN = AUTHZ as unknown as InjectionToken<Authz>;\n\n/**\n * Enforces the `@RequirePermission(...)` metadata against the `@velajs/authz`\n * engine. For each required permission it calls `authz.can(identity, perm)`,\n * requiring **all** of them (AND semantics — contrast {@link RolesGuard}, which\n * is OR over roles). The caller's `Identity` is derived from the better-auth\n * user that {@link AuthGuard} placed in the request context, so this guard must\n * run *after* `AuthGuard` (e.g. `@UseGuards(AuthGuard, PermissionGuard)`).\n *\n * `AUTHZ` is resolved at **request time** from the per-request container (the\n * same container `REQUEST_CONTEXT` is resolved from), not constructor-injected.\n * This deliberately avoids DI visibility coupling: the guard works whether or\n * not `AuthzModule` is registered as global — a present-but-non-global\n * `AuthzModule` resolves fine and, crucially, never crashes bootstrap. If\n * `AuthzModule` is not registered at all the resolve fails and the guard fails\n * closed (403) rather than granting access.\n *\n * Fail-closed on every abnormal path — no branch grants access on missing\n * wiring or a missing caller:\n * - no required permissions → allow (nothing to enforce);\n * - `AUTHZ` unresolvable (`AuthzModule` not registered) → deny (`ForbiddenException`);\n * - no authenticated user in the request context → deny;\n * - any single required permission not granted → deny.\n *\n * The guard is stateless (no injected dependencies), so it is safe to register\n * as a plain provided guard.\n */\n@Injectable()\nexport class PermissionGuard implements CanActivate {\n private readonly reflector = new Reflector();\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n const required = this.reflector.getAllAndOverride(RequirePermission, context);\n if (!required || required.length === 0) return true;\n\n const honoCtx = context.getContext() as {\n get: (k: string) => { resolve<T>(t: unknown): T };\n };\n const container = honoCtx.get('container');\n\n // Resolve AUTHZ at request time from the per-request container — the SAME\n // container REQUEST_CONTEXT resolves from. Because this lookup carries no\n // requesting module, it matches AUTHZ by its exporter, so a non-global\n // `AuthzModule` is reachable without forcing the app to declare it global.\n // `resolve` throws (or, defensively, could yield undefined) for an\n // unregistered token, so wrap it and fail closed on any failure.\n let authz: Authz | undefined;\n try {\n authz = container.resolve<Authz>(AUTHZ_TOKEN);\n } catch {\n authz = undefined;\n }\n if (!authz) {\n throw new ForbiddenException('Authorization is not configured');\n }\n\n const reqCtx = container.resolve<RequestContext>(REQUEST_CONTEXT);\n const user = reqCtx.get<User & { id?: string; role?: string | string[] }>(AUTH_USER_KEY);\n if (!user) {\n throw new ForbiddenException('Permission check requires authentication');\n }\n\n const identity = identityFromUser(user);\n for (const permission of required) {\n if (!(await authz.can(identity, permission))) {\n throw new ForbiddenException(`Missing permission: ${permission}`);\n }\n }\n return true;\n }\n}\n","import {\n defineModule,\n lazyProvider,\n provideGlobal,\n stableHash,\n type DynamicModule,\n type InferTokens,\n type ProviderOptions,\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 { BetterAuthInstance, BetterAuthModuleOptions } from './better-auth.types';\nimport { AuthGuard } from './guards/auth.guard';\nimport { RolesGuard } from './guards/roles.guard';\nimport { PermissionGuard } from './guards/permission.guard';\n\nconst DEFAULT_BASE_PATH = '/api/auth';\n\n/** Structural options with defaults applied (everything but the auth instance). */\ninterface NormalizedOptions {\n basePath: string;\n isGlobal: boolean;\n defaultPolicy: 'deny' | 'allow';\n mountHandler: boolean;\n}\n\nfunction normalize(options: Partial<BetterAuthModuleOptions>): NormalizedOptions {\n return {\n basePath: options.basePath ?? DEFAULT_BASE_PATH,\n isGlobal: options.isGlobal ?? false,\n defaultPolicy: options.defaultPolicy ?? 'deny',\n mountHandler: options.mountHandler ?? true,\n };\n}\n\n/** Providers, controllers, and exports shared by both entry points. */\nfunction commonContributions(n: NormalizedOptions): {\n providers: Array<Type | ProviderOptions>;\n controllers: Type[];\n exports: DynamicModule['exports'];\n} {\n return {\n providers: [BetterAuthService, AuthGuard, RolesGuard, PermissionGuard],\n controllers: n.mountHandler ? [createBetterAuthCatchallController(n.basePath)] : [],\n exports: [BetterAuthService, BETTER_AUTH_OPTIONS, AuthGuard, RolesGuard, PermissionGuard],\n };\n}\n\n/**\n * The blessed engine generates `forRoot`. `setup` runs once per instance at\n * call time: it re-provides {@link BETTER_AUTH_OPTIONS} with defaults applied,\n * derives the auth builder from those options, mounts the catch-all controller,\n * and — via the `global:` slot — registers the app-wide guard when `isGlobal`.\n *\n * `isGlobal` here means \"apply AuthGuard app-wide\", NOT \"make this a global\n * module\", so the default `isGlobal → global: true` extras transform is\n * replaced with identity; the flag reaches `setup` through the options bag.\n */\nconst authModuleHost = defineModule<BetterAuthModuleOptions>({\n name: 'BetterAuth',\n optionsToken: BETTER_AUTH_OPTIONS,\n transform: (definition) => definition,\n // The auth instance is a stateful value — key off the structural subset only.\n key: (options) => stableHash(normalize(options)),\n setup: ({ OPTIONS, options }) => {\n const n = normalize(options);\n const common = commonContributions(n);\n const auth = (options as BetterAuthModuleOptions).auth;\n return {\n providers: [\n // Override the auto-provided raw bag with the normalized shape so\n // BETTER_AUTH_OPTIONS consumers always see defaults + the auth instance.\n { provide: OPTIONS, useValue: { ...n, auth } },\n // Eager auth: the builder hands back the instance the caller passed in.\n lazyProvider({\n provide: BETTER_AUTH_BUILDER,\n inject: [OPTIONS],\n useFactory: (o: BetterAuthModuleOptions) => o.auth,\n }),\n ...common.providers,\n ],\n controllers: common.controllers,\n exports: common.exports,\n global: n.isGlobal ? { guards: [AuthGuard] } : undefined,\n };\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: [D1Service, ConfigService], // captured as readonly tuple\n * useFactory: (d1, config) => // d1: D1Service, config: ConfigService\n * betterAuth({ database: drizzleAdapter(drizzle(d1.database), ...) }),\n * });\n * ```\n */\ninterface ForRootAsyncOptions<\n Inject extends readonly Token<unknown>[] = readonly Token<unknown>[],\n> {\n inject?: Inject;\n imports?: DynamicModule['imports'];\n useFactory: (...deps: InferTokens<Inject>) => BetterAuthInstance;\n isGlobal?: boolean;\n mountHandler?: boolean;\n basePath?: string;\n defaultPolicy?: 'deny' | 'allow';\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 // Delegate to the generated static, then rebrand the module identity so the\n // public `BetterAuthModule` class is the one registered (consistent with\n // `forRootAsync` and better diagnostics).\n return { ...authModuleHost.ConfigurableModuleClass.forRoot(options), module: BetterAuthModule };\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 behind {@link lazyProvider}'s memoized thunk. This is what makes\n * Cloudflare bindings (D1, KV, R2) work: the binding isn't ready at boot, but\n * it IS by the time a request flows through and the guard / catch-all reads\n * the service. Inject deps resolve at module load (cheap BindingRef wrappers);\n * their *values* are read at first auth use, inside your factory body.\n */\n static forRootAsync<const Inject extends readonly Token<unknown>[] = readonly Token<unknown>[]>(\n options: ForRootAsyncOptions<Inject>,\n ): DynamicModule {\n const n = normalize(options);\n const common = commonContributions(n);\n return {\n module: BetterAuthModule,\n key: options.key ?? stableHash({ ...n, inject: options.inject }),\n imports: options.imports ?? [],\n providers: [\n { provide: BETTER_AUTH_OPTIONS, useValue: n },\n // The deferred auth builder: `lazyProvider` wraps the user factory in a\n // memoized thunk, replacing the hand-rolled `(...deps) => () => f(...deps)`.\n lazyProvider({\n provide: BETTER_AUTH_BUILDER,\n inject: options.inject,\n useFactory: options.useFactory,\n }),\n ...common.providers,\n ...(n.isGlobal ? provideGlobal('guard', AuthGuard) : []),\n ],\n controllers: common.controllers,\n exports: common.exports,\n };\n }\n}\n","import {\n createLazyParamDecorator,\n REQUEST_CONTEXT,\n type ExecutionContext,\n type RequestContext,\n} from '@velajs/vela';\nimport { AUTH_USER_KEY } from '../better-auth.tokens';\nimport type { User } from '../better-auth.types';\n\nexport const CurrentUser = createLazyParamDecorator((_data: unknown, ctx: ExecutionContext) => {\n const honoCtx = ctx.getContext() as { get: (k: string) => { resolve<T>(t: unknown): T } };\n const reqCtx = honoCtx.get('container').resolve<RequestContext>(REQUEST_CONTEXT);\n return reqCtx.get<User>(AUTH_USER_KEY);\n});\n","import {\n createLazyParamDecorator,\n REQUEST_CONTEXT,\n type ExecutionContext,\n type RequestContext,\n} from '@velajs/vela';\nimport { AUTH_SESSION_KEY } from '../better-auth.tokens';\nimport type { Session } from '../better-auth.types';\n\nexport const CurrentSession = createLazyParamDecorator((_data: unknown, ctx: ExecutionContext) => {\n const honoCtx = ctx.getContext() as { get: (k: string) => { resolve<T>(t: unknown): T } };\n const reqCtx = honoCtx.get('container').resolve<RequestContext>(REQUEST_CONTEXT);\n return reqCtx.get<Session>(AUTH_SESSION_KEY);\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["ACCESS_DENIED","normalizeRoles"],"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/decorators/optional-auth.decorator.ts","../src/guards/auth.guard.ts","../src/decorators/roles.decorator.ts","../src/guards/roles.guard.ts","../src/authz-bridge.ts","../src/decorators/require-permission.decorator.ts","../src/guards/permission.guard.ts","../src/better-auth.module.ts","../src/decorators/current-user.decorator.ts","../src/decorators/current-session.decorator.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\n/**\n * Default-path (`/api/auth`) catch-all controller. Retained for back-compat;\n * `BetterAuthModule` now mounts {@link createBetterAuthCatchallController} with\n * the configured `basePath`. Prefer the factory for a custom base path.\n */\nexport const BetterAuthCatchallController = createBetterAuthCatchallController();\n","import { InjectionToken } from '@velajs/vela';\nimport type { BetterAuthModuleOptions } from './better-auth.types';\n\nexport const BETTER_AUTH_OPTIONS = new InjectionToken<BetterAuthModuleOptions>(\n 'vela.BetterAuthOptions',\n);\n\nexport const AUTH_USER_KEY = Symbol.for('vela.better-auth.user');\nexport const AUTH_SESSION_KEY = Symbol.for('vela.better-auth.session');\nexport const AUTH_ISSUER_KEY = Symbol.for('vela.better-auth.issuer');\nexport const AUTH_PRINCIPAL_TYPE_KEY = Symbol.for('vela.better-auth.principal-type');\n","import type { ExecutionContext } from '@velajs/vela';\nimport type { Session, User } from './better-auth.types';\n\nexport interface AuthenticatedRequestState {\n readonly authenticated: true;\n readonly user: User;\n readonly session: Session;\n readonly issuer: string;\n readonly principalType: 'user';\n}\n\ninterface AnonymousRequestState {\n readonly authenticated: false;\n}\n\nexport type AuthRequestState = AuthenticatedRequestState | AnonymousRequestState;\n\nconst ANONYMOUS: AnonymousRequestState = Object.freeze({ authenticated: false });\n\n// Canonical authentication state is request-local and unforgeable by upstream\n// Hono middleware. It deliberately does not depend on resolving a DI token:\n// guards execute before parameter extraction and test harnesses may load Vela's\n// public/internal entry points as separate module instances. Both execution\n// contexts still expose the same raw Request object.\nconst stateByRequest = new WeakMap<Request, AuthRequestState>();\n\n/** Clear any state before a guard evaluates a request. */\nexport const beginAuthRequest = (context: ExecutionContext): void => {\n stateByRequest.set(context.getRequest(), ANONYMOUS);\n};\n\n/** Publish a fully verified session for downstream guards and parameters. */\nexport const authenticateRequest = (\n context: ExecutionContext,\n state: Omit<AuthenticatedRequestState, 'authenticated'>,\n): AuthenticatedRequestState => {\n const authenticated: AuthenticatedRequestState = Object.freeze({\n authenticated: true,\n ...state,\n });\n stateByRequest.set(context.getRequest(), authenticated);\n return authenticated;\n};\n\n/** Missing state is anonymous: no guard means no ambient identity. */\nexport const getAuthRequestState = (context: ExecutionContext): AuthRequestState =>\n context.getType() === 'http'\n ? (stateByRequest.get(context.getRequest()) ?? ANONYMOUS)\n : ANONYMOUS;\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 REQUEST_CONTEXT,\n Reflector,\n UnauthorizedException,\n clearTrustedRequestIdentity,\n setTrustedRequestIdentity,\n type CanActivate,\n type ExecutionContext,\n type RequestContext,\n} from '@velajs/vela';\nimport {\n AUTH_ISSUER_KEY,\n AUTH_PRINCIPAL_TYPE_KEY,\n AUTH_SESSION_KEY,\n AUTH_USER_KEY,\n BETTER_AUTH_OPTIONS,\n} from '../better-auth.tokens';\nimport { BetterAuthService } from '../better-auth.service';\nimport type { BetterAuthModuleOptions, Session, User } from '../better-auth.types';\nimport {\n authenticateRequest,\n beginAuthRequest,\n type AuthRequestState,\n} from '../auth-request-state';\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 rather than the raw better-auth instance.\n // The service's lazy `.auth` getter defers construction to first use, so\n // forRootAsync factories that depend on values only available at\n // request time (Cloudflare D1/KV bindings, etc.) build safely on the\n // first canActivate — not at module-load bootstrap.\n @Inject(BetterAuthService) private readonly auth: BetterAuthService,\n @Inject(BETTER_AUTH_OPTIONS) private readonly opts: BetterAuthModuleOptions,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n // WebSocket upgrades are authenticated before allocation. Frames reuse\n // only that normalized attachment identity; never call HTTP-only context\n // accessors or re-read ambient cookies after the connection is established.\n if (context.getType() === 'ws') {\n if (hasValidWebSocketIdentity(context)) return true;\n throw new AuthenticationRequiredException();\n }\n\n // The guard owns this request's identity epoch. Clear first so public,\n // optional, malformed, and throwing auth paths can never inherit state.\n beginAuthRequest(context);\n mirrorRequestContext(context, { authenticated: false });\n\n if (this.reflector.getAllAndOverride(Public, context)) return true;\n\n const request = context.getRequest();\n const raw = await this.auth.api.getSession({ headers: request.headers });\n const data = validateSessionData(raw);\n\n if (data) {\n const state = authenticateRequest(context, {\n user: data.user,\n session: data.session,\n issuer: this.opts.issuer ?? 'better-auth',\n principalType: 'user',\n });\n mirrorRequestContext(context, state);\n return true;\n }\n\n if (this.reflector.getAllAndOverride(OptionalAuth, context)) {\n return true;\n }\n\n throw new AuthenticationRequiredException();\n }\n}\n\nfunction hasValidWebSocketIdentity(context: ExecutionContext): boolean {\n try {\n const client = context.switchToWs().getClient<{ data?: unknown }>();\n const data = client?.data;\n if (!data || typeof data !== 'object') return false;\n const record = data as Record<string, unknown>;\n const principal = record.principal;\n if (!principal || typeof principal !== 'object') return false;\n const fields = principal as Record<string, unknown>;\n return (\n typeof fields.issuer === 'string' &&\n fields.issuer.length > 0 &&\n typeof fields.subject === 'string' &&\n fields.subject.length > 0 &&\n (fields.principalType === 'user' || fields.principalType === 'service') &&\n typeof record.tenantId === 'string' &&\n record.tenantId.length > 0 &&\n typeof record.expiresAtMs === 'number' &&\n Number.isSafeInteger(record.expiresAtMs) &&\n record.expiresAtMs > Date.now()\n );\n } catch {\n return false;\n }\n}\n\ninterface ContainerLike {\n resolve<T>(token: unknown): T;\n}\n\n/**\n * Preserve the public REQUEST_CONTEXT symbols for applications that consume\n * them directly. The private Request-keyed state above remains canonical: a\n * missing/duplicated framework token must not prevent a verified guard from\n * publishing identity to its own downstream decorators and guards.\n */\nfunction mirrorRequestContext(context: ExecutionContext, state: AuthRequestState): void {\n const request = context.getRequest();\n if (!state.authenticated) {\n clearTrustedRequestIdentity(request);\n } else {\n const tenantId = readActiveOrganizationId(state.session);\n setTrustedRequestIdentity(request, {\n principal: {\n issuer: state.issuer,\n subject: state.user.id,\n principalType: state.principalType,\n },\n ...(tenantId === undefined ? {} : { tenantId }),\n });\n }\n\n const honoCtx = context.getContext() as { get: (k: string) => ContainerLike | undefined };\n const container = honoCtx.get('container');\n if (!container) return;\n\n let reqCtx: RequestContext;\n try {\n reqCtx = container.resolve<RequestContext>(REQUEST_CONTEXT);\n } catch {\n return;\n }\n\n if (!state.authenticated) {\n reqCtx.set<User | undefined>(AUTH_USER_KEY, undefined);\n reqCtx.set<Session | undefined>(AUTH_SESSION_KEY, undefined);\n reqCtx.set<string | undefined>(AUTH_ISSUER_KEY, undefined);\n reqCtx.set<'user' | undefined>(AUTH_PRINCIPAL_TYPE_KEY, undefined);\n return;\n }\n\n reqCtx.set(AUTH_USER_KEY, state.user);\n reqCtx.set(AUTH_SESSION_KEY, state.session);\n reqCtx.set(AUTH_ISSUER_KEY, state.issuer);\n reqCtx.set(AUTH_PRINCIPAL_TYPE_KEY, state.principalType);\n}\n\nfunction readActiveOrganizationId(session: Session): string | undefined {\n const descriptor = Object.getOwnPropertyDescriptor(session, 'activeOrganizationId');\n if (descriptor === undefined || !('value' in descriptor)) return undefined;\n const value: unknown = descriptor.value;\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\ninterface SessionData {\n user: User;\n session: Session;\n}\n\n/** A test/provider override is trusted code, but its runtime result is not. */\nfunction validateSessionData(value: unknown): SessionData | undefined {\n if (value === null || typeof value !== 'object') return undefined;\n\n try {\n const userDescriptor = Object.getOwnPropertyDescriptor(value, 'user');\n const sessionDescriptor = Object.getOwnPropertyDescriptor(value, 'session');\n if (\n userDescriptor === undefined ||\n !('value' in userDescriptor) ||\n sessionDescriptor === undefined ||\n !('value' in sessionDescriptor)\n ) {\n return undefined;\n }\n\n const user: unknown = userDescriptor.value;\n const session: unknown = sessionDescriptor.value;\n if (\n user === null ||\n typeof user !== 'object' ||\n session === null ||\n typeof session !== 'object'\n ) {\n return undefined;\n }\n\n const userIdDescriptor = Object.getOwnPropertyDescriptor(user, 'id');\n const sessionIdDescriptor = Object.getOwnPropertyDescriptor(session, 'id');\n const sessionUserIdDescriptor = Object.getOwnPropertyDescriptor(session, 'userId');\n const userId =\n userIdDescriptor !== undefined && 'value' in userIdDescriptor\n ? userIdDescriptor.value\n : undefined;\n const sessionId =\n sessionIdDescriptor !== undefined && 'value' in sessionIdDescriptor\n ? sessionIdDescriptor.value\n : undefined;\n const sessionUserId =\n sessionUserIdDescriptor !== undefined && 'value' in sessionUserIdDescriptor\n ? sessionUserIdDescriptor.value\n : undefined;\n\n if (\n typeof userId !== 'string' ||\n userId.length === 0 ||\n typeof sessionId !== 'string' ||\n sessionId.length === 0 ||\n sessionUserId !== userId\n ) {\n return undefined;\n }\n\n // The identity-bearing fields above are validated as own data properties.\n // Remaining Better Auth/plugin fields stay intact for typed consumers.\n return { user: user as User, session: session as Session };\n } catch {\n return undefined;\n }\n}\n\n/**\n * `UnauthorizedException` preserves Nest-style direct behavior. The structural\n * VelaError brand also survives test/runtime package duplication, so the\n * central renderer still maps this to 401 rather than treating it as a foreign\n * 500 error.\n */\nclass AuthenticationRequiredException extends UnauthorizedException {\n readonly type = 'VelaError' as const;\n readonly code = 'unauthorized';\n readonly status = 401;\n\n constructor() {\n super('Authentication required');\n }\n}\n","import { Reflector } from '@velajs/vela';\n\nexport const Roles = Reflector.createDecorator<string[]>({ key: 'vela.auth.roles' });\nexport const ROLES_KEY = Roles.KEY;\n","import {\n ForbiddenException,\n Injectable,\n Reflector,\n type CanActivate,\n type ExecutionContext,\n} from '@velajs/vela';\nimport { getAuthRequestState } from '../auth-request-state';\nimport { Roles } from '../decorators/roles.decorator';\n\nconst ACCESS_DENIED = 'Access denied';\n\n@Injectable()\nexport class RolesGuard implements CanActivate {\n private readonly reflector = new Reflector();\n\n canActivate(context: ExecutionContext): boolean {\n const required = this.reflector.getAllAndOverride(Roles, context);\n if (!required || required.length === 0) return true;\n\n const state = getAuthRequestState(context);\n if (!state.authenticated) {\n throw new ForbiddenException(ACCESS_DENIED);\n }\n\n const userRoles = normalizeRoles((state.user as { role?: string | string[] }).role);\n const ok = required.some((r) => userRoles.includes(r));\n if (!ok) {\n throw new ForbiddenException(ACCESS_DENIED);\n }\n return true;\n }\n}\n\nfunction normalizeRoles(role: string | string[] | undefined): string[] {\n if (!role) return [];\n if (Array.isArray(role)) return role;\n return role\n .split(',')\n .map((r) => r.trim())\n .filter(Boolean);\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 = User & { 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 * Adapts a 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","import { Reflector } from '@velajs/vela';\n\n/**\n * Declares the `@velajs/authz` permission(s) required to reach a controller or\n * route handler. Read via `Reflector` in an authorization guard, then checked\n * against the caller's `Identity` with `authz.can(...)`.\n *\n * ```ts\n * @RequirePermission(['posts:write'])\n * @Post()\n * create() { ... }\n * ```\n *\n * The metadata is a plain `string[]` of permission strings in the granted-side\n * format `@velajs/authz` matches (`resource:action`, or wildcards like\n * `posts:*`). Handler-level metadata overrides class-level (standard\n * `Reflector.getAllAndOverride` precedence).\n *\n * Semantics are **require-ALL** (AND): every listed permission must be granted\n * for access — the `PermissionGuard` denies if any one is missing. This\n * contrasts with `@Roles`, which is **OR** (any one of the listed roles\n * suffices).\n */\nexport const RequirePermission = Reflector.createDecorator<string[]>({\n key: 'vela.authz.permissions',\n});\n\nexport const REQUIRE_PERMISSION_KEY = RequirePermission.KEY;\n","import {\n ForbiddenException,\n Injectable,\n Reflector,\n type CanActivate,\n type ExecutionContext,\n type InjectionToken,\n} from '@velajs/vela';\nimport { AUTHZ } from '@velajs/authz/vela';\nimport type { Authz, Identity } from '@velajs/authz';\nimport { getAuthRequestState } from '../auth-request-state';\nimport { identityFromUser } from '../authz-bridge';\nimport { RequirePermission } from '../decorators/require-permission.decorator';\n\n// The linked `@velajs/authz` is built against its own (newer) `@velajs/vela`\n// copy, so the `AUTHZ` token's `InjectionToken` type is nominally distinct from\n// this package's `InjectionToken` — even though it is the very same runtime\n// token object (the DI container matches tokens by object identity). Re-type it\n// to the local `InjectionToken` so the request-time `container.resolve(...)`\n// accepts it without a structural clash. This is purely a compile-time alias;\n// it changes nothing at runtime. (Version-skew workaround until both publish.)\nconst AUTHZ_TOKEN = AUTHZ as unknown as InjectionToken<Authz>;\nconst ACCESS_DENIED = 'Access denied';\n\ninterface ContainerLike {\n resolve<T>(token: unknown, requestingModuleId?: string): T;\n resolveAll?<T>(token: unknown, requestingModuleId?: string): T[];\n}\n\nfunction resolveSingleAuthz(container: ContainerLike, moduleId: string): Authz | undefined {\n try {\n if (typeof container.resolveAll === 'function') {\n const candidates = container.resolveAll<Authz>(AUTHZ_TOKEN, moduleId);\n return candidates.length === 1 ? candidates[0] : undefined;\n }\n return container.resolve<Authz>(AUTHZ_TOKEN, moduleId);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Enforces the `@RequirePermission(...)` metadata against the `@velajs/authz`\n * engine. For each required permission it calls `authz.can(identity, perm)`,\n * requiring **all** of them (AND semantics — contrast {@link RolesGuard}, which\n * is OR over roles). The caller's `Identity` is derived from the better-auth\n * user that {@link AuthGuard} placed in canonical request-local auth state, so this guard must\n * run *after* `AuthGuard` (e.g. `@UseGuards(AuthGuard, PermissionGuard)`).\n *\n * `AUTHZ` is resolved at request time from the per-request container. Exactly\n * one reachable engine is required; zero or multiple registrations deny rather\n * than selecting one by import order.\n *\n * Fail-closed on every abnormal path — no branch grants access on missing\n * wiring or a missing caller:\n * - no required permissions → allow (nothing to enforce);\n * - `AUTHZ` unresolvable (`AuthzModule` not registered) → deny (`ForbiddenException`);\n * - no authenticated user in request-local auth state → deny;\n * - any single required permission not granted → deny.\n *\n * The guard is stateless (no injected dependencies), so it is safe to register\n * as a plain provided guard.\n */\n@Injectable()\nexport class PermissionGuard implements CanActivate {\n private readonly reflector = new Reflector();\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n const required = this.reflector.getAllAndOverride(RequirePermission, context);\n if (!required || required.length === 0) return true;\n\n const container = resolveContextContainer(context);\n const moduleId = context.getModuleId();\n\n // Resolve all visible AUTHZ registrations and accept only an unambiguous\n // single engine. This prevents import order from selecting another tenant's\n // or feature module's authorization policy.\n const authz =\n container === undefined || moduleId === undefined\n ? undefined\n : resolveSingleAuthz(container, moduleId);\n if (!authz) {\n throw new ForbiddenException(ACCESS_DENIED);\n }\n\n const identity = resolveContextIdentity(context);\n if (identity === undefined) throw new ForbiddenException(ACCESS_DENIED);\n for (const permission of required) {\n if (!(await authz.can(identity, permission))) {\n throw new ForbiddenException(ACCESS_DENIED);\n }\n }\n return true;\n }\n}\n\nfunction resolveContextContainer(context: ExecutionContext): ContainerLike | undefined {\n const direct = context.getContainer?.<ContainerLike>();\n if (direct !== undefined) return direct;\n if (context.getType() !== 'http') return undefined;\n try {\n const honoCtx = context.getContext() as { get: (key: string) => ContainerLike | undefined };\n return honoCtx.get('container');\n } catch {\n return undefined;\n }\n}\n\nfunction resolveContextIdentity(context: ExecutionContext): Identity | undefined {\n if (context.getType() === 'ws') {\n try {\n const data = context.switchToWs().getClient<{ data?: unknown }>()?.data;\n if (!data || typeof data !== 'object') return undefined;\n const record = data as Record<string, unknown>;\n const principal = record.principal;\n if (!principal || typeof principal !== 'object') return undefined;\n const fields = principal as Record<string, unknown>;\n if (\n typeof fields.issuer !== 'string' ||\n fields.issuer.length === 0 ||\n typeof fields.subject !== 'string' ||\n fields.subject.length === 0 ||\n (fields.principalType !== 'user' && fields.principalType !== 'service') ||\n typeof record.tenantId !== 'string' ||\n record.tenantId.length === 0 ||\n typeof record.expiresAtMs !== 'number' ||\n !Number.isSafeInteger(record.expiresAtMs) ||\n record.expiresAtMs <= Date.now()\n ) {\n return undefined;\n }\n return {\n issuer: fields.issuer,\n subject: fields.subject,\n principalType: fields.principalType,\n userId: fields.subject,\n roles: [],\n };\n } catch {\n return undefined;\n }\n }\n\n const state = getAuthRequestState(context);\n return state.authenticated\n ? identityFromUser(state.user, state.issuer, state.principalType)\n : undefined;\n}\n","import {\n defineModule,\n lazyProvider,\n provideGlobal,\n stableHash,\n type DynamicModule,\n type InferTokens,\n type ProviderOptions,\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 { BetterAuthInstance, BetterAuthModuleOptions } from './better-auth.types';\nimport { AuthGuard } from './guards/auth.guard';\nimport { RolesGuard } from './guards/roles.guard';\nimport { PermissionGuard } from './guards/permission.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 defaultPolicy: 'deny';\n mountHandler: boolean;\n}\n\nfunction normalize(options: Partial<BetterAuthModuleOptions>): 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 if (options.defaultPolicy !== undefined && options.defaultPolicy !== 'deny') {\n throw new Error(\n '@velajs/better-auth: defaultPolicy is deny-only; mark anonymous routes with @Public() or @OptionalAuth()',\n );\n }\n return {\n basePath,\n issuer,\n isGlobal: options.isGlobal ?? true,\n defaultPolicy: 'deny',\n mountHandler: options.mountHandler ?? true,\n };\n}\n\n/** Providers, controllers, and exports shared by both entry points. */\nfunction commonContributions(n: NormalizedOptions): {\n providers: Array<Type | ProviderOptions>;\n controllers: Type[];\n exports: DynamicModule['exports'];\n} {\n return {\n providers: [BetterAuthService, AuthGuard, RolesGuard, PermissionGuard],\n controllers: n.mountHandler ? [createBetterAuthCatchallController(n.basePath)] : [],\n exports: [BetterAuthService, BETTER_AUTH_OPTIONS, AuthGuard, RolesGuard, PermissionGuard],\n };\n}\n\n/**\n * The blessed engine generates `forRoot`. `setup` runs once per instance at\n * call time: it re-provides {@link BETTER_AUTH_OPTIONS} with defaults applied,\n * derives the auth builder from those options, mounts the catch-all controller,\n * and — via the `global:` slot — registers the app-wide guard when `isGlobal`.\n *\n * `isGlobal` here means \"apply AuthGuard app-wide\", NOT \"make this a global\n * module\", so the default `isGlobal → global: true` extras transform is\n * replaced with identity; the flag reaches `setup` through the options bag.\n */\nconst authModuleHost = defineModule<BetterAuthModuleOptions>({\n name: 'BetterAuth',\n optionsToken: BETTER_AUTH_OPTIONS,\n transform: (definition) => definition,\n // Public entry points always supply an identity-aware key. Keep this fallback\n // for direct host use in tests and future refactors.\n key: (options) => stableHash(normalize(options)),\n setup: ({ OPTIONS, options }) => {\n const n = normalize(options);\n const common = commonContributions(n);\n const auth = (options as BetterAuthModuleOptions).auth;\n return {\n providers: [\n // Override the auto-provided raw bag with the normalized shape so\n // BETTER_AUTH_OPTIONS consumers always see defaults + the auth instance.\n { provide: OPTIONS, useValue: { ...n, auth } },\n // Eager auth: the builder hands back the instance the caller passed in.\n lazyProvider({\n provide: BETTER_AUTH_BUILDER,\n inject: [OPTIONS],\n useFactory: (o: BetterAuthModuleOptions) => o.auth,\n }),\n ...common.providers,\n ],\n controllers: common.controllers,\n exports: common.exports,\n global: n.isGlobal ? { guards: [AuthGuard] } : undefined,\n };\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: [D1Service, ConfigService], // captured as readonly tuple\n * useFactory: (d1, config) => // d1: D1Service, config: ConfigService\n * betterAuth({ database: drizzleAdapter(drizzle(d1.database), ...) }),\n * });\n * ```\n */\ninterface ForRootAsyncOptions<\n Inject extends readonly Token<unknown>[] = readonly Token<unknown>[],\n> {\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 /** @deprecated Authentication is deny-by-default. Only `'deny'` is accepted. */\n defaultPolicy?: 'deny';\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 // Delegate to the generated static, then rebrand the module identity so the\n // public `BetterAuthModule` class is the one registered (consistent with\n // `forRootAsync` and better diagnostics).\n return {\n ...authModuleHost.ConfigurableModuleClass.forRoot({ ...options, key }),\n module: BetterAuthModule,\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 behind {@link lazyProvider}'s memoized thunk. This is what makes\n * Cloudflare bindings (D1, KV, R2) work: the binding isn't ready at boot, but\n * it IS by the time a request flows through and the guard / catch-all reads\n * the service. Inject deps resolve at module load (cheap BindingRef wrappers);\n * their *values* are read at first auth use, inside your factory body.\n */\n static forRootAsync<const Inject extends readonly Token<unknown>[] = readonly Token<unknown>[]>(\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 { provide: BETTER_AUTH_OPTIONS, useValue: n },\n // The deferred auth builder: `lazyProvider` wraps the user factory in a\n // memoized thunk, replacing the hand-rolled `(...deps) => () => f(...deps)`.\n lazyProvider({\n provide: BETTER_AUTH_BUILDER,\n inject: options.inject,\n useFactory: options.useFactory,\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.authenticated ? state.user : undefined;\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.authenticated ? state.session : undefined;\n },\n);\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velajs/better-auth",
3
- "version": "0.6.1",
3
+ "version": "1.0.0",
4
4
  "description": "better-auth integration for the Vela framework",
5
5
  "keywords": [
6
6
  "auth",
@@ -43,14 +43,14 @@
43
43
  }
44
44
  },
45
45
  "dependencies": {
46
- "@velajs/authz": "^1.0.0"
46
+ "@velajs/authz": "^1.1.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@arethetypeswrong/cli": "^0.18.5",
50
50
  "@changesets/cli": "^2.31.0",
51
51
  "@swc/core": "^1.15.43",
52
- "@velajs/testing": "^0.4.0",
53
- "@velajs/vela": "^1.12.0",
52
+ "@velajs/testing": "^1.0.0",
53
+ "@velajs/vela": "^1.21.0",
54
54
  "better-auth": "^1.6.20",
55
55
  "hono": "^4.12.26",
56
56
  "oxfmt": "^0.58.0",
@@ -59,10 +59,11 @@
59
59
  "tsdown": "^0.22.4",
60
60
  "typescript": "^7.0.2",
61
61
  "unplugin-swc": "^1.5.9",
62
+ "vite": "^8.0.16",
62
63
  "vitest": "^4.1.10"
63
64
  },
64
65
  "peerDependencies": {
65
- "@velajs/vela": ">=1.11.0",
66
+ "@velajs/vela": ">=1.21.0 <2",
66
67
  "better-auth": ">=1.2.0",
67
68
  "hono": ">=4"
68
69
  },
@@ -80,7 +81,9 @@
80
81
  "attw": "attw --pack . --profile esm-only",
81
82
  "changeset": "changeset",
82
83
  "version-packages": "changeset version",
83
- "release": "pnpm build && changeset publish",
84
+ "release:preflight": "npm view @velajs/vela@1.21.0 version && npm view @velajs/authz@1.1.0 version && npm view @velajs/testing@1.0.0 version",
85
+ "release:check": "node scripts/check-release-lock.mjs && pnpm verify && pnpm audit --audit-level=high",
86
+ "release": "pnpm release:preflight && pnpm release:check && changeset publish",
84
87
  "verify": "pnpm lint && pnpm format:check && pnpm build && pnpm typecheck && pnpm test && pnpm publint && pnpm attw"
85
88
  }
86
89
  }