@lenne.tech/nest-server 11.41.3 → 11.41.5

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 (109) hide show
  1. package/.claude/rules/architecture.md +1 -0
  2. package/.claude/rules/configurable-features.md +1 -0
  3. package/.claude/rules/role-system.md +15 -1
  4. package/.claude/rules/testing.md +16 -4
  5. package/CLAUDE.md +6 -3
  6. package/FRAMEWORK-API.md +4 -1
  7. package/dist/core/common/helpers/graceful-shutdown.helper.js.map +1 -1
  8. package/dist/core/common/helpers/logging.helper.js +2 -0
  9. package/dist/core/common/helpers/logging.helper.js.map +1 -1
  10. package/dist/core/common/helpers/process-diagnostics.helper.js.map +1 -1
  11. package/dist/core/common/interfaces/server-options.interface.d.ts +21 -1
  12. package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.d.ts +2 -2
  13. package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js +46 -6
  14. package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js.map +1 -1
  15. package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js +7 -1
  16. package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js.map +1 -1
  17. package/dist/core/modules/api-token/core-api-token.constants.d.ts +6 -0
  18. package/dist/core/modules/api-token/core-api-token.constants.js +11 -0
  19. package/dist/core/modules/api-token/core-api-token.constants.js.map +1 -0
  20. package/dist/core/modules/api-token/core-api-token.decorators.d.ts +1 -0
  21. package/dist/core/modules/api-token/core-api-token.decorators.js +8 -0
  22. package/dist/core/modules/api-token/core-api-token.decorators.js.map +1 -0
  23. package/dist/core/modules/api-token/core-api-token.helpers.d.ts +127 -0
  24. package/dist/core/modules/api-token/core-api-token.helpers.js +398 -0
  25. package/dist/core/modules/api-token/core-api-token.helpers.js.map +1 -0
  26. package/dist/core/modules/api-token/core-api-token.middleware.d.ts +10 -0
  27. package/dist/core/modules/api-token/core-api-token.middleware.js +58 -0
  28. package/dist/core/modules/api-token/core-api-token.middleware.js.map +1 -0
  29. package/dist/core/modules/api-token/core-api-token.model.d.ts +20 -0
  30. package/dist/core/modules/api-token/core-api-token.model.js +199 -0
  31. package/dist/core/modules/api-token/core-api-token.model.js.map +1 -0
  32. package/dist/core/modules/api-token/core-api-token.module.d.ts +11 -0
  33. package/dist/core/modules/api-token/core-api-token.module.js +39 -0
  34. package/dist/core/modules/api-token/core-api-token.module.js.map +1 -0
  35. package/dist/core/modules/api-token/core-api-token.registry.d.ts +9 -0
  36. package/dist/core/modules/api-token/core-api-token.registry.js +17 -0
  37. package/dist/core/modules/api-token/core-api-token.registry.js.map +1 -0
  38. package/dist/core/modules/api-token/core-api-token.service.d.ts +104 -0
  39. package/dist/core/modules/api-token/core-api-token.service.js +550 -0
  40. package/dist/core/modules/api-token/core-api-token.service.js.map +1 -0
  41. package/dist/core/modules/auth/guards/roles.guard.js +17 -1
  42. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  43. package/dist/core/modules/better-auth/better-auth-roles.guard.js +12 -2
  44. package/dist/core/modules/better-auth/better-auth-roles.guard.js.map +1 -1
  45. package/dist/core/modules/better-auth/core-better-auth.middleware.js +4 -0
  46. package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -1
  47. package/dist/core/modules/better-auth/core-better-auth.module.d.ts +4 -0
  48. package/dist/core/modules/better-auth/core-better-auth.module.js +18 -0
  49. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  50. package/dist/core/modules/migrate/migration-runner.d.ts +1 -0
  51. package/dist/core/modules/migrate/migration-runner.js +3 -0
  52. package/dist/core/modules/migrate/migration-runner.js.map +1 -1
  53. package/dist/core/modules/tenant/core-tenant-guard.registry.d.ts +2 -0
  54. package/dist/core/modules/tenant/core-tenant-guard.registry.js +19 -0
  55. package/dist/core/modules/tenant/core-tenant-guard.registry.js.map +1 -0
  56. package/dist/core/modules/tenant/core-tenant.guard.d.ts +1 -0
  57. package/dist/core/modules/tenant/core-tenant.guard.js +28 -12
  58. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  59. package/dist/core/modules/tenant/core-tenant.helpers.d.ts +1 -0
  60. package/dist/core/modules/tenant/core-tenant.helpers.js +19 -0
  61. package/dist/core/modules/tenant/core-tenant.helpers.js.map +1 -1
  62. package/dist/core/modules/tenant/core-tenant.module.d.ts +5 -2
  63. package/dist/core/modules/tenant/core-tenant.module.js +8 -0
  64. package/dist/core/modules/tenant/core-tenant.module.js.map +1 -1
  65. package/dist/core/modules/user/core-user.service.js +7 -0
  66. package/dist/core/modules/user/core-user.service.js.map +1 -1
  67. package/dist/core.module.js +6 -0
  68. package/dist/core.module.js.map +1 -1
  69. package/dist/index.d.ts +7 -0
  70. package/dist/index.js +7 -0
  71. package/dist/index.js.map +1 -1
  72. package/dist/tsconfig.build.tsbuildinfo +1 -1
  73. package/docs/REQUEST-LIFECYCLE.md +49 -1
  74. package/docs/security-overrides.md +21 -13
  75. package/migration-guides/11.41.3-to-11.41.4.md +172 -0
  76. package/migration-guides/11.41.4-to-11.41.5.md +144 -0
  77. package/package.json +35 -34
  78. package/src/core/common/helpers/graceful-shutdown.helper.ts +9 -0
  79. package/src/core/common/helpers/logging.helper.ts +7 -0
  80. package/src/core/common/helpers/process-diagnostics.helper.ts +4 -0
  81. package/src/core/common/interfaces/server-options.interface.ts +122 -6
  82. package/src/core/modules/ai/INTEGRATION-CHECKLIST.md +17 -10
  83. package/src/core/modules/ai/README.md +9 -1
  84. package/src/core/modules/ai/helpers/ai-mcp-oauth.helper.ts +74 -7
  85. package/src/core/modules/ai/services/core-ai-mcp-oauth.service.ts +13 -1
  86. package/src/core/modules/api-token/INTEGRATION-CHECKLIST.md +121 -0
  87. package/src/core/modules/api-token/README.md +212 -0
  88. package/src/core/modules/api-token/core-api-token.constants.ts +27 -0
  89. package/src/core/modules/api-token/core-api-token.decorators.ts +29 -0
  90. package/src/core/modules/api-token/core-api-token.helpers.ts +711 -0
  91. package/src/core/modules/api-token/core-api-token.middleware.ts +57 -0
  92. package/src/core/modules/api-token/core-api-token.model.ts +193 -0
  93. package/src/core/modules/api-token/core-api-token.module.ts +48 -0
  94. package/src/core/modules/api-token/core-api-token.registry.ts +53 -0
  95. package/src/core/modules/api-token/core-api-token.service.ts +822 -0
  96. package/src/core/modules/auth/guards/roles.guard.ts +23 -2
  97. package/src/core/modules/better-auth/better-auth-roles.guard.ts +18 -4
  98. package/src/core/modules/better-auth/core-better-auth.middleware.ts +8 -0
  99. package/src/core/modules/better-auth/core-better-auth.module.ts +33 -0
  100. package/src/core/modules/migrate/README.md +15 -7
  101. package/src/core/modules/migrate/migration-runner.ts +9 -0
  102. package/src/core/modules/tenant/README.md +17 -0
  103. package/src/core/modules/tenant/core-tenant-guard.registry.ts +36 -0
  104. package/src/core/modules/tenant/core-tenant.guard.ts +52 -12
  105. package/src/core/modules/tenant/core-tenant.helpers.ts +30 -0
  106. package/src/core/modules/tenant/core-tenant.module.ts +19 -2
  107. package/src/core/modules/user/core-user.service.ts +14 -0
  108. package/src/core.module.ts +12 -0
  109. package/src/index.ts +12 -0
