@velajs/better-auth 1.0.0 → 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,6 +1,6 @@
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, clearTrustedRequestIdentity, createParamDecorator, defineModule, lazyProvider, provideGlobal, setTrustedRequestIdentity, 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;
@@ -59,38 +59,114 @@ function createBetterAuthCatchallController(basePath = "/api/auth") {
59
59
  ], BetterAuthCatchallController);
60
60
  return BetterAuthCatchallController;
61
61
  }
62
- /**
63
- * Default-path (`/api/auth`) catch-all controller. Retained for back-compat;
64
- * `BetterAuthModule` now mounts {@link createBetterAuthCatchallController} with
65
- * the configured `basePath`. Prefer the factory for a custom base path.
66
- */
67
- const BetterAuthCatchallController = createBetterAuthCatchallController();
68
62
  //#endregion
69
63
  //#region src/better-auth.tokens.ts
70
64
  const BETTER_AUTH_OPTIONS = new InjectionToken("vela.BetterAuthOptions");
71
- const AUTH_USER_KEY = Symbol.for("vela.better-auth.user");
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
65
  //#endregion
76
66
  //#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
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 }
88
82
  });
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;
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
+ }
94
170
  //#endregion
95
171
  //#region src/decorators/optional-auth.decorator.ts
96
172
  const OptionalAuth = Reflector.createDecorator({ key: "vela.auth.optional" });
@@ -107,25 +183,18 @@ let AuthGuard = class AuthGuard {
107
183
  }
108
184
  async canActivate(context) {
109
185
  if (context.getType() === "ws") {
110
- if (hasValidWebSocketIdentity(context)) return true;
111
- throw new AuthenticationRequiredException();
186
+ if (getContextIdentity(context)) return true;
187
+ throw new UnauthorizedException("Authentication required");
112
188
  }
113
189
  beginAuthRequest(context);
114
- mirrorRequestContext(context, { authenticated: false });
115
190
  if (this.reflector.getAllAndOverride(Public, context)) return true;
116
- const request = context.getRequest();
117
- const data = validateSessionData(await this.auth.api.getSession({ headers: request.headers }));
191
+ const data = validateSessionData(await this.auth.api.getSession({ headers: context.getRequest().headers }));
118
192
  if (data) {
119
- mirrorRequestContext(context, authenticateRequest(context, {
120
- user: data.user,
121
- session: data.session,
122
- issuer: this.opts.issuer ?? "better-auth",
123
- principalType: "user"
124
- }));
193
+ authenticateRequest(context, data, this.opts.issuer ?? "better-auth");
125
194
  return true;
126
195
  }
127
196
  if (this.reflector.getAllAndOverride(OptionalAuth, context)) return true;
128
- throw new AuthenticationRequiredException();
197
+ throw new UnauthorizedException("Authentication required");
129
198
  }
130
199
  };
