@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/dist/index.js CHANGED
@@ -1,9 +1,22 @@
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";
3
- import { AUTHZ } from "@velajs/authz/vela";
1
+ import { a as __decorateMetadata, i as __decorateParam, n as BetterAuthService, r as __decorate, t as BETTER_AUTH_BUILDER } from "./better-auth.service-DurQ4JQf.js";
2
+ import { All, Controller, Inject, Injectable, InjectionToken, Reflector, Req, UnauthorizedException, clearTrustedRequestIdentity, createParamDecorator, defineProvider, getTrustedRequestIdentity, provideGlobal, setTrustedRequestIdentity, stableHash } from "@velajs/vela";
3
+ import { getContextIdentity } from "@velajs/authz/vela";
4
4
  //#region src/decorators/public.decorator.ts
5
5
  const Public = Reflector.createDecorator({ key: "vela.auth.public" });
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,24 +52,121 @@ 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])
45
59
  ], BetterAuthCatchallController);
46
60
  return BetterAuthCatchallController;
47
61
  }
48
- /**
49
- * Default-path (`/api/auth`) catch-all controller. Retained for back-compat;
50
- * `BetterAuthModule` now mounts {@link createBetterAuthCatchallController} with
51
- * the configured `basePath`. Prefer the factory for a custom base path.
52
- */
53
- const BetterAuthCatchallController = createBetterAuthCatchallController();
54
62
  //#endregion
55
63
  //#region src/better-auth.tokens.ts
56
64
  const BETTER_AUTH_OPTIONS = new InjectionToken("vela.BetterAuthOptions");
57
- const AUTH_USER_KEY = Symbol.for("vela.better-auth.user");
58
- const AUTH_SESSION_KEY = Symbol.for("vela.better-auth.session");
65
+ //#endregion
66
+ //#region src/auth-request-state.ts
67
+ const sessions = /* @__PURE__ */ new WeakMap();
68
+ function beginAuthRequest(context) {
69
+ clearTrustedRequestIdentity(context.getRequest());
70
+ }
71
+ function authenticateRequest(context, data, issuer) {
72
+ const request = context.getRequest();
73
+ setTrustedRequestIdentity(request, {
74
+ principal: {
75
+ issuer,
76
+ subject: data.user.id,
77
+ principalType: "user"
78
+ },
79
+ expiresAtMs: data.session.expiresAt.getTime(),
80
+ roles: data.roles,
81
+ ...data.tenantId === void 0 ? {} : { tenantId: data.tenantId }
82
+ });
83
+ const identity = getTrustedRequestIdentity(request);
84
+ if (identity) sessions.set(identity, data);
85
+ }
86
+ function getAuthRequestState(context) {
87
+ if (context.getType() !== "http") return void 0;
88
+ const identity = getTrustedRequestIdentity(context.getRequest());
89
+ return identity && sessions.get(identity);
90
+ }
91
+ //#endregion
92
+ //#region src/session-data.ts
93
+ function own(value, key) {
94
+ if (typeof value !== "object" || value === null) return void 0;
95
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
96
+ return descriptor && "value" in descriptor ? descriptor.value : void 0;
97
+ }
98
+ function requiredString(value) {
99
+ return typeof value === "string" && value.length > 0;
100
+ }
101
+ function date(value) {
102
+ return value instanceof Date && Number.isFinite(value.getTime()) ? new Date(value) : void 0;
103
+ }
104
+ function optionalString(value) {
105
+ return value === void 0 || value === null || typeof value === "string";
106
+ }
107
+ /** Keep plugin data opaque; base model fields below are validated individually. */
108
+ function dataProperties(value) {
109
+ return Object.fromEntries(Object.keys(value).map((key) => [key, own(value, key)]));
110
+ }
111
+ /** Validate the full Better Auth base models, not merely ids masquerading as them. */
112
+ function validateSessionData(value) {
113
+ try {
114
+ const user = own(value, "user");
115
+ const session = own(value, "session");
116
+ if (typeof user !== "object" || user === null || typeof session !== "object" || session === null) return;
117
+ const id = own(user, "id");
118
+ const email = own(user, "email");
119
+ const name = own(user, "name");
120
+ const emailVerified = own(user, "emailVerified");
121
+ const image = own(user, "image");
122
+ const createdAt = date(own(user, "createdAt"));
123
+ const updatedAt = date(own(user, "updatedAt"));
124
+ const sessionId = own(session, "id");
125
+ const userId = own(session, "userId");
126
+ const token = own(session, "token");
127
+ const sessionCreatedAt = date(own(session, "createdAt"));
128
+ const sessionUpdatedAt = date(own(session, "updatedAt"));
129
+ const expiresAt = date(own(session, "expiresAt"));
130
+ const ipAddress = own(session, "ipAddress");
131
+ const userAgent = own(session, "userAgent");
132
+ const organization = own(session, "activeOrganizationId");
133
+ const role = own(user, "role");
134
+ let roles = [];
135
+ if (role !== void 0 && role !== null) {
136
+ if (typeof role === "string") roles = role.split(",").map((part) => part.trim()).filter(Boolean);
137
+ else if (Array.isArray(role) && role.every((entry) => typeof entry === "string")) roles = role.map((entry) => entry.trim()).filter(Boolean);
138
+ else return void 0;
139
+ }
140
+ if (!requiredString(id) || !requiredString(email) || typeof name !== "string" || typeof emailVerified !== "boolean" || !optionalString(image) || !createdAt || !updatedAt || !requiredString(sessionId) || userId !== id || !requiredString(token) || !sessionCreatedAt || !sessionUpdatedAt || !expiresAt || expiresAt.getTime() <= Date.now() || !optionalString(ipAddress) || !optionalString(userAgent) || organization !== void 0 && organization !== null && !requiredString(organization)) return void 0;
141
+ return {
142
+ user: Object.freeze({
143
+ ...dataProperties(user),
144
+ id,
145
+ email,
146
+ name,
147
+ emailVerified,
148
+ createdAt,
149
+ updatedAt,
150
+ image
151
+ }),
152
+ session: Object.freeze({
153
+ ...dataProperties(session),
154
+ id: sessionId,
155
+ userId: id,
156
+ token,
157
+ createdAt: sessionCreatedAt,
158
+ updatedAt: sessionUpdatedAt,
159
+ expiresAt,
160
+ ipAddress,
161
+ userAgent
162
+ }),
163
+ roles: Object.freeze(roles),
164
+ ...typeof organization === "string" ? { tenantId: organization } : {}
165
+ };
166
+ } catch {
167
+ return;
168
+ }
169
+ }
59
170
  //#endregion
