@velajs/better-auth 0.6.1 → 2.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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.0
4
+
5
+ Native environment configuration and one immutable verified identity shared with provider-independent authorization.
6
+
7
+ Requires the coordinated Vela 2.0 package set. See the workspace migration guide.
8
+
9
+ ## 1.0.0
10
+
11
+ ### Major Changes
12
+
13
+ - 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.
14
+
3
15
  ## 0.6.1
4
16
 
5
17
  ### 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)
@@ -73,22 +74,19 @@ imports: [
73
74
 
74
75
  ### Pattern B — DI'd plugin construction
75
76
 
76
- `forRootAsync` lets vela services participate in your better-auth config. Required for Cloudflare D1 / Hyperdrive bindings, since env bindings only resolve at request time.
77
+ `forRootAsync` requires an explicit `inject` tuple (use `inject: []` when there are no dependencies), so factory parameter types always have matching runtime tokens. It lets Vela services participate in your Better Auth configuration. On Workers, declare `WORKER_ENV = new InjectionToken<WorkerEnv>('app.Env')` and pass that token to `createCloudflareWorker(AppModule, { envToken: WORKER_ENV })`; the native event environment is available before DI factories run.
77
78
 
78
79
  ```ts
79
80
  imports: [
80
- D1Module.forRoot({ binding: 'DB' }),
81
81
  BetterAuthModule.forRootAsync({
82
- inject: [D1Service, EmailService],
83
- useFactory: (d1: D1Service, email: EmailService) => ({
84
- auth: betterAuth({
85
- database: drizzleAdapter(drizzle(d1.binding), { provider: 'sqlite' }),
82
+ inject: [WORKER_ENV, EmailService],
83
+ useFactory: (env, email) => betterAuth({
84
+ database: drizzleAdapter(drizzle(env.DB), { provider: 'sqlite' }),
86
85
  plugins: [
87
86
  magicLink({ sendMagicLink: (data) => email.send(data) }), // DI'd EmailService
88
87
  apiKey(),
89
88
  twoFactor(),
90
89
  ],
91
- }),
92
90
  }),
93
91
  isGlobal: true,
94
92
  }),
@@ -101,7 +99,7 @@ Each feature ships a self-contained vela module that exports a plugin token. App
101
99
 
102
100
  ```ts
103
101
  // magic-link-auth.module.ts
104
- import { Module, InjectionToken } from '@velajs/vela';
102
+ import { Module, InjectionToken, defineProvider } from '@velajs/vela';
105
103
  import { magicLink } from 'better-auth/plugins';
106
104
  import { EmailService } from './email.service';
107
105
 
@@ -112,12 +110,11 @@ export const MAGIC_LINK_PLUGIN = new InjectionToken<ReturnType<typeof magicLink>
112
110
  @Module({
113
111
  providers: [
114
112
  EmailService,
115
- {
116
- provide: MAGIC_LINK_PLUGIN,
113
+ defineProvider(MAGIC_LINK_PLUGIN, {
117
114
  inject: [EmailService],
118
115
  useFactory: (email: EmailService) =>
119
116
  magicLink({ sendMagicLink: (d) => email.send({ to: d.email, link: d.url }) }),
120
- },
117
+ }),
121
118
  ],
122
119
  exports: [MAGIC_LINK_PLUGIN],
123
120
  })
@@ -133,9 +130,7 @@ export class MagicLinkAuthModule {}
133
130
  BetterAuthModule.forRootAsync({
134
131
  imports: [MagicLinkAuthModule, OAuthAuthModule],
135
132
  inject: [MAGIC_LINK_PLUGIN, OAUTH_PLUGIN],
136
- useFactory: (magicLink, oauth) => ({
137
- auth: betterAuth({ database, plugins: [magicLink, oauth] }),
138
- }),
133
+ useFactory: (magicLink, oauth) => betterAuth({ database, plugins: [magicLink, oauth] }),
139
134
  isGlobal: true,
140
135
  }),
141
136
  ],
@@ -153,7 +148,7 @@ import { BetterAuthService } from '@velajs/better-auth';
153
148
 
154
149
  @Injectable()
