@lenne.tech/nest-server 11.13.3 → 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 (31) hide show
  1. package/dist/core/common/interfaces/server-options.interface.d.ts +1 -0
  2. package/dist/core/modules/better-auth/better-auth.config.js +6 -1
  3. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  4. package/dist/core/modules/better-auth/core-better-auth-models.d.ts +1 -0
  5. package/dist/core/modules/better-auth/core-better-auth-models.js +4 -0
  6. package/dist/core/modules/better-auth/core-better-auth-models.js.map +1 -1
  7. package/dist/core/modules/better-auth/core-better-auth.controller.js +14 -6
  8. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  9. package/dist/core/modules/better-auth/core-better-auth.module.js +44 -19
  10. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  11. package/dist/core/modules/better-auth/core-better-auth.resolver.js +3 -2
  12. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
  13. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +2 -0
  14. package/dist/core/modules/better-auth/core-better-auth.service.js +12 -4
  15. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  16. package/dist/core/modules/error-code/error-codes.d.ts +9 -0
  17. package/dist/core/modules/error-code/error-codes.js +8 -0
  18. package/dist/core/modules/error-code/error-codes.js.map +1 -1
  19. package/dist/server/modules/error-code/error-codes.d.ts +1 -0
  20. package/dist/tsconfig.build.tsbuildinfo +1 -1
  21. package/package.json +1 -1
  22. package/src/core/common/interfaces/server-options.interface.ts +10 -7
  23. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +6 -0
  24. package/src/core/modules/better-auth/README.md +32 -0
  25. package/src/core/modules/better-auth/better-auth.config.ts +12 -2
  26. package/src/core/modules/better-auth/core-better-auth-models.ts +3 -0
  27. package/src/core/modules/better-auth/core-better-auth.controller.ts +44 -16
  28. package/src/core/modules/better-auth/core-better-auth.module.ts +72 -37
  29. package/src/core/modules/better-auth/core-better-auth.resolver.ts +16 -9
  30. package/src/core/modules/better-auth/core-better-auth.service.ts +30 -7
  31. package/src/core/modules/error-code/error-codes.ts +9 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.13.3",
3
+ "version": "11.13.4",
4
4
  "description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
