@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.
- package/.claude/rules/architecture.md +1 -0
- package/.claude/rules/configurable-features.md +1 -0
- package/.claude/rules/role-system.md +15 -1
- package/.claude/rules/testing.md +16 -4
- package/CLAUDE.md +6 -3
- package/FRAMEWORK-API.md +4 -1
- package/dist/core/common/helpers/graceful-shutdown.helper.js.map +1 -1
- package/dist/core/common/helpers/logging.helper.js +2 -0
- package/dist/core/common/helpers/logging.helper.js.map +1 -1
- package/dist/core/common/helpers/process-diagnostics.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +21 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.d.ts +2 -2
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js +46 -6
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js +7 -1
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js.map +1 -1
- package/dist/core/modules/api-token/core-api-token.constants.d.ts +6 -0
- package/dist/core/modules/api-token/core-api-token.constants.js +11 -0
- package/dist/core/modules/api-token/core-api-token.constants.js.map +1 -0
- package/dist/core/modules/api-token/core-api-token.decorators.d.ts +1 -0
- package/dist/core/modules/api-token/core-api-token.decorators.js +8 -0
- package/dist/core/modules/api-token/core-api-token.decorators.js.map +1 -0
- package/dist/core/modules/api-token/core-api-token.helpers.d.ts +127 -0
- package/dist/core/modules/api-token/core-api-token.helpers.js +398 -0
- package/dist/core/modules/api-token/core-api-token.helpers.js.map +1 -0
- package/dist/core/modules/api-token/core-api-token.middleware.d.ts +10 -0
- package/dist/core/modules/api-token/core-api-token.middleware.js +58 -0
- package/dist/core/modules/api-token/core-api-token.middleware.js.map +1 -0
- package/dist/core/modules/api-token/core-api-token.model.d.ts +20 -0
- package/dist/core/modules/api-token/core-api-token.model.js +199 -0
- package/dist/core/modules/api-token/core-api-token.model.js.map +1 -0
- package/dist/core/modules/api-token/core-api-token.module.d.ts +11 -0
- package/dist/core/modules/api-token/core-api-token.module.js +39 -0
- package/dist/core/modules/api-token/core-api-token.module.js.map +1 -0
- package/dist/core/modules/api-token/core-api-token.registry.d.ts +9 -0
- package/dist/core/modules/api-token/core-api-token.registry.js +17 -0
- package/dist/core/modules/api-token/core-api-token.registry.js.map +1 -0
- package/dist/core/modules/api-token/core-api-token.service.d.ts +104 -0
- package/dist/core/modules/api-token/core-api-token.service.js +550 -0
- package/dist/core/modules/api-token/core-api-token.service.js.map +1 -0
- package/dist/core/modules/auth/guards/roles.guard.js +17 -1
- package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth-roles.guard.js +12 -2
- package/dist/core/modules/better-auth/better-auth-roles.guard.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.middleware.js +4 -0
- package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.d.ts +4 -0
- package/dist/core/modules/better-auth/core-better-auth.module.js +18 -0
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
- package/dist/core/modules/migrate/migration-runner.d.ts +1 -0
- package/dist/core/modules/migrate/migration-runner.js +3 -0
- package/dist/core/modules/migrate/migration-runner.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant-guard.registry.d.ts +2 -0
- package/dist/core/modules/tenant/core-tenant-guard.registry.js +19 -0
- package/dist/core/modules/tenant/core-tenant-guard.registry.js.map +1 -0
- package/dist/core/modules/tenant/core-tenant.guard.d.ts +1 -0
- package/dist/core/modules/tenant/core-tenant.guard.js +28 -12
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.helpers.d.ts +1 -0
- package/dist/core/modules/tenant/core-tenant.helpers.js +19 -0
- package/dist/core/modules/tenant/core-tenant.helpers.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.module.d.ts +5 -2
- package/dist/core/modules/tenant/core-tenant.module.js +8 -0
- package/dist/core/modules/tenant/core-tenant.module.js.map +1 -1
- package/dist/core/modules/user/core-user.service.js +7 -0
- package/dist/core/modules/user/core-user.service.js.map +1 -1
- package/dist/core.module.js +6 -0
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +49 -1
- package/docs/security-overrides.md +21 -13
- package/migration-guides/11.41.3-to-11.41.4.md +172 -0
- package/migration-guides/11.41.4-to-11.41.5.md +144 -0
- package/package.json +35 -34
- package/src/core/common/helpers/graceful-shutdown.helper.ts +9 -0
- package/src/core/common/helpers/logging.helper.ts +7 -0
- package/src/core/common/helpers/process-diagnostics.helper.ts +4 -0
- package/src/core/common/interfaces/server-options.interface.ts +122 -6
- package/src/core/modules/ai/INTEGRATION-CHECKLIST.md +17 -10
- package/src/core/modules/ai/README.md +9 -1
- package/src/core/modules/ai/helpers/ai-mcp-oauth.helper.ts +74 -7
- package/src/core/modules/ai/services/core-ai-mcp-oauth.service.ts +13 -1
- package/src/core/modules/api-token/INTEGRATION-CHECKLIST.md +121 -0
- package/src/core/modules/api-token/README.md +212 -0
- package/src/core/modules/api-token/core-api-token.constants.ts +27 -0
- package/src/core/modules/api-token/core-api-token.decorators.ts +29 -0
- package/src/core/modules/api-token/core-api-token.helpers.ts +711 -0
- package/src/core/modules/api-token/core-api-token.middleware.ts +57 -0
- package/src/core/modules/api-token/core-api-token.model.ts +193 -0
- package/src/core/modules/api-token/core-api-token.module.ts +48 -0
- package/src/core/modules/api-token/core-api-token.registry.ts +53 -0
- package/src/core/modules/api-token/core-api-token.service.ts +822 -0
- package/src/core/modules/auth/guards/roles.guard.ts +23 -2
- package/src/core/modules/better-auth/better-auth-roles.guard.ts +18 -4
- package/src/core/modules/better-auth/core-better-auth.middleware.ts +8 -0
- package/src/core/modules/better-auth/core-better-auth.module.ts +33 -0
- package/src/core/modules/migrate/README.md +15 -7
- package/src/core/modules/migrate/migration-runner.ts +9 -0
- package/src/core/modules/tenant/README.md +17 -0
- package/src/core/modules/tenant/core-tenant-guard.registry.ts +36 -0
- package/src/core/modules/tenant/core-tenant.guard.ts +52 -12
- package/src/core/modules/tenant/core-tenant.helpers.ts +30 -0
- package/src/core/modules/tenant/core-tenant.module.ts +19 -2
- package/src/core/modules/user/core-user.service.ts +14 -0
- package/src/core.module.ts +12 -0
- 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
|
|
15
|
-
|
|
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`
|
|
20
|
-
| `js-yaml` | [GHSA-pm4m-ph32-ghv5](https://github.com/advisories/GHSA-pm4m-ph32-ghv5) — high: exponential parsing time in flow collections (DoS)
|
|
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
|
|
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
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
|
|
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
|
-
#
|
|
48
|
-
'multer@>=2.0.0 <2.
|
|
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`
|