@lenne.tech/nest-server 11.10.1 → 11.10.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 (75) hide show
  1. package/dist/config.env.js +16 -133
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/interfaces/server-options.interface.d.ts +4 -0
  4. package/dist/core/modules/auth/guards/auth.guard.d.ts +2 -2
  5. package/dist/core/modules/auth/guards/auth.guard.js +68 -8
  6. package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
  7. package/dist/core/modules/auth/guards/roles.guard.d.ts +3 -4
  8. package/dist/core/modules/auth/guards/roles.guard.js +64 -159
  9. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  10. package/dist/core/modules/better-auth/better-auth-token.service.d.ts +21 -0
  11. package/dist/core/modules/better-auth/better-auth-token.service.js +153 -0
  12. package/dist/core/modules/better-auth/better-auth-token.service.js.map +1 -0
  13. package/dist/core/modules/better-auth/better-auth.config.d.ts +3 -0
  14. package/dist/core/modules/better-auth/better-auth.config.js +176 -47
  15. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  16. package/dist/core/modules/better-auth/better-auth.types.d.ts +13 -0
  17. package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
  18. package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +5 -1
  19. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +101 -8
  20. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
  21. package/dist/core/modules/better-auth/core-better-auth-challenge.service.d.ts +20 -0
  22. package/dist/core/modules/better-auth/core-better-auth-challenge.service.js +142 -0
  23. package/dist/core/modules/better-auth/core-better-auth-challenge.service.js.map +1 -0
  24. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +1 -1
  25. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
  26. package/dist/core/modules/better-auth/core-better-auth-web.helper.d.ts +2 -0
  27. package/dist/core/modules/better-auth/core-better-auth-web.helper.js +29 -1
  28. package/dist/core/modules/better-auth/core-better-auth-web.helper.js.map +1 -1
  29. package/dist/core/modules/better-auth/core-better-auth.controller.js +5 -13
  30. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  31. package/dist/core/modules/better-auth/core-better-auth.middleware.d.ts +0 -1
  32. package/dist/core/modules/better-auth/core-better-auth.middleware.js +6 -19
  33. package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -1
  34. package/dist/core/modules/better-auth/core-better-auth.module.d.ts +6 -1
  35. package/dist/core/modules/better-auth/core-better-auth.module.js +82 -19
  36. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  37. package/dist/core/modules/better-auth/core-better-auth.resolver.js +7 -6
  38. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
  39. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +1 -2
  40. package/dist/core/modules/better-auth/core-better-auth.service.js +27 -37
  41. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  42. package/dist/core/modules/better-auth/index.d.ts +1 -0
  43. package/dist/core/modules/better-auth/index.js +1 -0
  44. package/dist/core/modules/better-auth/index.js.map +1 -1
  45. package/dist/core.module.js +4 -0
  46. package/dist/core.module.js.map +1 -1
  47. package/dist/server/modules/better-auth/better-auth.module.d.ts +4 -1
  48. package/dist/server/modules/better-auth/better-auth.module.js +4 -1
  49. package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
  50. package/dist/server/server.module.js +1 -4
  51. package/dist/server/server.module.js.map +1 -1
  52. package/dist/tsconfig.build.tsbuildinfo +1 -1
  53. package/package.json +1 -1
  54. package/src/config.env.ts +24 -174
  55. package/src/core/common/interfaces/server-options.interface.ts +288 -35
  56. package/src/core/modules/auth/guards/auth.guard.ts +136 -23
  57. package/src/core/modules/auth/guards/roles.guard.ts +119 -239
  58. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +82 -56
  59. package/src/core/modules/better-auth/README.md +132 -35
  60. package/src/core/modules/better-auth/better-auth-token.service.ts +241 -0
  61. package/src/core/modules/better-auth/better-auth.config.ts +402 -70
  62. package/src/core/modules/better-auth/better-auth.types.ts +37 -0
  63. package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +158 -18
  64. package/src/core/modules/better-auth/core-better-auth-challenge.service.ts +254 -0
  65. package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +1 -1
  66. package/src/core/modules/better-auth/core-better-auth-web.helper.ts +64 -1
  67. package/src/core/modules/better-auth/core-better-auth.controller.ts +7 -15
  68. package/src/core/modules/better-auth/core-better-auth.middleware.ts +7 -20
  69. package/src/core/modules/better-auth/core-better-auth.module.ts +182 -25
  70. package/src/core/modules/better-auth/core-better-auth.resolver.ts +8 -7
  71. package/src/core/modules/better-auth/core-better-auth.service.ts +40 -48
  72. package/src/core/modules/better-auth/index.ts +1 -0
  73. package/src/core.module.ts +8 -0
  74. package/src/server/modules/better-auth/better-auth.module.ts +40 -10
  75. package/src/server/server.module.ts +2 -4