60
171
  //#region src/decorators/optional-auth.decorator.ts
61
172
  const OptionalAuth = Reflector.createDecorator({ key: "vela.auth.optional" });
@@ -71,19 +182,18 @@ let AuthGuard = class AuthGuard {
71
182
  this.opts = opts;
72
183
  }
73
184
  async canActivate(context) {
185
+ if (context.getType() === "ws") {
186
+ if (getContextIdentity(context)) return true;
187
+ throw new UnauthorizedException("Authentication required");
188
+ }
189
+ beginAuthRequest(context);
74
190
  if (this.reflector.getAllAndOverride(Public, context)) return true;
75
- 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 });
191
+ const data = validateSessionData(await this.auth.api.getSession({ headers: context.getRequest().headers }));
80
192
  if (data) {
81
- const reqCtx = resolveRequestContext(context);
82
- reqCtx.set(AUTH_USER_KEY, data.user);
83
- reqCtx.set(AUTH_SESSION_KEY, data.session);
193
+ authenticateRequest(context, data, this.opts.issuer ?? "better-auth");
84
194
  return true;
85
195
  }
86
- if (this.opts.defaultPolicy === "allow" || this.reflector.getAllAndOverride(OptionalAuth, context)) return true;
196
+ if (this.reflector.getAllAndOverride(OptionalAuth, context)) return true;
87
197
  throw new UnauthorizedException("Authentication required");
88
198
  }
89
199
  };
@@ -93,209 +203,52 @@ AuthGuard = __decorate([
93
203
  __decorateParam(1, Inject(BETTER_AUTH_OPTIONS)),
94
204
  __decorateMetadata("design:paramtypes", [typeof BetterAuthService === "undefined" ? Object : BetterAuthService, Object])
95
205
  ], AuthGuard);
