@velajs/better-auth 0.5.0 → 0.6.1

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +6 -1
  2. package/dist/better-auth.service-BMkyFX-w.js +63 -0
  3. package/dist/better-auth.service-BMkyFX-w.js.map +1 -0
  4. package/dist/index.d.ts +270 -12
  5. package/dist/index.js +364 -15
  6. package/dist/index.js.map +1 -0
  7. package/dist/testing/index.d.ts +50 -0
  8. package/dist/testing/index.js +60 -0
  9. package/dist/testing/index.js.map +1 -0
  10. package/package.json +60 -39
  11. package/dist/better-auth.controller.d.ts +0 -21
  12. package/dist/better-auth.controller.js +0 -68
  13. package/dist/better-auth.module.d.ts +0 -52
  14. package/dist/better-auth.module.js +0 -138
  15. package/dist/better-auth.service.d.ts +0 -42
  16. package/dist/better-auth.service.js +0 -51
  17. package/dist/better-auth.tokens.d.ts +0 -5
  18. package/dist/better-auth.tokens.js +0 -4
  19. package/dist/better-auth.types.d.ts +0 -11
  20. package/dist/better-auth.types.js +0 -1
  21. package/dist/decorators/current-session.decorator.d.ts +0 -1
  22. package/dist/decorators/current-session.decorator.js +0 -7
  23. package/dist/decorators/current-user.decorator.d.ts +0 -1
  24. package/dist/decorators/current-user.decorator.js +0 -7
  25. package/dist/decorators/optional-auth.decorator.d.ts +0 -2
  26. package/dist/decorators/optional-auth.decorator.js +0 -5
  27. package/dist/decorators/public.decorator.d.ts +0 -2
  28. package/dist/decorators/public.decorator.js +0 -5
  29. package/dist/decorators/roles.decorator.d.ts +0 -2
  30. package/dist/decorators/roles.decorator.js +0 -5
  31. package/dist/guards/auth.guard.d.ts +0 -10
  32. package/dist/guards/auth.guard.js +0 -68
  33. package/dist/guards/roles.guard.d.ts +0 -5
  34. package/dist/guards/roles.guard.js +0 -36
