@lenne.tech/nest-server 11.7.1 → 11.7.2

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 (60) hide show
  1. package/dist/core/common/interfaces/server-options.interface.d.ts +18 -15
  2. package/dist/core/modules/auth/core-auth.controller.js +2 -2
  3. package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
  4. package/dist/core/modules/auth/core-auth.resolver.js +2 -2
  5. package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
  6. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +1 -1
  7. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -1
  8. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js +1 -1
  9. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js.map +1 -1
  10. package/dist/core/modules/better-auth/better-auth-user.mapper.js +7 -55
  11. package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +1 -1
  12. package/dist/core/modules/better-auth/better-auth.config.js +29 -10
  13. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  14. package/dist/core/modules/better-auth/better-auth.middleware.d.ts +1 -0
  15. package/dist/core/modules/better-auth/better-auth.middleware.js +55 -1
  16. package/dist/core/modules/better-auth/better-auth.middleware.js.map +1 -1
  17. package/dist/core/modules/better-auth/better-auth.module.d.ts +1 -1
  18. package/dist/core/modules/better-auth/better-auth.module.js +46 -18
  19. package/dist/core/modules/better-auth/better-auth.module.js.map +1 -1
  20. package/dist/core/modules/better-auth/better-auth.resolver.js +0 -11
  21. package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
  22. package/dist/core/modules/better-auth/better-auth.service.d.ts +22 -1
  23. package/dist/core/modules/better-auth/better-auth.service.js +209 -8
  24. package/dist/core/modules/better-auth/better-auth.service.js.map +1 -1
  25. package/dist/core/modules/better-auth/better-auth.types.d.ts +2 -0
  26. package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
  27. package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +5 -0
  28. package/dist/core/modules/better-auth/core-better-auth.resolver.js +58 -12
  29. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
  30. package/dist/core.module.js +6 -3
  31. package/dist/core.module.js.map +1 -1
  32. package/dist/server/modules/better-auth/better-auth.module.d.ts +1 -1
  33. package/dist/server/modules/better-auth/better-auth.module.js +2 -1
  34. package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
  35. package/dist/server/modules/better-auth/better-auth.resolver.d.ts +3 -0
  36. package/dist/server/modules/better-auth/better-auth.resolver.js +14 -11
  37. package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -1
  38. package/dist/server/modules/user/user.controller.js +0 -8
  39. package/dist/server/modules/user/user.controller.js.map +1 -1
  40. package/dist/tsconfig.build.tsbuildinfo +1 -1
  41. package/package.json +1 -1
  42. package/src/core/common/interfaces/server-options.interface.ts +129 -58
  43. package/src/core/modules/auth/core-auth.controller.ts +2 -2
  44. package/src/core/modules/auth/core-auth.resolver.ts +2 -2
  45. package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +1 -1
  46. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +12 -11
  47. package/src/core/modules/better-auth/README.md +82 -43
  48. package/src/core/modules/better-auth/better-auth-rate-limiter.service.ts +1 -1
  49. package/src/core/modules/better-auth/better-auth-user.mapper.ts +9 -77
  50. package/src/core/modules/better-auth/better-auth.config.ts +45 -15
  51. package/src/core/modules/better-auth/better-auth.middleware.ts +85 -2
  52. package/src/core/modules/better-auth/better-auth.module.ts +83 -27
  53. package/src/core/modules/better-auth/better-auth.resolver.ts +0 -11
  54. package/src/core/modules/better-auth/better-auth.service.ts +367 -12
  55. package/src/core/modules/better-auth/better-auth.types.ts +16 -0
  56. package/src/core/modules/better-auth/core-better-auth.resolver.ts +111 -16
  57. package/src/core.module.ts +9 -3
  58. package/src/server/modules/better-auth/better-auth.module.ts +9 -3
  59. package/src/server/modules/better-auth/better-auth.resolver.ts +9 -11
  60. package/src/server/modules/user/user.controller.ts +1 -9
@@ -243,7 +243,6 @@ export class BetterAuthUserMapper {
243
243
  }
244
244
 
245
245
  if (!plainPassword) {
246
- this.logger.debug('No plain password provided - cannot create bcrypt hash for legacy');
247
246
  return false;
248
247
  }
249
248
 
@@ -266,13 +265,7 @@ export class BetterAuthUserMapper {
266
265
  { $set: { password: bcryptHash, updatedAt: new Date() } },
267
266
  );
268
267
 