131
200
  AuthGuard = __decorate([
@@ -134,280 +203,6 @@ AuthGuard = __decorate([
134
203
  __decorateParam(1, Inject(BETTER_AUTH_OPTIONS)),
135
204
  __decorateMetadata("design:paramtypes", [typeof BetterAuthService === "undefined" ? Object : BetterAuthService, Object])
136
205
  ], AuthGuard);
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
- }
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
- };
235
- //#endregion
236
- //#region src/decorators/roles.decorator.ts
237
- const Roles = Reflector.createDecorator({ key: "vela.auth.roles" });
238
- const ROLES_KEY = Roles.KEY;
239
- //#endregion
240
- //#region src/guards/roles.guard.ts
241
- const ACCESS_DENIED$1 = "Access denied";
242
- let RolesGuard = class RolesGuard {
243
- reflector = new Reflector();
244
- canActivate(context) {
245
- const required = this.reflector.getAllAndOverride(Roles, context);
246
- if (!required || required.length === 0) return true;
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);
251
- return true;
252
- }
253
- };
254
- RolesGuard = __decorate([Injectable()], RolesGuard);
255
- function normalizeRoles$1(role) {
256
- if (!role) return [];
257
- if (Array.isArray(role)) return role;
258
- return role.split(",").map((r) => r.trim()).filter(Boolean);
259
- }
260
- //#endregion
261
- //#region src/authz-bridge.ts
262
- /** Stable issuer namespace used for better-auth session principals. */
263
- const BETTER_AUTH_ISSUER = "better-auth";
264
- const normalizeRoles = (role) => {
265
- if (!role) return [];
266
- if (Array.isArray(role)) return role.filter(Boolean);
267
- return role.split(",").map((r) => r.trim()).filter(Boolean);
268
- };
269
- /**
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.
273
- *
274
- * Fail-closed: a missing user (`null`/`undefined`, i.e. an unauthenticated
275
- * request) maps to the zero-privilege identity `{ roles: [] }`, so downstream
276
- * `can()` checks grant nothing.
277
- */
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");
281
- return {
282
- issuer,
283
- subject: user.id,
284
- principalType,
285
- userId: user.id,
286
- roles: normalizeRoles(user.role)
287
- };
288
- };
289
- /**
290
- * Flattens a better-auth AC role's `statements` into `resource:action`
291
- * permission strings — the granted-side format `@velajs/authz` matches
292
- * (wildcards included).
293
- */
294
- const permissionsFromAcRole = (role) => {
295
- const permissions = [];
296
- for (const [resource, actions] of Object.entries(role.statements ?? {})) for (const action of actions ?? []) permissions.push(`${resource}:${action}`);
297
- return permissions;
298
- };
299
- /**
300
- * Builds a fail-closed `@velajs/authz` {@link PermissionResolver} from a
301
- * better-auth access-control role table (`{ roleName: acRole }` — the same map
302
- * shape passed to better-auth's admin/organization plugins). An identity's
303
- * `roles` are unioned into their granted permission strings; unknown roles
304
- * contribute nothing.
305
- *
306
- * ```ts
307
- * const ac = createAccessControl({ posts: ['read', 'write'] });
308
- * const authz = createAuthz({
309
- * resolver: betterAuthAcResolver({ editor: ac.newRole({ posts: ['write'] }) }),
310
- * });
311
- * await authz.can(identityFromUser(user), 'posts:write');
312
- * ```
313
- */
314
- const betterAuthAcResolver = (roles) => {
315
- const grantsByRole = /* @__PURE__ */ new Map();
316
- for (const [name, role] of Object.entries(roles)) grantsByRole.set(name, permissionsFromAcRole(role));
317
- return { grants(identity) {
318
- const out = /* @__PURE__ */ new Set();
319
- for (const name of identity.roles ?? []) for (const permission of grantsByRole.get(name) ?? []) out.add(permission);
320
- return out;
321
- } };
322
- };
323
- //#endregion
324
- //#region src/decorators/require-permission.decorator.ts
325
- /**
326
- * Declares the `@velajs/authz` permission(s) required to reach a controller or
327
- * route handler. Read via `Reflector` in an authorization guard, then checked
328
- * against the caller's `Identity` with `authz.can(...)`.
329
- *
330
- * ```ts
331
- * @RequirePermission(['posts:write'])
332
- * @Post()
333
- * create() { ... }
334
- * ```
335
- *
336
- * The metadata is a plain `string[]` of permission strings in the granted-side
337
- * format `@velajs/authz` matches (`resource:action`, or wildcards like
338
- * `posts:*`). Handler-level metadata overrides class-level (standard
339
- * `Reflector.getAllAndOverride` precedence).
340
- *
341
- * Semantics are **require-ALL** (AND): every listed permission must be granted
342
- * for access — the `PermissionGuard` denies if any one is missing. This
343
- * contrasts with `@Roles`, which is **OR** (any one of the listed roles
344
- * suffices).
345
- */
346
- const RequirePermission = Reflector.createDecorator({ key: "vela.authz.permissions" });
347
- const REQUIRE_PERMISSION_KEY = RequirePermission.KEY;
348
- //#endregion
349
- //#region src/guards/permission.guard.ts
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
- }
363
- let PermissionGuard = class PermissionGuard {
364
- reflector = new Reflector();
365
- async canActivate(context) {
366
- const required = this.reflector.getAllAndOverride(RequirePermission, context);
367
- if (!required || required.length === 0) return true;
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);
375
- return true;
376
- }
377
- };
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
- }
411
206
  //#endregion
412
207
  //#region src/better-auth.module.ts
413
208
  const referenceIds = /* @__PURE__ */ new WeakMap();
@@ -435,75 +230,25 @@ function normalize(options) {
435
230
  const basePath = normalizeBetterAuthBasePath(options.basePath);
436
231
  const issuer = options.issuer ?? `better-auth:${basePath}`;
437
232
  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()");
439
233
  return {
440
234
  basePath,
441
235
  issuer,
442
236
  isGlobal: options.isGlobal ?? true,
443
- defaultPolicy: "deny",
444
237
  mountHandler: options.mountHandler ?? true
445
238
  };
446
239
  }
447
240
  /** Providers, controllers, and exports shared by both entry points. */
448
241
  function commonContributions(n) {
449
242
  return {
450
- providers: [
451
- BetterAuthService,
452
- AuthGuard,
453
- RolesGuard,
454
- PermissionGuard
455
- ],
243
+ providers: [BetterAuthService, AuthGuard],
456
244
  controllers: n.mountHandler ? [createBetterAuthCatchallController(n.basePath)] : [],
457
245
  exports: [
458
246
  BetterAuthService,
459
247
  BETTER_AUTH_OPTIONS,
460
- AuthGuard,
461
- RolesGuard,
462
- PermissionGuard
248
+ AuthGuard
463
249
  ]
464
250
  };
465
251
  }