@@ -112,6 +112,7 @@ JWT-based authentication for existing projects:
112
112
  | **Tenant Isolation** | Header-based multi-tenant isolation with membership validation (opt-in) |
113
113
  | **Tenant Guard** | `CoreTenantGuard` validates tenant membership; system roles (`S_EVERYONE`, `S_USER`, `S_VERIFIED`) are checked as OR alternatives before real roles; hierarchy roles (`@Roles(DefaultHR.MEMBER)`), `@SkipTenantCheck()`, BetterAuth auto-skip (`betterAuth.skipTenantCheck`) |
114
114
  | **Tenant Plugin Safety Net** | Mongoose tenant plugin throws `ForbiddenException` when tenant-schema is accessed without valid tenant context |
115
+ | **API Tokens** | `apiTokens` config (opt-in): USER tokens act as their user without global roles; TENANT tokens (multi-tenancy only) act with the lowest tenant role in their own tenant. `CoreApiTokenMiddleware` authenticates `Authorization: Bearer` / `x-api-key`; every guard denies a token unless the route declares `@ApiTokenScopes()`. Signed short-lived assertions for embedded pages. See `src/core/modules/api-token/README.md` |
115
116
 
116
117
  ### Data & CRUD
117
118
 
@@ -153,7 +154,8 @@ JWT-based authentication for existing projects:
153
154
  | `@ResponseModel(Model)` | REST response type hint for auto-conversion |
154
155
  | `@Translatable()` | Multi-language field metadata |
155
156
  | `@CommonError(code)` | Error code registration |
156
- | `@SkipTenantCheck()` | Opt out of CoreTenantGuard validation on a method |
157
+ | `@SkipTenantCheck()` | Opt out of CoreTenantGuard validation on a method (not for tenant-restricted API tokens — they stay bound) |
158
+ | `@ApiTokenScopes(...scopes)` | Open a route/class to API tokens holding one of the scopes; tokens are denied everywhere else |
157
159
 
158
160
  ### File Handling
159
161
 
@@ -335,11 +337,18 @@ The following diagram shows the exact order of execution from HTTP request to re
335
337
  | - Accept-Language for translations |
336
338
  | |
337
339
  | 2. CoreBetterAuthMiddleware |
340
+ | - Skips API-token credentials entirely |
338
341
  | - Strategy 1: Auth header (JWT/Session) |
339
342
  | - Strategy 2: JWT cookie |
340
343
  | - Strategy 3: Session cookie |
341
344
  | - Sets req.user |
342
345
  | |