269
- if (result.modifiedCount > 0) {
270
- this.logger.debug(`Created bcrypt hash for legacy auth for user ${userEmail}`);
271
- return true;
272
- }
273
-
274
- this.logger.debug(`No user found to sync password for ${userEmail}`);
275
- return false;
268
+ return result.modifiedCount > 0;
276
269
  } catch (error) {
277
270
  this.logger.error(
278
271
  `Error syncing password to legacy: ${error instanceof Error ? error.message : 'Unknown error'}`,
@@ -304,7 +297,6 @@ export class BetterAuthUserMapper {
304
297
  }
305
298
 
306
299
  if (!plainPassword) {
307
- this.logger.debug('No plain password provided - cannot sync to IAM');
308
300
  return false;
309
301
  }
310
302
 
@@ -315,7 +307,6 @@ export class BetterAuthUserMapper {
315
307
  // Find the user
316
308
  const user = await usersCollection.findOne({ email: userEmail });
317
309
  if (!user) {
318
- this.logger.debug(`No user found with email ${userEmail} - cannot sync password to IAM`);
319
310
  return false;
320
311
  }
321
312
 
@@ -326,7 +317,6 @@ export class BetterAuthUserMapper {
326
317
  });
327
318
 
328
319
  if (!existingAccount) {
329
- this.logger.debug(`No IAM credential account found for ${userEmail} - sync not needed`);
330
320
  return false;
331
321
  }
332
322
 
@@ -334,7 +324,7 @@ export class BetterAuthUserMapper {
334
324
  const scryptHash = await this.hashPasswordForBetterAuth(plainPassword);
335
325
 
336
326
  // Update the account password
337
- const result = await accountCollection.updateOne(
327
+ await accountCollection.updateOne(
338
328
  { _id: existingAccount._id },
339
329
  {
340
330
  $set: {
@@ -344,12 +334,6 @@ export class BetterAuthUserMapper {
344
334
  },
345
335
  );
346
336
 
347
- if (result.modifiedCount > 0) {
348
- this.logger.debug(`Synced password change to IAM for user ${userEmail}`);
349
- return true;
350
- }
351
-
352
- this.logger.debug(`Password already up to date in IAM for ${userEmail}`);
353
337
  return true;
354
338
  } catch (error) {
355
339
  this.logger.error(`Error syncing password to IAM: ${error instanceof Error ? error.message : 'Unknown error'}`);
@@ -371,7 +355,6 @@ export class BetterAuthUserMapper {
371
355
  */
372
356
  async migrateAccountToIam(userEmail: string, plainPassword?: string): Promise<boolean> {
373
357
  if (!this.connection) {
374
- this.logger.warn('No database connection available - cannot migrate account to IAM');
375
358
  return false;
376
359
  }
377
360
 
@@ -383,7 +366,6 @@ export class BetterAuthUserMapper {
383
366
  const legacyUser = await usersCollection.findOne({ email: userEmail });
384
367
 
385
368
  if (!legacyUser?.password) {
386
- this.logger.debug(`No legacy user with password found for ${userEmail}`);
387
369
  return false;
388
370
  }
389
371
 
@@ -396,17 +378,12 @@ export class BetterAuthUserMapper {
396
378
  const directMatch = await bcrypt.compare(plainPassword, legacyUser.password);
397
379
  const sha256Match = await bcrypt.compare(sha256(plainPassword), legacyUser.password);
398
380
  if (!directMatch && !sha256Match) {
399
- // Security: Wrong password provided for migration - reject strongly
400
- this.logger.warn(
401
- `SECURITY: Password verification failed for ${userEmail} - migration rejected. This may indicate an attempted unauthorized migration.`,
402
- );
403
- // Return false instead of throwing to avoid exposing user existence
404
- // The calling code (CoreAuthService) will handle this as authentication failure
381
+ // Security: Wrong password provided for migration - reject
382
+ this.logger.warn(`Migration password verification failed for ${userEmail}`);
405
383
  return false;
406
384
  }
407
385
  } else {
408
386
  // No password provided - cannot verify, cannot migrate
409
- this.logger.debug(`No password provided for ${userEmail} - migration skipped`);
410
387
  return false;
411
388
  }
412
389
 
@@ -433,8 +410,6 @@ export class BetterAuthUserMapper {
433
410
  },
434
411
  },
435
412
  );
436
-
437
- this.logger.debug(`Added Better-Auth id ${stringId} to legacy user ${userEmail}`);
438
413
  }
439
414
 
440
415
  // Check if credential account already exists
@@ -445,23 +420,11 @@ export class BetterAuthUserMapper {
445
420
  });
446
421
 
447
422
  if (existingAccount) {
448
- this.logger.debug(`Credential account already exists for ${userEmail}`);
449
423
  return true;
450
424
  }
451
425
 
452
- // Create the credential account
453
- // If we have the plain password, create a Better-Auth compatible scrypt hash
454
- // Otherwise, migration is not possible (legacy bcrypt hash is not compatible)
455
- let passwordHash: string;
456
- if (plainPassword) {
457
- // Better-Auth uses scrypt with format: salt:hash (both hex encoded)
458
- passwordHash = await this.hashPasswordForBetterAuth(plainPassword);
459
- this.logger.debug(`Created Better-Auth compatible scrypt hash for ${userEmail}`);
460
- } else {
461
- // Without plain password, we cannot migrate - Better-Auth uses scrypt, Legacy uses bcrypt
462
- this.logger.warn(`Cannot migrate ${userEmail} without plain password - hash formats are incompatible`);
463
- return false;
464
- }
426
+ // Create the credential account with Better-Auth compatible scrypt hash
427
+ const passwordHash = await this.hashPasswordForBetterAuth(plainPassword);
465
428
 
466
429
  const now = new Date();
467
430
  // Store account matching Better-Auth's format:
@@ -477,7 +440,6 @@ export class BetterAuthUserMapper {
477
440
  userId: userMongoId,
478
441
  });
479
442
 
480
- this.logger.debug(`Created IAM credential account for legacy user ${userEmail}`);
481
443
  return true;
482
444
  } catch (error) {
483
445
  this.logger.error(`Error migrating account to IAM: ${error instanceof Error ? error.message : 'Unknown error'}`);
@@ -685,22 +647,15 @@ export class BetterAuthUserMapper {
685
647
  const user = await usersCollection.findOne({ email: newEmail });
686
648
 
687
649
  if (!user) {
688
- this.logger.debug(`No user found with new email ${newEmail}`);
689
650
  return false;
690
651
  }
691
652
 
692
653
  // Invalidate all existing sessions for this user
693
654
  // This forces re-authentication with the new email
694
655
  if (user._id) {
695
- const deleteResult = await sessionCollection.deleteMany({ userId: user._id });
696
- if (deleteResult.deletedCount > 0) {
697
- this.logger.debug(
698
- `Invalidated ${deleteResult.deletedCount} sessions after email change for user ${newEmail}`,
699
- );
700
- }
656
+ await sessionCollection.deleteMany({ userId: user._id });
701
657
  }
702
658
 
703
- this.logger.debug(`Email change synced from Legacy to IAM: ${oldEmail} → ${newEmail}`);
704
659
  return true;
705
660
  } catch (error) {
706
661
  this.logger.error(
@@ -744,13 +699,7 @@ export class BetterAuthUserMapper {
744
699
  { returnDocument: 'after' },
745
700
  );
746
701
 
747
- if (!result) {
748
- this.logger.debug(`No user found with iamId ${userId}`);
749
- return false;
750
- }
751
-
752
- this.logger.debug(`Email change synced from IAM to Legacy for user ${userId}: → ${newEmail}`);
753
- return true;
702
+ return !!result;
754
703
  } catch (error) {
755
704
  this.logger.error(
756
705
  `Error syncing email change from IAM: ${error instanceof Error ? error.message : 'Unknown error'}`,
@@ -807,7 +756,6 @@ export class BetterAuthUserMapper {
807
756
  }
808
757
 
809
758
  if (!user) {
810
- this.logger.debug(`No user found with identifier ${userIdentifier}`);
811
759
  return { accountsDeleted: 0, sessionsDeleted: 0, success: false, userDeleted: false };
812
760
  }
813
761
 
@@ -874,12 +822,6 @@ export class BetterAuthUserMapper {
874
822
  const accountsResult = await accountCollection.deleteMany({ userId: userObjectId });
875
823
  const accountsDeleted = accountsResult.deletedCount;
876
824
 
877
- if (sessionsDeleted > 0 || accountsDeleted > 0) {
878
- this.logger.debug(
879
- `Cleaned up IAM data for user ${userId}: accounts=${accountsDeleted}, sessions=${sessionsDeleted}`,
880
- );
881
- }
882
-
883
825
  return {
884
826
  accountsDeleted,
885
827
  sessionsDeleted,
@@ -914,13 +856,7 @@ export class BetterAuthUserMapper {
914
856
  $or: [{ iamId: iamUserId }, { id: iamUserId }],
915
857
  });
916
858
 
917
- if (result.deletedCount > 0) {
918
- this.logger.debug(`Cleaned up Legacy user for IAM user ${iamUserId}`);
919
- return true;
920
- }
921
-
922
- this.logger.debug(`No Legacy user found for IAM user ${iamUserId}`);
923
- return false;
859
+ return result.deletedCount > 0;
924
860
  } catch (error) {
925
861
  this.logger.error(`Error cleaning up Legacy data: ${error instanceof Error ? error.message : 'Unknown error'}`);
926
862
  return false;
@@ -1043,10 +979,6 @@ export class BetterAuthUserMapper {
1043
979
  // Can disable legacy auth only if ALL users are fully migrated
1044
980
  const canDisableLegacyAuth = totalUsers > 0 && fullyMigratedUsers === totalUsers;
1045
981
 
1046
- this.logger.debug(
1047
- `Migration status: ${fullyMigratedUsers}/${totalUsers} users migrated (${migrationPercentage}%)`,
1048
- );
1049
-
1050
982
  return {
1051
983
  canDisableLegacyAuth,
1052
984
  fullyMigratedUsers,
@@ -168,42 +168,48 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
168
168
  * Builds the plugins array based on configuration.
169
169
  * Merges built-in plugins (jwt, twoFactor, passkey) with custom plugins from config.
170
170
  *
171
- * Plugins are enabled by default when their configuration block is present.
172
- * Set `enabled: false` to explicitly disable a configured plugin.
171
+ * Plugins accept both boolean and object configuration:
172
+ * - `true` or `{}`: Enable with defaults
173
+ * - `{ option: value }`: Enable with custom settings
174
+ * - `false` or `{ enabled: false }`: Disable
175
+ * - `undefined`: Disabled (default)
173
176
  */
174
177
  function buildPlugins(config: IBetterAuth): BetterAuthPlugin[] {
175
178
  const plugins: BetterAuthPlugin[] = [];
176
179
 
177
180
  // JWT Plugin for API client compatibility
178
- // Enabled by default when jwt config is present, unless explicitly disabled
179
- if (config.jwt && config.jwt.enabled !== false) {
181
+ // JWT is enabled by default unless explicitly disabled (jwt: false or jwt: { enabled: false })
182
+ const jwtExplicitlyDisabled =
183
+ config.jwt === false || (typeof config.jwt === 'object' && config.jwt?.enabled === false);
184
+ if (!jwtExplicitlyDisabled) {
185
+ const jwtConfig = typeof config.jwt === 'object' ? config.jwt : {};
180
186
  plugins.push(
181
187
  jwt({
182
188
  jwt: {
183
- expirationTime: config.jwt.expiresIn || '15m',
189
+ expirationTime: jwtConfig.expiresIn || '15m',
184
190
  },
185
191
  }),
186
192
  );
187
193
  }
188
194
 
189
195
  // Two-Factor Authentication Plugin
190
- // Enabled by default when twoFactor config is present, unless explicitly disabled
191
- if (config.twoFactor && config.twoFactor.enabled !== false) {
196
+ const twoFactorConfig = getPluginConfig(config.twoFactor);
197
+ if (twoFactorConfig) {
192
198
  plugins.push(
193
199
  twoFactor({
194
- issuer: config.twoFactor.appName || 'Nest Server',
200
+ issuer: twoFactorConfig.appName || 'Nest Server',
195
201
  }),
196
202
  );
197
203
  }
198
204
 
199
205
  // Passkey/WebAuthn Plugin
200
- // Enabled by default when passkey config is present, unless explicitly disabled
201
- if (config.passkey && config.passkey.enabled !== false) {
206
+ const passkeyConfig = getPluginConfig(config.passkey);
207
+ if (passkeyConfig) {
202
208
  plugins.push(
203
209
  passkey({
204
- origin: config.passkey.origin || 'http://localhost:3000',
205
- rpID: config.passkey.rpId || 'localhost',
206
- rpName: config.passkey.rpName || 'Nest Server',
210
+ origin: passkeyConfig.origin || 'http://localhost:3000',
211
+ rpID: passkeyConfig.rpId || 'localhost',
212
+ rpName: passkeyConfig.rpName || 'Nest Server',
207
213
  }),
208
214
  );
209
215
  }
@@ -322,6 +328,29 @@ function getAutoGeneratedSecret(): string {
322
328
  return cachedAutoGeneratedSecret;
323
329
  }
324
330
 
331
+ /**
332
+ * Gets plugin configuration as object, handling boolean shorthand.
333
+ * Returns undefined if disabled, or the config object if enabled.
334
+ */
335
+ function getPluginConfig<T extends { enabled?: boolean }>(config: boolean | T | undefined): T | undefined {
336
+ if (!isPluginEnabled(config)) return undefined;
337
+ if (typeof config === 'boolean') return {} as T;
338
+ return config;
339
+ }
340
+
341
+ /**
342
+ * Checks if a plugin configuration is enabled.
343
+ * Supports both boolean and object configuration:
344
+ * - `true` or `{}` or `{ enabled: true }` → enabled
345
+ * - `false` or `{ enabled: false }` → disabled
346
+ * - `undefined` → disabled
347
+ */
348
+ function isPluginEnabled<T extends { enabled?: boolean }>(config: boolean | T | undefined): boolean {
349
+ if (config === undefined) return false;
350
+ if (typeof config === 'boolean') return config;
351
+ return config.enabled !== false;
352
+ }
353
+
325
354
  /**
326
355
  * Checks if a secret has valid minimum length (32 characters)
327
356
  */
@@ -416,8 +445,9 @@ function validateConfig(config: IBetterAuth, fallbackSecrets?: (string | undefin
416
445
  }
417
446
 
418
447
  // Validate passkey origin
419
- if (config.passkey?.enabled && config.passkey.origin && !isValidUrl(config.passkey.origin)) {
420
- errors.push(`Invalid passkey origin format: ${config.passkey.origin}`);
448
+ const passkeyConfig = typeof config.passkey === 'object' ? config.passkey : null;
449
+ if (passkeyConfig?.enabled && passkeyConfig.origin && !isValidUrl(passkeyConfig.origin)) {
450
+ errors.push(`Invalid passkey origin format: ${passkeyConfig.origin}`);
421
451
  }
422
452
 
423
453
  // Validate social providers dynamically
@@ -49,7 +49,7 @@ export class BetterAuthMiddleware implements NestMiddleware {
49
49
  }
50
50
 
51
51
  try {
52
- // Get session from Better-Auth
52
+ // Strategy 1: Try session-based authentication (cookies)
53
53
  const session = await this.getSession(req);
54
54
 
55
55
  if (session?.user) {
@@ -63,7 +63,69 @@ export class BetterAuthMiddleware implements NestMiddleware {
63
63
  if (mappedUser) {
64
64
  // Attach the mapped user to the request
65
65
  // This makes it compatible with @CurrentUser() and RolesGuard
66
- req.user = mappedUser;
66
+ // Set _authenticatedViaBetterAuth flag so AuthGuard skips Passport JWT verification
67
+ req.user = { ...mappedUser, _authenticatedViaBetterAuth: true };
68
+ return next();
69
+ }
70
+ }
71
+
72
+ // Strategy 2: Try Authorization header (Bearer token)
73
+ // The token could be a BetterAuth JWT, a Legacy JWT, or a session token
74
+ if (req.headers.authorization) {
75
+ const authHeader = req.headers.authorization;
76
+ const token = authHeader.startsWith('Bearer ') ? authHeader.substring(7) : authHeader;
77
+ const tokenParts = token.split('.').length;
78
+
79
+ // Check if token looks like a JWT (has 3 parts)
80
+ if (tokenParts === 3) {
81
+ // Decode JWT payload to check if it's a Legacy JWT or BetterAuth JWT
82
+ // Legacy JWTs have 'id' claim, BetterAuth JWTs have 'sub' claim
83
+ const isLegacyJwt = this.isLegacyJwt(token);
84
+ if (isLegacyJwt) {
85
+ // Legacy JWT - skip BetterAuth processing, let Passport handle it
86
+ return next();
87
+ }
88
+
89
+ // Try BetterAuth JWT verification
90
+ if (this.betterAuthService.isJwtEnabled()) {
91
+ const jwtPayload = await this.betterAuthService.verifyJwtFromRequest(req);
92
+
93
+ if (jwtPayload?.sub) {
94
+ // JWT payload contains user info - create a session-like user object
95
+ const sessionUser: BetterAuthSessionUser = {
96
+ email: jwtPayload.email || '',
97
+ emailVerified: jwtPayload.emailVerified,
98
+ id: jwtPayload.sub,
99
+ name: jwtPayload.name,
100
+ };
101
+
102
+ req.betterAuthUser = sessionUser;
103
+
104
+ // Map the JWT user to our User model with hasRole()
105
+ const mappedUser = await this.userMapper.mapSessionUser(sessionUser);
106
+
107
+ if (mappedUser) {
108
+ req.user = { ...mappedUser, _authenticatedViaBetterAuth: true };
109
+ return next();
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ // If user is still not set, try session token verification as fallback
116
+ // This handles both non-JWT tokens and JWTs that couldn't be verified
117
+ if (!req.user) {
118
+ const sessionResult = await this.betterAuthService.getSessionByToken(token);
119
+ if (sessionResult?.user) {
120
+ req.betterAuthSession = { session: sessionResult.session, user: sessionResult.user };
121
+ req.betterAuthUser = sessionResult.user;
122
+
123
+ const mappedUser = await this.userMapper.mapSessionUser(sessionResult.user);
124
+ if (mappedUser) {
125
+ req.user = { ...mappedUser, _authenticatedViaBetterAuth: true };
126
+ return next();
127
+ }
128
+ }
67
129
  }
68
130
  }
69
131
  } catch (error) {
@@ -75,6 +137,27 @@ export class BetterAuthMiddleware implements NestMiddleware {
75
137
  next();
76
138
  }
77
139
 
140
+ /**
141
+ * Checks if a JWT token is a Legacy Auth JWT (has 'id' claim but no 'sub' claim)
142
+ * Legacy JWTs use 'id' for user ID, BetterAuth JWTs use 'sub'
143
+ */
144
+ private isLegacyJwt(token: string): boolean {
145
+ try {
146
+ const parts = token.split('.');
147
+ if (parts.length !== 3) return false;
148
+
149
+ // Decode the payload (second part)
150
+ const payloadStr = Buffer.from(parts[1], 'base64url').toString('utf-8');
151
+ const payload = JSON.parse(payloadStr);
152
+
153
+ // Legacy JWT has 'id' claim (and typically 'deviceId', 'tokenId')
154
+ // BetterAuth JWT has 'sub' claim
155
+ return payload.id !== undefined && payload.sub === undefined;
156
+ } catch {
157
+ return false;
158
+ }
159
+ }
160
+
78
161
  /**
79
162
  * Gets the session from Better-Auth
80
163
  */
@@ -20,7 +20,7 @@ import { BetterAuthUserMapper } from './better-auth-user.mapper';
20
20
  import { BetterAuthInstance, createBetterAuthInstance } from './better-auth.config';
21
21
  import { BetterAuthMiddleware } from './better-auth.middleware';
22
22
  import { BetterAuthResolver } from './better-auth.resolver';
23
- import { BetterAuthService } from './better-auth.service';
23
+ import { BETTER_AUTH_CONFIG, BetterAuthService } from './better-auth.service';
24
24
  import { CoreBetterAuthController } from './core-better-auth.controller';
25
25
  import { CoreBetterAuthResolver } from './core-better-auth.resolver';
26
26
 
@@ -34,9 +34,13 @@ export const BETTER_AUTH_INSTANCE = 'BETTER_AUTH_INSTANCE';
34
34
  */
35
35
  export interface BetterAuthModuleOptions {
36
36
  /**
37
- * Better-auth configuration
37
+ * Better-auth configuration.
38
+ * Accepts:
39
+ * - `true`: Enable with all defaults (including JWT)
40
+ * - `false`: Disable BetterAuth
41
+ * - `{ ... }`: Enable with custom configuration
38
42
  */
39
- config: IBetterAuth;
43
+ config: boolean | IBetterAuth;
40
44
 
41
45
  /**
42
46
  * Custom controller class to use instead of the default CoreBetterAuthController.
@@ -100,6 +104,20 @@ export interface BetterAuthModuleOptions {
100
104
  resolver?: Type<CoreBetterAuthResolver>;
101
105
  }
102
106
 
107
+ /**
108
+ * Normalizes betterAuth config from boolean | IBetterAuth to IBetterAuth | null
109
+ * - `true` → `{}` (enabled with defaults)
110
+ * - `false` → `null` (disabled)
111
+ * - `undefined` → `null` (disabled for backward compatibility)
112
+ * - `{ ... }` → `{ ... }` (pass through)
113
+ */
114
+ function normalizeBetterAuthConfig(config: boolean | IBetterAuth | undefined): IBetterAuth | null {
115
+ if (config === undefined || config === null) return null;
116
+ if (config === true) return {};
117
+ if (config === false) return null;
118
+ return config;
119
+ }
120
+
103
121
  /**
104
122
  * BetterAuthModule provides integration with the better-auth authentication framework.
105
123
  *
@@ -178,12 +196,12 @@ export class BetterAuthModule implements NestModule, OnModuleInit {
178
196
  // Apply rate limiting to Better-Auth endpoints only
179
197
  if (BetterAuthModule.currentConfig?.rateLimit?.enabled) {
180
198
  consumer.apply(BetterAuthRateLimitMiddleware).forRoutes(`${basePath}/*path`);
181
- BetterAuthModule.logger.log(`Rate limiting enabled for ${basePath}/*path endpoints`);
199
+ BetterAuthModule.logger.debug(`Rate limiting middleware registered for ${basePath}/*path endpoints`);
182
200
  }
183
201
 
184
202
  // Apply session middleware to all routes
185
203
  consumer.apply(BetterAuthMiddleware).forRoutes('(.*)'); // New path-to-regexp syntax for wildcard
186
- BetterAuthModule.logger.log('BetterAuthMiddleware registered for all routes');
204
+ BetterAuthModule.logger.debug('BetterAuthMiddleware registered for all routes');
187
205
  }
188
206
  }
189
207
 
@@ -224,7 +242,10 @@ export class BetterAuthModule implements NestModule, OnModuleInit {
224
242
  * @returns Dynamic module configuration
225
243
  */
226
244
  static forRoot(options: BetterAuthModuleOptions): DynamicModule {
227
- const { config, controller, fallbackSecrets, resolver } = options;
245
+ const { config: rawConfig, controller, fallbackSecrets, resolver } = options;
246
+
247
+ // Normalize config: true → {}, false/undefined → null
248
+ const config = normalizeBetterAuthConfig(rawConfig);
228
249
 
229
250
  // Store config for middleware configuration
230
251
  this.currentConfig = config;
@@ -233,12 +254,11 @@ export class BetterAuthModule implements NestModule, OnModuleInit {
233
254
  // Store custom resolver if provided
234
255
  this.customResolver = resolver || null;
235
256
 
236
- // If better-auth is explicitly disabled, return minimal module
257
+ // If better-auth is disabled (config is null or enabled: false), return minimal module
237
258
  // Note: We don't provide middleware classes when disabled because they depend on BetterAuthService
238
259
  // and won't be used anyway (middleware is only applied when enabled)
239
- // BetterAuth is enabled by default unless explicitly set to false
240
- if (config?.enabled === false) {
241
- this.logger.debug('BetterAuth is explicitly disabled - skipping initialization');
260
+ if (config === null || config?.enabled === false) {
261
+ this.logger.debug('BetterAuth is disabled - skipping initialization');
242
262
  this.betterAuthEnabled = false;
243
263
  return {
244
264
  exports: [BETTER_AUTH_INSTANCE, BetterAuthService, BetterAuthUserMapper, BetterAuthRateLimiter],
@@ -285,15 +305,16 @@ export class BetterAuthModule implements NestModule, OnModuleInit {
285
305
  inject: [ConfigService],
286
306
  provide: BETTER_AUTH_INSTANCE,
287
307
  useFactory: async (configService: ConfigService) => {
288
- const config = configService.get<IBetterAuth>('betterAuth');
289
-
290
- // Store config for middleware configuration
291
- this.currentConfig = config || null;
292
-
293
- // BetterAuth is enabled by default unless explicitly set to false
294
- if (config?.enabled === false) {
295
- this.logger.debug('BetterAuth is explicitly disabled');
308
+ // Get raw config (can be boolean or object)
309
+ const rawConfig = configService.get<boolean | IBetterAuth>('betterAuth');
310
+ // Normalize: true {}, false/undefined → null
311
+ const config = normalizeBetterAuthConfig(rawConfig);
312
+
313
+ // BetterAuth is disabled if config is null or enabled: false
314
+ if (config === null || config?.enabled === false) {
315
+ this.logger.debug('BetterAuth is disabled');
296
316
  this.betterAuthEnabled = false;
317
+ this.currentConfig = config;
297
318
  return null;
298
319
  }
299
320
 
@@ -315,6 +336,10 @@ export class BetterAuthModule implements NestModule, OnModuleInit {
315
336
  // with fallback to jwt.secret, jwt.refresh.secret, or auto-generation
316
337
  this.authInstance = createBetterAuthInstance({ config, db, fallbackSecrets });
317
338
 
339
+ // IMPORTANT: Store the config AFTER createBetterAuthInstance mutates it
340
+ // This ensures BetterAuthService has access to the resolved secret (with fallback applied)
341
+ this.currentConfig = config;
342
+
318
343
  if (this.authInstance) {
319
344
  this.logger.log('BetterAuth initialized successfully');
320
345
  this.logEnabledFeatures(config);
@@ -323,13 +348,22 @@ export class BetterAuthModule implements NestModule, OnModuleInit {
323
348
  return this.authInstance;
324
349
  },
325
350
  },
351
+ // Provide the resolved config for BetterAuthService
352
+ {
353
+ provide: BETTER_AUTH_CONFIG,
354
+ useFactory: () => this.currentConfig,
355
+ },
326
356
  // BetterAuthService needs to be a factory that explicitly depends on BETTER_AUTH_INSTANCE
327
357
  // to ensure proper initialization order
328
358
  {
329
- inject: [BETTER_AUTH_INSTANCE, ConfigService],
359
+ inject: [BETTER_AUTH_INSTANCE, BETTER_AUTH_CONFIG, getConnectionToken()],
330
360
  provide: BetterAuthService,
331
- useFactory: (authInstance: BetterAuthInstance | null, configService: ConfigService) => {
332
- return new BetterAuthService(authInstance, configService);
361
+ useFactory: (
362
+ authInstance: BetterAuthInstance | null,
363
+ resolvedConfig: IBetterAuth | null,
364
+ connection: Connection,
365
+ ) => {
366
+ return new BetterAuthService(authInstance, connection, resolvedConfig);
333
367
  },
334
368
  },
335
369
  BetterAuthUserMapper,
@@ -399,6 +433,10 @@ export class BetterAuthModule implements NestModule, OnModuleInit {
399
433
  this.authInstance = createBetterAuthInstance({ config, db, fallbackSecrets });
400
434
  }
401
435
 
436
+ // IMPORTANT: Store the config AFTER createBetterAuthInstance mutates it
437
+ // This ensures BetterAuthService has access to the resolved secret (with fallback applied)
438
+ this.currentConfig = config;
439
+
402
440
  if (this.authInstance && !this.initLogged) {
403
441
  this.initLogged = true;
404
442
  this.logger.log('BetterAuth initialized');
@@ -408,13 +446,22 @@ export class BetterAuthModule implements NestModule, OnModuleInit {
408
446
  return this.authInstance;
409
447
  },
410
448
  },
449
+ // Provide the resolved config for BetterAuthService
450
+ {
451
+ provide: BETTER_AUTH_CONFIG,
452
+ useFactory: () => this.currentConfig,
453
+ },
411
454
  // BetterAuthService needs to be a factory that explicitly depends on BETTER_AUTH_INSTANCE
412
455
  // to ensure proper initialization order
413
456
  {
414
- inject: [BETTER_AUTH_INSTANCE, ConfigService],
457
+ inject: [BETTER_AUTH_INSTANCE, BETTER_AUTH_CONFIG, getConnectionToken()],
415
458
  provide: BetterAuthService,
416
- useFactory: (authInstance: BetterAuthInstance | null, configService: ConfigService) => {
417
- return new BetterAuthService(authInstance, configService);
459
+ useFactory: (
460
+ authInstance: BetterAuthInstance | null,
461
+ resolvedConfig: IBetterAuth | null,
462
+ connection: Connection,
463
+ ) => {
464
+ return new BetterAuthService(authInstance, connection, resolvedConfig);
418
465
  },
419
466
  },
420
467
  BetterAuthUserMapper,
@@ -434,14 +481,23 @@ export class BetterAuthModule implements NestModule, OnModuleInit {
434
481
  private static logEnabledFeatures(config: IBetterAuth): void {
435
482
  const features: string[] = [];
436
483
 
484
+ // Helper to check if a plugin is enabled
485
+ // Supports: true, { enabled: true }, { ... } (enabled by default when config block present)
486
+ // Disabled only when: false or { enabled: false }
487
+ const isPluginEnabled = <T extends { enabled?: boolean }>(value: boolean | T | undefined): boolean => {
488
+ if (value === undefined || value === false) return false;
489
+ if (value === true) return true;
490
+ return value.enabled !== false;
491
+ };
492
+
437
493
  // Plugins are enabled by default when config block is present
438
- if (config.jwt && config.jwt.enabled !== false) {
494
+ if (isPluginEnabled(config.jwt)) {
439
495
  features.push('JWT');
440
496
  }
441
- if (config.twoFactor && config.twoFactor.enabled !== false) {
497
+ if (isPluginEnabled(config.twoFactor)) {
442
498
  features.push('2FA/TOTP');
443
499
  }
444
- if (config.passkey && config.passkey.enabled !== false) {
500
+ if (isPluginEnabled(config.passkey)) {
445
501
  features.push('Passkey/WebAuthn');
446
502
  }
447
503