@lenne.tech/nest-server 11.10.1 → 11.10.3

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 (75) 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/guards/auth.guard.d.ts +2 -2
  5. package/dist/core/modules/auth/guards/auth.guard.js +68 -8
  6. package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
  7. package/dist/core/modules/auth/guards/roles.guard.d.ts +3 -4
  8. package/dist/core/modules/auth/guards/roles.guard.js +64 -159
  9. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  10. package/dist/core/modules/better-auth/better-auth-token.service.d.ts +21 -0
  11. package/dist/core/modules/better-auth/better-auth-token.service.js +153 -0
  12. package/dist/core/modules/better-auth/better-auth-token.service.js.map +1 -0
  13. package/dist/core/modules/better-auth/better-auth.config.d.ts +3 -0
  14. package/dist/core/modules/better-auth/better-auth.config.js +176 -47
  15. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  16. package/dist/core/modules/better-auth/better-auth.types.d.ts +13 -0
  17. package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
  18. package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +5 -1
  19. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +101 -8
  20. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
  21. package/dist/core/modules/better-auth/core-better-auth-challenge.service.d.ts +20 -0
  22. package/dist/core/modules/better-auth/core-better-auth-challenge.service.js +142 -0
  23. package/dist/core/modules/better-auth/core-better-auth-challenge.service.js.map +1 -0
  24. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +1 -1
  25. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
  26. package/dist/core/modules/better-auth/core-better-auth-web.helper.d.ts +2 -0
  27. package/dist/core/modules/better-auth/core-better-auth-web.helper.js +29 -1
  28. package/dist/core/modules/better-auth/core-better-auth-web.helper.js.map +1 -1
  29. package/dist/core/modules/better-auth/core-better-auth.controller.js +5 -13
  30. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  31. package/dist/core/modules/better-auth/core-better-auth.middleware.d.ts +0 -1
  32. package/dist/core/modules/better-auth/core-better-auth.middleware.js +6 -19
  33. package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -1
  34. package/dist/core/modules/better-auth/core-better-auth.module.d.ts +6 -1
  35. package/dist/core/modules/better-auth/core-better-auth.module.js +82 -19
  36. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  37. package/dist/core/modules/better-auth/core-better-auth.resolver.js +7 -6
  38. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
  39. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +1 -2
  40. package/dist/core/modules/better-auth/core-better-auth.service.js +27 -37
  41. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  42. package/dist/core/modules/better-auth/index.d.ts +1 -0
  43. package/dist/core/modules/better-auth/index.js +1 -0
  44. package/dist/core/modules/better-auth/index.js.map +1 -1
  45. package/dist/core.module.js +4 -0
  46. package/dist/core.module.js.map +1 -1
  47. package/dist/server/modules/better-auth/better-auth.module.d.ts +4 -1
  48. package/dist/server/modules/better-auth/better-auth.module.js +4 -1
  49. package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
  50. package/dist/server/server.module.js +1 -4
  51. package/dist/server/server.module.js.map +1 -1
  52. package/dist/tsconfig.build.tsbuildinfo +1 -1
  53. package/package.json +1 -1
  54. package/src/config.env.ts +24 -174
  55. package/src/core/common/interfaces/server-options.interface.ts +288 -35
  56. package/src/core/modules/auth/guards/auth.guard.ts +136 -23
  57. package/src/core/modules/auth/guards/roles.guard.ts +119 -239
  58. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +82 -56
  59. package/src/core/modules/better-auth/README.md +132 -35
  60. package/src/core/modules/better-auth/better-auth-token.service.ts +241 -0
  61. package/src/core/modules/better-auth/better-auth.config.ts +402 -70
  62. package/src/core/modules/better-auth/better-auth.types.ts +37 -0
  63. package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +158 -18
  64. package/src/core/modules/better-auth/core-better-auth-challenge.service.ts +254 -0
  65. package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +1 -1
  66. package/src/core/modules/better-auth/core-better-auth-web.helper.ts +64 -1
  67. package/src/core/modules/better-auth/core-better-auth.controller.ts +7 -15
  68. package/src/core/modules/better-auth/core-better-auth.middleware.ts +7 -20
  69. package/src/core/modules/better-auth/core-better-auth.module.ts +182 -25
  70. package/src/core/modules/better-auth/core-better-auth.resolver.ts +8 -7
  71. package/src/core/modules/better-auth/core-better-auth.service.ts +40 -48
  72. package/src/core/modules/better-auth/index.ts +1 -0
  73. package/src/core.module.ts +8 -0
  74. package/src/server/modules/better-auth/better-auth.module.ts +40 -10
  75. 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
  }
