@lenne.tech/nest-server 11.10.2 → 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 (56) 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/better-auth/better-auth.config.d.ts +3 -0
  5. package/dist/core/modules/better-auth/better-auth.config.js +176 -47
  6. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  7. package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +5 -1
  8. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +101 -8
  9. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
  10. package/dist/core/modules/better-auth/core-better-auth-challenge.service.d.ts +20 -0
  11. package/dist/core/modules/better-auth/core-better-auth-challenge.service.js +142 -0
  12. package/dist/core/modules/better-auth/core-better-auth-challenge.service.js.map +1 -0
  13. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +1 -1
  14. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
  15. package/dist/core/modules/better-auth/core-better-auth-web.helper.d.ts +2 -0
  16. package/dist/core/modules/better-auth/core-better-auth-web.helper.js +29 -1
  17. package/dist/core/modules/better-auth/core-better-auth-web.helper.js.map +1 -1
  18. package/dist/core/modules/better-auth/core-better-auth.controller.js +5 -13
  19. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  20. package/dist/core/modules/better-auth/core-better-auth.middleware.d.ts +0 -1
  21. package/dist/core/modules/better-auth/core-better-auth.middleware.js +6 -19
  22. package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -1
  23. package/dist/core/modules/better-auth/core-better-auth.module.d.ts +4 -1
  24. package/dist/core/modules/better-auth/core-better-auth.module.js +53 -19
  25. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  26. package/dist/core/modules/better-auth/core-better-auth.resolver.js +7 -6
  27. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
  28. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +0 -2
  29. package/dist/core/modules/better-auth/core-better-auth.service.js +23 -37
  30. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  31. package/dist/core.module.js +3 -0
  32. package/dist/core.module.js.map +1 -1
  33. package/dist/server/modules/better-auth/better-auth.module.d.ts +4 -1
  34. package/dist/server/modules/better-auth/better-auth.module.js +4 -1
  35. package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
  36. package/dist/server/server.module.js +1 -4
  37. package/dist/server/server.module.js.map +1 -1
  38. package/dist/tsconfig.build.tsbuildinfo +1 -1
  39. package/package.json +1 -1
  40. package/src/config.env.ts +24 -174
  41. package/src/core/common/interfaces/server-options.interface.ts +288 -35
  42. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +82 -56
  43. package/src/core/modules/better-auth/README.md +132 -35
  44. package/src/core/modules/better-auth/better-auth.config.ts +402 -70
  45. package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +158 -18
  46. package/src/core/modules/better-auth/core-better-auth-challenge.service.ts +254 -0
  47. package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +1 -1
  48. package/src/core/modules/better-auth/core-better-auth-web.helper.ts +64 -1
  49. package/src/core/modules/better-auth/core-better-auth.controller.ts +6 -14
  50. package/src/core/modules/better-auth/core-better-auth.middleware.ts +7 -20
  51. package/src/core/modules/better-auth/core-better-auth.module.ts +135 -25
  52. package/src/core/modules/better-auth/core-better-auth.resolver.ts +7 -6
  53. package/src/core/modules/better-auth/core-better-auth.service.ts +27 -48
  54. package/src/core.module.ts +5 -0
  55. package/src/server/modules/better-auth/better-auth.module.ts +40 -10
  56. 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
  }
@@ -20,6 +20,7 @@ import { BetterAuthTokenService } from './better-auth-token.service';
20
20
  import { BetterAuthInstance, createBetterAuthInstance } from './better-auth.config';
21
21
  import { DefaultBetterAuthResolver } from './better-auth.resolver';
22
22
  import { CoreBetterAuthApiMiddleware } from './core-better-auth-api.middleware';
23
+ import { CoreBetterAuthChallengeService } from './core-better-auth-challenge.service';
23
24
  import { CoreBetterAuthRateLimitMiddleware } from './core-better-auth-rate-limit.middleware';
24
25
  import { CoreBetterAuthRateLimiter } from './core-better-auth-rate-limiter.service';