96
- function resolveRequestContext(context) {
97
- return context.getContext().get("container").resolve(REQUEST_CONTEXT);
98
- }
99
- //#endregion
100
- //#region src/decorators/roles.decorator.ts
101
- const Roles = Reflector.createDecorator({ key: "vela.auth.roles" });
102
- const ROLES_KEY = Roles.KEY;
103
- //#endregion
104
- //#region src/guards/roles.guard.ts
105
- let RolesGuard = class RolesGuard {
106
- reflector = new Reflector();
107
- canActivate(context) {
108
- const required = this.reflector.getAllAndOverride(Roles, context);
109
- 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`);
114
- return true;
115
- }
116
- };
117
- RolesGuard = __decorate([Injectable()], RolesGuard);
118
- function normalizeRoles$1(role) {
119
- if (!role) return [];
120
- if (Array.isArray(role)) return role;
121
- return role.split(",").map((r) => r.trim()).filter(Boolean);
122
- }
123
- //#endregion
124
- //#region src/authz-bridge.ts
125
- const normalizeRoles = (role) => {
126
- if (!role) return [];
127
- if (Array.isArray(role)) return role.filter(Boolean);
128
- return role.split(",").map((r) => r.trim()).filter(Boolean);
129
- };
130
- /**
131
- * Adapts a better-auth user into a `@velajs/authz` {@link Identity}. Maps
132
- * `user.id` → `userId` and the admin-plugin `role` field → `roles`.
133
- *
134
- * Fail-closed: a missing user (`null`/`undefined`, i.e. an unauthenticated
135
- * request) maps to the zero-privilege identity `{ roles: [] }`, so downstream
136
- * `can()` checks grant nothing.
137
- */
138
- const identityFromUser = (user) => {
139
- if (!user) return { roles: [] };
140
- return {
141
- userId: user.id,
142
- roles: normalizeRoles(user.role)
143
- };
144
- };
145
- /**
146
- * Flattens a better-auth AC role's `statements` into `resource:action`
147
- * permission strings — the granted-side format `@velajs/authz` matches
148
- * (wildcards included).
149
- */
150
- const permissionsFromAcRole = (role) => {
151
- const permissions = [];
152
- for (const [resource, actions] of Object.entries(role.statements ?? {})) for (const action of actions ?? []) permissions.push(`${resource}:${action}`);
153
- return permissions;
154
- };
155
- /**
156
- * Builds a fail-closed `@velajs/authz` {@link PermissionResolver} from a
157
- * better-auth access-control role table (`{ roleName: acRole }` — the same map
158
- * shape passed to better-auth's admin/organization plugins). An identity's
159
- * `roles` are unioned into their granted permission strings; unknown roles
160
- * contribute nothing.
161
- *
162
- * ```ts
163
- * const ac = createAccessControl({ posts: ['read', 'write'] });
164
- * const authz = createAuthz({
165
- * resolver: betterAuthAcResolver({ editor: ac.newRole({ posts: ['write'] }) }),
166
- * });
167
- * await authz.can(identityFromUser(user), 'posts:write');
168
- * ```
169
- */
170
- const betterAuthAcResolver = (roles) => {
171
- const grantsByRole = /* @__PURE__ */ new Map();
172
- for (const [name, role] of Object.entries(roles)) grantsByRole.set(name, permissionsFromAcRole(role));
173
- return { grants(identity) {
174
- const out = /* @__PURE__ */ new Set();
175
- for (const name of identity.roles ?? []) for (const permission of grantsByRole.get(name) ?? []) out.add(permission);
176
- return out;
177
- } };
178
- };
179
- //#endregion
180
- //#region src/decorators/require-permission.decorator.ts
181
- /**
182
- * Declares the `@velajs/authz` permission(s) required to reach a controller or
183
- * route handler. Read via `Reflector` in an authorization guard, then checked
184
- * against the caller's `Identity` with `authz.can(...)`.
185
- *
186
- * ```ts
187
- * @RequirePermission(['posts:write'])
188
- * @Post()
189
- * create() { ... }
190
- * ```
191
- *
192
- * The metadata is a plain `string[]` of permission strings in the granted-side
193
- * format `@velajs/authz` matches (`resource:action`, or wildcards like
194
- * `posts:*`). Handler-level metadata overrides class-level (standard
195
- * `Reflector.getAllAndOverride` precedence).
196
- *
197
- * Semantics are **require-ALL** (AND): every listed permission must be granted
198
- * for access — the `PermissionGuard` denies if any one is missing. This
199
- * contrasts with `@Roles`, which is **OR** (any one of the listed roles
200
- * suffices).
201
- */
202
- const RequirePermission = Reflector.createDecorator({ key: "vela.authz.permissions" });
203
- const REQUIRE_PERMISSION_KEY = RequirePermission.KEY;
204
- //#endregion
205
- //#region src/guards/permission.guard.ts
206
- const AUTHZ_TOKEN = AUTHZ;
207
- let PermissionGuard = class PermissionGuard {
208
- reflector = new Reflector();
209
- async canActivate(context) {
210
- const required = this.reflector.getAllAndOverride(RequirePermission, context);
211
- 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}`);
224
- return true;
225
- }
226
- };
227
- PermissionGuard = __decorate([Injectable()], PermissionGuard);
228
206
  //#endregion