466
- /**
467
- * The blessed engine generates `forRoot`. `setup` runs once per instance at
468
- * call time: it re-provides {@link BETTER_AUTH_OPTIONS} with defaults applied,
469
- * derives the auth builder from those options, mounts the catch-all controller,
470
- * and — via the `global:` slot — registers the app-wide guard when `isGlobal`.
471
- *
472
- * `isGlobal` here means "apply AuthGuard app-wide", NOT "make this a global
473
- * module", so the default `isGlobal → global: true` extras transform is
474
- * replaced with identity; the flag reaches `setup` through the options bag.
475
- */
476
- const authModuleHost = defineModule({
477
- name: "BetterAuth",
478
- optionsToken: BETTER_AUTH_OPTIONS,
479
- transform: (definition) => definition,
480
- key: (options) => stableHash(normalize(options)),
481
- setup: ({ OPTIONS, options }) => {
482
- const n = normalize(options);
483
- const common = commonContributions(n);
484
- const auth = options.auth;
485
- return {
486
- providers: [
487
- {
488
- provide: OPTIONS,
489
- useValue: {
490
- ...n,
491
- auth
492
- }
493
- },
494
- lazyProvider({
495
- provide: BETTER_AUTH_BUILDER,
496
- inject: [OPTIONS],
497
- useFactory: (o) => o.auth
498
- }),
499
- ...common.providers
500
- ],
501
- controllers: common.controllers,
502
- exports: common.exports,
503
- global: n.isGlobal ? { guards: [AuthGuard] } : void 0
504
- };
505
- }
506
- });
507
252
  var BetterAuthModule = class BetterAuthModule {
508
253
  /**
509
254
  * Synchronous registration. The auth instance is constructed by the consumer
@@ -512,14 +257,21 @@ var BetterAuthModule = class BetterAuthModule {
512
257
  * connection, in-memory adapters, etc.).
513
258
  */
514
259
  static forRoot(options) {
515
- const shape = stableHash(normalize(options));
260
+ const normalized = normalize(options);
261
+ const shape = stableHash(normalized);
516
262
  const key = options.key === void 0 ? `${shape}:auth:${referenceId(options.auth)}` : claimExplicitKey(options.key, "auth", options.auth, shape);
263
+ const common = commonContributions(normalized);
517
264
  return {
518
- ...authModuleHost.ConfigurableModuleClass.forRoot({
519
- ...options,
520
- key
521
- }),
522
- 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
523
275
  };
524
276
  }
525
277
  /**
@@ -527,11 +279,9 @@ var BetterAuthModule = class BetterAuthModule {
527
279
  * first time anything reads `BetterAuthService.auth` (or `.api` / `.handler`).
528
280
  * In normal request handling that's `AuthGuard.canActivate` or the catch-all
529
281
  * controller's `.handle`. At module load the factory does NOT run — it's only
530
- * captured behind {@link lazyProvider}'s memoized thunk. This is what makes
531
- * Cloudflare bindings (D1, KV, R2) work: the binding isn't ready at boot, but
532
- * it IS by the time a request flows through and the guard / catch-all reads
533
- * the service. Inject deps resolve at module load (cheap BindingRef wrappers);
534
- * 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.
535
285
  */
536
286
  static forRootAsync(options) {
537
287
  const n = normalize(options);
@@ -546,14 +296,10 @@ var BetterAuthModule = class BetterAuthModule {
546
296
  key,
547
297
  imports: options.imports ?? [],
548
298
  providers: [
549
- {
550
- provide: BETTER_AUTH_OPTIONS,
551
- useValue: n
552
- },
553
- lazyProvider({
554
- provide: BETTER_AUTH_BUILDER,
299
+ defineProvider(BETTER_AUTH_OPTIONS, { useValue: n }),
300
+ defineProvider(BETTER_AUTH_BUILDER, {
555
301
  inject: options.inject,
556
- useFactory: options.useFactory
302
+ useFactory: (...deps) => () => options.useFactory(...deps)
557
303
  }),
558
304
  ...common.providers,
559
305
  ...n.isGlobal ? provideGlobal("guard", AuthGuard) : []
@@ -566,16 +312,78 @@ var BetterAuthModule = class BetterAuthModule {
566
312
  //#endregion
567
313
  //#region src/decorators/current-user.decorator.ts
568
314
  const CurrentUser = createParamDecorator((_data, ctx) => {
569
- const state = getAuthRequestState(ctx);
570
- return state.authenticated ? state.user : void 0;
315
+ return getAuthRequestState(ctx)?.user;
571
316
  });
572
317
  //#endregion
573
318
  //#region src/decorators/current-session.decorator.ts
574
319
  const CurrentSession = createParamDecorator((_data, ctx) => {
575
- const state = getAuthRequestState(ctx);
576
- return state.authenticated ? state.session : void 0;
320
+ return getAuthRequestState(ctx)?.session;
577
321
  });
578
322
  //#endregion
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 };
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 };
580
388
 
581
389
  //# sourceMappingURL=index.js.map