@lenne.tech/nest-server 11.11.1 → 11.13.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 (113) hide show
  1. package/dist/config.env.js +1 -0
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/interfaces/server-options.interface.d.ts +16 -0
  4. package/dist/core/modules/auth/core-auth.controller.js +1 -1
  5. package/dist/core/modules/auth/core-auth.controller.js.map +1 -1
  6. package/dist/core/modules/auth/core-auth.resolver.js +1 -1
  7. package/dist/core/modules/auth/core-auth.resolver.js.map +1 -1
  8. package/dist/core/modules/better-auth/better-auth-token.service.js +1 -4
  9. package/dist/core/modules/better-auth/better-auth-token.service.js.map +1 -1
  10. package/dist/core/modules/better-auth/better-auth.config.d.ts +13 -0
  11. package/dist/core/modules/better-auth/better-auth.config.js +114 -17
  12. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  13. package/dist/core/modules/better-auth/better-auth.resolver.d.ts +7 -3
  14. package/dist/core/modules/better-auth/better-auth.resolver.js +16 -6
  15. package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -1
  16. package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +4 -2
  17. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +63 -18
  18. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
  19. package/dist/core/modules/better-auth/core-better-auth-auth.model.d.ts +1 -0
  20. package/dist/core/modules/better-auth/core-better-auth-auth.model.js +7 -0
  21. package/dist/core/modules/better-auth/core-better-auth-auth.model.js.map +1 -1
  22. package/dist/core/modules/better-auth/core-better-auth-cookie.helper.d.ts +41 -0
  23. package/dist/core/modules/better-auth/core-better-auth-cookie.helper.js +107 -0
  24. package/dist/core/modules/better-auth/core-better-auth-cookie.helper.js.map +1 -0
  25. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.d.ts +48 -0
  26. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +241 -0
  27. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -0
  28. package/dist/core/modules/better-auth/core-better-auth-models.d.ts +2 -1
  29. package/dist/core/modules/better-auth/core-better-auth-models.js +8 -4
  30. package/dist/core/modules/better-auth/core-better-auth-models.js.map +1 -1
  31. package/dist/core/modules/better-auth/core-better-auth-signup-validator.service.d.ts +18 -0
  32. package/dist/core/modules/better-auth/core-better-auth-signup-validator.service.js +82 -0
  33. package/dist/core/modules/better-auth/core-better-auth-signup-validator.service.js.map +1 -0
  34. package/dist/core/modules/better-auth/core-better-auth-token.helper.d.ts +16 -0
  35. package/dist/core/modules/better-auth/core-better-auth-token.helper.js +66 -0
  36. package/dist/core/modules/better-auth/core-better-auth-token.helper.js.map +1 -0
  37. package/dist/core/modules/better-auth/core-better-auth-user.mapper.d.ts +0 -1
  38. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +15 -8
  39. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
  40. package/dist/core/modules/better-auth/core-better-auth-web.helper.d.ts +3 -3
  41. package/dist/core/modules/better-auth/core-better-auth-web.helper.js +64 -44
  42. package/dist/core/modules/better-auth/core-better-auth-web.helper.js.map +1 -1
  43. package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +13 -1
  44. package/dist/core/modules/better-auth/core-better-auth.controller.js +108 -49
  45. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  46. package/dist/core/modules/better-auth/core-better-auth.middleware.d.ts +0 -1
  47. package/dist/core/modules/better-auth/core-better-auth.middleware.js +57 -39
  48. package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -1
  49. package/dist/core/modules/better-auth/core-better-auth.module.d.ts +6 -0
  50. package/dist/core/modules/better-auth/core-better-auth.module.js +129 -24
  51. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  52. package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +12 -5
  53. package/dist/core/modules/better-auth/core-better-auth.resolver.js +64 -17
  54. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
  55. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +4 -1
  56. package/dist/core/modules/better-auth/core-better-auth.service.js +143 -23
  57. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  58. package/dist/core/modules/better-auth/index.d.ts +4 -0
  59. package/dist/core/modules/better-auth/index.js +4 -0
  60. package/dist/core/modules/better-auth/index.js.map +1 -1
  61. package/dist/core/modules/error-code/error-codes.d.ts +45 -0
  62. package/dist/core/modules/error-code/error-codes.js +40 -0
  63. package/dist/core/modules/error-code/error-codes.js.map +1 -1
  64. package/dist/core/modules/user/core-user.model.d.ts +1 -0
  65. package/dist/core/modules/user/core-user.model.js +11 -0
  66. package/dist/core/modules/user/core-user.model.js.map +1 -1
  67. package/dist/server/modules/better-auth/better-auth.controller.d.ts +3 -1
  68. package/dist/server/modules/better-auth/better-auth.controller.js +12 -3
  69. package/dist/server/modules/better-auth/better-auth.controller.js.map +1 -1
  70. package/dist/server/modules/better-auth/better-auth.resolver.d.ts +7 -3
  71. package/dist/server/modules/better-auth/better-auth.resolver.js +16 -6
  72. package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -1
  73. package/dist/server/modules/error-code/error-codes.d.ts +5 -0
  74. package/dist/server/modules/user/user.model.d.ts +5 -0
  75. package/dist/templates/email-verification-de.ejs +78 -0
  76. package/dist/templates/email-verification-en.ejs +78 -0
  77. package/dist/test/test.helper.d.ts +4 -0
  78. package/dist/test/test.helper.js +54 -1
  79. package/dist/test/test.helper.js.map +1 -1
  80. package/dist/tsconfig.build.tsbuildinfo +1 -1
  81. package/package.json +10 -10
  82. package/src/config.env.ts +2 -0
  83. package/src/core/common/interfaces/server-options.interface.ts +240 -0
  84. package/src/core/modules/auth/core-auth.controller.ts +2 -2
  85. package/src/core/modules/auth/core-auth.resolver.ts +2 -2
  86. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +113 -0
  87. package/src/core/modules/better-auth/README.md +72 -7
  88. package/src/core/modules/better-auth/better-auth-token.service.ts +5 -8
  89. package/src/core/modules/better-auth/better-auth.config.ts +282 -29
  90. package/src/core/modules/better-auth/better-auth.resolver.ts +16 -5
  91. package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +100 -22
  92. package/src/core/modules/better-auth/core-better-auth-auth.model.ts +10 -0
  93. package/src/core/modules/better-auth/core-better-auth-cookie.helper.ts +323 -0
  94. package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +433 -0
  95. package/src/core/modules/better-auth/core-better-auth-models.ts +6 -3
  96. package/src/core/modules/better-auth/core-better-auth-signup-validator.service.ts +178 -0
  97. package/src/core/modules/better-auth/core-better-auth-token.helper.ts +200 -0
  98. package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +18 -14
  99. package/src/core/modules/better-auth/core-better-auth-web.helper.ts +119 -69
  100. package/src/core/modules/better-auth/core-better-auth.controller.ts +197 -84
  101. package/src/core/modules/better-auth/core-better-auth.middleware.ts +93 -64
  102. package/src/core/modules/better-auth/core-better-auth.module.ts +215 -38
  103. package/src/core/modules/better-auth/core-better-auth.resolver.ts +140 -20
  104. package/src/core/modules/better-auth/core-better-auth.service.ts +210 -32
  105. package/src/core/modules/better-auth/index.ts +4 -0
  106. package/src/core/modules/error-code/error-codes.ts +45 -0
  107. package/src/core/modules/user/core-user.model.ts +15 -0
  108. package/src/server/modules/better-auth/better-auth.controller.ts +6 -2
  109. package/src/server/modules/better-auth/better-auth.resolver.ts +16 -5
  110. package/src/templates/email-verification-de.ejs +78 -0
  111. package/src/templates/email-verification-en.ejs +78 -0
  112. package/src/test/README.md +190 -0
  113. package/src/test/test.helper.ts +82 -1