@@ -9,14 +9,18 @@ import {
9
9
  Optional,
10
10
  Type,
11
11
  } from '@nestjs/common';
12
+ import { APP_GUARD } from '@nestjs/core';
12
13
  import { getConnectionToken } from '@nestjs/mongoose';
13
14
  import mongoose, { Connection } from 'mongoose';
14
15
 
15
16
  import { IBetterAuth } from '../../common/interfaces/server-options.interface';
16
17
  import { ConfigService } from '../../common/services/config.service';
18
+ import { RolesGuard } from '../auth/guards/roles.guard';
19
+ import { BetterAuthTokenService } from './better-auth-token.service';
17
20
  import { BetterAuthInstance, createBetterAuthInstance } from './better-auth.config';
18
21
  import { DefaultBetterAuthResolver } from './better-auth.resolver';
19
22
  import { CoreBetterAuthApiMiddleware } from './core-better-auth-api.middleware';
23
+ import { CoreBetterAuthChallengeService } from './core-better-auth-challenge.service';
20
24
  import { CoreBetterAuthRateLimitMiddleware } from './core-better-auth-rate-limit.middleware';
21
25
  import { CoreBetterAuthRateLimiter } from './core-better-auth-rate-limiter.service';
22
26
  import { CoreBetterAuthUserMapper } from './core-better-auth-user.mapper';
@@ -35,13 +39,14 @@ export const BETTER_AUTH_INSTANCE = 'BETTER_AUTH_INSTANCE';
35
39
  */
36
40
  export interface CoreBetterAuthModuleOptions {
37
41
  /**
38
- * Better-auth configuration.
42
+ * Better-auth configuration (optional - auto-read from ConfigService).
39
43
  * Accepts:
40
44
  * - `true`: Enable with all defaults (including JWT)
41
45
  * - `false`: Disable BetterAuth
42
46
  * - `{ ... }`: Enable with custom configuration
47
+ * - `undefined`: Auto-read from ConfigService (Zero-Config)
43
48
  */
44
- config: boolean | IBetterAuth;
49
+ config?: boolean | IBetterAuth;
45
50
 
46
51
  /**
47
52
  * Custom controller class to use instead of the default CoreBetterAuthController.
@@ -79,6 +84,19 @@ export interface CoreBetterAuthModuleOptions {
79
84
  */
80
85
  fallbackSecrets?: (string | undefined)[];
81
86
 
87
+ /**
88
+ * Register RolesGuard as a global guard.
89
+ *
90
+ * This should be set to `true` for IAM-only setups (CoreModule.forRoot with 1 parameter)
91
+ * where CoreAuthModule is not imported (which normally registers RolesGuard globally).
92
+ *
93
+ * When `true`, all `@Roles()` decorators will be enforced automatically without
94
+ * needing explicit `@UseGuards(RolesGuard)` on each endpoint.
95
+ *
96
+ * @default false
97
+ */
98
+ registerRolesGuardGlobally?: boolean;
99
+
82
100
  /**
83
101
  * Custom resolver class to use instead of the default DefaultBetterAuthResolver.
84
102
  * The class must extend CoreBetterAuthResolver.
@@ -103,19 +121,66 @@ export interface CoreBetterAuthModuleOptions {
103
121
  * ```
104
122
  */
105
123
  resolver?: Type<CoreBetterAuthResolver>;
124
+
125
+ /**
126
+ * Server-level app/frontend URL (from IServerOptions.appUrl).
127
+ * This is the frontend application URL where the browser runs.
128
+ *
129
+ * Used for:
130
+ * - CORS trustedOrigins
131
+ * - Passkey/WebAuthn origin
132
+ *
133
+ * Auto-Detection:
134
+ * - If not set, derived from `serverBaseUrl`:
135
+ * - 'https://api.example.com' → 'https://example.com'
136
+ * - 'https://example.com' → 'https://example.com'
137
+ * - When `serverEnv: 'local'` and not set: defaults to 'http://localhost:3001'
138
+ *
139
+ * @example 'https://example.com'
140
+ */
141
+ serverAppUrl?: string;
142
+
143
+ /**
144
+ * Server-level base URL (from IServerOptions.baseUrl).
145
+ * This is the API server URL.
146
+ *
147
+ * Used for:
148
+ * - Email links (password reset, verification)
149
+ * - OAuth callback URLs
150
+ * - As fallback for betterAuth.baseUrl
151
+ *
152
+ * Auto-Detection:
153
+ * - When `serverEnv: 'local'` and not set: defaults to 'http://localhost:3000'
154
+ *
155
+ * @example 'https://api.example.com'
156
+ */
157
+ serverBaseUrl?: string;
158
+
159
+ /**
160
+ * Server environment (from IServerOptions.env).
161
+ * Used for local environment defaults:
162
+ * - When `env: 'local'` and no URLs are set:
163
+ * - `serverBaseUrl` defaults to 'http://localhost:3000'
164
+ * - `serverAppUrl` defaults to 'http://localhost:3001'
165
+ */
166
+ serverEnv?: string;
106
167
  }
