@lenne.tech/nest-server 11.8.0 → 11.10.0

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 (173) hide show
  1. package/dist/config.env.js +5 -0
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/helpers/logging.helper.d.ts +6 -0
  4. package/dist/core/common/helpers/logging.helper.js +55 -0
  5. package/dist/core/common/helpers/logging.helper.js.map +1 -0
  6. package/dist/core/common/interfaces/server-options.interface.d.ts +50 -19
  7. package/dist/core/modules/auth/guards/roles.guard.js +37 -5
  8. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  9. package/dist/core/modules/auth/services/core-auth.service.d.ts +5 -5
  10. package/dist/core/modules/auth/services/core-auth.service.js +9 -8
  11. package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
  12. package/dist/core/modules/auth/tokens.decorator.d.ts +1 -1
  13. package/dist/core/modules/better-auth/better-auth.config.js +32 -10
  14. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  15. package/dist/core/modules/better-auth/better-auth.resolver.d.ts +16 -16
  16. package/dist/core/modules/better-auth/better-auth.resolver.js +34 -34
  17. package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
  18. package/dist/core/modules/better-auth/better-auth.types.d.ts +2 -1
  19. package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
  20. package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +10 -0
  21. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +91 -0
  22. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -0
  23. package/dist/core/modules/better-auth/core-better-auth-auth.model.d.ts +9 -0
  24. package/dist/core/modules/better-auth/{better-auth-auth.model.js → core-better-auth-auth.model.js} +17 -17
  25. package/dist/core/modules/better-auth/core-better-auth-auth.model.js.map +1 -0
  26. package/dist/core/modules/better-auth/{better-auth-migration-status.model.d.ts → core-better-auth-migration-status.model.d.ts} +1 -1
  27. package/dist/core/modules/better-auth/{better-auth-migration-status.model.js → core-better-auth-migration-status.model.js} +14 -14
  28. package/dist/core/modules/better-auth/core-better-auth-migration-status.model.js.map +1 -0
  29. package/dist/core/modules/better-auth/{better-auth-models.d.ts → core-better-auth-models.d.ts} +8 -8
  30. package/dist/core/modules/better-auth/{better-auth-models.js → core-better-auth-models.js} +61 -61
  31. package/dist/core/modules/better-auth/core-better-auth-models.js.map +1 -0
  32. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.d.ts +12 -0
  33. package/dist/core/modules/better-auth/{better-auth-rate-limit.middleware.js → core-better-auth-rate-limit.middleware.js} +10 -10
  34. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -0
  35. package/dist/core/modules/better-auth/{better-auth-rate-limiter.service.d.ts → core-better-auth-rate-limiter.service.d.ts} +1 -1
  36. package/dist/core/modules/better-auth/{better-auth-rate-limiter.service.js → core-better-auth-rate-limiter.service.js} +8 -8
  37. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -0
  38. package/dist/core/modules/better-auth/{better-auth-user.mapper.d.ts → core-better-auth-user.mapper.d.ts} +1 -1
  39. package/dist/core/modules/better-auth/{better-auth-user.mapper.js → core-better-auth-user.mapper.js} +10 -9
  40. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -0
  41. package/dist/core/modules/better-auth/core-better-auth-web.helper.d.ts +19 -0
  42. package/dist/core/modules/better-auth/core-better-auth-web.helper.js +152 -0
  43. package/dist/core/modules/better-auth/core-better-auth-web.helper.js.map +1 -0
  44. package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +23 -32
  45. package/dist/core/modules/better-auth/core-better-auth.controller.js +184 -201
  46. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  47. package/dist/core/modules/better-auth/core-better-auth.middleware.d.ts +22 -0
  48. package/dist/core/modules/better-auth/{better-auth.middleware.js → core-better-auth.middleware.js} +45 -18
  49. package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -0
  50. package/dist/core/modules/better-auth/{better-auth.module.d.ts → core-better-auth.module.d.ts} +6 -6
  51. package/dist/core/modules/better-auth/{better-auth.module.js → core-better-auth.module.js} +65 -60
  52. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -0
  53. package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +19 -19
  54. package/dist/core/modules/better-auth/core-better-auth.resolver.js +18 -18
  55. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
  56. package/dist/core/modules/better-auth/{better-auth.service.d.ts → core-better-auth.service.d.ts} +3 -2
  57. package/dist/core/modules/better-auth/{better-auth.service.js → core-better-auth.service.js} +75 -35
  58. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -0
  59. package/dist/core/modules/better-auth/index.d.ts +11 -9
  60. package/dist/core/modules/better-auth/index.js +11 -9
  61. package/dist/core/modules/better-auth/index.js.map +1 -1
  62. package/dist/core/modules/error-code/core-error-code.controller.d.ts +7 -0
  63. package/dist/core/modules/error-code/core-error-code.controller.js +45 -0
  64. package/dist/core/modules/error-code/core-error-code.controller.js.map +1 -0
  65. package/dist/core/modules/error-code/core-error-code.service.d.ts +16 -0
  66. package/dist/core/modules/error-code/core-error-code.service.js +65 -0
  67. package/dist/core/modules/error-code/core-error-code.service.js.map +1 -0
  68. package/dist/core/modules/error-code/error-code.module.d.ts +7 -0
  69. package/dist/core/modules/error-code/error-code.module.js +64 -0
  70. package/dist/core/modules/error-code/error-code.module.js.map +1 -0
  71. package/dist/core/modules/error-code/error-codes.d.ts +219 -0
  72. package/dist/core/modules/error-code/error-codes.js +204 -0
  73. package/dist/core/modules/error-code/error-codes.js.map +1 -0
  74. package/dist/core/modules/error-code/index.d.ts +5 -0
  75. package/dist/core/modules/error-code/index.js +22 -0
  76. package/dist/core/modules/error-code/index.js.map +1 -0
  77. package/dist/core/modules/error-code/interfaces/error-code.interfaces.d.ts +12 -0
  78. package/dist/core/modules/error-code/interfaces/error-code.interfaces.js +3 -0
  79. package/dist/core/modules/error-code/interfaces/error-code.interfaces.js.map +1 -0
  80. package/dist/core/modules/user/interfaces/core-user-service-options.interface.d.ts +2 -2
  81. package/dist/core.module.js +14 -6
  82. package/dist/core.module.js.map +1 -1
  83. package/dist/index.d.ts +2 -0
  84. package/dist/index.js +2 -0
  85. package/dist/index.js.map +1 -1
  86. package/dist/server/modules/better-auth/better-auth.controller.d.ts +5 -5
  87. package/dist/server/modules/better-auth/better-auth.controller.js +4 -4
  88. package/dist/server/modules/better-auth/better-auth.controller.js.map +1 -1
  89. package/dist/server/modules/better-auth/better-auth.module.js +3 -3
  90. package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
  91. package/dist/server/modules/better-auth/better-auth.resolver.d.ts +17 -17
  92. package/dist/server/modules/better-auth/better-auth.resolver.js +18 -18
  93. package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -1
  94. package/dist/server/modules/error-code/error-code.controller.d.ts +8 -0
  95. package/dist/server/modules/error-code/error-code.controller.js +55 -0
  96. package/dist/server/modules/error-code/error-code.controller.js.map +1 -0
  97. package/dist/server/modules/error-code/error-code.service.d.ts +4 -0
  98. package/dist/server/modules/error-code/error-code.service.js +27 -0
  99. package/dist/server/modules/error-code/error-code.service.js.map +1 -0
  100. package/dist/server/modules/error-code/error-codes.d.ts +45 -0
  101. package/dist/server/modules/error-code/error-codes.js +24 -0
  102. package/dist/server/modules/error-code/error-codes.js.map +1 -0
  103. package/dist/server/modules/error-code/index.d.ts +3 -0
  104. package/dist/server/modules/error-code/index.js +20 -0
  105. package/dist/server/modules/error-code/index.js.map +1 -0
  106. package/dist/server/modules/user/user.service.d.ts +2 -2
  107. package/dist/server/modules/user/user.service.js +2 -2
  108. package/dist/server/modules/user/user.service.js.map +1 -1
  109. package/dist/server/server.module.js +7 -0
  110. package/dist/server/server.module.js.map +1 -1
  111. package/dist/test/test.helper.d.ts +1 -0
  112. package/dist/test/test.helper.js +5 -1
  113. package/dist/test/test.helper.js.map +1 -1
  114. package/dist/tsconfig.build.tsbuildinfo +1 -1
  115. package/package.json +6 -4
  116. package/src/config.env.ts +19 -0
  117. package/src/core/common/helpers/logging.helper.ts +134 -0
  118. package/src/core/common/interfaces/server-options.interface.ts +511 -237
  119. package/src/core/modules/auth/guards/roles.guard.ts +49 -7
  120. package/src/core/modules/auth/services/core-auth.service.ts +9 -8
  121. package/src/core/modules/better-auth/ARCHITECTURE.md +102 -0
  122. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +277 -8
  123. package/src/core/modules/better-auth/README.md +97 -53
  124. package/src/core/modules/better-auth/better-auth.config.ts +66 -18
  125. package/src/core/modules/better-auth/better-auth.resolver.ts +32 -32
  126. package/src/core/modules/better-auth/better-auth.types.ts +3 -2
  127. package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +134 -0
  128. package/src/core/modules/better-auth/{better-auth-auth.model.ts → core-better-auth-auth.model.ts} +6 -6
  129. package/src/core/modules/better-auth/{better-auth-migration-status.model.ts → core-better-auth-migration-status.model.ts} +1 -1
  130. package/src/core/modules/better-auth/{better-auth-models.ts → core-better-auth-models.ts} +9 -9
  131. package/src/core/modules/better-auth/{better-auth-rate-limit.middleware.ts → core-better-auth-rate-limit.middleware.ts} +5 -5
  132. package/src/core/modules/better-auth/{better-auth-rate-limiter.service.ts → core-better-auth-rate-limiter.service.ts} +2 -2
  133. package/src/core/modules/better-auth/{better-auth-user.mapper.ts → core-better-auth-user.mapper.ts} +4 -3
  134. package/src/core/modules/better-auth/core-better-auth-web.helper.ts +272 -0
  135. package/src/core/modules/better-auth/core-better-auth.controller.ts +386 -230
  136. package/src/core/modules/better-auth/{better-auth.middleware.ts → core-better-auth.middleware.ts} +57 -17
  137. package/src/core/modules/better-auth/{better-auth.module.ts → core-better-auth.module.ts} +77 -66
  138. package/src/core/modules/better-auth/core-better-auth.resolver.ts +42 -42
  139. package/src/core/modules/better-auth/{better-auth.service.ts → core-better-auth.service.ts} +86 -40
  140. package/src/core/modules/better-auth/index.ts +18 -11
  141. package/src/core/modules/error-code/INTEGRATION-CHECKLIST.md +291 -0
  142. package/src/core/modules/error-code/core-error-code.controller.ts +55 -0
  143. package/src/core/modules/error-code/core-error-code.service.ts +135 -0
  144. package/src/core/modules/error-code/error-code.module.ts +119 -0
  145. package/src/core/modules/error-code/error-codes.ts +405 -0
  146. package/src/core/modules/error-code/index.ts +14 -0
  147. package/src/core/modules/error-code/interfaces/error-code.interfaces.ts +99 -0
  148. package/src/core/modules/user/interfaces/core-user-service-options.interface.ts +3 -3
  149. package/src/core.module.ts +28 -12
  150. package/src/index.ts +7 -0
  151. package/src/server/modules/better-auth/better-auth.controller.ts +4 -4
  152. package/src/server/modules/better-auth/better-auth.module.ts +1 -1
  153. package/src/server/modules/better-auth/better-auth.resolver.ts +31 -31
  154. package/src/server/modules/error-code/README.md +131 -0
  155. package/src/server/modules/error-code/error-code.controller.ts +91 -0
  156. package/src/server/modules/error-code/error-code.service.ts +42 -0
  157. package/src/server/modules/error-code/error-codes.ts +65 -0
  158. package/src/server/modules/error-code/index.ts +8 -0
  159. package/src/server/modules/user/user.service.ts +2 -2
  160. package/src/server/server.module.ts +10 -0
  161. package/src/test/test.helper.ts +13 -1
  162. package/dist/core/modules/better-auth/better-auth-auth.model.d.ts +0 -9
  163. package/dist/core/modules/better-auth/better-auth-auth.model.js.map +0 -1
  164. package/dist/core/modules/better-auth/better-auth-migration-status.model.js.map +0 -1
  165. package/dist/core/modules/better-auth/better-auth-models.js.map +0 -1
  166. package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.d.ts +0 -12
  167. package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.js.map +0 -1
  168. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js.map +0 -1
  169. package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +0 -1
  170. package/dist/core/modules/better-auth/better-auth.middleware.d.ts +0 -21
  171. package/dist/core/modules/better-auth/better-auth.middleware.js.map +0 -1
  172. package/dist/core/modules/better-auth/better-auth.module.js.map +0 -1
  173. package/dist/core/modules/better-auth/better-auth.service.js.map +0 -1
