@lenne.tech/nest-server 11.7.1 → 11.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) 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/guards/roles.guard.d.ts +12 -2
  7. package/dist/core/modules/auth/guards/roles.guard.js +192 -5
  8. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  9. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +1 -1
  10. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -1
  11. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js +1 -1
  12. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js.map +1 -1
  13. package/dist/core/modules/better-auth/better-auth-user.mapper.js +7 -55
  14. package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +1 -1
  15. package/dist/core/modules/better-auth/better-auth.config.js +29 -10
  16. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  17. package/dist/core/modules/better-auth/better-auth.middleware.d.ts +1 -0
  18. package/dist/core/modules/better-auth/better-auth.middleware.js +55 -1
  19. package/dist/core/modules/better-auth/better-auth.middleware.js.map +1 -1
  20. package/dist/core/modules/better-auth/better-auth.module.d.ts +1 -1
  21. package/dist/core/modules/better-auth/better-auth.module.js +46 -18
  22. package/dist/core/modules/better-auth/better-auth.module.js.map +1 -1
  23. package/dist/core/modules/better-auth/better-auth.resolver.js +0 -11
  24. package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
  25. package/dist/core/modules/better-auth/better-auth.service.d.ts +22 -1
  26. package/dist/core/modules/better-auth/better-auth.service.js +209 -8
  27. package/dist/core/modules/better-auth/better-auth.service.js.map +1 -1
  28. package/dist/core/modules/better-auth/better-auth.types.d.ts +2 -0
  29. package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
  30. package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +5 -0
  31. package/dist/core/modules/better-auth/core-better-auth.resolver.js +58 -12
  32. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
  33. package/dist/core/modules/user/core-user.service.d.ts +1 -0
  34. package/dist/core/modules/user/core-user.service.js +12 -0
  35. package/dist/core/modules/user/core-user.service.js.map +1 -1
  36. package/dist/core.module.js +6 -3
  37. package/dist/core.module.js.map +1 -1
  38. package/dist/server/modules/better-auth/better-auth.module.d.ts +1 -1
  39. package/dist/server/modules/better-auth/better-auth.module.js +2 -1
  40. package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
  41. package/dist/server/modules/better-auth/better-auth.resolver.d.ts +3 -0
  42. package/dist/server/modules/better-auth/better-auth.resolver.js +14 -11
  43. package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -1
  44. package/dist/server/modules/user/user.controller.js +0 -8
  45. package/dist/server/modules/user/user.controller.js.map +1 -1
  46. package/dist/tsconfig.build.tsbuildinfo +1 -1
  47. package/package.json +1 -1
  48. package/src/core/common/interfaces/server-options.interface.ts +129 -58
  49. package/src/core/modules/auth/core-auth.controller.ts +2 -2
  50. package/src/core/modules/auth/core-auth.resolver.ts +2 -2
  51. package/src/core/modules/auth/guards/roles.guard.ts +298 -5
  52. package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +1 -1
  53. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +12 -11
  54. package/src/core/modules/better-auth/README.md +82 -43
  55. package/src/core/modules/better-auth/better-auth-rate-limiter.service.ts +1 -1
  56. package/src/core/modules/better-auth/better-auth-user.mapper.ts +9 -77
  57. package/src/core/modules/better-auth/better-auth.config.ts +45 -15
  58. package/src/core/modules/better-auth/better-auth.middleware.ts +85 -2
  59. package/src/core/modules/better-auth/better-auth.module.ts +83 -27
  60. package/src/core/modules/better-auth/better-auth.resolver.ts +0 -11
  61. package/src/core/modules/better-auth/better-auth.service.ts +367 -12
  62. package/src/core/modules/better-auth/better-auth.types.ts +16 -0
  63. package/src/core/modules/better-auth/core-better-auth.resolver.ts +111 -16
  64. package/src/core/modules/user/core-user.service.ts +27 -0
  65. package/src/core.module.ts +9 -3
  66. package/src/server/modules/better-auth/better-auth.module.ts +9 -3
  67. package/src/server/modules/better-auth/better-auth.resolver.ts +9 -11
  68. package/src/server/modules/user/user.controller.ts +1 -9