107
168
 
108
169
  /**
109
170
  * Normalizes betterAuth config from boolean | IBetterAuth to IBetterAuth | null
110
171
  * - `true` → `{}` (enabled with defaults)
111
172
  * - `false` → `null` (disabled)
112
- * - `undefined` → `null` (disabled for backward compatibility)
173
+ * - `undefined` → `{}` (enabled by default - zero-config)
174
+ * - `{ enabled: false }` → `null` (disabled)
113
175
  * - `{ ... }` → `{ ... }` (pass through)
114
176
  */
115
177
  function normalizeBetterAuthConfig(config: boolean | IBetterAuth | undefined): IBetterAuth | null {
116
- if (config === undefined || config === null) return null;
178
+ // BetterAuth is enabled by default (zero-config)
179
+ if (config === undefined || config === null) return {};
117
180
  if (config === true) return {};
118
181
  if (config === false) return null;
182
+ // Check for explicit { enabled: false }
183
+ if (typeof config === 'object' && config.enabled === false) return null;
119
184
  return config;
120
185
  }
121
186
 
@@ -152,6 +217,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
152
217
  private static currentConfig: IBetterAuth | null = null;
153
218
  private static customController: null | Type<CoreBetterAuthController> = null;
154
219
  private static customResolver: null | Type<CoreBetterAuthResolver> = null;
220
+ private static shouldRegisterRolesGuardGlobally = false;
155
221
 
156
222
  /**
157
223
  * Gets the controller class to use (custom or default)
@@ -251,10 +317,39 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
251
317
  * @returns Dynamic module configuration
252
318
  */