229
207
  //#region src/better-auth.module.ts
230
- const DEFAULT_BASE_PATH = "/api/auth";
208
+ const referenceIds = /* @__PURE__ */ new WeakMap();
209
+ const explicitKeyClaims = /* @__PURE__ */ new Map();
210
+ let nextReferenceId = 1;
211
+ function referenceId(reference) {
212
+ const existing = referenceIds.get(reference);
213
+ if (existing !== void 0) return existing;
214
+ const id = nextReferenceId++;
215
+ referenceIds.set(reference, id);
216
+ return id;
217
+ }
218
+ function claimExplicitKey(key, kind, reference, shape) {
219
+ if (key.length === 0 || key !== key.trim()) throw new Error("@velajs/better-auth: an explicit module key must be a non-empty string");
220
+ const existing = explicitKeyClaims.get(key);
221
+ 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`);
222
+ explicitKeyClaims.set(key, {
223
+ kind,
224
+ reference,
225
+ shape
226
+ });
227
+ return `explicit:${key}:ref:${referenceId(reference)}`;
228
+ }
231
229
  function normalize(options) {
230
+ const basePath = normalizeBetterAuthBasePath(options.basePath);
231
+ const issuer = options.issuer ?? `better-auth:${basePath}`;
232
+ if (issuer.length === 0 || issuer !== issuer.trim()) throw new Error("@velajs/better-auth: issuer must be a non-empty stable namespace");
232
233
  return {
233
- basePath: options.basePath ?? DEFAULT_BASE_PATH,
234
- isGlobal: options.isGlobal ?? false,
235
- defaultPolicy: options.defaultPolicy ?? "deny",
234
+ basePath,
235
+ issuer,
236
+ isGlobal: options.isGlobal ?? true,
236
237
  mountHandler: options.mountHandler ?? true
237
238
  };
238
239
  }
239
240
  /** Providers, controllers, and exports shared by both entry points. */
240
241
  function commonContributions(n) {
241
242
  return {
242
- providers: [
243
- BetterAuthService,
244
- AuthGuard,
245
- RolesGuard,
246
- PermissionGuard
247
- ],
243
+ providers: [BetterAuthService, AuthGuard],
248
244
  controllers: n.mountHandler ? [createBetterAuthCatchallController(n.basePath)] : [],
249
245
  exports: [
250
246
  BetterAuthService,
251
247
  BETTER_AUTH_OPTIONS,
252
- AuthGuard,
253
- RolesGuard,
254
- PermissionGuard
248
+ AuthGuard
255
249
  ]
256
250
  };
257
251
  }
258
- /**
259
- * The blessed engine generates `forRoot`. `setup` runs once per instance at
260
- * call time: it re-provides {@link BETTER_AUTH_OPTIONS} with defaults applied,
261
- * derives the auth builder from those options, mounts the catch-all controller,
262
- * and — via the `global:` slot — registers the app-wide guard when `isGlobal`.
263
- *
264
- * `isGlobal` here means "apply AuthGuard app-wide", NOT "make this a global
265
- * module", so the default `isGlobal → global: true` extras transform is
266
- * replaced with identity; the flag reaches `setup` through the options bag.
267
- */
268
- const authModuleHost = defineModule({
269
- name: "BetterAuth",
270
- optionsToken: BETTER_AUTH_OPTIONS,
271
- transform: (definition) => definition,
272
- key: (options) => stableHash(normalize(options)),
273
- setup: ({ OPTIONS, options }) => {
274
- const n = normalize(options);
275
- const common = commonContributions(n);
276
- const auth = options.auth;
277
- return {
278
- providers: [
279
- {
280
- provide: OPTIONS,
281
- useValue: {
282
- ...n,
283
- auth
284
- }
285
- },
286
- lazyProvider({
287
- provide: BETTER_AUTH_BUILDER,
288
- inject: [OPTIONS],
289
- useFactory: (o) => o.auth
290
- }),
291
- ...common.providers
292
- ],
293
- controllers: common.controllers,
294
- exports: common.exports,
295
- global: n.isGlobal ? { guards: [AuthGuard] } : void 0
296
- };
297
- }
298
- });
299
252
  var BetterAuthModule = class BetterAuthModule {
300
253
  /**
301
254
  * Synchronous registration. The auth instance is constructed by the consumer
@@ -304,9 +257,21 @@ var BetterAuthModule = class BetterAuthModule {
304
257
  * connection, in-memory adapters, etc.).
305
258
  */
306
259
  static forRoot(options) {
260
+ const normalized = normalize(options);
261
+ const shape = stableHash(normalized);
262
+ const key = options.key === void 0 ? `${shape}:auth:${referenceId(options.auth)}` : claimExplicitKey(options.key, "auth", options.auth, shape);
263
+ const common = commonContributions(normalized);
307
264
  return {
308
- ...authModuleHost.ConfigurableModuleClass.forRoot(options),
309
- module: BetterAuthModule
265
+ module: BetterAuthModule,
266
+ key,
267
+ providers: [
268
+ defineProvider(BETTER_AUTH_OPTIONS, { useValue: normalized }),
269
+ defineProvider(BETTER_AUTH_BUILDER, { useValue: () => options.auth }),
270
+ ...common.providers,
271
+ ...normalized.isGlobal ? provideGlobal("guard", AuthGuard) : []
272
+ ],
273
+ controllers: common.controllers,
274
+ exports: common.exports
310
275
  };
311
276
  }
312
277
  /**
@@ -314,31 +279,27 @@ var BetterAuthModule = class BetterAuthModule {
314
279
  * first time anything reads `BetterAuthService.auth` (or `.api` / `.handler`).
315
280
  * In normal request handling that's `AuthGuard.canActivate` or the catch-all
316
281
  * controller's `.handle`. At module load the factory does NOT run — it's only
317
- * captured behind {@link lazyProvider}'s memoized thunk. This is what makes
318
- * Cloudflare bindings (D1, KV, R2) work: the binding isn't ready at boot, but
319
- * it IS by the time a request flows through and the guard / catch-all reads
320
- * the service. Inject deps resolve at module load (cheap BindingRef wrappers);
321
- * their *values* are read at first auth use, inside your factory body.
282
+ * captured in the checked builder provider. The Workers adapter supplies its
283
+ * native environment before DI; the lazily constructed auth instance belongs
284
+ * to that environment's application and never captures another app's bindings.
322
285
  */
323
286
  static forRootAsync(options) {
324
287
  const n = normalize(options);
325
288
  const common = commonContributions(n);
289
+ const shape = stableHash({
290
+ ...n,
291
+ inject: options.inject
292
+ });
293
+ const key = options.key === void 0 ? `${shape}:factory:${referenceId(options.useFactory)}` : claimExplicitKey(options.key, "factory", options.useFactory, shape);
326
294
  return {
327
295
  module: BetterAuthModule,
328
- key: options.key ?? stableHash({
329
- ...n,
330
- inject: options.inject
331
- }),
296
+ key,
332
297
  imports: options.imports ?? [],
333
298
  providers: [
334
- {
335
- provide: BETTER_AUTH_OPTIONS,
336
- useValue: n
337
- },
338
- lazyProvider({
339
- provide: BETTER_AUTH_BUILDER,
299
+ defineProvider(BETTER_AUTH_OPTIONS, { useValue: n }),
300
+ defineProvider(BETTER_AUTH_BUILDER, {
340
301
  inject: options.inject,
341
- useFactory: options.useFactory
302
+ useFactory: (...deps) => () => options.useFactory(...deps)
342
303
  }),
343
304
  ...common.providers,
344
305
  ...n.isGlobal ? provideGlobal("guard", AuthGuard) : []
@@ -350,15 +311,79 @@ var BetterAuthModule = class BetterAuthModule {
350
311
  };
351
312
  //#endregion
352
313
  //#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);
314
+ const CurrentUser = createParamDecorator((_data, ctx) => {
315
+ return getAuthRequestState(ctx)?.user;
355
316
  });
356
317
  //#endregion
357
318
  //#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);
319
+ const CurrentSession = createParamDecorator((_data, ctx) => {
320
+ return getAuthRequestState(ctx)?.session;
360
321
  });
361
322
  //#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 };
323
+ //#region src/authz-bridge.ts
324
+ /** Stable issuer namespace used for better-auth session principals. */
325
+ const BETTER_AUTH_ISSUER = "better-auth";
326
+ const normalizeRoles = (role) => {
327
+ if (!role) return [];
328
+ if (Array.isArray(role)) return role.filter(Boolean);
329
+ return role.split(",").map((r) => r.trim()).filter(Boolean);
330
+ };
331
+ /**
332
+ * Pure authorization projection; this does not authenticate or publish trusted state.
333
+ * Adapts an already verified better-auth user into a stable `@velajs/authz` {@link Identity}.
334
+ * The issuer scopes `user.id` as both `subject` and the compatibility `userId`;
335
+ * the admin-plugin `role` field supplies local roles.
336
+ *
337
+ * Fail-closed: a missing user (`null`/`undefined`, i.e. an unauthenticated
338
+ * request) maps to the zero-privilege identity `{ roles: [] }`, so downstream
339
+ * `can()` checks grant nothing.
340
+ */
341
+ const identityFromUser = (user, issuer = BETTER_AUTH_ISSUER, principalType = "user") => {
342
+ if (!user || typeof user.id !== "string" || user.id.length === 0) return { roles: [] };
343
+ if (issuer.length === 0) throw new Error("@velajs/better-auth: identity issuer must be non-empty");
344
+ return {
345
+ issuer,
346
+ subject: user.id,
347
+ principalType,
348
+ userId: user.id,
349
+ roles: normalizeRoles(user.role)
350
+ };
351
+ };
352
+ /**
353
+ * Flattens a better-auth AC role's `statements` into `resource:action`
354
+ * permission strings — the granted-side format `@velajs/authz` matches
355
+ * (wildcards included).
356
+ */
357
+ const permissionsFromAcRole = (role) => {
358
+ const permissions = [];
359
+ for (const [resource, actions] of Object.entries(role.statements ?? {})) for (const action of actions ?? []) permissions.push(`${resource}:${action}`);
360
+ return permissions;
361
+ };
362
+ /**
363
+ * Builds a fail-closed `@velajs/authz` {@link PermissionResolver} from a
364
+ * better-auth access-control role table (`{ roleName: acRole }` — the same map
365
+ * shape passed to better-auth's admin/organization plugins). An identity's
366
+ * `roles` are unioned into their granted permission strings; unknown roles
367
+ * contribute nothing.
368
+ *
369
+ * ```ts
370
+ * const ac = createAccessControl({ posts: ['read', 'write'] });
371
+ * const authz = createAuthz({
372
+ * resolver: betterAuthAcResolver({ editor: ac.newRole({ posts: ['write'] }) }),
373
+ * });
374
+ * await authz.can(identityFromUser(user), 'posts:write');
375
+ * ```
376
+ */
377
+ const betterAuthAcResolver = (roles) => {
378
+ const grantsByRole = /* @__PURE__ */ new Map();
379
+ for (const [name, role] of Object.entries(roles)) grantsByRole.set(name, permissionsFromAcRole(role));
380
+ return { grants(identity) {
381
+ const out = /* @__PURE__ */ new Set();
382
+ for (const name of identity.roles ?? []) for (const permission of grantsByRole.get(name) ?? []) out.add(permission);
383
+ return out;
384
+ } };
385
+ };
386
+ //#endregion
387
+ export { AuthGuard, BETTER_AUTH_ISSUER, BETTER_AUTH_OPTIONS, BetterAuthModule, BetterAuthService, CurrentSession, CurrentUser, OPTIONAL_AUTH_KEY, OptionalAuth, PUBLIC_KEY, Public, betterAuthAcResolver, createBetterAuthCatchallController, identityFromUser, permissionsFromAcRole };
363
388
 
364
389
  //# sourceMappingURL=index.js.map