25
26
  import { CoreBetterAuthUserMapper } from './core-better-auth-user.mapper';
@@ -38,13 +39,14 @@ export const BETTER_AUTH_INSTANCE = 'BETTER_AUTH_INSTANCE';
38
39
  */
39
40
  export interface CoreBetterAuthModuleOptions {
40
41
  /**
41
- * Better-auth configuration.
42
+ * Better-auth configuration (optional - auto-read from ConfigService).
42
43
  * Accepts:
43
44
  * - `true`: Enable with all defaults (including JWT)
44
45
  * - `false`: Disable BetterAuth
45
46
  * - `{ ... }`: Enable with custom configuration
47
+ * - `undefined`: Auto-read from ConfigService (Zero-Config)
46
48
  */
47
- config: boolean | IBetterAuth;
49
+ config?: boolean | IBetterAuth;
48
50
 
49
51
  /**
50
52
  * Custom controller class to use instead of the default CoreBetterAuthController.
@@ -119,19 +121,66 @@ export interface CoreBetterAuthModuleOptions {
119
121
  * ```
120
122
  */
121
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;
122
167
  }
123
168
 
124
169
  /**
125
170
  * Normalizes betterAuth config from boolean | IBetterAuth to IBetterAuth | null
126
171
  * - `true` → `{}` (enabled with defaults)
127
172
  * - `false` → `null` (disabled)
128
- * - `undefined` → `null` (disabled for backward compatibility)
173
+ * - `undefined` → `{}` (enabled by default - zero-config)
174
+ * - `{ enabled: false }` → `null` (disabled)
129
175
  * - `{ ... }` → `{ ... }` (pass through)
130
176
  */
131
177
  function normalizeBetterAuthConfig(config: boolean | IBetterAuth | undefined): IBetterAuth | null {
132
- if (config === undefined || config === null) return null;
178
+ // BetterAuth is enabled by default (zero-config)
179
+ if (config === undefined || config === null) return {};
133
180
  if (config === true) return {};
134
181
  if (config === false) return null;
182
+ // Check for explicit { enabled: false }
183
+ if (typeof config === 'object' && config.enabled === false) return null;
135
184
  return config;
136
185
  }
137
186
 
@@ -268,10 +317,39 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
268
317
  * @returns Dynamic module configuration
269
318
  */
270
319
  static forRoot(options: CoreBetterAuthModuleOptions): DynamicModule {
271
- const { config: rawConfig, controller, fallbackSecrets, registerRolesGuardGlobally, 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;
272
350
 
273
351
  // Normalize config: true → {}, false/undefined → null
274
- const config = normalizeBetterAuthConfig(rawConfig);
352
+ const config = normalizeBetterAuthConfig(effectiveRawConfig);
275
353
 
276
354
  // Store config for middleware configuration
277
355
  this.currentConfig = config;
@@ -289,7 +367,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
289
367
  this.logger.debug('BetterAuth is disabled - skipping initialization');
290
368
  this.betterAuthEnabled = false;
291
369
  return {
292
- exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService],
370
+ exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService, CoreBetterAuthChallengeService],
293
371
  module: CoreBetterAuthModule,
294
372
  providers: [
295
373
  {
@@ -302,6 +380,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
302
380
  CoreBetterAuthUserMapper,
303
381
  CoreBetterAuthRateLimiter,
304
382
  BetterAuthTokenService,
383
+ CoreBetterAuthChallengeService,
305
384
  ],
306
385
  };
307
386
  }
@@ -314,7 +393,12 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
314
393
 
315
394
  // Always use deferred initialization to ensure MongoDB is ready
316
395
  // This prevents timing issues during application startup
317
- 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
+ });
318
402
  }
319
403
 