253
319
  static forRoot(options: CoreBetterAuthModuleOptions): DynamicModule {
254
- const { config: rawConfig, controller, fallbackSecrets, resolver } = options;
320
+ const {
321
+ config: rawConfig,
322
+ controller,
323
+ fallbackSecrets,
324
+ registerRolesGuardGlobally,
325
+ resolver,
326
+ serverAppUrl,
327
+ serverBaseUrl,
328
+ serverEnv,
329
+ } = options;
330
+
331
+ // Auto-read from global ConfigService if not explicitly provided
332
+ // This allows projects to use BetterAuthModule.forRoot({}) for true Zero-Config
333
+ // as all values are already available from CoreModule.forRoot(envConfig)
334
+ const globalConfig = ConfigService.configFastButReadOnly;
335
+
336
+ // Auto-detect config from ConfigService if not explicitly provided
337
+ const effectiveRawConfig = rawConfig ?? globalConfig?.betterAuth;
338
+
339
+ // Auto-detect fallbackSecrets from ConfigService if not explicitly provided
340
+ const effectiveFallbackSecrets = fallbackSecrets ?? (
341
+ globalConfig?.jwt
342
+ ? [globalConfig.jwt.secret, globalConfig.jwt.refresh?.secret].filter(Boolean)
343
+ : undefined
344
+ );
345
+
346
+ // Auto-detect server URLs from ConfigService if not explicitly provided
347
+ const effectiveServerAppUrl = serverAppUrl ?? globalConfig?.appUrl;
348
+ const effectiveServerBaseUrl = serverBaseUrl ?? globalConfig?.baseUrl;
349
+ const effectiveServerEnv = serverEnv ?? globalConfig?.env;
255
350
 
256
351
  // Normalize config: true → {}, false/undefined → null
257
- const config = normalizeBetterAuthConfig(rawConfig);
352
+ const config = normalizeBetterAuthConfig(effectiveRawConfig);
258
353
 
259
354
  // Store config for middleware configuration
260
355
  this.currentConfig = config;
@@ -262,6 +357,8 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
262
357
  this.customController = controller || null;
263
358
  // Store custom resolver if provided
264
359
  this.customResolver = resolver || null;
360
+ // Store whether to register RolesGuard globally (for IAM-only setups)
361
+ this.shouldRegisterRolesGuardGlobally = registerRolesGuardGlobally ?? false;
265
362
 
266
363
  // If better-auth is disabled (config is null or enabled: false), return minimal module
267
364
  // Note: We don't provide middleware classes when disabled because they depend on CoreBetterAuthService
@@ -270,7 +367,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
270
367
  this.logger.debug('BetterAuth is disabled - skipping initialization');
271
368
  this.betterAuthEnabled = false;
272
369
  return {
273
- exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter],
370
+ exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService, CoreBetterAuthChallengeService],
274
371
  module: CoreBetterAuthModule,
275
372
  providers: [
276
373
  {
@@ -282,6 +379,8 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
282
379
  CoreBetterAuthService,
283
380
  CoreBetterAuthUserMapper,
284
381
  CoreBetterAuthRateLimiter,
382
+ BetterAuthTokenService,
383
+ CoreBetterAuthChallengeService,
285
384
  ],
286
385
  };
287
386
  }
@@ -294,7 +393,12 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
294
393
 
295
394
  // Always use deferred initialization to ensure MongoDB is ready
296
395
  // This prevents timing issues during application startup
297
- return this.createDeferredModule(config, fallbackSecrets);
396
+ // Pass server-level URLs for Passkey auto-detection (using effective values from ConfigService fallback)
397
+ return this.createDeferredModule(config, effectiveFallbackSecrets, {
398
+ serverAppUrl: effectiveServerAppUrl,
399
+ serverBaseUrl: effectiveServerBaseUrl,
400
+ serverEnv: effectiveServerEnv,
401
+ });
298
402
  }
299
403
 
