@velajs/better-auth 1.29.0 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.30.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4d0342b: Build on the tiered `@velajs/vela` entry points: module-author seams such as `Container`, `MetadataRegistry`, `DiscoveryService`, `PipelineRunner`, trusted request identity and entrypoint scopes come from `@velajs/vela/module-kit`, and features from their subpaths. The package's own exports are unchanged.
8
+
9
+ **Behavior change:** this release requires the `@velajs/vela` release that introduces `@velajs/vela/module-kit` and the feature subpaths; upgrade both together. Application code that imported these framework names from the root moves them as follows (the `@velajs/vela` changelog lists every name):
10
+
11
+ | Old import | New import | Examples |
12
+ |---|---|---|
13
+ | `@velajs/vela` | `@velajs/vela/module-kit` | `Container`, `MetadataRegistry`, `DiscoveryService`, `createDiscoverableDecorator`, `registerEntrypointKind`, `runInEntrypointScope`, `PipelineRunner`, `RuntimeAdapter`, `invokeScheduledJob`, `getRequestContainer`, `setTrustedRequestIdentity`, `resolveErrorReporter`, `lazyProvider`, `stableHash`, `defineMetadata` |
14
+ | `@velajs/vela` | `@velajs/vela/cache`, `/throttler`, `/schedule`, `/events`, `/health`, `/logging`, `/http-client` | `CacheModule`, `ResponseCacheModule`, `ThrottlerModule`, `ScheduleModule`, `Cron`, `EventEmitterModule`, `HealthModule`, `LoggingModule`, `HttpModule` |
15
+ | `@velajs/vela` | `@velajs/vela/openapi` | `Endpoint`, `defineEndpoint`, `createOpenApiDocument`, `ApiDoc`, `ApiTags`, `ApiResponse` |
16
+ | `@velajs/vela` | `@velajs/vela/security`, `/dispatch` | `SecurityModule`, `CorsModule`, `signUrl`, `NONCE_STORE`; `InternalDispatcher`, `SignedInvocation` |
17
+ | `@velajs/vela` | `@velajs/vela/validation`, `/websocket` | `ValidationPipe`, `defineDto`, `parseSchemaAsync`; `WebSocketGateway`, `WebSocketModule` |
18
+ | `@velajs/vela/internal` | `@velajs/vela/module-kit` | `Container`, `MetadataRegistry` |
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies [4d0342b]
23
+ - Updated dependencies [4467619]
24
+ - Updated dependencies [7372d90]
25
+ - Updated dependencies [c101033]
26
+ - Updated dependencies [4d0342b]
27
+ - @velajs/authz@1.30.0
28
+ - @velajs/vela@1.30.0
29
+
3
30
  ## 1.29.0
4
31
 
5
32
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { a as Session, i as BetterAuthRuntimeOptions, n as BetterAuthInstance, o as User, r as BetterAuthModuleOptions, t as BetterAuthService } from "./better-auth.service-CvmboUID.js";
2
- import { CanActivate, DynamicModule, ExecutionContext, FactoryInject, InferTokens, InjectionToken, Reflector, Token, Type } from "@velajs/vela";
2
+ import { CanActivate, DynamicModule, ExecutionContext, InjectionToken, Reflector, Token, Type } from "@velajs/vela";
3
+ import { FactoryInject, InferTokens } from "@velajs/vela/module-kit";
3
4
  import { UpgradeAuthenticator, WebSocketUpgradeAuthenticationContext, WebSocketUpgradeIdentity } from "@velajs/vela/websocket";
4
5
  import { Identity, PermissionResolver } from "@velajs/authz";
5
6
  //#region src/better-auth.module.d.ts
@@ -115,10 +116,10 @@ export declare class BetterAuthUpgradeAuthenticator implements UpgradeAuthentica
115
116
  }
116
117
  //#endregion
117
118
  //#region src/decorators/current-user.decorator.d.ts
118
- export declare const CurrentUser: (data?: unknown, ...pipes: import("@velajs/vela").PipeType[]) => ParameterDecorator;
119
+ export declare const CurrentUser: (data?: unknown, ...pipes: import("@velajs/vela/module-kit").PipeType[]) => ParameterDecorator;
119
120
  //#endregion
120
121
  //#region src/decorators/current-session.decorator.d.ts
121
- export declare const CurrentSession: (data?: unknown, ...pipes: import("@velajs/vela").PipeType[]) => ParameterDecorator;
122
+ export declare const CurrentSession: (data?: unknown, ...pipes: import("@velajs/vela/module-kit").PipeType[]) => ParameterDecorator;
122
123
  //#endregion
123
124
  //#region src/decorators/public.decorator.d.ts
124
125
  export declare const Public: import("@velajs/vela").ReflectableDecorator<boolean>;
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { a as __decorateMetadata, i as __decorateParam, n as BetterAuthService, r as __decorate, t as BETTER_AUTH_BUILDER } from "./better-auth.service-SNg1YFTK.js";
2
- import { All, Controller, Inject, Injectable, InjectionToken, Optional, Reflector, Req, UnauthorizedException, clearTrustedRequestIdentity, createParamDecorator, createTrustedRequestIdentityStore, defineProvider, getTrustedContextRequest, lazyProvider, provideGlobal, setTrustedRequestIdentity, stableHash } from "@velajs/vela";
2
+ import { All, Controller, Inject, Injectable, InjectionToken, Optional, Reflector, Req, UnauthorizedException, createParamDecorator, defineProvider } from "@velajs/vela";
3
+ import { clearTrustedRequestIdentity, createTrustedRequestIdentityStore, getTrustedContextRequest, lazyProvider, provideGlobal, setTrustedRequestIdentity, stableHash } from "@velajs/vela/module-kit";
3
4
  import { getContextIdentity } from "@velajs/authz/vela";
4
5
  //#region src/decorators/public.decorator.ts