@@ -1,41 +1,41 @@
1
1
  import {
2
+ All,
2
3
  BadRequestException,
3
4
  Body,
4
5
  Controller,
5
6
  Get,
6
7
  HttpCode,
7
8
  HttpStatus,
9
+ InternalServerErrorException,
8
10
  Logger,
9
11
  Post,
10
12
  Req,
11
13
  Res,
12
14
  UnauthorizedException,
13
15
  } from '@nestjs/common';
14
- import { ApiBody, ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
16
+ import { ApiBody, ApiCreatedResponse, ApiExcludeEndpoint, ApiOkResponse, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
15
17
  import { Request, Response } from 'express';
16
18
 
17
19
  import { Roles } from '../../common/decorators/roles.decorator';
18
20
  import { RoleEnum } from '../../common/enums/role.enum';
21
+ import { isProduction, maskToken } from '../../common/helpers/logging.helper';
19
22
  import { ConfigService } from '../../common/services/config.service';
20
- import { BetterAuthSessionUser, BetterAuthUserMapper } from './better-auth-user.mapper';
21
- import { BetterAuthService } from './better-auth.service';
22
- import { hasSession, hasUser, requires2FA } from './better-auth.types';
23
+ import { BetterAuthSignInResponse, hasSession, hasUser, requires2FA } from './better-auth.types';
24
+ import { BetterAuthSessionUser, CoreBetterAuthUserMapper } from './core-better-auth-user.mapper';
25
+ import { sendWebResponse, toWebRequest } from './core-better-auth-web.helper';
26
+ import { CoreBetterAuthService } from './core-better-auth.service';
23
27
 
24
28
  // ===================================================================================================================
25
29
  // Response Models
26
30
  // ===================================================================================================================
27
31
 
28
- /**
29
- * Token response interface for JWT tokens
30
- */
31
- interface TokenResponse {
32
- token?: string;
33
- }
34
-
35
32
  /**
36
33
  * Session info for REST responses
34
+ *
35
+ * NOTE: The session token is NOT included in this response for security reasons.
36
+ * It is set as an httpOnly cookie instead.
37
37
  */
38
- export class BetterAuthSessionInfo {
38
+ export class CoreBetterAuthSessionInfo {
39
39
  @ApiProperty({ description: 'Session expiration time' })
40
40
  expiresAt: string;
41
41
 
@@ -46,7 +46,7 @@ export class BetterAuthSessionInfo {
46
46
  /**
47
47
  * User model for REST responses
48
48
  */
49
- export class BetterAuthUserResponse {
49
+ export class CoreBetterAuthUserResponse {
50
50
  @ApiProperty({ description: 'User email address' })
51
51
  email: string;
52
52
 
@@ -66,15 +66,15 @@ export class BetterAuthUserResponse {
66
66
  /**
67
67
  * Standard auth response
68
68
  */
69
- export class BetterAuthResponse {
69
+ export class CoreBetterAuthResponse {
70
70
  @ApiProperty({ description: 'Error message if failed', required: false })
71
71
  error?: string;
72
72
 
73
73
  @ApiProperty({ description: 'Whether 2FA is required', required: false })
74
74
  requiresTwoFactor?: boolean;
75
75
 
76
- @ApiProperty({ description: 'Session information', required: false, type: BetterAuthSessionInfo })
77
- session?: BetterAuthSessionInfo;
76
+ @ApiProperty({ description: 'Session information', required: false, type: CoreBetterAuthSessionInfo })
77
+ session?: CoreBetterAuthSessionInfo;
78
78
 
79
79
  @ApiProperty({ description: 'Whether operation succeeded' })
80
80
  success: boolean;
@@ -82,8 +82,8 @@ export class BetterAuthResponse {
82
82
  @ApiProperty({ description: 'JWT token (if JWT plugin enabled)', required: false })
83
83
  token?: string;
84
84
 
85
- @ApiProperty({ description: 'User information', required: false, type: BetterAuthUserResponse })
86
- user?: BetterAuthUserResponse;
85
+ @ApiProperty({ description: 'User information', required: false, type: CoreBetterAuthUserResponse })
86
+ user?: CoreBetterAuthUserResponse;
87
87
  }
88
88
 
89
89
  // ===================================================================================================================
@@ -93,7 +93,7 @@ export class BetterAuthResponse {
93
93
  /**
94
94
  * Sign-in input DTO
95
95
  */
96
- export class BetterAuthSignInInput {
96
+ export class CoreBetterAuthSignInInput {
97
97
  @ApiProperty({ description: 'User email address', example: 'user@example.com' })
98
98
  email: string;
99
99
 
@@ -104,7 +104,7 @@ export class BetterAuthSignInInput {
104
104
  /**
105
105
  * Sign-up input DTO
106
106
  */
107
- export class BetterAuthSignUpInput {
107
+ export class CoreBetterAuthSignUpInput {
108
108
  @ApiProperty({ description: 'User email address', example: 'user@example.com' })
109
109
  email: string;
110
110
 
@@ -115,31 +115,6 @@ export class BetterAuthSignUpInput {
115
115
  password: string;
116
116
  }
117
117
 
118
- /**
119
- * 2FA verification input DTO
120
- */
121
- export class BetterAuthTwoFactorInput {
122
- @ApiProperty({ description: 'TOTP code from authenticator app', example: '123456' })
123
- code: string;
124
- }
125
-
126
- /**
127
- * 2FA setup response
128
- */
129
- export class BetterAuthTwoFactorSetupResponse {
130
- @ApiProperty({ description: 'Backup codes for recovery' })
131
- backupCodes: string[];
132
-
133
- @ApiProperty({ description: 'Whether operation succeeded' })
134
- success: boolean;
135
-
136
- @ApiProperty({ description: 'TOTP secret for manual entry' })
137
- totpSecret: string;
138
-
139
- @ApiProperty({ description: 'QR code URI for authenticator apps' })
140
- totpUri: string;
141
- }
142
-
143
118
  // ===================================================================================================================
144
119
  // Controller
145
120
  // ===================================================================================================================
@@ -151,21 +126,47 @@ export class BetterAuthTwoFactorSetupResponse {
151
126
  * This controller follows the same pattern as CoreAuthController and can be
152
127
  * extended by project-specific implementations.
153
128
  *
129
+ * ## Why Custom Controller Instead of Native Better-Auth Endpoints?
130
+ *
131
+ * This controller implements custom endpoints rather than directly using Better-Auth's
132
+ * native API. This architecture is **necessary** for nest-server's requirements:
133
+ *
134
+ * ### 1. Better-Auth Hooks Cannot:
135
+ * - Access plaintext passwords in after-hooks (needed for Legacy sync)
136
+ * - Modify HTTP responses (needed for custom response format)
137
+ * - Set cookies (needed for multi-cookie auth strategy)
138
+ * - Access NestJS Dependency Injection (needed for UserService, etc.)
139
+ *
140
+ * ### 2. Custom Endpoints Enable:
141
+ * - **Hybrid Auth**: Bidirectional Legacy Auth ↔ Better-Auth synchronization
142
+ * - **Password Normalization**: SHA256 pre-hashing for security
143
+ * - **Legacy Migration**: Automatic migration of legacy users on sign-in
144
+ * - **Multi-Cookie Support**: Setting multiple auth cookies for compatibility
145
+ * - **Role Mapping**: Integration with nest-server's role-based access control
146
+ *
147
+ * ### 3. Native Handler Where Possible:
148
+ * Despite custom endpoints, we use `authInstance.handler()` for:
149
+ * - Plugin routes (Passkey, 2FA, OAuth)
150
+ * - 2FA verification (for correct cookie handling)
151
+ * - All plugin-provided functionality
152
+ *
153
+ * See README.md section "Architecture: Why Custom Controllers?" for details.
154
+ *
154
155
  * @example
155
156
  * ```typescript
156
157
  * // In your project - src/server/modules/better-auth/better-auth.controller.ts
157
158
  * @Controller('iam')
158
159
  * export class BetterAuthController extends CoreBetterAuthController {
159
160
  * constructor(
160
- * betterAuthService: BetterAuthService,
161
- * userMapper: BetterAuthUserMapper,
161
+ * betterAuthService: CoreBetterAuthService,
162
+ * userMapper: CoreBetterAuthUserMapper,
162
163
  * configService: ConfigService,
163
164
  * private readonly emailService: EmailService,
164
165
  * ) {
165
166
  * super(betterAuthService, userMapper, configService);
166
167
  * }
167
168
  *
168
- * override async signUp(res: Response, input: BetterAuthSignUpInput) {
169
+ * override async signUp(res: Response, input: CoreBetterAuthSignUpInput) {
169
170
  * const result = await super.signUp(res, input);
170
171
  * if (result.success && result.user) {
171
172
  * await this.emailService.sendWelcomeEmail(result.user.email);
@@ -182,8 +183,8 @@ export class CoreBetterAuthController {
182
183
  protected readonly logger = new Logger(CoreBetterAuthController.name);
183
184
 
184
185
  constructor(
185
- protected readonly betterAuthService: BetterAuthService,
186
- protected readonly userMapper: BetterAuthUserMapper,
186
+ protected readonly betterAuthService: CoreBetterAuthService,
187
+ protected readonly userMapper: CoreBetterAuthUserMapper,
187
188
  protected readonly configService: ConfigService,
188
189
  ) {}
189
190
 
@@ -193,17 +194,35 @@ export class CoreBetterAuthController {
193
194
 
194
195
  /**
195
196
  * Sign in with email and password
197
+ *
198
+ * **Why Custom Implementation (not hooks):**
199
+ * - Hooks cannot access plaintext password for legacy migration
200
+ * - Hooks cannot modify response format
201
+ * - Hooks cannot set multi-cookie auth strategy
202
+ *
203
+ * **Flow:**
204
+ * 1. Try legacy user migration if the user exists in legacy system
205
+ * → Requires plaintext password (unavailable in after-hooks)
206
+ * 2. Normalize password to SHA256 format for Better Auth
207
+ * 3. Call Better Auth API directly for consistent response format
208
+ * 4. For 2FA: Use native handler to ensure cookies are set correctly
209
+ * → Hooks cannot set cookies, so we use authInstance.handler()
210
+ * 5. Return response with multiple auth cookies
211
+ * → Hooks cannot modify response or set cookies
212
+ *
213
+ * @see README.md "Architecture: Why Custom Controllers?"
196
214
  */
197
- @ApiBody({ type: BetterAuthSignInInput })
198
- @ApiCreatedResponse({ description: 'Signed in successfully', type: BetterAuthResponse })
215
+ @ApiBody({ type: CoreBetterAuthSignInInput })
216
+ @ApiCreatedResponse({ description: 'Signed in successfully', type: CoreBetterAuthResponse })
199
217
  @ApiOperation({ description: 'Sign in via Better-Auth with email and password', summary: 'Sign In' })
200
218
  @HttpCode(HttpStatus.OK)
201
219
  @Post('sign-in/email')
202
220
  @Roles(RoleEnum.S_EVERYONE)
203
221
  async signIn(
222
+ @Req() req: Request,
204
223
  @Res({ passthrough: true }) res: Response,
205
- @Body() input: BetterAuthSignInInput,
206
- ): Promise<BetterAuthResponse> {
224
+ @Body() input: CoreBetterAuthSignInInput,
225
+ ): Promise<CoreBetterAuthResponse> {
207
226
  this.ensureEnabled();
208
227
 
209
228
  const api = this.betterAuthService.getApi();
@@ -211,47 +230,102 @@ export class CoreBetterAuthController {
211
230
  throw new BadRequestException('Better-Auth API not available');
212
231
  }
213
232
 
214
- // Try to sign in, with automatic legacy user migration
215
- return this.attemptSignIn(res, input, api, true);
216
- }
233
+ // Step 1: Try legacy user migration BEFORE Better Auth handles the request
234
+ // This allows users who exist in legacy system to be migrated automatically
235
+ try {
236
+ const migrated = await this.userMapper.migrateAccountToIam(input.email, input.password);
237
+ if (migrated) {
238
+ this.logger.debug(`Migrated legacy user ${input.email} to IAM`);
239
+ }
240
+ } catch (error) {
241
+ // Migration failure is not fatal - user might not exist in legacy or already migrated
242
+ this.logger.debug(`Legacy migration check: ${error instanceof Error ? error.message : 'not needed'}`);
243
+ }
217
244
 
218
- /**
219
- * Attempt sign-in with optional legacy user migration
220
- * @param res - Response object
221
- * @param input - Sign-in credentials
222
- * @param api - Better-Auth API instance
223
- * @param allowMigration - Whether to attempt legacy migration on failure
224
- */
225
- private async attemptSignIn(
226
- res: Response,
227
- input: BetterAuthSignInInput,
228
- api: ReturnType<BetterAuthService['getApi']>,
229
- allowMigration: boolean,
230
- ): Promise<BetterAuthResponse> {
231
- // Normalize password to SHA256 format for consistency with Legacy Auth
232
- // This ensures users can sign in with either plain password or SHA256 hash
245
+ // Step 2: Normalize password for Better Auth (SHA256 format)
233
246
  const normalizedPassword = this.userMapper.normalizePasswordForIam(input.password);
234
247
 
248
+ // Step 3: Call Better Auth API to check response type
235
249
  try {
236
- const response = await api!.signInEmail({
237
- body: { email: input.email, password: normalizedPassword },
238
- });
250
+ const response = (await api.signInEmail({
251
+ body: {
252
+ email: input.email,
253
+ password: normalizedPassword,
254
+ },
255
+ })) as BetterAuthSignInResponse | null;
239
256
 
240
257
  if (!response) {
241
258
  throw new UnauthorizedException('Invalid credentials');
242
259
  }
243
260
 
244
261
  // Check for 2FA requirement
262
+ // When 2FA is required, we need to use the native Better Auth handler
263
+ // because api.signInEmail() doesn't return the session token needed for 2FA verification
245
264
  if (requires2FA(response)) {
246
- return { requiresTwoFactor: true, success: false };
265
+ if (!isProduction()) {
266
+ this.logger.debug(`2FA required for ${input.email}, forwarding to native handler for cookie handling`);
267
+ }
268
+
269
+ // Forward to native Better Auth handler which sets the session cookie correctly
270
+ // We need to modify the request body to use the normalized password
271
+ const authInstance = this.betterAuthService.getInstance();
272
+ if (!authInstance) {
273
+ throw new InternalServerErrorException('Better-Auth not initialized');
274
+ }
275
+
276
+ // Create a modified request body with normalized password
277
+ const modifiedBody = JSON.stringify({
278
+ email: input.email,
279
+ password: normalizedPassword,
280
+ });
281
+
282
+ // Build the sign-in URL
283
+ const basePath = this.betterAuthService.getBasePath();
284
+ const baseUrl = this.betterAuthService.getBaseUrl();
285
+ const signInUrl = new URL(`${basePath}/sign-in/email`, baseUrl);
286
+
287
+ // Create a new Web Request for Better Auth's native handler
288
+ const webRequest = new Request(signInUrl.toString(), {
289
+ body: modifiedBody,
290
+ headers: new Headers({
291
+ 'Content-Type': 'application/json',
292
+ 'Origin': req.headers.origin || baseUrl,
293
+ }),
294
+ method: 'POST',
295
+ });
296
+
297
+ // Call Better Auth's native handler
298
+ const nativeResponse = await authInstance.handler(webRequest);
299
+
300
+ // Extract and forward Set-Cookie headers
301
+ const setCookieHeaders = nativeResponse.headers.getSetCookie?.() || [];
302
+ for (const cookie of setCookieHeaders) {
303
+ res.setHeader('Set-Cookie', cookie);
304
+ }
305
+
306
+ // Return the structured response
307
+ return {
308
+ requiresTwoFactor: true,
309
+ success: false,
310
+ };
311
+ }
312
+
313
+ // Check if response indicates an error
314
+ const responseAny = response as any;
315
+ if (responseAny?.error || responseAny?.code === 'CREDENTIAL_ACCOUNT_NOT_FOUND') {
316
+ throw new UnauthorizedException('Invalid credentials');
247
317
  }
248
318
 
249
- // Get user data
250
319
  if (hasUser(response)) {
320
+ // Link or create user in our database (in case it doesn't exist)
321
+ await this.userMapper.linkOrCreateUser(response.user);
322
+
251
323
  const mappedUser = await this.userMapper.mapSessionUser(response.user);
252
- const token = this.betterAuthService.isJwtEnabled() ? (response as TokenResponse).token : undefined;
253
324
 
254
- const result: BetterAuthResponse = {
325
+ // Get token (JWT if available, session token otherwise)
326
+ const token = responseAny.accessToken || responseAny.token;
327
+
328
+ const result: CoreBetterAuthResponse = {
255
329
  requiresTwoFactor: false,
256
330
  session: hasSession(response) ? this.mapSession(response.session) : undefined,
257
331
  success: true,
@@ -264,17 +338,11 @@ export class CoreBetterAuthController {
264
338
 
265
339
  throw new UnauthorizedException('Invalid credentials');
266
340
  } catch (error) {
267
- this.logger.debug(`Sign-in error: ${error instanceof Error ? error.message : 'Unknown error'}`);
268
-
269
- // If migration is allowed, try to migrate legacy user and retry
270
- if (allowMigration) {
271
- // Pass the original password for legacy verification, but migration uses normalized password
272
- const migrated = await this.userMapper.migrateAccountToIam(input.email, input.password);
273
- if (migrated) {
274
- this.logger.debug(`Migrated legacy user ${input.email} to IAM, retrying sign-in`);
275
- // Retry sign-in after migration (without allowing another migration to prevent loops)
276
- return this.attemptSignIn(res, input, api, false);
277
- }
341
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
342
+ this.logger.debug(`Sign-in error: ${errorMessage}`);
343
+
344
+ if (error instanceof UnauthorizedException) {
345
+ throw error;
278
346
  }
279
347
 
280
348
  throw new UnauthorizedException('Invalid credentials');
@@ -283,16 +351,33 @@ export class CoreBetterAuthController {
283
351
 
284
352
  /**
285
353
  * Sign up with email and password
354
+ *
355
+ * **Why Custom Implementation (not hooks):**
356
+ * - After-hooks don't have access to plaintext password
357
+ * → Cannot call syncPasswordToLegacy() in hooks
358
+ * - Hooks cannot access NestJS services
359
+ * → Cannot use UserMapper for user linking
360
+ * - Hooks cannot modify response format
361
+ *
362
+ * **Custom Logic:**
363
+ * 1. Normalize password to SHA256 for Better Auth storage
364
+ * 2. Create user via Better Auth API
365
+ * 3. Link user to Legacy system (requires NestJS UserMapper)
366
+ * 4. Sync plaintext password to Legacy Auth (bcrypt hash)
367
+ * → CRITICAL: This requires plaintext, unavailable in after-hooks
368
+ * 5. Return response with session cookies
369
+ *
370
+ * @see README.md "Architecture: Why Custom Controllers?"
286
371
  */
287
- @ApiBody({ type: BetterAuthSignUpInput })
288
- @ApiCreatedResponse({ description: 'Signed up successfully', type: BetterAuthResponse })
372
+ @ApiBody({ type: CoreBetterAuthSignUpInput })
373
+ @ApiCreatedResponse({ description: 'Signed up successfully', type: CoreBetterAuthResponse })
289
374
  @ApiOperation({ description: 'Sign up via Better-Auth with email and password', summary: 'Sign Up' })
290
375
  @Post('sign-up/email')
291
376
  @Roles(RoleEnum.S_EVERYONE)
292
377
  async signUp(
293
378
  @Res({ passthrough: true }) res: Response,
294
- @Body() input: BetterAuthSignUpInput,
295
- ): Promise<BetterAuthResponse> {
379
+ @Body() input: CoreBetterAuthSignUpInput,
380
+ ): Promise<CoreBetterAuthResponse> {
296
381
  this.ensureEnabled();
297
382
 
298
383
  const api = this.betterAuthService.getApi();
@@ -326,7 +411,7 @@ export class CoreBetterAuthController {
326
411
 
327
412
  const mappedUser = await this.userMapper.mapSessionUser(response.user);
328
413
 
329
- const result: BetterAuthResponse = {
414
+ const result: CoreBetterAuthResponse = {
330
415
  requiresTwoFactor: false,
331
416
  session: hasSession(response) ? this.mapSession(response.session) : undefined,
332
417
  success: true,
@@ -349,12 +434,20 @@ export class CoreBetterAuthController {
349
434
 
350
435
  /**
351
436
  * Sign out (logout)
437
+ *
438
+ * **Why Custom Implementation (not hooks):**
439
+ * - Must clear multiple cookies (token, session, better-auth.session_token, etc.)
440
+ * - Hooks cannot modify response or set/clear cookies
441
+ *
442
+ * NOTE: Better-Auth uses POST for sign-out (matches better-auth convention)
443
+ *
444
+ * @see README.md "Architecture: Why Custom Controllers?"
352
445
  */
353
- @ApiOkResponse({ description: 'Signed out successfully', type: BetterAuthResponse })
446
+ @ApiOkResponse({ description: 'Signed out successfully', type: CoreBetterAuthResponse })
354
447
  @ApiOperation({ description: 'Sign out from Better-Auth', summary: 'Sign Out' })
355
- @Get('sign-out')
448
+ @Post('sign-out')
356
449
  @Roles(RoleEnum.S_EVERYONE)
357
- async signOut(@Req() req: Request, @Res({ passthrough: true }) res: Response): Promise<BetterAuthResponse> {
450
+ async signOut(@Req() req: Request, @Res({ passthrough: true }) res: Response): Promise<CoreBetterAuthResponse> {
358
451
  if (!this.betterAuthService.isEnabled()) {
359
452
  return { success: true };
360
453
  }
@@ -381,12 +474,19 @@ export class CoreBetterAuthController {
381
474
 
382
475
  /**
383
476
  * Get current session
477
+ *
478
+ * **Why Custom Implementation (not hooks):**
479
+ * - Must map Better Auth user to nest-server user with roles
480
+ * - Hooks cannot access NestJS UserMapper service
481
+ * - Custom response format with mapped user data
482
+ *
483
+ * @see README.md "Architecture: Why Custom Controllers?"
384
484
  */
385
- @ApiOkResponse({ description: 'Current session', type: BetterAuthResponse })
485
+ @ApiOkResponse({ description: 'Current session', type: CoreBetterAuthResponse })
386
486
  @ApiOperation({ description: 'Get current session from Better-Auth', summary: 'Get Session' })
387
487
  @Get('session')
388
488
  @Roles(RoleEnum.S_EVERYONE)
389
- async getSession(@Req() req: Request): Promise<BetterAuthResponse> {
489
+ async getSession(@Req() req: Request): Promise<CoreBetterAuthResponse> {
390
490
  if (!this.betterAuthService.isEnabled()) {
391
491
  return { error: 'Better-Auth is disabled', success: false };
392
492
  }
@@ -412,133 +512,42 @@ export class CoreBetterAuthController {
412
512
  }
413
513
 
414
514
  // ===================================================================================================================
415
- // Two-Factor Authentication Endpoints
515
+ // Catch-All Route for Better Auth Plugins
416
516
  // ===================================================================================================================
417
517
 
418
518
  /**
419
- * Enable 2FA for current user
420
- */
421
- @ApiOkResponse({ description: '2FA setup information', type: BetterAuthTwoFactorSetupResponse })
422
- @ApiOperation({ description: 'Enable Two-Factor Authentication', summary: 'Enable 2FA' })
423
- @Post('two-factor/enable')
424
- @Roles(RoleEnum.S_USER)
425
- async enableTwoFactor(@Req() req: Request): Promise<BetterAuthResponse | BetterAuthTwoFactorSetupResponse> {
426
- this.ensureEnabled();
427
-
428
- if (!this.betterAuthService.isTwoFactorEnabled()) {
429
- throw new BadRequestException('Two-factor authentication is not enabled');
430
- }
431
-
432
- const api = this.betterAuthService.getApi();
433
- if (!api || !('enableTwoFactor' in api)) {
434
- throw new BadRequestException('2FA API not available');
435
- }
436
-
437
- try {
438
- const headers = this.extractHeaders(req);
439
- const response = await (api as any).enableTwoFactor({ headers });
440
-
441
- if (!response) {
442
- throw new BadRequestException('Failed to enable 2FA');
443
- }
444
-
445
- return {
446
- backupCodes: response.backupCodes || [],
447
- success: true,
448
- totpSecret: response.totpSecret || '',
449
- totpUri: response.totpURI || '',
450
- };
451
- } catch (error) {
452
- this.logger.debug(`Enable 2FA error: ${error instanceof Error ? error.message : 'Unknown error'}`);
453
- throw new BadRequestException('Failed to enable 2FA');
454
- }
455
- }
456
-
457
- /**
458
- * Verify 2FA code during sign-in
519
+ * Catch-all route for all other Better Auth plugin endpoints.
520
+ *
521
+ * **This route USES the native Better Auth handler** via `authInstance.handler()`.
522
+ * It's the best of both worlds:
523
+ * - Custom endpoints where we need NestJS features (sign-in, sign-up, etc.)
524
+ * - Native handler for plugins that work correctly out-of-the-box
525
+ *
526
+ * **Why Not Fully Native:**
527
+ * Even this catch-all requires custom logic:
528
+ * - Session token injection into request (before-hooks can't inject tokens)
529
+ * - Converting Express Request to Web Standard Request
530
+ *
531
+ * **Handles:**
532
+ * - Passkey/WebAuthn (all endpoints)
533
+ * - Two-Factor Authentication (all endpoints)
534
+ * - Social Login OAuth flows
535
+ * - Email verification
536
+ * - Magic link authentication
537
+ * - Any other Better Auth plugin functionality
538
+ *
539
+ * IMPORTANT: This route must be defined LAST in the controller to ensure
540
+ * it doesn't intercept the explicitly defined routes above.
541
+ *
542
+ * Better Auth handles authentication internally - it returns appropriate
543
+ * errors (401, 403) if a user is not authenticated for protected endpoints.
544
+ *
545
+ * @see README.md "Architecture: Why Custom Controllers?"
459
546
  */
460
- @ApiBody({ type: BetterAuthTwoFactorInput })
461
- @ApiOkResponse({ description: 'Verification result', type: BetterAuthResponse })
462
- @ApiOperation({ description: 'Verify Two-Factor Authentication code', summary: 'Verify 2FA' })
463
- @HttpCode(HttpStatus.OK)
464
- @Post('two-factor/verify')
547
+ @All('*path')
465
548
  @Roles(RoleEnum.S_EVERYONE)
466
- async verifyTwoFactor(
467
- @Req() req: Request,
468
- @Res({ passthrough: true }) res: Response,
469
- @Body() input: BetterAuthTwoFactorInput,
470
- ): Promise<BetterAuthResponse> {
471
- this.ensureEnabled();
472
-
473
- if (!this.betterAuthService.isTwoFactorEnabled()) {
474
- throw new BadRequestException('Two-factor authentication is not enabled');
475
- }
476
-
477
- const api = this.betterAuthService.getApi();
478
- if (!api || !('verifyTOTP' in api)) {
479
- throw new BadRequestException('2FA API not available');
480
- }
481
-
482
- try {
483
- const headers = this.extractHeaders(req);
484
- const response = await (api as any).verifyTOTP({
485
- body: { code: input.code },
486
- headers,
487
- });
488
-
489
- if (!response) {
490
- throw new UnauthorizedException('Invalid 2FA code');
491
- }
492
-
493
- if (hasUser(response)) {
494
- const mappedUser = await this.userMapper.mapSessionUser(response.user);
495
- const token = this.betterAuthService.isJwtEnabled() ? (response as TokenResponse).token : undefined;
496
-
497
- const result: BetterAuthResponse = {
498
- session: hasSession(response) ? this.mapSession(response.session) : undefined,
499
- success: true,
500
- token,
501
- user: mappedUser ? this.mapUser(response.user, mappedUser) : undefined,
502
- };
503
-
504
- return this.processCookies(res, result);
505
- }
506
-
507
- throw new UnauthorizedException('Invalid 2FA code');
508
- } catch (error) {
509
- this.logger.debug(`Verify 2FA error: ${error instanceof Error ? error.message : 'Unknown error'}`);
510
- throw new UnauthorizedException('Invalid 2FA code');
511
- }
512
- }
513
-
514
- /**
515
- * Disable 2FA for current user
516
- */
517
- @ApiOkResponse({ description: 'Disable result', type: BetterAuthResponse })
518
- @ApiOperation({ description: 'Disable Two-Factor Authentication', summary: 'Disable 2FA' })
519
- @Post('two-factor/disable')
520
- @Roles(RoleEnum.S_USER)
521
- async disableTwoFactor(@Req() req: Request): Promise<BetterAuthResponse> {
522
- this.ensureEnabled();
523
-
524
- if (!this.betterAuthService.isTwoFactorEnabled()) {
525
- throw new BadRequestException('Two-factor authentication is not enabled');
526
- }
527
-
528
- const api = this.betterAuthService.getApi();
529
- if (!api || !('disableTwoFactor' in api)) {
530
- throw new BadRequestException('2FA API not available');
531
- }
532
-
533
- try {
534
- const headers = this.extractHeaders(req);
535
- await (api as any).disableTwoFactor({ headers });
536
-
537
- return { success: true };
538
- } catch (error) {
539
- this.logger.debug(`Disable 2FA error: ${error instanceof Error ? error.message : 'Unknown error'}`);
540
- throw new BadRequestException('Failed to disable 2FA');
541
- }
549
+ async handlePluginRoutes(@Req() req: Request, @Res() res: Response): Promise<void> {
550
+ return this.handleBetterAuthPlugins(req, res);
542
551
  }
543
552
 
544
553
  // ===================================================================================================================
@@ -587,12 +596,16 @@ export class CoreBetterAuthController {
587
596
 
588
597
  /**
589
598
  * Map session to response format
599
+ *
600
+ * NOTE: The session token is intentionally NOT included in the response.
601
+ * It is set as an httpOnly cookie for security.
590
602
  */
591
- protected mapSession(session: null | undefined | { expiresAt: Date; id: string }): BetterAuthSessionInfo | undefined {
603
+ protected mapSession(session: null | undefined | { expiresAt: Date; id: string; token?: string }): CoreBetterAuthSessionInfo | undefined {
592
604
  if (!session) return undefined;
593
605
  return {
594
606
  expiresAt: session.expiresAt instanceof Date ? session.expiresAt.toISOString() : String(session.expiresAt),
595
607
  id: session.id,
608
+ // NOTE: token is intentionally NOT returned - it's set as httpOnly cookie
596
609
  };
597
610
  }
598
611
 
@@ -602,7 +615,7 @@ export class CoreBetterAuthController {
602
615
  * @param _mappedUser - The synced user from legacy system (available for override customization)
603
616
  */
604
617
  // eslint-disable-next-line unused-imports/no-unused-vars
605
- protected mapUser(sessionUser: BetterAuthSessionUser, _mappedUser: any): BetterAuthUserResponse {
618
+ protected mapUser(sessionUser: BetterAuthSessionUser, _mappedUser: any): CoreBetterAuthUserResponse {
606
619
  return {
607
620
  email: sessionUser.email,
608
621
  emailVerified: sessionUser.emailVerified || false,
@@ -613,19 +626,63 @@ export class CoreBetterAuthController {
613
626
 
614
627
  /**
615
628
  * Process cookies for response
629
+ *
630
+ * Sets multiple cookies for authentication compatibility:
631
+ *
632
+ * | Cookie Name | Purpose |
633
+ * |-------------|---------|
634
+ * | `token` | Primary session token (nest-server compatibility) |
635
+ * | `{basePath}.session_token` | Better Auth's native cookie for plugins (e.g., `iam.session_token`) |
636
+ * | `better-auth.session_token` | Legacy Better Auth cookie name (backwards compatibility) |
637
+ * | `{configured}` | Custom cookie name if configured via `options.advanced.cookies.session_token.name` |
638
+ * | `session` | Session ID for reference/debugging |
639
+ *
640
+ * IMPORTANT: Better Auth's sign-in returns a session token in `result.token`.
641
+ * This is NOT a JWT - it's the session token stored in the database.
642
+ * The JWT plugin generates JWTs separately via the /token endpoint when needed.
643
+ *
644
+ * For plugins like Passkey to work, the session token must be available in a cookie
645
+ * that Better Auth's plugin system recognizes (default: `{basePath}.session_token`).
646
+ *
647
+ * @param res - Express Response object
648
+ * @param result - The CoreBetterAuthResponse to return
649
+ * @param sessionToken - Optional session token to set in cookies (if not provided, uses result.token)
616
650
  */
617
- protected processCookies(res: Response, result: BetterAuthResponse): BetterAuthResponse {
651
+ protected processCookies(res: Response, result: CoreBetterAuthResponse, sessionToken?: string): CoreBetterAuthResponse {
618
652
  // Check if cookie handling is activated
619
653
  if (this.configService.getFastButReadOnly('cookies')) {
620
654
  const cookieOptions = { httpOnly: true, sameSite: 'lax' as const, secure: process.env.NODE_ENV === 'production' };
621
655
 
622
- // Set or clear token cookie
623
- if (result.token) {
624
- res.cookie('token', result.token, cookieOptions);
625
- delete result.token; // Remove from response body
656
+ // Use provided sessionToken or fall back to result.token
657
+ const tokenToSet = sessionToken || result.token;
658
+
659
+ if (tokenToSet) {
660
+ // Set the primary token cookie (for nest-server compatibility)
661
+ res.cookie('token', tokenToSet, cookieOptions);
662
+
663
+ // Set Better Auth's native session token cookies for plugin compatibility
664
+ // This is CRITICAL for Passkey/WebAuthn to work
665
+ const basePath = this.betterAuthService.getBasePath().replace(/^\//, '').replace(/\//g, '.');
666
+ const defaultCookieName = `${basePath}.session_token`;
667
+ res.cookie(defaultCookieName, tokenToSet, cookieOptions);
668
+
669
+ // Also set the legacy cookie name for backwards compatibility
670
+ res.cookie('better-auth.session_token', tokenToSet, cookieOptions);
671
+
672
+ // Get configured cookie name and set if different from defaults
673
+ const betterAuthConfig = this.configService.getFastButReadOnly('betterAuth');
674
+ const configuredCookieName = betterAuthConfig?.options?.advanced?.cookies?.session_token?.name;
675
+ if (configuredCookieName && configuredCookieName !== 'token' && configuredCookieName !== defaultCookieName) {
676
+ res.cookie(configuredCookieName, tokenToSet, cookieOptions);
677
+ }
678
+
679
+ // Remove token from response body (it's now in cookies)
680
+ if (result.token) {
681
+ delete result.token;
682
+ }
626
683
  }
627
684
 
628
- // Set session cookie if we have a session
685
+ // Set session ID cookie (for reference/debugging)
629
686
  if (result.session) {
630
687
  res.cookie('session', result.session.id, cookieOptions);
631
688
  }
@@ -642,5 +699,104 @@ export class CoreBetterAuthController {
642
699
  res.cookie('token', '', { ...cookieOptions, maxAge: 0 });
643
700
  res.cookie('session', '', { ...cookieOptions, maxAge: 0 });
644
701
  res.cookie('better-auth.session_token', '', { ...cookieOptions, maxAge: 0 });
702
+
703
+ // Clear the path-based session token cookie
704
+ const basePath = this.betterAuthService.getBasePath().replace(/^\//, '').replace(/\//g, '.');
705
+ const defaultCookieName = `${basePath}.session_token`;
706
+ res.cookie(defaultCookieName, '', { ...cookieOptions, maxAge: 0 });
707
+
708
+ // Clear configured session token cookie if different
709
+ const betterAuthConfig = this.configService.getFastButReadOnly('betterAuth');
710
+ const configuredCookieName = betterAuthConfig?.options?.advanced?.cookies?.session_token?.name;
711
+ if (configuredCookieName && configuredCookieName !== 'token' && configuredCookieName !== defaultCookieName) {
712
+ res.cookie(configuredCookieName, '', { ...cookieOptions, maxAge: 0 });
713
+ }
714
+ }
715
+
716
+ // ===================================================================================================================
717
+ // Better Auth Plugin Handler (shared implementation)
718
+ // ===================================================================================================================
719
+
720
+ /**
721
+ * Handler for Better Auth plugin endpoints (Passkey, Social Login, etc.)
722
+ *
723
+ * This method forwards requests to Better Auth's native handler. It enables:
724
+ * - Passkey/WebAuthn registration and authentication
725
+ * - Social Login OAuth flows
726
+ * - Email verification links
727
+ * - Magic link authentication
728
+ * - And other plugin-provided functionality
729
+ *
730
+ * IMPORTANT: This method injects the session token into both cookies AND
731
+ * Authorization header to ensure Better Auth can find the session via
732
+ * multiple lookup strategies.
733
+ */
734
+ @ApiExcludeEndpoint() // Don't show in Swagger docs
735
+ protected async handleBetterAuthPlugins(req: Request, res: Response): Promise<void> {
736
+ this.ensureEnabled();
737
+
738
+ const authInstance = this.betterAuthService.getInstance();
739
+ if (!authInstance) {
740
+ throw new InternalServerErrorException('Better-Auth not initialized');
741
+ }
742
+
743
+ if (!isProduction()) {
744
+ this.logger.debug(`Forwarding to Better Auth: ${req.method} ${req.path}`);
745
+ }
746
+
747
+ try {
748
+ // Extract session token from the validated middleware session or cookies
749
+ const sessionToken = this.getSessionTokenFromRequest(req);
750
+
751
+ if (!isProduction()) {
752
+ this.logger.debug(`Session token for forwarding: ${maskToken(sessionToken)}`);
753
+ }
754
+
755
+ // Get config for signing cookies
756
+ const config = this.betterAuthService.getConfig();
757
+
758
+ // Convert Express request to Web Standard Request with enhanced session context
759
+ const webRequest = await toWebRequest(req, {
760
+ basePath: this.betterAuthService.getBasePath(),
761
+ baseUrl: this.betterAuthService.getBaseUrl(),
762
+ logger: this.logger,
763
+ secret: config.secret,
764
+ sessionToken,
765
+ });
766
+
767
+ // Call Better Auth's native handler
768
+ const response = await authInstance.handler(webRequest);
769
+
770
+ if (!isProduction()) {
771
+ this.logger.debug(`Better Auth handler response status: ${response.status}`);
772
+ }
773
+
774
+ // Send the response back
775
+ await sendWebResponse(res, response);
776
+ } catch (error) {
777
+ this.logger.error(`Better Auth handler error: ${error instanceof Error ? error.message : 'Unknown error'}`);
778
+
779
+ // Re-throw NestJS exceptions
780
+ if (error instanceof BadRequestException || error instanceof UnauthorizedException || error instanceof InternalServerErrorException) {
781
+ throw error;
782
+ }
783
+
784
+ throw new InternalServerErrorException('Authentication handler error');
785
+ }
786
+ }
787
+
788
+ /**
789
+ * Gets the session token from the request.
790
+ * Prioritizes the middleware-validated session, then falls back to cookies.
791
+ */
792
+ private getSessionTokenFromRequest(req: Request): null | string {
793
+ // First, try to get token from middleware-validated session
794
+ const betterAuthReq = req as any;
795
+ if (betterAuthReq.betterAuthSession?.session?.token) {
796
+ return betterAuthReq.betterAuthSession.session.token;
797
+ }
798
+
799
+ // Fall back to extracting from cookies
800
+ return this.extractSessionToken(req);
645
801
  }
646
802
  }