@lenne.tech/nest-server 11.10.2 → 11.10.4

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 (67) hide show
  1. package/dist/config.env.js +16 -133
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/interfaces/server-options.interface.d.ts +4 -0
  4. package/dist/core/modules/auth/core-auth.module.js +8 -4
  5. package/dist/core/modules/auth/core-auth.module.js.map +1 -1
  6. package/dist/core/modules/auth/guards/roles-guard-registry.d.ts +9 -0
  7. package/dist/core/modules/auth/guards/roles-guard-registry.js +30 -0
  8. package/dist/core/modules/auth/guards/roles-guard-registry.js.map +1 -0
  9. package/dist/core/modules/better-auth/better-auth.config.d.ts +3 -0
  10. package/dist/core/modules/better-auth/better-auth.config.js +176 -47
  11. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  12. package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +5 -1
  13. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +101 -8
  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-challenge.service.d.ts +20 -0
  16. package/dist/core/modules/better-auth/core-better-auth-challenge.service.js +142 -0
  17. package/dist/core/modules/better-auth/core-better-auth-challenge.service.js.map +1 -0
  18. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +1 -1
  19. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
  20. package/dist/core/modules/better-auth/core-better-auth-web.helper.d.ts +2 -0
  21. package/dist/core/modules/better-auth/core-better-auth-web.helper.js +29 -1
  22. package/dist/core/modules/better-auth/core-better-auth-web.helper.js.map +1 -1
  23. package/dist/core/modules/better-auth/core-better-auth.controller.js +5 -13
  24. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  25. package/dist/core/modules/better-auth/core-better-auth.middleware.d.ts +0 -1
  26. package/dist/core/modules/better-auth/core-better-auth.middleware.js +6 -19
  27. package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -1
  28. package/dist/core/modules/better-auth/core-better-auth.module.d.ts +5 -1
  29. package/dist/core/modules/better-auth/core-better-auth.module.js +74 -27
  30. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  31. package/dist/core/modules/better-auth/core-better-auth.resolver.js +7 -6
  32. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
  33. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +0 -2
  34. package/dist/core/modules/better-auth/core-better-auth.service.js +23 -37
  35. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  36. package/dist/core.module.js +10 -1
  37. package/dist/core.module.js.map +1 -1
  38. package/dist/index.d.ts +1 -0
  39. package/dist/index.js +1 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/server/modules/better-auth/better-auth.module.d.ts +4 -1
  42. package/dist/server/modules/better-auth/better-auth.module.js +4 -1
  43. package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
  44. package/dist/server/server.module.js +1 -4
  45. package/dist/server/server.module.js.map +1 -1
  46. package/dist/tsconfig.build.tsbuildinfo +1 -1
  47. package/package.json +1 -1
  48. package/src/config.env.ts +24 -174
  49. package/src/core/common/interfaces/server-options.interface.ts +288 -35
  50. package/src/core/modules/auth/core-auth.module.ts +11 -5
  51. package/src/core/modules/auth/guards/roles-guard-registry.ts +57 -0
  52. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +85 -56
  53. package/src/core/modules/better-auth/README.md +132 -35
  54. package/src/core/modules/better-auth/better-auth.config.ts +402 -70
  55. package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +158 -18
  56. package/src/core/modules/better-auth/core-better-auth-challenge.service.ts +254 -0
  57. package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +1 -1
  58. package/src/core/modules/better-auth/core-better-auth-web.helper.ts +64 -1
  59. package/src/core/modules/better-auth/core-better-auth.controller.ts +6 -14
  60. package/src/core/modules/better-auth/core-better-auth.middleware.ts +7 -20
  61. package/src/core/modules/better-auth/core-better-auth.module.ts +173 -38
  62. package/src/core/modules/better-auth/core-better-auth.resolver.ts +7 -6
  63. package/src/core/modules/better-auth/core-better-auth.service.ts +27 -48
  64. package/src/core.module.ts +21 -3
  65. package/src/index.ts +1 -0
  66. package/src/server/modules/better-auth/better-auth.module.ts +40 -10
  67. package/src/server/server.module.ts +2 -4
@@ -1,7 +1,7 @@
1
1
  import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
2
2
  import { NextFunction, Request, Response } from 'express';
3
3
 
4
- import { isProduction, maskEmail, maskToken } from '../../common/helpers/logging.helper';
4
+ import { maskEmail, maskToken } from '../../common/helpers/logging.helper';
5
5
  import { BetterAuthSessionUser, CoreBetterAuthUserMapper, MappedUser } from './core-better-auth-user.mapper';
6
6
  import { extractSessionToken } from './core-better-auth-web.helper';
