@lenne.tech/nest-server 11.13.2 → 11.13.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 (45) hide show
  1. package/dist/config.env.js +230 -1
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/interfaces/server-options.interface.d.ts +2 -0
  4. package/dist/core/modules/better-auth/better-auth.config.js +40 -1
  5. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  6. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.d.ts +1 -0
  7. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +7 -0
  8. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
  9. package/dist/core/modules/better-auth/core-better-auth-models.d.ts +1 -0
  10. package/dist/core/modules/better-auth/core-better-auth-models.js +4 -0
  11. package/dist/core/modules/better-auth/core-better-auth-models.js.map +1 -1
  12. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +1 -0
  13. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +27 -0
  14. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
  15. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +14 -16
  16. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
  17. package/dist/core/modules/better-auth/core-better-auth.controller.js +14 -6
  18. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  19. package/dist/core/modules/better-auth/core-better-auth.module.js +44 -19
  20. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  21. package/dist/core/modules/better-auth/core-better-auth.resolver.js +3 -2
  22. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
  23. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +2 -0
  24. package/dist/core/modules/better-auth/core-better-auth.service.js +12 -4
  25. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  26. package/dist/core/modules/error-code/error-codes.d.ts +9 -0
  27. package/dist/core/modules/error-code/error-codes.js +8 -0
  28. package/dist/core/modules/error-code/error-codes.js.map +1 -1
  29. package/dist/server/modules/error-code/error-codes.d.ts +1 -0
  30. package/dist/tsconfig.build.tsbuildinfo +1 -1
  31. package/package.json +4 -4
  32. package/src/config.env.ts +259 -2
  33. package/src/core/common/interfaces/server-options.interface.ts +17 -7
  34. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +6 -0
  35. package/src/core/modules/better-auth/README.md +32 -0
  36. package/src/core/modules/better-auth/better-auth.config.ts +63 -2
  37. package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +16 -0
  38. package/src/core/modules/better-auth/core-better-auth-models.ts +3 -0
  39. package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +40 -0
  40. package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +24 -24
  41. package/src/core/modules/better-auth/core-better-auth.controller.ts +44 -16
  42. package/src/core/modules/better-auth/core-better-auth.module.ts +72 -37
  43. package/src/core/modules/better-auth/core-better-auth.resolver.ts +16 -9
  44. package/src/core/modules/better-auth/core-better-auth.service.ts +30 -7
  45. package/src/core/modules/error-code/error-codes.ts +9 -0
@@ -370,27 +370,38 @@ export class CoreBetterAuthUserMapper {
370
370
  return false;
371
371
  }
372
372
 
373
+ // Better-Auth stores account.userId as ObjectId that references users._id
374
+ const userMongoId = legacyUser._id as ObjectId;
375
+
376
+ // FAST PATH: Check if credential account already exists BEFORE expensive bcrypt
377
+ // For already-migrated users this avoids ~130ms of bcrypt.compare() per sign-in
378
+ const existingAccount = await accountsCollection.findOne({
379
+ providerId: 'credential',
380
+ userId: userMongoId,
381
+ });
382
+
383
+ if (existingAccount) {
384
+ return true;
385
+ }
386
+
387
+ // No password provided - cannot verify, cannot migrate
388
+ if (!plainPassword) {
389
+ return false;
390
+ }
391
+
373
392
  // IMPORTANT: Verify the provided password matches the legacy hash
374
393
  // This prevents migration with a wrong password
375
394
  // Legacy Auth uses two formats for backwards compatibility:
376
395
  // 1. bcrypt(password) - direct hash
377
396
  // 2. bcrypt(sha256(password)) - sha256 then bcrypt
378
- if (plainPassword) {
379
- const directMatch = await bcrypt.compare(plainPassword, legacyUser.password);
380
- const sha256Match = await bcrypt.compare(sha256(plainPassword), legacyUser.password);
381
- if (!directMatch && !sha256Match) {
382
- // Security: Wrong password provided for migration - reject
383
- this.logger.warn(`Migration password verification failed for ${maskEmail(userEmail)}`);
384
- return false;
385
- }
386
- } else {
387
- // No password provided - cannot verify, cannot migrate
397
+ const directMatch = await bcrypt.compare(plainPassword, legacyUser.password);
398
+ const sha256Match = !directMatch ? await bcrypt.compare(sha256(plainPassword), legacyUser.password) : false;
399
+ if (!directMatch && !sha256Match) {
400
+ // Security: Wrong password provided for migration - reject
401
+ this.logger.warn(`Migration password verification failed for ${maskEmail(userEmail)}`);
388
402
  return false;
389
403
  }
390
404
 
391
- // Better-Auth stores account.userId as ObjectId that references users._id
392
- // The id field is a secondary string identifier used in API responses
393
- const userMongoId = legacyUser._id as ObjectId;
394
405
  const userIdHex = userMongoId.toHexString();
395
406
 
396
407
  // Update user with Better-Auth fields if not already present
@@ -413,17 +424,6 @@ export class CoreBetterAuthUserMapper {
413
424
  );
414
425
  }
