@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.
- package/dist/config.env.js +16 -133
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +4 -0
- package/dist/core/modules/auth/guards/auth.guard.d.ts +2 -2
- package/dist/core/modules/auth/guards/auth.guard.js +68 -8
- package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
- package/dist/core/modules/auth/guards/roles.guard.d.ts +3 -4
- package/dist/core/modules/auth/guards/roles.guard.js +64 -159
- package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth-token.service.d.ts +21 -0
- package/dist/core/modules/better-auth/better-auth-token.service.js +153 -0
- package/dist/core/modules/better-auth/better-auth-token.service.js.map +1 -0
- package/dist/core/modules/better-auth/better-auth.config.d.ts +3 -0
- package/dist/core/modules/better-auth/better-auth.config.js +176 -47
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.types.d.ts +13 -0
- package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +5 -1
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +101 -8
- package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-challenge.service.d.ts +20 -0
- package/dist/core/modules/better-auth/core-better-auth-challenge.service.js +142 -0
- package/dist/core/modules/better-auth/core-better-auth-challenge.service.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +1 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-web.helper.d.ts +2 -0
- package/dist/core/modules/better-auth/core-better-auth-web.helper.js +29 -1
- package/dist/core/modules/better-auth/core-better-auth-web.helper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.controller.js +5 -13
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.middleware.d.ts +0 -1
- package/dist/core/modules/better-auth/core-better-auth.middleware.js +6 -19
- package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.d.ts +6 -1
- package/dist/core/modules/better-auth/core-better-auth.module.js +82 -19
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.resolver.js +7 -6
- package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.service.d.ts +1 -2
- package/dist/core/modules/better-auth/core-better-auth.service.js +27 -37
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core/modules/better-auth/index.d.ts +1 -0
- package/dist/core/modules/better-auth/index.js +1 -0
- package/dist/core/modules/better-auth/index.js.map +1 -1
- package/dist/core.module.js +4 -0
- package/dist/core.module.js.map +1 -1
- package/dist/server/modules/better-auth/better-auth.module.d.ts +4 -1
- package/dist/server/modules/better-auth/better-auth.module.js +4 -1
- package/dist/server/modules/better-auth/better-auth.module.js.map +1 -1
- package/dist/server/server.module.js +1 -4
- package/dist/server/server.module.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/config.env.ts +24 -174
- package/src/core/common/interfaces/server-options.interface.ts +288 -35
- package/src/core/modules/auth/guards/auth.guard.ts +136 -23
- package/src/core/modules/auth/guards/roles.guard.ts +119 -239
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +82 -56
- package/src/core/modules/better-auth/README.md +132 -35
- package/src/core/modules/better-auth/better-auth-token.service.ts +241 -0
- package/src/core/modules/better-auth/better-auth.config.ts +402 -70
- package/src/core/modules/better-auth/better-auth.types.ts +37 -0
- package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +158 -18
- package/src/core/modules/better-auth/core-better-auth-challenge.service.ts +254 -0
- package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +1 -1
- package/src/core/modules/better-auth/core-better-auth-web.helper.ts +64 -1
- package/src/core/modules/better-auth/core-better-auth.controller.ts +7 -15
- package/src/core/modules/better-auth/core-better-auth.middleware.ts +7 -20
- package/src/core/modules/better-auth/core-better-auth.module.ts +182 -25
- package/src/core/modules/better-auth/core-better-auth.resolver.ts +8 -7
- package/src/core/modules/better-auth/core-better-auth.service.ts +40 -48
- package/src/core/modules/better-auth/index.ts +1 -0
- package/src/core.module.ts +8 -0
- package/src/server/modules/better-auth/better-auth.module.ts +40 -10
- package/src/server/server.module.ts +2 -4
|
@@ -62,6 +62,24 @@ export interface CreateBetterAuthOptions {
|
|
|
62
62
|
* ```
|
|
63
63
|
*/
|
|
64
64
|
fallbackSecrets?: (string | undefined)[];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Server-level app/frontend URL (from IServerOptions.appUrl).
|
|
68
|
+
* Used for Passkey origin and CORS trustedOrigins.
|
|
69
|
+
*/
|
|
70
|
+
serverAppUrl?: string;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Server-level base URL (from IServerOptions.baseUrl).
|
|
74
|
+
* Used as fallback for betterAuth.baseUrl.
|
|
75
|
+
*/
|
|
76
|
+
serverBaseUrl?: string;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Server environment (from IServerOptions.env).
|
|
80
|
+
* Used for local environment defaults.
|
|
81
|
+
*/
|
|
82
|
+
serverEnv?: string;
|
|
65
83
|
}
|
|
66
84
|
|
|
67
85
|
/**
|
|
@@ -70,6 +88,46 @@ export interface CreateBetterAuthOptions {
|
|
|
70
88
|
*/
|
|
71
89
|
type BetterAuthFieldType = 'boolean' | 'date' | 'json' | 'number' | 'number[]' | 'string' | 'string[]';
|
|
72
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Normalized Passkey configuration with required fields
|
|
93
|
+
*/
|
|
94
|
+
interface NormalizedPasskeyConfig {
|
|
95
|
+
authenticatorAttachment?: 'cross-platform' | 'platform';
|
|
96
|
+
challengeStorage?: 'cookie' | 'database';
|
|
97
|
+
challengeTtlSeconds?: number;
|
|
98
|
+
origin: string;
|
|
99
|
+
residentKey?: 'discouraged' | 'preferred' | 'required';
|
|
100
|
+
rpId: string;
|
|
101
|
+
rpName: string;
|
|
102
|
+
userVerification?: 'discouraged' | 'preferred' | 'required';
|
|
103
|
+
webAuthnChallengeCookie?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Result of Passkey configuration normalization
|
|
108
|
+
*/
|
|
109
|
+
interface PasskeyNormalizationResult {
|
|
110
|
+
/**
|
|
111
|
+
* Whether Passkey should be enabled after normalization
|
|
112
|
+
*/
|
|
113
|
+
enabled: boolean;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Normalized Passkey configuration (with auto-detected values)
|
|
117
|
+
*/
|
|
118
|
+
normalizedConfig: NormalizedPasskeyConfig | null;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Auto-detected trustedOrigins (to be merged with config)
|
|
122
|
+
*/
|
|
123
|
+
trustedOrigins: null | string[];
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Warnings generated during normalization
|
|
127
|
+
*/
|
|
128
|
+
warnings: string[];
|
|
129
|
+
}
|
|
130
|
+
|
|
73
131
|
/**
|
|
74
132
|
* Social provider configuration for better-auth
|
|
75
133
|
*/
|
|
@@ -115,8 +173,26 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
|
|
|
115
173
|
return null;
|
|
116
174
|
}
|
|
117
175
|
|
|
176
|
+
// Resolve URLs with auto-detection and local defaults
|
|
177
|
+
const resolvedUrls = resolveUrls(options);
|
|
178
|
+
|
|
179
|
+
// Log URL resolution warnings
|
|
180
|
+
for (const warning of resolvedUrls.warnings) {
|
|
181
|
+
logger.log(warning);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Normalize Passkey configuration with auto-detection from resolved URLs
|
|
185
|
+
// This must happen BEFORE validation to allow graceful degradation
|
|
186
|
+
// Passkey is now AUTO-ACTIVATED by default (not opt-in)
|
|
187
|
+
const passkeyNormalization = normalizePasskeyConfig(config, resolvedUrls);
|
|
188
|
+
|
|
189
|
+
// Log passkey normalization warnings (info about auto-detection or disabled status)
|
|
190
|
+
for (const warning of passkeyNormalization.warnings) {
|
|
191
|
+
logger.warn(warning);
|
|
192
|
+
}
|
|
193
|
+
|
|
118
194
|
// Validate configuration (with fallback secrets for backwards compatibility)
|
|
119
|
-
const validation = validateConfig(config, fallbackSecrets);
|
|
195
|
+
const validation = validateConfig(config, fallbackSecrets, passkeyNormalization);
|
|
120
196
|
|
|
121
197
|
// Log warnings
|
|
122
198
|
for (const warning of validation.warnings) {
|
|
@@ -128,16 +204,17 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
|
|
|
128
204
|
throw new Error(`BetterAuth configuration invalid: ${validation.errors.join('; ')}`);
|
|
129
205
|
}
|
|
130
206
|
|
|
131
|
-
// Build configuration components
|
|
132
|
-
const plugins = buildPlugins(config);
|
|
207
|
+
// Build configuration components (pass normalized passkey config and resolved URLs)
|
|
208
|
+
const plugins = buildPlugins(config, passkeyNormalization);
|
|
133
209
|
const socialProviders = buildSocialProviders(config);
|
|
134
|
-
const trustedOrigins = buildTrustedOrigins(config);
|
|
210
|
+
const trustedOrigins = buildTrustedOrigins(config, passkeyNormalization, resolvedUrls);
|
|
135
211
|
const additionalFields = buildUserFields(config);
|
|
136
212
|
|
|
137
213
|
// Build the base Better-Auth configuration
|
|
214
|
+
// Use resolved baseUrl (with local defaults) or fallback
|
|
138
215
|
const betterAuthConfig: Record<string, unknown> = {
|
|
139
216
|
basePath: config.basePath || '/iam',
|
|
140
|
-
baseURL: config.baseUrl || 'http://localhost:3000',
|
|
217
|
+
baseURL: resolvedUrls.baseUrl || config.baseUrl || 'http://localhost:3000',
|
|
141
218
|
database: mongodbAdapter(db),
|
|
142
219
|
// Enable email/password authentication by default (required by Better-Auth 1.x)
|
|
143
220
|
// Can be disabled by setting config.emailAndPassword.enabled = false
|
|
@@ -178,8 +255,11 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
|
|
|
178
255
|
* - `{ option: value }`: Enable with custom settings
|
|
179
256
|
* - `false` or `{ enabled: false }`: Disable
|
|
180
257
|
* - `undefined`: Disabled (default)
|
|
258
|
+
*
|
|
259
|
+
* @param config - Better-auth configuration
|
|
260
|
+
* @param passkeyNormalization - Normalized passkey configuration from normalizePasskeyConfig()
|
|
181
261
|
*/
|
|
182
|
-
function buildPlugins(config: IBetterAuth): BetterAuthPlugin[] {
|
|
262
|
+
function buildPlugins(config: IBetterAuth, passkeyNormalization: PasskeyNormalizationResult): BetterAuthPlugin[] {
|
|
183
263
|
const plugins: BetterAuthPlugin[] = [];
|
|
184
264
|
|
|
185
265
|
// JWT Plugin for API client compatibility
|
|
@@ -198,8 +278,11 @@ function buildPlugins(config: IBetterAuth): BetterAuthPlugin[] {
|
|
|
198
278
|
}
|
|
199
279
|
|
|
200
280
|
// Two-Factor Authentication Plugin
|
|
201
|
-
|
|
202
|
-
|
|
281
|
+
// 2FA is enabled by default unless explicitly disabled (twoFactor: false or twoFactor: { enabled: false })
|
|
282
|
+
const twoFactorExplicitlyDisabled =
|
|
283
|
+
config.twoFactor === false || (typeof config.twoFactor === 'object' && config.twoFactor?.enabled === false);
|
|
284
|
+
if (!twoFactorExplicitlyDisabled) {
|
|
285
|
+
const twoFactorConfig = typeof config.twoFactor === 'object' ? config.twoFactor : {};
|
|
203
286
|
plugins.push(
|
|
204
287
|
twoFactor({
|
|
205
288
|
issuer: twoFactorConfig.appName || 'Nest Server',
|
|
@@ -208,27 +291,29 @@ function buildPlugins(config: IBetterAuth): BetterAuthPlugin[] {
|
|
|
208
291
|
}
|
|
209
292
|
|
|
210
293
|
// Passkey/WebAuthn Plugin
|
|
211
|
-
|
|
212
|
-
if (
|
|
213
|
-
|
|
294
|
+
// Uses normalized config from normalizePasskeyConfig() which handles auto-detection
|
|
295
|
+
if (passkeyNormalization.enabled && passkeyNormalization.normalizedConfig) {
|
|
296
|
+
const normalizedPasskey = passkeyNormalization.normalizedConfig;
|
|
297
|
+
|
|
298
|
+
// Build passkey options from normalized config
|
|
214
299
|
const passkeyOptions: Record<string, unknown> = {
|
|
215
|
-
origin:
|
|
216
|
-
rpID:
|
|
217
|
-
rpName:
|
|
300
|
+
origin: normalizedPasskey.origin,
|
|
301
|
+
rpID: normalizedPasskey.rpId,
|
|
302
|
+
rpName: normalizedPasskey.rpName,
|
|
218
303
|
};
|
|
219
304
|
|
|
220
305
|
// Add optional WebAuthn configuration if specified
|
|
221
|
-
if (
|
|
222
|
-
passkeyOptions.authenticatorAttachment =
|
|
306
|
+
if (normalizedPasskey.authenticatorAttachment) {
|
|
307
|
+
passkeyOptions.authenticatorAttachment = normalizedPasskey.authenticatorAttachment;
|
|
223
308
|
}
|
|
224
|
-
if (
|
|
225
|
-
passkeyOptions.residentKey =
|
|
309
|
+
if (normalizedPasskey.residentKey) {
|
|
310
|
+
passkeyOptions.residentKey = normalizedPasskey.residentKey;
|
|
226
311
|
}
|
|
227
|
-
if (
|
|
228
|
-
passkeyOptions.userVerification =
|
|
312
|
+
if (normalizedPasskey.userVerification) {
|
|
313
|
+
passkeyOptions.userVerification = normalizedPasskey.userVerification;
|
|
229
314
|
}
|
|
230
|
-
if (
|
|
231
|
-
passkeyOptions.webAuthnChallengeCookie =
|
|
315
|
+
if (normalizedPasskey.webAuthnChallengeCookie) {
|
|
316
|
+
passkeyOptions.webAuthnChallengeCookie = normalizedPasskey.webAuthnChallengeCookie;
|
|
232
317
|
}
|
|
233
318
|
|
|
234
319
|
plugins.push(passkey(passkeyOptions));
|
|
@@ -273,24 +358,32 @@ function buildSocialProviders(config: IBetterAuth): Record<string, SocialProvide
|
|
|
273
358
|
*
|
|
274
359
|
* Behavior:
|
|
275
360
|
* - If trustedOrigins is explicitly configured → use those origins
|
|
276
|
-
* - If Passkey
|
|
361
|
+
* - If Passkey is enabled → use trustedOrigins from normalizePasskeyConfig()
|
|
362
|
+
* - Fallback to resolved appUrl
|
|
277
363
|
* - Otherwise → return undefined (allows all origins via Better-Auth's default)
|
|
278
364
|
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
365
|
+
* @param config - Better-auth configuration
|
|
366
|
+
* @param passkeyNormalization - Normalized passkey configuration from normalizePasskeyConfig()
|
|
367
|
+
* @param resolvedUrls - Resolved URLs from resolveUrls()
|
|
282
368
|
*/
|
|
283
|
-
function buildTrustedOrigins(
|
|
369
|
+
function buildTrustedOrigins(
|
|
370
|
+
config: IBetterAuth,
|
|
371
|
+
passkeyNormalization: PasskeyNormalizationResult,
|
|
372
|
+
resolvedUrls: ResolvedUrls,
|
|
373
|
+
): string[] | undefined {
|
|
284
374
|
// If trustedOrigins is explicitly configured, use it
|
|
285
375
|
if (config.trustedOrigins?.length) {
|
|
286
376
|
return config.trustedOrigins;
|
|
287
377
|
}
|
|
288
378
|
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
379
|
+
// If Passkey is enabled, use the trustedOrigins from normalization
|
|
380
|
+
if (passkeyNormalization.enabled && passkeyNormalization.trustedOrigins?.length) {
|
|
381
|
+
return passkeyNormalization.trustedOrigins;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Fallback to resolved appUrl (for CORS even without Passkey)
|
|
385
|
+
if (resolvedUrls.appUrl) {
|
|
386
|
+
return [resolvedUrls.appUrl];
|
|
294
387
|
}
|
|
295
388
|
|
|
296
389
|
// Otherwise, let Better-Auth handle CORS with its default behavior
|
|
@@ -365,29 +458,6 @@ function getAutoGeneratedSecret(): string {
|
|
|
365
458
|
return cachedAutoGeneratedSecret;
|
|
366
459
|
}
|
|
367
460
|
|
|
368
|
-
/**
|
|
369
|
-
* Gets plugin configuration as object, handling boolean shorthand.
|
|
370
|
-
* Returns undefined if disabled, or the config object if enabled.
|
|
371
|
-
*/
|
|
372
|
-
function getPluginConfig<T extends { enabled?: boolean }>(config: boolean | T | undefined): T | undefined {
|
|
373
|
-
if (!isPluginEnabled(config)) return undefined;
|
|
374
|
-
if (typeof config === 'boolean') return {} as T;
|
|
375
|
-
return config;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Checks if a plugin configuration is enabled.
|
|
380
|
-
* Supports both boolean and object configuration:
|
|
381
|
-
* - `true` or `{}` or `{ enabled: true }` → enabled
|
|
382
|
-
* - `false` or `{ enabled: false }` → disabled
|
|
383
|
-
* - `undefined` → disabled
|
|
384
|
-
*/
|
|
385
|
-
function isPluginEnabled<T extends { enabled?: boolean }>(config: boolean | T | undefined): boolean {
|
|
386
|
-
if (config === undefined) return false;
|
|
387
|
-
if (typeof config === 'boolean') return config;
|
|
388
|
-
return config.enabled !== false;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
461
|
/**
|
|
392
462
|
* Checks if a secret has valid minimum length (32 characters)
|
|
393
463
|
*/
|
|
@@ -407,6 +477,263 @@ function isValidUrl(url: string): boolean {
|
|
|
407
477
|
}
|
|
408
478
|
}
|
|
409
479
|
|
|
480
|
+
/**
|
|
481
|
+
* Default URLs for local/test environments (local, ci, e2e)
|
|
482
|
+
* These environments typically run on localhost and don't have a deployed domain.
|
|
483
|
+
*/
|
|
484
|
+
const LOCALHOST_DEFAULTS = {
|
|
485
|
+
API_URL: 'http://localhost:3000',
|
|
486
|
+
APP_URL: 'http://localhost:3001',
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Environments that use localhost defaults for URLs.
|
|
491
|
+
* These are typically development/test environments without deployed domains.
|
|
492
|
+
*/
|
|
493
|
+
const LOCALHOST_ENVS = ['local', 'ci', 'e2e'];
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Resolves the effective URLs for BetterAuth configuration.
|
|
497
|
+
*
|
|
498
|
+
* Resolution priority:
|
|
499
|
+
* 1. Explicit betterAuth config values
|
|
500
|
+
* 2. Server-level URLs (IServerOptions.baseUrl, IServerOptions.appUrl)
|
|
501
|
+
* 3. Auto-derived values (appUrl from baseUrl)
|
|
502
|
+
* 4. Local environment defaults (when env: 'local')
|
|
503
|
+
*
|
|
504
|
+
* @param options - CreateBetterAuthOptions containing config and server URLs
|
|
505
|
+
* @returns Resolved URLs with warnings for logging
|
|
506
|
+
*/
|
|
507
|
+
interface ResolvedUrls {
|
|
508
|
+
appUrl: string | undefined;
|
|
509
|
+
baseUrl: string | undefined;
|
|
510
|
+
rpId: string | undefined;
|
|
511
|
+
warnings: string[];
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Derives appUrl from baseUrl by removing 'api.' prefix from subdomain.
|
|
516
|
+
*
|
|
517
|
+
* Examples:
|
|
518
|
+
* - 'https://api.example.com' → 'https://example.com'
|
|
519
|
+
* - 'https://api.dev.example.com' → 'https://dev.example.com'
|
|
520
|
+
* - 'https://example.com' → 'https://example.com' (unchanged)
|
|
521
|
+
* - 'http://localhost:3000' → 'http://localhost:3000' (unchanged)
|
|
522
|
+
*
|
|
523
|
+
* @param baseUrl - The API base URL
|
|
524
|
+
* @returns Derived app URL or the original URL if no 'api.' prefix
|
|
525
|
+
*/
|
|
526
|
+
function deriveAppUrlFromBaseUrl(baseUrl: string): string {
|
|
527
|
+
try {
|
|
528
|
+
const url = new URL(baseUrl);
|
|
529
|
+
const hostname = url.hostname;
|
|
530
|
+
|
|
531
|
+
// Check if hostname starts with 'api.'
|
|
532
|
+
if (hostname.startsWith('api.')) {
|
|
533
|
+
// Remove 'api.' prefix
|
|
534
|
+
url.hostname = hostname.substring(4);
|
|
535
|
+
return url.origin;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Return original URL if no 'api.' prefix
|
|
539
|
+
return url.origin;
|
|
540
|
+
} catch {
|
|
541
|
+
return baseUrl;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Extracts the root domain from a URL for use as Passkey rpId.
|
|
547
|
+
*
|
|
548
|
+
* For localhost: Returns 'localhost' (not 'localhost:3000')
|
|
549
|
+
* For domains: Returns the registrable domain (e.g., 'example.com')
|
|
550
|
+
*
|
|
551
|
+
* Examples:
|
|
552
|
+
* - 'https://api.example.com' → 'example.com'
|
|
553
|
+
* - 'https://app.dev.example.com' → 'example.com'
|
|
554
|
+
* - 'http://localhost:3000' → 'localhost'
|
|
555
|
+
* - 'https://example.com' → 'example.com'
|
|
556
|
+
*
|
|
557
|
+
* @param url - The URL to extract rpId from
|
|
558
|
+
* @returns The root domain suitable for Passkey rpId
|
|
559
|
+
*/
|
|
560
|
+
function extractRpIdFromUrl(url: string): string {
|
|
561
|
+
try {
|
|
562
|
+
const parsedUrl = new URL(url);
|
|
563
|
+
const hostname = parsedUrl.hostname;
|
|
564
|
+
|
|
565
|
+
// localhost is a special case
|
|
566
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
|
567
|
+
return 'localhost';
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// For regular domains, extract the registrable domain
|
|
571
|
+
// This is a simplified approach - for complex TLDs like .co.uk, a library would be better
|
|
572
|
+
const parts = hostname.split('.');
|
|
573
|
+
|
|
574
|
+
// If it's just a simple domain (example.com), return it
|
|
575
|
+
if (parts.length <= 2) {
|
|
576
|
+
return hostname;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// For subdomains, return the last two parts (example.com from api.example.com)
|
|
580
|
+
// Note: This doesn't handle .co.uk style TLDs correctly, but works for common cases
|
|
581
|
+
return parts.slice(-2).join('.');
|
|
582
|
+
} catch {
|
|
583
|
+
return 'localhost';
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Normalizes Passkey configuration with auto-detection from resolved URLs.
|
|
589
|
+
*
|
|
590
|
+
* **AUTO-ACTIVATION:** Passkey is enabled by default when BetterAuth is active.
|
|
591
|
+
* It is only disabled when:
|
|
592
|
+
* 1. Explicitly disabled: `passkey: false` or `passkey: { enabled: false }`
|
|
593
|
+
* 2. Required URLs cannot be resolved (Graceful Degradation)
|
|
594
|
+
*
|
|
595
|
+
* Auto-Detection Logic (using resolvedUrls from resolveUrls()):
|
|
596
|
+
* - `rpId`: from passkey.rpId > resolvedUrls.rpId (derived from appUrl)
|
|
597
|
+
* - `origin`: from passkey.origin > resolvedUrls.appUrl
|
|
598
|
+
* - `trustedOrigins`: from config.trustedOrigins > [resolvedUrls.appUrl]
|
|
599
|
+
*
|
|
600
|
+
* @param config - Better-auth configuration
|
|
601
|
+
* @param resolvedUrls - Resolved URLs from resolveUrls()
|
|
602
|
+
* @returns Normalization result with enabled status, config, and warnings
|
|
603
|
+
*/
|
|
604
|
+
function normalizePasskeyConfig(config: IBetterAuth, resolvedUrls: ResolvedUrls): PasskeyNormalizationResult {
|
|
605
|
+
const warnings: string[] = [];
|
|
606
|
+
|
|
607
|
+
// Check if Passkey is explicitly DISABLED
|
|
608
|
+
// passkey: false OR passkey: { enabled: false }
|
|
609
|
+
const isExplicitlyDisabled =
|
|
610
|
+
config.passkey === false || (typeof config.passkey === 'object' && config.passkey?.enabled === false);
|
|
611
|
+
|
|
612
|
+
if (isExplicitlyDisabled) {
|
|
613
|
+
return { enabled: false, normalizedConfig: null, trustedOrigins: null, warnings };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Get the raw passkey config (if object)
|
|
617
|
+
const rawConfig = typeof config.passkey === 'object' ? config.passkey : {};
|
|
618
|
+
|
|
619
|
+
// Resolve values: explicit config > resolved URLs
|
|
620
|
+
const finalRpId = rawConfig.rpId || resolvedUrls.rpId;
|
|
621
|
+
const finalOrigin = rawConfig.origin || resolvedUrls.appUrl;
|
|
622
|
+
const finalTrustedOrigins = config.trustedOrigins?.length ? config.trustedOrigins : resolvedUrls.appUrl ? [resolvedUrls.appUrl] : undefined;
|
|
623
|
+
|
|
624
|
+
// Check if we have all required values for Passkey
|
|
625
|
+
const hasRequiredConfig = finalRpId && finalOrigin && finalTrustedOrigins?.length;
|
|
626
|
+
|
|
627
|
+
if (!hasRequiredConfig) {
|
|
628
|
+
// Graceful Degradation: Disable Passkey with warning
|
|
629
|
+
const missingFields: string[] = [];
|
|
630
|
+
if (!finalRpId) missingFields.push('rpId');
|
|
631
|
+
if (!finalOrigin) missingFields.push('origin');
|
|
632
|
+
if (!finalTrustedOrigins?.length) missingFields.push('trustedOrigins');
|
|
633
|
+
|
|
634
|
+
warnings.push(
|
|
635
|
+
`⚠️ PASSKEY DISABLED: Cannot auto-detect required values (${missingFields.join(', ')}). ` +
|
|
636
|
+
'To enable Passkey, set baseUrl in your config or configure passkey values explicitly.',
|
|
637
|
+
);
|
|
638
|
+
warnings.push(
|
|
639
|
+
'Fix options:\n' +
|
|
640
|
+
' 1. Set baseUrl: "https://api.example.com" → auto-detects appUrl, rpId, origin\n' +
|
|
641
|
+
' 2. Set env: "local" → uses localhost:3000 (API) and localhost:3001 (App) defaults\n' +
|
|
642
|
+
' 3. Disable Passkey explicitly: passkey: false (no warning)',
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
return { enabled: false, normalizedConfig: null, trustedOrigins: null, warnings };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Log auto-detection info
|
|
649
|
+
if (!rawConfig.rpId && resolvedUrls.rpId) {
|
|
650
|
+
warnings.push(`PASSKEY: Using rpId="${finalRpId}" (auto-detected from appUrl)`);
|
|
651
|
+
}
|
|
652
|
+
if (!rawConfig.origin && resolvedUrls.appUrl) {
|
|
653
|
+
warnings.push(`PASSKEY: Using origin="${finalOrigin}" (= appUrl)`);
|
|
654
|
+
}
|
|
655
|
+
if (!config.trustedOrigins?.length && resolvedUrls.appUrl) {
|
|
656
|
+
warnings.push(`PASSKEY: Using trustedOrigins=${JSON.stringify(finalTrustedOrigins)} (= [appUrl])`);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Build normalized config
|
|
660
|
+
const normalizedConfig: NormalizedPasskeyConfig = {
|
|
661
|
+
origin: finalOrigin,
|
|
662
|
+
rpId: finalRpId,
|
|
663
|
+
rpName: rawConfig.rpName || 'Nest Server',
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
// Copy optional fields from explicit config
|
|
667
|
+
if (rawConfig.authenticatorAttachment) {
|
|
668
|
+
normalizedConfig.authenticatorAttachment = rawConfig.authenticatorAttachment;
|
|
669
|
+
}
|
|
670
|
+
if (rawConfig.challengeStorage) {
|
|
671
|
+
normalizedConfig.challengeStorage = rawConfig.challengeStorage;
|
|
672
|
+
}
|
|
673
|
+
if (rawConfig.challengeTtlSeconds) {
|
|
674
|
+
normalizedConfig.challengeTtlSeconds = rawConfig.challengeTtlSeconds;
|
|
675
|
+
}
|
|
676
|
+
if (rawConfig.residentKey) {
|
|
677
|
+
normalizedConfig.residentKey = rawConfig.residentKey;
|
|
678
|
+
}
|
|
679
|
+
if (rawConfig.userVerification) {
|
|
680
|
+
normalizedConfig.userVerification = rawConfig.userVerification;
|
|
681
|
+
}
|
|
682
|
+
if (rawConfig.webAuthnChallengeCookie) {
|
|
683
|
+
normalizedConfig.webAuthnChallengeCookie = rawConfig.webAuthnChallengeCookie;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return {
|
|
687
|
+
enabled: true,
|
|
688
|
+
normalizedConfig,
|
|
689
|
+
trustedOrigins: finalTrustedOrigins,
|
|
690
|
+
warnings,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function resolveUrls(options: CreateBetterAuthOptions): ResolvedUrls {
|
|
695
|
+
const { config, serverAppUrl, serverBaseUrl, serverEnv } = options;
|
|
696
|
+
const warnings: string[] = [];
|
|
697
|
+
const usesLocalhostDefaults = LOCALHOST_ENVS.includes(serverEnv || '');
|
|
698
|
+
|
|
699
|
+
// Step 1: Resolve baseUrl
|
|
700
|
+
// Priority: betterAuth.baseUrl > serverBaseUrl > localhost default (for local/ci/e2e)
|
|
701
|
+
let baseUrl = config.baseUrl || serverBaseUrl;
|
|
702
|
+
if (!baseUrl && usesLocalhostDefaults) {
|
|
703
|
+
baseUrl = LOCALHOST_DEFAULTS.API_URL;
|
|
704
|
+
warnings.push(`URL: Using localhost default baseUrl="${baseUrl}" (env: '${serverEnv}')`);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Step 2: Resolve appUrl
|
|
708
|
+
// Priority: serverAppUrl > localhost default (when baseUrl is localhost) > derived from baseUrl
|
|
709
|
+
let appUrl = serverAppUrl;
|
|
710
|
+
|
|
711
|
+
// For localhost environments with localhost baseUrl, use localhost app default
|
|
712
|
+
// This handles the common case where API runs on :3000 and App on :3001
|
|
713
|
+
const isBaseUrlLocalhost = baseUrl && (baseUrl.includes('localhost') || baseUrl.includes('127.0.0.1'));
|
|
714
|
+
if (!appUrl && usesLocalhostDefaults && isBaseUrlLocalhost) {
|
|
715
|
+
appUrl = LOCALHOST_DEFAULTS.APP_URL;
|
|
716
|
+
warnings.push(`URL: Using localhost default appUrl="${appUrl}" (env: '${serverEnv}')`);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// For non-localhost environments, try to derive appUrl from baseUrl
|
|
720
|
+
if (!appUrl && baseUrl) {
|
|
721
|
+
appUrl = deriveAppUrlFromBaseUrl(baseUrl);
|
|
722
|
+
if (appUrl !== baseUrl) {
|
|
723
|
+
warnings.push(`URL: Auto-derived appUrl="${appUrl}" from baseUrl="${baseUrl}"`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Step 3: Resolve rpId from appUrl (not baseUrl!)
|
|
728
|
+
// rpId should be the domain where the user authenticates (app domain)
|
|
729
|
+
let rpId: string | undefined;
|
|
730
|
+
if (appUrl) {
|
|
731
|
+
rpId = extractRpIdFromUrl(appUrl);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return { appUrl, baseUrl, rpId, warnings };
|
|
735
|
+
}
|
|
736
|
+
|
|
410
737
|
/**
|
|
411
738
|
* Validates the better-auth configuration and applies fallback secret if needed.
|
|
412
739
|
* Mutates config.secret if fallback is applied.
|
|
@@ -418,8 +745,13 @@ function isValidUrl(url: string): boolean {
|
|
|
418
745
|
*
|
|
419
746
|
* @param config - Better-auth configuration
|
|
420
747
|
* @param fallbackSecrets - Optional array of fallback secrets to try
|
|
748
|
+
* @param passkeyNormalization - Result of passkey normalization (handles graceful degradation)
|
|
421
749
|
*/
|
|
422
|
-
function validateConfig(
|
|
750
|
+
function validateConfig(
|
|
751
|
+
config: IBetterAuth,
|
|
752
|
+
fallbackSecrets?: (string | undefined)[],
|
|
753
|
+
passkeyNormalization?: PasskeyNormalizationResult,
|
|
754
|
+
): ValidationResult {
|
|
423
755
|
const errors: string[] = [];
|
|
424
756
|
const warnings: string[] = [];
|
|
425
757
|
|
|
@@ -472,7 +804,7 @@ function validateConfig(config: IBetterAuth, fallbackSecrets?: (string | undefin
|
|
|
472
804
|
errors.push(`Invalid baseUrl format: ${config.baseUrl}`);
|
|
473
805
|
}
|
|
474
806
|
|
|
475
|
-
// Validate trustedOrigins
|
|
807
|
+
// Validate trustedOrigins (if explicitly configured)
|
|
476
808
|
if (config.trustedOrigins) {
|
|
477
809
|
for (const origin of config.trustedOrigins) {
|
|
478
810
|
if (!isValidUrl(origin)) {
|
|
@@ -481,21 +813,21 @@ function validateConfig(config: IBetterAuth, fallbackSecrets?: (string | undefin
|
|
|
481
813
|
}
|
|
482
814
|
}
|
|
483
815
|
|
|
484
|
-
//
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
'Configure betterAuth.trustedOrigins with your frontend URLs, e.g.: ["http://localhost:3001", "https://app.example.com"]',
|
|
492
|
-
);
|
|
816
|
+
// Validate auto-detected trustedOrigins from passkey normalization
|
|
817
|
+
if (passkeyNormalization?.trustedOrigins) {
|
|
818
|
+
for (const origin of passkeyNormalization.trustedOrigins) {
|
|
819
|
+
if (!isValidUrl(origin)) {
|
|
820
|
+
errors.push(`Invalid auto-detected trustedOrigin format: ${origin}`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
493
823
|
}
|
|
494
824
|
|
|
495
|
-
// Validate passkey origin
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
825
|
+
// Validate passkey origin (only if passkey is enabled after normalization)
|
|
826
|
+
if (passkeyNormalization?.enabled && passkeyNormalization.normalizedConfig) {
|
|
827
|
+
const normalizedOrigin = passkeyNormalization.normalizedConfig.origin;
|
|
828
|
+
if (!isValidUrl(normalizedOrigin)) {
|
|
829
|
+
errors.push(`Invalid passkey origin format: ${normalizedOrigin}`);
|
|
830
|
+
}
|
|
499
831
|
}
|
|
500
832
|
|
|
501
833
|
// Validate social providers dynamically
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* and reduce the need for `as any` casts throughout the codebase.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { Types } from 'mongoose';
|
|
9
|
+
|
|
8
10
|
import { BetterAuthSessionUser } from './core-better-auth-user.mapper';
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -23,6 +25,41 @@ export interface BetterAuth2FAResponse {
|
|
|
23
25
|
user?: BetterAuthSessionUser;
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Authenticated user object created from BetterAuth token verification.
|
|
30
|
+
*
|
|
31
|
+
* This interface represents a user that has been authenticated via BetterAuth
|
|
32
|
+
* (either JWT or session token). It includes the `hasRole` method for
|
|
33
|
+
* role-based access control and the `_authenticatedViaBetterAuth` flag
|
|
34
|
+
* to identify BetterAuth-authenticated users.
|
|
35
|
+
*/
|
|
36
|
+
export interface BetterAuthenticatedUser {
|
|
37
|
+
/** Allow additional properties from MongoDB document */
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
/** Flag indicating this user was authenticated via BetterAuth */
|
|
40
|
+
_authenticatedViaBetterAuth: true;
|
|
41
|
+
/** MongoDB _id field */
|
|
42
|
+
_id?: Types.ObjectId;
|
|
43
|
+
/** User's email address */
|
|
44
|
+
email: string;
|
|
45
|
+
/** Whether user's email is verified */
|
|
46
|
+
emailVerified?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Check if user has any of the specified roles
|
|
49
|
+
* @param roles - Array of role names to check
|
|
50
|
+
* @returns true if user has at least one of the roles
|
|
51
|
+
*/
|
|
52
|
+
hasRole: (roles: string[]) => boolean;
|
|
53
|
+
/** User ID as string */
|
|
54
|
+
id: string;
|
|
55
|
+
/** User's assigned roles */
|
|
56
|
+
roles?: string[];
|
|
57
|
+
/** Whether the user is verified */
|
|
58
|
+
verified?: boolean;
|
|
59
|
+
/** Timestamp when user was verified */
|
|
60
|
+
verifiedAt?: Date;
|
|
61
|
+
}
|
|
62
|
+
|
|
26
63
|
/**
|
|
27
64
|
* Better-Auth session response from getSession API
|
|
28
65
|
*/
|