@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.
Files changed (44) hide show
  1. package/.claude/rules/better-auth.md +32 -0
  2. package/.claude/rules/configurable-features.md +1 -1
  3. package/.claude/rules/framework-compatibility.md +1 -0
  4. package/.claude/rules/testing.md +4 -4
  5. package/.claude/rules/versioning.md +6 -0
  6. package/CLAUDE.md +22 -5
  7. package/FRAMEWORK-API.md +1 -1
  8. package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
  9. package/dist/core/modules/auth/guards/roles.guard.d.ts +1 -0
  10. package/dist/core/modules/auth/guards/roles.guard.js +33 -0
  11. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  12. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +2 -0
  13. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
  14. package/dist/core/modules/better-auth/core-better-auth.constants.d.ts +4 -0
  15. package/dist/core/modules/better-auth/core-better-auth.constants.js +5 -1
  16. package/dist/core/modules/better-auth/core-better-auth.constants.js.map +1 -1
  17. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +2 -0
  18. package/dist/core/modules/better-auth/core-better-auth.service.js +68 -0
  19. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  20. package/dist/core/modules/error-code/error-codes.d.ts +3 -3
  21. package/dist/core/modules/error-code/error-codes.js +3 -3
  22. package/dist/core/modules/error-code/error-codes.js.map +1 -1
  23. package/dist/core/modules/system-setup/core-system-setup.controller.js +4 -1
  24. package/dist/core/modules/system-setup/core-system-setup.controller.js.map +1 -1
  25. package/dist/core/modules/system-setup/core-system-setup.service.js +9 -1
  26. package/dist/core/modules/system-setup/core-system-setup.service.js.map +1 -1
  27. package/dist/tsconfig.build.tsbuildinfo +1 -1
  28. package/docs/REQUEST-LIFECYCLE.md +25 -1
  29. package/migration-guides/11.32.x-to-11.33.x.md +37 -4
  30. package/migration-guides/11.36.4-to-11.36.5.md +184 -0
  31. package/migration-guides/11.36.x-to-11.37.0.md +344 -0
  32. package/package.json +17 -4
  33. package/src/core/modules/ai/core-ai-mcp.controller.ts +9 -3
  34. package/src/core/modules/auth/guards/roles.guard.ts +68 -0
  35. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +24 -1
  36. package/src/core/modules/better-auth/README.md +64 -0
  37. package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +7 -0
  38. package/src/core/modules/better-auth/core-better-auth.constants.ts +26 -0
  39. package/src/core/modules/better-auth/core-better-auth.service.ts +179 -3
  40. package/src/core/modules/error-code/error-codes.ts +13 -3
  41. package/src/core/modules/system-setup/INTEGRATION-CHECKLIST.md +8 -7
  42. package/src/core/modules/system-setup/README.md +43 -9
  43. package/src/core/modules/system-setup/core-system-setup.controller.ts +4 -1
  44. 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 only works when `countDocuments({}) === 0`
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
- **Cause:** Users already exist in the database.
234
+ **Two different guards raise this, and the response cannot tell you which.**
230
235
 
231
- **Solutions:**
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 locked |
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({ description: 'System setup not available - users already exist', status: 403 })
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
- const iamUser = await context.internalAdapter.createUser({
254
- email: input.email,
255
- emailVerified: true,
256
- name: input.name || input.email.split('@')[0],
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,