415
426
 
416
- // Check if credential account already exists
417
- // Better-Auth stores userId as ObjectId referencing users._id
418
- const existingAccount = await accountsCollection.findOne({
419
- providerId: 'credential',
420
- userId: userMongoId,
421
- });
422
-
423
- if (existingAccount) {
424
- return true;
425
- }
426
-
427
427
  // Create the credential account with Better-Auth compatible scrypt hash
428
428
  const passwordHash = await this.hashPasswordForBetterAuth(plainPassword);
429
429
 
@@ -14,7 +14,15 @@ import {
14
14
  Res,
15
15
  UnauthorizedException,
16
16
  } from '@nestjs/common';
17
- import { ApiBody, ApiCreatedResponse, ApiExcludeEndpoint, ApiOkResponse, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
17
+ import {
18
+ ApiBody,
19
+ ApiCreatedResponse,
20
+ ApiExcludeEndpoint,
21
+ ApiOkResponse,
22
+ ApiOperation,
23
+ ApiProperty,
24
+ ApiTags,
25
+ } from '@nestjs/swagger';
18
26
  import { Request, Response } from 'express';
19
27
 
20
28
  import { Roles } from '../../common/decorators/roles.decorator';
@@ -251,7 +259,10 @@ export class CoreBetterAuthController {
251
259
  * @since 11.13.0
252
260
  */
253
261
  @ApiOkResponse({ description: 'Better-Auth feature flags' })
254
- @ApiOperation({ description: 'Get enabled Better-Auth features for client-side feature detection', summary: 'Get Features' })
262
+ @ApiOperation({
263
+ description: 'Get enabled Better-Auth features for client-side feature detection',
264
+ summary: 'Get Features',
265
+ })
255
266
  @Get('features')
256
267
  @Roles(RoleEnum.S_EVERYONE)
257
268
  getFeatures(): Record<string, boolean | number | string[]> {
@@ -262,6 +273,7 @@ export class CoreBetterAuthController {
262
273
  passkey: this.betterAuthService.isPasskeyEnabled(),
263
274
  resendCooldownSeconds: this.emailVerificationService?.getConfig()?.resendCooldownSeconds ?? 60,
264
275
  signUpChecks: this.signUpValidator?.isEnabled() ?? false,
276
+ signUpEnabled: this.betterAuthService.isSignUpEnabled(),
265
277
  socialProviders: this.betterAuthService.getEnabledSocialProviders(),
266
278
  twoFactor: this.betterAuthService.isTwoFactorEnabled(),
267
279
  };
@@ -345,7 +357,9 @@ export class CoreBetterAuthController {
345
357
  // Without this, users with 2FA enabled but unverified email could bypass verification
346
358
  await this.checkEmailVerificationByEmail(input.email);
347
359
 
348
- this.logger.debug(`2FA required for ${maskEmail(input.email)}, forwarding to native handler for cookie handling`);
360
+ this.logger.debug(
361
+ `2FA required for ${maskEmail(input.email)}, forwarding to native handler for cookie handling`,
362
+ );
349
363
 
350
364
  // Forward to native Better Auth handler which sets the session cookie correctly
351
365
  // We need to modify the request body to use the normalized password
@@ -370,7 +384,7 @@ export class CoreBetterAuthController {
370
384
  body: modifiedBody,
371
385
  headers: new Headers({
372
386
  'Content-Type': 'application/json',
373
- 'Origin': req.headers.origin || baseUrl,
387
+ Origin: req.headers.origin || baseUrl,
374
388
  }),
375
389
  method: 'POST',
376
390
  });
@@ -407,7 +421,8 @@ export class CoreBetterAuthController {
407
421
  const mappedUser = await this.userMapper.mapSessionUser(response.user);
408
422
 
409
423
  // Get token: JWT accessToken > top-level token > session.token
410
- const rawToken = responseAny.accessToken || responseAny.token || (hasSession(response) ? response.session.token : undefined);
424
+ const rawToken =
425
+ responseAny.accessToken || responseAny.token || (hasSession(response) ? response.session.token : undefined);
411
426
  const token = await this.resolveJwtToken(rawToken);
412
427
 
413
428
  const result: CoreBetterAuthResponse = {
@@ -464,6 +479,7 @@ export class CoreBetterAuthController {
464
479
  @Body() input: CoreBetterAuthSignUpInput,
465
480
  ): Promise<CoreBetterAuthResponse> {
466
481
  this.ensureEnabled();
482
+ this.betterAuthService.ensureSignUpEnabled();
467
483
 
468
484
  // Validate sign-up input (termsAndPrivacyAccepted is required by default)
469
485
  if (this.signUpValidator) {
@@ -494,7 +510,9 @@ export class CoreBetterAuthController {
494
510
  if (hasUser(response)) {
495
511
  // Link or create user in our database
496
512
  // Pass termsAndPrivacyAccepted to store the acceptance timestamp
497
- await this.userMapper.linkOrCreateUser(response.user, { termsAndPrivacyAccepted: input.termsAndPrivacyAccepted });
513
+ await this.userMapper.linkOrCreateUser(response.user, {
514
+ termsAndPrivacyAccepted: input.termsAndPrivacyAccepted,
515
+ });
498
516
 
499
517
  // Sync password to legacy (enables IAM Sign-Up → Legacy Sign-In)
500
518
  // Pass the plain password so it can be hashed with bcrypt for Legacy Auth
@@ -506,7 +524,8 @@ export class CoreBetterAuthController {
506
524
  // Without this, no session cookies are set after sign-up, causing 401 on
507
525
  // subsequent authenticated requests (e.g., Passkey, 2FA, /token)
508
526
  const responseAny = response as any;
509
- const rawToken = responseAny.accessToken || responseAny.token || (hasSession(response) ? response.session.token : undefined);
527
+ const rawToken =
528
+ responseAny.accessToken || responseAny.token || (hasSession(response) ? response.session.token : undefined);
510
529
  const token = await this.resolveJwtToken(rawToken);
511
530
 
512
531
  // If email verification is enabled, revoke the session and don't return session data
@@ -518,7 +537,9 @@ export class CoreBetterAuthController {
518
537
  await this.betterAuthService.revokeSession(sessionToken);
519
538
  }
520
539
  this.clearAuthCookies(res);
521
- this.logger.debug(`[SignUp] Email verification required for ${maskEmail(response.user.email)}, session revoked`);
540
+ this.logger.debug(
541
+ `[SignUp] Email verification required for ${maskEmail(response.user.email)}, session revoked`,
542
+ );
522
543
  return {
523
544
  emailVerificationRequired: true,
524
545
  requiresTwoFactor: false,
@@ -692,10 +713,7 @@ export class CoreBetterAuthController {
692
713
  * @throws UnauthorizedException if email is not verified and verification is required
693
714
  */
694
715
  protected checkEmailVerification(sessionUser: BetterAuthSessionUser): void {
695
- if (
696
- this.emailVerificationService?.isEnabled()
697
- && !sessionUser.emailVerified
698
- ) {
716
+ if (this.emailVerificationService?.isEnabled() && !sessionUser.emailVerified) {
699
717
  this.logger.debug(`[SignIn] Email not verified for ${maskEmail(sessionUser.email)}, blocking login`);
700
718
  throw new UnauthorizedException(ErrorCode.EMAIL_VERIFICATION_REQUIRED);
701
719
  }
@@ -764,7 +782,9 @@ export class CoreBetterAuthController {
764
782
  * NOTE: The session token is intentionally NOT included in the response.
765
783
  * It is set as an httpOnly cookie for security.
766
784
  */
767
- protected mapSession(session: null | undefined | { expiresAt: Date; id: string; token?: string }): CoreBetterAuthSessionInfo | undefined {
785
+ protected mapSession(
786
+ session: null | undefined | { expiresAt: Date; id: string; token?: string },
787
+ ): CoreBetterAuthSessionInfo | undefined {
768
788
  if (!session) return undefined;
769
789
  return {
770
790
  expiresAt: session.expiresAt instanceof Date ? session.expiresAt.toISOString() : String(session.expiresAt),
@@ -778,7 +798,7 @@ export class CoreBetterAuthController {
778
798
  * @param sessionUser - The user from Better-Auth session
779
799
  * @param _mappedUser - The synced user from legacy system (available for override customization)
780
800
  */
781
-
801
+
782
802
  protected mapUser(sessionUser: BetterAuthSessionUser, _mappedUser: any): CoreBetterAuthUserResponse {
783
803
  return {
784
804
  email: sessionUser.email,
@@ -802,7 +822,11 @@ export class CoreBetterAuthController {
802
822
  * @param result - The CoreBetterAuthResponse to return
803
823
  * @param sessionToken - Optional session token to set in cookies (if not provided, uses result.token)
804
824
  */
805
- protected processCookies(res: Response, result: CoreBetterAuthResponse, sessionToken?: string): CoreBetterAuthResponse {
825
+ protected processCookies(
826
+ res: Response,
827
+ result: CoreBetterAuthResponse,
828
+ sessionToken?: string,
829
+ ): CoreBetterAuthResponse {
806
830
  const cookiesEnabled = this.configService.getFastButReadOnly('cookies') !== false;
807
831
 
808
832
  // If a specific session token is provided, use it directly
@@ -883,7 +907,11 @@ export class CoreBetterAuthController {
883
907
  this.logger.error(`Better Auth handler error: ${error instanceof Error ? error.message : 'Unknown error'}`);
884
908
 
885
909
  // Re-throw NestJS exceptions
886
- if (error instanceof BadRequestException || error instanceof UnauthorizedException || error instanceof InternalServerErrorException) {
910
+ if (
911
+ error instanceof BadRequestException ||
912
+ error instanceof UnauthorizedException ||
913
+ error instanceof InternalServerErrorException
914
+ ) {
887
915
  throw error;
888
916
  }
889
917
 
@@ -313,15 +313,17 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
313
313
  if (CoreBetterAuthModule.currentConfig) {
314
314
  const globalConfig = ConfigService.configFastButReadOnly;
315
315
  const cookiesDisabled = globalConfig?.cookies === false;
316
- const jwtExplicitlyDisabled = CoreBetterAuthModule.currentConfig.jwt === false
317
- || (typeof CoreBetterAuthModule.currentConfig.jwt === 'object' && CoreBetterAuthModule.currentConfig.jwt?.enabled === false);
316
+ const jwtExplicitlyDisabled =
317
+ CoreBetterAuthModule.currentConfig.jwt === false ||
318
+ (typeof CoreBetterAuthModule.currentConfig.jwt === 'object' &&
319
+ CoreBetterAuthModule.currentConfig.jwt?.enabled === false);
318
320
 
319
321
  if (cookiesDisabled && jwtExplicitlyDisabled) {
320
322
  CoreBetterAuthModule.logger.warn(
321
323
  'CONFIGURATION WARNING: cookies is set to false, but betterAuth.jwt is not enabled. ' +
322
- 'Without cookies, BetterAuth cannot establish sessions via Set-Cookie headers. ' +
323
- 'Enable betterAuth.jwt (set jwt: true in betterAuth config) to use Bearer token authentication, ' +
324
- 'or set cookies: true to use cookie-based sessions.',
324
+ 'Without cookies, BetterAuth cannot establish sessions via Set-Cookie headers. ' +
325
+ 'Enable betterAuth.jwt (set jwt: true in betterAuth config) to use Bearer token authentication, ' +
326
+ 'or set cookies: true to use cookie-based sessions.',
325
327
  );
326
328
  }
327
329
  }
@@ -331,8 +333,8 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
331
333
  if (CoreBetterAuthModule.rolesGuardExplicitlyDisabled && !RolesGuardRegistry.isRegistered()) {
332
334
  CoreBetterAuthModule.logger.warn(
333
335
  '⚠️ SECURITY WARNING: registerRolesGuardGlobally is explicitly set to false, ' +
334
- 'but no RolesGuard is registered globally. @Roles() decorators will NOT enforce access control! ' +
335
- 'Either set registerRolesGuardGlobally: true, or ensure CoreAuthModule (Legacy) is imported.',
336
+ 'but no RolesGuard is registered globally. @Roles() decorators will NOT enforce access control! ' +
337
+ 'Either set registerRolesGuardGlobally: true, or ensure CoreAuthModule (Legacy) is imported.',
336
338
  );
337
339
  }
338
340
  }
@@ -422,10 +424,10 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
422
424
  if (this.forRootCalled && !process.env.VITEST) {
423
425
  this.logger.warn(
424
426
  'CoreBetterAuthModule.forRoot() was called more than once. ' +
425
- 'The second call is ignored by NestJS (DynamicModule deduplication). ' +
426
- 'Custom controller/resolver from the second call will NOT be registered. ' +
427
- 'Solutions: (1) Use betterAuth.controller/resolver in config, or ' +
428
- '(2) Set betterAuth.autoRegister: false and import your module separately.',
427
+ 'The second call is ignored by NestJS (DynamicModule deduplication). ' +
428
+ 'Custom controller/resolver from the second call will NOT be registered. ' +
429
+ 'Solutions: (1) Use betterAuth.controller/resolver in config, or ' +
430
+ '(2) Set betterAuth.autoRegister: false and import your module separately.',
429
431
  );
430
432
  if (this.cachedDynamicModule) {
431
433
  return this.cachedDynamicModule;
@@ -453,11 +455,9 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
453
455
  const effectiveRawConfig = rawConfig ?? globalConfig?.betterAuth;
454
456
 
455
457
  // Auto-detect fallbackSecrets from ConfigService if not explicitly provided
456
- const effectiveFallbackSecrets = fallbackSecrets ?? (
457
- globalConfig?.jwt
458
- ? [globalConfig.jwt.secret, globalConfig.jwt.refresh?.secret].filter(Boolean)
459
- : undefined
460
- );
458
+ const effectiveFallbackSecrets =
459
+ fallbackSecrets ??
460
+ (globalConfig?.jwt ? [globalConfig.jwt.secret, globalConfig.jwt.refresh?.secret].filter(Boolean) : undefined);
461
461
 
462
462
  // Auto-detect server URLs from ConfigService if not explicitly provided
463
463
  const effectiveServerAppUrl = serverAppUrl ?? globalConfig?.appUrl;
@@ -489,7 +489,14 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
489
489
  this.logger.debug('BetterAuth is disabled - skipping initialization');
490
490
  this.betterAuthEnabled = false;
491
491
  this.cachedDynamicModule = {
492
- exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService, CoreBetterAuthChallengeService],
492
+ exports: [
493
+ BETTER_AUTH_INSTANCE,
494
+ CoreBetterAuthService,
495
+ CoreBetterAuthUserMapper,
496
+ CoreBetterAuthRateLimiter,
497
+ BetterAuthTokenService,
498
+ CoreBetterAuthChallengeService,
499
+ ],
493
500
  module: CoreBetterAuthModule,
494
501
  providers: [
495
502
  {
@@ -537,7 +544,16 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
537
544
  static forRootAsync(): DynamicModule {
538
545
  return {
539
546
  controllers: [this.getControllerClass()],
540
- exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService, CoreBetterAuthChallengeService, CoreBetterAuthEmailVerificationService, CoreBetterAuthSignUpValidatorService],
547
+ exports: [
548
+ BETTER_AUTH_INSTANCE,
549
+ CoreBetterAuthService,
550
+ CoreBetterAuthUserMapper,
551
+ CoreBetterAuthRateLimiter,
552
+ BetterAuthTokenService,
553
+ CoreBetterAuthChallengeService,
554
+ CoreBetterAuthEmailVerificationService,
555
+ CoreBetterAuthSignUpValidatorService,
556
+ ],
541
557
  imports: [],
542
558
  module: CoreBetterAuthModule,
543
559
  providers: [
@@ -559,7 +575,10 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
559
575
  {
560
576
  inject: [ConfigService, CoreBetterAuthEmailVerificationService],
561
577
  provide: BETTER_AUTH_INSTANCE,
562
- useFactory: async (configService: ConfigService, emailVerificationService: CoreBetterAuthEmailVerificationService) => {
578
+ useFactory: async (
579
+ configService: ConfigService,
580
+ emailVerificationService: CoreBetterAuthEmailVerificationService,
581
+ ) => {
563
582
  // Set static reference for callbacks BEFORE creating Better-Auth instance
564
583
  this.setEmailVerificationService(emailVerificationService);
565
584
 
@@ -608,9 +627,8 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
608
627
  // The original config object may be frozen (from ConfigService), so we
609
628
  // create a shallow copy with the resolved fallback secret applied.
610
629
  const resolvedSecret = config.secret || fallbackSecrets?.find((s) => s && s.length >= 32);
611
- this.currentConfig = resolvedSecret && resolvedSecret !== config.secret
612
- ? { ...config, secret: resolvedSecret }
613
- : config;
630
+ this.currentConfig =
631
+ resolvedSecret && resolvedSecret !== config.secret ? { ...config, secret: resolvedSecret } : config;
614
632
 
615
633
  if (this.authInstance) {
616
634
  this.logger.log('BetterAuth initialized successfully');
@@ -725,7 +743,11 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
725
743
  */
726
744
  private static createEmailVerificationCallbacks(): {
727
745
  onEmailVerified: (userId: string) => Promise<void>;
728
- sendVerificationEmail: (options: { token: string; url: string; user: { email: string; id: string; name?: null | string } }) => Promise<void>;
746
+ sendVerificationEmail: (options: {
747
+ token: string;
748
+ url: string;
749
+ user: { email: string; id: string; name?: null | string };
750
+ }) => Promise<void>;
729
751
  } {
730
752
  return {
731
753
  onEmailVerified: async (userId: string) => {
@@ -735,16 +757,17 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
735
757
  const db = this.mongoConnection?.db;
736
758
  if (db) {
737
759
  const { ObjectId } = await import('mongodb');
738
- await db.collection('users').updateOne(
739
- { _id: new ObjectId(userId) },
740
- { $set: { verified: true, verifiedAt: new Date() } },
741
- );
760
+ await db
761
+ .collection('users')
762
+ .updateOne({ _id: new ObjectId(userId) }, { $set: { verified: true, verifiedAt: new Date() } });
742
763
  this.logger.debug(`Email verified for user ${userId} - synced verified/verifiedAt`);
743
764
  } else {
744
765
  this.logger.warn(`Cannot sync verifiedAt for user ${userId} - no database connection`);
745
766
  }
746
767
  } catch (error) {
747
- this.logger.error(`Failed to sync verifiedAt for user ${userId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
768
+ this.logger.error(
769
+ `Failed to sync verifiedAt for user ${userId}: ${error instanceof Error ? error.message : 'Unknown error'}`,
770
+ );
748
771
  }
749
772
  },
750
773
  sendVerificationEmail: async (options) => {
@@ -778,7 +801,16 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
778
801
  ): DynamicModule {
779
802
  return {
780
803
  controllers: [this.getControllerClass()],
781
- exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService, CoreBetterAuthChallengeService, CoreBetterAuthEmailVerificationService, CoreBetterAuthSignUpValidatorService],
804
+ exports: [
805
+ BETTER_AUTH_INSTANCE,
806
+ CoreBetterAuthService,
807
+ CoreBetterAuthUserMapper,
808
+ CoreBetterAuthRateLimiter,
809
+ BetterAuthTokenService,
810
+ CoreBetterAuthChallengeService,
811
+ CoreBetterAuthEmailVerificationService,
812
+ CoreBetterAuthSignUpValidatorService,
813
+ ],
782
814
  module: CoreBetterAuthModule,
783
815
  providers: [
784
816
  // Optional BrevoService: uses factory to avoid constructor error when brevo config is missing
@@ -801,7 +833,10 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
801
833
  // Also inject EmailVerificationService to set static reference before Better-Auth init
802
834
  inject: [getConnectionToken(), CoreBetterAuthEmailVerificationService],
803
835
  provide: BETTER_AUTH_INSTANCE,
804
- useFactory: async (connection: Connection, emailVerificationService: CoreBetterAuthEmailVerificationService) => {
836
+ useFactory: async (
837
+ connection: Connection,
838
+ emailVerificationService: CoreBetterAuthEmailVerificationService,
839
+ ) => {
805
840
  // Set static references for callbacks BEFORE creating Better-Auth instance
806
841
  this.setEmailVerificationService(emailVerificationService);
807
842
  this.mongoConnection = connection;
@@ -843,9 +878,8 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
843
878
  // Store a config copy with the resolved secret (same as first forRoot variant)
844
879
  const fallbacks = options?.fallbackSecrets;
845
880
  const resolvedSecret2 = config.secret || fallbacks?.find((s) => s && s.length >= 32);
846
- this.currentConfig = resolvedSecret2 && resolvedSecret2 !== config.secret
847
- ? { ...config, secret: resolvedSecret2 }
848
- : config;
881
+ this.currentConfig =
882
+ resolvedSecret2 && resolvedSecret2 !== config.secret ? { ...config, secret: resolvedSecret2 } : config;
849
883
 
850
884
  // Keep static betterAuthEnabled in sync with the authInstance state.
851
885
  // This is important because forRoot() sets it synchronously, but reset()
@@ -907,10 +941,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
907
941
  ...(this.shouldRegisterRolesGuardGlobally && !RolesGuardRegistry.isRegistered()
908
942
  ? (() => {
909
943
  RolesGuardRegistry.markRegistered('CoreBetterAuthModule');
910
- return [
911
- BetterAuthRolesGuard,
912
- { provide: APP_GUARD, useExisting: BetterAuthRolesGuard },
913
- ];
944
+ return [BetterAuthRolesGuard, { provide: APP_GUARD, useExisting: BetterAuthRolesGuard }];
914
945
  })()
915
946
  : []),
916
947
  ],
@@ -979,6 +1010,10 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
979
1010
  features.push('Sign-Up Checks');
980
1011
  }
981
1012
 
1013
+ if (config.emailAndPassword?.disableSignUp) {
1014
+ features.push('Sign-Up Disabled');
1015
+ }
1016
+
982
1017
  if (features.length > 0) {
983
1018
  this.logger.log(`Enabled features: ${features.join(', ')}`);
984
1019
  }
@@ -107,10 +107,7 @@ export class CoreBetterAuthResolver {
107
107
  * @throws UnauthorizedException if email is not verified and verification is required
108
108
  */
109
109
  protected checkEmailVerification(sessionUser: BetterAuthSessionUser): void {
110
- if (
111
- this.emailVerificationService?.isEnabled()
112
- && !sessionUser.emailVerified
113
- ) {
110
+ if (this.emailVerificationService?.isEnabled() && !sessionUser.emailVerified) {
114
111
  this.logger.debug(`[SignIn] Email not verified for ${maskEmail(sessionUser.email)}, blocking login`);
115
112
  throw new UnauthorizedException(ErrorCode.EMAIL_VERIFICATION_REQUIRED);
116
113
  }
@@ -193,6 +190,7 @@ export class CoreBetterAuthResolver {
193
190
  enabled: this.betterAuthService.isEnabled(),
194
191
  jwt: this.betterAuthService.isJwtEnabled(),
195
192
  passkey: this.betterAuthService.isPasskeyEnabled(),
193
+ signUpEnabled: this.betterAuthService.isSignUpEnabled(),
196
194
  socialProviders: this.betterAuthService.getEnabledSocialProviders(),
197
195
  twoFactor: this.betterAuthService.isTwoFactorEnabled(),
198
196
  };
@@ -303,12 +301,16 @@ export class CoreBetterAuthResolver {
303
301
  body: { email, password },
304
302
  })) as BetterAuthSignInResponse | null;
305
303
 
306
- this.logger.debug(`[SignIn] API response for ${maskEmail(email)}: ${JSON.stringify(response)?.substring(0, 200)}`);
304
+ this.logger.debug(
305
+ `[SignIn] API response for ${maskEmail(email)}: ${JSON.stringify(response)?.substring(0, 200)}`,
306
+ );
307
307
 
308
308
  // Check if response indicates an error (Better-Auth returns error objects, not throws)
309
309
  const responseAny = response as any;
310
310
  if (responseAny?.error || responseAny?.code === 'CREDENTIAL_ACCOUNT_NOT_FOUND') {
311
- this.logger.debug(`[SignIn] API returned error for ${maskEmail(email)}: ${responseAny?.error || responseAny?.code}`);
311
+ this.logger.debug(
312
+ `[SignIn] API returned error for ${maskEmail(email)}: ${responseAny?.error || responseAny?.code}`,
313
+ );
312
314
  throw new Error(responseAny?.error || responseAny?.code || 'Credential account not found');
313
315
  }
314
316
 
@@ -341,7 +343,8 @@ export class CoreBetterAuthResolver {
341
343
  // 2. token (top-level, some BetterAuth versions)
342
344
  // 3. session.token (session-based fallback)
343
345
  const responseAny = response as any;
344
- const rawToken = responseAny.accessToken || responseAny.token || (hasSession(response) ? response.session.token : undefined);
346
+ const rawToken =
347
+ responseAny.accessToken || responseAny.token || (hasSession(response) ? response.session.token : undefined);
345
348
  const token = await this.resolveJwtToken(rawToken);
346
349
 
347
350
  return {
@@ -410,7 +413,8 @@ export class CoreBetterAuthResolver {
410
413
  // 2. token (top-level, some BetterAuth versions)
411
414
  // 3. session.token (session-based fallback)
412
415
  const responseAny = response as any;
413
- const rawToken = responseAny.accessToken || responseAny.token || (hasSession(response) ? response.session.token : undefined);
416
+ const rawToken =
417
+ responseAny.accessToken || responseAny.token || (hasSession(response) ? response.session.token : undefined);
414
418
  const token = await this.resolveJwtToken(rawToken);
415
419
 
416
420
  return {
@@ -448,6 +452,7 @@ export class CoreBetterAuthResolver {
448
452
  @Args('termsAndPrivacyAccepted', { nullable: true }) termsAndPrivacyAccepted?: boolean,
449
453
  ): Promise<CoreBetterAuthAuthModel> {
450
454
  this.ensureEnabled();
455
+ this.betterAuthService.ensureSignUpEnabled();
451
456
 
452
457
  // Validate sign-up input (termsAndPrivacyAccepted is required by default)
453
458
  if (this.signUpValidator) {
@@ -487,7 +492,9 @@ export class CoreBetterAuthResolver {
487
492
  if (sessionToken) {
488
493
  await this.betterAuthService.revokeSession(sessionToken);
489
494
  }
490
- this.logger.debug(`[SignUp] Email verification required for ${maskEmail(sessionUser.email)}, session revoked`);
495
+ this.logger.debug(
496
+ `[SignUp] Email verification required for ${maskEmail(sessionUser.email)}, session revoked`,
497
+ );
491
498
  return {
492
499
  emailVerificationRequired: true,
493
500
  requiresTwoFactor: false,
@@ -1,4 +1,4 @@
1
- import { Inject, Injectable, Logger, Optional } from '@nestjs/common';
1
+ import { BadRequestException, Inject, Injectable, Logger, Optional } from '@nestjs/common';
2
2
  import { InjectConnection } from '@nestjs/mongoose';
3
3
  import { Request } from 'express';
4
4
  import { importJWK, jwtVerify } from 'jose';
@@ -7,6 +7,7 @@ import { Connection } from 'mongoose';
7
7
  import { 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
+ import { ErrorCode } from '../error-code/error-codes';
10
11
  import { BetterAuthInstance } from './better-auth.config';
11
12
  import { BetterAuthSessionUser } from './core-better-auth-user.mapper';
12
13
  import { convertExpressHeaders, parseCookieHeader, signCookieValueIfNeeded } from './core-better-auth-web.helper';
@@ -155,6 +156,26 @@ export class CoreBetterAuthService {
155
156
  return true;
156
157
  }
157
158
 
159
+ /**
160
+ * Checks if sign-up is enabled.
161
+ * Sign-up is enabled by default unless explicitly disabled via
162
+ * emailAndPassword.disableSignUp: true
163
+ */
164
+ isSignUpEnabled(): boolean {
165
+ if (!this.isEnabled()) return false;
166
+ return this.config.emailAndPassword?.disableSignUp !== true;
167
+ }
168
+
169
+ /**
170
+ * Throws BadRequestException if sign-up is disabled.
171
+ * Used by Controller and Resolver as a guard before sign-up logic.
172
+ */
173
+ ensureSignUpEnabled(): void {
174
+ if (!this.isSignUpEnabled()) {
175
+ throw new BadRequestException(ErrorCode.SIGNUP_DISABLED);
176
+ }
177
+ }
178
+
158
179
  /**
159
180
  * Gets the list of enabled social providers
160
181
  * Dynamically iterates over all configured providers.
@@ -593,10 +614,7 @@ export class CoreBetterAuthService {
593
614
  {
594
615
  $match: {
595
616
  $expr: {
596
- $or: [
597
- { $eq: ['$userId', userId] },
598
- { $eq: [{ $toString: '$userId' }, userId] },
599
- ],
617
+ $or: [{ $eq: ['$userId', userId] }, { $eq: [{ $toString: '$userId' }, userId] }],
600
618
  },
601
619
  expiresAt: { $gt: new Date() },
602
620
  },
@@ -690,7 +708,9 @@ export class CoreBetterAuthService {
690
708
  }
691
709
  return !!result.user.emailVerified;
692
710
  } catch (error) {
693
- this.logger.debug(`isUserEmailVerified error for ${maskEmail(email)}: ${error instanceof Error ? error.message : 'Unknown error'}`);
711
+ this.logger.debug(
712
+ `isUserEmailVerified error for ${maskEmail(email)}: ${error instanceof Error ? error.message : 'Unknown error'}`,
713
+ );
694
714
  return null;
695
715
  }
696
716
  }
@@ -867,7 +887,10 @@ export class CoreBetterAuthService {
867
887
  * @param token - Optional JWT token to verify directly (bypasses header extraction)
868
888
  * @returns The JWT payload with user info, or null if no valid token
869
889
  */
870
- async verifyJwtFromRequest(req: Request, token?: string): Promise<null | {
890
+ async verifyJwtFromRequest(
891
+ req: Request,
892
+ token?: string,
893
+ ): Promise<null | {
871
894
  [key: string]: any;
872
895
  email?: string;
873
896
  sub: string;
@@ -278,6 +278,15 @@ export const LtnsErrors = {
278
278
  },
279
279
  },
280
280
 
281
+ SIGNUP_DISABLED: {
282
+ code: 'LTNS_0026',
283
+ message: 'Sign-up is currently disabled',
284
+ translations: {
285
+ de: 'Die Registrierung ist derzeit deaktiviert.',
286
+ en: 'Sign-up is currently disabled.',
287
+ },
288
+ },
289
+
281
290
  // =====================================================
282
291
  // Authorization Errors (LTNS_0100-LTNS_0199)
283
292
  // =====================================================