346
+ | 2c. CoreApiTokenMiddleware [if apiTokens enabled] |
347
+ | - Bearer / x-api-key with the token prefix only |
348
+ | - Invalid token or assertion -> 401 on every route |
349
+ | - Per-token rate limit -> 429 + Retry-After |
350
+ | - Sets req.user (tenant principal / token's user) |
351
+ | |
343
352
  | 3. graphqlUploadExpress() [GraphQL only] |
344
353
  | - Handles multipart file uploads |
345
354
  +----------------------------+----------------------------+
@@ -348,6 +357,8 @@ The following diagram shows the exact order of execution from HTTP request to re
348
357
  | GUARDS |
349
358
  | |
350
359
  | 4. RolesGuard / BetterAuthRolesGuard |
360
+ | - API token: @ApiTokenScopes() required, else 403 |
361
+ | (checked BEFORE the public-route shortcut) |
351
362
  | - Reads @Roles() metadata |
352
363
  | - Validates JWT / session token |
353
364
  | - Checks real roles (ADMIN) |
@@ -355,6 +366,8 @@ The following diagram shows the exact order of execution from HTTP request to re
355
366
  | - Throws 401 (Unauthorized) or 403 (Forbidden) |
356
367
  | |
357
368
  | 4b. CoreTenantGuard [if multiTenancy enabled] |
369
+ | - Tenant API token: bound to its tenant, lowest role|
370
+ | - User API token: tenant restriction + role cap |
358
371
  | - Reads X-Tenant-Id header |
359
372
  | - Validates membership via hierarchy roles (level comparison) |
360
373
  | - Non-admin + header + no membership = always 403 |
@@ -640,6 +653,22 @@ makes every project on that host unreachable over http for up to a year, with no
640
653
  Behind a TLS-terminating proxy the inward connection is plain http, which makes `trustProxy`
641
654
  load-bearing for this header too.
642
655
 
656
+ #### 2c. CoreApiTokenMiddleware
657
+
658
+ Registered by `CoreApiTokenModule` (auto-imported by `CoreModule` when `apiTokens` is configured).
659
+ Claims only credentials carrying the configured prefix — `Authorization: Bearer <prefix>_…` /
660
+ `<prefix>s_…`, or the same value in `x-api-key` — so sessions, JWTs and legacy tokens pass untouched;
661
+ `CoreBetterAuthMiddleware` in turn skips prefixed credentials, including its cookie fallback, so a
662
+ session cookie riding along can never turn a token request into a session request.
663
+
664
+ - A token resolves to a TENANT principal (no global roles, bound to its tenant) or to the owning USER,
665
+ loaded from the `users` collection with global roles removed. Both carry a token context
666
+ (`getApiTokenContext(user)`), recognised by a module-private symbol — never by a data field.
667
+ - A prefixed credential that does not authenticate (unknown, revoked, expired, tampered, bad
668
+ signature, deleted owner, two different credentials in the two headers) answers **401 on every
669
+ route**, public ones included — unlike an invalid session, which degrades to anonymous.
670
+ - A per-token fixed-window rate limit (`RateLimitStore`, Redis-shared when configured) answers 429.
671
+
643
672
  #### 3. graphqlUploadExpress
644
673
 
645
674
  Only for GraphQL routes. Handles multipart file upload requests according to the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec).
@@ -694,6 +723,25 @@ Two consequences worth knowing when you own a public endpoint:
694
723
 
695
724
  **Important:** `@Roles()` already handles JWT authentication internally. Do NOT add `@UseGuards(AuthGuard(JWT))` — it is redundant.
696
725
 