320
404
  /**
@@ -326,7 +410,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
326
410
  static forRootAsync(): DynamicModule {
327
411
  return {
328
412
  controllers: [this.getControllerClass()],
329
- exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService],
413
+ exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService, CoreBetterAuthChallengeService],
330
414
  imports: [],
331
415
  module: CoreBetterAuthModule,
332
416
  providers: [
@@ -408,6 +492,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
408
492
  return new BetterAuthTokenService(betterAuthService, connection);
409
493
  },
410
494
  },
495
+ CoreBetterAuthChallengeService,
411
496
  this.getResolverClass(),
412
497
  ],
413
498
  };
@@ -446,11 +531,19 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
446
531
  /**
447
532
  * Creates a deferred initialization module that waits for mongoose connection
448
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
449
538
  */
450
- 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 {
451
544
  return {
452
545
  controllers: [this.getControllerClass()],
453
- exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService],
546
+ exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService, CoreBetterAuthChallengeService],
454
547
  module: CoreBetterAuthModule,
455
548
  providers: [
456
549
  {
@@ -467,9 +560,23 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
467
560
  if (!globalDb) {
468
561
  throw new Error('MongoDB database not available');
469
562
  }
470
- 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
+ });
471
571
  } else {
472
- 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
+ });
473
580
  }
474
581
 
475
582
  // IMPORTANT: Store the config AFTER createBetterAuthInstance mutates it
@@ -516,6 +623,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
516
623
  return new BetterAuthTokenService(betterAuthService, connection);
517
624
  },
518
625
  },
626
+ CoreBetterAuthChallengeService,
519
627
  this.getResolverClass(),
520
628
  // Conditionally register RolesGuard globally for IAM-only setups
521
629
  // In Legacy mode, RolesGuard is already registered globally via CoreAuthModule
@@ -539,24 +647,26 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
539
647
  private static logEnabledFeatures(config: IBetterAuth): void {
540
648
  const features: string[] = [];
541
649
 
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;
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;
549
655
  };
550
656
 