5
5
  "keywords": [
6
6
  "node",
@@ -1693,6 +1693,14 @@ interface IBetterAuthBase {
1693
1693
  * Set `enabled: false` to explicitly disable email/password auth.
1694
1694
  */
1695
1695
  emailAndPassword?: {
1696
+ /**
1697
+ * Disable user registration (sign-up) via BetterAuth.
1698
+ * Passed through to better-auth's native emailAndPassword.disableSignUp.
1699
+ * Custom endpoints (GraphQL + REST) also check this flag early.
1700
+ * @default false
1701
+ */
1702
+ disableSignUp?: boolean;
1703
+
1696
1704
  /**
1697
1705
  * Whether email/password authentication is enabled.
1698
1706
  * @default true
@@ -1993,10 +2001,7 @@ interface IBetterAuthBase {
1993
2001
  * };
1994
2002
  * ```
1995
2003
  */
1996
- type IBetterAuthPasskeyDisabled =
1997
- | false
1998
- | (Omit<IBetterAuthPasskeyConfig, 'enabled'> & { enabled: false })
1999
- | undefined;
2004
+ type IBetterAuthPasskeyDisabled = false | (Omit<IBetterAuthPasskeyConfig, 'enabled'> & { enabled: false }) | undefined;
2000
2005
 
2001
2006
  /**
2002
2007
  * Passkey configuration that is considered "enabled".
@@ -2006,9 +2011,7 @@ type IBetterAuthPasskeyDisabled =
2006
2011
  * - `{ enabled: true, ... }` (explicit enabled)
2007
2012
  * - `{ rpName: 'My App', ... }` (config without explicit enabled = defaults to true)
2008
2013
  */
2009
- type IBetterAuthPasskeyEnabled =
2010
- | (Omit<IBetterAuthPasskeyConfig, 'enabled'> & { enabled?: true })
2011
- | true;
2014
+ type IBetterAuthPasskeyEnabled = (Omit<IBetterAuthPasskeyConfig, 'enabled'> & { enabled?: true }) | true;
2012
2015
 
2013
2016
  /**
2014
2017
  * BetterAuth configuration WITHOUT Passkey (or Passkey disabled).
@@ -313,6 +313,12 @@ After integration, verify:
313
313
  - [ ] Passkey login redirects to dashboard after successful authentication
314
314
  - [ ] Passkey can be registered, listed, and deleted from security settings
315
315
 
316
+ ### Optional: Disable Sign-Up (`emailAndPassword.disableSignUp: true`)
317
+ - [ ] REST `POST /iam/sign-up/email` returns `400` with error `LTNS_0026`
318
+ - [ ] GraphQL `betterAuthSignUp` returns error `LTNS_0026`
319
+ - [ ] `GET /iam/features` reports `signUpEnabled: false`
320
+ - [ ] Sign-in still works for existing users
321
+
316
322
  ### Additional checks for Migration scenario:
317
323
  - [ ] Sign-in via Legacy Auth works for BetterAuth-created users
318
324
  - [ ] Sign-in via BetterAuth works for Legacy-created users
@@ -574,6 +574,32 @@ const config = {
574
574
  };
575
575
  ```
576
576
 
577
+ ### Disable Sign-Up
578
+
579
+ Disable user registration while keeping sign-in active (e.g., invite-only apps, admin-created accounts):
580
+
581
+ ```typescript
582
+ const config = {
583
+ betterAuth: {
584
+ emailAndPassword: {
585
+ disableSignUp: true, // Block new registrations (REST + GraphQL)
586
+ },
587
+ },
588
+ };
589
+ ```
590
+
591
+ When disabled:
592
+ - REST `POST /iam/sign-up/email` returns `400 Bad Request` with error `LTNS_0026`
593
+ - GraphQL `betterAuthSignUp` mutation returns error `LTNS_0026`
594
+ - `betterAuthFeatures` reports `signUpEnabled: false`
595
+ - Sign-in continues to work for existing users
596
+
597
+ **Defense in Depth:** The flag is enforced at two layers:
598
+ 1. **Custom check** (`CoreBetterAuthService.ensureSignUpEnabled()`) runs in Controller/Resolver *before* any BetterAuth API call and returns a structured `LTNS_0026` error.
599
+ 2. **Native BetterAuth** `emailAndPassword.disableSignUp` acts as a safety net for any direct API access that bypasses the custom check.
600
+
601
+ **Default:** `false` (sign-up enabled) - fully backward compatible.
602
+
577
603
  ### Additional User Fields
578
604
 
579
605
  Add custom fields to the Better-Auth user schema:
@@ -1049,6 +1075,7 @@ type CoreBetterAuthFeaturesModel {
1049
1075
  jwt: Boolean!
1050
1076
  twoFactor: Boolean!
1051
1077
  passkey: Boolean!
1078
+ signUpEnabled: Boolean!
1052
1079
  socialProviders: [String!]!
1053
1080
  }
1054
1081
  ```
@@ -1100,6 +1127,7 @@ query {
1100
1127
  jwt
1101
1128
  twoFactor
1102
1129
  passkey
1130
+ signUpEnabled
1103
1131
  socialProviders
1104
1132
  }
1105
1133
  }
@@ -1247,6 +1275,7 @@ export class MyService {
1247
1275
  | `isJwtEnabled()` | Check if JWT plugin is enabled |
1248
1276
  | `isTwoFactorEnabled()` | Check if 2FA is enabled |
1249
1277
  | `isPasskeyEnabled()` | Check if Passkey is enabled |
1278
+ | `isSignUpEnabled()` | Check if sign-up is enabled |
1250
1279
  | `getEnabledSocialProviders()` | Get list of enabled social providers |
1251
1280
  | `getBasePath()` | Get the base path for endpoints |
1252
1281
  | `getBaseUrl()` | Get the base URL |
@@ -1970,6 +1999,9 @@ These protected methods are available for use in your custom resolver:
1970
1999
  // Check if Better-Auth is enabled (throws if not)
1971
2000
  this.ensureEnabled();
1972
2001
 
2002
+ // Check if sign-up is enabled (throws if not) - delegated to service
2003
+ this.betterAuthService.ensureSignUpEnabled();
2004
+
1973
2005
  // Convert Express headers to Web API Headers
1974
2006
  const headers = this.convertHeaders(ctx.req.headers);
1975
2007
 
@@ -314,6 +314,12 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
314
314
  // Enable email/password authentication by default (required by Better-Auth 1.x)
315
315
  // Can be disabled by setting config.emailAndPassword.enabled = false
316
316
  emailAndPassword: {
317
+ // Defense in Depth: This native Better-Auth flag is the second layer.
318
+ // The first layer is CoreBetterAuthService.ensureSignUpEnabled() which
319
+ // runs in Controller/Resolver BEFORE the BetterAuth API is called and
320
+ // returns a structured LTNS_0026 error. The native flag acts as a safety
321
+ // net in case the custom check is bypassed (e.g., direct API calls).
322
+ disableSignUp: config.emailAndPassword?.disableSignUp === true,
317
323
  enabled: config.emailAndPassword?.enabled !== false,
318
324
  password: {
319
325
  hash: nativeScryptHash,
@@ -426,7 +432,7 @@ function buildEmailVerificationConfig(
426
432
  _request?: Request,
427
433
  ) => {
428
434
  // Don't await to prevent timing attacks (as recommended by Better-Auth docs)
429
-
435
+
430
436
  sendVerificationEmail(data);
431
437
  };
432
438
  }
@@ -918,7 +924,11 @@ function normalizePasskeyConfig(
918
924
  // Resolve values: explicit config > resolved URLs
919
925
  const finalRpId = rawConfig.rpId || resolvedUrls.rpId;
920
926
  const finalOrigin = rawConfig.origin || resolvedUrls.appUrl;
921
- const finalTrustedOrigins = config.trustedOrigins?.length ? config.trustedOrigins : resolvedUrls.appUrl ? [resolvedUrls.appUrl] : undefined;
927
+ const finalTrustedOrigins = config.trustedOrigins?.length
928
+ ? config.trustedOrigins
929
+ : resolvedUrls.appUrl
930
+ ? [resolvedUrls.appUrl]
931
+ : undefined;
922
932
 
923
933
  // Check if we have all required values for Passkey
924
934
  const hasRequiredConfig = finalRpId && finalOrigin && finalTrustedOrigins?.length;
@@ -135,6 +135,9 @@ export class CoreBetterAuthFeaturesModel {
135
135
  @Field(() => Boolean, { description: 'Whether Passkey is enabled' })
136
136
  passkey: boolean;
137
137
 
138
+ @Field(() => Boolean, { description: 'Whether sign-up is enabled' })
139
+ signUpEnabled: boolean;
140
+
138
141
  @Field(() => [String], { description: 'List of enabled social providers' })
139
142
  socialProviders: string[];
140
143
 
@@ -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,