@@ -1,11 +1,11 @@
1
1
  import { ExecutionContext, ForbiddenException, Injectable, Logger, Optional, UnauthorizedException } from '@nestjs/common';
2
2
  import { ModuleRef, Reflector } from '@nestjs/core';
3
3
  import { GqlExecutionContext } from '@nestjs/graphql';
4
- import { getConnectionToken } from '@nestjs/mongoose';
5
- import { Connection, Types } from 'mongoose';
6
4
  import { firstValueFrom, isObservable } from 'rxjs';
7
5
 
8
6
  import { RoleEnum } from '../../../common/enums/role.enum';
7
+ import { BetterAuthTokenService } from '../../better-auth/better-auth-token.service';
8
+ import { BetterAuthenticatedUser } from '../../better-auth/better-auth.types';
9
9
  import { CoreBetterAuthService } from '../../better-auth/core-better-auth.service';
10
10
  import { ErrorCode } from '../../error-code';
11
11
  import { AuthGuardStrategy } from '../auth-guard-strategy.enum';
@@ -23,7 +23,7 @@ import { AuthGuard } from './auth.guard';
23
23
  * MULTI-TOKEN SUPPORT:
24
24
  * This guard supports multiple authentication token types:
25
25
  * 1. Legacy JWT tokens (Passport JWT strategy)
26
- * 2. BetterAuth JWT tokens (verified via BetterAuth service)
26
+ * 2. BetterAuth JWT tokens (verified via BetterAuthTokenService)
27
27
  * 3. BetterAuth session tokens (verified via database lookup)
28
28
  *
29
29
  * When Passport JWT validation fails, the guard falls back to BetterAuth verification:
@@ -37,7 +37,7 @@ import { AuthGuard } from './auth.guard';
37
37
  export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
38
38
  private readonly logger = new Logger(RolesGuard.name);
39
39
  private betterAuthService: CoreBetterAuthService | null = null;
40
- private mongoConnection: Connection | null = null;
40
+ private tokenService: BetterAuthTokenService | null = null;
41
41
  private servicesResolved = false;
42
42
 
43
43
  /**
@@ -51,7 +51,7 @@ export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
51
51
  }
52
52
 
53
53
  /**
54
- * Lazily resolve BetterAuth service and MongoDB connection
54
+ * Lazily resolve BetterAuth services
55
55
  */