551
- // Plugins are enabled by default when config block is present
552
- if (isPluginEnabled(config.jwt)) {
657
+ // JWT and 2FA are enabled by default unless explicitly disabled
658
+ if (!isExplicitlyDisabled(config.jwt)) {
553
659
  features.push('JWT');
554
660
  }
555
- if (isPluginEnabled(config.twoFactor)) {
661
+ if (!isExplicitlyDisabled(config.twoFactor)) {
556
662
  features.push('2FA/TOTP');
557
663
  }
558
- if (isPluginEnabled(config.passkey)) {
559
- 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})`);
560
670
  }
561
671
 
562
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,
@@ -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
  }
@@ -265,6 +265,11 @@ export class CoreModule implements NestModule {
265
265
  // In IAM-only mode, register RolesGuard globally to enforce @Roles() decorators
266
266
  // In Legacy mode (autoRegister), RolesGuard is already registered via CoreAuthModule
267
267
  registerRolesGuardGlobally: isIamOnlyMode,
268
+ // Pass server-level URLs for Passkey auto-detection
269
+ // When env: 'local', defaults are: baseUrl=localhost:3000, appUrl=localhost:3001
270
+ serverAppUrl: config.appUrl,
271
+ serverBaseUrl: config.baseUrl,
272
+ serverEnv: config.env,
268
273
  }),
269
274
  );
270
275
  }
@@ -7,6 +7,19 @@ import { BetterAuthResolver } from './better-auth.resolver';
7
7
 
8
8
  /**
9
9
  * Options for BetterAuthModule.forRoot()
10
+ *
11
+ * All options are optional when using Zero-Config:
12
+ * All values are auto-read from ConfigService (set by CoreModule.forRoot)
13
+ *
14
+ * @example
15
+ * // Zero-Config - all values auto-detected from ConfigService
16
+ * BetterAuthModule.forRoot({})
17
+ *
18
+ * // Or with explicit overrides
19
+ * BetterAuthModule.forRoot({
20
+ * config: { secret: 'custom-secret' },
21
+ * serverAppUrl: 'https://custom-app.com',
22
+ * })
10
23
  */
11
24
  export interface ServerBetterAuthModuleOptions {
12
25
  /**
@@ -15,14 +28,33 @@ export interface ServerBetterAuthModuleOptions {
15
28
  * - `true`: Enable with all defaults (including JWT)
16
29
  * - `false`: Disable BetterAuth
17
30
  * - `{ ... }`: Enable with custom configuration
31
+ * - `undefined`: Auto-read from ConfigService (Zero-Config)
18
32
  */
19
- config: boolean | IBetterAuth;
33
+ config?: boolean | IBetterAuth;
20
34
 
21
35
  /**
22
36
  * Fallback secrets for backwards compatibility with JWT config.
23
37
  * If no betterAuth.secret is configured, these secrets are tried in order.
24
38
  */
25
39
  fallbackSecrets?: (string | undefined)[];
40
+
41
+ /**
42
+ * Server-level app URL for Passkey auto-detection.
43
+ * @see IServerOptions.appUrl
44
+ */
45
+ serverAppUrl?: string;
46
+
47
+ /**
48
+ * Server-level base URL for Passkey auto-detection.
49
+ * @see IServerOptions.baseUrl
50
+ */
51
+ serverBaseUrl?: string;
52
+
53
+ /**
54
+ * Server environment for localhost defaults (local, ci, e2e).
55
+ * @see IServerOptions.env
56
+ */
57
+ serverEnv?: string;
26
58
  }
27
59
 
28
60
  /**
@@ -42,14 +74,9 @@ export interface ServerBetterAuthModuleOptions {
42
74
  *
43
75
  * @Module({
44
76
  * imports: [
45
- * CoreModule.forRoot(CoreAuthService, AuthModule.forRoot(envConfig.jwt), {
46
- * ...envConfig,
47
- * betterAuth: { ...envConfig.betterAuth, autoRegister: false },
48
- * }),
49
- * BetterAuthModule.forRoot({
50
- * config: envConfig.betterAuth,
51
- * fallbackSecrets: [envConfig.jwt?.secret, envConfig.jwt?.refresh?.secret],
52
- * }),
77
+ * CoreModule.forRoot(CoreAuthService, AuthModule.forRoot(envConfig.jwt), envConfig),
78
+ * // Zero-Config: All values auto-read from ConfigService
79
+ * BetterAuthModule.forRoot({}),
53
80
  * ],
54
81
  * })
55
82
  * export class ServerModule {}
@@ -64,7 +91,7 @@ export class BetterAuthModule {
64
91
  * @returns Dynamic module configuration
65
92
  */
66
93
  static forRoot(options: ServerBetterAuthModuleOptions): DynamicModule {
67
- const { config, fallbackSecrets } = options;
94
+ const { config, fallbackSecrets, serverAppUrl, serverBaseUrl, serverEnv } = options;
68
95
 
69
96
  // If better-auth is explicitly disabled, return minimal module
70
97
  // Supports: false, { enabled: false }, or undefined/null
@@ -85,6 +112,9 @@ export class BetterAuthModule {
85
112
  controller: BetterAuthController,
86
113
  fallbackSecrets,
87
114
  resolver: BetterAuthResolver,
115
+ serverAppUrl,
116
+ serverBaseUrl,
117
+ serverEnv,
88
118
  }),
89
119
  ],
90
120
  module: BetterAuthModule,
@@ -45,11 +45,9 @@ import { ServerController } from './server.controller';
45
45
  AuthModule.forRoot(envConfig.jwt),
46
46
 
47
47
  // Include BetterAuthModule for better-auth integration
48
+ // Zero-Config: All values are auto-read from ConfigService (set by CoreModule.forRoot)
48
49
  // This allows project-specific customization via BetterAuthResolver
49
- BetterAuthModule.forRoot({
50
- config: envConfig.betterAuth,
51
- fallbackSecrets: [envConfig.jwt?.secret, envConfig.jwt?.refresh?.secret],
52
- }),
50
+ BetterAuthModule.forRoot({}),
53
51
 
54
52
  // Include ErrorCodeModule with project-specific error codes
55
53
  // Uses Core ErrorCodeModule.forRoot() with custom service and controller