@lenne.tech/nest-server 11.41.3 → 11.41.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.
Files changed (103) 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 +2 -1
  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 +20 -0
  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/tenant/core-tenant-guard.registry.d.ts +2 -0
  51. package/dist/core/modules/tenant/core-tenant-guard.registry.js +19 -0
  52. package/dist/core/modules/tenant/core-tenant-guard.registry.js.map +1 -0
  53. package/dist/core/modules/tenant/core-tenant.guard.d.ts +1 -0
  54. package/dist/core/modules/tenant/core-tenant.guard.js +28 -12
  55. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  56. package/dist/core/modules/tenant/core-tenant.helpers.d.ts +1 -0
  57. package/dist/core/modules/tenant/core-tenant.helpers.js +19 -0
  58. package/dist/core/modules/tenant/core-tenant.helpers.js.map +1 -1
  59. package/dist/core/modules/tenant/core-tenant.module.d.ts +5 -2
  60. package/dist/core/modules/tenant/core-tenant.module.js +8 -0
  61. package/dist/core/modules/tenant/core-tenant.module.js.map +1 -1
  62. package/dist/core/modules/user/core-user.service.js +7 -0
  63. package/dist/core/modules/user/core-user.service.js.map +1 -1
  64. package/dist/core.module.js +6 -0
  65. package/dist/core.module.js.map +1 -1
  66. package/dist/index.d.ts +7 -0
  67. package/dist/index.js +7 -0
  68. package/dist/index.js.map +1 -1
  69. package/dist/tsconfig.build.tsbuildinfo +1 -1
  70. package/docs/REQUEST-LIFECYCLE.md +49 -1
  71. package/docs/security-overrides.md +16 -10
  72. package/migration-guides/11.41.3-to-11.41.4.md +172 -0
  73. package/package.json +29 -29
  74. package/src/core/common/helpers/graceful-shutdown.helper.ts +9 -0
  75. package/src/core/common/helpers/logging.helper.ts +7 -0
  76. package/src/core/common/helpers/process-diagnostics.helper.ts +4 -0
  77. package/src/core/common/interfaces/server-options.interface.ts +110 -0
  78. package/src/core/modules/ai/INTEGRATION-CHECKLIST.md +17 -10
  79. package/src/core/modules/ai/README.md +9 -1
  80. package/src/core/modules/ai/helpers/ai-mcp-oauth.helper.ts +74 -7
  81. package/src/core/modules/ai/services/core-ai-mcp-oauth.service.ts +13 -1
  82. package/src/core/modules/api-token/INTEGRATION-CHECKLIST.md +121 -0
  83. package/src/core/modules/api-token/README.md +212 -0
  84. package/src/core/modules/api-token/core-api-token.constants.ts +27 -0
  85. package/src/core/modules/api-token/core-api-token.decorators.ts +29 -0
  86. package/src/core/modules/api-token/core-api-token.helpers.ts +711 -0
  87. package/src/core/modules/api-token/core-api-token.middleware.ts +57 -0
  88. package/src/core/modules/api-token/core-api-token.model.ts +193 -0
  89. package/src/core/modules/api-token/core-api-token.module.ts +48 -0
  90. package/src/core/modules/api-token/core-api-token.registry.ts +53 -0
  91. package/src/core/modules/api-token/core-api-token.service.ts +822 -0
  92. package/src/core/modules/auth/guards/roles.guard.ts +23 -2
  93. package/src/core/modules/better-auth/better-auth-roles.guard.ts +18 -4
  94. package/src/core/modules/better-auth/core-better-auth.middleware.ts +8 -0
  95. package/src/core/modules/better-auth/core-better-auth.module.ts +33 -0
  96. package/src/core/modules/tenant/README.md +17 -0
  97. package/src/core/modules/tenant/core-tenant-guard.registry.ts +36 -0
  98. package/src/core/modules/tenant/core-tenant.guard.ts +52 -12
  99. package/src/core/modules/tenant/core-tenant.helpers.ts +30 -0
  100. package/src/core/modules/tenant/core-tenant.module.ts +19 -2
  101. package/src/core/modules/user/core-user.service.ts +14 -0
  102. package/src/core.module.ts +12 -0
  103. 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,20 @@ 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` | Older `@nestjs/swagger` releases declare `"js-yaml": "5.2.1"` — an **exact pin**, the same shape as the `ws` case. 11.4.7, the version this framework declares, pins `5.3.0` |
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.4:** with the `@nestjs/*` versions this framework declares, all three resolve to
24
+ a patched version on their own. The entries below are insurance for a project whose own
25
+ `package.json` pins an older `@nestjs/graphql`, `@nestjs/swagger` or `@nestjs/platform-express` —
26
+ duplicates of those are exactly how an old exact pin comes back. Keep them; they cost nothing while
27
+ inert, and keep the `multer` target in lockstep (now `2.4.0`).
22
28
 
23
29
  `@nestjs/graphql` is a plain `dependencies` entry, so `ws` is installed even when you run with
24
30
  `graphQl: false`. `@nestjs/swagger` and `@nestjs/platform-express` are likewise plain `dependencies`
@@ -37,15 +43,15 @@ overrides:
37
43
  # Remove once @nestjs/graphql stops pinning it.
38
44
  'ws@>=8.0.0 <8.21.0': '8.21.3'
39
45
 
40
- # @nestjs/swagger exact-pins js-yaml@5.2.1 (GHSA-pm4m-ph32-ghv5, high, patched >=5.2.2).
46
+ # Older @nestjs/swagger releases exact-pin js-yaml@5.2.1 (GHSA-pm4m-ph32-ghv5, high, patched >=5.2.2).
41
47
  # Same shape as the ws entry: an exact pin cannot resolve forward.
42
48
  # Remove once @nestjs/swagger stops pinning it.
43
49
  'js-yaml@>=5.0.0 <5.2.2': '5.2.2'
44
50
 
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'
51
+ # @nestjs/platform-express <=11.2.5 exact-pins multer@2.2.0 (GHSA-wc9g-mqfw-jrwm and three more, patched >=2.3.0).
52
+ # Keep the target in LOCKSTEP with the multer version @lenne.tech/nest-server declares (2.4.0 since 11.41.4).
53
+ # Inert on @nestjs/platform-express 11.2.6+, which pins 2.4.0 itself.
54
+ 'multer@>=2.0.0 <2.4.0': '2.4.0'
49
55
  ```
50
56
 
51
57
  ### 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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.41.3",
3
+ "version": "11.41.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",
@@ -16,8 +16,8 @@
16
16
  "scripts": {
17
17
  "build": "rimraf dist && nest build && pnpm run build:copy-types && pnpm run build:copy-templates && pnpm run build:add-type-references && pnpm run build:framework-api",
18
18
  "build:add-type-references": "node scripts/add-type-references.js",
19
- "build:copy-templates": "mkdir -p dist/core/modules/migrate/templates && cp src/core/modules/migrate/templates/migration-project.template.ts dist/core/modules/migrate/templates/",
20
- "build:copy-types": "mkdir -p dist/types && cp src/types/*.d.ts dist/types/",
19
+ "build:copy-templates": "node scripts/copy-build-assets.mjs templates",
20
+ "build:copy-types": "node scripts/copy-build-assets.mjs types",
21
21
  "build:dev": "pnpm run build",
22
22
  "build:framework-api": "tsx scripts/generate-framework-api.ts",
23
23
  "build:pack": "pnpm pack && echo \"use file:/ROOT_PATH_TO_TGZ_FILE to integrate the package\"",
@@ -92,42 +92,42 @@
92
92
  "@apollo/server": "5.5.1",
93
93
  "@as-integrations/express5": "1.1.2",
94
94
  "@getbrevo/brevo": "6.0.3",
95
- "@modelcontextprotocol/sdk": "1.30.0",
95
+ "@modelcontextprotocol/sdk": "1.30.1",
96
96
  "@nestjs/apollo": "13.4.5",
97
- "@nestjs/common": "11.2.1",
98
- "@nestjs/core": "11.2.1",
97
+ "@nestjs/common": "11.2.6",
98
+ "@nestjs/core": "11.2.6",
99
99
  "@nestjs/graphql": "13.4.5",
100
100
  "@nestjs/jwt": "11.0.2",
101
101
  "@nestjs/mongoose": "11.0.4",
102
102
  "@nestjs/passport": "11.0.5",
103
- "@nestjs/platform-express": "11.2.1",
103
+ "@nestjs/platform-express": "11.2.6",
104
104
  "@nestjs/schedule": "6.1.3",
105
105
  "@nestjs/swagger": "11.4.7",
106
106
  "@nestjs/terminus": "11.1.1",
107
107
  "@tus/file-store": "2.1.1",
108
- "@tus/server": "2.4.4",
108
+ "@tus/server": "2.4.5",
109
109
  "@types/supertest": "7.2.1",
110
110
  "bcrypt": "6.0.0",
111
111
  "class-transformer": "0.5.1",
112
112
  "class-validator": "0.15.1",
113
- "compression": "1.8.1",
113
+ "compression": "1.8.2",
114
114
  "cookie-parser": "1.4.7",
115
115
  "cron": "4.4.0",
116
116
  "dotenv": "17.4.2",
117
117
  "ejs": "6.0.1",
118
118
  "express": "5.2.1",
119
- "graphql": "16.14.0",
119
+ "graphql": "16.14.2",
120
120
  "graphql-query-complexity": "2.0.0",
121
121
  "graphql-subscriptions": "3.0.0",
122
122
  "graphql-upload": "15.0.2",
123
123
  "graphql-ws": "6.2.1",
124
- "jose": "6.2.9",
124
+ "jose": "6.2.12",
125
125
  "js-sha256": "1.0.0",
126
126
  "json-to-graphql-query": "2.3.0",
127
127
  "lodash": "4.18.1",
128
- "mongodb": "7.5.0",
129
- "mongoose": "9.9.3",
130
- "multer": "2.3.0",
128
+ "mongodb": "7.6.0",
129
+ "mongoose": "9.10.2",
130
+ "multer": "2.4.0",
131
131
  "node-mailjet": "6.0.11",
132
132
  "nodemailer": "9.1.1",
133
133
  "passport": "0.7.0",
@@ -135,7 +135,7 @@
135
135
  "reflect-metadata": "0.2.2",
136
136
  "rfdc": "1.4.1",
137
137
  "rxjs": "7.8.2",
138
- "supertest": "7.2.2",
138
+ "supertest": "7.3.0",
139
139
  "ts-morph": "28.0.0",
140
140
  "ws": "8.21.3",
141
141
  "yuml-diagram": "1.2.0"
@@ -177,47 +177,47 @@
177
177
  }
178
178
  },
179
179
  "devDependencies": {
180
- "@aws-sdk/client-s3": "3.1115.0",
181
- "@aws-sdk/s3-request-presigner": "3.1115.0",
180
+ "@aws-sdk/client-s3": "3.1140.0",
181
+ "@aws-sdk/s3-request-presigner": "3.1140.0",
182
182
  "@better-auth/core": "1.7.1",
183
183
  "@better-auth/passkey": "1.7.1",
184
184
  "@compodoc/compodoc": "2.0.0",
185
185
  "@nestjs/cli": "11.0.24",
186
186
  "@nestjs/schematics": "11.1.0",
187
- "@nestjs/testing": "11.2.1",
187
+ "@nestjs/testing": "11.2.6",
188
188
  "@swc/cli": "0.8.1",
189
- "@swc/core": "1.16.1",
190
- "@tus/s3-store": "2.0.6",
189
+ "@swc/core": "1.16.2",
190
+ "@tus/s3-store": "2.0.7",
191
191
  "@types/compression": "1.8.1",
192
192
  "@types/cookie-parser": "1.4.10",
193
193
  "@types/ejs": "3.1.5",
194
194
  "@types/express": "5.0.6",
195
195
  "@types/lodash": "4.17.25",
196
196
  "@types/multer": "2.2.0",
197
- "@types/node": "26.2.0",
198
- "@types/nodemailer": "8.0.1",
197
+ "@types/node": "26.6.2",
198
+ "@types/nodemailer": "8.0.2",
199
199
  "@types/passport": "1.0.17",
200
200
  "@vitest/coverage-v8": "4.1.11",
201
201
  "ansi-colors": "4.1.3",
202
202
  "better-auth": "1.7.1",
203
- "bullmq": "6.2.0",
203
+ "bullmq": "6.3.8",
204
204
  "cross-env": "10.1.0",
205
205
  "find-file-up": "2.0.1",
206
206
  "husky": "9.1.7",
207
207
  "ioredis": "6.0.0",
208
208
  "nodemon": "3.1.14",
209
209
  "npm-watch": "0.13.0",
210
- "otpauth": "9.5.1",
211
- "oxfmt": "0.64.0",
212
- "oxlint": "1.79.0",
210
+ "otpauth": "9.5.2",
211
+ "oxfmt": "0.70.0",
212
+ "oxlint": "1.85.0",
213
213
  "rimraf": "6.1.3",
214
214
  "ts-node": "10.9.2",
215
215
  "tsconfig-paths": "4.2.0",
216
- "tsx": "4.23.12",
216
+ "tsx": "4.23.15",
217
217
  "tus-js-client": "4.3.1",
218
218
  "typescript": "5.9.3",
219
- "unplugin-swc": "1.5.11",
220
- "vite": "8.2.2",
219
+ "unplugin-swc": "2.0.0",
220
+ "vite": "8.3.1",
221
221
  "vite-plugin-node": "8.0.0",
222
222
  "vitest": "4.1.11"
223
223
  },
@@ -47,6 +47,15 @@ const SHUTDOWN_DELAY_ADVISORY_MS = 10_000;
47
47
  * Without a configured delay this is exactly `app.enableShutdownHooks()`, which is what the
48
48
  * framework did before.
49
49
  *
50
+ * **Windows: a termination from outside runs none of this.** Windows has no SIGTERM to deliver.
51
+ * Ending a process from outside, whether by `child.kill()`, `taskkill /F` (what `lt dev down` and
52
+ * the `check` watchdog use) or the Task Manager, terminates it at once. Measured on the Windows CI
53
+ * runner (`tests/unit/process-diagnostics-signal.spec.ts`): the process exits and the handler
54
+ * never runs. So on Windows neither `shutdownDelayMs` nor any `onModuleDestroy` /
55
+ * `onApplicationShutdown` hook runs on such a termination. Ctrl+C in a console window reaches Node
56
+ * as SIGINT and SHOULD reach this handler — that path is **unmeasured**. Do not rely on a graceful
57
+ * drain there; production targets Linux containers, where all of the above holds.
58
+ *
50
59
  * @param app the Nest application to shut down
51
60
  * @returns the same app, so it can be chained
52
61
  */