300
404
  /**
@@ -306,7 +410,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
306
410
  static forRootAsync(): DynamicModule {
307
411
  return {
308
412
  controllers: [this.getControllerClass()],
309
- exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter],
413
+ exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService, CoreBetterAuthChallengeService],
310
414
  imports: [],
311
415
  module: CoreBetterAuthModule,
312
416
  providers: [
@@ -380,6 +484,15 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
380
484
  CoreBetterAuthApiMiddleware,
381
485
  CoreBetterAuthRateLimiter,
382
486
  CoreBetterAuthRateLimitMiddleware,
487
+ // BetterAuthTokenService needs explicit factory to ensure proper dependency injection
488
+ {
489
+ inject: [CoreBetterAuthService, getConnectionToken()],
490
+ provide: BetterAuthTokenService,
491
+ useFactory: (betterAuthService: CoreBetterAuthService, connection: Connection) => {
492
+ return new BetterAuthTokenService(betterAuthService, connection);
493
+ },
494
+ },
495
+ CoreBetterAuthChallengeService,
383
496
  this.getResolverClass(),
384
497
  ],
385
498
  };
@@ -412,16 +525,25 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
412
525
  this.currentConfig = null;
413
526
  this.customController = null;
414
527
  this.customResolver = null;
528
+ this.shouldRegisterRolesGuardGlobally = false;
415
529
  }
416
530
 
417
531
  /**
418
532
  * Creates a deferred initialization module that waits for mongoose connection
419
533
  * By injecting the Connection token, NestJS ensures Mongoose is ready first
534
+ *
535
+ * @param config - BetterAuth configuration
536
+ * @param fallbackSecrets - Fallback secrets for backwards compatibility
537
+ * @param serverUrls - Server-level URLs for Passkey auto-detection
420
538
  */
421
- private static createDeferredModule(config: IBetterAuth, fallbackSecrets?: (string | undefined)[]): DynamicModule {
539
+ private static createDeferredModule(
540
+ config: IBetterAuth,
541
+ fallbackSecrets?: (string | undefined)[],
542
+ serverUrls?: { serverAppUrl?: string; serverBaseUrl?: string; serverEnv?: string },
543
+ ): DynamicModule {
422
544
  return {
423
545
  controllers: [this.getControllerClass()],
424
- exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter],
546
+ exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService, CoreBetterAuthChallengeService],
425
547
  module: CoreBetterAuthModule,
426
548
  providers: [
427
549
  {
@@ -438,9 +560,23 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
438
560
  if (!globalDb) {
439
561
  throw new Error('MongoDB database not available');
440
562
  }
441
- this.authInstance = createBetterAuthInstance({ config, db: globalDb, fallbackSecrets });
563
+ this.authInstance = createBetterAuthInstance({
564
+ config,
565
+ db: globalDb,
566
+ fallbackSecrets,
567
+ serverAppUrl: serverUrls?.serverAppUrl,
568
+ serverBaseUrl: serverUrls?.serverBaseUrl,
569
+ serverEnv: serverUrls?.serverEnv,
570
+ });
442
571
  } else {
443
- this.authInstance = createBetterAuthInstance({ config, db, fallbackSecrets });
572
+ this.authInstance = createBetterAuthInstance({
573
+ config,
574
+ db,
575
+ fallbackSecrets,
576
+ serverAppUrl: serverUrls?.serverAppUrl,
577
+ serverBaseUrl: serverUrls?.serverBaseUrl,
578
+ serverEnv: serverUrls?.serverEnv,
579
+ });
444
580
  }
445
581
 
446
582
  // IMPORTANT: Store the config AFTER createBetterAuthInstance mutates it
@@ -479,7 +615,26 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
479
615
  CoreBetterAuthApiMiddleware,
480
616
  CoreBetterAuthRateLimiter,
481
617
  CoreBetterAuthRateLimitMiddleware,
618
+ // BetterAuthTokenService needs explicit factory to ensure proper dependency injection
619
+ {
620
+ inject: [CoreBetterAuthService, getConnectionToken()],
621
+ provide: BetterAuthTokenService,
622
+ useFactory: (betterAuthService: CoreBetterAuthService, connection: Connection) => {
623
+ return new BetterAuthTokenService(betterAuthService, connection);
624
+ },
625
+ },
626
+ CoreBetterAuthChallengeService,
482
627
  this.getResolverClass(),
628
+ // Conditionally register RolesGuard globally for IAM-only setups
629
+ // In Legacy mode, RolesGuard is already registered globally via CoreAuthModule
630
+ ...(this.shouldRegisterRolesGuardGlobally
631
+ ? [
632
+ {
633
+ provide: APP_GUARD,
634
+ useClass: RolesGuard,
635
+ },
636
+ ]
637
+ : []),
483
638
  ],
484
639
  };