7
7
  import { CoreBetterAuthService } from './core-better-auth.service';
@@ -33,7 +33,6 @@ export interface CoreBetterAuthRequest extends Request {
33
33
  @Injectable()
34
34
  export class CoreBetterAuthMiddleware implements NestMiddleware {
35
35
  private readonly logger = new Logger(CoreBetterAuthMiddleware.name);
36
- private readonly isProd = isProduction();
37
36
 
38
37
  constructor(
39
38
  private readonly betterAuthService: CoreBetterAuthService,
@@ -134,9 +133,7 @@ export class CoreBetterAuthMiddleware implements NestMiddleware {
134
133
  } catch (error) {
135
134
  // Don't block the request on auth errors
136
135
  // The guards will handle unauthorized access
137
- if (!this.isProd) {
138
- this.logger.debug(`Session validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
139
- }
136
+ this.logger.debug(`Session validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
140
137
  }
141
138
 
142
139
  next();
@@ -176,26 +173,18 @@ export class CoreBetterAuthMiddleware implements NestMiddleware {
176
173
  const basePath = this.betterAuthService.getBasePath();
177
174
  const sessionToken = extractSessionToken(req, basePath);
178
175
 
179
- if (!this.isProd) {
180
- this.logger.debug(`[MIDDLEWARE] getSession called, token found: ${sessionToken ? 'yes' : 'no'}`);
181
- }
176
+ this.logger.debug(`[MIDDLEWARE] getSession called, token found: ${sessionToken ? 'yes' : 'no'}`);
182
177
 
183
178
  if (sessionToken) {
184
- if (!this.isProd) {
185
- this.logger.debug(`[MIDDLEWARE] Found session token in cookies: ${maskToken(sessionToken)}`);
186
- }
179
+ this.logger.debug(`[MIDDLEWARE] Found session token in cookies: ${maskToken(sessionToken)}`);
187
180
 
188
181
  // Use getSessionByToken to validate session directly from database
189
182
  const sessionResult = await this.betterAuthService.getSessionByToken(sessionToken);
190
183
 
191
- if (!this.isProd) {
192
- this.logger.debug(`[MIDDLEWARE] getSessionByToken result: user=${maskEmail(sessionResult?.user?.email)}, session=${!!sessionResult?.session}`);
193
- }
184
+ this.logger.debug(`[MIDDLEWARE] getSessionByToken result: user=${maskEmail(sessionResult?.user?.email)}, session=${!!sessionResult?.session}`);
194
185
 
195
186
  if (sessionResult?.user && sessionResult?.session) {
196
- if (!this.isProd) {
197
- this.logger.debug(`[MIDDLEWARE] Session validated for user: ${maskEmail(sessionResult.user.email)}`);
198
- }
187
+ this.logger.debug(`[MIDDLEWARE] Session validated for user: ${maskEmail(sessionResult.user.email)}`);
199
188
  return sessionResult as { session: any; user: BetterAuthSessionUser };
200
189
  }
201
190
  }
@@ -225,9 +214,7 @@ export class CoreBetterAuthMiddleware implements NestMiddleware {
225
214
 
226
215
  return null;
227
216
  } catch (error) {
228
- if (!this.isProd) {
229
- this.logger.debug(`getSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
230
- }
217
+ this.logger.debug(`getSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
231
218
  return null;
232
219
  }
233
220
  }
@@ -15,11 +15,13 @@ import mongoose, { Connection } from 'mongoose';
15
15
 
16
16
  import { IBetterAuth } from '../../common/interfaces/server-options.interface';
17
17
  import { ConfigService } from '../../common/services/config.service';
18
+ import { RolesGuardRegistry } from '../auth/guards/roles-guard-registry';
18
19
  import { RolesGuard } from '../auth/guards/roles.guard';
19
20
  import { BetterAuthTokenService } from './better-auth-token.service';
20
21
  import { BetterAuthInstance, createBetterAuthInstance } from './better-auth.config';
21
22
  import { DefaultBetterAuthResolver } from './better-auth.resolver';
22
23
  import { CoreBetterAuthApiMiddleware } from './core-better-auth-api.middleware';
24
+ import { CoreBetterAuthChallengeService } from './core-better-auth-challenge.service';
23
25
  import { CoreBetterAuthRateLimitMiddleware } from './core-better-auth-rate-limit.middleware';
24
26
  import { CoreBetterAuthRateLimiter } from './core-better-auth-rate-limiter.service';
25
27
  import { CoreBetterAuthUserMapper } from './core-better-auth-user.mapper';
@@ -38,13 +40,14 @@ export const BETTER_AUTH_INSTANCE = 'BETTER_AUTH_INSTANCE';
38
40
  */
39
41
  export interface CoreBetterAuthModuleOptions {
40
42
  /**
41
- * Better-auth configuration.
43
+ * Better-auth configuration (optional - auto-read from ConfigService).
42
44
  * Accepts:
43
45
  * - `true`: Enable with all defaults (including JWT)
44
46
  * - `false`: Disable BetterAuth
45
47
  * - `{ ... }`: Enable with custom configuration
48
+ * - `undefined`: Auto-read from ConfigService (Zero-Config)
46
49
  */
47
- config: boolean | IBetterAuth;
50
+ config?: boolean | IBetterAuth;
48
51
 
49
52
  /**
50
53
  * Custom controller class to use instead of the default CoreBetterAuthController.
@@ -85,13 +88,14 @@ export interface CoreBetterAuthModuleOptions {
85
88
  /**
86
89
  * Register RolesGuard as a global guard.
87
90
  *
88
- * This should be set to `true` for IAM-only setups (CoreModule.forRoot with 1 parameter)
89
- * where CoreAuthModule is not imported (which normally registers RolesGuard globally).
90
- *
91
91
  * When `true`, all `@Roles()` decorators will be enforced automatically without
92
92
  * needing explicit `@UseGuards(RolesGuard)` on each endpoint.
93
93
  *
94
- * @default false
94
+ * **Important:** This should be `false` in Legacy mode (3-parameter CoreModule.forRoot)
95
+ * because CoreAuthModule already registers RolesGuard globally. Setting it to `true`
96
+ * in Legacy mode would cause duplicate guard registration.
97
+ *
98
+ * @default true (secure by default - ensures @Roles() decorators are enforced)
95
99
  */
96
100
  registerRolesGuardGlobally?: boolean;
97
101
 
@@ -119,19 +123,66 @@ export interface CoreBetterAuthModuleOptions {
119
123
  * ```
120
124
  */
121
125
  resolver?: Type<CoreBetterAuthResolver>;
126
+
127
+ /**
128
+ * Server-level app/frontend URL (from IServerOptions.appUrl).
129
+ * This is the frontend application URL where the browser runs.
130
+ *
131
+ * Used for:
132
+ * - CORS trustedOrigins
133
+ * - Passkey/WebAuthn origin
134
+ *
135
+ * Auto-Detection:
136
+ * - If not set, derived from `serverBaseUrl`:
137
+ * - 'https://api.example.com' → 'https://example.com'
138
+ * - 'https://example.com' → 'https://example.com'
139
+ * - When `serverEnv: 'local'` and not set: defaults to 'http://localhost:3001'
140
+ *
141
+ * @example 'https://example.com'
142
+ */
143
+ serverAppUrl?: string;
144
+
145
+ /**
146
+ * Server-level base URL (from IServerOptions.baseUrl).
147
+ * This is the API server URL.
148
+ *
149
+ * Used for:
150
+ * - Email links (password reset, verification)
151
+ * - OAuth callback URLs
152
+ * - As fallback for betterAuth.baseUrl
153
+ *
154
+ * Auto-Detection:
155
+ * - When `serverEnv: 'local'` and not set: defaults to 'http://localhost:3000'
156
+ *
157
+ * @example 'https://api.example.com'
158
+ */
159
+ serverBaseUrl?: string;
160
+
161
+ /**
162
+ * Server environment (from IServerOptions.env).
163
+ * Used for local environment defaults:
164
+ * - When `env: 'local'` and no URLs are set:
165
+ * - `serverBaseUrl` defaults to 'http://localhost:3000'
166
+ * - `serverAppUrl` defaults to 'http://localhost:3001'
167
+ */
168
+ serverEnv?: string;
122
169
  }
123
170
 
124
171
  /**
125
172
  * Normalizes betterAuth config from boolean | IBetterAuth to IBetterAuth | null
126
173
  * - `true` → `{}` (enabled with defaults)
127
174
  * - `false` → `null` (disabled)
128
- * - `undefined` → `null` (disabled for backward compatibility)
175
+ * - `undefined` → `{}` (enabled by default - zero-config)
176
+ * - `{ enabled: false }` → `null` (disabled)
129
177
  * - `{ ... }` → `{ ... }` (pass through)
130
178
  */
131
179
  function normalizeBetterAuthConfig(config: boolean | IBetterAuth | undefined): IBetterAuth | null {
132
- if (config === undefined || config === null) return null;
180
+ // BetterAuth is enabled by default (zero-config)
181
+ if (config === undefined || config === null) return {};
133
182
  if (config === true) return {};
134
183
  if (config === false) return null;
184
+ // Check for explicit { enabled: false }
185
+ if (typeof config === 'object' && config.enabled === false) return null;
135
186
  return config;
136
187
  }
137
188
 
@@ -169,6 +220,8 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
169
220
  private static customController: null | Type<CoreBetterAuthController> = null;
170
221
  private static customResolver: null | Type<CoreBetterAuthResolver> = null;
171
222
  private static shouldRegisterRolesGuardGlobally = false;
223
+ // Track if registerRolesGuardGlobally was explicitly set to false (for warning)
224
+ private static rolesGuardExplicitlyDisabled = false;
172
225
 
173
226
  /**
174
227
  * Gets the controller class to use (custom or default)
@@ -199,6 +252,16 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
199
252
  if (this.rateLimiter && CoreBetterAuthModule.currentConfig?.rateLimit) {
200
253
  this.rateLimiter.configure(CoreBetterAuthModule.currentConfig.rateLimit);
201
254
  }
255
+
256
+ // Security warning: Check if RolesGuard is registered when explicitly disabled
257
+ // This warning helps developers identify potential security misconfigurations
258
+ if (CoreBetterAuthModule.rolesGuardExplicitlyDisabled && !RolesGuardRegistry.isRegistered()) {
259
+ CoreBetterAuthModule.logger.warn(
260
+ '⚠️ SECURITY WARNING: registerRolesGuardGlobally is explicitly set to false, ' +
261
+ 'but no RolesGuard is registered globally. @Roles() decorators will NOT enforce access control! ' +
262
+ 'Either set registerRolesGuardGlobally: true, or ensure CoreAuthModule (Legacy) is imported.',
263
+ );
264
+ }
202
265
  }
203
266
 
204
267
  /**
@@ -268,10 +331,39 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
268
331
  * @returns Dynamic module configuration
269
332
  */
270
333
  static forRoot(options: CoreBetterAuthModuleOptions): DynamicModule {
271
- const { config: rawConfig, controller, fallbackSecrets, registerRolesGuardGlobally, resolver } = options;
334
+ const {
335
+ config: rawConfig,
336
+ controller,
337
+ fallbackSecrets,
338
+ registerRolesGuardGlobally,
339
+ resolver,
340
+ serverAppUrl,
341
+ serverBaseUrl,
342
+ serverEnv,
343
+ } = options;
344
+
345
+ // Auto-read from global ConfigService if not explicitly provided
346
+ // This allows projects to use BetterAuthModule.forRoot({}) for true Zero-Config
347
+ // as all values are already available from CoreModule.forRoot(envConfig)
348
+ const globalConfig = ConfigService.configFastButReadOnly;
349
+
350
+ // Auto-detect config from ConfigService if not explicitly provided
351
+ const effectiveRawConfig = rawConfig ?? globalConfig?.betterAuth;
352
+
353
+ // Auto-detect fallbackSecrets from ConfigService if not explicitly provided
354
+ const effectiveFallbackSecrets = fallbackSecrets ?? (
355
+ globalConfig?.jwt
356
+ ? [globalConfig.jwt.secret, globalConfig.jwt.refresh?.secret].filter(Boolean)
357
+ : undefined
358
+ );
359
+
360
+ // Auto-detect server URLs from ConfigService if not explicitly provided
361
+ const effectiveServerAppUrl = serverAppUrl ?? globalConfig?.appUrl;
362
+ const effectiveServerBaseUrl = serverBaseUrl ?? globalConfig?.baseUrl;
363
+ const effectiveServerEnv = serverEnv ?? globalConfig?.env;
272
364
 
273
365
  // Normalize config: true → {}, false/undefined → null
274
- const config = normalizeBetterAuthConfig(rawConfig);
366
+ const config = normalizeBetterAuthConfig(effectiveRawConfig);
275
367
 
276
368
  // Store config for middleware configuration
277
369
  this.currentConfig = config;
@@ -279,8 +371,12 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
279
371
  this.customController = controller || null;
280
372
  // Store custom resolver if provided
281
373
  this.customResolver = resolver || null;
282
- // Store whether to register RolesGuard globally (for IAM-only setups)
283
- this.shouldRegisterRolesGuardGlobally = registerRolesGuardGlobally ?? false;
374
+ // Store whether to register RolesGuard globally
375
+ // Default is true (secure by default) - ensures @Roles() decorators are enforced
376
+ // CoreModule.forRoot sets this to false in Legacy mode (where CoreAuthModule handles it)
377
+ this.shouldRegisterRolesGuardGlobally = registerRolesGuardGlobally ?? true;
378
+ // Track if explicitly disabled (for security warning in onModuleInit)
379
+ this.rolesGuardExplicitlyDisabled = registerRolesGuardGlobally === false;
284
380
 
285
381
  // If better-auth is disabled (config is null or enabled: false), return minimal module
286
382
  // Note: We don't provide middleware classes when disabled because they depend on CoreBetterAuthService
@@ -289,7 +385,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
289
385
  this.logger.debug('BetterAuth is disabled - skipping initialization');
290
386
  this.betterAuthEnabled = false;
291
387
  return {
292
- exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService],
388
+ exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService, CoreBetterAuthChallengeService],
293
389
  module: CoreBetterAuthModule,
294
390
  providers: [
295
391
  {
@@ -302,6 +398,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
302
398
  CoreBetterAuthUserMapper,
303
399
  CoreBetterAuthRateLimiter,
304
400
  BetterAuthTokenService,
401
+ CoreBetterAuthChallengeService,
305
402
  ],
306
403
  };
307
404
  }
@@ -314,7 +411,12 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
314
411
 
315
412
  // Always use deferred initialization to ensure MongoDB is ready
316
413
  // This prevents timing issues during application startup
317
- return this.createDeferredModule(config, fallbackSecrets);
414
+ // Pass server-level URLs for Passkey auto-detection (using effective values from ConfigService fallback)
415
+ return this.createDeferredModule(config, effectiveFallbackSecrets, {
416
+ serverAppUrl: effectiveServerAppUrl,
417
+ serverBaseUrl: effectiveServerBaseUrl,
418
+ serverEnv: effectiveServerEnv,
419
+ });
318
420
  }
319
421
 
320
422
  /**
@@ -326,7 +428,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
326
428
  static forRootAsync(): DynamicModule {
327
429
  return {
328
430
  controllers: [this.getControllerClass()],
329
- exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService],
431
+ exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService, CoreBetterAuthChallengeService],
330
432
  imports: [],
331
433
  module: CoreBetterAuthModule,
332
434
  providers: [
@@ -408,6 +510,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
408
510
  return new BetterAuthTokenService(betterAuthService, connection);
409
511
  },
410
512
  },
513
+ CoreBetterAuthChallengeService,
411
514
  this.getResolverClass(),
412
515
  ],
413
516
  };
@@ -441,16 +544,27 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
441
544
  this.customController = null;
442
545
  this.customResolver = null;
443
546
  this.shouldRegisterRolesGuardGlobally = false;
547
+ this.rolesGuardExplicitlyDisabled = false;
548
+ // Reset shared RolesGuard registry (shared with CoreAuthModule)
549
+ RolesGuardRegistry.reset();
444
550
  }
445
551
 
446
552
  /**
447
553
  * Creates a deferred initialization module that waits for mongoose connection
448
554
  * By injecting the Connection token, NestJS ensures Mongoose is ready first
555
+ *
556
+ * @param config - BetterAuth configuration
557
+ * @param fallbackSecrets - Fallback secrets for backwards compatibility
558
+ * @param serverUrls - Server-level URLs for Passkey auto-detection
449
559
  */
450
- private static createDeferredModule(config: IBetterAuth, fallbackSecrets?: (string | undefined)[]): DynamicModule {
560
+ private static createDeferredModule(
561
+ config: IBetterAuth,
562
+ fallbackSecrets?: (string | undefined)[],
563
+ serverUrls?: { serverAppUrl?: string; serverBaseUrl?: string; serverEnv?: string },
564
+ ): DynamicModule {
451
565
  return {
452
566
  controllers: [this.getControllerClass()],
453
- exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService],
567
+ exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService, CoreBetterAuthChallengeService],
454
568
  module: CoreBetterAuthModule,
455
569
  providers: [
456
570
  {
@@ -467,9 +581,23 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
467
581
  if (!globalDb) {
468
582
  throw new Error('MongoDB database not available');
469
583
  }
470
- this.authInstance = createBetterAuthInstance({ config, db: globalDb, fallbackSecrets });
584
+ this.authInstance = createBetterAuthInstance({
585
+ config,
586
+ db: globalDb,
587
+ fallbackSecrets,
588
+ serverAppUrl: serverUrls?.serverAppUrl,
589
+ serverBaseUrl: serverUrls?.serverBaseUrl,
590
+ serverEnv: serverUrls?.serverEnv,
591
+ });
471
592
  } else {
472
- this.authInstance = createBetterAuthInstance({ config, db, fallbackSecrets });
593
+ this.authInstance = createBetterAuthInstance({
594
+ config,
595
+ db,
596
+ fallbackSecrets,
597
+ serverAppUrl: serverUrls?.serverAppUrl,
598
+ serverBaseUrl: serverUrls?.serverBaseUrl,
599
+ serverEnv: serverUrls?.serverEnv,
600
+ });
473
601
  }
474
602
 
475
603
  // IMPORTANT: Store the config AFTER createBetterAuthInstance mutates it
@@ -516,16 +644,21 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
516
644
  return new BetterAuthTokenService(betterAuthService, connection);
517
645
  },
518
646
  },
647
+ CoreBetterAuthChallengeService,
519
648
  this.getResolverClass(),
520
649
  // Conditionally register RolesGuard globally for IAM-only setups
521
650
  // In Legacy mode, RolesGuard is already registered globally via CoreAuthModule
522
- ...(this.shouldRegisterRolesGuardGlobally
523
- ? [
524
- {
525
- provide: APP_GUARD,
526
- useClass: RolesGuard,
527
- },
528
- ]
651
+ // Uses shared RolesGuardRegistry to prevent duplicate registration across modules
652
+ ...(this.shouldRegisterRolesGuardGlobally && !RolesGuardRegistry.isRegistered()
653
+ ? (() => {
654
+ RolesGuardRegistry.markRegistered('CoreBetterAuthModule');
655
+ return [
656
+ {
657
+ provide: APP_GUARD,
658
+ useClass: RolesGuard,
659
+ },
660
+ ];
661
+ })()
529
662
  : []),
530
663
  ],
531
664
  };
@@ -539,24 +672,26 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
539
672
  private static logEnabledFeatures(config: IBetterAuth): void {
540
673
  const features: string[] = [];
541
674
 
542
- // Helper to check if a plugin is enabled
543
- // Supports: true, { enabled: true }, { ... } (enabled by default when config block present)
544
- // Disabled only when: false or { enabled: false }
545
- const isPluginEnabled = <T extends { enabled?: boolean }>(value: boolean | T | undefined): boolean => {
546
- if (value === undefined || value === false) return false;
547
- if (value === true) return true;
548
- return value.enabled !== false;
675
+ // Helper to check if a plugin is explicitly disabled
676
+ const isExplicitlyDisabled = <T extends { enabled?: boolean }>(value: boolean | T | undefined): boolean => {
677
+ if (value === false) return true;
678
+ if (typeof value === 'object' && value?.enabled === false) return true;
679
+ return false;
549
680
  };
550
681
 
551
- // Plugins are enabled by default when config block is present
552
- if (isPluginEnabled(config.jwt)) {
682
+ // JWT and 2FA are enabled by default unless explicitly disabled
683
+ if (!isExplicitlyDisabled(config.jwt)) {
553
684
  features.push('JWT');
554
685
  }
555
- if (isPluginEnabled(config.twoFactor)) {
686
+ if (!isExplicitlyDisabled(config.twoFactor)) {
556
687
  features.push('2FA/TOTP');
557
688
  }
558
- if (isPluginEnabled(config.passkey)) {
559
- features.push('Passkey/WebAuthn');
689
+ // Passkey is enabled by default, unless explicitly set to false
690
+ if (config.passkey !== false && !(typeof config.passkey === 'object' && config.passkey?.enabled === false)) {
691
+ const passkeyConfig = typeof config.passkey === 'object' ? config.passkey : null;
692
+ // Challenge storage is 'database' by default, can be overridden via config
693
+ const challengeStorage = passkeyConfig?.challengeStorage || 'database';
694
+ features.push(`Passkey/WebAuthn (challenges: ${challengeStorage})`);
560
695
  }
561
696
 
562
697
  // Dynamically collect enabled social providers
@@ -4,6 +4,7 @@ import { Request, Response } from 'express';
4
4
 
5
5
  import { Roles } from '../../common/decorators/roles.decorator';
6
6
  import { RoleEnum } from '../../common/enums/role.enum';
7
+ import { maskEmail } from '../../common/helpers/logging.helper';
7
8
  import {
8
9
  BetterAuth2FAResponse,
9
10
  BetterAuthSignInResponse,
@@ -228,12 +229,12 @@ export class CoreBetterAuthResolver {
228
229
  body: { email, password },
229
230
  })) as BetterAuthSignInResponse | null;
230
231
 
231
- this.logger.debug(`[SignIn] API response for ${email}: ${JSON.stringify(response)?.substring(0, 200)}`);
232
+ this.logger.debug(`[SignIn] API response for ${maskEmail(email)}: ${JSON.stringify(response)?.substring(0, 200)}`);
232
233
 
233
234
  // Check if response indicates an error (Better-Auth returns error objects, not throws)
234
235
  const responseAny = response as any;
235
236
  if (responseAny?.error || responseAny?.code === 'CREDENTIAL_ACCOUNT_NOT_FOUND') {
236
- this.logger.debug(`[SignIn] API returned error for ${email}: ${responseAny?.error || responseAny?.code}`);
237
+ this.logger.debug(`[SignIn] API returned error for ${maskEmail(email)}: ${responseAny?.error || responseAny?.code}`);
237
238
  throw new Error(responseAny?.error || responseAny?.code || 'Credential account not found');
238
239
  }
239
240
 
@@ -273,17 +274,17 @@ export class CoreBetterAuthResolver {
273
274
  throw new UnauthorizedException('Invalid credentials');
274
275
  } catch (error) {
275
276
  this.logger.debug(
276
- `[SignIn] Sign-in failed for ${email}: ${error instanceof Error ? error.message : 'Unknown error'}`,
277
+ `[SignIn] Sign-in failed for ${maskEmail(email)}: ${error instanceof Error ? error.message : 'Unknown error'}`,
277
278
  );
278
279
 
279
280
  // If migration is allowed, try to migrate legacy user and retry
280
281
  if (allowMigration) {
281
- this.logger.debug(`[SignIn] Attempting migration for ${email}...`);
282
+ this.logger.debug(`[SignIn] Attempting migration for ${maskEmail(email)}...`);
282
283
  // Pass the original password for legacy verification
283
284
  const migrated = await this.userMapper.migrateAccountToIam(email, password);
284
- this.logger.debug(`[SignIn] Migration result for ${email}: ${migrated}`);
285
+ this.logger.debug(`[SignIn] Migration result for ${maskEmail(email)}: ${migrated}`);
285
286
  if (migrated) {
286
- this.logger.debug(`[SignIn] Migrated legacy user ${email} to IAM, retrying sign-in`);
287
+ this.logger.debug(`[SignIn] Migrated legacy user ${maskEmail(email)} to IAM, retrying sign-in`);
287
288
  // Retry sign-in after migration with normalized password (as migrateAccountToIam stores it)
288
289
  const normalizedPassword = this.userMapper.normalizePasswordForIam(password);
289
290
  return this.attemptSignInDirect(email, normalizedPassword, api);
@@ -4,7 +4,7 @@ import { Request } from 'express';
4
4
  import { importJWK, jwtVerify } from 'jose';
5
5
  import { Connection } from 'mongoose';
6
6
 
7
- import { isProduction, maskCookieHeader, maskEmail, maskToken } from '../../common/helpers/logging.helper';
7
+ import { maskCookieHeader, maskEmail, maskToken } from '../../common/helpers/logging.helper';
8
8
  import { IBetterAuth } from '../../common/interfaces/server-options.interface';
9
9
  import { ConfigService } from '../../common/services/config.service';
10
10
  import { BetterAuthInstance } from './better-auth.config';
@@ -55,7 +55,6 @@ export const BETTER_AUTH_CONFIG = 'BETTER_AUTH_CONFIG';
55
55
  @Injectable()
56
56
  export class CoreBetterAuthService {
57
57
  private readonly logger = new Logger(CoreBetterAuthService.name);
58
- private readonly isProd = isProduction();
59
58
  private readonly config: IBetterAuth;
60
59
 
61
60
  constructor(
@@ -129,32 +128,30 @@ export class CoreBetterAuthService {
129
128
 
130
129
  /**
131
130
  * Checks if 2FA is enabled.
132
- * Supports both boolean and object configuration:
133
- * - `true` or `{}` enabled
134
- * - `false` or `{ enabled: false }` → disabled
131
+ * 2FA is enabled by default when BetterAuth is enabled.
132
+ * Only disabled when explicitly set to:
133
+ * - `false`
134
+ * - `{ enabled: false }`
135
135
  */
136
136
  isTwoFactorEnabled(): boolean {
137
- return this.isEnabled() && this.isPluginEnabled(this.config.twoFactor);
137
+ if (!this.isEnabled()) return false;
138
+ if (this.config.twoFactor === false) return false;
139
+ if (typeof this.config.twoFactor === 'object' && this.config.twoFactor?.enabled === false) return false;
140
+ return true;
138
141
  }
139
142
 
140
143
  /**
141
144
  * Checks if Passkey/WebAuthn is enabled.
142
- * Supports both boolean and object configuration:
143
- * - `true` or `{}` enabled
144
- * - `false` or `{ enabled: false }` → disabled
145
+ * Passkey is enabled by default when BetterAuth is enabled.
146
+ * Only disabled when explicitly set to:
147
+ * - `false`
148
+ * - `{ enabled: false }`
145
149
  */
146
150
  isPasskeyEnabled(): boolean {
147
- return this.isEnabled() && this.isPluginEnabled(this.config.passkey);
148
- }
149
-
150
- /**
151
- * Helper to check if a plugin configuration is enabled.
152
- * Supports both boolean and object configuration.
153
- */
154
- private isPluginEnabled<T extends { enabled?: boolean }>(config: boolean | T | undefined): boolean {
155
- if (config === undefined) return false;
156
- if (typeof config === 'boolean') return config;
157
- return config.enabled !== false;
151
+ if (!this.isEnabled()) return false;
152
+ if (this.config.passkey === false) return false;
153
+ if (typeof this.config.passkey === 'object' && this.config.passkey?.enabled === false) return false;
154
+ return true;
158
155
  }
159
156
 
160
157
  /**
@@ -320,17 +317,13 @@ export class CoreBetterAuthService {
320
317
  }
321
318
 
322
319
  // Debug: Log the cookie header being sent to api.getSession (masked for security)
323
- if (!this.isProd) {
324
- const cookieHeader = headers.get('cookie');
325
- this.logger.debug(`getSession called with cookies: ${maskCookieHeader(cookieHeader)}`);
326
- }
320
+ const cookieHeader = headers.get('cookie');
321
+ this.logger.debug(`getSession called with cookies: ${maskCookieHeader(cookieHeader)}`);
327
322
 
328
323
  const response = await api.getSession({ headers });
329
324
 
330
325
  // Debug: Log the response from api.getSession
331
- if (!this.isProd) {
332
- this.logger.debug(`getSession response: ${JSON.stringify(response)?.substring(0, 200)}`);
333
- }
326
+ this.logger.debug(`getSession response: ${JSON.stringify(response)?.substring(0, 200)}`);
334
327
 
335
328
  if (response && typeof response === 'object' && 'user' in response) {
336
329
  return response as SessionResult;
@@ -338,9 +331,7 @@ export class CoreBetterAuthService {
338
331
 
339
332
  return { session: null, user: null };
340
333
  } catch (error) {
341
- if (!this.isProd) {
342
- this.logger.debug(`getSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
343
- }
334
+ this.logger.debug(`getSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
344
335
  return { session: null, user: null };
345
336
  }
346
337
  }
@@ -383,9 +374,7 @@ export class CoreBetterAuthService {
383
374
  await api.signOut({ headers });
384
375
  return true;
385
376
  } catch (error) {
386
- if (!this.isProd) {
387
- this.logger.debug(`revokeSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
388
- }
377
+ this.logger.debug(`revokeSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
389
378
  return false;
390
379
  }
391
380
  }
@@ -494,31 +483,23 @@ export class CoreBetterAuthService {
494
483
  const result = results[0];
495
484
 
496
485
  if (!result) {
497
- if (!this.isProd) {
498
- this.logger.debug(`getSessionByToken: session not found for token ${maskToken(token)}`);
499
- }
486
+ this.logger.debug(`getSessionByToken: session not found for token ${maskToken(token)}`);
500
487
  return { session: null, user: null };
501
488
  }
502
489
 
503
490
  // Check if session is expired
504
491
  if (result.expiresAt && new Date(result.expiresAt) < new Date()) {
505
- if (!this.isProd) {
506
- this.logger.debug(`getSessionByToken: session expired`);
507
- }
492
+ this.logger.debug(`getSessionByToken: session expired`);
508
493
  return { session: null, user: null };
509
494
  }
510
495
 
511
496
  const user = result.userDoc;
512
497
  if (!user) {
513
- if (!this.isProd) {
514
- this.logger.debug(`getSessionByToken: user not found for session`);
515
- }
498
+ this.logger.debug(`getSessionByToken: user not found for session`);
516
499
  return { session: null, user: null };
517
500
  }
518
501
 
519
- if (!this.isProd) {
520
- this.logger.debug(`getSessionByToken: found session for user ${maskEmail(user.email)}`);
521
- }
502
+ this.logger.debug(`getSessionByToken: found session for user ${maskEmail(user.email)}`);
522
503
 
523
504
  return {
524
505
  session: {
@@ -535,9 +516,7 @@ export class CoreBetterAuthService {
535
516
  },
536
517
  };
537
518
  } catch (error) {
538
- if (!this.isProd) {
539
- this.logger.debug(`getSessionByToken error: ${error instanceof Error ? error.message : 'Unknown error'}`);
540
- }
519
+ this.logger.debug(`getSessionByToken error: ${error instanceof Error ? error.message : 'Unknown error'}`);
541
520
  return { session: null, user: null };
542
521
  }
543
522
  }