@@ -171,6 +171,13 @@ export function redactSensitiveText(text: string): string {
171
171
  /(\/(?:request-password-reset|reset-password|forget-password|forgot-password|set-password|change-email|magic-?link|verify|reset|confirm|activate|invite)\/)([A-Za-z0-9._~-]{16,})/gi,
172
172
  (_m, prefix, token) => `${prefix}${maskToken(token)}`,
173
173
  )
174
+ // tenant API tokens (`<prefix>_<24 hex>_<64 hex>`) and their signed assertions
175
+ // (`<prefix>s_<payload>.<43-char HMAC>`) anywhere in the line — they are long-lived bearer
176
+ // credentials, and a line that quotes one outside an Authorization header would pass the rule below
177
+ .replace(/\b[a-z][a-z0-9]{1,15}_[0-9a-f]{24}_[0-9a-f]{64}\b/g, (match) => maskToken(match))
178
+ .replace(/\b[a-z][a-z0-9]{1,15}s_[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{43}(?![A-Za-z0-9_-])/g, (match) =>
179
+ maskToken(match),
180
+ )
174
181
  // authorization: Bearer xyz / Authorization=xyz
175
182
  .replace(
176
183
  /(authorization["']?\s*[:=]\s*["']?)(?:Bearer\s+)?([^\s"',;]+)/gi,
@@ -208,6 +208,10 @@ function describeError(value: unknown): string {
208
208
  * Call this as the first statement of `bootstrap()`, before `NestFactory.create()`. See the module
209
209
  * docblock for why it must not live inside `CoreModule.forRoot()`.
210
210
  *
211
+ * On Windows the signal labels are not written when the process is ended from outside: there is
212
+ * no signal to deliver, the process is terminated at once and no handler runs (measured on the
213
+ * Windows CI runner, see `installGracefulShutdown()`). Ctrl+C in a console is unmeasured.
214
+ *
211
215
  * @param options - Injectable dependencies; defaults target the real `process`
212
216
  *
213
217
  * @example