5
6
  const Public = Reflector.createDecorator({ key: "vela.auth.public" });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/decorators/public.decorator.ts","../src/base-path.ts","../src/better-auth.controller.ts","../src/better-auth.tokens.ts","../src/auth-request-state.ts","../src/session-data.ts","../src/decorators/optional-auth.decorator.ts","../src/guards/auth.guard.ts","../src/better-auth.module.ts","../src/upgrade-authenticator.ts","../src/decorators/current-user.decorator.ts","../src/decorators/current-session.decorator.ts","../src/authz-bridge.ts"],"sourcesContent":["import { Reflector } from '@velajs/vela';\n\nexport const Public = Reflector.createDecorator<boolean>({ key: 'vela.auth.public' });\nexport const PUBLIC_KEY = Public.KEY;\n","export const DEFAULT_BETTER_AUTH_BASE_PATH = '/api/auth';\n\n/** Validate and canonicalize the route prefix used by the public auth controller. */\nexport function normalizeBetterAuthBasePath(value?: string): string {\n const basePath = value ?? DEFAULT_BETTER_AUTH_BASE_PATH;\n if (\n basePath.length === 0 ||\n basePath !== basePath.trim() ||\n !basePath.startsWith('/') ||\n basePath.startsWith('//') ||\n basePath === '/' ||\n basePath.endsWith('/') ||\n /[\\\\?#*]/u.test(basePath) ||\n /%(?:2e|2f|5c)/iu.test(basePath)\n ) {\n throw new Error(\n '@velajs/better-auth: basePath must be a canonical absolute path such as \"/api/auth\"',\n );\n }\n\n let decoded: string;\n try {\n decoded = decodeURIComponent(basePath);\n } catch {\n throw new Error('@velajs/better-auth: basePath contains invalid percent encoding');\n }\n if (decoded.split('/').some((segment) => segment === '.' || segment === '..')) {\n throw new Error('@velajs/better-auth: basePath must not contain dot segments');\n }\n return basePath;\n}\n","import { All, Controller, Inject, Req, type Type } from '@velajs/vela';\nimport type { Context } from 'hono';\nimport { BetterAuthService } from './better-auth.service';\nimport { Public } from './decorators/public.decorator';\nimport { normalizeBetterAuthBasePath } from './base-path';\n\n/**\n * Build a catch-all controller that mounts better-auth's handler at `basePath`\n * (default `/api/auth`). This is a factory because vela reads a controller's\n * route off the class at decoration time, so a custom base path needs its own\n * decorated class — the path can't be parametrized on a single shared class.\n *\n * Two base paths to keep consistent:\n * - this `basePath` is RELATIVE to vela's `globalPrefix` (always prepended);\n * - better-auth routes against its OWN absolute `basePath` (the one you pass to\n * `betterAuth({ basePath })`), which must equal `globalPrefix + basePath`.\n *\n * Both default to `/api/auth`, so the no-prefix / no-config case just works.\n */\nexport function createBetterAuthCatchallController(basePath: string = '/api/auth'): Type {\n const normalizedBasePath = normalizeBetterAuthBasePath(basePath);\n @Public(true)\n @Controller(normalizedBasePath)\n 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","import { InjectionToken } from '@velajs/vela';\nimport type { BetterAuthRuntimeOptions } from './better-auth.types';\n\nexport const BETTER_AUTH_OPTIONS = new InjectionToken<BetterAuthRuntimeOptions>(\n 'vela.BetterAuthOptions',\n);\n","import {\n clearTrustedRequestIdentity,\n createTrustedRequestIdentityStore,\n getTrustedContextRequest,\n setTrustedRequestIdentity,\n type ExecutionContext,\n} from '@velajs/vela';\nimport type { SessionData } from './session-data';\n\n// This map stores provider payload, never a second authentication authority.\n// A clear, expiry, or identity replacement makes the payload unreachable.\nconst sessions = createTrustedRequestIdentityStore<SessionData>();\n\nexport function beginAuthRequest(context: ExecutionContext): void {\n clearTrustedRequestIdentity(context.getRequest());\n}\n\nexport function authenticateRequest(\n context: ExecutionContext,\n data: SessionData,\n issuer: string,\n): void {\n const request = context.getRequest();\n setTrustedRequestIdentity(request, {\n principal: { issuer, subject: data.user.id, principalType: 'user' },\n expiresAtMs: data.session.expiresAt.getTime(),\n roles: data.roles,\n ...(data.tenantId === undefined ? {} : { tenantId: data.tenantId }),\n });\n sessions.set(request, data);\n}\n\nexport function getAuthRequestState(context: ExecutionContext): SessionData | undefined {\n const request = getTrustedContextRequest(context);\n return request ? sessions.get(request) : undefined;\n}\n","import type { Session, User } from './better-auth.types';\n\nexport interface SessionData {\n readonly user: User;\n readonly session: Session;\n readonly roles: readonly string[];\n readonly tenantId?: string;\n}\n\nfunction own(value: unknown, key: string): unknown {\n if (typeof value !== 'object' || value === null) return undefined;\n const descriptor = Object.getOwnPropertyDescriptor(value, key);\n return descriptor && 'value' in descriptor ? descriptor.value : undefined;\n}\n\nfunction requiredString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0;\n}\n\nfunction date(value: unknown): Date | undefined {\n return value instanceof Date && Number.isFinite(value.getTime()) ? new Date(value) : undefined;\n}\n\nfunction optionalString(value: unknown): value is string | null | undefined {\n return value === undefined || value === null || typeof value === 'string';\n}\n\n/** Keep plugin data opaque; base model fields below are validated individually. */\nfunction dataProperties(value: object): Record<string, unknown> {\n return Object.fromEntries(Object.keys(value).map((key) => [key, own(value, key)]));\n}\n\n/** Validate the full Better Auth base models, not merely ids masquerading as them. */\nexport function validateSessionData(value: unknown): SessionData | undefined {\n try {\n const user = own(value, 'user');\n const session = own(value, 'session');\n if (\n typeof user !== 'object' ||\n user === null ||\n typeof session !== 'object' ||\n session === null\n ) {\n return undefined;\n }\n const id = own(user, 'id');\n const email = own(user, 'email');\n const name = own(user, 'name');\n const emailVerified = own(user, 'emailVerified');\n const image = own(user, 'image');\n const createdAt = date(own(user, 'createdAt'));\n const updatedAt = date(own(user, 'updatedAt'));\n const sessionId = own(session, 'id');\n const userId = own(session, 'userId');\n const token = own(session, 'token');\n const sessionCreatedAt = date(own(session, 'createdAt'));\n const sessionUpdatedAt = date(own(session, 'updatedAt'));\n const expiresAt = date(own(session, 'expiresAt'));\n const ipAddress = own(session, 'ipAddress');\n const userAgent = own(session, 'userAgent');\n const organization = own(session, 'activeOrganizationId');\n const role = own(user, 'role');\n let roles: string[] = [];\n if (role !== undefined && role !== null) {\n if (typeof role === 'string')\n roles = role\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean);\n else if (\n Array.isArray(role) &&\n role.every((entry: unknown): entry is string => typeof entry === 'string')\n ) {\n roles = role.map((entry: string) => entry.trim()).filter(Boolean);\n } else return undefined;\n }\n if (\n !requiredString(id) ||\n !requiredString(email) ||\n typeof name !== 'string' ||\n typeof emailVerified !== 'boolean' ||\n !optionalString(image) ||\n !createdAt ||\n !updatedAt ||\n !requiredString(sessionId) ||\n userId !== id ||\n !requiredString(token) ||\n !sessionCreatedAt ||\n !sessionUpdatedAt ||\n !expiresAt ||\n expiresAt.getTime() <= Date.now() ||\n !optionalString(ipAddress) ||\n !optionalString(userAgent) ||\n (organization !== undefined && organization !== null && !requiredString(organization))\n )\n return undefined;\n return {\n user: Object.freeze({\n ...dataProperties(user),\n id,\n email,\n name,\n emailVerified,\n createdAt,\n updatedAt,\n image,\n }),\n session: Object.freeze({\n ...dataProperties(session),\n id: sessionId,\n userId: id,\n token,\n createdAt: sessionCreatedAt,\n updatedAt: sessionUpdatedAt,\n expiresAt,\n ipAddress,\n userAgent,\n }),\n roles: Object.freeze(roles),\n ...(typeof organization === 'string' ? { tenantId: organization } : {}),\n };\n } catch {\n return undefined;\n }\n}\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 Reflector,\n UnauthorizedException,\n getTrustedContextRequest,\n type CanActivate,\n type ExecutionContext,\n} from '@velajs/vela';\nimport { getContextIdentity } from '@velajs/authz/vela';\nimport { BETTER_AUTH_OPTIONS } from '../better-auth.tokens';\nimport { BetterAuthService } from '../better-auth.service';\nimport type { BetterAuthRuntimeOptions } from '../better-auth.types';\nimport { authenticateRequest, beginAuthRequest, getAuthRequestState } from '../auth-request-state';\nimport { validateSessionData } from '../session-data';\nimport { OptionalAuth } from '../decorators/optional-auth.decorator';\nimport { Public } from '../decorators/public.decorator';\n\n@Injectable()\nexport class AuthGuard implements CanActivate {\n constructor(\n @Inject(BetterAuthService) private readonly auth: BetterAuthService,\n @Inject(BETTER_AUTH_OPTIONS) private readonly opts: BetterAuthRuntimeOptions,\n @Inject(Reflector) private readonly reflector: Reflector,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n // Frames trust only the authenticated connection attachment; HTTP cookies\n // and request payload never become ambient WebSocket authentication.\n if (context.getType() === 'ws') {\n if (getContextIdentity(context)) return true;\n throw new UnauthorizedException('Authentication required');\n }\n // Custom HTTP-backed dispatchers authenticate once at the outer boundary.\n // Never re-read credentials or clear shared authority across sibling fields.\n if (context.getType() !== 'http') {\n if (\n getTrustedContextRequest(context) &&\n (getAuthRequestState(context) ||\n this.reflector.getAllAndOverride(Public, context) ||\n this.reflector.getAllAndOverride(OptionalAuth, context))\n )\n return true;\n throw new UnauthorizedException('Authentication required');\n }\n beginAuthRequest(context);\n if (this.reflector.getAllAndOverride(Public, context)) return true;\n\n const raw = await this.auth.api.getSession({ headers: context.getRequest().headers });\n const data = validateSessionData(raw);\n if (data) {\n authenticateRequest(context, data, this.opts.issuer ?? 'better-auth');\n return true;\n }\n if (this.reflector.getAllAndOverride(OptionalAuth, context)) return true;\n throw new UnauthorizedException('Authentication required');\n }\n}\n","import {\n defineProvider,\n lazyProvider,\n provideGlobal,\n stableHash,\n type DynamicModule,\n type FactoryInject,\n type InferTokens,\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 {\n BetterAuthInstance,\n BetterAuthModuleOptions,\n BetterAuthRuntimeOptions,\n} from './better-auth.types';\nimport { AuthGuard } from './guards/auth.guard';\nimport { normalizeBetterAuthBasePath } from './base-path';\n\nconst referenceIds = new WeakMap<object, number>();\nlet nextReferenceId = 1;\n\nfunction referenceId(reference: object): number {\n const existing = referenceIds.get(reference);\n if (existing !== undefined) return existing;\n const id = nextReferenceId++;\n referenceIds.set(reference, id);\n return id;\n}\n\n/**\n * An explicit key names a registration without widening its identity: the\n * options shape and auth reference stay in the module key, so the same\n * registration deduplicates within an application while a different one\n * (including a root rebuilt per environment) becomes its own instance. No\n * process-wide state is kept, so repeated bootstraps in one isolate succeed.\n */\nfunction explicitKey(key: string, shape: string, reference: object): string {\n if (key.length === 0 || key !== key.trim()) {\n throw new Error('@velajs/better-auth: an explicit module key must be a non-empty string');\n }\n return `explicit:${key}:${shape}:ref:${referenceId(reference)}`;\n}\n\n/** Structural options with defaults applied (everything but the auth instance). */\ninterface NormalizedOptions {\n basePath: string;\n issuer: string;\n isGlobal: boolean;\n mountHandler: boolean;\n}\n\nfunction normalize(options: BetterAuthRuntimeOptions): NormalizedOptions {\n const basePath = normalizeBetterAuthBasePath(options.basePath);\n const issuer = options.issuer ?? `better-auth:${basePath}`;\n if (issuer.length === 0 || issuer !== issuer.trim()) {\n throw new Error('@velajs/better-auth: issuer must be a non-empty stable namespace');\n }\n return {\n basePath,\n issuer,\n isGlobal: options.isGlobal ?? true,\n mountHandler: options.mountHandler ?? true,\n };\n}\n\n/** Providers, controllers, and exports shared by both entry points. */\nfunction commonContributions(n: NormalizedOptions): {\n providers: NonNullable<DynamicModule['providers']>;\n controllers: Type[];\n exports: DynamicModule['exports'];\n} {\n return {\n providers: [BetterAuthService, AuthGuard],\n controllers: n.mountHandler ? [createBetterAuthCatchallController(n.basePath)] : [],\n exports: [BetterAuthService, BETTER_AUTH_OPTIONS, AuthGuard],\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: [ENV, ConfigService], // captured as readonly tuple\n * useFactory: (env, config) => // inferred from the tokens\n * betterAuth({ database: drizzleAdapter(drizzle(env.DB), ...) }),\n * });\n * ```\n *\n * A factory without parameters may omit `inject`.\n */\ntype ForRootAsyncOptions<Inject extends readonly Token[] = readonly Token[]> = {\n imports?: DynamicModule['imports'];\n useFactory: (...deps: InferTokens<Inject>) => BetterAuthInstance;\n isGlobal?: boolean;\n mountHandler?: boolean;\n basePath?: string;\n issuer?: string;\n key?: string;\n} & FactoryInject<Inject>;\n\nexport class BetterAuthModule {\n /**\n * Synchronous registration. The auth instance is constructed by the consumer\n * at module-load time and passed in directly. Use this when the inputs to\n * `betterAuth({...})` are available at startup (Node apps with a static DB\n * connection, in-memory adapters, etc.).\n */\n static forRoot(\n options: BetterAuthModuleOptions & { isGlobal?: boolean; key?: string },\n ): DynamicModule {\n const normalized = normalize(options);\n const shape = stableHash(normalized);\n const key =\n options.key === undefined\n ? `${shape}:auth:${referenceId(options.auth)}`\n : explicitKey(options.key, shape, options.auth);\n const common = commonContributions(normalized);\n return {\n module: BetterAuthModule,\n key,\n providers: [\n defineProvider(BETTER_AUTH_OPTIONS, { useValue: normalized }),\n defineProvider(BETTER_AUTH_BUILDER, { useValue: () => options.auth }),\n ...common.providers,\n ...(normalized.isGlobal ? provideGlobal('guard', AuthGuard) : []),\n ],\n controllers: common.controllers,\n exports: common.exports,\n };\n }\n\n /**\n * Deferred / DI-driven registration. The user factory runs **lazily**, on the\n * first time anything reads `BetterAuthService.auth` (or `.api` / `.handler`).\n * In normal request handling that's `AuthGuard.canActivate` or the catch-all\n * controller's `.handle`. At module load the factory does NOT run — it's only\n * captured in the checked builder provider. The Workers adapter supplies its\n * native environment before DI; the lazily constructed auth instance belongs\n * to that environment's application and never captures another app's bindings.\n */\n static forRootAsync<const Inject extends readonly Token[] = readonly Token[]>(\n options: ForRootAsyncOptions<Inject>,\n ): DynamicModule {\n const n = normalize(options);\n const common = commonContributions(n);\n const shape = stableHash({ ...n, inject: options.inject });\n const key =\n options.key === undefined\n ? `${shape}:factory:${referenceId(options.useFactory)}`\n : explicitKey(options.key, shape, options.useFactory);\n return {\n module: BetterAuthModule,\n key,\n imports: options.imports ?? [],\n providers: [\n defineProvider(BETTER_AUTH_OPTIONS, { useValue: n }),\n // Unmemoized: BetterAuthService caches the instance it builds.\n lazyProvider<BetterAuthInstance, Inject>({\n ...options,\n provide: BETTER_AUTH_BUILDER,\n memoize: false,\n }),\n ...common.providers,\n ...(n.isGlobal ? provideGlobal('guard', AuthGuard) : []),\n ],\n controllers: common.controllers,\n exports: common.exports,\n };\n }\n}\n","import { Inject, Injectable, InjectionToken, Optional } from '@velajs/vela';\nimport type {\n UpgradeAuthenticator,\n WebSocketUpgradeAuthenticationContext,\n WebSocketUpgradeIdentity,\n} from '@velajs/vela/websocket';\nimport { BetterAuthService } from './better-auth.service';\nimport { BETTER_AUTH_OPTIONS } from './better-auth.tokens';\nimport type { BetterAuthRuntimeOptions, Session, User } from './better-auth.types';\nimport { validateSessionData } from './session-data';\n\n/** The validated Better Auth session behind a WebSocket upgrade. */\nexport interface BetterAuthUpgradeSession {\n readonly user: User;\n readonly session: Session;\n}\n\n/**\n * Chooses the tenant a WebSocket connection joins. Returning no tenant (or\n * throwing) refuses the upgrade.\n */\nexport type BetterAuthUpgradeTenantResolver = (\n session: BetterAuthUpgradeSession,\n context: WebSocketUpgradeAuthenticationContext,\n request: Request,\n) => string | undefined | Promise<string | undefined>;\n\n/**\n * Optional tenant resolver for {@link BetterAuthUpgradeAuthenticator}. Provide it\n * in the module that declares the gateway; without one, the session's active\n * organization is the tenant.\n */\nexport const BETTER_AUTH_UPGRADE_TENANT = new InjectionToken<BetterAuthUpgradeTenantResolver>(\n 'vela.better-auth.UpgradeTenant',\n);\n\n/**\n * WebSocket upgrade authenticator backed by the Better Auth session cookie:\n * `@WebSocketGateway({ authenticator: BetterAuthUpgradeAuthenticator })`.\n *\n * The principal uses the `BetterAuthModule` issuer, as `AuthGuard` does for\n * HTTP. The identity expires with the session. Every WebSocket identity carries\n * a tenant, so a session without an active organization is refused unless a\n * {@link BETTER_AUTH_UPGRADE_TENANT} resolver supplies one.\n */\n@Injectable()\nexport class BetterAuthUpgradeAuthenticator implements UpgradeAuthenticator {\n constructor(\n @Inject(BetterAuthService) private readonly auth: BetterAuthService,\n @Inject(BETTER_AUTH_OPTIONS) private readonly options: BetterAuthRuntimeOptions,\n @Optional()\n @Inject(BETTER_AUTH_UPGRADE_TENANT)\n private readonly resolveTenant?: BetterAuthUpgradeTenantResolver,\n ) {}\n\n async authenticate(\n request: Request,\n context: WebSocketUpgradeAuthenticationContext,\n ): Promise<WebSocketUpgradeIdentity | false> {\n const data = validateSessionData(await this.auth.api.getSession({ headers: request.headers }));\n if (!data) return false;\n const tenantId = this.resolveTenant\n ? await this.resolveTenant({ user: data.user, session: data.session }, context, request)\n : data.tenantId;\n if (typeof tenantId !== 'string' || tenantId.length === 0) return false;\n return {\n principal: {\n issuer: this.options.issuer ?? 'better-auth',\n subject: data.user.id,\n principalType: 'user',\n },\n tenantId,\n expiresAtMs: data.session.expiresAt.getTime(),\n };\n }\n}\n","import { createParamDecorator, type ExecutionContext } from '@velajs/vela';\nimport { getAuthRequestState } from '../auth-request-state';\nimport type { User } from '../better-auth.types';\n\nexport const CurrentUser = createParamDecorator(\n (_data: unknown, ctx: ExecutionContext): User | undefined => {\n const state = getAuthRequestState(ctx);\n return state?.user;\n },\n);\n","import { createParamDecorator, type ExecutionContext } from '@velajs/vela';\nimport { getAuthRequestState } from '../auth-request-state';\nimport type { Session } from '../better-auth.types';\n\nexport const CurrentSession = createParamDecorator(\n (_data: unknown, ctx: ExecutionContext): Session | undefined => {\n const state = getAuthRequestState(ctx);\n return state?.session;\n },\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 = Pick<User, 'id'> & { role?: string | string[] | null };\n\n/** Stable issuer namespace used for better-auth session principals. */\nexport const BETTER_AUTH_ISSUER = 'better-auth';\n\nconst normalizeRoles = (role: string | string[] | null | undefined): string[] => {\n if (!role) return [];\n // Strip empty entries and return a fresh array (never alias the caller's\n // input), matching the comma-string path below.\n if (Array.isArray(role)) return role.filter(Boolean);\n return role\n .split(',')\n .map((r) => r.trim())\n .filter(Boolean);\n};\n\n/**\n * Pure authorization projection; this does not authenticate or publish trusted state.\n * Adapts an already verified better-auth user into a stable `@velajs/authz` {@link Identity}.\n * The issuer scopes `user.id` as both `subject` and the compatibility `userId`;\n * the admin-plugin `role` field supplies local roles.\n *\n * Fail-closed: a missing user (`null`/`undefined`, i.e. an unauthenticated\n * request) maps to the zero-privilege identity `{ roles: [] }`, so downstream\n * `can()` checks grant nothing.\n */\nexport const identityFromUser = (\n user: AuthUser | null | undefined,\n issuer: string = BETTER_AUTH_ISSUER,\n principalType: 'user' | 'service' = 'user',\n): Identity => {\n if (!user || typeof user.id !== 'string' || user.id.length === 0) return { roles: [] };\n if (issuer.length === 0)\n throw new Error('@velajs/better-auth: identity issuer must be non-empty');\n return {\n issuer,\n subject: user.id,\n principalType,\n userId: user.id,\n roles: normalizeRoles(user.role),\n };\n};\n\n/**\n * The minimal slice of a better-auth access-control role consumed here. Both\n * `createAccessControl(...).newRole(...)` and the standalone `role(...)` return\n * `{ authorize, statements }`; `statements` is the `{ resource: actions[] }`\n * grant map for that role — the only accessor {@link betterAuthAcResolver}\n * reads.\n */\nexport interface BetterAuthAcRole {\n readonly statements: Readonly<Record<string, readonly string[]>>;\n}\n\n/**\n * Flattens a better-auth AC role's `statements` into `resource:action`\n * permission strings — the granted-side format `@velajs/authz` matches\n * (wildcards included).\n */\nexport const permissionsFromAcRole = (role: BetterAuthAcRole): string[] => {\n const permissions: string[] = [];\n for (const [resource, actions] of Object.entries(role.statements ?? {})) {\n for (const action of actions ?? []) permissions.push(`${resource}:${action}`);\n }\n return permissions;\n};\n\n/**\n * Builds a fail-closed `@velajs/authz` {@link PermissionResolver} from a\n * better-auth access-control role table (`{ roleName: acRole }` — the same map\n * shape passed to better-auth's admin/organization plugins). An identity's\n * `roles` are unioned into their granted permission strings; unknown roles\n * contribute nothing.\n *\n * ```ts\n * const ac = createAccessControl({ posts: ['read', 'write'] });\n * const authz = createAuthz({\n * resolver: betterAuthAcResolver({ editor: ac.newRole({ posts: ['write'] }) }),\n * });\n * await authz.can(identityFromUser(user), 'posts:write');\n * ```\n */\nexport const betterAuthAcResolver = (\n roles: Readonly<Record<string, BetterAuthAcRole>>,\n): PermissionResolver => {\n const grantsByRole = new Map<string, string[]>();\n for (const [name, role] of Object.entries(roles)) {\n grantsByRole.set(name, permissionsFromAcRole(role));\n }\n return {\n grants(identity: Identity): Set<string> {\n const out = new Set<string>();\n for (const name of identity.roles ?? []) {\n for (const permission of grantsByRole.get(name) ?? []) out.add(permission);\n }\n return out;\n },\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/decorators/public.decorator.ts","../src/base-path.ts","../src/better-auth.controller.ts","../src/better-auth.tokens.ts","../src/auth-request-state.ts","../src/session-data.ts","../src/decorators/optional-auth.decorator.ts","../src/guards/auth.guard.ts","../src/better-auth.module.ts","../src/upgrade-authenticator.ts","../src/decorators/current-user.decorator.ts","../src/decorators/current-session.decorator.ts","../src/authz-bridge.ts"],"sourcesContent":["import { Reflector } from '@velajs/vela';\n\nexport const Public = Reflector.createDecorator<boolean>({ key: 'vela.auth.public' });\nexport const PUBLIC_KEY = Public.KEY;\n","export const DEFAULT_BETTER_AUTH_BASE_PATH = '/api/auth';\n\n/** Validate and canonicalize the route prefix used by the public auth controller. */\nexport function normalizeBetterAuthBasePath(value?: string): string {\n const basePath = value ?? DEFAULT_BETTER_AUTH_BASE_PATH;\n if (\n basePath.length === 0 ||\n basePath !== basePath.trim() ||\n !basePath.startsWith('/') ||\n basePath.startsWith('//') ||\n basePath === '/' ||\n basePath.endsWith('/') ||\n /[\\\\?#*]/u.test(basePath) ||\n /%(?:2e|2f|5c)/iu.test(basePath)\n ) {\n throw new Error(\n '@velajs/better-auth: basePath must be a canonical absolute path such as \"/api/auth\"',\n );\n }\n\n let decoded: string;\n try {\n decoded = decodeURIComponent(basePath);\n } catch {\n throw new Error('@velajs/better-auth: basePath contains invalid percent encoding');\n }\n if (decoded.split('/').some((segment) => segment === '.' || segment === '..')) {\n throw new Error('@velajs/better-auth: basePath must not contain dot segments');\n }\n return basePath;\n}\n","import { All, Controller, Inject, Req, type Type } from '@velajs/vela';\nimport type { Context } from 'hono';\nimport { BetterAuthService } from './better-auth.service';\nimport { Public } from './decorators/public.decorator';\nimport { normalizeBetterAuthBasePath } from './base-path';\n\n/**\n * Build a catch-all controller that mounts better-auth's handler at `basePath`\n * (default `/api/auth`). This is a factory because vela reads a controller's\n * route off the class at decoration time, so a custom base path needs its own\n * decorated class — the path can't be parametrized on a single shared class.\n *\n * Two base paths to keep consistent:\n * - this `basePath` is RELATIVE to vela's `globalPrefix` (always prepended);\n * - better-auth routes against its OWN absolute `basePath` (the one you pass to\n * `betterAuth({ basePath })`), which must equal `globalPrefix + basePath`.\n *\n * Both default to `/api/auth`, so the no-prefix / no-config case just works.\n */\nexport function createBetterAuthCatchallController(basePath: string = '/api/auth'): Type {\n const normalizedBasePath = normalizeBetterAuthBasePath(basePath);\n @Public(true)\n @Controller(normalizedBasePath)\n 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","import { InjectionToken } from '@velajs/vela';\nimport type { BetterAuthRuntimeOptions } from './better-auth.types';\n\nexport const BETTER_AUTH_OPTIONS = new InjectionToken<BetterAuthRuntimeOptions>(\n 'vela.BetterAuthOptions',\n);\n","import type { ExecutionContext } from '@velajs/vela';\nimport {\n clearTrustedRequestIdentity,\n createTrustedRequestIdentityStore,\n getTrustedContextRequest,\n setTrustedRequestIdentity,\n} from '@velajs/vela/module-kit';\nimport type { SessionData } from './session-data';\n\n// This map stores provider payload, never a second authentication authority.\n// A clear, expiry, or identity replacement makes the payload unreachable.\nconst sessions = createTrustedRequestIdentityStore<SessionData>();\n\nexport function beginAuthRequest(context: ExecutionContext): void {\n clearTrustedRequestIdentity(context.getRequest());\n}\n\nexport function authenticateRequest(\n context: ExecutionContext,\n data: SessionData,\n issuer: string,\n): void {\n const request = context.getRequest();\n setTrustedRequestIdentity(request, {\n principal: { issuer, subject: data.user.id, principalType: 'user' },\n expiresAtMs: data.session.expiresAt.getTime(),\n roles: data.roles,\n ...(data.tenantId === undefined ? {} : { tenantId: data.tenantId }),\n });\n sessions.set(request, data);\n}\n\nexport function getAuthRequestState(context: ExecutionContext): SessionData | undefined {\n const request = getTrustedContextRequest(context);\n return request ? sessions.get(request) : undefined;\n}\n","import type { Session, User } from './better-auth.types';\n\nexport interface SessionData {\n readonly user: User;\n readonly session: Session;\n readonly roles: readonly string[];\n readonly tenantId?: string;\n}\n\nfunction own(value: unknown, key: string): unknown {\n if (typeof value !== 'object' || value === null) return undefined;\n const descriptor = Object.getOwnPropertyDescriptor(value, key);\n return descriptor && 'value' in descriptor ? descriptor.value : undefined;\n}\n\nfunction requiredString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0;\n}\n\nfunction date(value: unknown): Date | undefined {\n return value instanceof Date && Number.isFinite(value.getTime()) ? new Date(value) : undefined;\n}\n\nfunction optionalString(value: unknown): value is string | null | undefined {\n return value === undefined || value === null || typeof value === 'string';\n}\n\n/** Keep plugin data opaque; base model fields below are validated individually. */\nfunction dataProperties(value: object): Record<string, unknown> {\n return Object.fromEntries(Object.keys(value).map((key) => [key, own(value, key)]));\n}\n\n/** Validate the full Better Auth base models, not merely ids masquerading as them. */\nexport function validateSessionData(value: unknown): SessionData | undefined {\n try {\n const user = own(value, 'user');\n const session = own(value, 'session');\n if (\n typeof user !== 'object' ||\n user === null ||\n typeof session !== 'object' ||\n session === null\n ) {\n return undefined;\n }\n const id = own(user, 'id');\n const email = own(user, 'email');\n const name = own(user, 'name');\n const emailVerified = own(user, 'emailVerified');\n const image = own(user, 'image');\n const createdAt = date(own(user, 'createdAt'));\n const updatedAt = date(own(user, 'updatedAt'));\n const sessionId = own(session, 'id');\n const userId = own(session, 'userId');\n const token = own(session, 'token');\n const sessionCreatedAt = date(own(session, 'createdAt'));\n const sessionUpdatedAt = date(own(session, 'updatedAt'));\n const expiresAt = date(own(session, 'expiresAt'));\n const ipAddress = own(session, 'ipAddress');\n const userAgent = own(session, 'userAgent');\n const organization = own(session, 'activeOrganizationId');\n const role = own(user, 'role');\n let roles: string[] = [];\n if (role !== undefined && role !== null) {\n if (typeof role === 'string')\n roles = role\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean);\n else if (\n Array.isArray(role) &&\n role.every((entry: unknown): entry is string => typeof entry === 'string')\n ) {\n roles = role.map((entry: string) => entry.trim()).filter(Boolean);\n } else return undefined;\n }\n if (\n !requiredString(id) ||\n !requiredString(email) ||\n typeof name !== 'string' ||\n typeof emailVerified !== 'boolean' ||\n !optionalString(image) ||\n !createdAt ||\n !updatedAt ||\n !requiredString(sessionId) ||\n userId !== id ||\n !requiredString(token) ||\n !sessionCreatedAt ||\n !sessionUpdatedAt ||\n !expiresAt ||\n expiresAt.getTime() <= Date.now() ||\n !optionalString(ipAddress) ||\n !optionalString(userAgent) ||\n (organization !== undefined && organization !== null && !requiredString(organization))\n )\n return undefined;\n return {\n user: Object.freeze({\n ...dataProperties(user),\n id,\n email,\n name,\n emailVerified,\n createdAt,\n updatedAt,\n image,\n }),\n session: Object.freeze({\n ...dataProperties(session),\n id: sessionId,\n userId: id,\n token,\n createdAt: sessionCreatedAt,\n updatedAt: sessionUpdatedAt,\n expiresAt,\n ipAddress,\n userAgent,\n }),\n roles: Object.freeze(roles),\n ...(typeof organization === 'string' ? { tenantId: organization } : {}),\n };\n } catch {\n return undefined;\n }\n}\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 Reflector,\n UnauthorizedException,\n type CanActivate,\n type ExecutionContext,\n} from '@velajs/vela';\nimport { getTrustedContextRequest } from '@velajs/vela/module-kit';\nimport { getContextIdentity } from '@velajs/authz/vela';\nimport { BETTER_AUTH_OPTIONS } from '../better-auth.tokens';\nimport { BetterAuthService } from '../better-auth.service';\nimport type { BetterAuthRuntimeOptions } from '../better-auth.types';\nimport { authenticateRequest, beginAuthRequest, getAuthRequestState } from '../auth-request-state';\nimport { validateSessionData } from '../session-data';\nimport { OptionalAuth } from '../decorators/optional-auth.decorator';\nimport { Public } from '../decorators/public.decorator';\n\n@Injectable()\nexport class AuthGuard implements CanActivate {\n constructor(\n @Inject(BetterAuthService) private readonly auth: BetterAuthService,\n @Inject(BETTER_AUTH_OPTIONS) private readonly opts: BetterAuthRuntimeOptions,\n @Inject(Reflector) private readonly reflector: Reflector,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n // Frames trust only the authenticated connection attachment; HTTP cookies\n // and request payload never become ambient WebSocket authentication.\n if (context.getType() === 'ws') {\n if (getContextIdentity(context)) return true;\n throw new UnauthorizedException('Authentication required');\n }\n // Custom HTTP-backed dispatchers authenticate once at the outer boundary.\n // Never re-read credentials or clear shared authority across sibling fields.\n if (context.getType() !== 'http') {\n if (\n getTrustedContextRequest(context) &&\n (getAuthRequestState(context) ||\n this.reflector.getAllAndOverride(Public, context) ||\n this.reflector.getAllAndOverride(OptionalAuth, context))\n )\n return true;\n throw new UnauthorizedException('Authentication required');\n }\n beginAuthRequest(context);\n if (this.reflector.getAllAndOverride(Public, context)) return true;\n\n const raw = await this.auth.api.getSession({ headers: context.getRequest().headers });\n const data = validateSessionData(raw);\n if (data) {\n authenticateRequest(context, data, this.opts.issuer ?? 'better-auth');\n return true;\n }\n if (this.reflector.getAllAndOverride(OptionalAuth, context)) return true;\n throw new UnauthorizedException('Authentication required');\n }\n}\n","import { defineProvider, type DynamicModule, type Token, type Type } from '@velajs/vela';\nimport {\n lazyProvider,\n provideGlobal,\n stableHash,\n type FactoryInject,\n type InferTokens,\n} from '@velajs/vela/module-kit';\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 {\n BetterAuthInstance,\n BetterAuthModuleOptions,\n BetterAuthRuntimeOptions,\n} from './better-auth.types';\nimport { AuthGuard } from './guards/auth.guard';\nimport { normalizeBetterAuthBasePath } from './base-path';\n\nconst referenceIds = new WeakMap<object, number>();\nlet nextReferenceId = 1;\n\nfunction referenceId(reference: object): number {\n const existing = referenceIds.get(reference);\n if (existing !== undefined) return existing;\n const id = nextReferenceId++;\n referenceIds.set(reference, id);\n return id;\n}\n\n/**\n * An explicit key names a registration without widening its identity: the\n * options shape and auth reference stay in the module key, so the same\n * registration deduplicates within an application while a different one\n * (including a root rebuilt per environment) becomes its own instance. No\n * process-wide state is kept, so repeated bootstraps in one isolate succeed.\n */\nfunction explicitKey(key: string, shape: string, reference: object): string {\n if (key.length === 0 || key !== key.trim()) {\n throw new Error('@velajs/better-auth: an explicit module key must be a non-empty string');\n }\n return `explicit:${key}:${shape}:ref:${referenceId(reference)}`;\n}\n\n/** Structural options with defaults applied (everything but the auth instance). */\ninterface NormalizedOptions {\n basePath: string;\n issuer: string;\n isGlobal: boolean;\n mountHandler: boolean;\n}\n\nfunction normalize(options: BetterAuthRuntimeOptions): NormalizedOptions {\n const basePath = normalizeBetterAuthBasePath(options.basePath);\n const issuer = options.issuer ?? `better-auth:${basePath}`;\n if (issuer.length === 0 || issuer !== issuer.trim()) {\n throw new Error('@velajs/better-auth: issuer must be a non-empty stable namespace');\n }\n return {\n basePath,\n issuer,\n isGlobal: options.isGlobal ?? true,\n mountHandler: options.mountHandler ?? true,\n };\n}\n\n/** Providers, controllers, and exports shared by both entry points. */\nfunction commonContributions(n: NormalizedOptions): {\n providers: NonNullable<DynamicModule['providers']>;\n controllers: Type[];\n exports: DynamicModule['exports'];\n} {\n return {\n providers: [BetterAuthService, AuthGuard],\n controllers: n.mountHandler ? [createBetterAuthCatchallController(n.basePath)] : [],\n exports: [BetterAuthService, BETTER_AUTH_OPTIONS, AuthGuard],\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: [ENV, ConfigService], // captured as readonly tuple\n * useFactory: (env, config) => // inferred from the tokens\n * betterAuth({ database: drizzleAdapter(drizzle(env.DB), ...) }),\n * });\n * ```\n *\n * A factory without parameters may omit `inject`.\n */\ntype ForRootAsyncOptions<Inject extends readonly Token[] = readonly Token[]> = {\n imports?: DynamicModule['imports'];\n useFactory: (...deps: InferTokens<Inject>) => BetterAuthInstance;\n isGlobal?: boolean;\n mountHandler?: boolean;\n basePath?: string;\n issuer?: string;\n key?: string;\n} & FactoryInject<Inject>;\n\nexport class BetterAuthModule {\n /**\n * Synchronous registration. The auth instance is constructed by the consumer\n * at module-load time and passed in directly. Use this when the inputs to\n * `betterAuth({...})` are available at startup (Node apps with a static DB\n * connection, in-memory adapters, etc.).\n */\n static forRoot(\n options: BetterAuthModuleOptions & { isGlobal?: boolean; key?: string },\n ): DynamicModule {\n const normalized = normalize(options);\n const shape = stableHash(normalized);\n const key =\n options.key === undefined\n ? `${shape}:auth:${referenceId(options.auth)}`\n : explicitKey(options.key, shape, options.auth);\n const common = commonContributions(normalized);\n return {\n module: BetterAuthModule,\n key,\n providers: [\n defineProvider(BETTER_AUTH_OPTIONS, { useValue: normalized }),\n defineProvider(BETTER_AUTH_BUILDER, { useValue: () => options.auth }),\n ...common.providers,\n ...(normalized.isGlobal ? provideGlobal('guard', AuthGuard) : []),\n ],\n controllers: common.controllers,\n exports: common.exports,\n };\n }\n\n /**\n * Deferred / DI-driven registration. The user factory runs **lazily**, on the\n * first time anything reads `BetterAuthService.auth` (or `.api` / `.handler`).\n * In normal request handling that's `AuthGuard.canActivate` or the catch-all\n * controller's `.handle`. At module load the factory does NOT run — it's only\n * captured in the checked builder provider. The Workers adapter supplies its\n * native environment before DI; the lazily constructed auth instance belongs\n * to that environment's application and never captures another app's bindings.\n */\n static forRootAsync<const Inject extends readonly Token[] = readonly Token[]>(\n options: ForRootAsyncOptions<Inject>,\n ): DynamicModule {\n const n = normalize(options);\n const common = commonContributions(n);\n const shape = stableHash({ ...n, inject: options.inject });\n const key =\n options.key === undefined\n ? `${shape}:factory:${referenceId(options.useFactory)}`\n : explicitKey(options.key, shape, options.useFactory);\n return {\n module: BetterAuthModule,\n key,\n imports: options.imports ?? [],\n providers: [\n defineProvider(BETTER_AUTH_OPTIONS, { useValue: n }),\n // Unmemoized: BetterAuthService caches the instance it builds.\n lazyProvider<BetterAuthInstance, Inject>({\n ...options,\n provide: BETTER_AUTH_BUILDER,\n memoize: false,\n }),\n ...common.providers,\n ...(n.isGlobal ? provideGlobal('guard', AuthGuard) : []),\n ],\n controllers: common.controllers,\n exports: common.exports,\n };\n }\n}\n","import { Inject, Injectable, InjectionToken, Optional } from '@velajs/vela';\nimport type {\n UpgradeAuthenticator,\n WebSocketUpgradeAuthenticationContext,\n WebSocketUpgradeIdentity,\n} from '@velajs/vela/websocket';\nimport { BetterAuthService } from './better-auth.service';\nimport { BETTER_AUTH_OPTIONS } from './better-auth.tokens';\nimport type { BetterAuthRuntimeOptions, Session, User } from './better-auth.types';\nimport { validateSessionData } from './session-data';\n\n/** The validated Better Auth session behind a WebSocket upgrade. */\nexport interface BetterAuthUpgradeSession {\n readonly user: User;\n readonly session: Session;\n}\n\n/**\n * Chooses the tenant a WebSocket connection joins. Returning no tenant (or\n * throwing) refuses the upgrade.\n */\nexport type BetterAuthUpgradeTenantResolver = (\n session: BetterAuthUpgradeSession,\n context: WebSocketUpgradeAuthenticationContext,\n request: Request,\n) => string | undefined | Promise<string | undefined>;\n\n/**\n * Optional tenant resolver for {@link BetterAuthUpgradeAuthenticator}. Provide it\n * in the module that declares the gateway; without one, the session's active\n * organization is the tenant.\n */\nexport const BETTER_AUTH_UPGRADE_TENANT = new InjectionToken<BetterAuthUpgradeTenantResolver>(\n 'vela.better-auth.UpgradeTenant',\n);\n\n/**\n * WebSocket upgrade authenticator backed by the Better Auth session cookie:\n * `@WebSocketGateway({ authenticator: BetterAuthUpgradeAuthenticator })`.\n *\n * The principal uses the `BetterAuthModule` issuer, as `AuthGuard` does for\n * HTTP. The identity expires with the session. Every WebSocket identity carries\n * a tenant, so a session without an active organization is refused unless a\n * {@link BETTER_AUTH_UPGRADE_TENANT} resolver supplies one.\n */\n@Injectable()\nexport class BetterAuthUpgradeAuthenticator implements UpgradeAuthenticator {\n constructor(\n @Inject(BetterAuthService) private readonly auth: BetterAuthService,\n @Inject(BETTER_AUTH_OPTIONS) private readonly options: BetterAuthRuntimeOptions,\n @Optional()\n @Inject(BETTER_AUTH_UPGRADE_TENANT)\n private readonly resolveTenant?: BetterAuthUpgradeTenantResolver,\n ) {}\n\n async authenticate(\n request: Request,\n context: WebSocketUpgradeAuthenticationContext,\n ): Promise<WebSocketUpgradeIdentity | false> {\n const data = validateSessionData(await this.auth.api.getSession({ headers: request.headers }));\n if (!data) return false;\n const tenantId = this.resolveTenant\n ? await this.resolveTenant({ user: data.user, session: data.session }, context, request)\n : data.tenantId;\n if (typeof tenantId !== 'string' || tenantId.length === 0) return false;\n return {\n principal: {\n issuer: this.options.issuer ?? 'better-auth',\n subject: data.user.id,\n principalType: 'user',\n },\n tenantId,\n expiresAtMs: data.session.expiresAt.getTime(),\n };\n }\n}\n","import { createParamDecorator, type ExecutionContext } from '@velajs/vela';\nimport { getAuthRequestState } from '../auth-request-state';\nimport type { User } from '../better-auth.types';\n\nexport const CurrentUser = createParamDecorator(\n (_data: unknown, ctx: ExecutionContext): User | undefined => {\n const state = getAuthRequestState(ctx);\n return state?.user;\n },\n);\n","import { createParamDecorator, type ExecutionContext } from '@velajs/vela';\nimport { getAuthRequestState } from '../auth-request-state';\nimport type { Session } from '../better-auth.types';\n\nexport const CurrentSession = createParamDecorator(\n (_data: unknown, ctx: ExecutionContext): Session | undefined => {\n const state = getAuthRequestState(ctx);\n return state?.session;\n },\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 = Pick<User, 'id'> & { role?: string | string[] | null };\n\n/** Stable issuer namespace used for better-auth session principals. */\nexport const BETTER_AUTH_ISSUER = 'better-auth';\n\nconst normalizeRoles = (role: string | string[] | null | undefined): string[] => {\n if (!role) return [];\n // Strip empty entries and return a fresh array (never alias the caller's\n // input), matching the comma-string path below.\n if (Array.isArray(role)) return role.filter(Boolean);\n return role\n .split(',')\n .map((r) => r.trim())\n .filter(Boolean);\n};\n\n/**\n * Pure authorization projection; this does not authenticate or publish trusted state.\n * Adapts an already verified better-auth user into a stable `@velajs/authz` {@link Identity}.\n * The issuer scopes `user.id` as both `subject` and the compatibility `userId`;\n * the admin-plugin `role` field supplies local roles.\n *\n * Fail-closed: a missing user (`null`/`undefined`, i.e. an unauthenticated\n * request) maps to the zero-privilege identity `{ roles: [] }`, so downstream\n * `can()` checks grant nothing.\n */\nexport const identityFromUser = (\n user: AuthUser | null | undefined,\n issuer: string = BETTER_AUTH_ISSUER,\n principalType: 'user' | 'service' = 'user',\n): Identity => {\n if (!user || typeof user.id !== 'string' || user.id.length === 0) return { roles: [] };\n if (issuer.length === 0)\n throw new Error('@velajs/better-auth: identity issuer must be non-empty');\n return {\n issuer,\n subject: user.id,\n principalType,\n userId: user.id,\n roles: normalizeRoles(user.role),\n };\n};\n\n/**\n * The minimal slice of a better-auth access-control role consumed here. Both\n * `createAccessControl(...).newRole(...)` and the standalone `role(...)` return\n * `{ authorize, statements }`; `statements` is the `{ resource: actions[] }`\n * grant map for that role — the only accessor {@link betterAuthAcResolver}\n * reads.\n */\nexport interface BetterAuthAcRole {\n readonly statements: Readonly<Record<string, readonly string[]>>;\n}\n\n/**\n * Flattens a better-auth AC role's `statements` into `resource:action`\n * permission strings — the granted-side format `@velajs/authz` matches\n * (wildcards included).\n */\nexport const permissionsFromAcRole = (role: BetterAuthAcRole): string[] => {\n const permissions: string[] = [];\n for (const [resource, actions] of Object.entries(role.statements ?? {})) {\n for (const action of actions ?? []) permissions.push(`${resource}:${action}`);\n }\n return permissions;\n};\n\n/**\n * Builds a fail-closed `@velajs/authz` {@link PermissionResolver} from a\n * better-auth access-control role table (`{ roleName: acRole }` — the same map\n * shape passed to better-auth's admin/organization plugins). An identity's\n * `roles` are unioned into their granted permission strings; unknown roles\n * contribute nothing.\n *\n * ```ts\n * const ac = createAccessControl({ posts: ['read', 'write'] });\n * const authz = createAuthz({\n * resolver: betterAuthAcResolver({ editor: ac.newRole({ posts: ['write'] }) }),\n * });\n * await authz.can(identityFromUser(user), 'posts:write');\n * ```\n */\nexport const betterAuthAcResolver = (\n roles: Readonly<Record<string, BetterAuthAcRole>>,\n): PermissionResolver => {\n const grantsByRole = new Map<string, string[]>();\n for (const [name, role] of Object.entries(roles)) {\n grantsByRole.set(name, permissionsFromAcRole(role));\n }\n return {\n grants(identity: Identity): Set<string> {\n const out = new Set<string>();\n for (const name of identity.roles ?? []) {\n for (const permission of grantsByRole.get(name) ?? []) out.add(permission);\n }\n return out;\n },\n };\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velajs/better-auth",
3
- "version": "1.29.0",
3
+ "version": "1.30.0",
4
4
  "description": "better-auth integration for the Vela framework",
5
5
  "keywords": [
6
6
  "auth",
@@ -47,7 +47,7 @@
47
47
  "access": "public"
48
48
  },
49
49
  "dependencies": {
50
- "@velajs/authz": "1.29.0"
50
+ "@velajs/authz": "1.30.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@arethetypeswrong/cli": "0.18.5",
@@ -63,14 +63,14 @@
63
63
  "unplugin-swc": "1.5.9",
64
64
  "vite": "^8.0.16",
65
65
  "vitest": "4.1.11",
66
- "@velajs/tenant": "1.29.0",
67
- "@velajs/vela": "1.29.0",
68
- "@velajs/testing": "1.29.0"
66
+ "@velajs/tenant": "1.30.0",
67
+ "@velajs/vela": "1.30.0",
68
+ "@velajs/testing": "1.30.0"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "better-auth": ">=1.2.0",
72
72
  "hono": ">=4",
73
- "@velajs/vela": "^1.29.0"
73
+ "@velajs/vela": "^1.30.0"
74
74
  },
75
75
  "engines": {
76
76
  "node": ">=24"