@lenne.tech/nest-server 11.36.4 → 11.37.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.
- package/.claude/rules/better-auth.md +32 -0
- package/.claude/rules/configurable-features.md +1 -1
- package/.claude/rules/framework-compatibility.md +1 -0
- package/.claude/rules/testing.md +4 -4
- package/.claude/rules/versioning.md +6 -0
- package/CLAUDE.md +22 -5
- package/FRAMEWORK-API.md +1 -1
- package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
- package/dist/core/modules/auth/guards/roles.guard.d.ts +1 -0
- package/dist/core/modules/auth/guards/roles.guard.js +33 -0
- package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +2 -0
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.constants.d.ts +4 -0
- package/dist/core/modules/better-auth/core-better-auth.constants.js +5 -1
- package/dist/core/modules/better-auth/core-better-auth.constants.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 +68 -0
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core/modules/error-code/error-codes.d.ts +3 -3
- package/dist/core/modules/error-code/error-codes.js +3 -3
- package/dist/core/modules/error-code/error-codes.js.map +1 -1
- package/dist/core/modules/system-setup/core-system-setup.controller.js +4 -1
- package/dist/core/modules/system-setup/core-system-setup.controller.js.map +1 -1
- package/dist/core/modules/system-setup/core-system-setup.service.js +9 -1
- package/dist/core/modules/system-setup/core-system-setup.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +25 -1
- package/migration-guides/11.32.x-to-11.33.x.md +37 -4
- package/migration-guides/11.36.4-to-11.36.5.md +184 -0
- package/migration-guides/11.36.x-to-11.37.0.md +344 -0
- package/package.json +17 -4
- package/src/core/modules/ai/core-ai-mcp.controller.ts +9 -3
- package/src/core/modules/auth/guards/roles.guard.ts +68 -0
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +24 -1
- package/src/core/modules/better-auth/README.md +64 -0
- package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +7 -0
- package/src/core/modules/better-auth/core-better-auth.constants.ts +26 -0
- package/src/core/modules/better-auth/core-better-auth.service.ts +179 -3
- package/src/core/modules/error-code/error-codes.ts +13 -3
- package/src/core/modules/system-setup/INTEGRATION-CHECKLIST.md +8 -7
- package/src/core/modules/system-setup/README.md +43 -9
- package/src/core/modules/system-setup/core-system-setup.controller.ts +4 -1
- package/src/core/modules/system-setup/core-system-setup.service.ts +54 -5
|
@@ -94,7 +94,7 @@ Creates the initial admin user. Only works when zero users exist.
|
|
|
94
94
|
|
|
95
95
|
```json
|
|
96
96
|
{
|
|
97
|
-
"message": "LTNS_0050: System setup not available - users already exist"
|
|
97
|
+
"message": "LTNS_0050: System setup not available - users already exist, or the initial-admin setup is claimed"
|
|
98
98
|
}
|
|
99
99
|
```
|
|
100
100
|
|
|
@@ -169,6 +169,9 @@ NEST_SERVER_CONFIG='{ "systemSetup": { "initialAdmin": { "email": "admin@example
|
|
|
169
169
|
instance claims the setup by upserting the marker `{ _id: 'initial-admin' }` into the
|
|
170
170
|
`system-setup-locks` collection — an atomic operation only one instance wins. The others log a
|
|
171
171
|
debug message and skip. If creation fails, the marker is removed again so a later boot can retry.
|
|
172
|
+
After a SUCCESSFUL setup the marker REMAINS — setup must never run twice in a deployment. Test
|
|
173
|
+
suites that reset the database to replay the setup flow therefore have to drop `system-setup-locks`
|
|
174
|
+
alongside `users`; clearing only `users` leaves setup refused until the claim goes stale (5 min).
|
|
172
175
|
|
|
173
176
|
### Security Best Practices
|
|
174
177
|
|
|
@@ -181,7 +184,9 @@ NEST_SERVER_CONFIG='{ "systemSetup": { "initialAdmin": { "email": "admin@example
|
|
|
181
184
|
|
|
182
185
|
## Security
|
|
183
186
|
|
|
184
|
-
1. **Zero-user guard** - Init
|
|
187
|
+
1. **Zero-user guard** - Init requires `countDocuments({}) === 0`. Necessary but not sufficient:
|
|
188
|
+
the atomic claim below must also be winnable, so an empty `users` collection alone does not
|
|
189
|
+
re-open setup
|
|
185
190
|
2. **Enabled by default** - Safe because endpoints are permanently locked once any user exists
|
|
186
191
|
3. **Race condition protection** - MongoDB unique email index prevents duplicates; auto-creation on
|
|
187
192
|
bootstrap is additionally claimed atomically via the `system-setup-locks` marker
|
|
@@ -226,13 +231,42 @@ if (needsSetup) {
|
|
|
226
231
|
|
|
227
232
|
### Init returns 403 "System setup not available"
|
|
228
233
|
|
|
229
|
-
**
|
|
234
|
+
**Two different guards raise this, and the response cannot tell you which.**
|
|
230
235
|
|
|
231
|
-
**
|
|
236
|
+
**Cause 1 - users already exist.** The ordinary case on a deployment that is already set up.
|
|
232
237
|
|
|
233
238
|
1. Check `GET /system-setup/status` - `needsSetup` should be `true`
|
|
234
239
|
2. If this is a fresh deployment, verify the database is empty
|
|
235
240
|
|
|
241
|
+
**Cause 2 - the initial-admin setup is claimed.** The marker `{ _id: 'initial-admin' }` in
|
|
242
|
+
`system-setup-locks` is held. Either another instance is running the setup right now, or a previous
|
|
243
|
+
SUCCESSFUL setup left it behind: it is removed only when creation FAILS.
|
|
244
|
+
|
|
245
|
+
This is the one that costs time, because the obvious checks all point the other way. `needsSetup` is
|
|
246
|
+
computed from the **user count alone**, so it reports `true` while init is refused - step 1 above
|
|
247
|
+
confirms a state that is not the problem, and step 2 confirms an empty database that is genuinely
|
|
248
|
+
empty.
|
|
249
|
+
|
|
250
|
+
1. Look at the server log. Since 11.36.5 the service logs a warning naming the collection, the
|
|
251
|
+
release-on-failure semantics and the stale window.
|
|
252
|
+
2. Check the collection directly: `db.getCollection('system-setup-locks').find()`
|
|
253
|
+
3. If you reset the database to replay the setup, drop `system-setup-locks` alongside `users` -
|
|
254
|
+
see **Resetting for tests** below. Otherwise wait for the claim to go stale (5 minutes after it
|
|
255
|
+
was taken).
|
|
256
|
+
|
|
257
|
+
### Resetting for tests
|
|
258
|
+
|
|
259
|
+
Emptying `users` is not enough - the claim outlives a successful setup. The framework's own reset,
|
|
260
|
+
executed on every test run in `tests/stories/system-setup.e2e-spec.ts`:
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
// deleteMany, not drop: dropping `users` would drop its unique email index,
|
|
264
|
+
// which is what prevents a duplicate admin.
|
|
265
|
+
for (const col of ['users', 'account', 'session', 'system-setup-locks']) {
|
|
266
|
+
await db.collection(col).deleteMany({});
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
236
270
|
### Init returns 403 "System setup requires BetterAuth"
|
|
237
271
|
|
|
238
272
|
**Cause:** BetterAuth is not configured or not enabled.
|
|
@@ -265,11 +299,11 @@ if (needsSetup) {
|
|
|
265
299
|
|
|
266
300
|
## Error Codes
|
|
267
301
|
|
|
268
|
-
| Code | Key | Description
|
|
269
|
-
| --------- | ---------------------------------- |
|
|
270
|
-
| LTNS_0050 | `SYSTEM_SETUP_NOT_AVAILABLE` | Users already exist, setup
|
|
271
|
-
| LTNS_0051 | `SYSTEM_SETUP_DISABLED` | System setup is disabled in config
|
|
272
|
-
| LTNS_0052 | `SYSTEM_SETUP_BETTERAUTH_REQUIRED` | BetterAuth must be enabled
|
|
302
|
+
| Code | Key | Description |
|
|
303
|
+
| --------- | ---------------------------------- | ---------------------------------------------------------- |
|
|
304
|
+
| LTNS_0050 | `SYSTEM_SETUP_NOT_AVAILABLE` | Users already exist, OR the initial-admin setup is claimed |
|
|
305
|
+
| LTNS_0051 | `SYSTEM_SETUP_DISABLED` | System setup is disabled in config |
|
|
306
|
+
| LTNS_0052 | `SYSTEM_SETUP_BETTERAUTH_REQUIRED` | BetterAuth must be enabled |
|
|
273
307
|
|
|
274
308
|
---
|
|
275
309
|
|
|
@@ -60,7 +60,10 @@ export class CoreSystemSetupController {
|
|
|
60
60
|
summary: 'Create initial admin',
|
|
61
61
|
})
|
|
62
62
|
@ApiResponse({ description: 'Initial admin created', status: 201 })
|
|
63
|
-
@ApiResponse({
|
|
63
|
+
@ApiResponse({
|
|
64
|
+
description: 'System setup not available - users already exist, or the initial-admin setup is claimed',
|
|
65
|
+
status: 403,
|
|
66
|
+
})
|
|
64
67
|
@Post('init')
|
|
65
68
|
@Roles(RoleEnum.S_EVERYONE)
|
|
66
69
|
async createInitialAdmin(@Body() input: SystemSetupInitDto): Promise<SystemSetupInitResult> {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createLocalAccountIssuer } from '@better-auth/core/db';
|
|
1
2
|
import { ForbiddenException, Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
|
|
2
3
|
import { InjectConnection } from '@nestjs/mongoose';
|
|
3
4
|
import { isEmail } from 'class-validator';
|
|
@@ -239,6 +240,27 @@ export class CoreSystemSetupService implements OnApplicationBootstrap {
|
|
|
239
240
|
// racing a fresh deployment can no longer obtain an admin account ALONGSIDE the
|
|
240
241
|
// configured one.
|
|
241
242
|
if (!(await this.claimInitialAdminSetup())) {
|
|
243
|
+
// Say WHICH guard refused. This one and the user-count guard above share
|
|
244
|
+
// ErrorCode.SYSTEM_SETUP_NOT_AVAILABLE, and the response cannot tell them apart — so
|
|
245
|
+
// without this line a caller that just emptied `users` is told users exist while the
|
|
246
|
+
// collection is empty. That is a genuinely expensive thing to diagnose from the outside:
|
|
247
|
+
// the marker outlives a SUCCESSFUL setup (it is released only on failure), so the usual
|
|
248
|
+
// "reset the database" reflex does not clear it.
|
|
249
|
+
// `log`, not `warn`. Both entry points reach this line, and for one of them a refusal is
|
|
250
|
+
// the EXPECTED outcome: on a multi-replica fresh rollout every replica but one loses the
|
|
251
|
+
// claim race, so `warn` would fire N-1 times per normal deployment — each time immediately
|
|
252
|
+
// followed by onApplicationBootstrap's own INFO line calling the same event a normal skip.
|
|
253
|
+
// Two records with contradictory severity for one non-event trains operators to ignore the
|
|
254
|
+
// louder one. The HTTP caller is not left without a diagnosis: the message is unchanged and
|
|
255
|
+
// an operator investigating a 403 reads it at the default level.
|
|
256
|
+
this.logger.log(
|
|
257
|
+
`System setup refused: the initial-admin claim in "${SETUP_LOCK_COLLECTION}" is held. ` +
|
|
258
|
+
'Either another instance is running the setup right now, or a previous SUCCESSFUL setup ' +
|
|
259
|
+
'left the marker behind — it is removed only when creation FAILS. If you reset the ' +
|
|
260
|
+
`database to replay the setup, drop "${SETUP_LOCK_COLLECTION}" alongside "users"; ` +
|
|
261
|
+
`otherwise setup stays refused until the claim goes stale after ` +
|
|
262
|
+
`${INITIAL_ADMIN_CLAIM_STALE_AFTER_MS / 60_000} minutes.`,
|
|
263
|
+
);
|
|
242
264
|
throw new ForbiddenException(ErrorCode.SYSTEM_SETUP_NOT_AVAILABLE);
|
|
243
265
|
}
|
|
244
266
|
|
|
@@ -250,11 +272,32 @@ export class CoreSystemSetupService implements OnApplicationBootstrap {
|
|
|
250
272
|
const normalizedPassword = this.userMapper.normalizePasswordForIam(input.password);
|
|
251
273
|
|
|
252
274
|
// Create user via internalAdapter (bypasses disableSignUp)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
275
|
+
//
|
|
276
|
+
// better-auth >= 1.7 requires a provisioning source. Without it, `createUser` throws
|
|
277
|
+
// FORBIDDEN/validation_source_missing as soon as a project configures
|
|
278
|
+
// `betterAuth.options.user.validateUserInfo`. `admin` is the honest value — the system
|
|
279
|
+
// provisions the FIRST admin, nobody signs up — and it is what better-auth's own admin
|
|
280
|
+
// plugin passes.
|
|
281
|
+
//
|
|
282
|
+
// This does NOT bypass `validateUserInfo`. The hook still runs and receives
|
|
283
|
+
// `{ method: 'admin', action: 'create-user' }`, so a project gate can still reject the
|
|
284
|
+
// initial admin; branch on `method` there if the setup should be exempt.
|
|
285
|
+
//
|
|
286
|
+
// KNOWN LIMITATION, and the reason this is spelled out: when `validateUserInfo` is
|
|
287
|
+
// configured, better-auth additionally calls `getCurrentAuthContext()`, which throws outside
|
|
288
|
+
// an endpoint context. System setup runs from `OnApplicationBootstrap` / a plain controller,
|
|
289
|
+
// never inside better-auth's request pipeline, so the call fails with
|
|
290
|
+
// FORBIDDEN/validation_context_missing and no initial admin is created. Fail-closed, but
|
|
291
|
+
// opaque. A project using that hook must provision the first admin another way — see
|
|
292
|
+
// migration-guides/11.36.x-to-11.37.0.md §7.
|
|
293
|
+
const iamUser = await context.internalAdapter.createUser(
|
|
294
|
+
{
|
|
295
|
+
email: input.email,
|
|
296
|
+
emailVerified: true,
|
|
297
|
+
name: input.name || input.email.split('@')[0],
|
|
298
|
+
},
|
|
299
|
+
{ method: 'admin' },
|
|
300
|
+
);
|
|
258
301
|
|
|
259
302
|
if (!iamUser) {
|
|
260
303
|
throw new Error('Failed to create IAM user');
|
|
@@ -262,8 +305,14 @@ export class CoreSystemSetupService implements OnApplicationBootstrap {
|
|
|
262
305
|
|
|
263
306
|
// Hash password and create credential account
|
|
264
307
|
const hashedPassword = await context.password.hash(normalizedPassword);
|
|
308
|
+
// better-auth >= 1.7 keys accounts by (issuer, accountId) and requires the
|
|
309
|
+
// issuer. Credential accounts have no real issuer, so better-auth derives a
|
|
310
|
+
// synthetic one — always via this helper, never a hand-written literal:
|
|
311
|
+
// the format is better-auth's to change, and a copy of it would silently
|
|
312
|
+
// stop matching the accounts better-auth writes itself.
|
|
265
313
|
await context.internalAdapter.linkAccount({
|
|
266
314
|
accountId: iamUser.id,
|
|
315
|
+
issuer: createLocalAccountIssuer('credential'),
|
|
267
316
|
password: hashedPassword,
|
|
268
317
|
providerId: 'credential',
|
|
269
318
|
userId: iamUser.id,
|