@@ -10,8 +10,11 @@ Integration of the [better-auth](https://better-auth.com) authentication framewo
10
10
  CoreModule.forRoot(envConfig), // IAM-only (new projects)
11
11
  BetterAuthModule.forRoot({ config: envConfig.betterAuth, fallbackSecrets: [envConfig.jwt?.secret] }),
12
12
 
13
- // 3. Configure in config.env.ts:
14
- betterAuth: { jwt: {}, twoFactor: {}, passkey: {} }
13
+ // 3. Configure in config.env.ts (minimal - JWT enabled by default):
14
+ betterAuth: true // or betterAuth: {} for same effect
15
+
16
+ // With optional features:
17
+ betterAuth: { twoFactor: {}, passkey: {} }
15
18
  ```
16
19
 
17
20
  **Quick Links:** [Integration Checklist](./INTEGRATION-CHECKLIST.md) | [REST API](#rest-api-endpoints) | [GraphQL API](#graphql-api) | [Configuration](#configuration)
@@ -36,11 +39,11 @@ betterAuth: { jwt: {}, twoFactor: {}, passkey: {} }
36
39
 
37
40
  ## Features
38
41
 
39
- ### Built-in Plugins (Explicit Configuration)
42
+ ### Built-in Plugins
40
43
 
41
- - **JWT Tokens** - For API clients and stateless authentication
42
- - **Two-Factor Authentication (2FA)** - TOTP-based second factor
43
- - **Passkey/WebAuthn** - Passwordless authentication
44
+ - **JWT Tokens** - For API clients and stateless authentication (**enabled by default**)
45
+ - **Two-Factor Authentication (2FA)** - TOTP-based second factor (opt-in)
46
+ - **Passkey/WebAuthn** - Passwordless authentication (opt-in)
44
47
 
45
48
  ### Core Features
46
49
 
@@ -93,7 +96,9 @@ https://github.com/lenneTech/nest-server/tree/develop/src/server/modules/better-
93
96
  **Copy from:** Reference implementation
94
97
 
95
98
  **WHY must ALL decorators be re-declared?**
96
- GraphQL schema is built from decorators at compile time. The parent class (`CoreBetterAuthResolver`) is marked as `isAbstract: true`, so its methods are not registered in the schema. You MUST re-declare `@Query`, `@Mutation`, `@Roles`, `@UseGuards` decorators in the child class for the methods to appear in the GraphQL schema.
99
+ GraphQL schema is built from decorators at compile time. The parent class (`CoreBetterAuthResolver`) is marked as `isAbstract: true`, so its methods are not registered in the schema. You MUST re-declare `@Query`, `@Mutation`, `@Roles` decorators in the child class for the methods to appear in the GraphQL schema.
100
+
101
+ **Note:** `@UseGuards(AuthGuard(JWT))` is NOT needed when using `@Roles(S_USER)` or `@Roles(ADMIN)` because `RolesGuard` already extends `AuthGuard(JWT)` internally.
97
102
 
98
103
  ### Step 3: Create BetterAuth Controller
99
104
  **Create:** `src/server/modules/better-auth/better-auth.controller.ts`
@@ -149,22 +154,28 @@ Add `betterAuth` configuration block. See reference for all available options in
149
154
 
150
155
  ## Quick Reference
151
156
 
157
+ **Configuration formats:**
158
+ ```typescript
159
+ betterAuth: true // Enable with all defaults (JWT enabled)
160
+ betterAuth: false // Disable completely
161
+ betterAuth: {} // Same as true
162
+ betterAuth: { ... } // Enable with custom settings
163
+ betterAuth: { enabled: false } // Disable (allows pre-configuration)
164
+ ```
165
+
152
166
  **Default values (used when not configured):**
153
167
 
168
+ - **JWT**: Enabled by default
154
169
  - **Secret**: Falls back to `jwt.secret` → `jwt.refresh.secret` → auto-generated
155
170
  - **Base URL**: `http://localhost:3000`
156
171
  - **Base Path**: `/iam`
157
- - **Passkey Origin**: `http://localhost:3000`
158
- - **Passkey rpId**: `localhost`
159
- - **Passkey rpName**: `Nest Server`
172
+ - **2FA/Passkey**: Disabled (opt-in)
160
173
 
161
174
  To **explicitly disable** Better-Auth:
162
175
 
163
176
  ```typescript
164
177
  const config = {
165
- betterAuth: {
166
- enabled: false,
167
- },
178
+ betterAuth: false, // or betterAuth: { enabled: false }
168
179
  };
169
180
  ```
170
181
 
@@ -330,28 +341,29 @@ const config = {
330
341
  export default {
331
342
  // ... other config
332
343
 
333
- // OPTIONAL: Better-Auth configuration
334
- // Omit entirely for default behavior, or customize as needed:
344
+ // MINIMAL: Just enable BetterAuth (JWT enabled by default)
345
+ betterAuth: true,
346
+
347
+ // OR with customization:
335
348
  betterAuth: {
336
349
  // enabled: true by default - only set to false to disable
337
350
  // secret: auto-generated if not set (see Security section above)
338
351
  // baseUrl: 'http://localhost:3000', // Default
339
352
  // basePath: '/iam', // Default
340
353
 
341
- // JWT Plugin (enabled by default when config block is present)
342
- // Set enabled: false to explicitly disable
354
+ // JWT Plugin - ENABLED BY DEFAULT (no config needed)
355
+ // Only add this block to customize or explicitly disable
343
356
  jwt: {
344
- expiresIn: '15m',
357
+ expiresIn: '30m', // Default: '15m'
358
+ // enabled: false, // Uncomment to disable JWT
345
359
  },
346
360
 
347
- // Two-Factor Authentication (enabled by default when config block is present)
348
- // Set enabled: false to explicitly disable
361
+ // Two-Factor Authentication (opt-in - requires config block)
349
362
  twoFactor: {
350
363
  appName: 'My Application',
351
364
  },
352
365
 
353
- // Passkey/WebAuthn (enabled by default when config block is present)
354
- // Set enabled: false to explicitly disable
366
+ // Passkey/WebAuthn (opt-in - requires config block)
355
367
  passkey: {
356
368
  rpId: 'localhost',
357
369
  rpName: 'My Application',
@@ -515,23 +527,25 @@ Better-Auth provides a rich plugin ecosystem. This module uses a **hybrid approa
515
527
  - **Built-in plugins** (JWT, 2FA, Passkey): Explicitly configured with typed options
516
528
  - **Additional plugins**: Dynamically added via the `plugins` array
517
529
 
518
- ### Built-in Plugins (Explicit Configuration)
530
+ ### Built-in Plugins
519
531
 
520
- These plugins are enabled by default when their config block is present. **All properties have sensible defaults**, so an empty block `{}` is sufficient!
532
+ | Plugin | Default State | Minimal Config to Enable | Default Values |
533
+ | ------------------ | ------------- | ------------------------ | --------------------------------------------------------------------------------- |
534
+ | **JWT** | **ENABLED** | *(none needed)* | `expiresIn: '15m'` |
535
+ | **Two-Factor** | Disabled | `twoFactor: {}` | `appName: 'Nest Server'` |
536
+ | **Passkey** | Disabled | `passkey: {}` | `origin: 'http://localhost:3000'`, `rpId: 'localhost'`, `rpName: 'Nest Server'` |
521
537
 
522
- | Plugin | Minimal Config | Default Values |
523
- | ------------------ | -------------------- | --------------------------------------------------------------------------------- |
524
- | **JWT** | `jwt: {}` | `expiresIn: '15m'` |
525
- | **Two-Factor** | `twoFactor: {}` | `appName: 'Nest Server'` |
526
- | **Passkey** | `passkey: {}` | `origin: 'http://localhost:3000'`, `rpId: 'localhost'`, `rpName: 'Nest Server'` |
538
+ **JWT is enabled by default** - no configuration needed. 2FA and Passkey require explicit configuration.
527
539
 
528
540
  #### Minimal Syntax (Recommended for Development)
529
541
 
530
542
  ```typescript
531
543
  const config = {
544
+ // JWT is enabled automatically with BetterAuth
545
+ betterAuth: true, // or betterAuth: {}
546
+
547
+ // To also enable 2FA and Passkey:
532
548
  betterAuth: {
533
- // Just add empty blocks - all defaults are applied!
534
- jwt: {},
535
549
  twoFactor: {},
536
550
  passkey: {},
537
551
  },
@@ -554,17 +568,20 @@ const config = {
554
568
  };
555
569
  ```
556
570
 
557
- #### Disabling a Plugin
571
+ #### Disabling Plugins
558
572
 
559
573
  ```typescript
560
574
  const config = {
561
575
  betterAuth: {
562
- jwt: { enabled: false }, // Explicitly disable JWT
563
- twoFactor: {}, // 2FA still enabled with defaults
576
+ jwt: false, // Disable JWT (or jwt: { enabled: false })
577
+ twoFactor: {}, // 2FA enabled with defaults
578
+ passkey: { enabled: false }, // Passkey explicitly disabled
564
579
  },
565
580
  };
566
581
  ```
567
582
 
583
+ **Note:** JWT is the only plugin enabled by default. To disable it, use `jwt: false` or `jwt: { enabled: false }`.
584
+
568
585
  ### Dynamic Plugins (plugins Array)
569
586
 
570
587
  For all other Better-Auth plugins, use the `plugins` array. This provides maximum flexibility without requiring updates to this package.
@@ -733,9 +750,9 @@ To explicitly disable Better-Auth:
733
750
 
734
751
  ```typescript
735
752
  const config = {
736
- betterAuth: {
737
- enabled: false,
738
- },
753
+ betterAuth: false, // Simple boolean
754
+ // or
755
+ betterAuth: { enabled: false }, // Allows pre-configuration
739
756
  };
740
757
  ```
741
758
 
@@ -749,10 +766,30 @@ When enabled, Better-Auth exposes the following endpoints at the configured `bas
749
766
  | `/iam/sign-in/email` | POST | Sign in with email/password |
750
767
  | `/iam/sign-out` | GET | Sign out (invalidate session)|
751
768
  | `/iam/session` | GET | Get current session |
769
+ | `/iam/token` | GET | Get fresh JWT token |
752
770
  | `/iam/forgot-password` | POST | Request password reset |
753
771
  | `/iam/reset-password` | POST | Reset password with token |
754
772
  | `/iam/verify-email` | POST | Verify email address |
755
773
 
774
+ ### JWT Token Endpoint
775
+
776
+ The `/iam/token` endpoint returns a fresh JWT token for the current session. Use this when your JWT has expired but your session is still valid.
777
+
778
+ **Request:**
779
+ ```bash
780
+ curl -X GET https://api.example.com/iam/token \
781
+ -H "Cookie: better-auth.session_token=..."
782
+ ```
783
+
784
+ **Response:**
785
+ ```json
786
+ {
787
+ "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6Ii4uLiJ9..."
788
+ }
789
+ ```
790
+
791
+ **Use case:** Microservice authentication - pass the JWT to other services that verify tokens via JWKS (`/iam/jwks`) without database access.
792
+
756
793
  ### Social Login Endpoints
757
794
 
758
795
  | Endpoint | Method | Description |
@@ -781,12 +818,14 @@ In addition to REST endpoints, Better-Auth provides GraphQL queries and mutation
781
818
 
782
819
  ### Queries
783
820
 
784
- | Query | Arguments | Return Type | Description |
785
- | ------------------------ | --------- | ---------------------------- | ------------------------------- |
786
- | `betterAuthEnabled` | - | `Boolean` | Check if Better-Auth is enabled |
787
- | `betterAuthFeatures` | - | `BetterAuthFeaturesModel` | Get enabled features status |
788
- | `betterAuthSession` | - | `BetterAuthSessionModel` | Get current session (auth req.) |
789
- | `betterAuthListPasskeys` | - | `[BetterAuthPasskeyModel]` | List user's passkeys (auth req.)|
821
+ | Query | Arguments | Return Type | Description |
822
+ | ------------------------ | --------- | ---------------------------- | --------------------------------- |
823
+ | `betterAuthEnabled` | - | `Boolean` | Check if Better-Auth is enabled |
824
+ | `betterAuthFeatures` | - | `BetterAuthFeaturesModel` | Get enabled features status |
825
+ | `betterAuthSession` | - | `BetterAuthSessionModel` | Get current session (auth req.) |
826
+ | `betterAuthToken` | - | `String` | Get fresh JWT token (auth req.) |
827
+ | `betterAuthListPasskeys` | - | `[BetterAuthPasskeyModel]` | List user's passkeys (auth req.) |
828
+ | `betterAuthMigrationStatus` | - | `BetterAuthMigrationStatusModel` | Migration status (admin only) |
790
829
 
791
830
  ### Mutations
792
831
 
@@ -100,7 +100,7 @@ export class BetterAuthRateLimiter {
100
100
  };
101
101
 
102
102
  if (this.config.enabled) {
103
- this.logger.log(`Rate limiting enabled: ${this.config.max} requests per ${this.config.windowSeconds}s`);
103
+ this.logger.debug(`Rate limiting enabled: ${this.config.max} requests per ${this.config.windowSeconds}s`);
104
104
  }
105
105
  }
106
106
 
@@ -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