485
640
  }
@@ -492,24 +647,26 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
492
647
  private static logEnabledFeatures(config: IBetterAuth): void {
493
648
  const features: string[] = [];
494
649
 
495
- // Helper to check if a plugin is enabled
496
- // Supports: true, { enabled: true }, { ... } (enabled by default when config block present)
497
- // Disabled only when: false or { enabled: false }
498
- const isPluginEnabled = <T extends { enabled?: boolean }>(value: boolean | T | undefined): boolean => {
499
- if (value === undefined || value === false) return false;
500
- if (value === true) return true;
501
- return value.enabled !== false;
650
+ // Helper to check if a plugin is explicitly disabled
651
+ const isExplicitlyDisabled = <T extends { enabled?: boolean }>(value: boolean | T | undefined): boolean => {
652
+ if (value === false) return true;
653
+ if (typeof value === 'object' && value?.enabled === false) return true;
654
+ return false;
502
655
  };
503
656
 
504
- // Plugins are enabled by default when config block is present
505
- if (isPluginEnabled(config.jwt)) {
657
+ // JWT and 2FA are enabled by default unless explicitly disabled
658
+ if (!isExplicitlyDisabled(config.jwt)) {
506
659
  features.push('JWT');
507
660
  }
508
- if (isPluginEnabled(config.twoFactor)) {
661
+ if (!isExplicitlyDisabled(config.twoFactor)) {
509
662
  features.push('2FA/TOTP');
510
663
  }
511
- if (isPluginEnabled(config.passkey)) {
512
- features.push('Passkey/WebAuthn');
664
+ // Passkey is enabled by default, unless explicitly set to false
665
+ if (config.passkey !== false && !(typeof config.passkey === 'object' && config.passkey?.enabled === false)) {
666
+ const passkeyConfig = typeof config.passkey === 'object' ? config.passkey : null;
667
+ // Challenge storage is 'database' by default, can be overridden via config
668
+ const challengeStorage = passkeyConfig?.challengeStorage || 'database';
669
+ features.push(`Passkey/WebAuthn (challenges: ${challengeStorage})`);
513
670
  }
514
671
 
515
672
  // 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,
@@ -195,7 +196,7 @@ export class CoreBetterAuthResolver {
195
196
  async betterAuthSignIn(
196
197
  @Args('email') email: string,
197
198
  @Args('password') password: string,
198
- // eslint-disable-next-line unused-imports/no-unused-vars -- Reserved for future cookie/session handling
199
+
199
200
  @Context() _ctx: { req: Request; res: Response },
200
201
  ): Promise<CoreBetterAuthAuthModel> {
201
202
  this.ensureEnabled();
@@ -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
  /**
@@ -201,6 +198,19 @@ export class CoreBetterAuthService {
201
198
  return this.config.baseUrl || 'http://localhost:3000';
202
199
  }
203
200
 
201
+ /**
202
+ * Gets the session cookie name based on the configured base path.
203
+ *
204
+ * The cookie name follows the pattern: `{basePath}.session_token`
205
+ * For example, with basePath '/iam', the cookie name is 'iam.session_token'
206
+ *
207
+ * @returns The session cookie name
208
+ */
209
+ getSessionCookieName(): string {
210
+ const basePath = this.getBasePath()?.replace(/^\//, '').replace(/\//g, '.') || 'iam';
211
+ return `${basePath}.session_token`;
212
+ }
213
+
204
214
  // ===================================================================================================================
205
215
  // JWT Token Methods
206
216
  // ===================================================================================================================
@@ -307,17 +317,13 @@ export class CoreBetterAuthService {
307
317
  }
308
318
 
309
319
  // Debug: Log the cookie header being sent to api.getSession (masked for security)
310
- if (!this.isProd) {
311
- const cookieHeader = headers.get('cookie');
312
- this.logger.debug(`getSession called with cookies: ${maskCookieHeader(cookieHeader)}`);
313
- }
320
+ const cookieHeader = headers.get('cookie');
321
+ this.logger.debug(`getSession called with cookies: ${maskCookieHeader(cookieHeader)}`);
314
322
 
315
323
  const response = await api.getSession({ headers });
316
324
 
317
325
  // Debug: Log the response from api.getSession
318
- if (!this.isProd) {
319
- this.logger.debug(`getSession response: ${JSON.stringify(response)?.substring(0, 200)}`);
320
- }
326
+ this.logger.debug(`getSession response: ${JSON.stringify(response)?.substring(0, 200)}`);
321
327
 
322
328
  if (response && typeof response === 'object' && 'user' in response) {
323
329
  return response as SessionResult;
@@ -325,9 +331,7 @@ export class CoreBetterAuthService {
325
331
 
326
332
  return { session: null, user: null };
327
333
  } catch (error) {
328
- if (!this.isProd) {
329
- this.logger.debug(`getSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
330
- }
334
+ this.logger.debug(`getSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
331
335
  return { session: null, user: null };
332
336
  }
333
337
  }
@@ -370,9 +374,7 @@ export class CoreBetterAuthService {
370
374
  await api.signOut({ headers });
371
375
  return true;
372
376
  } catch (error) {
373
- if (!this.isProd) {
374
- this.logger.debug(`revokeSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
375
- }
377
+ this.logger.debug(`revokeSession error: ${error instanceof Error ? error.message : 'Unknown error'}`);
376
378
  return false;
377
379
  }
378
380
  }
@@ -481,31 +483,23 @@ export class CoreBetterAuthService {
481
483
  const result = results[0];
482
484
 
483
485
  if (!result) {
484
- if (!this.isProd) {
485
- this.logger.debug(`getSessionByToken: session not found for token ${maskToken(token)}`);
486
- }
486
+ this.logger.debug(`getSessionByToken: session not found for token ${maskToken(token)}`);
487
487
  return { session: null, user: null };
488
488
  }
489
489
 
490
490
  // Check if session is expired
491
491
  if (result.expiresAt && new Date(result.expiresAt) < new Date()) {
492
- if (!this.isProd) {
493
- this.logger.debug(`getSessionByToken: session expired`);
494
- }
492
+ this.logger.debug(`getSessionByToken: session expired`);
495
493
  return { session: null, user: null };
496
494
  }
497
495
 
498
496
  const user = result.userDoc;
499
497
  if (!user) {
500
- if (!this.isProd) {
501
- this.logger.debug(`getSessionByToken: user not found for session`);
502
- }
498
+ this.logger.debug(`getSessionByToken: user not found for session`);
503
499
  return { session: null, user: null };
504
500
  }
505
501
 
506
- if (!this.isProd) {
507
- this.logger.debug(`getSessionByToken: found session for user ${maskEmail(user.email)}`);
508
- }
502
+ this.logger.debug(`getSessionByToken: found session for user ${maskEmail(user.email)}`);
509
503
 
510
504
  return {
511
505
  session: {
@@ -522,9 +516,7 @@ export class CoreBetterAuthService {
522
516
  },
523
517
  };
524
518
  } catch (error) {
525
- if (!this.isProd) {
526
- this.logger.debug(`getSessionByToken error: ${error instanceof Error ? error.message : 'Unknown error'}`);
527
- }
519
+ this.logger.debug(`getSessionByToken error: ${error instanceof Error ? error.message : 'Unknown error'}`);
528
520
  return { session: null, user: null };
529
521
  }
530
522
  }
@@ -22,6 +22,7 @@
22
22
  * - DefaultBetterAuthResolver: Default resolver implementation (use as fallback)
23
23
  */
24
24
 
25
+ export * from './better-auth-token.service';
25
26
  export * from './better-auth.config';
26
27
  export * from './better-auth.resolver';
27
28
  export * from './better-auth.types';