@@ -4,6 +4,8 @@ import { betterAuth, BetterAuthPlugin } from 'better-auth';
4
4
  import { mongodbAdapter } from 'better-auth/adapters/mongodb';
5
5
  import { jwt, twoFactor } from 'better-auth/plugins';
6
6
  import * as crypto from 'crypto';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
7
9
 
8
10
  import { IBetterAuth } from '../../common/interfaces/server-options.interface';
9
11
 
@@ -27,13 +29,16 @@ function generateSecureSecret(): string {
27
29
 
28
30
  /**
29
31
  * Cached auto-generated secret for the current server instance.
30
- * Generated once at module load to ensure consistency within a single run.
32
+ * Generated once at a module load to ensure consistency within a single run.
31
33
  */
32
34
  let cachedAutoGeneratedSecret: null | string = null;
33
35
 
34
36
  /**
35
- * Options for creating a better-auth instance
37
+ * Cached project app name for the current server instance.
38
+ * Read once from package.json to avoid repeated file reads.
36
39
  */
40
+ let cachedProjectAppName: null | string = null;
41
+
37
42
  export interface CreateBetterAuthOptions {
38
43
  /**
39
44
  * Better-auth configuration from server options
@@ -63,6 +68,18 @@ export interface CreateBetterAuthOptions {
63
68
  */
64
69
  fallbackSecrets?: (string | undefined)[];
65
70
 
71
+ /**
72
+ * Callback for when email is verified (to sync verifiedAt)
73
+ * Injected from CoreBetterAuthModule
74
+ */
75
+ onEmailVerified?: OnEmailVerifiedCallback;
76
+
77
+ /**
78
+ * Callback for sending verification email
79
+ * Injected from CoreBetterAuthModule to use NestJS services
80
+ */
81
+ sendVerificationEmail?: SendVerificationEmailCallback;
82
+
66
83
  /**
67
84
  * Server-level app/frontend URL (from IServerOptions.appUrl).
68
85
  * Used for Passkey origin and CORS trustedOrigins.
@@ -82,6 +99,25 @@ export interface CreateBetterAuthOptions {
82
99
  serverEnv?: string;
83
100
  }
84
101
 
102
+ /**
103
+ * Callback for when email is verified
104
+ * Injected from CoreBetterAuthModule to sync verifiedAt
105
+ */
106
+ export type OnEmailVerifiedCallback = (userId: string) => Promise<void>;
107
+
108
+ /**
109
+ * Options for creating a better-auth instance
110
+ */
111
+ /**
112
+ * Callback for sending verification email
113
+ * Injected from CoreBetterAuthModule to use NestJS services
114
+ */
115
+ export type SendVerificationEmailCallback = (options: {
116
+ token: string;
117
+ url: string;
118
+ user: { email: string; id: string; name?: null | string };
119
+ }) => Promise<void>;
120
+
85
121
  /**
86
122
  * Better-Auth field type definition
87
123
  * Matches the DBFieldType from better-auth
@@ -152,6 +188,7 @@ interface UserFieldConfig {
152
188
  */
153
189
  interface ValidationResult {
154
190
  errors: string[];
191
+ resolvedSecret?: string;
155
192
  valid: boolean;
156
193
  warnings: string[];
157
194
  }
@@ -165,7 +202,7 @@ interface ValidationResult {
165
202
  */
166
203
  export function createBetterAuthInstance(options: CreateBetterAuthOptions): BetterAuthInstance | null {
167
204
  const logger = new Logger('BetterAuthConfig');
168
- const { config, db, fallbackSecrets } = options;
205
+ const { config, db, fallbackSecrets, onEmailVerified, sendVerificationEmail, serverEnv } = options;
169
206
 
170
207
  // Return null only if better-auth is explicitly disabled
171
208
  // BetterAuth is enabled by default (zero-config)
@@ -184,7 +221,7 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
184
221
  // Normalize Passkey configuration with auto-detection from resolved URLs
185
222
  // This must happen BEFORE validation to allow graceful degradation
186
223
  // Passkey is now AUTO-ACTIVATED by default (not opt-in)
187
- const passkeyNormalization = normalizePasskeyConfig(config, resolvedUrls);
224
+ const passkeyNormalization = normalizePasskeyConfig(config, { resolvedUrls, serverEnv });
188
225
 
189
226
  // Log passkey normalization warnings (info about auto-detection or disabled status)
190
227
  for (const warning of passkeyNormalization.warnings) {
@@ -192,7 +229,7 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
192
229
  }
193
230
 
194
231
  // Validate configuration (with fallback secrets for backwards compatibility)
195
- const validation = validateConfig(config, fallbackSecrets, passkeyNormalization);
232
+ const validation = validateConfig(config, { fallbackSecrets, passkeyNormalization });
196
233
 
197
234
  // Log warnings
198
235
  for (const warning of validation.warnings) {
@@ -205,15 +242,26 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
205
242
  }
206
243
 
207
244
  // Build configuration components (pass normalized passkey config and resolved URLs)
208
- const plugins = buildPlugins(config, passkeyNormalization);
245
+ const plugins = buildPlugins(config, { passkeyNormalization, serverEnv });
209
246
  const socialProviders = buildSocialProviders(config);
210
- const trustedOrigins = buildTrustedOrigins(config, passkeyNormalization, resolvedUrls);
247
+ const trustedOrigins = buildTrustedOrigins(config, { passkeyNormalization, resolvedUrls });
211
248
  const additionalFields = buildUserFields(config);
212
249
 
250
+ // Build email verification configuration
251
+ const emailVerificationConfig = buildEmailVerificationConfig(config, { onEmailVerified, sendVerificationEmail });
252
+
213
253
  // Build the base Better-Auth configuration
214
254
  // Use resolved baseUrl (with local defaults) or fallback
255
+ const basePath = config.basePath || '/iam';
256
+ // Cookie prefix derived from basePath (e.g., '/iam' → 'iam')
257
+ // This ensures Better-Auth looks for cookies like 'iam.session_token' instead of 'better-auth.session_token'
258
+ const cookiePrefix = basePath.replace(/^\//, '').replace(/\//g, '.');
259
+
215
260
  const betterAuthConfig: Record<string, unknown> = {
216
- basePath: config.basePath || '/iam',
261
+ advanced: {
262
+ cookiePrefix,
263
+ },
264
+ basePath,
217
265
  baseURL: resolvedUrls.baseUrl || config.baseUrl || 'http://localhost:3000',
218
266
  database: mongodbAdapter(db),
219
267
  // Enable email/password authentication by default (required by Better-Auth 1.x)
@@ -222,7 +270,7 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
222
270
  enabled: config.emailAndPassword?.enabled !== false,
223
271
  },
224
272
  plugins,
225
- secret: config.secret,
273
+ secret: validation.resolvedSecret || config.secret,
226
274
  socialProviders,
227
275
  user: {
228
276
  additionalFields,
@@ -230,6 +278,11 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
230
278
  },
231
279
  };
232
280
 
281
+ // Add email verification config if enabled
282
+ if (emailVerificationConfig) {
283
+ betterAuthConfig.emailVerification = emailVerificationConfig;
284
+ }
285
+
233
286
  // Only add trustedOrigins if explicitly configured
234
287
  // When undefined, Better-Auth uses its default CORS behavior (allows all origins)
235
288
  if (trustedOrigins) {
@@ -246,6 +299,99 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
246
299
  return betterAuth(finalConfig as any);
247
300
  }
248
301
 
302
+ /**
303
+ * Formats a package name to a human-readable display name.
304
+ * Converts kebab-case and snake_case to Title Case.
305
+ *
306
+ * @example
307
+ * formatProjectName('my-awesome-app') // → 'My Awesome App'
308
+ * formatProjectName('@org/my-app') // → 'My App'
309
+ * formatProjectName('nest_server_starter') // → 'Nest Server Starter'
310
+ */
311
+ export function formatProjectName(name: string): string {
312
+ // Remove scope (e.g., '@org/my-app' → 'my-app')
313
+ let formatted = name.replace(/^@[^/]+\//, '');
314
+
315
+ // Split by hyphens and underscores, capitalize each word
316
+ formatted = formatted
317
+ .split(/[-_]/)
318
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
319
+ .join(' ');
320
+
321
+ return formatted;
322
+ }
323
+
324
+ /**
325
+ * Builds the email verification configuration for Better-Auth.
326
+ *
327
+ * Email verification is enabled by default unless explicitly disabled.
328
+ * Uses the sendVerificationEmail callback to send emails via NestJS services.
329
+ *
330
+ * @param config - Better-auth configuration
331
+ * @param callbacks - Optional callbacks for email verification lifecycle
332
+ * @returns Email verification config for Better-Auth or null if disabled
333
+ */
334
+ function buildEmailVerificationConfig(
335
+ config: IBetterAuth,
336
+ callbacks?: {
337
+ onEmailVerified?: OnEmailVerifiedCallback;
338
+ sendVerificationEmail?: SendVerificationEmailCallback;
339
+ },
340
+ ): null | Record<string, unknown> {
341
+ const { onEmailVerified, sendVerificationEmail } = callbacks ?? {};
342
+ // Email verification is enabled by default (zero-config):
343
+ // - undefined/null: enabled with defaults
344
+ // - true: enabled with defaults
345
+ // - false: explicitly disabled
346
+ // - { ... }: enabled with custom settings (unless enabled: false)
347
+ const emailVerificationConfig = config.emailVerification;
348
+
349
+ if (emailVerificationConfig === false) {
350
+ return null;
351
+ }
352
+
353
+ if (typeof emailVerificationConfig === 'object' && emailVerificationConfig.enabled === false) {
354
+ return null;
355
+ }
356
+
357
+ // Get configuration values (with defaults)
358
+ const configObj = typeof emailVerificationConfig === 'object' ? emailVerificationConfig : {};
359
+ const expiresIn = configObj.expiresIn ?? 86400; // 24 hours
360
+ const autoSignInAfterVerification = configObj.autoSignInAfterVerification ?? true;
361
+
362
+ const result: Record<string, unknown> = {
363
+ autoSignInAfterVerification,
364
+ expiresIn,
365
+ // Also send on sign-in if user is not verified
366
+ sendOnSignIn: true,
367
+ // Send verification email on sign-up by default
368
+ sendOnSignUp: true,
369
+ };
370
+
371
+ // Add sendVerificationEmail callback if provided
372
+ if (sendVerificationEmail) {
373
+ result.sendVerificationEmail = async (
374
+ data: { token: string; url: string; user: { email: string; id: string; name?: null | string } },
375
+ _request?: Request,
376
+ ) => {
377
+ // Don't await to prevent timing attacks (as recommended by Better-Auth docs)
378
+
379
+ sendVerificationEmail(data);
380
+ };
381
+ }
382
+
383
+ // Add callback for when email is verified (to sync verifiedAt)
384
+ if (onEmailVerified) {
385
+ result.afterEmailVerification = async (user: string | { id: string }, _request?: Request) => {
386
+ // Better-Auth passes a user object { id: string } to afterEmailVerification
387
+ const userId = typeof user === 'string' ? user : user.id;
388
+ await onEmailVerified(userId);
389
+ };
390
+ }
391
+
392
+ return result;
393
+ }
394
+
249
395
  /**
250
396
  * Builds the plugins array based on configuration.
251
397
  * Merges built-in plugins (jwt, twoFactor, passkey) with custom plugins from config.
@@ -257,9 +403,15 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
257
403
  * - `undefined`: Disabled (default)
258
404
  *
259
405
  * @param config - Better-auth configuration
260
- * @param passkeyNormalization - Normalized passkey configuration from normalizePasskeyConfig()
406
+ * @param options - Additional options
407
+ * @param options.passkeyNormalization - Normalized passkey configuration from normalizePasskeyConfig()
408
+ * @param options.serverEnv - Server environment for auto-detected app name suffix
261
409
  */
262
- function buildPlugins(config: IBetterAuth, passkeyNormalization: PasskeyNormalizationResult): BetterAuthPlugin[] {
410
+ function buildPlugins(
411
+ config: IBetterAuth,
412
+ options: { passkeyNormalization: PasskeyNormalizationResult; serverEnv?: string },
413
+ ): BetterAuthPlugin[] {
414
+ const { passkeyNormalization, serverEnv } = options;
263
415
  const plugins: BetterAuthPlugin[] = [];
264
416
 
265
417
  // JWT Plugin for API client compatibility
@@ -285,7 +437,8 @@ function buildPlugins(config: IBetterAuth, passkeyNormalization: PasskeyNormaliz
285
437
  const twoFactorConfig = typeof config.twoFactor === 'object' ? config.twoFactor : {};
286
438
  plugins.push(
287
439
  twoFactor({
288
- issuer: twoFactorConfig.appName || 'Nest Server',
440
+ // issuer: Use explicit config, or auto-detect from package.json with environment suffix
441
+ issuer: twoFactorConfig.appName || getProjectAppName(serverEnv),
289
442
  }),
290
443
  );
291
444
  }
@@ -363,14 +516,16 @@ function buildSocialProviders(config: IBetterAuth): Record<string, SocialProvide
363
516
  * - Otherwise → return undefined (allows all origins via Better-Auth's default)
364
517
  *
365
518
  * @param config - Better-auth configuration
366
- * @param passkeyNormalization - Normalized passkey configuration from normalizePasskeyConfig()
367
- * @param resolvedUrls - Resolved URLs from resolveUrls()
519
+ * @param options - Passkey normalization and resolved URLs context
368
520
  */
369
521
  function buildTrustedOrigins(
370
522
  config: IBetterAuth,
371
- passkeyNormalization: PasskeyNormalizationResult,
372
- resolvedUrls: ResolvedUrls,
523
+ options: {
524
+ passkeyNormalization: PasskeyNormalizationResult;
525
+ resolvedUrls: ResolvedUrls;
526
+ },
373
527
  ): string[] | undefined {
528
+ const { passkeyNormalization, resolvedUrls } = options;
374
529
  // If trustedOrigins is explicitly configured, use it
375
530
  if (config.trustedOrigins?.length) {
376
531
  return config.trustedOrigins;
@@ -419,6 +574,12 @@ function buildUserFields(config: IBetterAuth): Record<string, UserFieldConfig> {
419
574
  fieldName: 'roles',
420
575
  type: 'string[]',
421
576
  },
577
+ // Track when terms and privacy policy were accepted (for sign-up checks)
578
+ termsAndPrivacyAcceptedAt: {
579
+ defaultValue: null,
580
+ fieldName: 'termsAndPrivacyAcceptedAt',
581
+ type: 'date',
582
+ },
422
583
  twoFactorEnabled: {
423
584
  defaultValue: false,
424
585
  fieldName: 'twoFactorEnabled',
@@ -429,6 +590,12 @@ function buildUserFields(config: IBetterAuth): Record<string, UserFieldConfig> {
429
590
  fieldName: 'verified',
430
591
  type: 'boolean',
431
592
  },
593
+ // Track when email was verified (synced from Better-Auth)
594
+ verifiedAt: {
595
+ defaultValue: null,
596
+ fieldName: 'verifiedAt',
597
+ type: 'date',
598
+ },
432
599
  };
433
600
 
434
601
  // Merge with custom additional fields from configuration
@@ -447,6 +614,28 @@ function buildUserFields(config: IBetterAuth): Record<string, UserFieldConfig> {
447
614
  return coreFields;
448
615
  }
449
616
 
617
+ /**
618
+ * Formats the environment name as a suffix.
619
+ * Only adds suffix for development/test environments.
620
+ *
621
+ * @example
622
+ * formatEnvSuffix('local') // → '(Local)'
623
+ * formatEnvSuffix('development') // → '(Development)'
624
+ * formatEnvSuffix('test') // → '(Test)'
625
+ * formatEnvSuffix('production') // → '' (no suffix for production)
626
+ * formatEnvSuffix(undefined) // → ''
627
+ */
628
+ function formatEnvSuffix(env?: string): string {
629
+ if (!env) return '';
630
+
631
+ // Don't add suffix for production
632
+ if (env === 'production' || env === 'prod') return '';
633
+
634
+ // Capitalize first letter
635
+ const formatted = env.charAt(0).toUpperCase() + env.slice(1).toLowerCase();
636
+ return `(${formatted})`;
637
+ }
638
+
450
639
  /**
451
640
  * Gets or generates the fallback secret for development.
452
641
  * The secret is cached to ensure consistency during the server's lifetime.
@@ -458,6 +647,37 @@ function getAutoGeneratedSecret(): string {
458
647
  return cachedAutoGeneratedSecret;
459
648
  }
460
649
 
650
+ /**
651
+ * Gets the project's app name from package.json.
652
+ *
653
+ * This function reads the `name` field from the project's package.json
654
+ * and formats it for display (converts a kebab-case to Title Case).
655
+ *
656
+ * Used as default for:
657
+ * - `betterAuth.passkey.rpName` (displayed in browser Passkey prompts)
658
+ * - `betterAuth.twoFactor.appName` (displayed in authenticator apps)
659
+ *
660
+ * @param serverEnv - Optional server environment to append as suffix (e.g., 'local' → '(Local)')
661
+ * @returns The formatted project name, or 'Nest Server' as fallback
662
+ *
663
+ * @example
664
+ * // package.json: { "name": "my-awesome-app" }
665
+ * getProjectAppName() // → 'My Awesome App'
666
+ * getProjectAppName('local') // → 'My Awesome App (Local)'
667
+ * getProjectAppName('test') // → 'My Awesome App (Test)'
668
+ */
669
+ function getProjectAppName(serverEnv?: string): string {
670
+ // Return cached value if available (without env suffix, will be added after)
671
+ if (cachedProjectAppName === null) {
672
+ cachedProjectAppName = readProjectNameFromPackageJson();
673
+ }
674
+
675
+ // Add environment suffix for non-production environments
676
+ // This helps developers distinguish Passkeys when running multiple projects locally
677
+ const envSuffix = formatEnvSuffix(serverEnv);
678
+ return envSuffix ? `${cachedProjectAppName} ${envSuffix}` : cachedProjectAppName;
679
+ }
680
+
461
681
  /**
462
682
  * Checks if a secret has valid minimum length (32 characters)
463
683
  */
@@ -477,6 +697,28 @@ function isValidUrl(url: string): boolean {
477
697
  }
478
698
  }
479
699
 
700
+ /**
701
+ * Reads the project name from package.json in the current working directory.
702
+ * Falls back to 'Nest Server' if package.json cannot be read or has no name.
703
+ */
704
+ function readProjectNameFromPackageJson(): string {
705
+ const fallback = 'Nest Server';
706
+
707
+ try {
708
+ const packageJsonPath = path.join(process.cwd(), 'package.json');
709
+ const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf-8');
710
+ const packageJson = JSON.parse(packageJsonContent);
711
+
712
+ if (packageJson.name && typeof packageJson.name === 'string') {
713
+ return formatProjectName(packageJson.name);
714
+ }
715
+ } catch {
716
+ // Ignore errors - use fallback
717
+ }
718
+
719
+ return fallback;
720
+ }
721
+
480
722
  /**
481
723
  * Default URLs for local/test environments (local, ci, e2e)
482
724
  * These environments typically run on localhost and don't have a deployed domain.
@@ -598,10 +840,16 @@ function extractRpIdFromUrl(url: string): string {
598
840
  * - `trustedOrigins`: from config.trustedOrigins > [resolvedUrls.appUrl]
599
841
  *
600
842
  * @param config - Better-auth configuration
601
- * @param resolvedUrls - Resolved URLs from resolveUrls()
843
+ * @param options - Additional options
844
+ * @param options.resolvedUrls - Resolved URLs from resolveUrls()
845
+ * @param options.serverEnv - Server environment for auto-detected app name suffix
602
846
  * @returns Normalization result with enabled status, config, and warnings
603
847
  */
604
- function normalizePasskeyConfig(config: IBetterAuth, resolvedUrls: ResolvedUrls): PasskeyNormalizationResult {
848
+ function normalizePasskeyConfig(
849
+ config: IBetterAuth,
850
+ options: { resolvedUrls: ResolvedUrls; serverEnv?: string },
851
+ ): PasskeyNormalizationResult {
852
+ const { resolvedUrls, serverEnv } = options;
605
853
  const warnings: string[] = [];
606
854
 
607
855
  // Check if Passkey is explicitly DISABLED
@@ -657,10 +905,11 @@ function normalizePasskeyConfig(config: IBetterAuth, resolvedUrls: ResolvedUrls)
657
905
  }
658
906
 
659
907
  // Build normalized config
908
+ // rpName: Use explicit config, or auto-detect from package.json with environment suffix
660
909
  const normalizedConfig: NormalizedPasskeyConfig = {
661
910
  origin: finalOrigin,
662
911
  rpId: finalRpId,
663
- rpName: rawConfig.rpName || 'Nest Server',
912
+ rpName: rawConfig.rpName || getProjectAppName(serverEnv),
664
913
  };
665
914
 
666
915
  // Copy optional fields from explicit config
@@ -744,37 +993,40 @@ function resolveUrls(options: CreateBetterAuthOptions): ResolvedUrls {
744
993
  * 3. Auto-generated secure secret (with warning)
745
994
  *
746
995
  * @param config - Better-auth configuration
747
- * @param fallbackSecrets - Optional array of fallback secrets to try
748
- * @param passkeyNormalization - Result of passkey normalization (handles graceful degradation)
996
+ * @param options - Optional validation context (fallback secrets, passkey normalization)
749
997
  */
750
998
  function validateConfig(
751
999
  config: IBetterAuth,
752
- fallbackSecrets?: (string | undefined)[],
753
- passkeyNormalization?: PasskeyNormalizationResult,
1000
+ options?: {
1001
+ fallbackSecrets?: (string | undefined)[];
1002
+ passkeyNormalization?: PasskeyNormalizationResult;
1003
+ },
754
1004
  ): ValidationResult {
1005
+ const { fallbackSecrets, passkeyNormalization } = options ?? {};
755
1006
  const errors: string[] = [];
756
1007
  const warnings: string[] = [];
757
1008
 
758
1009
  // Track secret source for appropriate messaging
759
1010
  let secretSource: 'auto-generated' | 'explicit' | 'fallback' = 'explicit';
1011
+ let resolvedSecret = config.secret;
760
1012
 
761
- // Resolve secret with fallback chain
762
- if (!config.secret || config.secret.trim() === '') {
1013
+ // Resolve secret with fallback chain (without mutating the config object)
1014
+ if (!resolvedSecret || resolvedSecret.trim() === '') {
763
1015
  // Try fallback secrets in order
764
1016
  const validFallback = fallbackSecrets?.find((secret) => secret && isValidSecretLength(secret));
765
1017
 
766
1018
  if (validFallback) {
767
- config.secret = validFallback;
1019
+ resolvedSecret = validFallback;
768
1020
  secretSource = 'fallback';
769
1021
  } else {
770
1022
  // Last resort: auto-generate
771
- config.secret = getAutoGeneratedSecret();
1023
+ resolvedSecret = getAutoGeneratedSecret();
772
1024
  secretSource = 'auto-generated';
773
1025
  }
774
1026
  }
775
1027
 
776
1028
  // Validate the resolved secret
777
- const secretValidation = validateSecret(config.secret);
1029
+ const secretValidation = validateSecret(resolvedSecret);
778
1030
  if (!secretValidation.valid) {
779
1031
  errors.push(secretValidation.message!);
780
1032
  } else if (secretValidation.message) {
@@ -853,7 +1105,7 @@ function validateConfig(
853
1105
  errors.push(`Social provider '${name}' is missing clientSecret`);
854
1106
  }
855
1107
  } else {
856
- // No credentials provided but provider is configured and not disabled
1108
+ // No credentials provided, but the provider is configured and not disabled
857
1109
  // This is likely a configuration mistake - warn the user
858
1110
  warnings.push(
859
1111
  `Social provider '${name}' is configured but missing both clientId and clientSecret. ` +
@@ -866,6 +1118,7 @@ function validateConfig(
866
1118
 
867
1119
  return {
868
1120
  errors,
1121
+ resolvedSecret,
869
1122
  valid: errors.length === 0,
870
1123
  warnings,
871
1124
  };
@@ -1,9 +1,11 @@
1
+ import { Optional } from '@nestjs/common';
1
2
  import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
2
3
  import { Request, Response } from 'express';
3
4
 
4
5
  import { Roles } from '../../common/decorators/roles.decorator';
5
6
  import { RoleEnum } from '../../common/enums/role.enum';
6
7
  import { CoreBetterAuthAuthModel } from './core-better-auth-auth.model';
8
+ import { CoreBetterAuthEmailVerificationService } from './core-better-auth-email-verification.service';
7
9
  import {
8
10
  CoreBetterAuth2FASetupModel,
9
11
  CoreBetterAuthFeaturesModel,
@@ -11,6 +13,7 @@ import {
11
13
  CoreBetterAuthPasskeyModel,
12
14
  CoreBetterAuthSessionModel,
13
15
  } from './core-better-auth-models';
16
+ import { CoreBetterAuthSignUpValidatorService } from './core-better-auth-signup-validator.service';
14
17
  import { CoreBetterAuthUserMapper } from './core-better-auth-user.mapper';
15
18
  import { CoreBetterAuthResolver } from './core-better-auth.resolver';
16
19
  import { CoreBetterAuthService } from './core-better-auth.service';
@@ -38,8 +41,13 @@ import { CoreBetterAuthService } from './core-better-auth.service';
38
41
  * super(betterAuthService, userMapper);
39
42
  * }
40
43
  *
41
- * override async betterAuthSignUp(email: string, password: string, name?: string) {
42
- * const result = await super.betterAuthSignUp(email, password, name);
44
+ * override async betterAuthSignUp(
45
+ * email: string,
46
+ * password: string,
47
+ * name?: string,
48
+ * termsAndPrivacyAccepted?: boolean,
49
+ * ) {
50
+ * const result = await super.betterAuthSignUp(email, password, name, termsAndPrivacyAccepted);
43
51
  *
44
52
  * // Send welcome email after successful sign-up
45
53
  * if (result.success && result.user) {
@@ -57,8 +65,10 @@ export class DefaultBetterAuthResolver extends CoreBetterAuthResolver {
57
65
  constructor(
58
66
  protected override readonly betterAuthService: CoreBetterAuthService,
59
67
  protected override readonly userMapper: CoreBetterAuthUserMapper,
68
+ @Optional() protected override readonly signUpValidator?: CoreBetterAuthSignUpValidatorService,
69
+ @Optional() protected override readonly emailVerificationService?: CoreBetterAuthEmailVerificationService,
60
70
  ) {
61
- super(betterAuthService, userMapper);
71
+ super(betterAuthService, userMapper, signUpValidator, emailVerificationService);
62
72
  }
63
73
 
64
74
  // ===========================================================================
@@ -106,7 +116,7 @@ export class DefaultBetterAuthResolver extends CoreBetterAuthResolver {
106
116
  override async betterAuthSignIn(
107
117
  @Args('email') email: string,
108
118
  @Args('password') password: string,
109
- @Context() ctx: { req: Request; res: Response },
119
+ @Context() ctx?: { req: Request; res: Response },
110
120
  ): Promise<CoreBetterAuthAuthModel> {
111
121
  return super.betterAuthSignIn(email, password, ctx);
112
122
  }
@@ -119,8 +129,9 @@ export class DefaultBetterAuthResolver extends CoreBetterAuthResolver {
119
129
  @Args('email') email: string,
120
130
  @Args('password') password: string,
121
131
  @Args('name', { nullable: true }) name?: string,
132
+ @Args('termsAndPrivacyAccepted', { nullable: true }) termsAndPrivacyAccepted?: boolean,
122
133
  ): Promise<CoreBetterAuthAuthModel> {
123
- return super.betterAuthSignUp(email, password, name);
134
+ return super.betterAuthSignUp(email, password, name, termsAndPrivacyAccepted);
124
135
  }
125
136
 
126
137
  @Mutation(() => Boolean, { description: 'Sign out via Better-Auth' })