56
56
  private resolveServices(): void {
57
57
  if (this.servicesResolved || !this.moduleRef) {
@@ -65,10 +65,9 @@ export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
65
65
  }
66
66
 
67
67
  try {
68
- // Get the Mongoose connection to query users directly
69
- this.mongoConnection = this.moduleRef.get(getConnectionToken(), { strict: false });
68
+ this.tokenService = this.moduleRef.get(BetterAuthTokenService, { strict: false });
70
69
  } catch {
71
- // MongoDB connection not available
70
+ // BetterAuthTokenService not available
72
71
  }
73
72
 
74
73
  this.servicesResolved = true;
@@ -78,291 +77,172 @@ export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
78
77
  * Override canActivate to add BetterAuth JWT fallback
79
78
  *
80
79
  * Flow:
81
- * 1. Try Passport JWT authentication (Legacy JWT)
82
- * 2. If that fails, try BetterAuth JWT verification
83
- * 3. If BetterAuth succeeds, load the user and proceed
80
+ * 1. Check if roles are required - if not, skip authentication entirely
81
+ * 2. Check if user is already authenticated via BetterAuth middleware
82
+ * 3. If BetterAuth is enabled, try BetterAuth token verification first
83
+ * 4. Otherwise, try Passport JWT authentication (Legacy JWT)
84
+ * 5. If Passport fails and BetterAuth is enabled, try BetterAuth as fallback
85
+ *
86
+ * This order ensures IAM-only setups work without requiring JWT strategy registration.
84
87
  */
85
88
  override async canActivate(context: ExecutionContext): Promise<boolean> {
86
- // Resolve services lazily
87
- this.resolveServices();
89
+ // Get roles FIRST to check if authentication is even needed
90
+ const reflectorRoles = this.reflector.getAll<string[][]>('roles', [context.getHandler(), context.getClass()]);
91
+ const roles: string[] = reflectorRoles[0]
92
+ ? reflectorRoles[1]
93
+ ? [...reflectorRoles[0], ...reflectorRoles[1]]
94
+ : reflectorRoles[0]
95
+ : reflectorRoles[1];
88
96
 
89
- // First, try the parent canActivate (Passport JWT)
90
- try {
91
- const result = super.canActivate(context);
92
- return isObservable(result) ? await firstValueFrom(result) : await result;
93
- } catch (passportError) {
94
- // Passport JWT validation failed - try BetterAuth token fallback (JWT or session)
95
- if (!this.betterAuthService?.isEnabled()) {
96
- // BetterAuth not available - rethrow original error
97
- throw passportError;
98
- }
97
+ // Check if locked - always deny
98
+ if (roles && roles.includes(RoleEnum.S_NO_ONE)) {
99
+ throw new UnauthorizedException(ErrorCode.UNAUTHORIZED);
100
+ }
99
101
 
100
- // Try to verify the token via BetterAuth (JWT or session token)
101
- const user = await this.verifyBetterAuthTokenFromContext(context);
102
- if (!user) {
103
- // BetterAuth verification also failed - rethrow original Passport error
104
- throw passportError;
105
- }
102
+ // If no roles required, or S_EVERYONE is set, allow access without authentication
103
+ // This allows public endpoints (without @Roles decorator or with S_EVERYONE) to work
104
+ if (!roles || !roles.some((value) => !!value) || roles.includes(RoleEnum.S_EVERYONE)) {
105
+ return true;
106
+ }
106
107
 
107
- // BetterAuth token is valid - set the user on the request
108
- const request = this.getRequest(context);
109
- if (request) {
110
- request.user = user;
111
- }
108
+ // Resolve services lazily (only needed if authentication is required)
109
+ this.resolveServices();
112
110
 
113
- // Now call handleRequest with the BetterAuth-authenticated user to check roles
114
- this.handleRequest(null, user, null, context);
111
+ // Get request and check for existing user (from BetterAuth middleware)
112
+ const request = this.getRequest(context);
113
+ const existingUser = request?.user;
115
114
 
115
+ // If user is already authenticated via BetterAuth middleware, validate roles directly
116
+ if (existingUser && existingUser._authenticatedViaBetterAuth === true) {
117
+ this.handleRequest(null, existingUser, null, context);
116
118
  return true;
117
119
  }
118
- }
119
-
120
- /**
121
- * Verify BetterAuth token (JWT or session) and load the corresponding user
122
- *
123
- * This method tries multiple verification strategies:
124
- * 1. BetterAuth JWT verification (if JWT plugin is enabled)
125
- * 2. BetterAuth session token lookup (database lookup)
126
- *
127
- * @param context - ExecutionContext to extract request from
128
- * @returns User object if verification succeeds, null otherwise
129
- */
130
- private async verifyBetterAuthTokenFromContext(context: ExecutionContext): Promise<any> {
131
- if (!this.betterAuthService || !this.mongoConnection) {
132
- return null;
133
- }
134
120
 
135
- try {
136
- // Get the raw HTTP request from multiple possible sources
137
- let authHeader: string | undefined;
138
-
139
- // Try GraphQL context first
140
- try {
141
- const gqlContext = GqlExecutionContext.create(context);
142
- const ctx = gqlContext.getContext();
143
- if (ctx?.req?.headers) {
144
- authHeader = ctx.req.headers.authorization || ctx.req.headers.Authorization;
145
- }
146
- } catch {
147
- // GraphQL context not available
148
- }
149
-
150
- // Fallback to HTTP context
151
- if (!authHeader) {
152
- try {
153
- const httpRequest = context.switchToHttp().getRequest();
154
- if (httpRequest?.headers) {
155
- authHeader = httpRequest.headers.authorization || httpRequest.headers.Authorization;
156
- }
157
- } catch {
158
- // HTTP context not available
121
+ // If BetterAuth is enabled, try BetterAuth verification FIRST
122
+ // This allows IAM-only setups to work without JWT strategy
123
+ if (this.betterAuthService?.isEnabled()) {
124
+ const user = await this.verifyBetterAuthTokenFromContext(context);
125
+ if (user) {
126
+ // BetterAuth token is valid - set the user on the request
127
+ if (request) {
128
+ request.user = user;
159
129
  }
130
+ // Validate roles
131
+ this.handleRequest(null, user, null, context);
132
+ return true;
160
133
  }
134
+ }
161
135
 
162
- let token: string | undefined;
163
-
164
- if (authHeader?.startsWith('Bearer ')) {
165
- token = authHeader.substring(7);
166
- } else if (authHeader?.startsWith('bearer ')) {
167
- // Handle lowercase 'bearer' as well
168
- token = authHeader.substring(7);
169
- }
170
-
171
- // If no token in header, try cookies (for REST endpoints)
172
- if (!token) {
173
- let cookies: Record<string, string> | undefined;
174
-
175
- // Try GraphQL context first
176
- try {
177
- const gqlContext = GqlExecutionContext.create(context);
178
- const ctx = gqlContext.getContext();
179
- if (ctx?.req?.cookies) {
180
- cookies = ctx.req.cookies;
181
- }
182
- } catch {
183
- // GraphQL context not available
184
- }
185
-
186
- // Fallback to HTTP context
187
- if (!cookies) {
188
- try {
189
- const httpRequest = context.switchToHttp().getRequest();
190
- if (httpRequest?.cookies) {
191
- cookies = httpRequest.cookies;
192
- }
193
- } catch {
194
- // HTTP context not available
195
- }
196
- }
197
-
198
- // Extract session token from cookies (try multiple cookie names)
199
- if (cookies) {
200
- // Get the basePath for cookie name (e.g., 'iam' -> 'iam.session_token')
201
- const basePath = this.betterAuthService.getBasePath?.()?.replace(/^\//, '').replace(/\//g, '.') || 'iam';
202
- const basePathCookie = `${basePath}.session_token`;
203
-
204
- token =
205
- cookies[basePathCookie] ||
206
- cookies['better-auth.session_token'] ||
207
- cookies['token'] ||
208
- undefined;
136
+ // Try Passport JWT authentication (Legacy JWT)
137
+ try {
138
+ const result = super.canActivate(context);
139
+ return isObservable(result) ? await firstValueFrom(result) : await result;
140
+ } catch (passportError) {
141
+ // Check if this is an "Unknown authentication strategy" error
142
+ // This happens in IAM-only setups where JWT strategy is not registered
143
+ const errorMessage = passportError instanceof Error ? passportError.message : String(passportError);
144
+ const isStrategyError = errorMessage.includes('Unknown authentication strategy');
145
+
146
+ // If BetterAuth is enabled but verification failed earlier, or if this is a strategy error
147
+ if (this.betterAuthService?.isEnabled()) {
148
+ // For strategy errors, BetterAuth verification already failed above
149
+ // Rethrow with a more descriptive error
150
+ if (isStrategyError) {
151
+ throw new InvalidTokenException();
209
152
  }
210
- }
211
153
 
212
- if (!token) {
213
- return null;
214
- }
215
-
216
- // Strategy 1: Try JWT verification (if JWT plugin is enabled)
217
- if (this.betterAuthService.isJwtEnabled()) {
218
- try {
219
- const payload = await this.betterAuthService.verifyJwtToken(token);
220
- if (payload?.sub) {
221
- const user = await this.loadUserFromPayload(payload);
222
- if (user) {
223
- return user;
224
- }
154
+ // For other errors (e.g., invalid JWT), try BetterAuth as fallback one more time
155
+ const user = await this.verifyBetterAuthTokenFromContext(context);
156
+ if (user) {
157
+ if (request) {
158
+ request.user = user;
225
159
  }
226
- } catch {
227
- // JWT verification failed - try session token next
228
- }
229
- }
230
-
231
- // Strategy 2: Try session token lookup (database lookup)
232
- try {
233
- const sessionResult = await this.betterAuthService.getSessionByToken(token);
234
- if (sessionResult?.user) {
235
- return this.loadUserFromSessionResult(sessionResult.user);
160
+ this.handleRequest(null, user, null, context);
161
+ return true;
236
162
  }
237
- } catch {
238
- // Session lookup failed
239
163
  }
240
164
 
241
- return null;
242
- } catch (error) {
243
- this.logger.debug(
244
- `BetterAuth token fallback failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
245
- );
246
- return null;
165
+ // BetterAuth verification also failed - rethrow original error
166
+ throw passportError;
247
167
  }
248
168
  }
249
169
 
250
170
  /**
251
- * Load user from JWT payload using direct MongoDB query
171
+ * Verify BetterAuth token (JWT or session) and load the corresponding user.
172
+ *
173
+ * Delegates to BetterAuthTokenService for token extraction and verification.
174
+ * Handles both GraphQL and HTTP contexts.
252
175
  *
253
- * @param payload - JWT payload with sub (user ID or iamId)
254
- * @returns User object with hasRole method
176
+ * @param context - ExecutionContext to extract request from
177
+ * @returns User object if verification succeeds, null otherwise
255
178
  */
256
- private async loadUserFromPayload(payload: { [key: string]: any; sub: string }): Promise<any> {
257
- if (!this.mongoConnection) {
179
+ private async verifyBetterAuthTokenFromContext(context: ExecutionContext): Promise<BetterAuthenticatedUser | null> {
180
+ if (!this.tokenService) {
258
181
  return null;
259
182
  }
260
183
 
261
184
  try {
262
- const usersCollection = this.mongoConnection.collection('users');
263
- let user: any = null;
264
-
265
- // Try to find by MongoDB _id first
266
- if (Types.ObjectId.isValid(payload.sub)) {
267
- user = await usersCollection.findOne({ _id: new Types.ObjectId(payload.sub) });
268
- }
269
-
270
- // If not found, try by iamId
271
- if (!user) {
272
- user = await usersCollection.findOne({ iamId: payload.sub });
185
+ // Extract request from context (supports both GraphQL and HTTP)
186
+ const request = this.extractRequestFromContext(context);
187
+ if (!request) {
188
+ return null;
273
189
  }
274
190
 
275
- if (!user) {
191
+ // Extract token from request
192
+ const { token } = this.tokenService.extractTokenFromRequest(request);
193
+ if (!token) {
276
194
  return null;
277
195
  }
278
196
 
279
- // Convert MongoDB document to user-like object with hasRole method
280
- const userObject = {
281
- ...user,
282
- _authenticatedViaBetterAuth: true,
283
- // Add hasRole method for role checking
284
- hasRole: (roles: string[]): boolean => {
285
- if (!user.roles || !Array.isArray(user.roles)) {
286
- return false;
287
- }
288
- return roles.some((role) => user.roles.includes(role));
289
- },
290
- id: user._id?.toString(),
291
- };
292
-
293
- return userObject;
197
+ // Verify token and load user
198
+ return await this.tokenService.verifyAndLoadUser(token);
294
199
  } catch (error) {
295
200
  this.logger.debug(
296
- `Failed to load user from payload: ${error instanceof Error ? error.message : 'Unknown error'}`,
201
+ `BetterAuth token verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
297
202
  );
298
203
  return null;
299
204
  }
300
205
  }
301
206
 
302
207
  /**
303
- * Load user from session result (from getSessionByToken)
208
+ * Extracts the request object from ExecutionContext.
209
+ * Handles both GraphQL and HTTP contexts.
304
210
  *
305
- * @param sessionUser - User object from session lookup
306
- * @returns User object with hasRole method
211
+ * @param context - ExecutionContext
212
+ * @returns Request object with headers and cookies
307
213
  */
308
- private async loadUserFromSessionResult(sessionUser: any): Promise<any> {
309
- if (!this.mongoConnection || !sessionUser) {
310
- return null;
311
- }
312
-
214
+ private extractRequestFromContext(context: ExecutionContext): null | {
215
+ cookies?: Record<string, string>;
216
+ headers?: Record<string, string | string[] | undefined>;
217
+ } {
218
+ // Try GraphQL context first
313
219
  try {
314
- const usersCollection = this.mongoConnection.collection('users');
315
-
316
- // The sessionUser might have id (BetterAuth ID) or email
317
- // We need to find the corresponding user in our users collection
318
- let user: any = null;
319
-
320
- // Try to find by email (most reliable)
321
- if (sessionUser.email) {
322
- user = await usersCollection.findOne({ email: sessionUser.email });
323
- }
324
-
325
- // If not found by email, try by iamId
326
- if (!user && sessionUser.id) {
327
- user = await usersCollection.findOne({ iamId: sessionUser.id });
328
- }
329
-
330
- // If still not found, try by _id (if the ID looks like a MongoDB ObjectId)
331
- if (!user && sessionUser.id && Types.ObjectId.isValid(sessionUser.id)) {
332
- user = await usersCollection.findOne({ _id: new Types.ObjectId(sessionUser.id) });
220
+ const gqlContext = GqlExecutionContext.create(context);
221
+ const ctx = gqlContext.getContext();
222
+ if (ctx?.req) {
223
+ return ctx.req;
333
224
  }
225
+ } catch {
226
+ // GraphQL context not available
227
+ }
334
228
 
335
- if (!user) {
336
- return null;
229
+ // Fallback to HTTP context
230
+ try {
231
+ const httpRequest = context.switchToHttp().getRequest();
232
+ if (httpRequest) {
233
+ return httpRequest;
337
234
  }
338
-
339
- // Convert MongoDB document to user-like object with hasRole method
340
- const userObject = {
341
- ...user,
342
- _authenticatedViaBetterAuth: true,
343
- // Add hasRole method for role checking
344
- hasRole: (roles: string[]): boolean => {
345
- if (!user.roles || !Array.isArray(user.roles)) {
346
- return false;
347
- }
348
- return roles.some((role) => user.roles.includes(role));
349
- },
350
- id: user._id?.toString(),
351
- };
352
-
353
- return userObject;
354
- } catch (error) {
355
- this.logger.debug(
356
- `Failed to load user from session: ${error instanceof Error ? error.message : 'Unknown error'}`,
357
- );
358
- return null;
235
+ } catch {
236
+ // HTTP context not available
359
237
  }
238
+
239
+ return null;
360
240
  }
361
241
 
362
242
  /**
363
243
  * Handle request
364
244
  */
365
- override handleRequest(err, user, info, context) {
245
+ override handleRequest(err: Error | null, user: any, info: any, context: ExecutionContext) {
366
246
  // Get roles
367
247
  const reflectorRoles = this.reflector.getAll<string[][]>('roles', [context.getHandler(), context.getClass()]);
368
248
  const roles: string[] = reflectorRoles[0]
@@ -129,38 +129,47 @@ export class ServerModule {}
129
129
  **Modify:** `src/config.env.ts`
130
130
  **Reference:** `node_modules/@lenne.tech/nest-server/src/config.env.ts`
131
131
 
132
- #### For New Projects (IAM-Only):
132
+ #### Zero-Config (Default):
133
+ BetterAuth is **enabled by default** with JWT + 2FA. No configuration required!
134
+
133
135
  ```typescript
134
136
  const config = {
135
- // Disable Legacy Auth endpoints
137
+ // BetterAuth is automatically enabled with:
138
+ // - JWT tokens (for API clients)
139
+ // - 2FA/TOTP (users can enable it in settings)
140
+ // No betterAuth config needed!
141
+
142
+ // For new projects, disable Legacy Auth:
136
143
  auth: {
137
144
  legacyEndpoints: {
138
145
  enabled: false,
139
146
  },
140
147
  },
141
- // BetterAuth configuration (minimal - JWT enabled by default)
142
- betterAuth: true, // or betterAuth: {} for same effect
148
+ };
149
+ ```
143
150
 
144
- // OR with optional features:
145
- betterAuth: {
146
- twoFactor: {}, // Enable 2FA (opt-in)
147
- passkey: {}, // Enable Passkeys (opt-in)
148
- // JWT is already enabled by default
149
- },
151
+ #### With Passkey (auto-detected from baseUrl):
152
+ ```typescript
153
+ const config = {
154
+ // Passkey is auto-activated when URLs can be resolved
155
+ // Option 1: Set root-level baseUrl (production)
156
+ baseUrl: 'https://api.example.com', // rpId, origin, trustedOrigins auto-detected
157
+ env: 'production',
158
+
159
+ // Option 2: Use env: 'local'/'ci'/'e2e' (development)
160
+ // env: 'local', // Uses localhost defaults: API=:3000, App=:3001
150
161
  };
151
162
  ```
152
163
 
153
- #### For Existing Projects (Migration):
164
+ #### Disabling Features:
154
165
  ```typescript
155
166
  const config = {
156
- // Keep Legacy Auth endpoints enabled during migration
157
- auth: {
158
- legacyEndpoints: {
159
- enabled: true, // Default - can disable after migration
160
- },
167
+ betterAuth: {
168
+ jwt: false, // Disable JWT (use cookies only)
169
+ twoFactor: false, // Disable 2FA
161
170
  },
162
- // BetterAuth configuration (JWT enabled by default)
163
- betterAuth: true, // Minimal config, or use object for more options
171
+ // OR disable BetterAuth completely:
172
+ betterAuth: false,
164
173
  };
165
174
  ```
166
175
 
@@ -345,26 +354,31 @@ async function useBackupCode(code: string) {
345
354
 
346
355
  ### Passkey Login Flow (Client-Side)
347
356
 
348
- Handle passkey authentication with session validation fallback:
357
+ Handle passkey authentication with session validation fallback.
358
+
359
+ **IMPORTANT:** For JWT mode (`cookies: false`), you MUST use the `authenticateWithPasskey()` function from the composable instead of `authClient.signIn.passkey()` directly. This is because JWT mode requires sending a `challengeId` to the server for challenge verification.
349
360
 
350
361
  ```typescript
351
- // login.vue - Passkey login
362
+ // login.vue - Passkey login (JWT-compatible)
363
+ // Use authenticateWithPasskey from useBetterAuth composable
364
+ const { authenticateWithPasskey, setUser, validateSession } = useBetterAuth();
365
+
352
366
  async function onPasskeyLogin() {
353
367
  try {
354
- // Use official Better Auth passkey sign-in
355
- const result = await authClient.signIn.passkey();
368
+ // Use composable method which handles challengeId for JWT mode
369
+ const result = await authenticateWithPasskey();
356
370
 
357
- if (result.error) {
358
- showError(result.error.message || 'Passkey authentication failed');
371
+ // Check for error (returns { success, error?, user?, session? })
372
+ if (!result.success) {
373
+ showError(result.error || 'Passkey authentication failed');
359
374
  return;
360
375
  }
361
376
 
362
377
  // Update auth state with user data if available
363
- if (result.data?.user) {
364
- setUser(result.data.user);
365
- } else if (result.data?.session) {
366
- // IMPORTANT: Passkey auth returns session without user
367
- // Fetch user data via session validation
378
+ if (result.user) {
379
+ setUser(result.user);
380
+ } else {
381
+ // Passkey auth may return success without user - fetch via session validation
368
382
  await validateSession();
369
383
  }
370
384
 
@@ -378,33 +392,36 @@ async function onPasskeyLogin() {
378
392
  showError(err instanceof Error ? err.message : 'Passkey login failed');
379
393
  }
380
394
  }
381
-
382
- // Helper: Validate session and fetch user data
383
- async function validateSession() {
384
- const sessionResult = await authClient.$fetch('/session');
385
- if (sessionResult.user) {
386
- setUser(sessionResult.user);
387
- return true;
388
- }
389
- return false;
390
- }
391
395
  ```
392
396
 
397
+ **Why not use `authClient.signIn.passkey()` directly?**
398
+
399
+ In JWT mode (`cookies: false`), the server stores WebAuthn challenges in the database instead of cookies. The server returns a `challengeId` in the generate-options response, which must be sent back in the verify request. The `authenticateWithPasskey()` composable function handles this automatically.
400
+
401
+ | Mode | Challenge Storage | Client Approach |
402
+ |------|------------------|-----------------|
403
+ | Cookie (`cookies: true` or not set) | Cookie (`better-auth.better-auth-passkey`) | Either approach works |
404
+ | JWT (`cookies: false`) | Database with `challengeId` mapping (auto-enabled) | **Must use composable** |
405
+
406
+ **Note:** Database challenge storage is automatically enabled when `options.cookies: false` is set. No additional configuration is required.
407
+
393
408
  ### Passkey Registration (Client-Side)
394
409
 
395
- Register a new passkey for an authenticated user:
410
+ Register a new passkey for an authenticated user.
411
+
412
+ **IMPORTANT:** For JWT mode (`cookies: false`), you MUST use the `registerPasskey()` function from the composable. This ensures the `challengeId` is correctly sent to the server.
396
413
 
397
414
  ```typescript
398
- // settings.vue - Register new passkey
399
- async function registerPasskey() {
415
+ // settings.vue - Register new passkey (JWT-compatible)
416
+ const { registerPasskey, listPasskeys, deletePasskey } = useBetterAuth();
417
+
418
+ async function onRegisterPasskey() {
400
419
  try {
401
- // This calls generate-register-options verify-registration
402
- const result = await authClient.passkey.addPasskey({
403
- name: 'My Device', // Optional: passkey name
404
- });
420
+ // Use composable method which handles challengeId for JWT mode
421
+ const result = await registerPasskey('My Device'); // Optional name
405
422
 
406
- if (result.error) {
407
- showError(result.error.message);
423
+ if (!result.success) {
424
+ showError(result.error || 'Passkey registration failed');
408
425
  return;
409
426
  }
410
427
 
@@ -420,23 +437,32 @@ async function registerPasskey() {
420
437
  }
421
438
  }
422
439
 
423
- // List user's passkeys
440
+ // List user's passkeys (works in both modes)
424
441
  async function loadPasskeys() {
425
- const result = await authClient.passkey.listUserPasskeys();
426
- if (!result.error) {
427
- passkeys.value = result.data || [];
442
+ const result = await listPasskeys();
443
+ if (result.success) {
444
+ passkeys.value = result.passkeys || [];
428
445
  }
429
446
  }
430
447
 
431
- // Delete a passkey
432
- async function deletePasskey(passkeyId: string) {
433
- const result = await authClient.passkey.deletePasskey({ id: passkeyId });
434
- if (!result.error) {
448
+ // Delete a passkey (works in both modes)
449
+ async function onDeletePasskey(passkeyId: string) {
450
+ const result = await deletePasskey(passkeyId);
451
+ if (result.success) {
435
452
  await loadPasskeys();
436
453
  }
437
454
  }
438
455
  ```
439
456
 
457
+ **Alternative for Cookie mode only:**
458
+
459
+ If you're only using Cookie mode (`cookies: true`), you can use `authClient.passkey` directly:
460
+
461
+ ```typescript
462
+ // Cookie mode ONLY - does NOT work with JWT mode
463
+ const result = await authClient.passkey.addPasskey({ name: 'My Device' });
464
+ ```
465
+
440
466
  ---
441
467
 
442
468
  ## Better-Auth Hooks: Limitations & Warnings