@lenne.tech/nest-server 11.13.1 → 11.13.2

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 (38) hide show
  1. package/dist/config.env.js +12 -3
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/interfaces/server-options.interface.d.ts +3 -0
  4. package/dist/core/modules/auth/core-auth.module.js +5 -2
  5. package/dist/core/modules/auth/core-auth.module.js.map +1 -1
  6. package/dist/core/modules/auth/guards/roles.guard.d.ts +2 -0
  7. package/dist/core/modules/auth/guards/roles.guard.js +23 -2
  8. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  9. package/dist/core/modules/auth/tokens.decorator.d.ts +1 -1
  10. package/dist/core/modules/better-auth/better-auth-roles.guard.d.ts +10 -0
  11. package/dist/core/modules/better-auth/better-auth-roles.guard.js +136 -0
  12. package/dist/core/modules/better-auth/better-auth-roles.guard.js.map +1 -0
  13. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +3 -0
  14. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
  15. package/dist/core/modules/better-auth/core-better-auth.module.d.ts +13 -1
  16. package/dist/core/modules/better-auth/core-better-auth.module.js +54 -9
  17. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  18. package/dist/core/modules/better-auth/index.d.ts +1 -0
  19. package/dist/core/modules/better-auth/index.js +1 -0
  20. package/dist/core/modules/better-auth/index.js.map +1 -1
  21. package/dist/core.module.d.ts +1 -0
  22. package/dist/core.module.js +82 -2
  23. package/dist/core.module.js.map +1 -1
  24. package/dist/server/modules/error-code/error-codes.d.ts +1 -1
  25. package/dist/tsconfig.build.tsbuildinfo +1 -1
  26. package/package.json +1 -1
  27. package/src/config.env.ts +12 -3
  28. package/src/core/common/interfaces/server-options.interface.ts +39 -0
  29. package/src/core/modules/auth/core-auth.module.ts +9 -3
  30. package/src/core/modules/auth/guards/roles.guard.ts +40 -5
  31. package/src/core/modules/better-auth/CUSTOMIZATION.md +520 -0
  32. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +15 -0
  33. package/src/core/modules/better-auth/README.md +24 -1
  34. package/src/core/modules/better-auth/better-auth-roles.guard.ts +205 -0
  35. package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +6 -0
  36. package/src/core/modules/better-auth/core-better-auth.module.ts +86 -9
  37. package/src/core/modules/better-auth/index.ts +1 -0
  38. package/src/core.module.ts +120 -2
