@lenne.tech/nest-server 11.13.2 → 11.13.4
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 +230 -1
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +2 -0
- package/dist/core/modules/better-auth/better-auth.config.js +40 -1
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.d.ts +1 -0
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +7 -0
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-models.d.ts +1 -0
- package/dist/core/modules/better-auth/core-better-auth-models.js +4 -0
- package/dist/core/modules/better-auth/core-better-auth-models.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +1 -0
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +27 -0
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +14 -16
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.controller.js +14 -6
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.js +44 -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 +3 -2
- 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 +2 -0
- package/dist/core/modules/better-auth/core-better-auth.service.js +12 -4
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core/modules/error-code/error-codes.d.ts +9 -0
- package/dist/core/modules/error-code/error-codes.js +8 -0
- package/dist/core/modules/error-code/error-codes.js.map +1 -1
- package/dist/server/modules/error-code/error-codes.d.ts +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/config.env.ts +259 -2
- package/src/core/common/interfaces/server-options.interface.ts +17 -7
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +6 -0
- package/src/core/modules/better-auth/README.md +32 -0
- package/src/core/modules/better-auth/better-auth.config.ts +63 -2
- package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +16 -0
- package/src/core/modules/better-auth/core-better-auth-models.ts +3 -0
- package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +40 -0
- package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +24 -24
- package/src/core/modules/better-auth/core-better-auth.controller.ts +44 -16
- package/src/core/modules/better-auth/core-better-auth.module.ts +72 -37
- package/src/core/modules/better-auth/core-better-auth.resolver.ts +16 -9
- package/src/core/modules/better-auth/core-better-auth.service.ts +30 -7
- package/src/core/modules/error-code/error-codes.ts +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.13.
|
|
3
|
+
"version": "11.13.4",
|
|
4
4
|
"description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node",
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
"prepack": "npm run prestart:prod",
|
|
55
55
|
"prepublishOnly": "npm run lint && npm run test:ci",
|
|
56
56
|
"preversion": "npm run lint",
|
|
57
|
-
"vitest": "NODE_ENV=
|
|
57
|
+
"vitest": "NODE_ENV=e2e vitest run --config vitest-e2e.config.ts",
|
|
58
58
|
"vitest:ci": "NODE_ENV=ci vitest run --config vitest-e2e.config.ts",
|
|
59
|
-
"vitest:cov": "NODE_ENV=
|
|
60
|
-
"vitest:watch": "NODE_ENV=
|
|
59
|
+
"vitest:cov": "NODE_ENV=e2e vitest run --coverage --config vitest-e2e.config.ts",
|
|
60
|
+
"vitest:watch": "NODE_ENV=e2e vitest --config vitest-e2e.config.ts",
|
|
61
61
|
"vitest:unit": "vitest run --config vitest.config.ts",
|
|
62
62
|
"test:unit:watch": "vitest --config vitest.config.ts",
|
|
63
63
|
"test:types": "tsc --noEmit --skipLibCheck -p tests/types/tsconfig.json",
|
package/src/config.env.ts
CHANGED
|
@@ -11,6 +11,145 @@ import { IServerOptions } from './core/common/interfaces/server-options.interfac
|
|
|
11
11
|
*/
|
|
12
12
|
dotenv.config();
|
|
13
13
|
const config: { [env: string]: IServerOptions } = {
|
|
14
|
+
// ===========================================================================
|
|
15
|
+
// CI environment
|
|
16
|
+
// ===========================================================================
|
|
17
|
+
ci: {
|
|
18
|
+
auth: {
|
|
19
|
+
legacyEndpoints: { enabled: true },
|
|
20
|
+
},
|
|
21
|
+
automaticObjectIdFiltering: true,
|
|
22
|
+
betterAuth: {
|
|
23
|
+
// Email verification disabled for test environment (no real mailbox available)
|
|
24
|
+
emailVerification: false,
|
|
25
|
+
// JWT enabled by default (zero-config)
|
|
26
|
+
jwt: { enabled: true, expiresIn: '15m' },
|
|
27
|
+
// Passkey auto-activated when URLs can be resolved (env: 'local' → localhost defaults)
|
|
28
|
+
passkey: { enabled: true, origin: 'http://localhost:3001', rpId: 'localhost', rpName: 'Nest Server Local' },
|
|
29
|
+
rateLimit: { enabled: true, max: 100, windowSeconds: 60 },
|
|
30
|
+
secret: 'BETTER_AUTH_SECRET_LOCAL_32_CHARS_M',
|
|
31
|
+
// Social providers disabled in local environment (no credentials)
|
|
32
|
+
socialProviders: {
|
|
33
|
+
apple: { clientId: '', clientSecret: '', enabled: false },
|
|
34
|
+
github: { clientId: '', clientSecret: '', enabled: false },
|
|
35
|
+
google: { clientId: '', clientSecret: '', enabled: false },
|
|
36
|
+
},
|
|
37
|
+
// Trusted origins for Passkey (localhost defaults)
|
|
38
|
+
trustedOrigins: ['http://localhost:3000', 'http://localhost:3001'],
|
|
39
|
+
// 2FA enabled for local testing
|
|
40
|
+
twoFactor: { appName: 'Nest Server Local', enabled: true },
|
|
41
|
+
},
|
|
42
|
+
compression: true,
|
|
43
|
+
cookies: false,
|
|
44
|
+
cronJobs: {
|
|
45
|
+
sayHello: {
|
|
46
|
+
cronTime: CronExpression.EVERY_10_SECONDS,
|
|
47
|
+
disabled: false,
|
|
48
|
+
runOnInit: false,
|
|
49
|
+
runParallel: 1,
|
|
50
|
+
throwException: false,
|
|
51
|
+
timeZone: 'Europe/Berlin',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
email: {
|
|
55
|
+
defaultSender: {
|
|
56
|
+
email: 'oren.satterfield@ethereal.email',
|
|
57
|
+
name: 'Nest Server Local',
|
|
58
|
+
},
|
|
59
|
+
mailjet: {
|
|
60
|
+
api_key_private: 'MAILJET_API_KEY_PRIVATE',
|
|
61
|
+
api_key_public: 'MAILJET_API_KEY_PUBLIC',
|
|
62
|
+
},
|
|
63
|
+
passwordResetLink: 'http://localhost:4200/user/password-reset',
|
|
64
|
+
smtp: {
|
|
65
|
+
auth: {
|
|
66
|
+
pass: 'K4DvD8U31VKseT7vQC',
|
|
67
|
+
user: 'oren.satterfield@ethereal.email',
|
|
68
|
+
},
|
|
69
|
+
host: 'mailhog.lenne.tech',
|
|
70
|
+
port: 1025,
|
|
71
|
+
secure: false,
|
|
72
|
+
},
|
|
73
|
+
verificationLink: 'http://localhost:4200/user/verification',
|
|
74
|
+
},
|
|
75
|
+
env: 'ci',
|
|
76
|
+
// Disable auto-registration to allow Server ErrorCodeModule with SRV_* codes
|
|
77
|
+
errorCode: {
|
|
78
|
+
autoRegister: false,
|
|
79
|
+
},
|
|
80
|
+
execAfterInit: 'npm run docs:bootstrap',
|
|
81
|
+
filter: {
|
|
82
|
+
maxLimit: null,
|
|
83
|
+
},
|
|
84
|
+
graphQl: {
|
|
85
|
+
driver: {
|
|
86
|
+
introspection: true,
|
|
87
|
+
},
|
|
88
|
+
maxComplexity: 1000,
|
|
89
|
+
},
|
|
90
|
+
healthCheck: {
|
|
91
|
+
configs: {
|
|
92
|
+
database: {
|
|
93
|
+
enabled: true,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
enabled: true,
|
|
97
|
+
},
|
|
98
|
+
hostname: '127.0.0.1',
|
|
99
|
+
ignoreSelectionsForPopulate: true,
|
|
100
|
+
jwt: {
|
|
101
|
+
// Each secret should be unique and not reused in other environments,
|
|
102
|
+
// also the JWT secret should be different from the Refresh secret!
|
|
103
|
+
// crypto.randomBytes(512).toString('base64') (see https://nodejs.org/api/crypto.html#crypto)
|
|
104
|
+
refresh: {
|
|
105
|
+
renewal: true,
|
|
106
|
+
// Each secret should be unique and not reused in other environments,
|
|
107
|
+
// also the JWT secret should be different from the Refresh secret!
|
|
108
|
+
// crypto.randomBytes(512).toString('base64') (see https://nodejs.org/api/crypto.html#crypto)
|
|
109
|
+
secret: 'SECRET_OR_PRIVATE_KEY_LOCAL_REFRESH',
|
|
110
|
+
signInOptions: {
|
|
111
|
+
expiresIn: '7d',
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
sameTokenIdPeriod: 2000,
|
|
115
|
+
secret: 'SECRET_OR_PRIVATE_KEY_LOCAL',
|
|
116
|
+
signInOptions: {
|
|
117
|
+
expiresIn: '15m',
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
loadLocalConfig: true,
|
|
121
|
+
logExceptions: true,
|
|
122
|
+
mongoose: {
|
|
123
|
+
collation: {
|
|
124
|
+
locale: 'de',
|
|
125
|
+
},
|
|
126
|
+
modelDocumentation: true,
|
|
127
|
+
uri: 'mongodb://127.0.0.1/nest-server-ci',
|
|
128
|
+
},
|
|
129
|
+
port: 3000,
|
|
130
|
+
security: {
|
|
131
|
+
checkResponseInterceptor: {
|
|
132
|
+
checkObjectItself: false,
|
|
133
|
+
debug: false,
|
|
134
|
+
ignoreUndefined: true,
|
|
135
|
+
mergeRoles: true,
|
|
136
|
+
removeUndefinedFromResultArray: true,
|
|
137
|
+
throwError: false,
|
|
138
|
+
},
|
|
139
|
+
checkSecurityInterceptor: true,
|
|
140
|
+
mapAndValidatePipe: true,
|
|
141
|
+
},
|
|
142
|
+
sha256: true,
|
|
143
|
+
staticAssets: {
|
|
144
|
+
options: { prefix: '' },
|
|
145
|
+
path: join(__dirname, '..', 'public'),
|
|
146
|
+
},
|
|
147
|
+
templates: {
|
|
148
|
+
engine: 'ejs',
|
|
149
|
+
path: join(__dirname, 'templates'),
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
|
|
14
153
|
// ===========================================================================
|
|
15
154
|
// Development environment
|
|
16
155
|
// ===========================================================================
|
|
@@ -117,9 +256,9 @@ const config: { [env: string]: IServerOptions } = {
|
|
|
117
256
|
},
|
|
118
257
|
|
|
119
258
|
// ===========================================================================
|
|
120
|
-
//
|
|
259
|
+
// E2E environment
|
|
121
260
|
// ===========================================================================
|
|
122
|
-
|
|
261
|
+
e2e: {
|
|
123
262
|
auth: {
|
|
124
263
|
legacyEndpoints: { enabled: true },
|
|
125
264
|
},
|
|
@@ -177,6 +316,124 @@ const config: { [env: string]: IServerOptions } = {
|
|
|
177
316
|
},
|
|
178
317
|
verificationLink: 'http://localhost:4200/user/verification',
|
|
179
318
|
},
|
|
319
|
+
env: 'e2e',
|
|
320
|
+
// Disable auto-registration to allow Server ErrorCodeModule with SRV_* codes
|
|
321
|
+
errorCode: {
|
|
322
|
+
autoRegister: false,
|
|
323
|
+
},
|
|
324
|
+
execAfterInit: 'npm run docs:bootstrap',
|
|
325
|
+
filter: {
|
|
326
|
+
maxLimit: null,
|
|
327
|
+
},
|
|
328
|
+
graphQl: {
|
|
329
|
+
driver: {
|
|
330
|
+
introspection: true,
|
|
331
|
+
},
|
|
332
|
+
maxComplexity: 1000,
|
|
333
|
+
},
|
|
334
|
+
healthCheck: {
|
|
335
|
+
configs: {
|
|
336
|
+
database: {
|
|
337
|
+
enabled: true,
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
enabled: true,
|
|
341
|
+
},
|
|
342
|
+
hostname: '127.0.0.1',
|
|
343
|
+
ignoreSelectionsForPopulate: true,
|
|
344
|
+
jwt: {
|
|
345
|
+
// Each secret should be unique and not reused in other environments,
|
|
346
|
+
// also the JWT secret should be different from the Refresh secret!
|
|
347
|
+
// crypto.randomBytes(512).toString('base64') (see https://nodejs.org/api/crypto.html#crypto)
|
|
348
|
+
refresh: {
|
|
349
|
+
renewal: true,
|
|
350
|
+
// Each secret should be unique and not reused in other environments,
|
|
351
|
+
// also the JWT secret should be different from the Refresh secret!
|
|
352
|
+
// crypto.randomBytes(512).toString('base64') (see https://nodejs.org/api/crypto.html#crypto)
|
|
353
|
+
secret: 'SECRET_OR_PRIVATE_KEY_LOCAL_REFRESH',
|
|
354
|
+
signInOptions: {
|
|
355
|
+
expiresIn: '7d',
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
sameTokenIdPeriod: 2000,
|
|
359
|
+
secret: 'SECRET_OR_PRIVATE_KEY_LOCAL',
|
|
360
|
+
signInOptions: {
|
|
361
|
+
expiresIn: '15m',
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
loadLocalConfig: true,
|
|
365
|
+
logExceptions: true,
|
|
366
|
+
mongoose: {
|
|
367
|
+
collation: {
|
|
368
|
+
locale: 'de',
|
|
369
|
+
},
|
|
370
|
+
modelDocumentation: true,
|
|
371
|
+
uri: 'mongodb://127.0.0.1/nest-server-e2e',
|
|
372
|
+
},
|
|
373
|
+
port: 3000,
|
|
374
|
+
security: {
|
|
375
|
+
checkResponseInterceptor: {
|
|
376
|
+
checkObjectItself: false,
|
|
377
|
+
debug: false,
|
|
378
|
+
ignoreUndefined: true,
|
|
379
|
+
mergeRoles: true,
|
|
380
|
+
removeUndefinedFromResultArray: true,
|
|
381
|
+
throwError: false,
|
|
382
|
+
},
|
|
383
|
+
checkSecurityInterceptor: true,
|
|
384
|
+
mapAndValidatePipe: true,
|
|
385
|
+
},
|
|
386
|
+
sha256: true,
|
|
387
|
+
staticAssets: {
|
|
388
|
+
options: { prefix: '' },
|
|
389
|
+
path: join(__dirname, '..', 'public'),
|
|
390
|
+
},
|
|
391
|
+
templates: {
|
|
392
|
+
engine: 'ejs',
|
|
393
|
+
path: join(__dirname, 'templates'),
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
// ===========================================================================
|
|
398
|
+
// Local environment (env: 'local' → auto URLs + Passkey)
|
|
399
|
+
// ===========================================================================
|
|
400
|
+
local: {
|
|
401
|
+
auth: {
|
|
402
|
+
legacyEndpoints: { enabled: true },
|
|
403
|
+
},
|
|
404
|
+
automaticObjectIdFiltering: true,
|
|
405
|
+
compression: true,
|
|
406
|
+
cronJobs: {
|
|
407
|
+
sayHello: {
|
|
408
|
+
cronTime: CronExpression.EVERY_10_SECONDS,
|
|
409
|
+
disabled: false,
|
|
410
|
+
runOnInit: false,
|
|
411
|
+
runParallel: 1,
|
|
412
|
+
throwException: false,
|
|
413
|
+
timeZone: 'Europe/Berlin',
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
email: {
|
|
417
|
+
defaultSender: {
|
|
418
|
+
email: 'oren.satterfield@ethereal.email',
|
|
419
|
+
name: 'Nest Server Local',
|
|
420
|
+
},
|
|
421
|
+
mailjet: {
|
|
422
|
+
api_key_private: 'MAILJET_API_KEY_PRIVATE',
|
|
423
|
+
api_key_public: 'MAILJET_API_KEY_PUBLIC',
|
|
424
|
+
},
|
|
425
|
+
passwordResetLink: 'http://localhost:4200/user/password-reset',
|
|
426
|
+
smtp: {
|
|
427
|
+
auth: {
|
|
428
|
+
pass: 'K4DvD8U31VKseT7vQC',
|
|
429
|
+
user: 'oren.satterfield@ethereal.email',
|
|
430
|
+
},
|
|
431
|
+
host: 'mailhog.lenne.tech',
|
|
432
|
+
port: 1025,
|
|
433
|
+
secure: false,
|
|
434
|
+
},
|
|
435
|
+
verificationLink: 'http://localhost:4200/user/verification',
|
|
436
|
+
},
|
|
180
437
|
env: 'local',
|
|
181
438
|
// Disable auto-registration to allow Server ErrorCodeModule with SRV_* codes
|
|
182
439
|
errorCode: {
|
|
@@ -524,6 +524,13 @@ export interface IBetterAuthRateLimit {
|
|
|
524
524
|
*/
|
|
525
525
|
max?: number;
|
|
526
526
|
|
|
527
|
+
/**
|
|
528
|
+
* Maximum number of entries in the in-memory rate limit store.
|
|
529
|
+
* When exceeded, the oldest entries are evicted to prevent unbounded memory growth.
|
|
530
|
+
* @default 10000
|
|
531
|
+
*/
|
|
532
|
+
maxEntries?: number;
|
|
533
|
+
|
|
527
534
|
/**
|
|
528
535
|
* Custom message when rate limit is exceeded
|
|
529
536
|
* default: 'Too many requests, please try again later.'
|
|
@@ -1686,6 +1693,14 @@ interface IBetterAuthBase {
|
|
|
1686
1693
|
* Set `enabled: false` to explicitly disable email/password auth.
|
|
1687
1694
|
*/
|
|
1688
1695
|
emailAndPassword?: {
|
|
1696
|
+
/**
|
|
1697
|
+
* Disable user registration (sign-up) via BetterAuth.
|
|
1698
|
+
* Passed through to better-auth's native emailAndPassword.disableSignUp.
|
|
1699
|
+
* Custom endpoints (GraphQL + REST) also check this flag early.
|
|
1700
|
+
* @default false
|
|
1701
|
+
*/
|
|
1702
|
+
disableSignUp?: boolean;
|
|
1703
|
+
|
|
1689
1704
|
/**
|
|
1690
1705
|
* Whether email/password authentication is enabled.
|
|
1691
1706
|
* @default true
|
|
@@ -1986,10 +2001,7 @@ interface IBetterAuthBase {
|
|
|
1986
2001
|
* };
|
|
1987
2002
|
* ```
|
|
1988
2003
|
*/
|
|
1989
|
-
type IBetterAuthPasskeyDisabled =
|
|
1990
|
-
| false
|
|
1991
|
-
| (Omit<IBetterAuthPasskeyConfig, 'enabled'> & { enabled: false })
|
|
1992
|
-
| undefined;
|
|
2004
|
+
type IBetterAuthPasskeyDisabled = false | (Omit<IBetterAuthPasskeyConfig, 'enabled'> & { enabled: false }) | undefined;
|
|
1993
2005
|
|
|
1994
2006
|
/**
|
|
1995
2007
|
* Passkey configuration that is considered "enabled".
|
|
@@ -1999,9 +2011,7 @@ type IBetterAuthPasskeyDisabled =
|
|
|
1999
2011
|
* - `{ enabled: true, ... }` (explicit enabled)
|
|
2000
2012
|
* - `{ rpName: 'My App', ... }` (config without explicit enabled = defaults to true)
|
|
2001
2013
|
*/
|
|
2002
|
-
type IBetterAuthPasskeyEnabled =
|
|
2003
|
-
| (Omit<IBetterAuthPasskeyConfig, 'enabled'> & { enabled?: true })
|
|
2004
|
-
| true;
|
|
2014
|
+
type IBetterAuthPasskeyEnabled = (Omit<IBetterAuthPasskeyConfig, 'enabled'> & { enabled?: true }) | true;
|
|
2005
2015
|
|
|
2006
2016
|
/**
|
|
2007
2017
|
* BetterAuth configuration WITHOUT Passkey (or Passkey disabled).
|
|
@@ -313,6 +313,12 @@ After integration, verify:
|
|
|
313
313
|
- [ ] Passkey login redirects to dashboard after successful authentication
|
|
314
314
|
- [ ] Passkey can be registered, listed, and deleted from security settings
|
|
315
315
|
|
|
316
|
+
### Optional: Disable Sign-Up (`emailAndPassword.disableSignUp: true`)
|
|
317
|
+
- [ ] REST `POST /iam/sign-up/email` returns `400` with error `LTNS_0026`
|
|
318
|
+
- [ ] GraphQL `betterAuthSignUp` returns error `LTNS_0026`
|
|
319
|
+
- [ ] `GET /iam/features` reports `signUpEnabled: false`
|
|
320
|
+
- [ ] Sign-in still works for existing users
|
|
321
|
+
|
|
316
322
|
### Additional checks for Migration scenario:
|
|
317
323
|
- [ ] Sign-in via Legacy Auth works for BetterAuth-created users
|
|
318
324
|
- [ ] Sign-in via BetterAuth works for Legacy-created users
|
|
@@ -574,6 +574,32 @@ const config = {
|
|
|
574
574
|
};
|
|
575
575
|
```
|
|
576
576
|
|
|
577
|
+
### Disable Sign-Up
|
|
578
|
+
|
|
579
|
+
Disable user registration while keeping sign-in active (e.g., invite-only apps, admin-created accounts):
|
|
580
|
+
|
|
581
|
+
```typescript
|
|
582
|
+
const config = {
|
|
583
|
+
betterAuth: {
|
|
584
|
+
emailAndPassword: {
|
|
585
|
+
disableSignUp: true, // Block new registrations (REST + GraphQL)
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
When disabled:
|
|
592
|
+
- REST `POST /iam/sign-up/email` returns `400 Bad Request` with error `LTNS_0026`
|
|
593
|
+
- GraphQL `betterAuthSignUp` mutation returns error `LTNS_0026`
|
|
594
|
+
- `betterAuthFeatures` reports `signUpEnabled: false`
|
|
595
|
+
- Sign-in continues to work for existing users
|
|
596
|
+
|
|
597
|
+
**Defense in Depth:** The flag is enforced at two layers:
|
|
598
|
+
1. **Custom check** (`CoreBetterAuthService.ensureSignUpEnabled()`) runs in Controller/Resolver *before* any BetterAuth API call and returns a structured `LTNS_0026` error.
|
|
599
|
+
2. **Native BetterAuth** `emailAndPassword.disableSignUp` acts as a safety net for any direct API access that bypasses the custom check.
|
|
600
|
+
|
|
601
|
+
**Default:** `false` (sign-up enabled) - fully backward compatible.
|
|
602
|
+
|
|
577
603
|
### Additional User Fields
|
|
578
604
|
|
|
579
605
|
Add custom fields to the Better-Auth user schema:
|
|
@@ -1049,6 +1075,7 @@ type CoreBetterAuthFeaturesModel {
|
|
|
1049
1075
|
jwt: Boolean!
|
|
1050
1076
|
twoFactor: Boolean!
|
|
1051
1077
|
passkey: Boolean!
|
|
1078
|
+
signUpEnabled: Boolean!
|
|
1052
1079
|
socialProviders: [String!]!
|
|
1053
1080
|
}
|
|
1054
1081
|
```
|
|
@@ -1100,6 +1127,7 @@ query {
|
|
|
1100
1127
|
jwt
|
|
1101
1128
|
twoFactor
|
|
1102
1129
|
passkey
|
|
1130
|
+
signUpEnabled
|
|
1103
1131
|
socialProviders
|
|
1104
1132
|
}
|
|
1105
1133
|
}
|
|
@@ -1247,6 +1275,7 @@ export class MyService {
|
|
|
1247
1275
|
| `isJwtEnabled()` | Check if JWT plugin is enabled |
|
|
1248
1276
|
| `isTwoFactorEnabled()` | Check if 2FA is enabled |
|
|
1249
1277
|
| `isPasskeyEnabled()` | Check if Passkey is enabled |
|
|
1278
|
+
| `isSignUpEnabled()` | Check if sign-up is enabled |
|
|
1250
1279
|
| `getEnabledSocialProviders()` | Get list of enabled social providers |
|
|
1251
1280
|
| `getBasePath()` | Get the base path for endpoints |
|
|
1252
1281
|
| `getBaseUrl()` | Get the base URL |
|
|
@@ -1970,6 +1999,9 @@ These protected methods are available for use in your custom resolver:
|
|
|
1970
1999
|
// Check if Better-Auth is enabled (throws if not)
|
|
1971
2000
|
this.ensureEnabled();
|
|
1972
2001
|
|
|
2002
|
+
// Check if sign-up is enabled (throws if not) - delegated to service
|
|
2003
|
+
this.betterAuthService.ensureSignUpEnabled();
|
|
2004
|
+
|
|
1973
2005
|
// Convert Express headers to Web API Headers
|
|
1974
2006
|
const headers = this.convertHeaders(ctx.req.headers);
|
|
1975
2007
|
|
|
@@ -14,6 +14,21 @@ import { IBetterAuth } from '../../common/interfaces/server-options.interface';
|
|
|
14
14
|
*/
|
|
15
15
|
export type BetterAuthInstance = ReturnType<typeof betterAuth>;
|
|
16
16
|
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Performance-optimized password hashing using Node.js native crypto.scrypt
|
|
19
|
+
//
|
|
20
|
+
// Better-Auth's default uses @noble/hashes scrypt which runs on the main
|
|
21
|
+
// event loop. Under concurrent load this blocks all requests while hashing.
|
|
22
|
+
// Node.js crypto.scrypt() offloads the work to the libuv thread pool,
|
|
23
|
+
// allowing the event loop to remain responsive.
|
|
24
|
+
//
|
|
25
|
+
// Parameters match Better-Auth's defaults exactly:
|
|
26
|
+
// N=16384, r=16, p=1, dkLen=64, 16-byte salt
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
const SCRYPT_PARAMS = { maxmem: 128 * 16384 * 16 * 2, N: 16384, p: 1, r: 16 };
|
|
30
|
+
const SCRYPT_KEY_LENGTH = 64;
|
|
31
|
+
|
|
17
32
|
/**
|
|
18
33
|
* Generates a cryptographically secure random secret.
|
|
19
34
|
* Used as fallback when no BETTER_AUTH_SECRET is configured.
|
|
@@ -27,6 +42,38 @@ function generateSecureSecret(): string {
|
|
|
27
42
|
return crypto.randomBytes(32).toString('base64');
|
|
28
43
|
}
|
|
29
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Hash a password using Node.js native crypto.scrypt (libuv thread pool).
|
|
47
|
+
* Output format matches Better-Auth: "salt:hash" (both hex encoded).
|
|
48
|
+
*/
|
|
49
|
+
async function nativeScryptHash(password: string): Promise<string> {
|
|
50
|
+
const salt = crypto.randomBytes(16).toString('hex');
|
|
51
|
+
const normalized = password.normalize('NFKC');
|
|
52
|
+
const key = await new Promise<Buffer>((resolve, reject) => {
|
|
53
|
+
crypto.scrypt(normalized, salt, SCRYPT_KEY_LENGTH, SCRYPT_PARAMS, (err, derivedKey) => {
|
|
54
|
+
if (err) reject(err);
|
|
55
|
+
else resolve(derivedKey);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
return `${salt}:${key.toString('hex')}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Verify a password against a Better-Auth scrypt hash using Node.js native crypto.scrypt.
|
|
63
|
+
*/
|
|
64
|
+
async function nativeScryptVerify(data: { hash: string; password: string }): Promise<boolean> {
|
|
65
|
+
const [salt, storedKey] = data.hash.split(':');
|
|
66
|
+
if (!salt || !storedKey) return false;
|
|
67
|
+
const normalized = data.password.normalize('NFKC');
|
|
68
|
+
const key = await new Promise<Buffer>((resolve, reject) => {
|
|
69
|
+
crypto.scrypt(normalized, salt, SCRYPT_KEY_LENGTH, SCRYPT_PARAMS, (err, derivedKey) => {
|
|
70
|
+
if (err) reject(err);
|
|
71
|
+
else resolve(derivedKey);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
return crypto.timingSafeEqual(key, Buffer.from(storedKey, 'hex'));
|
|
75
|
+
}
|
|
76
|
+
|
|
30
77
|
/**
|
|
31
78
|
* Cached auto-generated secret for the current server instance.
|
|
32
79
|
* Generated once at a module load to ensure consistency within a single run.
|
|
@@ -267,7 +314,17 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Bett
|
|
|
267
314
|
// Enable email/password authentication by default (required by Better-Auth 1.x)
|
|
268
315
|
// Can be disabled by setting config.emailAndPassword.enabled = false
|
|
269
316
|
emailAndPassword: {
|
|
317
|
+
// Defense in Depth: This native Better-Auth flag is the second layer.
|
|
318
|
+
// The first layer is CoreBetterAuthService.ensureSignUpEnabled() which
|
|
319
|
+
// runs in Controller/Resolver BEFORE the BetterAuth API is called and
|
|
320
|
+
// returns a structured LTNS_0026 error. The native flag acts as a safety
|
|
321
|
+
// net in case the custom check is bypassed (e.g., direct API calls).
|
|
322
|
+
disableSignUp: config.emailAndPassword?.disableSignUp === true,
|
|
270
323
|
enabled: config.emailAndPassword?.enabled !== false,
|
|
324
|
+
password: {
|
|
325
|
+
hash: nativeScryptHash,
|
|
326
|
+
verify: nativeScryptVerify,
|
|
327
|
+
},
|
|
271
328
|
},
|
|
272
329
|
plugins,
|
|
273
330
|
secret: validation.resolvedSecret || config.secret,
|
|
@@ -375,7 +432,7 @@ function buildEmailVerificationConfig(
|
|
|
375
432
|
_request?: Request,
|
|
376
433
|
) => {
|
|
377
434
|
// Don't await to prevent timing attacks (as recommended by Better-Auth docs)
|
|
378
|
-
|
|
435
|
+
|
|
379
436
|
sendVerificationEmail(data);
|
|
380
437
|
};
|
|
381
438
|
}
|
|
@@ -867,7 +924,11 @@ function normalizePasskeyConfig(
|
|
|
867
924
|
// Resolve values: explicit config > resolved URLs
|
|
868
925
|
const finalRpId = rawConfig.rpId || resolvedUrls.rpId;
|
|
869
926
|
const finalOrigin = rawConfig.origin || resolvedUrls.appUrl;
|
|
870
|
-
const finalTrustedOrigins = config.trustedOrigins?.length
|
|
927
|
+
const finalTrustedOrigins = config.trustedOrigins?.length
|
|
928
|
+
? config.trustedOrigins
|
|
929
|
+
: resolvedUrls.appUrl
|
|
930
|
+
? [resolvedUrls.appUrl]
|
|
931
|
+
: undefined;
|
|
871
932
|
|
|
872
933
|
// Check if we have all required values for Passkey
|
|
873
934
|
const hasRequiredConfig = finalRpId && finalOrigin && finalTrustedOrigins?.length;
|
|
@@ -409,11 +409,27 @@ export class CoreBetterAuthEmailVerificationService {
|
|
|
409
409
|
return elapsed < cooldown;
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Maximum entries in the lastSendTimes map to prevent unbounded growth.
|
|
414
|
+
* At 10,000 entries with email strings as keys, this uses ~1-2 MB max.
|
|
415
|
+
*/
|
|
416
|
+
private static readonly MAX_SEND_TIMES_ENTRIES = 10000;
|
|
417
|
+
|
|
412
418
|
/**
|
|
413
419
|
* Track that a verification email was sent to this address
|
|
414
420
|
*/
|
|
415
421
|
protected trackSend(email: string): void {
|
|
416
422
|
const key = email.toLowerCase();
|
|
423
|
+
|
|
424
|
+
// Evict oldest entry if map is at capacity (before adding new one)
|
|
425
|
+
if (!this.lastSendTimes.has(key) && this.lastSendTimes.size >= CoreBetterAuthEmailVerificationService.MAX_SEND_TIMES_ENTRIES) {
|
|
426
|
+
// Map preserves insertion order - first key is the oldest
|
|
427
|
+
const oldestKey = this.lastSendTimes.keys().next().value;
|
|
428
|
+
if (oldestKey) {
|
|
429
|
+
this.lastSendTimes.delete(oldestKey);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
417
433
|
this.lastSendTimes.set(key, Date.now());
|
|
418
434
|
|
|
419
435
|
// Schedule cleanup to prevent memory leak
|
|
@@ -135,6 +135,9 @@ export class CoreBetterAuthFeaturesModel {
|
|
|
135
135
|
@Field(() => Boolean, { description: 'Whether Passkey is enabled' })
|
|
136
136
|
passkey: boolean;
|
|
137
137
|
|
|
138
|
+
@Field(() => Boolean, { description: 'Whether sign-up is enabled' })
|
|
139
|
+
signUpEnabled: boolean;
|
|
140
|
+
|
|
138
141
|
@Field(() => [String], { description: 'List of enabled social providers' })
|
|
139
142
|
socialProviders: string[];
|
|
140
143
|
|
|
@@ -46,6 +46,7 @@ interface RateLimitEntry {
|
|
|
46
46
|
const DEFAULT_CONFIG: Required<IBetterAuthRateLimit> = {
|
|
47
47
|
enabled: false,
|
|
48
48
|
max: 10,
|
|
49
|
+
maxEntries: 10000,
|
|
49
50
|
message: 'Too many requests, please try again later.',
|
|
50
51
|
skipEndpoints: ['/session', '/callback'],
|
|
51
52
|
strictEndpoints: ['/sign-in', '/sign-up', '/forgot-password', '/reset-password'],
|
|
@@ -143,6 +144,11 @@ export class CoreBetterAuthRateLimiter {
|
|
|
143
144
|
let entry = this.store.get(key);
|
|
144
145
|
|
|
145
146
|
if (!entry || now >= entry.resetTime) {
|
|
147
|
+
// Evict oldest entries if store exceeds maxEntries
|
|
148
|
+
if (!entry && this.store.size >= this.config.maxEntries) {
|
|
149
|
+
this.evictOldest();
|
|
150
|
+
}
|
|
151
|
+
|
|
146
152
|
// Create new entry or reset expired one
|
|
147
153
|
entry = {
|
|
148
154
|
count: 1,
|
|
@@ -234,6 +240,40 @@ export class CoreBetterAuthRateLimiter {
|
|
|
234
240
|
}
|
|
235
241
|
}
|
|
236
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Evict the oldest entries when the store exceeds maxEntries.
|
|
245
|
+
* First removes all expired entries, then removes entries closest to expiry
|
|
246
|
+
* until the store is at 90% capacity.
|
|
247
|
+
*/
|
|
248
|
+
private evictOldest(): void {
|
|
249
|
+
const now = Date.now();
|
|
250
|
+
let evicted = 0;
|
|
251
|
+
|
|
252
|
+
// First pass: remove all expired entries
|
|
253
|
+
for (const [key, entry] of this.store.entries()) {
|
|
254
|
+
if (now >= entry.resetTime) {
|
|
255
|
+
this.store.delete(key);
|
|
256
|
+
evicted++;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// If still over limit, remove entries with earliest resetTime (oldest)
|
|
261
|
+
if (this.store.size >= this.config.maxEntries) {
|
|
262
|
+
const targetSize = Math.floor(this.config.maxEntries * 0.9);
|
|
263
|
+
const entries = [...this.store.entries()].sort((a, b) => a[1].resetTime - b[1].resetTime);
|
|
264
|
+
|
|
265
|
+
for (const [key] of entries) {
|
|
266
|
+
if (this.store.size <= targetSize) break;
|
|
267
|
+
this.store.delete(key);
|
|
268
|
+
evicted++;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (evicted > 0) {
|
|
273
|
+
this.logger.warn(`Evicted ${evicted} rate limit entries (store was at capacity: ${this.config.maxEntries})`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
237
277
|
/**
|
|
238
278
|
* Determine if an endpoint should skip rate limiting
|
|
239
279
|
*/
|