155
150
  class AdminUserService {
156
- constructor(@Inject(BetterAuthService) private readonly auth: BetterAuthService) {}
151
+ constructor(@Inject(BetterAuthService) private readonly auth: BetterAuthService<typeof auth>) {}
157
152
 
158
153
  listSessions(userId: string) {
159
154
  return this.auth.api.listUserSessions({ userId });
@@ -164,32 +159,47 @@ class AdminUserService {
164
159
  }
165
160
  ```
166
161
 
167
- Under `forRootAsync`, the underlying `betterAuth({...})` instance is constructed **lazily on first `.auth` / `.api` / `.handler` access**. That's what makes Cloudflare bindings (D1, KV, R2) work: the user factory only runs after request-time middleware has populated env. No proxies, no lifecycle hooks just a service with a cached field.
162
+ Under `forRootAsync`, the underlying `betterAuth({...})` instance is constructed lazily on first `.auth` / `.api` / `.handler` access. The Workers adapter supplies the typed environment before DI and owns a separate application for each environment, so a cached auth instance never crosses environments.
168
163
 
169
164
  ## Decorators
170
165
 
171
166
  | Decorator | Purpose |
172
167
  | ------------------- | ------------------------------------------------------------------------ |
173
- | `@CurrentUser()` | Lazy parameter — better-auth `User` from the request (read by AuthGuard) |
174
- | `@CurrentSession()` | Lazy parameter — better-auth `Session` |
168
+ | `@CurrentUser()` | Better-auth `User` from the request after guards run |
169
+ | `@CurrentSession()` | Better-auth `Session` after guards run |
175
170
  | `@Public(true)` | Class or method — bypass AuthGuard entirely |
176
171
  | `@OptionalAuth(true)` | Class or method — populate user if present, never throw 401 |
177
- | `@Roles(['admin'])` | Method — read by `RolesGuard`. Compares against `user.role`. |
178
172
 
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`:
173
+ Optional identities are ordinary values: an anonymous caller receives the actual `undefined`, so normal truthiness checks are safe.
180
174
 
181
175
  ```ts
182
176
  handle(@CurrentUser() user: User | undefined) {
183
- return { hasUser: user?.id != null }; // ✓ correct
177
+ return { hasUser: Boolean(user) };
184
178
  }
185
179
  ```
186
180
 
187
181
  ## Guards
188
182
 
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
- - **`RolesGuard`** singleton. Reads `@Roles([...])` metadata, compares against `user.role`. Use with `@UseGuards(AuthGuard, RolesGuard)` order matters.
183
+ - **`AuthGuard`** — singleton. Reads `Authorization` header / cookies via `auth.api.getSession`, validates the full base user/session models, and publishes Vela's trusted principal, tenant, roles, and session expiry 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.
184
+ Permission and role guards live in `@velajs/authz/vela`: import `PermissionGuard`, `RequirePermission`, `RolesGuard`, and `Roles` there. They read the same trusted identity for Better Auth and Cloudflare Access. Run authentication before authorization.
185
+
186
+ 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.
187
+
188
+ When using `ThrottlerModule`, import Better Auth first. Vela then rate-limits by
189
+ the verified issuer, subject, principal type, and active organization before it
190
+ falls back to a platform-attested client address.
191
+
192
+ `BETTER_AUTH_OPTIONS` now contains only runtime configuration; read the auth instance from `BetterAuthService`. The obsolete `defaultPolicy` option and default-path controller constant were removed. Use `createBetterAuthCatchallController()` for a manually mounted catch-all.
193
+
194
+ ## Trusted identity and typing
195
+
196
+ `@CurrentUser()` and `@CurrentSession()` expose only validated Better Auth data tied to the exact current trusted identity. Public routes, missing/rejected sessions, logout, expiry, and another provider replacing the identity invalidate those values. Hono user variables and removed compatibility symbols cannot grant roles or permissions.
197
+
198
+ Core `getTrustedRequestIdentity(request)` and authz `@CurrentIdentity()` return the verified issuer/subject/type, optional tenant, explicit roles, and credential expiry. WebSocket guards consume only the normalized server connection attachment and do not consult HTTP cookies or arbitrary socket role metadata.
199
+
200
+ The integration accepts the minimal `BetterAuthInstance` contract instead of `Auth<any>`. `new BetterAuthService(() => auth)` infers the concrete instance and retains plugin API/result types. For an injected service, annotate `BetterAuthService<typeof auth>` with the same configured instance type. The unparameterized service intentionally exposes only the operations the framework itself requires.
191
201
 
192
- Global registration: pass `isGlobal: true` to `forRoot` (binds AuthGuard to `APP_GUARD`). Routes are deny-by-default; mark public ones with `@Public(true)`.
202
+ Migration: identity symbols (`AUTH_USER_KEY`, `AUTH_SESSION_KEY`, issuer/type keys) were removed. Use validated parameter decorators or core's trusted identity reader. Authorization imports moved to `@velajs/authz/vela`; the module no longer registers duplicate provider-specific permission guards.
193
203
 
194
204
  ## Edge-safe DB adapters
195
205
 
@@ -223,7 +233,7 @@ class CustomCatchallController {
223
233
  }
224
234
  ```
225
235
 
226
- Pass `basePath: '/auth'` to `BetterAuthModule.forRoot` so `AuthGuard` skips the right paths, and keep your `betterAuth({ basePath: '/auth' })` config in sync.
236
+ 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
237
 
228
238
  ## License
229
239
 
@@ -1,17 +1,17 @@
1
1
  import { Inject, Injectable, InjectionToken } from "@velajs/vela";
2
- //#region \0@oxc-project+runtime@0.139.0/helpers/esm/decorateMetadata.js
2
+ //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateMetadata.js
3
3
  function __decorateMetadata(k, v) {
4
4
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
5
5
  }
6
6
  //#endregion
7
- //#region \0@oxc-project+runtime@0.139.0/helpers/esm/decorateParam.js
7
+ //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateParam.js
8
8
  function __decorateParam(paramIndex, decorator) {
9
9
  return function(target, key) {
10
10
  decorator(target, key, paramIndex);
11
11
  };
12
12
  }
13
13
  //#endregion
14
- //#region \0@oxc-project+runtime@0.139.0/helpers/esm/decorate.js
14
+ //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorate.js
15
15
  function __decorate(decorators, target, key, desc) {
16
16
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
17
17
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -60,4 +60,4 @@ BetterAuthService = __decorate([
60
60
  //#endregion
61
61
  export { __decorateMetadata as a, __decorateParam as i, BetterAuthService as n, __decorate as r, BETTER_AUTH_BUILDER as t };
62
62
 
63
- //# sourceMappingURL=better-auth.service-BMkyFX-w.js.map
63
+ //# sourceMappingURL=better-auth.service-DurQ4JQf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"better-auth.service-DurQ4JQf.js","names":[],"sources":["../src/better-auth.service.ts"],"sourcesContent":["import { Inject, Injectable, InjectionToken } from '@velajs/vela';\nimport type { BetterAuthInstance } from './better-auth.types';\n\n/**\n * Internal token holding the auth-construction closure with its inject deps\n * closed over. Resolves cheaply at module load (just captures references);\n * the inner call happens lazily on first auth use (see `BetterAuthService`).\n *\n * Not exported from the public surface — only the service consumes it.\n */\nexport const BETTER_AUTH_BUILDER = new InjectionToken<() => BetterAuthInstance>(\n 'vela.better-auth.Builder',\n);\n\n/**\n * The single injectable consumers reach for to interact with better-auth.\n * Wraps the underlying `betterAuth({...})` instance with lazy construction:\n *\n * - `forRoot({ auth })` — the builder returns the eagerly-provided instance,\n * so the first `.auth` / `.api` / `.handler` access is effectively a\n * read-and-cache.\n * - `forRootAsync({ inject, useFactory })` — the builder wraps the user's\n * factory + inject deps. First access triggers `useFactory(...deps)`. This\n * is what makes Cloudflare D1/KV bindings work: at module load the factory\n * doesn't run; on first request (when AuthGuard or the catch-all calls\n * `service.api` / `service.handler`), the native environment is registered and the\n * factory can read them safely.\n *\n * Used directly by AuthGuard and the catch-all controller. Consumers in\n * application code inject the same way: `@Inject(BetterAuthService)`.\n */\n@Injectable()\nexport class BetterAuthService<TAuth extends BetterAuthInstance = BetterAuthInstance> {\n private cached: TAuth | undefined;\n\n constructor(@Inject(BETTER_AUTH_BUILDER) private readonly build: () => TAuth) {}\n\n /**\n * The underlying better-auth instance. Constructed once on first access.\n * Safe to call from any request-time code path (guards, controllers,\n * services invoked from handlers).\n */\n get auth(): TAuth {\n if (!this.cached) this.cached = this.build();\n return this.cached;\n }\n\n /** Convenience accessor — equivalent to `service.auth.api`. */\n get api(): TAuth['api'] {\n return this.auth.api;\n }\n\n /** Convenience accessor — equivalent to `service.auth.handler`. */\n get handler(): TAuth['handler'] {\n return this.auth.handler;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAUA,MAAa,sBAAsB,IAAI,eACrC,0BACF;AAoBO,IAAM,oBAAN,MAAM,kBAAyE;CAG1B;CAF1D;CAEA,YAAY,OAAkE;EAApB,KAAA,QAAA;CAAqB;;;;;;CAO/E,IAAI,OAAc;EAChB,IAAI,CAAC,KAAK,QAAQ,KAAK,SAAS,KAAK,MAAM;EAC3C,OAAO,KAAK;CACd;;CAGA,IAAI,MAAoB;EACtB,OAAO,KAAK,KAAK;CACnB;;CAGA,IAAI,UAA4B;EAC9B,OAAO,KAAK,KAAK;CACnB;AACF;;CAzBC,WAAW;CAIG,gBAAA,GAAA,OAAO,mBAAmB,CAAA"}
@@ -0,0 +1,68 @@
1
+ import { InjectionToken } from "@velajs/vela";
2
+ import { Auth, Session, User } from "better-auth";
3
+ //#region src/better-auth.types.d.ts
4
+ /** The runtime surface required by the integration; concrete instances retain their generics. */
5
+ interface BetterAuthInstance {
6
+ readonly api: {
7
+ getSession(input: {
8
+ headers: Headers;
9
+ }): Promise<unknown>;
10
+ };
11
+ readonly handler: (request: Request) => Promise<Response>;
12
+ /** Used only by the real-session testing helper. */
13
+ readonly $context?: Promise<Pick<Awaited<Auth['$context']>, 'internalAdapter' | 'authCookies' | 'secret'>>;
14
+ }
15
+ interface BetterAuthModuleOptions<TAuth extends BetterAuthInstance = BetterAuthInstance> extends BetterAuthRuntimeOptions {
16
+ auth: TAuth;
17
+ }
18
+ /** Provider configuration, separate from the lazily constructed auth instance. */
19
+ interface BetterAuthRuntimeOptions {
20
+ /** Stable namespace paired with user ids in authorization identities. */
21
+ issuer?: string;
22
+ basePath?: string;
23
+ /**
24
+ * Register AuthGuard application-wide. Defaults to `true`; opt out only when
25
+ * the application installs an equivalent global authentication guard itself.
26
+ */
27
+ isGlobal?: boolean;
28
+ mountHandler?: boolean;
29
+ }
30
+ type User$1 = User;
31
+ type Session$1 = Session;
32
+ //#endregion
33
+ //#region src/better-auth.service.d.ts
34
+ /**
35
+ * The single injectable consumers reach for to interact with better-auth.
36
+ * Wraps the underlying `betterAuth({...})` instance with lazy construction:
37
+ *
38
+ * - `forRoot({ auth })` — the builder returns the eagerly-provided instance,
39
+ * so the first `.auth` / `.api` / `.handler` access is effectively a
40
+ * read-and-cache.
41
+ * - `forRootAsync({ inject, useFactory })` — the builder wraps the user's
42
+ * factory + inject deps. First access triggers `useFactory(...deps)`. This
43
+ * is what makes Cloudflare D1/KV bindings work: at module load the factory
44
+ * doesn't run; on first request (when AuthGuard or the catch-all calls
45
+ * `service.api` / `service.handler`), the native environment is registered and the
46
+ * factory can read them safely.
47
+ *
48
+ * Used directly by AuthGuard and the catch-all controller. Consumers in
49
+ * application code inject the same way: `@Inject(BetterAuthService)`.
50
+ */
51
+ declare class BetterAuthService<TAuth extends BetterAuthInstance = BetterAuthInstance> {
52
+ private readonly build;
53
+ private cached;
54
+ constructor(build: () => TAuth);
55
+ /**
56
+ * The underlying better-auth instance. Constructed once on first access.
57
+ * Safe to call from any request-time code path (guards, controllers,
58
+ * services invoked from handlers).
59
+ */
60
+ get auth(): TAuth;
61
+ /** Convenience accessor — equivalent to `service.auth.api`. */
62
+ get api(): TAuth['api'];
63
+ /** Convenience accessor — equivalent to `service.auth.handler`. */
64
+ get handler(): TAuth['handler'];
65
+ }
66
+ //#endregion
67
+ export { Session$1 as a, BetterAuthRuntimeOptions as i, BetterAuthInstance as n, User$1 as o, BetterAuthModuleOptions as r, BetterAuthService as t };
68
+ //# sourceMappingURL=better-auth.service-NOlWXxd4.d.ts.map
package/dist/index.d.ts CHANGED
@@ -1,18 +1,6 @@
1
+ import { a as Session, i as BetterAuthRuntimeOptions, n as BetterAuthInstance, o as User, r as BetterAuthModuleOptions, t as BetterAuthService } from "./better-auth.service-NOlWXxd4.js";
1
2
  import { CanActivate, DynamicModule, ExecutionContext, InferTokens, InjectionToken, Token, Type } from "@velajs/vela";
2
- import { Auth, Session as Session$1, User as User$1 } from "better-auth";
3
3
  import { Identity, PermissionResolver } from "@velajs/authz";
4
- //#region src/better-auth.types.d.ts
5
- type BetterAuthInstance = Auth<any>;
6
- interface BetterAuthModuleOptions {
7
- auth: BetterAuthInstance;
8
- basePath?: string;
9
- isGlobal?: boolean;
10
- defaultPolicy?: 'deny' | 'allow';
11
- mountHandler?: boolean;
12
- }
13
- type User = User$1;
14
- type Session = Session$1;
15
- //#endregion
16
4
  //#region src/better-auth.module.d.ts
17
5
  /**
18
6
  * Options for {@link BetterAuthModule.forRootAsync}.
@@ -23,23 +11,23 @@ type Session = Session$1;
23
11
  *
24
12
  * ```ts
25
13
  * BetterAuthModule.forRootAsync({
26
- * inject: [D1Service, ConfigService], // captured as readonly tuple
27
- * useFactory: (d1, config) => // d1: D1Service, config: ConfigService
28
- * betterAuth({ database: drizzleAdapter(drizzle(d1.database), ...) }),
14
+ * inject: [WORKER_ENV, ConfigService], // captured as readonly tuple
15
+ * useFactory: (env, config) => // inferred from the tokens
16
+ * betterAuth({ database: drizzleAdapter(drizzle(env.DB), ...) }),
29
17
  * });
30
18
  * ```
31
19
  */
32
- interface ForRootAsyncOptions<Inject extends readonly Token<unknown>[] = readonly Token<unknown>[]> {
33
- inject?: Inject;
20
+ interface ForRootAsyncOptions<Inject extends readonly Token[] = readonly Token[]> {
21
+ inject: Inject;
34
22
  imports?: DynamicModule['imports'];
35
23
  useFactory: (...deps: InferTokens<Inject>) => BetterAuthInstance;
36
24
  isGlobal?: boolean;
37
25
  mountHandler?: boolean;
38
26
  basePath?: string;
39
- defaultPolicy?: 'deny' | 'allow';
27
+ issuer?: string;
40
28
  key?: string;
41
29
  }
42
- declare class BetterAuthModule {
30
+ export declare class BetterAuthModule {
43
31
  /**
44
32
  * Synchronous registration. The auth instance is constructed by the consumer
45
33
  * at module-load time and passed in directly. Use this when the inputs to
@@ -55,47 +43,11 @@ declare class BetterAuthModule {
55
43
  * first time anything reads `BetterAuthService.auth` (or `.api` / `.handler`).
56
44
  * In normal request handling that's `AuthGuard.canActivate` or the catch-all
57
45
  * controller's `.handle`. At module load the factory does NOT run — it's only
58
- * captured behind {@link lazyProvider}'s memoized thunk. This is what makes
59
- * Cloudflare bindings (D1, KV, R2) work: the binding isn't ready at boot, but
60
- * it IS by the time a request flows through and the guard / catch-all reads
61
- * the service. Inject deps resolve at module load (cheap BindingRef wrappers);
62
- * their *values* are read at first auth use, inside your factory body.
63
- */
64
- static forRootAsync<const Inject extends readonly Token<unknown>[] = readonly Token<unknown>[]>(options: ForRootAsyncOptions<Inject>): DynamicModule;
65
- }
66
- //#endregion
67
- //#region src/better-auth.service.d.ts
68
- /**
69
- * The single injectable consumers reach for to interact with better-auth.
70
- * Wraps the underlying `betterAuth({...})` instance with lazy construction:
71
- *
72
- * - `forRoot({ auth })` — the builder returns the eagerly-provided instance,
73
- * so the first `.auth` / `.api` / `.handler` access is effectively a
74
- * read-and-cache.
75
- * - `forRootAsync({ inject, useFactory })` — the builder wraps the user's
76
- * factory + inject deps. First access triggers `useFactory(...deps)`. This
77
- * is what makes Cloudflare D1/KV bindings work: at module load the factory
78
- * doesn't run; on first request (when AuthGuard or the catch-all calls
79
- * `service.api` / `service.handler`), the bindings are populated and the
80
- * factory can read them safely.
81
- *
82
- * Used directly by AuthGuard and the catch-all controller. Consumers in
83
- * application code inject the same way: `@Inject(BetterAuthService)`.
84
- */
85
- declare class BetterAuthService {
86
- private readonly build;
87
- private cached;
88
- constructor(build: () => BetterAuthInstance);
89
- /**
90
- * The underlying better-auth instance. Constructed once on first access.
91
- * Safe to call from any request-time code path (guards, controllers,
92
- * services invoked from handlers).
46
+ * captured in the checked builder provider. The Workers adapter supplies its
47
+ * native environment before DI; the lazily constructed auth instance belongs
48
+ * to that environment's application and never captures another app's bindings.
93
49
  */
94
- get auth(): BetterAuthInstance;
95
- /** Convenience accessor — equivalent to `service.auth.api`. */
96
- get api(): BetterAuthInstance['api'];
97
- /** Convenience accessor — equivalent to `service.auth.handler`. */
98
- get handler(): BetterAuthInstance['handler'];
50
+ static forRootAsync<const Inject extends readonly Token[] = readonly Token[]>(options: ForRootAsyncOptions<Inject>): DynamicModule;
99
51
  }
100
52
  //#endregion
101
53
  //#region src/better-auth.controller.d.ts
@@ -112,108 +64,33 @@ declare class BetterAuthService {
112
64
  *
113
65
  * Both default to `/api/auth`, so the no-prefix / no-config case just works.
114
66
  */
115
- declare function createBetterAuthCatchallController(basePath?: string): Type;
116
- /**
117
- * Default-path (`/api/auth`) catch-all controller. Retained for back-compat;
118
- * `BetterAuthModule` now mounts {@link createBetterAuthCatchallController} with
119
- * the configured `basePath`. Prefer the factory for a custom base path.
120
- */
121
- declare const BetterAuthCatchallController: Type;
67
+ export declare function createBetterAuthCatchallController(basePath?: string): Type;
122
68
  //#endregion
123
69
  //#region src/better-auth.tokens.d.ts
124
- declare const BETTER_AUTH_OPTIONS: InjectionToken<BetterAuthModuleOptions>;
125
- declare const AUTH_USER_KEY: unique symbol;
126
- declare const AUTH_SESSION_KEY: unique symbol;
70
+ export declare const BETTER_AUTH_OPTIONS: InjectionToken<BetterAuthRuntimeOptions>;
127
71
  //#endregion
128
72
  //#region src/guards/auth.guard.d.ts
129
- declare class AuthGuard implements CanActivate {
73
+ export declare class AuthGuard implements CanActivate {
130
74
  private readonly auth;
131
75
  private readonly opts;
132
76
  private readonly reflector;
133
- constructor(auth: BetterAuthService, opts: BetterAuthModuleOptions);
134
- canActivate(context: ExecutionContext): Promise<boolean>;
135
- }
136
- //#endregion
137
- //#region src/guards/roles.guard.d.ts
138
- declare class RolesGuard implements CanActivate {
139
- private readonly reflector;
140
- canActivate(context: ExecutionContext): boolean;
141
- }
142
- //#endregion
143
- //#region src/guards/permission.guard.d.ts
144
- /**
145
- * Enforces the `@RequirePermission(...)` metadata against the `@velajs/authz`
146
- * engine. For each required permission it calls `authz.can(identity, perm)`,
147
- * requiring **all** of them (AND semantics — contrast {@link RolesGuard}, which
148
- * 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
150
- * run *after* `AuthGuard` (e.g. `@UseGuards(AuthGuard, PermissionGuard)`).
151
- *
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.
159
- *
160
- * Fail-closed on every abnormal path — no branch grants access on missing
161
- * wiring or a missing caller:
162
- * - no required permissions → allow (nothing to enforce);
163
- * - `AUTHZ` unresolvable (`AuthzModule` not registered) → deny (`ForbiddenException`);
164
- * - no authenticated user in the request context → deny;
165
- * - any single required permission not granted → deny.
166
- *
167
- * The guard is stateless (no injected dependencies), so it is safe to register
168
- * as a plain provided guard.
169
- */
170
- declare class PermissionGuard implements CanActivate {
171
- private readonly reflector;
77
+ constructor(auth: BetterAuthService, opts: BetterAuthRuntimeOptions);
172
78
  canActivate(context: ExecutionContext): Promise<boolean>;
173
79
  }
174
80
  //#endregion
175
81
  //#region src/decorators/current-user.decorator.d.ts
176
- declare const CurrentUser: (data?: unknown, ...pipes: import("@velajs/vela").PipeType[]) => ParameterDecorator;
82
+ export declare const CurrentUser: (data?: unknown, ...pipes: import("@velajs/vela").PipeType[]) => ParameterDecorator;
177
83
  //#endregion
178
84
  //#region src/decorators/current-session.decorator.d.ts
179
- declare const CurrentSession: (data?: unknown, ...pipes: import("@velajs/vela").PipeType[]) => ParameterDecorator;
85
+ export declare const CurrentSession: (data?: unknown, ...pipes: import("@velajs/vela").PipeType[]) => ParameterDecorator;
180
86
  //#endregion
181
87
  //#region src/decorators/public.decorator.d.ts
182
- declare const Public: import("@velajs/vela").ReflectableDecorator<boolean>;
183
- declare const PUBLIC_KEY: string;
88
+ export declare const Public: import("@velajs/vela").ReflectableDecorator<boolean>;
89
+ export declare const PUBLIC_KEY: string;
184
90
  //#endregion
185
91
  //#region src/decorators/optional-auth.decorator.d.ts
186
- declare const OptionalAuth: import("@velajs/vela").ReflectableDecorator<boolean>;
187
- declare const OPTIONAL_AUTH_KEY: string;
188
- //#endregion
189
- //#region src/decorators/roles.decorator.d.ts
190
- declare const Roles: import("@velajs/vela").ReflectableDecorator<string[]>;
191
- declare const ROLES_KEY: string;
192
- //#endregion
193
- //#region src/decorators/require-permission.decorator.d.ts
194
- /**
195
- * Declares the `@velajs/authz` permission(s) required to reach a controller or
196
- * route handler. Read via `Reflector` in an authorization guard, then checked
197
- * against the caller's `Identity` with `authz.can(...)`.
198
- *
199
- * ```ts
200
- * @RequirePermission(['posts:write'])
201
- * @Post()
202
- * create() { ... }
203
- * ```
204
- *
205
- * The metadata is a plain `string[]` of permission strings in the granted-side
206
- * format `@velajs/authz` matches (`resource:action`, or wildcards like
207
- * `posts:*`). Handler-level metadata overrides class-level (standard
208
- * `Reflector.getAllAndOverride` precedence).
209
- *
210
- * Semantics are **require-ALL** (AND): every listed permission must be granted
211
- * for access — the `PermissionGuard` denies if any one is missing. This
212
- * contrasts with `@Roles`, which is **OR** (any one of the listed roles
213
- * suffices).
214
- */
215
- declare const RequirePermission: import("@velajs/vela").ReflectableDecorator<string[]>;
216
- declare const REQUIRE_PERMISSION_KEY: string;
92
+ export declare const OptionalAuth: import("@velajs/vela").ReflectableDecorator<boolean>;
93
+ export declare const OPTIONAL_AUTH_KEY: string;
217
94
  //#endregion
218
95
  //#region src/authz-bridge.d.ts
219
96
  /**
@@ -221,18 +98,22 @@ declare const REQUIRE_PERMISSION_KEY: string;
221
98
  * admin plugin. `role` may be a single role, a comma-separated list, or an
222
99
  * array — {@link identityFromUser} normalizes all three.
223
100
  */
224
- type AuthUser = User & {
101
+ type AuthUser = Pick<User, 'id'> & {
225
102
  role?: string | string[] | null;
226
103
  };
104
+ /** Stable issuer namespace used for better-auth session principals. */
105
+ export declare const BETTER_AUTH_ISSUER = "better-auth";
227
106
  /**
228
- * Adapts a better-auth user into a `@velajs/authz` {@link Identity}. Maps
229
- * `user.id` `userId` and the admin-plugin `role` field `roles`.
107
+ * Pure authorization projection; this does not authenticate or publish trusted state.
108
+ * Adapts an already verified better-auth user into a stable `@velajs/authz` {@link Identity}.
109
+ * The issuer scopes `user.id` as both `subject` and the compatibility `userId`;
110
+ * the admin-plugin `role` field supplies local roles.
230
111
  *
231
112
  * Fail-closed: a missing user (`null`/`undefined`, i.e. an unauthenticated
232
113
  * request) maps to the zero-privilege identity `{ roles: [] }`, so downstream
233
114
  * `can()` checks grant nothing.
234
115
  */
235
- declare const identityFromUser: (user: AuthUser | null | undefined) => Identity;
116
+ export declare const identityFromUser: (user: AuthUser | null | undefined, issuer?: string, principalType?: 'user' | 'service') => Identity;
236
117
  /**
237
118
  * The minimal slice of a better-auth access-control role consumed here. Both
238
119
  * `createAccessControl(...).newRole(...)` and the standalone `role(...)` return
@@ -248,7 +129,7 @@ interface BetterAuthAcRole {
248
129
  * permission strings — the granted-side format `@velajs/authz` matches
249
130
  * (wildcards included).
250
131
  */
251
- declare const permissionsFromAcRole: (role: BetterAuthAcRole) => string[];
132
+ export declare const permissionsFromAcRole: (role: BetterAuthAcRole) => string[];
252
133
  /**
253
134
  * Builds a fail-closed `@velajs/authz` {@link PermissionResolver} from a
254
135
  * better-auth access-control role table (`{ roleName: acRole }` — the same map
@@ -264,7 +145,7 @@ declare const permissionsFromAcRole: (role: BetterAuthAcRole) => string[];
264
145
  * await authz.can(identityFromUser(user), 'posts:write');
265
146
  * ```
266
147
  */
267
- declare const betterAuthAcResolver: (roles: Readonly<Record<string, BetterAuthAcRole>>) => PermissionResolver;
148
+ export declare const betterAuthAcResolver: (roles: Readonly<Record<string, BetterAuthAcRole>>) => PermissionResolver;
268
149
  //#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 };
150
+ export { type AuthUser, type BetterAuthAcRole, type BetterAuthInstance, type BetterAuthModuleOptions, type BetterAuthRuntimeOptions, BetterAuthService, type Session, type User };
270
151
  //# sourceMappingURL=index.d.ts.map