@@ -0,0 +1,205 @@
1
+ import { CanActivate, ExecutionContext, ForbiddenException, Injectable, Logger, UnauthorizedException } from '@nestjs/common';
2
+ import { GqlExecutionContext } from '@nestjs/graphql';
3
+
4
+ import { RoleEnum } from '../../common/enums/role.enum';
5
+ import { ErrorCode } from '../error-code';
6
+ import { BetterAuthTokenService } from './better-auth-token.service';
7
+ import { BetterAuthenticatedUser } from './better-auth.types';
8
+ import { CoreBetterAuthModule } from './core-better-auth.module';
9
+
10
+ /**
11
+ * BetterAuth Roles Guard
12
+ *
13
+ * A simplified roles guard for BetterAuth (IAM-only) mode that does NOT extend AuthGuard.
14
+ * This avoids the mixin inheritance DI issues that occur with the standard RolesGuard.
15
+ *
16
+ * In IAM-only mode, authentication is handled by CoreBetterAuthMiddleware which:
17
+ * 1. Validates JWT tokens or session tokens
18
+ * 2. Sets req.user with the authenticated user (with _authenticatedViaBetterAuth flag)
19
+ *
20
+ * If the middleware hasn't set req.user (e.g., in test environments), this guard will
21
+ * try to verify the token directly using BetterAuthTokenService.
22
+ *
23
+ * No Passport integration is needed because BetterAuth handles all token validation.
24
+ *
25
+ * IMPORTANT: This guard has NO constructor dependencies. This is intentional because
26
+ * NestJS DI has issues resolving Reflector/ModuleRef for APP_GUARD providers in dynamic modules.
27
+ * Instead, we use Reflect.getMetadata directly to read decorator metadata, and access
28
+ * BetterAuthTokenService via CoreBetterAuthModule static reference.
29
+ */
30
+ @Injectable()
31
+ export class BetterAuthRolesGuard implements CanActivate {
32
+ private readonly logger = new Logger(BetterAuthRolesGuard.name);
33
+ private tokenService: BetterAuthTokenService | null = null;
34
+
35
+ /**
36
+ * Get BetterAuthTokenService lazily from CoreBetterAuthModule
37
+ * This avoids DI issues while still allowing token verification
38
+ */
39
+ private getTokenService(): BetterAuthTokenService | null {
40
+ if (!this.tokenService) {
41
+ this.tokenService = CoreBetterAuthModule.getTokenServiceInstance();
42
+ }
43
+ return this.tokenService;
44
+ }
45
+
46
+ /**
47
+ * Try to verify a BetterAuth token if user isn't already on the request.
48
+ * This handles cases where middleware didn't run (e.g., test environments).
49
+ */
50
+ private async verifyToken(request: any): Promise<BetterAuthenticatedUser | null> {
51
+ const tokenService = this.getTokenService();
52
+ if (!tokenService) {
53
+ return null;
54
+ }
55
+
56
+ try {
57
+ const { token } = tokenService.extractTokenFromRequest(request);
58
+ if (!token) {
59
+ return null;
60
+ }
61
+ return await tokenService.verifyAndLoadUser(token);
62
+ } catch (error) {
63
+ this.logger.debug(`Token verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
64
+ return null;
65
+ }
66
+ }
67
+
68
+ async canActivate(context: ExecutionContext): Promise<boolean> {
69
+ // Get roles from decorator metadata using Reflect.getMetadata directly
70
+ // This avoids the need for NestJS Reflector which causes DI issues in dynamic modules
71
+ const handlerRoles = Reflect.getMetadata('roles', context.getHandler()) as string[] | undefined;
72
+ const classRoles = Reflect.getMetadata('roles', context.getClass()) as string[] | undefined;
73
+
74
+ // Combine handler and class roles (handler takes precedence, like Reflector.getAll)
75
+ const reflectorRoles: (string[] | undefined)[] = [handlerRoles, classRoles];
76
+ const roles: string[] = reflectorRoles[0]
77
+ ? reflectorRoles[1]
78
+ ? [...reflectorRoles[0], ...reflectorRoles[1]]
79
+ : reflectorRoles[0]
80
+ : reflectorRoles[1];
81
+
82
+ // Check if locked - always deny
83
+ if (roles && roles.includes(RoleEnum.S_NO_ONE)) {
84
+ throw new UnauthorizedException(ErrorCode.UNAUTHORIZED);
85
+ }
86
+
87
+ // If no roles required, or S_EVERYONE is set, allow access without authentication
88
+ if (!roles || !roles.some((value) => !!value) || roles.includes(RoleEnum.S_EVERYONE)) {
89
+ return true;
90
+ }
91
+
92
+ // Get request and check for user (set by BetterAuth middleware)
93
+ const request = this.getRequest(context);
94
+ let user = request?.user;
95
+
96
+ // If user isn't set (e.g., middleware didn't run in test environment),
97
+ // try to verify the token directly
98
+ if (!user) {
99
+ user = await this.verifyToken(request);
100
+ if (user && request) {
101
+ // Store the verified user on the request for downstream handlers
102
+ request.user = user;
103
+ }
104
+ }
105
+
106
+ // Check if user is authenticated
107
+ if (!user) {
108
+ throw new UnauthorizedException(ErrorCode.UNAUTHORIZED);
109
+ }
110
+
111
+ // Check S_USER role - any authenticated user is allowed
112
+ if (roles.includes(RoleEnum.S_USER)) {
113
+ return true;
114
+ }
115
+
116
+ // Check S_SELF role - user is accessing their own data
117
+ if (roles.includes(RoleEnum.S_SELF)) {
118
+ // Get the target object's ID from params or args
119
+ const targetId = this.getTargetId(context);
120
+ if (targetId && user.id === targetId) {
121
+ return true;
122
+ }
123
+ }
124
+
125
+ // Check S_CREATOR role - user created the object
126
+ if (roles.includes(RoleEnum.S_CREATOR)) {
127
+ // This requires the object to have a createdBy field
128
+ // Usually checked in services, but we can't access the object here
129
+ // Let it pass and check in the service/resolver
130
+ this.logger.debug('S_CREATOR check deferred to service layer');
131
+ }
132
+
133
+ // Check S_VERIFIED role - user's email is verified
134
+ if (roles.includes(RoleEnum.S_VERIFIED)) {
135
+ if (!user.verified && !user.verifiedAt && !user.emailVerified) {
136
+ throw new ForbiddenException(ErrorCode.ACCESS_DENIED);
137
+ }
138
+ return true;
139
+ }
140
+
141
+ // Check if user has required role
142
+ if (user.hasRole?.(roles)) {
143
+ return true;
144
+ }
145
+
146
+ // Check if user's roles array includes any of the required roles
147
+ if (user.roles && Array.isArray(user.roles)) {
148
+ const hasRequiredRole = roles.some((role) => user.roles.includes(role));
149
+ if (hasRequiredRole) {
150
+ return true;
151
+ }
152
+ }
153
+
154
+ // User doesn't have required role
155
+ throw new ForbiddenException(ErrorCode.ACCESS_DENIED);
156
+ }
157
+
158
+ /**
159
+ * Get request from execution context
160
+ * Handles both GraphQL and HTTP contexts
161
+ */
162
+ private getRequest(context: ExecutionContext): any {
163
+ // Try GraphQL context first
164
+ try {
165
+ const gqlContext = GqlExecutionContext.create(context);
166
+ const ctx = gqlContext.getContext();
167
+ if (ctx?.req) {
168
+ return ctx.req;
169
+ }
170
+ } catch {
171
+ // GraphQL context not available
172
+ }
173
+
174
+ // Fallback to HTTP context
175
+ return context.switchToHttp().getRequest();
176
+ }
177
+
178
+ /**
179
+ * Get target ID from context for S_SELF checks
180
+ */
181
+ private getTargetId(context: ExecutionContext): null | string {
182
+ // Try GraphQL args
183
+ try {
184
+ const gqlContext = GqlExecutionContext.create(context);
185
+ const args = gqlContext.getArgs();
186
+ if (args?.id) {
187
+ return args.id;
188
+ }
189
+ } catch {
190
+ // GraphQL context not available
191
+ }
192
+
193
+ // Try HTTP params
194
+ try {
195
+ const request = context.switchToHttp().getRequest();
196
+ if (request?.params?.id) {
197
+ return request.params.id;
198
+ }
199
+ } catch {
200
+ // HTTP context not available
201
+ }
202
+
203
+ return null;
204
+ }
205
+ }
@@ -332,6 +332,12 @@ export class CoreBetterAuthApiMiddleware implements NestMiddleware {
332
332
  }
333
333
  }
334
334
 
335
+ // If Better Auth returned 404, the path is not handled by Better Auth.
336
+ // Call next() to let NestJS controllers handle it (e.g., custom controller endpoints).
337
+ if (response.status === 404) {
338
+ return next();
339
+ }
340
+
335
341
  // Convert Web Standard Response to Express response using shared helper
336
342
  await sendWebResponse(res, response);
337
343
  } catch (error) {
@@ -17,7 +17,7 @@ import { IBetterAuth } from '../../common/interfaces/server-options.interface';
17
17
  import { BrevoService } from '../../common/services/brevo.service';
18
18
  import { ConfigService } from '../../common/services/config.service';
19
19
  import { RolesGuardRegistry } from '../auth/guards/roles-guard-registry';
20
- import { RolesGuard } from '../auth/guards/roles.guard';
20
+ import { BetterAuthRolesGuard } from './better-auth-roles.guard';
21
21
  import { BetterAuthTokenService } from './better-auth-token.service';
22
22
  import { BetterAuthInstance, createBetterAuthInstance } from './better-auth.config';
23
23
  import { DefaultBetterAuthResolver } from './better-auth.resolver';
@@ -228,6 +228,13 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
228
228
  // Static reference to email verification service for Better-Auth hooks (outside DI context)
229
229
  private static emailVerificationService: CoreBetterAuthEmailVerificationService | null = null;
230
230
  private static mongoConnection: Connection | null = null;
231
+ // Safety Net: Track if forRoot() has already been called to detect duplicate registration
232
+ private static forRootCalled = false;
233
+ private static cachedDynamicModule: DynamicModule | null = null;
234
+ // Static references for lazy GraphQL driver (autoRegister: false) and BetterAuthRolesGuard
235
+ private static serviceInstance: CoreBetterAuthService | null = null;
236
+ private static userMapperInstance: CoreBetterAuthUserMapper | null = null;
237
+ private static tokenServiceInstance: BetterAuthTokenService | null = null;
231
238
 
232
239
  /**
233
240
  * Gets the controller class to use (custom or default)
@@ -243,9 +250,38 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
243
250
  return this.customResolver || DefaultBetterAuthResolver;
244
251
  }
245
252
 
253
+ /**
254
+ * Gets the static CoreBetterAuthService instance.
255
+ * Used by the lazy GraphQL driver when autoRegister: false.
256
+ * Safe because onConnect is called only after module initialization.
257
+ */
258
+ static getServiceInstance(): CoreBetterAuthService | null {
259
+ return this.serviceInstance;
260
+ }
261
+
262
+ /**
263
+ * Gets the static CoreBetterAuthUserMapper instance.
264
+ * Used by the lazy GraphQL driver when autoRegister: false.
265
+ * Safe because onConnect is called only after module initialization.
266
+ */
267
+ static getUserMapperInstance(): CoreBetterAuthUserMapper | null {
268
+ return this.userMapperInstance;
269
+ }
270
+
271
+ /**
272
+ * Gets the static BetterAuthTokenService instance.
273
+ * Used by BetterAuthRolesGuard for token verification when user isn't already on request.
274
+ * Safe because guards are invoked only after module initialization.
275
+ */
276
+ static getTokenServiceInstance(): BetterAuthTokenService | null {
277
+ return this.tokenServiceInstance;
278
+ }
279
+
246
280
  constructor(
247
281
  @Optional() private readonly betterAuthService?: CoreBetterAuthService,
248
282
  @Optional() private readonly rateLimiter?: CoreBetterAuthRateLimiter,
283
+ @Optional() private readonly userMapper?: CoreBetterAuthUserMapper,
284
+ @Optional() private readonly tokenService?: BetterAuthTokenService,
249
285
  ) {}
250
286
 
251
287
  onModuleInit() {
@@ -254,6 +290,17 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
254
290
  CoreBetterAuthModule.logger.log('CoreBetterAuthModule ready');
255
291
  }
256
292
 
293
+ // Store static references for lazy GraphQL driver (autoRegister: false) and BetterAuthRolesGuard
294
+ if (this.betterAuthService) {
295
+ CoreBetterAuthModule.serviceInstance = this.betterAuthService;
296
+ }
297
+ if (this.userMapper) {
298
+ CoreBetterAuthModule.userMapperInstance = this.userMapper;
299
+ }
300
+ if (this.tokenService) {
301
+ CoreBetterAuthModule.tokenServiceInstance = this.tokenService;
302
+ }
303
+
257
304
  // Configure rate limiter with stored config
258
305
  if (this.rateLimiter && CoreBetterAuthModule.currentConfig?.rateLimit) {
259
306
  this.rateLimiter.configure(CoreBetterAuthModule.currentConfig.rateLimit);
@@ -368,6 +415,24 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
368
415
  * @returns Dynamic module configuration
369
416
  */
370
417
  static forRoot(options: CoreBetterAuthModuleOptions): DynamicModule {
418
+ // Safety Net: Detect duplicate forRoot() calls
419
+ // NestJS deduplicates DynamicModules by module class — the FIRST import wins,
420
+ // subsequent imports are silently ignored. This means custom controller/resolver
421
+ // from the second call would be lost. Return cached module with a warning.
422
+ if (this.forRootCalled && !process.env.VITEST) {
423
+ this.logger.warn(
424
+ 'CoreBetterAuthModule.forRoot() was called more than once. ' +
425
+ 'The second call is ignored by NestJS (DynamicModule deduplication). ' +
426
+ 'Custom controller/resolver from the second call will NOT be registered. ' +
427
+ 'Solutions: (1) Use betterAuth.controller/resolver in config, or ' +
428
+ '(2) Set betterAuth.autoRegister: false and import your module separately.',
429
+ );
430
+ if (this.cachedDynamicModule) {
431
+ return this.cachedDynamicModule;
432
+ }
433
+ }
434
+ this.forRootCalled = true;
435
+
371
436
  const {
372
437
  config: rawConfig,
373
438
  controller,
@@ -423,7 +488,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
423
488
  if (config === null || config?.enabled === false) {
424
489
  this.logger.debug('BetterAuth is disabled - skipping initialization');
425
490
  this.betterAuthEnabled = false;
426
- return {
491
+ this.cachedDynamicModule = {
427
492
  exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService, CoreBetterAuthChallengeService],
428
493
  module: CoreBetterAuthModule,
429
494
  providers: [
@@ -442,6 +507,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
442
507
  // because they require ConfigService and have no purpose when BetterAuth is disabled
443
508
  ],
444
509
  };
510
+ return this.cachedDynamicModule;
445
511
  }
446
512
 
447
513
  // Enable middleware registration
@@ -453,12 +519,13 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
453
519
  // Always use deferred initialization to ensure MongoDB is ready
454
520
  // This prevents timing issues during application startup
455
521
  // Pass server-level URLs for Passkey auto-detection (using effective values from ConfigService fallback)
456
- return this.createDeferredModule(config, {
522
+ this.cachedDynamicModule = this.createDeferredModule(config, {
457
523
  fallbackSecrets: effectiveFallbackSecrets,
458
524
  serverAppUrl: effectiveServerAppUrl,
459
525
  serverBaseUrl: effectiveServerBaseUrl,
460
526
  serverEnv: effectiveServerEnv,
461
527
  });
528
+ return this.cachedDynamicModule;
462
529
  }
463
530
 
464
531
  /**
@@ -622,6 +689,13 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
622
689
  this.shouldRegisterRolesGuardGlobally = false;
623
690
  this.rolesGuardExplicitlyDisabled = false;
624
691
  this.emailVerificationService = null;
692
+ this.mongoConnection = null;
693
+ // Safety Net: Reset duplicate-call detection
694
+ this.forRootCalled = false;
695
+ this.cachedDynamicModule = null;
696
+ // Lazy GraphQL driver: Reset service references
697
+ this.serviceInstance = null;
698
+ this.userMapperInstance = null;
625
699
  // Reset shared RolesGuard registry (shared with CoreAuthModule)
626
700
  RolesGuardRegistry.reset();
627
701
  }
@@ -822,17 +896,20 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
822
896
  },
823
897
  CoreBetterAuthChallengeService,
824
898
  this.getResolverClass(),
825
- // Conditionally register RolesGuard globally for IAM-only setups
826
- // In Legacy mode, RolesGuard is already registered globally via CoreAuthModule
899
+ // Conditionally register BetterAuthRolesGuard globally for IAM-only setups
900
+ // In Legacy mode, RolesGuard is registered globally via CoreAuthModule instead
827
901
  // Uses shared RolesGuardRegistry to prevent duplicate registration across modules
902
+ //
903
+ // Note: We use BetterAuthRolesGuard (not RolesGuard) in IAM-only mode because:
904
+ // - RolesGuard extends AuthGuard (a Passport mixin) which has DI metadata conflicts
905
+ // - BetterAuthRolesGuard is a simpler guard that only checks roles
906
+ // - Authentication is handled by CoreBetterAuthMiddleware (JWT/session validation)
828
907
  ...(this.shouldRegisterRolesGuardGlobally && !RolesGuardRegistry.isRegistered()
829
908
  ? (() => {
830
909
  RolesGuardRegistry.markRegistered('CoreBetterAuthModule');
831
910
  return [
832
- {
833
- provide: APP_GUARD,
834
- useClass: RolesGuard,
835
- },
911
+ BetterAuthRolesGuard,
912
+ { provide: APP_GUARD, useExisting: BetterAuthRolesGuard },
836
913
  ];
837
914
  })()
838
915
  : []),
@@ -22,6 +22,7 @@
22
22
  * - DefaultBetterAuthResolver: Default resolver implementation (use as fallback)
23
23
  */
24
24
 
25
+ export * from './better-auth-roles.guard';
25
26
  export * from './better-auth-token.service';
26
27
  export * from './better-auth.config';
27
28
  export * from './better-auth.resolver';
@@ -137,9 +137,15 @@ export class CoreModule implements NestModule {
137
137
  };
138
138
  }
139
139
 
140
+ // Check if autoRegister: false for IAM-only mode (project imports its own BetterAuth module)
141
+ const rawBetterAuth = options?.betterAuth;
142
+ const isAutoRegisterDisabledEarly = typeof rawBetterAuth === 'object' && rawBetterAuth?.autoRegister === false;
143
+
140
144
  // Build GraphQL driver configuration based on auth mode
141
145
  const graphQlDriverConfig = isIamOnlyMode
142
- ? this.buildIamOnlyGraphQlDriver(cors, options)
146
+ ? (isAutoRegisterDisabledEarly
147
+ ? this.buildLazyIamGraphQlDriver(cors, options)
148
+ : this.buildIamOnlyGraphQlDriver(cors, options))
143
149
  : this.buildLegacyGraphQlDriver(AuthService, AuthModule, cors, options);
144
150
 
145
151
  const config: IServerOptions = merge(
@@ -267,17 +273,27 @@ export class CoreModule implements NestModule {
267
273
  : isExplicitlyEnabled;
268
274
 
269
275
  const isAutoRegister = typeof betterAuthConfig === 'object' && betterAuthConfig?.autoRegister === true;
276
+ // autoRegister: false means the project imports its own BetterAuthModule separately
277
+ const isAutoRegisterDisabled = typeof betterAuthConfig === 'object' && betterAuthConfig?.autoRegister === false;
278
+
279
+ // Extract custom controller/resolver from config (Pattern 2: Config-based)
280
+ const configController = typeof betterAuthConfig === 'object' ? betterAuthConfig?.controller : undefined;
281
+ const configResolver = typeof betterAuthConfig === 'object' ? betterAuthConfig?.resolver : undefined;
270
282
 
271
283
  if (isBetterAuthEnabled) {
272
- if (isIamOnlyMode || isAutoRegister) {
284
+ if ((isIamOnlyMode && !isAutoRegisterDisabled) || isAutoRegister) {
273
285
  imports.push(
274
286
  CoreBetterAuthModule.forRoot({
275
287
  config: betterAuthConfig === true ? {} : betterAuthConfig || {},
288
+ // Pass custom controller/resolver from config (Pattern 2)
289
+ controller: configController,
276
290
  // Pass JWT secrets for backwards compatibility fallback
277
291
  fallbackSecrets: [config.jwt?.secret, config.jwt?.refresh?.secret],
278
292
  // In IAM-only mode, register RolesGuard globally to enforce @Roles() decorators
279
293
  // In Legacy mode (autoRegister), RolesGuard is already registered via CoreAuthModule
280
294
  registerRolesGuardGlobally: isIamOnlyMode,
295
+ // Pass custom resolver from config (Pattern 2)
296
+ resolver: configResolver,
281
297
  // Pass server-level URLs for Passkey auto-detection
282
298
  // When env: 'local', defaults are: baseUrl=localhost:3000, appUrl=localhost:3001
283
299
  serverAppUrl: config.appUrl,
@@ -399,6 +415,108 @@ export class CoreModule implements NestModule {
399
415
  };
400
416
  }
401
417
 
418
+ /**
419
+ * Build a lazy GraphQL driver for IAM-only mode with autoRegister: false.
420
+ *
421
+ * When autoRegister: false, CoreBetterAuthModule is NOT imported by CoreModule,
422
+ * so we cannot use `imports` or `inject` to get BetterAuth services.
423
+ * Instead, we resolve them lazily via static getters on CoreBetterAuthModule.
424
+ * This is safe because `onConnect` is only called when a WebSocket connection is made,
425
+ * which happens after all modules are initialized.
426
+ */
427
+ private static buildLazyIamGraphQlDriver(cors: object, options: Partial<IServerOptions>) {
428
+ return {
429
+ useFactory: async () =>
430
+ Object.assign(
431
+ {
432
+ autoSchemaFile: 'schema.gql',
433
+ context: ({ req, res }) => ({ req, res }),
434
+ cors,
435
+ installSubscriptionHandlers: true,
436
+ subscriptions: {
437
+ 'graphql-ws': {
438
+ context: ({ extra }) => extra,
439
+ onConnect: async (context: Context<any>) => {
440
+ const { connectionParams, extra } = context;
441
+ const enableAuth = options?.graphQl?.enableSubscriptionAuth ?? true;
442
+
443
+ if (enableAuth) {
444
+ const betterAuthService = CoreBetterAuthModule.getServiceInstance();
445
+ const userMapper = CoreBetterAuthModule.getUserMapperInstance();
446
+
447
+ if (!betterAuthService || !userMapper) {
448
+ throw new UnauthorizedException('BetterAuth not initialized');
449
+ }
450
+
451
+ const headers = CoreModule.getHeaderFromArray(extra.request?.rawHeaders);
452
+ const authToken: string =
453
+ connectionParams?.Authorization?.split(' ')[1] ?? headers.Authorization?.split(' ')[1];
454
+
455
+ if (authToken) {
456
+ const { session, user: sessionUser } = await betterAuthService.getSession({
457
+ headers: { authorization: `Bearer ${authToken}` },
458
+ });
459
+
460
+ if (!session || !sessionUser) {
461
+ throw new UnauthorizedException('Invalid or expired session');
462
+ }
463
+
464
+ const user = await userMapper.mapSessionUser(sessionUser);
465
+ if (!user) {
466
+ throw new UnauthorizedException('User not found');
467
+ }
468
+
469
+ extra.user = user;
470
+ extra.headers = connectionParams ?? headers;
471
+ return extra;
472
+ }
473
+
474
+ throw new UnauthorizedException('Missing authentication token');
475
+ }
476
+ },
477
+ },
478
+ 'subscriptions-transport-ws': {
479
+ onConnect: async (connectionParams) => {
480
+ const enableAuth = options?.graphQl?.enableSubscriptionAuth ?? true;
481
+
482
+ if (enableAuth) {
483
+ const betterAuthService = CoreBetterAuthModule.getServiceInstance();
484
+ const userMapper = CoreBetterAuthModule.getUserMapperInstance();
485
+
486
+ if (!betterAuthService || !userMapper) {
487
+ throw new UnauthorizedException('BetterAuth not initialized');
488
+ }
489
+
490
+ const authToken: string = connectionParams?.Authorization?.split(' ')[1];
491
+
492
+ if (authToken) {
493
+ const { session, user: sessionUser } = await betterAuthService.getSession({
494
+ headers: { authorization: `Bearer ${authToken}` },
495
+ });
496
+
497
+ if (!session || !sessionUser) {
498
+ throw new UnauthorizedException('Invalid or expired session');
499
+ }
500
+
501
+ const user = await userMapper.mapSessionUser(sessionUser);
502
+ if (!user) {
503
+ throw new UnauthorizedException('User not found');
504
+ }
505
+
506
+ return { headers: connectionParams, user };
507
+ }
508
+
509
+ throw new UnauthorizedException('Missing authentication token');
510
+ }
511
+ },
512
+ },
513
+ },
514
+ },
515
+ options?.graphQl?.driver,
516
+ ),
517
+ };
518
+ }
519
+
402
520
  /**
403
521
  * Build GraphQL driver configuration for Legacy Auth mode
404
522
  *