package/dist/index.js CHANGED
@@ -1,15 +1,364 @@
1
- // Module
2
- export { BetterAuthModule } from "./better-auth.module.js";
3
- export { BetterAuthService } from "./better-auth.service.js";
4
- export { BetterAuthCatchallController, createBetterAuthCatchallController } from "./better-auth.controller.js";
5
- // Tokens & symbols
6
- export { BETTER_AUTH_OPTIONS, AUTH_USER_KEY, AUTH_SESSION_KEY } from "./better-auth.tokens.js";
7
- // Guards
8
- export { AuthGuard } from "./guards/auth.guard.js";
9
- export { RolesGuard } from "./guards/roles.guard.js";
10
- // Decorators
11
- export { CurrentUser } from "./decorators/current-user.decorator.js";
12
- export { CurrentSession } from "./decorators/current-session.decorator.js";
13
- export { Public, PUBLIC_KEY } from "./decorators/public.decorator.js";
14
- export { OptionalAuth, OPTIONAL_AUTH_KEY } from "./decorators/optional-auth.decorator.js";
15
- export { Roles, ROLES_KEY } from "./decorators/roles.decorator.js";
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";
4
+ //#region src/decorators/public.decorator.ts
5
+ const Public = Reflector.createDecorator({ key: "vela.auth.public" });
6
+ const PUBLIC_KEY = Public.KEY;
7
+ //#endregion
8
+ //#region src/better-auth.controller.ts
9
+ /**
10
+ * Build a catch-all controller that mounts better-auth's handler at `basePath`
11
+ * (default `/api/auth`). This is a factory because vela reads a controller's
12
+ * route off the class at decoration time, so a custom base path needs its own
13
+ * decorated class the path can't be parametrized on a single shared class.
14
+ *
15
+ * Two base paths to keep consistent:
16
+ * - this `basePath` is RELATIVE to vela's `globalPrefix` (always prepended);
17
+ * - better-auth routes against its OWN absolute `basePath` (the one you pass to
18
+ * `betterAuth({ basePath })`), which must equal `globalPrefix + basePath`.
19
+ *
20
+ * Both default to `/api/auth`, so the no-prefix / no-config case just works.
21
+ */
22
+ function createBetterAuthCatchallController(basePath = "/api/auth") {
23
+ let BetterAuthCatchallController = class BetterAuthCatchallController {
24
+ auth;
25
+ constructor(auth) {
26
+ this.auth = auth;
27
+ }
28
+ async handle(c) {
29
+ return this.auth.handler(c.req.raw);
30
+ }
31
+ };
32
+ __decorate([
33
+ All("/*"),
34
+ __decorateParam(0, Req()),
35
+ __decorateMetadata("design:type", Function),
36
+ __decorateMetadata("design:paramtypes", [Object]),
37
+ __decorateMetadata("design:returntype", Promise)
38
+ ], BetterAuthCatchallController.prototype, "handle", null);
39
+ BetterAuthCatchallController = __decorate([
40
+ Public(true),
41
+ Controller(basePath),
42
+ Injectable(),
43
+ __decorateParam(0, Inject(BetterAuthService)),
44
+ __decorateMetadata("design:paramtypes", [typeof BetterAuthService === "undefined" ? Object : BetterAuthService])
45
+ ], BetterAuthCatchallController);
46
+ return BetterAuthCatchallController;
47
+ }
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
+ //#endregion
55
+ //#region src/better-auth.tokens.ts
56
+ 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");
59
+ //#endregion
60
+ //#region src/decorators/optional-auth.decorator.ts
61
+ const OptionalAuth = Reflector.createDecorator({ key: "vela.auth.optional" });
62
+ const OPTIONAL_AUTH_KEY = OptionalAuth.KEY;
63
+ //#endregion
64
+ //#region src/guards/auth.guard.ts
65
+ let AuthGuard = class AuthGuard {
66
+ auth;
67
+ opts;
68
+ reflector = new Reflector();
69
+ constructor(auth, opts) {
70
+ this.auth = auth;
71
+ this.opts = opts;
72
+ }
73
+ async canActivate(context) {
74
+ 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 });
80
+ if (data) {
81
+ const reqCtx = resolveRequestContext(context);
82
+ reqCtx.set(AUTH_USER_KEY, data.user);
83
+ reqCtx.set(AUTH_SESSION_KEY, data.session);
84
+ return true;
85
+ }
86
+ if (this.opts.defaultPolicy === "allow" || this.reflector.getAllAndOverride(OptionalAuth, context)) return true;
87
+ throw new UnauthorizedException("Authentication required");
88
+ }
89
+ };
90
+ AuthGuard = __decorate([
91
+ Injectable(),
92
+ __decorateParam(0, Inject(BetterAuthService)),
93
+ __decorateParam(1, Inject(BETTER_AUTH_OPTIONS)),
94
+ __decorateMetadata("design:paramtypes", [typeof BetterAuthService === "undefined" ? Object : BetterAuthService, Object])
95
+ ], 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
+ //#endregion
229
+ //#region src/better-auth.module.ts
230
+ const DEFAULT_BASE_PATH = "/api/auth";
231
+ function normalize(options) {
232
+ return {
233
+ basePath: options.basePath ?? DEFAULT_BASE_PATH,
234
+ isGlobal: options.isGlobal ?? false,
235
+ defaultPolicy: options.defaultPolicy ?? "deny",
236
+ mountHandler: options.mountHandler ?? true
237
+ };
238
+ }
239
+ /** Providers, controllers, and exports shared by both entry points. */
240
+ function commonContributions(n) {
241
+ return {
242
+ providers: [
243
+ BetterAuthService,
244
+ AuthGuard,
245
+ RolesGuard,
246
+ PermissionGuard
247
+ ],
248
+ controllers: n.mountHandler ? [createBetterAuthCatchallController(n.basePath)] : [],
249
+ exports: [
250
+ BetterAuthService,
251
+ BETTER_AUTH_OPTIONS,
252
+ AuthGuard,
253
+ RolesGuard,
254
+ PermissionGuard
255
+ ]
256
+ };
257
+ }
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
+ var BetterAuthModule = class BetterAuthModule {
300
+ /**
301
+ * Synchronous registration. The auth instance is constructed by the consumer
302
+ * at module-load time and passed in directly. Use this when the inputs to
303
+ * `betterAuth({...})` are available at startup (Node apps with a static DB
304
+ * connection, in-memory adapters, etc.).
305
+ */
306
+ static forRoot(options) {
307
+ return {
308
+ ...authModuleHost.ConfigurableModuleClass.forRoot(options),
309
+ module: BetterAuthModule
310
+ };
311
+ }
312
+ /**
313
+ * Deferred / DI-driven registration. The user factory runs **lazily**, on the
314
+ * first time anything reads `BetterAuthService.auth` (or `.api` / `.handler`).
315
+ * In normal request handling that's `AuthGuard.canActivate` or the catch-all
316
+ * 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.
322
+ */
323
+ static forRootAsync(options) {
324
+ const n = normalize(options);
325
+ const common = commonContributions(n);
326
+ return {
327
+ module: BetterAuthModule,
328
+ key: options.key ?? stableHash({
329
+ ...n,
330
+ inject: options.inject
331
+ }),
332
+ imports: options.imports ?? [],
333
+ providers: [
334
+ {
335
+ provide: BETTER_AUTH_OPTIONS,
336
+ useValue: n
337
+ },
338
+ lazyProvider({
339
+ provide: BETTER_AUTH_BUILDER,
340
+ inject: options.inject,
341
+ useFactory: options.useFactory
342
+ }),
343
+ ...common.providers,
344
+ ...n.isGlobal ? provideGlobal("guard", AuthGuard) : []
345
+ ],
346
+ controllers: common.controllers,
347
+ exports: common.exports
348
+ };
349
+ }
350
+ };
351
+ //#endregion
352
+ //#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);
355
+ });
356
+ //#endregion
357
+ //#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);
360
+ });
361
+ //#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 };
363
+
364
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["normalizeRoles"],"sources":["../src/decorators/public.decorator.ts","../src/better-auth.controller.ts","../src/better-auth.tokens.ts","../src/decorators/optional-auth.decorator.ts","../src/guards/auth.guard.ts","../src/decorators/roles.decorator.ts","../src/guards/roles.guard.ts","../src/authz-bridge.ts","../src/decorators/require-permission.decorator.ts","../src/guards/permission.guard.ts","../src/better-auth.module.ts","../src/decorators/current-user.decorator.ts","../src/decorators/current-session.decorator.ts"],"sourcesContent":["import { Reflector } from '@velajs/vela';\n\nexport const Public = Reflector.createDecorator<boolean>({ key: 'vela.auth.public' });\nexport const PUBLIC_KEY = Public.KEY;\n","import { All, Controller, Inject, Injectable, Req, type Type } from '@velajs/vela';\nimport type { Context } from 'hono';\nimport { BetterAuthService } from './better-auth.service';\nimport { Public } from './decorators/public.decorator';\n\n/**\n * Build a catch-all controller that mounts better-auth's handler at `basePath`\n * (default `/api/auth`). This is a factory because vela reads a controller's\n * route off the class at decoration time, so a custom base path needs its own\n * decorated class — the path can't be parametrized on a single shared class.\n *\n * Two base paths to keep consistent:\n * - this `basePath` is RELATIVE to vela's `globalPrefix` (always prepended);\n * - better-auth routes against its OWN absolute `basePath` (the one you pass to\n * `betterAuth({ basePath })`), which must equal `globalPrefix + basePath`.\n *\n * Both default to `/api/auth`, so the no-prefix / no-config case just works.\n */\nexport function createBetterAuthCatchallController(basePath: string = '/api/auth'): Type {\n @Public(true)\n @Controller(basePath)\n @Injectable()\n class BetterAuthCatchallController {\n // Inject the service — its `.handler` getter triggers lazy construction\n // of the underlying betterAuth() instance on first access, AFTER any\n // runtime adapter middleware (Cloudflare env capture) has run.\n constructor(@Inject(BetterAuthService) private readonly auth: BetterAuthService) {}\n\n @All('/*')\n async handle(@Req() c: Context): Promise<Response> {\n return this.auth.handler(c.req.raw);\n }\n }\n return BetterAuthCatchallController;\n}\n\n/**\n * Default-path (`/api/auth`) catch-all controller. Retained for back-compat;\n * `BetterAuthModule` now mounts {@link createBetterAuthCatchallController} with\n * the configured `basePath`. Prefer the factory for a custom base path.\n */\nexport const BetterAuthCatchallController = createBetterAuthCatchallController();\n","import { InjectionToken } from '@velajs/vela';\nimport type { BetterAuthModuleOptions } from './better-auth.types';\n\nexport const BETTER_AUTH_OPTIONS = new InjectionToken<BetterAuthModuleOptions>(\n 'vela.BetterAuthOptions',\n);\n\nexport const AUTH_USER_KEY = Symbol.for('vela.better-auth.user');\nexport const AUTH_SESSION_KEY = Symbol.for('vela.better-auth.session');\n","import { Reflector } from '@velajs/vela';\n\nexport const OptionalAuth = Reflector.createDecorator<boolean>({ key: 'vela.auth.optional' });\nexport const OPTIONAL_AUTH_KEY = OptionalAuth.KEY;\n","import {\n Inject,\n Injectable,\n REQUEST_CONTEXT,\n Reflector,\n UnauthorizedException,\n type CanActivate,\n type ExecutionContext,\n type RequestContext,\n} from '@velajs/vela';\nimport { AUTH_SESSION_KEY, AUTH_USER_KEY, BETTER_AUTH_OPTIONS } from '../better-auth.tokens';\nimport { BetterAuthService } from '../better-auth.service';\nimport type { BetterAuthModuleOptions } from '../better-auth.types';\nimport { OptionalAuth } from '../decorators/optional-auth.decorator';\nimport { Public } from '../decorators/public.decorator';\n\n@Injectable()\nexport class AuthGuard implements CanActivate {\n private readonly reflector = new Reflector();\n\n constructor(\n // Inject BetterAuthService rather than the raw better-auth instance.\n // The service's lazy `.auth` getter defers construction to first use, so\n // forRootAsync factories that depend on values only available at\n // request time (Cloudflare D1/KV bindings, etc.) build safely on the\n // first canActivate — not at module-load bootstrap.\n @Inject(BetterAuthService) private readonly auth: BetterAuthService,\n @Inject(BETTER_AUTH_OPTIONS) private readonly opts: BetterAuthModuleOptions,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n if (this.reflector.getAllAndOverride(Public, context)) return true;\n\n const request = context.getRequest();\n const path = new URL(request.url).pathname;\n const basePath = this.opts.basePath ?? '/api/auth';\n if (path === basePath || path.startsWith(`${basePath}/`)) return true;\n\n const data = await this.auth.api.getSession({ headers: request.headers });\n\n if (data) {\n const reqCtx = resolveRequestContext(context);\n reqCtx.set(AUTH_USER_KEY, data.user);\n reqCtx.set(AUTH_SESSION_KEY, data.session);\n return true;\n }\n\n if (\n this.opts.defaultPolicy === 'allow' ||\n this.reflector.getAllAndOverride(OptionalAuth, context)\n ) {\n return true;\n }\n\n throw new UnauthorizedException('Authentication required');\n }\n}\n\ninterface ContainerLike {\n resolve<T>(token: unknown): T;\n}\n\nfunction resolveRequestContext(context: ExecutionContext): RequestContext {\n const honoCtx = context.getContext() as { get: (k: string) => ContainerLike };\n const container = honoCtx.get('container');\n return container.resolve<RequestContext>(REQUEST_CONTEXT);\n}\n","import { Reflector } from '@velajs/vela';\n\nexport const Roles = Reflector.createDecorator<string[]>({ key: 'vela.auth.roles' });\nexport const ROLES_KEY = Roles.KEY;\n","import {\n ForbiddenException,\n Injectable,\n REQUEST_CONTEXT,\n Reflector,\n type CanActivate,\n type ExecutionContext,\n type RequestContext,\n} from '@velajs/vela';\nimport { AUTH_USER_KEY } from '../better-auth.tokens';\nimport type { User } from '../better-auth.types';\nimport { Roles } from '../decorators/roles.decorator';\n\n@Injectable()\nexport class RolesGuard implements CanActivate {\n private readonly reflector = new Reflector();\n\n canActivate(context: ExecutionContext): boolean {\n const required = this.reflector.getAllAndOverride(Roles, context);\n if (!required || required.length === 0) return true;\n\n const honoCtx = context.getContext() as {\n get: (k: string) => { resolve<T>(t: unknown): T };\n };\n const reqCtx = honoCtx.get('container').resolve<RequestContext>(REQUEST_CONTEXT);\n const user = reqCtx.get<User & { role?: string | string[] }>(AUTH_USER_KEY);\n if (!user) {\n throw new ForbiddenException('Role check requires authentication');\n }\n\n const userRoles = normalizeRoles(user.role);\n const ok = required.some((r) => userRoles.includes(r));\n if (!ok) {\n throw new ForbiddenException(`Insufficient role; one of [${required.join(', ')}] required`);\n }\n return true;\n }\n}\n\nfunction normalizeRoles(role: string | string[] | undefined): string[] {\n if (!role) return [];\n if (Array.isArray(role)) return role;\n return role\n .split(',')\n .map((r) => r.trim())\n .filter(Boolean);\n}\n","import type { Identity, PermissionResolver } from '@velajs/authz';\nimport type { User } from './better-auth.types';\n\n/**\n * A better-auth user carrying the optional `role` field contributed by the\n * admin plugin. `role` may be a single role, a comma-separated list, or an\n * array — {@link identityFromUser} normalizes all three.\n */\nexport type AuthUser = User & { role?: string | string[] | null };\n\nconst normalizeRoles = (role: string | string[] | null | undefined): string[] => {\n if (!role) return [];\n // Strip empty entries and return a fresh array (never alias the caller's\n // input), matching the comma-string path below.\n if (Array.isArray(role)) return role.filter(Boolean);\n return role\n .split(',')\n .map((r) => r.trim())\n .filter(Boolean);\n};\n\n/**\n * Adapts a better-auth user into a `@velajs/authz` {@link Identity}. Maps\n * `user.id` → `userId` and the admin-plugin `role` field → `roles`.\n *\n * Fail-closed: a missing user (`null`/`undefined`, i.e. an unauthenticated\n * request) maps to the zero-privilege identity `{ roles: [] }`, so downstream\n * `can()` checks grant nothing.\n */\nexport const identityFromUser = (user: AuthUser | null | undefined): Identity => {\n if (!user) return { roles: [] };\n return {\n userId: user.id,\n roles: normalizeRoles(user.role),\n };\n};\n\n/**\n * The minimal slice of a better-auth access-control role consumed here. Both\n * `createAccessControl(...).newRole(...)` and the standalone `role(...)` return\n * `{ authorize, statements }`; `statements` is the `{ resource: actions[] }`\n * grant map for that role — the only accessor {@link betterAuthAcResolver}\n * reads.\n */\nexport interface BetterAuthAcRole {\n readonly statements: Readonly<Record<string, readonly string[]>>;\n}\n\n/**\n * Flattens a better-auth AC role's `statements` into `resource:action`\n * permission strings — the granted-side format `@velajs/authz` matches\n * (wildcards included).\n */\nexport const permissionsFromAcRole = (role: BetterAuthAcRole): string[] => {\n const permissions: string[] = [];\n for (const [resource, actions] of Object.entries(role.statements ?? {})) {\n for (const action of actions ?? []) permissions.push(`${resource}:${action}`);\n }\n return permissions;\n};\n\n/**\n * Builds a fail-closed `@velajs/authz` {@link PermissionResolver} from a\n * better-auth access-control role table (`{ roleName: acRole }` — the same map\n * shape passed to better-auth's admin/organization plugins). An identity's\n * `roles` are unioned into their granted permission strings; unknown roles\n * contribute nothing.\n *\n * ```ts\n * const ac = createAccessControl({ posts: ['read', 'write'] });\n * const authz = createAuthz({\n * resolver: betterAuthAcResolver({ editor: ac.newRole({ posts: ['write'] }) }),\n * });\n * await authz.can(identityFromUser(user), 'posts:write');\n * ```\n */\nexport const betterAuthAcResolver = (\n roles: Readonly<Record<string, BetterAuthAcRole>>,\n): PermissionResolver => {\n const grantsByRole = new Map<string, string[]>();\n for (const [name, role] of Object.entries(roles)) {\n grantsByRole.set(name, permissionsFromAcRole(role));\n }\n return {\n grants(identity: Identity): Set<string> {\n const out = new Set<string>();\n for (const name of identity.roles ?? []) {\n for (const permission of grantsByRole.get(name) ?? []) out.add(permission);\n }\n return out;\n },\n };\n};\n","import { Reflector } from '@velajs/vela';\n\n/**\n * Declares the `@velajs/authz` permission(s) required to reach a controller or\n * route handler. Read via `Reflector` in an authorization guard, then checked\n * against the caller's `Identity` with `authz.can(...)`.\n *\n * ```ts\n * @RequirePermission(['posts:write'])\n * @Post()\n * create() { ... }\n * ```\n *\n * The metadata is a plain `string[]` of permission strings in the granted-side\n * format `@velajs/authz` matches (`resource:action`, or wildcards like\n * `posts:*`). Handler-level metadata overrides class-level (standard\n * `Reflector.getAllAndOverride` precedence).\n *\n * Semantics are **require-ALL** (AND): every listed permission must be granted\n * for access — the `PermissionGuard` denies if any one is missing. This\n * contrasts with `@Roles`, which is **OR** (any one of the listed roles\n * suffices).\n */\nexport const RequirePermission = Reflector.createDecorator<string[]>({\n key: 'vela.authz.permissions',\n});\n\nexport const REQUIRE_PERMISSION_KEY = RequirePermission.KEY;\n","import {\n ForbiddenException,\n Injectable,\n InjectionToken,\n REQUEST_CONTEXT,\n Reflector,\n type CanActivate,\n type ExecutionContext,\n type RequestContext,\n} from '@velajs/vela';\nimport { AUTHZ } from '@velajs/authz/vela';\nimport type { Authz } from '@velajs/authz';\nimport { AUTH_USER_KEY } from '../better-auth.tokens';\nimport type { User } from '../better-auth.types';\nimport { identityFromUser } from '../authz-bridge';\nimport { RequirePermission } from '../decorators/require-permission.decorator';\n\n// The linked `@velajs/authz` is built against its own (newer) `@velajs/vela`\n// copy, so the `AUTHZ` token's `InjectionToken` type is nominally distinct from\n// this package's `InjectionToken` — even though it is the very same runtime\n// token object (the DI container matches tokens by object identity). Re-type it\n// to the local `InjectionToken` so the request-time `container.resolve(...)`\n// accepts it without a structural clash. This is purely a compile-time alias;\n// it changes nothing at runtime. (Version-skew workaround until both publish.)\nconst AUTHZ_TOKEN = AUTHZ as unknown as InjectionToken<Authz>;\n\n/**\n * Enforces the `@RequirePermission(...)` metadata against the `@velajs/authz`\n * engine. For each required permission it calls `authz.can(identity, perm)`,\n * requiring **all** of them (AND semantics — contrast {@link RolesGuard}, which\n * is OR over roles). The caller's `Identity` is derived from the better-auth\n * user that {@link AuthGuard} placed in the request context, so this guard must\n * run *after* `AuthGuard` (e.g. `@UseGuards(AuthGuard, PermissionGuard)`).\n *\n * `AUTHZ` is resolved at **request time** from the per-request container (the\n * same container `REQUEST_CONTEXT` is resolved from), not constructor-injected.\n * This deliberately avoids DI visibility coupling: the guard works whether or\n * not `AuthzModule` is registered as global — a present-but-non-global\n * `AuthzModule` resolves fine and, crucially, never crashes bootstrap. If\n * `AuthzModule` is not registered at all the resolve fails and the guard fails\n * closed (403) rather than granting access.\n *\n * Fail-closed on every abnormal path — no branch grants access on missing\n * wiring or a missing caller:\n * - no required permissions → allow (nothing to enforce);\n * - `AUTHZ` unresolvable (`AuthzModule` not registered) → deny (`ForbiddenException`);\n * - no authenticated user in the request context → deny;\n * - any single required permission not granted → deny.\n *\n * The guard is stateless (no injected dependencies), so it is safe to register\n * as a plain provided guard.\n */\n@Injectable()\nexport class PermissionGuard implements CanActivate {\n private readonly reflector = new Reflector();\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n const required = this.reflector.getAllAndOverride(RequirePermission, context);\n if (!required || required.length === 0) return true;\n\n const honoCtx = context.getContext() as {\n get: (k: string) => { resolve<T>(t: unknown): T };\n };\n const container = honoCtx.get('container');\n\n // Resolve AUTHZ at request time from the per-request container — the SAME\n // container REQUEST_CONTEXT resolves from. Because this lookup carries no\n // requesting module, it matches AUTHZ by its exporter, so a non-global\n // `AuthzModule` is reachable without forcing the app to declare it global.\n // `resolve` throws (or, defensively, could yield undefined) for an\n // unregistered token, so wrap it and fail closed on any failure.\n let authz: Authz | undefined;\n try {\n authz = container.resolve<Authz>(AUTHZ_TOKEN);\n } catch {\n authz = undefined;\n }\n if (!authz) {\n throw new ForbiddenException('Authorization is not configured');\n }\n\n const reqCtx = container.resolve<RequestContext>(REQUEST_CONTEXT);\n const user = reqCtx.get<User & { id?: string; role?: string | string[] }>(AUTH_USER_KEY);\n if (!user) {\n throw new ForbiddenException('Permission check requires authentication');\n }\n\n const identity = identityFromUser(user);\n for (const permission of required) {\n if (!(await authz.can(identity, permission))) {\n throw new ForbiddenException(`Missing permission: ${permission}`);\n }\n }\n return true;\n }\n}\n","import {\n defineModule,\n lazyProvider,\n provideGlobal,\n stableHash,\n type DynamicModule,\n type InferTokens,\n type ProviderOptions,\n type Token,\n type Type,\n} from '@velajs/vela';\nimport { createBetterAuthCatchallController } from './better-auth.controller';\nimport { BetterAuthService, BETTER_AUTH_BUILDER } from './better-auth.service';\nimport { BETTER_AUTH_OPTIONS } from './better-auth.tokens';\nimport type { BetterAuthInstance, BetterAuthModuleOptions } from './better-auth.types';\nimport { AuthGuard } from './guards/auth.guard';\nimport { RolesGuard } from './guards/roles.guard';\nimport { PermissionGuard } from './guards/permission.guard';\n\nconst DEFAULT_BASE_PATH = '/api/auth';\n\n/** Structural options with defaults applied (everything but the auth instance). */\ninterface NormalizedOptions {\n basePath: string;\n isGlobal: boolean;\n defaultPolicy: 'deny' | 'allow';\n mountHandler: boolean;\n}\n\nfunction normalize(options: Partial<BetterAuthModuleOptions>): NormalizedOptions {\n return {\n basePath: options.basePath ?? DEFAULT_BASE_PATH,\n isGlobal: options.isGlobal ?? false,\n defaultPolicy: options.defaultPolicy ?? 'deny',\n mountHandler: options.mountHandler ?? true,\n };\n}\n\n/** Providers, controllers, and exports shared by both entry points. */\nfunction commonContributions(n: NormalizedOptions): {\n providers: Array<Type | ProviderOptions>;\n controllers: Type[];\n exports: DynamicModule['exports'];\n} {\n return {\n providers: [BetterAuthService, AuthGuard, RolesGuard, PermissionGuard],\n controllers: n.mountHandler ? [createBetterAuthCatchallController(n.basePath)] : [],\n exports: [BetterAuthService, BETTER_AUTH_OPTIONS, AuthGuard, RolesGuard, PermissionGuard],\n };\n}\n\n/**\n * The blessed engine generates `forRoot`. `setup` runs once per instance at\n * call time: it re-provides {@link BETTER_AUTH_OPTIONS} with defaults applied,\n * derives the auth builder from those options, mounts the catch-all controller,\n * and — via the `global:` slot — registers the app-wide guard when `isGlobal`.\n *\n * `isGlobal` here means \"apply AuthGuard app-wide\", NOT \"make this a global\n * module\", so the default `isGlobal → global: true` extras transform is\n * replaced with identity; the flag reaches `setup` through the options bag.\n */\nconst authModuleHost = defineModule<BetterAuthModuleOptions>({\n name: 'BetterAuth',\n optionsToken: BETTER_AUTH_OPTIONS,\n transform: (definition) => definition,\n // The auth instance is a stateful value — key off the structural subset only.\n key: (options) => stableHash(normalize(options)),\n setup: ({ OPTIONS, options }) => {\n const n = normalize(options);\n const common = commonContributions(n);\n const auth = (options as BetterAuthModuleOptions).auth;\n return {\n providers: [\n // Override the auto-provided raw bag with the normalized shape so\n // BETTER_AUTH_OPTIONS consumers always see defaults + the auth instance.\n { provide: OPTIONS, useValue: { ...n, auth } },\n // Eager auth: the builder hands back the instance the caller passed in.\n lazyProvider({\n provide: BETTER_AUTH_BUILDER,\n inject: [OPTIONS],\n useFactory: (o: BetterAuthModuleOptions) => o.auth,\n }),\n ...common.providers,\n ],\n controllers: common.controllers,\n exports: common.exports,\n global: n.isGlobal ? { guards: [AuthGuard] } : undefined,\n };\n },\n});\n\n/**\n * Options for {@link BetterAuthModule.forRootAsync}.\n *\n * The `Inject` type parameter captures the literal `inject` tuple at the call\n * site (via `const` inference) so `useFactory` parameters are typed from the\n * inject array, position-by-position — no `as const`, no `(...deps: any[])`:\n *\n * ```ts\n * BetterAuthModule.forRootAsync({\n * inject: [D1Service, ConfigService], // captured as readonly tuple\n * useFactory: (d1, config) => // d1: D1Service, config: ConfigService\n * betterAuth({ database: drizzleAdapter(drizzle(d1.database), ...) }),\n * });\n * ```\n */\ninterface ForRootAsyncOptions<\n Inject extends readonly Token<unknown>[] = readonly Token<unknown>[],\n> {\n inject?: Inject;\n imports?: DynamicModule['imports'];\n useFactory: (...deps: InferTokens<Inject>) => BetterAuthInstance;\n isGlobal?: boolean;\n mountHandler?: boolean;\n basePath?: string;\n defaultPolicy?: 'deny' | 'allow';\n key?: string;\n}\n\nexport class BetterAuthModule {\n /**\n * Synchronous registration. The auth instance is constructed by the consumer\n * at module-load time and passed in directly. Use this when the inputs to\n * `betterAuth({...})` are available at startup (Node apps with a static DB\n * connection, in-memory adapters, etc.).\n */\n static forRoot(\n options: BetterAuthModuleOptions & { isGlobal?: boolean; key?: string },\n ): DynamicModule {\n // Delegate to the generated static, then rebrand the module identity so the\n // public `BetterAuthModule` class is the one registered (consistent with\n // `forRootAsync` and better diagnostics).\n return { ...authModuleHost.ConfigurableModuleClass.forRoot(options), module: BetterAuthModule };\n }\n\n /**\n * Deferred / DI-driven registration. The user factory runs **lazily**, on the\n * first time anything reads `BetterAuthService.auth` (or `.api` / `.handler`).\n * In normal request handling that's `AuthGuard.canActivate` or the catch-all\n * controller's `.handle`. At module load the factory does NOT run — it's only\n * captured behind {@link lazyProvider}'s memoized thunk. This is what makes\n * Cloudflare bindings (D1, KV, R2) work: the binding isn't ready at boot, but\n * it IS by the time a request flows through and the guard / catch-all reads\n * the service. Inject deps resolve at module load (cheap BindingRef wrappers);\n * their *values* are read at first auth use, inside your factory body.\n */\n static forRootAsync<const Inject extends readonly Token<unknown>[] = readonly Token<unknown>[]>(\n options: ForRootAsyncOptions<Inject>,\n ): DynamicModule {\n const n = normalize(options);\n const common = commonContributions(n);\n return {\n module: BetterAuthModule,\n key: options.key ?? stableHash({ ...n, inject: options.inject }),\n imports: options.imports ?? [],\n providers: [\n { provide: BETTER_AUTH_OPTIONS, useValue: n },\n // The deferred auth builder: `lazyProvider` wraps the user factory in a\n // memoized thunk, replacing the hand-rolled `(...deps) => () => f(...deps)`.\n lazyProvider({\n provide: BETTER_AUTH_BUILDER,\n inject: options.inject,\n useFactory: options.useFactory,\n }),\n ...common.providers,\n ...(n.isGlobal ? provideGlobal('guard', AuthGuard) : []),\n ],\n controllers: common.controllers,\n exports: common.exports,\n };\n }\n}\n","import {\n createLazyParamDecorator,\n REQUEST_CONTEXT,\n type ExecutionContext,\n type RequestContext,\n} from '@velajs/vela';\nimport { AUTH_USER_KEY } from '../better-auth.tokens';\nimport type { User } from '../better-auth.types';\n\nexport const CurrentUser = createLazyParamDecorator((_data: unknown, ctx: ExecutionContext) => {\n const honoCtx = ctx.getContext() as { get: (k: string) => { resolve<T>(t: unknown): T } };\n const reqCtx = honoCtx.get('container').resolve<RequestContext>(REQUEST_CONTEXT);\n return reqCtx.get<User>(AUTH_USER_KEY);\n});\n","import {\n createLazyParamDecorator,\n REQUEST_CONTEXT,\n type ExecutionContext,\n type RequestContext,\n} from '@velajs/vela';\nimport { AUTH_SESSION_KEY } from '../better-auth.tokens';\nimport type { Session } from '../better-auth.types';\n\nexport const CurrentSession = createLazyParamDecorator((_data: unknown, ctx: ExecutionContext) => {\n const honoCtx = ctx.getContext() as { get: (k: string) => { resolve<T>(t: unknown): T } };\n const reqCtx = honoCtx.get('container').resolve<RequestContext>(REQUEST_CONTEXT);\n return reqCtx.get<Session>(AUTH_SESSION_KEY);\n});\n"],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ //#region src/testing/acting-as.d.ts
2
+ /**
3
+ * The slice of `@velajs/testing`'s `TestingModule` this resolver depends on.
4
+ * Declared structurally so `@velajs/better-auth/testing` carries NO runtime
5
+ * (or type) dependency on `@velajs/testing` — a real `TestingModule` satisfies
6
+ * it, and the resolver stays assignable to `@velajs/testing`'s `ActingAsResolver`.
7
+ */
8
+ interface TestModuleLike {
9
+ get<T>(token: unknown): T;
10
+ }
11
+ /**
12
+ * A test principal. Opaque `Record<string, unknown>` to stay compatible with
13
+ * `@velajs/testing`'s `TestPrincipal`. Recognized fields:
14
+ *
15
+ * - `id` — reuse the user with this id if it already exists.
16
+ * - `email` — reuse the user with this email, else create one.
17
+ * - `name` — display name for a created user (defaults to the email).
18
+ *
19
+ * Any other fields are forwarded to `internalAdapter.createUser` (e.g. `role`)
20
+ * when a new user is minted, so role-guarded routes can be exercised.
21
+ */
22
+ type ActingAsPrincipal = Record<string, unknown>;
23
+ /**
24
+ * actingAs — a `@velajs/testing` auth resolver for better-auth.
25
+ *
26
+ * Resolves {@link BetterAuthService} from the module, mints a REAL better-auth
27
+ * session for `principal` through `auth.$context.internalAdapter`, and returns
28
+ * a `Headers` carrying a properly signed session cookie. Guarded routes
29
+ * (`AuthGuard`) then accept requests carrying those headers because
30
+ * `auth.api.getSession` validates the cookie against the same session store.
31
+ *
32
+ * The signature `(module, principal) => Promise<Headers>` is exactly
33
+ * `@velajs/testing`'s `ActingAsResolver`, so it plugs straight in:
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import { actingAs } from '@velajs/better-auth/testing';
38
+ *
39
+ * // As the default resolver for the module:
40
+ * module.setAuthResolver(actingAs);
41
+ * await module.http.get('/me').actingAs({ email: 'ada@example.com' }).send();
42
+ *
43
+ * // Or passed per-request:
44
+ * await module.http.get('/me').actingAs({ id: existingUserId }, actingAs).send();
45
+ * ```
46
+ */
47
+ declare function actingAs(module: TestModuleLike, principal: ActingAsPrincipal): Promise<Headers>;
48
+ //#endregion
49
+ export { type ActingAsPrincipal, type TestModuleLike, actingAs };
50
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,60 @@
1
+ import { n as BetterAuthService } from "../better-auth.service-BMkyFX-w.js";
2
+ import { makeSignature } from "better-auth/crypto";
3
+ //#region src/testing/acting-as.ts
4
+ /**
5
+ * actingAs — a `@velajs/testing` auth resolver for better-auth.
6
+ *
7
+ * Resolves {@link BetterAuthService} from the module, mints a REAL better-auth
8
+ * session for `principal` through `auth.$context.internalAdapter`, and returns
9
+ * a `Headers` carrying a properly signed session cookie. Guarded routes
10
+ * (`AuthGuard`) then accept requests carrying those headers because
11
+ * `auth.api.getSession` validates the cookie against the same session store.
12
+ *
13
+ * The signature `(module, principal) => Promise<Headers>` is exactly
14
+ * `@velajs/testing`'s `ActingAsResolver`, so it plugs straight in:
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { actingAs } from '@velajs/better-auth/testing';
19
+ *
20
+ * // As the default resolver for the module:
21
+ * module.setAuthResolver(actingAs);
22
+ * await module.http.get('/me').actingAs({ email: 'ada@example.com' }).send();
23
+ *
24
+ * // Or passed per-request:
25
+ * await module.http.get('/me').actingAs({ id: existingUserId }, actingAs).send();
26
+ * ```
27
+ */
28
+ async function actingAs(module, principal) {
29
+ const ctx = await module.get(BetterAuthService).auth.$context;
30
+ const internalAdapter = ctx.internalAdapter;
31
+ const id = typeof principal.id === "string" ? principal.id : void 0;
32
+ const email = typeof principal.email === "string" ? principal.email : void 0;
33
+ const name = typeof principal.name === "string" ? principal.name : void 0;
34
+ let user = null;
35
+ if (id) user = await internalAdapter.findUserById(id);
36
+ if (!user && email) user = (await internalAdapter.findUserByEmail(email))?.user ?? null;
37
+ if (!user) {
38
+ if (!email) throw new Error("actingAs: principal must carry an `email` (to create a user) or an `id` matching an existing user.");
39
+ const { id: _id, email: _email, name: _name, ...extra } = principal;
40
+ user = await internalAdapter.createUser({
41
+ ...extra,
42
+ email,
43
+ name: name ?? email,
44
+ ...id ? { id } : {}
45
+ });
46
+ }
47
+ const session = await internalAdapter.createSession(user.id, false, {
48
+ ipAddress: "127.0.0.1",
49
+ userAgent: "vela-test"
50
+ });
51
+ const cookieName = ctx.authCookies.sessionToken.name;
52
+ const signedToken = `${session.token}.${await makeSignature(session.token, ctx.secret)}`;
53
+ const headers = new Headers();
54
+ headers.set("Cookie", `${cookieName}=${signedToken}`);
55
+ return headers;
56
+ }
57
+ //#endregion
58
+ export { actingAs };
59
+
60
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/testing/acting-as.ts"],"sourcesContent":["// Mechanics adapted from @stratal/testing (MIT, © Temitayo Fadojutimi):\n// mint a real better-auth session via `$context.internalAdapter` and hand back\n// a signed session-cookie header. Adapted to better-auth >=1.6: the session\n// cookie is signed with the package's own `makeSignature` (better-auth/crypto)\n// — the same primitive better-auth's built-in test cookie builder uses — so no\n// endpoint-context shim or `setSessionCookie` mock is needed.\nimport { makeSignature } from 'better-auth/crypto';\nimport { BetterAuthService } from '../better-auth.service';\n\n/**\n * The slice of `@velajs/testing`'s `TestingModule` this resolver depends on.\n * Declared structurally so `@velajs/better-auth/testing` carries NO runtime\n * (or type) dependency on `@velajs/testing` — a real `TestingModule` satisfies\n * it, and the resolver stays assignable to `@velajs/testing`'s `ActingAsResolver`.\n */\nexport interface TestModuleLike {\n get<T>(token: unknown): T;\n}\n\n/**\n * A test principal. Opaque `Record<string, unknown>` to stay compatible with\n * `@velajs/testing`'s `TestPrincipal`. Recognized fields:\n *\n * - `id` — reuse the user with this id if it already exists.\n * - `email` — reuse the user with this email, else create one.\n * - `name` — display name for a created user (defaults to the email).\n *\n * Any other fields are forwarded to `internalAdapter.createUser` (e.g. `role`)\n * when a new user is minted, so role-guarded routes can be exercised.\n */\nexport type ActingAsPrincipal = Record<string, unknown>;\n\n/**\n * actingAs — a `@velajs/testing` auth resolver for better-auth.\n *\n * Resolves {@link BetterAuthService} from the module, mints a REAL better-auth\n * session for `principal` through `auth.$context.internalAdapter`, and returns\n * a `Headers` carrying a properly signed session cookie. Guarded routes\n * (`AuthGuard`) then accept requests carrying those headers because\n * `auth.api.getSession` validates the cookie against the same session store.\n *\n * The signature `(module, principal) => Promise<Headers>` is exactly\n * `@velajs/testing`'s `ActingAsResolver`, so it plugs straight in:\n *\n * @example\n * ```ts\n * import { actingAs } from '@velajs/better-auth/testing';\n *\n * // As the default resolver for the module:\n * module.setAuthResolver(actingAs);\n * await module.http.get('/me').actingAs({ email: 'ada@example.com' }).send();\n *\n * // Or passed per-request:\n * await module.http.get('/me').actingAs({ id: existingUserId }, actingAs).send();\n * ```\n */\nexport async function actingAs(\n module: TestModuleLike,\n principal: ActingAsPrincipal,\n): Promise<Headers> {\n const auth = module.get<BetterAuthService>(BetterAuthService).auth;\n const ctx = await auth.$context;\n const internalAdapter = ctx.internalAdapter;\n\n const id = typeof principal.id === 'string' ? principal.id : undefined;\n const email = typeof principal.email === 'string' ? principal.email : undefined;\n const name = typeof principal.name === 'string' ? principal.name : undefined;\n\n // `findUserById`/`createUser` yield a bare user; `findUserByEmail` nests it\n // under `{ user, accounts }` — normalize to the id we need.\n let user: { id: string } | null = null;\n if (id) user = await internalAdapter.findUserById(id);\n if (!user && email) {\n const found = await internalAdapter.findUserByEmail(email);\n user = found?.user ?? null;\n }\n if (!user) {\n if (!email) {\n throw new Error(\n 'actingAs: principal must carry an `email` (to create a user) or an ' +\n '`id` matching an existing user.',\n );\n }\n const { id: _id, email: _email, name: _name, ...extra } = principal;\n user = await internalAdapter.createUser({\n ...extra,\n email,\n name: name ?? email,\n ...(id ? { id } : {}),\n });\n }\n\n const session = await internalAdapter.createSession(user.id, false, {\n ipAddress: '127.0.0.1',\n userAgent: 'vela-test',\n });\n\n const cookieName = ctx.authCookies.sessionToken.name;\n const signedToken = `${session.token}.${await makeSignature(session.token, ctx.secret)}`;\n\n const headers = new Headers();\n headers.set('Cookie', `${cookieName}=${signedToken}`);\n return headers;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,eAAsB,SACpB,QACA,WACkB;CAElB,MAAM,MAAM,MADC,OAAO,IAAuB,iBAAiB,CAAC,CAAC,KACvC;CACvB,MAAM,kBAAkB,IAAI;CAE5B,MAAM,KAAK,OAAO,UAAU,OAAO,WAAW,UAAU,KAAK,KAAA;CAC7D,MAAM,QAAQ,OAAO,UAAU,UAAU,WAAW,UAAU,QAAQ,KAAA;CACtE,MAAM,OAAO,OAAO,UAAU,SAAS,WAAW,UAAU,OAAO,KAAA;CAInE,IAAI,OAA8B;CAClC,IAAI,IAAI,OAAO,MAAM,gBAAgB,aAAa,EAAE;CACpD,IAAI,CAAC,QAAQ,OAEX,QAAO,MADa,gBAAgB,gBAAgB,KAAK,EAAA,EAC3C,QAAQ;CAExB,IAAI,CAAC,MAAM;EACT,IAAI,CAAC,OACH,MAAM,IAAI,MACR,oGAEF;EAEF,MAAM,EAAE,IAAI,KAAK,OAAO,QAAQ,MAAM,OAAO,GAAG,UAAU;EAC1D,OAAO,MAAM,gBAAgB,WAAW;GACtC,GAAG;GACH;GACA,MAAM,QAAQ;GACd,GAAI,KAAK,EAAE,GAAG,IAAI,CAAC;EACrB,CAAC;CACH;CAEA,MAAM,UAAU,MAAM,gBAAgB,cAAc,KAAK,IAAI,OAAO;EAClE,WAAW;EACX,WAAW;CACb,CAAC;CAED,MAAM,aAAa,IAAI,YAAY,aAAa;CAChD,MAAM,cAAc,GAAG,QAAQ,MAAM,GAAG,MAAM,cAAc,QAAQ,OAAO,IAAI,MAAM;CAErF,MAAM,UAAU,IAAI,QAAQ;CAC5B,QAAQ,IAAI,UAAU,GAAG,WAAW,GAAG,aAAa;CACpD,OAAO;AACT"}