726
+ #### API tokens (`@ApiTokenScopes`)
727
+
728
+ All three guards call ONE function, `enforceApiTokenRoute()` (`core-api-token.helpers.ts`), before
729
+ their public-route shortcut, so the policy holds whichever guard runs first or alone:
730
+
731
+ 1. A token is refused (403) on every route that does not declare `@ApiTokenScopes()` — public ones
732
+ included — and on a route whose scopes it does not hold (method-level replaces class-level).
733
+ 2. A **TENANT** token is then decided completely: `S_NO_ONE` refuses; `S_EVERYONE` / `S_USER` /
734
+ `S_VERIFIED` count as satisfied; other roles resolve against the LOWEST tenant role, global roles
735
+ never; a route guarded only by `S_SELF` / `S_CREATOR` refuses (a tenant token is nobody's self);
736
+ an `X-Tenant-Id` naming another tenant refuses. The request gets `tenantId` / `tenantRole`
737
+ like a membership.
738
+ 3. A **USER** token continues through the ordinary checks as its user (global roles removed, so no
739
+ admin bypass). `CoreTenantGuard` adds its restrictions: a token restricted to one tenant is bound
740
+ to it (a foreign header refuses, `@SkipTenantCheck()` does not unbind it), and `maxTenantRole`
741
+ caps the membership role wherever it is used — including the no-header `tenantIds` resolution.
742
+ 4. A request that carries a token credential but reaches a guard without a token context (the
743
+ middleware did not run) is refused with 401 rather than treated as anonymous.
744
+
697
745
  #### System Roles (S_ prefix)
698
746
 
699
747
  System roles are evaluated at runtime and must **never** be stored in `user.roles`.
@@ -11,14 +11,21 @@ A green `pnpm audit` inside the framework repo says nothing about your tree.
11
11
 
12
12
  ## What this concretely means for you
13
13
 
14
- The framework pulls in three transitive packages that resolve to a **vulnerable** version unless you
15
- override them yourself:
14
+ The framework pulls in three transitive packages that its `@nestjs/*` dependencies used to
15
+ **exact-pin** to a vulnerable version:
16
16
 
17
17
  | Package | Advisory | Why it cannot resolve forward on its own |
18
18
  |---------|----------|------------------------------------------|
19
- | `ws` | [GHSA-96hv-2xvq-fx4p](https://github.com/advisories/GHSA-96hv-2xvq-fx4p) — high: memory-exhaustion DoS + uninitialized memory disclosure. Patched `>=8.21.0` | `@nestjs/graphql` declares `"ws": "8.20.1"` — an **exact pin**, not a caret. No amount of updating moves it |
20
- | `js-yaml` | [GHSA-pm4m-ph32-ghv5](https://github.com/advisories/GHSA-pm4m-ph32-ghv5) — high: exponential parsing time in flow collections (DoS). Patched `>=5.2.2` | `@nestjs/swagger` declares `"js-yaml": "5.2.1"` — an **exact pin**, the same shape as the `ws` case. It cannot resolve forward |
21
- | `multer` | [GHSA-wc9g-mqfw-jrwm](https://github.com/advisories/GHSA-wc9g-mqfw-jrwm), [GHSA-535w-7cp7-47q4](https://github.com/advisories/GHSA-535w-7cp7-47q4), [GHSA-qfvm-cv95-jqjf](https://github.com/advisories/GHSA-qfvm-cv95-jqjf) — high: DoS via crafted multipart input; [GHSA-qvfw-j98x-7q72](https://github.com/advisories/GHSA-qvfw-j98x-7q72) — low: file size limit bypass. Patched `>=2.3.0` | `@nestjs/platform-express` declares `"multer": "2.2.0"` — an **exact pin** in every 11.2.x. The framework's own `multer: 2.3.0` dependency does not move it: you get both copies, and FileInterceptor uses the vulnerable one |
19
+ | `ws` | [GHSA-96hv-2xvq-fx4p](https://github.com/advisories/GHSA-96hv-2xvq-fx4p) — high: memory-exhaustion DoS + uninitialized memory disclosure. Patched `>=8.21.0` | Older `@nestjs/graphql` releases declare `"ws": "8.20.1"` — an **exact pin**, not a caret. 13.4.5, the version this framework declares, pins `8.21.3` |
20
+ | `js-yaml` | [GHSA-pm4m-ph32-ghv5](https://github.com/advisories/GHSA-pm4m-ph32-ghv5) — high: exponential parsing time in flow collections (DoS), patched `>=5.2.2`; [GHSA-r3ph-w7gj-g6xm](https://github.com/advisories/GHSA-r3ph-w7gj-g6xm) — moderate: `maxTotalMergeKeys` does not bound CPU use for empty merge sources, covers `<=5.4.0`, patched `>=5.4.1` (published 2026-09-29) | `@nestjs/swagger` declares js-yaml as an **exact pin**, the same shape as the `ws` case: older releases `5.2.1`, 11.4.7 — the version this framework declares, and the newest 11.x — `5.3.0`. No swagger update reaches the fix, so **this entry is load-bearing again** |
21
+ | `multer` | [GHSA-wc9g-mqfw-jrwm](https://github.com/advisories/GHSA-wc9g-mqfw-jrwm), [GHSA-535w-7cp7-47q4](https://github.com/advisories/GHSA-535w-7cp7-47q4), [GHSA-qfvm-cv95-jqjf](https://github.com/advisories/GHSA-qfvm-cv95-jqjf) — high: DoS via crafted multipart input; [GHSA-qvfw-j98x-7q72](https://github.com/advisories/GHSA-qvfw-j98x-7q72) — low: file size limit bypass. Patched `>=2.3.0` | `@nestjs/platform-express` up to 11.2.5 declares `"multer": "2.2.0"` — an **exact pin**. A direct `multer` dependency does not move it: you get both copies, and FileInterceptor uses the vulnerable one. 11.2.6, the version this framework declares since 11.41.4, pins `2.4.0` |
22
+
23
+ **Status since 11.41.5:** `ws` and `multer` resolve to a patched version on their own with the
24
+ `@nestjs/*` versions this framework declares; their entries below are insurance for a project whose
25
+ own `package.json` pins an older `@nestjs/graphql` or `@nestjs/platform-express`. **`js-yaml` does
26
+ not**: GHSA-r3ph-w7gj-g6xm covers the `5.3.0` that `@nestjs/swagger` 11.4.7 pins, so without the
27
+ entry your `pnpm audit` reports it. Its key and target were raised in 11.41.5 — an entry copied
28
+ before that (`<5.2.2`) no longer reaches swagger's pin.
22
29
 
23
30
  `@nestjs/graphql` is a plain `dependencies` entry, so `ws` is installed even when you run with
24
31
  `graphQl: false`. `@nestjs/swagger` and `@nestjs/platform-express` are likewise plain `dependencies`
@@ -37,15 +44,16 @@ overrides:
37
44
  # Remove once @nestjs/graphql stops pinning it.
38
45
  'ws@>=8.0.0 <8.21.0': '8.21.3'
39
46
 
40
- # @nestjs/swagger exact-pins js-yaml@5.2.1 (GHSA-pm4m-ph32-ghv5, high, patched >=5.2.2).
41
- # Same shape as the ws entry: an exact pin cannot resolve forward.
42
- # Remove once @nestjs/swagger stops pinning it.
43
- 'js-yaml@>=5.0.0 <5.2.2': '5.2.2'
47
+ # @nestjs/swagger exact-pins js-yaml (11.4.7: 5.3.0) — GHSA-r3ph-w7gj-g6xm (moderate, patched >=5.4.1)
48
+ # and, for older swagger releases, GHSA-pm4m-ph32-ghv5 (high, patched >=5.2.2).
49
+ # Same shape as the ws entry: an exact pin cannot resolve forward. LOAD-BEARING since 11.41.5.
50
+ # Remove once @nestjs/swagger pins >=5.4.1 itself.
51
+ 'js-yaml@>=5.0.0 <5.4.2': '5.4.2'
44
52
 
45
- # @nestjs/platform-express exact-pins multer@2.2.0 (GHSA-wc9g-mqfw-jrwm and three more, patched >=2.3.0).
46
- # Keep the target in LOCKSTEP with the multer version @lenne.tech/nest-server declares.
47
- # Remove once @nestjs/platform-express requests >=2.3.0 itself.
48
- 'multer@>=2.0.0 <2.3.0': '2.3.0'
53
+ # @nestjs/platform-express <=11.2.5 exact-pins multer@2.2.0 (GHSA-wc9g-mqfw-jrwm and three more, patched >=2.3.0).
54
+ # Keep the target in LOCKSTEP with the multer version @lenne.tech/nest-server declares (2.4.0 since 11.41.4).
55
+ # Inert on @nestjs/platform-express 11.2.6+, which pins 2.4.0 itself.
56
+ 'multer@>=2.0.0 <2.4.0': '2.4.0'
49
57
  ```
50
58
 
51
59
  ### Retired: `@hono/node-server` (removed 2026-08-22, nest-server 11.36.1)
@@ -0,0 +1,172 @@
1
+ # Migration Guide: 11.41.3 → 11.41.4
2
+
3
+ ## Overview
4
+
5
+ | Category | Effort | Applies to |
6
+ | ------------------------ | ----------------------- | ------------------------------------------------------------------------------------------------------ |
7
+ | **New feature (opt-in)** | config + a thin controller | API tokens: USER tokens and — with multi-tenancy — TENANT tokens, signed assertions for embedded pages |
8
+ | Improvement | none, optional clean-up | `mountAiMcpOAuth(app)` takes the OAuth issuer from the server config |
9
+ | **Security fix** | none, verify once | Role guards hand non-system roles to the tenant guard only while one is REGISTERED |
10
+ | Dependencies | `pnpm run update` | `@nestjs/*` 11.2.6, mongoose 9.10.2 / mongodb 7.6.0, multer 2.4.0 — align your own pins (§4) |
11
+
12
+ No breaking change for a standard setup. Without an `apiTokens` block the token feature does nothing; the security fix only changes behaviour where `multiTenancy` is configured but no tenant guard is registered (see §3).
13
+
14
+ ## Quick Migration
15
+
16
+ ```bash
17
+ pnpm update @lenne.tech/nest-server
18
+ pnpm run update # starter-based projects: aligns the framework's ecosystem (see §4)
19
+ pnpm run build && pnpm test
20
+ ```
21
+
22
+ Vendor-mode projects adopt the new `src/core/modules/api-token/` directory and the touched guards
23
+ through the core update.
24
+
25
+ ## What's New in 11.41.4
26
+
27
+ ### 1. API tokens
28
+
29
+ Bearer credentials for machine clients, scripts and embedded pages that cannot carry a session cookie.
30
+
31
+ | | USER token | TENANT token |
32
+ | ------------------- | --------------------------------------------------- | ---------------------- |
33
+ | Belongs to | one user | one tenant |
34
+ | Acts with | the user's current rights, **without** global roles | the lowest tenant role |
35
+ | Optional limits | scopes, one tenant, `maxTenantRole`, expiry | scopes, expiry |
36
+ | Needs multi-tenancy | no | yes |
37
+
38
+ ```typescript
39
+ // config.env.ts
40
+ apiTokens: {
41
+ scopes: ['upload', 'read', 'export'],
42
+ encryptionKey: process.env.API_TOKEN_ENCRYPTION_KEY, // required in production/staging
43
+ },
44
+
45
+ // a route a token may call — tokens are denied on every other route, public ones included
46
+ @ApiTokenScopes('upload')
47
+ @Roles(RoleEnum.S_USER)
48
+ @Post('documents')
49
+ ```
50
+
51
+ Management goes through `CoreApiTokenService` (`createUserToken`, `createTenantToken`, …); the project
52
+ writes a thin controller. Full integration: `src/core/modules/api-token/INTEGRATION-CHECKLIST.md`.
53
+
54
+ Security properties worth knowing before you open a route:
55
+
56
+ - A token only reaches routes that declare `@ApiTokenScopes()`; every guard enforces that.
57
+ - A prefixed credential that fails answers **401 on every route** — it is never silently anonymous.
58
+ - Tenant boundaries hold for both kinds: a tenant token only in its tenant, a user token only in its
59
+ user's active memberships (and in one of them, if restricted).
60
+ - A tenant token is no person: a route guarded only by `S_SELF` / `S_CREATOR` refuses it, and
61
+ `@CurrentUser()` carries no `email` / `verified`.
62
+ - Tokens never manage tokens — `CoreApiTokenService` refuses a token caller even on a route you
63
+ released; a platform admin's user token is not a platform admin. Protected fields (`tenant`, `user`,
64
+ `kind`, `revokedAt`, …) cannot be set through a management input, dotted paths included.
65
+ - The MCP OAuth consent step (`mountAiMcpOAuth`) refuses token-authenticated requests: a consent
66
+ mints an access token with the user's full rights. An override of `authorizeConsent()` keeps this check.
67
+ - A password reset that ends the user's sessions ends their user tokens too: the legacy reset always,
68
+ the IAM reset while `betterAuth.emailAndPassword.revokeSessionsOnPasswordReset` is on (recommended).
69
+ Tenant tokens are not affected — they belong to the tenant.
70
+
71
+ What your project has to take care of itself:
72
+
73
+ | Topic | What to do |
74
+ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
75
+ | Deleting or off-boarding a tenant | Call `CoreApiTokenService.deleteAllForTenant(tenantId)`. The core has no tenant model to notice the deletion, and a tenant token depends on no member — until you call it, the tenant's tokens keep authenticating |
76
+ | Account compromise without a reset | Call `CoreApiTokenService.revokeAllForUser(userId)`. With `revokeSessionsOnPasswordReset` off, an IAM reset leaves user tokens alive, like personal access tokens elsewhere; a password CHANGE never revokes them |
77
+ | Routes mounted with `app.use()` | They run outside the Nest guards, so `@ApiTokenScopes()` does not protect them and `req.user` may be a token. Check `getApiTokenContext(req.user)` there |
78
+ | `@CurrentUser()` on an opened route | For a tenant token it is not a person: no `email`, no `verified`. Code that mails or notifies "the current user" must check `getApiTokenContext()` first |
79
+ | Better-Auth admin plugin (`banned`) | Not used by the framework. If you add it, override `CoreApiTokenService.loadTokenUser()` so a banned user's tokens are refused too |
80
+ | Brute force against tokens | Tokens carry 256 bits of secret, so guessing is not the risk — but failed attempts are not rate-limited per IP. Put an IP limit in front (reverse proxy / WAF) if the API is exposed |
81
+ | Secret scanning | Register the shape `<prefix>_<24 hex>_<64 hex>` as a custom pattern |
82
+
83
+ ### 2. `mountAiMcpOAuth(app)` without `baseUrl`
84
+
85
+ The OAuth issuer — and with it every URL in the discovery metadata that MCP clients follow — now
86
+ defaults to the server's `baseUrl` (`NSC__BASE_URL`), resolved like BetterAuth and CORS resolve it.
87
+ `local` / `ci` / `e2e` fall back to `http://localhost:3000`; any other environment without a
88
+ `baseUrl` fails the call instead of advertising a guessed localhost URL. The resolved issuer is logged
89
+ once at boot.
90
+
91
+ ```typescript
92
+ // Before
93
+ await mountAiMcpOAuth(app, { baseUrl: envConfig.baseUrl || `http://localhost:${envConfig.port}` });
94
+
95
+ // After
96
+ await mountAiMcpOAuth(app);
97
+ ```
98
+
99
+ An explicit `baseUrl` still wins. Remove a hand-written `localhost` fallback: it is exactly what made a
100
+ deployed API advertise `http://localhost:3000` and send MCP clients to the user's own machine.
101
+
102
+ ### 3. Role guards delegate only to a registered tenant guard (security fix)
103
+
104
+ `RolesGuard` and `BetterAuthRolesGuard` hand every non-system role to `CoreTenantGuard`, which
105
+ resolves it against the membership. They used to decide that from CONFIGURATION alone: with
106
+ `multiTenancy` set but no tenant guard actually registered, a role such as `RoleEnum.ADMIN` went to
107
+ a guard that never ran, and every authenticated caller passed. They now delegate only while a tenant
108
+ guard is registered, and otherwise check the roles against `user.roles` themselves (with a one-time
109
+ warning).
110
+
111
+ A project that lets `CoreModule` register the tenant module — the normal setup — sees no difference.
112
+ If your project replaces the tenant module with its own registration, make sure a tenant guard is
113
+ still registered, either through `CoreTenantModule.forRoot({ guard })` or as `CoreTenantGuard`
114
+ itself.
115
+
116
+ ### 4. Dependency updates
117
+
118
+ A maintenance pass ships with this release. No major version moved in `dependencies` or
119
+ `peerDependencies`; `better-auth` stays on its `>=1.7.1 <1.8.0` peer range.
120
+
121
+ | Package | 11.41.3 | 11.41.4 |
122
+ | ------- | ------- | ------- |
123
+ | `@nestjs/common`, `@nestjs/core`, `@nestjs/platform-express` | 11.2.1 | 11.2.6 |
124
+ | `mongoose` / `mongodb` | 9.9.3 / 7.5.0 | 9.10.2 / 7.6.0 |
125
+ | `multer` | 2.3.0 | 2.4.0 |
126
+ | `graphql` | 16.14.0 | 16.14.2 |
127
+ | `jose` | 6.2.9 | 6.2.12 |
128
+ | `@modelcontextprotocol/sdk` | 1.30.0 | 1.30.1 |
129
+ | `@tus/server` | 2.4.4 | 2.4.5 |
130
+ | `compression` | 1.8.1 | 1.8.2 |
131
+ | `supertest` | 7.2.2 | 7.3.0 |
132
+
133
+ **What to do:** if your project pins any of these itself — most do for `@nestjs/common`,
134
+ `@nestjs/core`, `@nestjs/platform-express` and `mongoose` — raise the pins to the versions above.
135
+ A pin that lags behind the framework puts a second copy into the tree, and with `@nestjs/*` that
136
+ surfaces as type errors about classes that "incorrectly extend" their base class. Starter-based
137
+ projects get this from `pnpm run update`, which also raises the tooling devDependencies a project
138
+ shares with the framework: `@nestjs/testing` 11.2.6, `@swc/core` 1.16.2, `@types/node` 26.6.2,
139
+ `oxfmt` 0.70.0, `oxlint` 1.85.0 and `unplugin-swc` 2.0.0 (a major, whose only breaking change is
140
+ dropping Node 18).
141
+
142
+ **Security overrides:** `@nestjs/platform-express` 11.2.6 pins `multer` 2.4.0 itself, so the
143
+ `multer` override from `docs/security-overrides.md` is inert on this version. Keep it as insurance
144
+ and raise its target in lockstep: `'multer@>=2.0.0 <2.4.0': '2.4.0'`. The `ws` and `js-yaml`
145
+ entries are likewise inert on the `@nestjs/graphql` / `@nestjs/swagger` versions this framework
146
+ declares; they only matter where your own `package.json` pins older ones.
147
+
148
+ ## Compatibility Notes
149
+
150
+ | Pattern | Status |
151
+ | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
152
+ | Projects without `apiTokens` | Unchanged |
153
+ | Custom guards extending `RolesGuard` / `BetterAuthRolesGuard` / `CoreTenantGuard` | Unchanged as long as they call `super.canActivate()`. A guard that replaces the logic entirely skips the token rules in THAT guard: route release and scopes are still enforced by the role guard, but a user token's tenant restriction and `maxTenantRole` are applied only by `CoreTenantGuard` — keep calling it |
154
+ | A project using the `x-api-key` header for its own keys | Only values carrying the token prefix are claimed; everything else passes through |
155
+ | `mountAiMcpOAuth(app, { baseUrl })` | Unchanged; the argument is optional now |
156
+ | `multiTenancy` configured without any registered tenant guard | Non-system roles are now checked against `user.roles` instead of being waved through — a route that relied on the gap starts answering 403 |
157
+
158
+ ## Troubleshooting
159
+
160
+ | Symptom | Cause | Fix |
161
+ | -------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------- |
162
+ | Boot error mentioning `apiTokens.manageRole` or "a role below" | The manage role is not a declared tenant role, or the hierarchy has a single level | Declare the role / add a lower role, or `tenantTokens: false` |
163
+ | Boot error "encryptionKey … required in production/staging" | No key configured | Set `apiTokens.encryptionKey` or `SECRETS_ENCRYPTION_KEY` |
164
+ | A token gets 403 on a route | Route not opened, scope missing, role too high, foreign tenant | `@ApiTokenScopes()`, token scopes, `X-Tenant-Id` |
165
+ | `mountAiMcpOAuth: no public server URL` at boot | Deployed without `baseUrl` | Set `NSC__BASE_URL` |
166
+
167
+ ## Module Documentation
168
+
169
+ - [API tokens README](../src/core/modules/api-token/README.md)
170
+ - [API tokens Integration Checklist](../src/core/modules/api-token/INTEGRATION-CHECKLIST.md)
171
+ - [AI module README → OAuth 2.1](../src/core/modules/ai/README.md)
172
+ - [Request lifecycle → API tokens](../docs/REQUEST-LIFECYCLE.md)
@@ -0,0 +1,144 @@
1
+ # Migration Guide: 11.41.4 → 11.41.5
2
+
3
+ ## Overview
4
+
5
+ | Category | Details |
6
+ | --------------------- | ---------------------------------------------------------------------------------------------------------------- |
7
+ | **Breaking Changes** | None in the package |
8
+ | **Bugfix** | `migrate down` now holds the migration lock, like `migrate up` already did |
9
+ | **Template change** | nest-server-starter's `docker-entrypoint.sh` now aborts the start when a migration RAN AND FAILED (§2) |
10
+ | **New option** | `MIGRATIONS_ALLOW_FAILURE=true` — start anyway for one deploy (starter and this repo's entrypoint) |
11
+ | **Dependencies** | nodemailer 9 → 10 (security; the only break is Node >= 20), js-yaml override now needed in your project (§3) |
12
+ | **Migration Effort** | Raise two overrides in your `pnpm-workspace.yaml` (§3); decide once whether to adopt the strict entrypoint (§2) |
13
+
14
+ ## Quick Migration
15
+
16
+ ```bash
17
+ pnpm update @lenne.tech/nest-server
18
+ pnpm run update # starter-based projects: aligns the framework's ecosystem (see §3)
19
+ pnpm run build && pnpm test
20
+ ```
21
+
22
+ Vendor-mode projects pick up `migration-runner.ts` through the core update.
23
+
24
+ ## What's New in 11.41.5
25
+
26
+ ### 1. `migrate down` holds the migration lock
27
+
28
+ Since 11.33.0, `migrate up` (`MigrationRunner.up()`) runs under the lock of the state store's lock
29
+ collection — `migrations_lock` by default for stores built by `createMigrationStore()` — so replicas
30
+ booting together serialize instead of each applying the same pending migration. `migrate down`
31
+ did not. A rollback is typically run while a deploy is failing, i.e. exactly while replicas restart
32
+ and each boots into `migrate up`; outside the lock the two read and rewrote the migration state
33
+ concurrently. `MigrationRunner.down()` now takes the same lock. `migrate list` only reads and stays
34
+ unlocked.
35
+
36
+ Nothing to do: projects using `createMigrationStore()` get it automatically. A store built with an
37
+ empty lock collection name (`createMigrationStore(uri, 'migrations', '')`) still runs unlocked, as
38
+ before.
39
+
40
+ ### 2. The starter's entrypoint aborts on a migration that ran and failed
41
+
42
+ This concerns `docker-entrypoint.sh` in **nest-server-starter** (and therefore in projects created
43
+ from it), not the npm package. Until now the starter's entrypoint logged a failed migration and
44
+ started the server anyway. A failed schema migration is then indistinguishable from a good deploy
45
+ on every level anyone watches — health check 200, the right commit on `/meta`, drift detection
46
+ green, `turbo deploy --wait` converging — over an app that works against the empty collections
47
+ Mongoose created at boot while the data still sits under the old names.
48
+
49
+ | Situation | Before | Now |
50
+ | --------- | ------ | --- |
51
+ | A migration ran and threw | warning, server starts | **start aborted** (exit 1); the container runtime retries |
52
+ | `MIGRATIONS_ALLOW_FAILURE=true` | — | warning, server starts — a deliberate exception for ONE deploy |
53
+ | `MIGRATE_FAILURE_POLICY=warn` / `abort` | `warn` was the default | still honoured; wins over `MIGRATIONS_ALLOW_FAILURE` |
54
+ | Unknown value in either variable | fell back to `warn` | falls back to `abort`, with a warning |
55
+ | A recorded migration whose FILE is gone | warning, server starts | unchanged: warning, server starts |
56
+ | No migrations bundled / no CLI in the image | skipped | unchanged: skipped |
57
+
58
+ The last-but-one row is deliberate: migrations that ran everywhere may simply be deleted once a new
59
+ instance no longer needs them (git history restores them). The migrate CLI reports such a file as a
60
+ warning, and the entrypoint passes no `--strict`. A project that wants the opposite sets
61
+ `NSC__MIGRATE__STRICT=true` explicitly — that stays available, it is just not the default.
62
+
63
+ **What to do in an existing project:** your `docker-entrypoint.sh` is your own file, so nothing
64
+ changes until you adopt the new one. To adopt it, copy `docker-entrypoint.sh` from
65
+ nest-server-starter 11.41.5. If a deploy must go out with a migration known to fail, set
66
+ `MIGRATIONS_ALLOW_FAILURE=true` for that deploy and unset it again once the migration is fixed.
67
+
68
+ This repository's own `docker-entrypoint.sh` already defaulted to `abort`; it gained the
69
+ `MIGRATIONS_ALLOW_FAILURE` alias.
70
+
71
+ ### 3. Dependency updates
72
+
73
+ | Package | 11.41.4 | 11.41.5 |
74
+ | ------- | ------- | ------- |
75
+ | `nodemailer` | 9.1.1 | **10.0.13** |
76
+ | `mongoose` | 9.10.2 | 9.10.3 |
77
+ | `@modelcontextprotocol/sdk` | 1.30.1 | 1.31.0 |
78
+ | dev: `@swc/core`, `@types/multer`, `@types/node`, `oxfmt`, `oxlint` | 1.16.2, 2.2.0, 26.6.2, 0.70.0, 1.85.0 | 1.16.12, 2.3.0, 26.6.3, 0.71.0, 1.86.0 |
79
+
80
+ **nodemailer 10 is a major, and it ships in a patch on purpose.** Five advisories cover 9.1.1 — two
81
+ of them high (GHSA-v53p-9fqp-m79j, GHSA-prgh-xp8r-p3m5), three moderate — and they are fixed only in
82
+ 10.x; there is no 9.x backport. The single breaking change of 10.0.0 is "Node.js 20 or newer is
83
+ required", and this package already requires Node >= 22.12. 10.x is a TypeScript rewrite that ships
84
+ its own declarations; `@types/nodemailer` is no longer read. Only one thing changes for code that
85
+ uses nodemailer's types directly:
86
+
87
+ ```typescript
88
+ import type * as SMTPTransport from 'nodemailer/lib/smtp-transport';
89
+
90
+ let transport: SMTPTransport; // before: the namespace WAS the class
91
+ let transport: SMTPTransport.default; // nodemailer 10: the class is the default export
92
+ let options: SMTPTransport.Options; // unchanged
93
+ ```
94
+
95
+ `MailTransportOptions` in `IServerOptions` was adjusted the same way; a project that only passes a
96
+ plain options object (`email.smtp`) is unaffected.
97
+
98
+ **What to do:**
99
+
100
+ 1. Starter-based projects: `pnpm run update` raises `mongoose` and the dev tooling to the versions above.
101
+ 2. Two overrides `pnpm run update` does NOT touch — edit `overrides:` in your `pnpm-workspace.yaml`:
102
+
103
+ ```yaml
104
+ # The lockstep entry from nest-server-starter. Left at 9.1.1 it forces the framework's
105
+ # nodemailer back DOWN to 9.1.1 — and keeps all five advisories.
106
+ nodemailer: 10.0.13
107
+
108
+ # @nestjs/swagger 11.4.7 (the newest 11.x) exact-pins js-yaml 5.3.0, covered by
109
+ # GHSA-r3ph-w7gj-g6xm (moderate, patched >=5.4.1). Replaces an older `<5.2.2` entry, which no
110
+ # longer reaches that pin. Background: docs/security-overrides.md.
111
+ 'js-yaml@>=5.0.0 <5.4.2': '5.4.2'
112
+ ```
113
+
114
+ 3. Run `pnpm install` and `pnpm audit` — both advisories must be gone.
115
+
116
+ Overrides in this repository never reach your project (pnpm applies them only to the root of an
117
+ install), which is why the js-yaml entry has to be repeated there.
118
+
119
+ ## Compatibility Notes
120
+
121
+ | Pattern | Status |
122
+ | ------- | ------ |
123
+ | `createMigrationStore(uri)` / `createMigrationStore(uri, 'migrations')` | Lock active for `up` and now `down` |
124
+ | A custom `MongoStateStore` without `lockCollectionName` | Unchanged: runs unlocked |
125
+ | `NSC__MIGRATE__STRICT=true` | Unchanged: a missing recorded migration file fails the run |
126
+ | A `nodemailer: 9.1.1` override (starter-derived projects) | Pins nodemailer 9.1.1 under the framework — raise it to `10.0.13` (§3) |
127
+ | Code typing `import * as X from 'nodemailer/lib/…'` as `X` | Use `X.default` (§3) |
128
+ | Your own `docker-entrypoint.sh` | Unchanged until you adopt the starter's (§2) |
129
+
130
+ ## Troubleshooting
131
+
132
+ | Symptom | Cause | Fix |
133
+ | ------- | ----- | --- |
134
+ | Container restarts in a loop, log: `refusing to start against a possibly half-applied schema` | A migration ran and failed (new entrypoint) | Fix the migration; for one deploy, `MIGRATIONS_ALLOW_FAILURE=true` |
135
+ | `migrate down` waits with `Waiting for migration lock release …` | Another replica is running `migrate up` | Wait; a lock whose holder died is broken after 60 s without heartbeat |
136
+ | `Strict mode: N recorded migration file(s) missing` | `NSC__MIGRATE__STRICT=true` and a migration file was deleted | Restore the file from git, or unset `NSC__MIGRATE__STRICT` |
137
+ | `pnpm audit` still reports nodemailer advisories after the update | A `nodemailer: 9.1.1` override pins the old version | Raise it to `10.0.13` |
138
+ | `pnpm audit` reports GHSA-r3ph-w7gj-g6xm (js-yaml) | No (or an old `<5.2.2`) js-yaml override | Add `'js-yaml@>=5.0.0 <5.4.2': '5.4.2'` |
139
+ | `TS2709` / `Cannot use namespace 'SMTPTransport' as a type` | nodemailer 10 types | Use `SMTPTransport.default` |
140
+
141
+ ## Module Documentation
142
+
143
+ - [Migrate module README](../src/core/modules/migrate/README.md) — locking, strict mode, CLI
144
+ - nest-server-starter `README.md` → Environment — `MIGRATE_FAILURE_POLICY` / `MIGRATIONS_ALLOW_FAILURE`