@lenne.tech/nest-server 11.42.3 → 11.42.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 (34) hide show
  1. package/.claude/rules/configurable-features.md +1 -0
  2. package/.claude/rules/package-management.md +70 -4
  3. package/.claude/rules/testing.md +4 -4
  4. package/CLAUDE.md +2 -1
  5. package/FRAMEWORK-API.md +2 -1
  6. package/dist/core/common/interfaces/server-options.interface.d.ts +8 -0
  7. package/dist/core/common/services/core-body-parser.initializer.d.ts +26 -0
  8. package/dist/core/common/services/core-body-parser.initializer.js +136 -0
  9. package/dist/core/common/services/core-body-parser.initializer.js.map +1 -0
  10. package/dist/core/modules/better-auth/core-better-auth.module.js +7 -3
  11. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  12. package/dist/core/modules/error-code/error-codes.d.ts +9 -0
  13. package/dist/core/modules/error-code/error-codes.js +8 -0
  14. package/dist/core/modules/error-code/error-codes.js.map +1 -1
  15. package/dist/core.module.js +2 -0
  16. package/dist/core.module.js.map +1 -1
  17. package/dist/server/modules/error-code/error-codes.d.ts +1 -0
  18. package/dist/test/test.helper.d.ts +60 -0
  19. package/dist/test/test.helper.js +104 -0
  20. package/dist/test/test.helper.js.map +1 -1
  21. package/dist/tsconfig.build.tsbuildinfo +1 -1
  22. package/docs/REQUEST-LIFECYCLE.md +28 -0
  23. package/docs/error-codes.md +16 -0
  24. package/docs/security-overrides.md +18 -2
  25. package/migration-guides/11.42.3-to-11.42.4.md +199 -0
  26. package/migration-guides/11.42.4-to-11.42.5.md +104 -0
  27. package/package.json +3 -3
  28. package/src/core/common/interfaces/server-options.interface.ts +89 -0
  29. package/src/core/common/services/core-body-parser.initializer.ts +232 -0
  30. package/src/core/modules/better-auth/core-better-auth.module.ts +24 -4
  31. package/src/core/modules/error-code/error-codes.ts +9 -0
  32. package/src/core.module.ts +4 -0
  33. package/src/test/README.md +79 -1
  34. package/src/test/test.helper.ts +303 -0
@@ -45,6 +45,7 @@ The `CoreModule` is a dynamic module that bootstraps the entire framework:
45
45
  | **Central S3 (optional)** | `CoreS3Service` — globally provided **and exported** by `CoreModule`, inert unless `s3` is configured *and* names a `bucket` (a bucket-less block is ignored with a warning). Backs `file.storage: 's3'` and TUS staging (`tus.s3Staging`). Requires the OPTIONAL peers `@aws-sdk/client-s3` (+ `@aws-sdk/s3-request-presigner` for `presignedDownloads`) — configured but missing **fails the boot**. `@aws-sdk/lib-storage` (optional) uploads streams of unknown length in parts; without it they are buffered in memory, with one boot warning |
46
46
  | **Configuration System** | `config.env.ts` with ENV variables, `NEST_SERVER_CONFIG` JSON, `NSC__*` prefixes |
47
47
  | **Cookie Handling** | Enabled by default (`cookies: true`), configurable via `ICookiesConfig` with `exposeTokenInBody` option |
48
+ | **Body Parser Limits** | `bodyParser.json.limit` / `bodyParser.urlencoded.limit` (default 100 kB each, body-parser's own). Applied in place to the parsers NestJS registers, so they hold under `Test.createTestingModule()` too. Every 413 is answered with `#LTNS_0304` plus size and limit, and logged with the route and the config key — see [0d. Body parsers](#0d-body-parsers) |
48
49
  | **Unified CORS** | Single `cors` config propagates to GraphQL, REST, and BetterAuth layers |
49
50
  | **Dual Auth Modes** | IAM-Only (BetterAuth) or Legacy+IAM for migration periods |
50
51
 
@@ -326,6 +327,12 @@ The following diagram shows the exact order of execution from HTTP request to re
326
327
  | - credentials: true when cookies enabled |
327
328
  | - Origins from appUrl/baseUrl/cors.allowedOrigins |
328
329
  | - Propagated to BetterAuth trustedOrigins |
330
+ | |
331
+ | 0d. Body parsers: json + urlencoded [always] |
332
+ | - Registered by NestJS in app.init(), limits from |
333
+ | bodyParser.*.limit (default 100 kB each) |
334
+ | - Over the limit: 413 #LTNS_0304 BEFORE auth and |
335
+ | routing, REST and GraphQL alike |
329
336
  +----------------------------+----------------------------+
330
337
  |
331
338
  +----------------------------v----------------------------+
@@ -551,6 +558,27 @@ if (!isCorsDisabled(envConfig.cors)) {
551
558
 
552
559
  > **Security:** steps 2 and 4 grant the derived origin credentialed CORS. If the apex domain is not trusted (e.g. a third-party-hosted marketing site whose XSS surface you do not control), set `cors.deriveAppUrl: false` and list the frontend origin explicitly via `appUrl` or `cors.allowedOrigins`. The derivation never yields a bare TLD (`https://api.dev` stays unchanged) and never emits the opaque `null` origin. With `cors.deriveAppUrl: false`, a host-split localhost `baseUrl` falls back to the `http://localhost:3001` default.
553
560
 
561
+ #### 0d. Body parsers
562
+
563
+ NestJS registers two global body parsers on every Express application — `application/json` and `application/x-www-form-urlencoded` (`extended: true`) — inside `app.init()`, i.e. after everything `main.ts` registers and before the NestJS middleware chain. GraphQL has no parser of its own: Apollo reads the body the global JSON parser produced, so one limit governs REST and GraphQL.
564
+
565
+ Both use body-parser's default limit of **100 kB** unless `bodyParser` says otherwise (since 11.42.4):
566
+
567
+ ```typescript
568
+ // config.env.ts — documents carry HTML: allow 2 MB of JSON, keep form posts at the default
569
+ bodyParser: { json: { limit: '2mb' } },
570
+ ```
571
+
572
+ `CoreBodyParserInitializer` applies it during module init by replacing the handle of the global parser layers NestJS created — in place, because NestJS registers them before any init hook runs and a parser appended later would sit behind the routes. Running during module init is what makes the limit hold under `Test.createTestingModule()`, which never runs `main.ts`. A path-scoped parser (`server.use('/upload', json({ limit }))` in `main.ts`) runs before the global one and is left untouched — that remains the way to give one path a different limit.
573
+
574
+ The parser runs **before authentication and routing**. Over the limit, the whole request is refused:
575
+
576
+ ```json
577
+ { "statusCode": 413, "message": "#LTNS_0304: Request body too large [137270 bytes, limit 102400 bytes]", "error": "Payload Too Large" }
578
+ ```
579
+
580
+ and the server logs a warning with method, path (query string stripped) and the config key to raise. Other parser errors (malformed JSON → 400) pass through unchanged. The default is deliberately not raised for everybody: `JSON.parse` is synchronous, and a larger pre-auth limit is a larger pre-auth event-loop block for every route, including unauthenticated ones.
581
+
554
582
  ### NestJS Middleware Chain (CoreModule)
555
583
 
556
584
  NestJS middleware runs for every request after Express-level middleware. Registration happens in `CoreModule.configure()`:
@@ -262,6 +262,22 @@ All error codes follow the format: `#PREFIX_XXXX: Description`
262
262
 
263
263
  ---
264
264
 
265
+ ### LTNS_0304: requestBodyTooLarge
266
+
267
+ **Message:** Request body too large
268
+
269
+ **Description:** Answered with HTTP 413 when a JSON or URL-encoded request body exceeds the body parser's limit. The parser runs before authentication and routing, so the whole request is rejected. The message carries the size and the limit in brackets, e.g. `#LTNS_0304: Request body too large [137270 bytes, limit 102400 bytes]` (the size is missing when the client sent no `Content-Length`). The server logs a warning with the route and the config key.
270
+
271
+ **Solution:** Send a smaller body (split the operation, or upload large content through the file endpoints). If payloads of this size are legitimate for the route, raise the limit via `bodyParser.json.limit` / `bodyParser.urlencoded.limit` in the server options.
272
+
273
+ **Parameters:** none (the size details are part of the developer message, not of the translation)
274
+
275
+ **Translations:**
276
+ - DE: Die gesendeten Daten sind zu groß und wurden nicht verarbeitet.
277
+ - EN: The submitted data is too large and was not processed.
278
+
279
+ ---
280
+
265
281
  ## Resource Errors (LTNS_0400-LTNS_0499)
266
282
 
267
283
  ### LTNS_0400: resourceNotFound
@@ -11,15 +11,23 @@ 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 its `@nestjs/*` dependencies used to
15
- **exact-pin** to a vulnerable version:
14
+ The framework pulls in four transitive packages that its `@nestjs/*` dependencies exact-pin, or
15
+ used to **exact-pin**, to a vulnerable version:
16
16
 
17
17
  | Package | Advisory | Why it cannot resolve forward on its own |
18
18
  |---------|----------|------------------------------------------|
19
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
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
+ | `@graphql-tools/utils` | [GHSA-7mx3-vvmw-hjmv](https://github.com/advisories/GHSA-7mx3-vvmw-hjmv) — high: prototype pollution in `mergeDeep`, which runs while the GraphQL schema is assembled. Covers `<=12.0.0`, patched `>=12.0.1` (added 2026-10-06) | `@nestjs/graphql` 13.4.5 — the version this framework declares, and the newest 13.x — declares `"@graphql-tools/utils": "12.0.0"`, an **exact pin**. Only `@nestjs/graphql` 14.x moves it, so **this entry is load-bearing**. `@apollo/server` reaches it too, through `@graphql-tools/schema` / `merge` — and in a lockfile that still holds schema 10.0.x (resolved before 2026-08-12), as **utils 11.x**: vulnerable too, with no 11.x fix, and outside the override key. Lift it with `pnpm update --depth Infinity @graphql-tools/schema @graphql-tools/merge`; the current releases request utils `^12.0.3`, which needs Node >=22.15 |
21
22
  | `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, which this framework declared from 11.41.4, pins `2.4.0`, and so does 11.2.7, declared since 11.41.7 |
22
23
 
24
+ **Status since 11.42.4:** `@graphql-tools/utils` joins `js-yaml` as load-bearing — no `@nestjs/graphql`
25
+ 13.x resolves past the exact pin. The 11.42.4 release also covers `proxy-addr` (GHSA-jqcg-44mw-7w3h,
26
+ critical: IP spoofing through an IPv4-mapped IPv6 address in a trusted subnet — it is what
27
+ `trustProxy` hands to Express). That one needs **no override**: `express` requests `^2.0.7` and the
28
+ fix `2.0.8` is in range, but an existing lockfile keeps `2.0.7` until you refresh it:
29
+ `pnpm update --depth Infinity proxy-addr`.
30
+
23
31
  **Status since 11.41.5:** `ws` and `multer` resolve to a patched version on their own with the
24
32
  `@nestjs/*` versions this framework declares; their entries below are insurance for a project whose
25
33
  own `package.json` pins an older `@nestjs/graphql` or `@nestjs/platform-express`. **`js-yaml` does
@@ -54,6 +62,14 @@ overrides:
54
62
  # Keep the target in LOCKSTEP with the multer version @lenne.tech/nest-server declares (2.4.0 since 11.41.4).
55
63
  # Inert on @nestjs/platform-express 11.2.6+, which pins 2.4.0 itself.
56
64
  'multer@>=2.0.0 <2.4.0': '2.4.0'
65
+
66
+ # @nestjs/graphql 13.4.5 exact-pins @graphql-tools/utils@12.0.0 — GHSA-7mx3-vvmw-hjmv (high, patched >=12.0.1).
67
+ # Target 12.0.1 rather than the newest 12.x: 12.0.2+ requires Node >=22.15 via @whatwg-node/promise-helpers@2.
68
+ # If your engines floor is already >=22.15, prefer the newest 12.x, so the caret requesters are not held back.
69
+ # Does NOT reach a utils 11.x pulled in by an older @graphql-tools/schema 10.0.x — refresh that one instead:
70
+ # pnpm update --depth Infinity @graphql-tools/schema @graphql-tools/merge (current releases need Node >=22.15)
71
+ # Remove once @nestjs/graphql pins >=12.0.1 in the major you use.
72
+ '@graphql-tools/utils@>=12.0.0 <12.0.1': '12.0.1'
57
73
  ```
58
74
 
59
75
  ### Retired: `@hono/node-server` (removed 2026-08-22, nest-server 11.36.1)
@@ -0,0 +1,199 @@
1
+ # Migration Guide: 11.42.3 → 11.42.4
2
+
3
+ ## Overview
4
+
5
+ | Category | Details |
6
+ | -------------------- | ---------------------------------------------------------------------------------------------------------------- |
7
+ | **New option** | `bodyParser.json.limit` / `bodyParser.urlencoded.limit` — the request body limit, configurable for the first time (§1) |
8
+ | **Improvement** | A request over the body limit is answered with `#LTNS_0304` plus size and limit, and logged with the route (§2) |
9
+ | **Security** | `proxy-addr` 2.0.8 (critical) and `@graphql-tools/utils` 12.0.1 (high) — your project needs a lockfile refresh and one override (§3) |
10
+ | **Bugfix** | Without `betterAuth.secret` and with JWT secrets shorter than 32 characters, native Better-Auth endpoints rejected the session right after sign-in (§4) |
11
+ | **Dependencies** | `nodemailer` 10.0.14 (patch: address-parser and DKIM header fixes) — no action needed |
12
+ | **Migration Effort** | Small — `pnpm update` plus one override in `pnpm-workspace.yaml` (§3). Projects that raise the body limit in `main.ts` can move it into the config (§1) |
13
+
14
+ ## Quick Migration
15
+
16
+ ```bash
17
+ pnpm update @lenne.tech/nest-server
18
+ pnpm update --depth Infinity proxy-addr
19
+ ```
20
+
21
+ Then add the `@graphql-tools/utils` override from §3 to `pnpm-workspace.yaml`, run `pnpm install`,
22
+ refresh `source-map-js` (§3), check §3 for an older `@graphql-tools/utils` 11.x copy, which the
23
+ override does not reach, and commit `pnpm-workspace.yaml` together with `pnpm-lock.yaml`.
24
+ Vendor-mode projects pick up the core update and need the same two dependency steps. The body limit
25
+ needs no configuration: unset, both parsers keep the 100 kB they had.
26
+
27
+ ## 1. The request body limit is configurable
28
+
29
+ NestJS registers a JSON and a URL-encoded body parser on every application, and until now neither had
30
+ a limit anybody chose: body-parser's default of **100 kB** applied, and nothing in the framework or the
31
+ starter could change it. A request over it is refused in middleware — before authentication, before
32
+ routing, GraphQL included — so a document that grew past 100 kB could no longer be saved, and every
33
+ retry failed the same way.
34
+
35
+ ```typescript
36
+ // config.env.ts — documents carry HTML: allow 2 MB of JSON, keep form posts at the default
37
+ bodyParser: {
38
+ json: { limit: '2mb' },
39
+ },
40
+ ```
41
+
42
+ | Value | Meaning |
43
+ | ---------------------- | -------------------------------------------------------------------- |
44
+ | unset | 100 kB, exactly as before |
45
+ | `2097152` | bytes |
46
+ | `'500kb'`, `'2mb'` | 1024-based size; units `b` / `kb` / `mb` / `gb`, case-insensitive |
47
+ | `0`, negative, `'lots'` | refused with an error log at boot; the parser keeps its limit |
48
+
49
+ The JSON and URL-encoded limits are separate on purpose: raising one does not raise the other.
50
+
51
+ **Raise it only where the data model needs it, and only as far.** The parser runs before
52
+ authentication, and `JSON.parse` blocks the event loop for the whole body — measured on a realistic
53
+ document shape, about 0.5 ms for 100 kB, 16 ms for 1 MB and 55 ms for 2 MB per request. That is why
54
+ the default stays at 100 kB.
55
+
56
+ **If you raise the limit in `main.ts` today**, move it into the config:
57
+
58
+ ```typescript
59
+ // main.ts — before: not applied in the e2e suite
60
+ server.useBodyParser('json', { limit: '2mb' });
61
+
62
+ // config.env.ts — after: applied in production AND under Test.createTestingModule()
63
+ bodyParser: { json: { limit: '2mb' } },
64
+ ```
65
+
66
+ **Starter-based projects:** `config.env.ts` builds `local` / `e2e` / `ci` from `localConfig()` and the
67
+ deployed environments from `deployedConfig()`. Put the limit where BOTH see it — the starter ships a
68
+ shared `PROJECT_BODY_PARSER` constant for this since its 11.42.4 update — or set
69
+ `NSC__BODY_PARSER__JSON__LIMIT` in the environment. A value only in the deployed block leaves the e2e
70
+ suite and `lt dev up` at 100 kB.
71
+
72
+ The config is applied during module init, so it holds in every test that builds the app with
73
+ `Test.createTestingModule(...).createNestApplication()` — which never runs `main.ts`, so a
74
+ `useBodyParser()` call there never reached the e2e suite. Once configured, the value also replaces a
75
+ global parser your `main.ts` registered; keeping both is harmless, but the config wins.
76
+
77
+ A **path-scoped** parser is left alone: `server.use('/upload', json({ limit: '20mb' }))` in `main.ts`
78
+ still gives that one path its own limit. The parser runs before routing, so a per-route limit can only
79
+ key on the path — and all GraphQL operations share one.
80
+
81
+ **`rawBody: true`:** `req.rawBody` is kept for apps created with `NestFactory.create(…, { rawBody:
82
+ true })`. Under `Test.createTestingModule()` the options passed to `createNestApplication()` are not
83
+ visible to providers, so a test app created with `{ rawBody: true }` gets no `req.rawBody` once a limit
84
+ is configured.
85
+
86
+ ## 2. An oversized request says what happened
87
+
88
+ Before, the caller received:
89
+
90
+ ```json
91
+ { "statusCode": 413, "message": "request entity too large" }
92
+ ```
93
+
94
+ and the server logged the same text as an error with a stack trace. Now, configured or not:
95
+
96
+ ```json
97
+ { "statusCode": 413, "message": "#LTNS_0304: Request body too large [137270 bytes, limit 102400 bytes]", "error": "Payload Too Large" }
98
+ ```
99
+
100
+ The size is present when the client sent a `Content-Length`. Frontends using the error translation of
101
+ `@lenne.tech/nuxt-extensions` show the translated `LTNS_0304` text. The server logs one warning per
102
+ refused request, with method, path (without query string) and the config key to raise:
103
+
104
+ ```
105
+ 413 PATCH /offers/6650…: json body of 137270 bytes exceeds the limit of 102400 bytes. If payloads of this size are legitimate here, raise `bodyParser.json.limit` in the server options.
106
+ ```
107
+
108
+ The status code is unchanged. Only clients that compare the message text against
109
+ `'request entity too large'` need adjusting — branch on the status or on `LTNS_0304` instead.
110
+
111
+ ## 3. Dependency advisories your project must fix itself
112
+
113
+ `pnpm` overrides apply only to the project that runs the install, so the framework's own fixes do not
114
+ reach your tree — see [docs/security-overrides.md](../docs/security-overrides.md).
115
+
116
+ | Package | Advisory | What your project needs |
117
+ | ------- | -------- | ----------------------- |
118
+ | `proxy-addr` | [GHSA-jqcg-44mw-7w3h](https://github.com/advisories/GHSA-jqcg-44mw-7w3h) — **critical**: IP spoofing through an IPv4-mapped IPv6 address in a trusted subnet. Patched `>=2.0.8` | A lockfile refresh. `express` requests `^2.0.7`, so `2.0.8` is in range, but an existing lockfile stays on `2.0.7` |
119
+ | `@graphql-tools/utils` | [GHSA-7mx3-vvmw-hjmv](https://github.com/advisories/GHSA-7mx3-vvmw-hjmv) — high: prototype pollution in `mergeDeep` (schema assembly). Covers `<=12.0.0` — **11.x included, with no 11.x fix**. Patched `>=12.0.1` | An override for the copy `@nestjs/graphql` 13.4.5 exact-pins (`12.0.0`), and — in an older lockfile — a refresh of `@graphql-tools/schema` / `merge`, which can still pull in 11.x (see below) |
120
+ | `source-map-js` | [GHSA-68fv-2mgg-jv7q](https://github.com/advisories/GHSA-68fv-2mgg-jv7q) — high: event-loop DoS through indexed source-map section offsets. Patched `>=1.2.2`. **Dev tooling only** (vitest > vite > postcss), but `pnpm audit` — and therefore `check` — reports it | A lockfile refresh: `pnpm update --depth Infinity source-map-js`. If `postcss` keeps a `1.2.1` copy, re-resolve it the same way rather than with `pnpm dedupe`, which moves unrelated peers too |
121
+
122
+ `proxy-addr` is what Express uses to compute `request.ip` from `X-Forwarded-For` as far as
123
+ `trustProxy` allows. With `trustProxy` set, every IP-keyed rate limit — the framework's own and any of
124
+ yours — depends on it.
125
+
126
+ ```bash
127
+ pnpm update --depth Infinity proxy-addr
128
+ ```
129
+
130
+ ```yaml
131
+ # pnpm-workspace.yaml
132
+ overrides:
133
+ # @nestjs/graphql 13.4.5 exact-pins @graphql-tools/utils@12.0.0 — GHSA-7mx3-vvmw-hjmv (high, patched >=12.0.1).
134
+ # 12.0.2+ requires Node >=22.15; take the newest 12.x instead if your engines floor allows it.
135
+ '@graphql-tools/utils@>=12.0.0 <12.0.1': '12.0.1'
136
+ ```
137
+
138
+ **Check for a second, older copy.** `@apollo/server` 5.x requests `@graphql-tools/schema` `^10.0.0`. A
139
+ lockfile resolved before schema 10.1.0 (published 2026-08-12) holds a 10.0.x, which requests utils
140
+ `^11.1.0` — vulnerable, outside the override key above, and with no fix in the 11.x line. Look for it:
141
+
142
+ ```bash
143
+ grep -n "'@graphql-tools/utils@11" pnpm-lock.yaml
144
+ ```
145
+
146
+ If it is there, lift the requesters into their current lines, which ask for utils 12:
147
+
148
+ ```bash
149
+ pnpm update --depth Infinity @graphql-tools/schema @graphql-tools/merge
150
+ ```
151
+
152
+ The current releases (schema 10.1.3, merge 9.2.6) request utils `^12.0.3`, which depends on
153
+ `@whatwg-node/promise-helpers` 2 and therefore needs **Node >=22.15**. Current Node 22 LTS images
154
+ (22.22) and Node 24 satisfy that; if your `engines.node` still declares a lower floor, raise it — the
155
+ override above cannot hold this copy back, because `^12.0.3` does not intersect its key.
156
+
157
+ Then `pnpm install` and `pnpm audit` — the advisories must be gone.
158
+
159
+ **Commit `pnpm-workspace.yaml` and `pnpm-lock.yaml` together.** Every step above leaves a consistent
160
+ lockfile once it has run to the end, but the override only reaches the lockfile through the
161
+ `pnpm install` that follows it. A `pnpm-workspace.yaml` committed with the override and without the
162
+ re-installed lockfile fails every `pnpm install --frozen-lockfile` — typically the Docker image build.
163
+ Check with `pnpm install --frozen-lockfile` locally before you push.
164
+
165
+ ## 4. Native Better-Auth endpoints accept the session when the secret is auto-generated
166
+
167
+ **Affects only** projects that configure no `betterAuth.secret` and whose fallback secrets
168
+ (`jwt.secret`, `jwt.refresh.secret`) are shorter than 32 characters — typically a local `.env`.
169
+ Better-Auth then generates a secret at startup and signs with it, while the NestJS layer (cookie
170
+ helper, API middleware, `CoreBetterAuthService`) still signed with the configured value or none. The
171
+ session cookie went out unsigned, so every endpoint Better-Auth serves itself and that reads the
172
+ cookie — `/iam/token`, the MCP `/iam/mcp/authorize`, passkey, 2FA — answered **401 right after a
173
+ successful sign-in**, and an MCP OAuth login looped back to the login page.
174
+
175
+ Since 11.42.4 the NestJS layer signs with exactly the secret Better-Auth uses. No action is needed.
176
+ It is still the wrong setup for anything but local development: a generated secret changes on every
177
+ restart and differs per replica, so sessions do not survive either — set `betterAuth.secret` (at
178
+ least 32 characters), as the boot warning says.
179
+
180
+ ## Troubleshooting
181
+
182
+ | Symptom | Cause | Fix |
183
+ | ---------------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------- |
184
+ | `413 … exceeds the limit` warnings for a route that should accept it | The 100 kB default is too small for that data | Set `bodyParser.json.limit` (§1) |
185
+ | `bodyParser.json.limit … is not a positive size` at boot | A typo or unit the parser does not know (`'2 megabytes'`) | Use bytes or `'2mb'` |
186
+ | `no global json body parser is registered` at boot | The app was created with `bodyParser: false` | Remove the option, or register your own parser |
187
+ | A webhook test loses `req.rawBody` after configuring a limit | Test app created via the testing module with `rawBody: true` | See the `rawBody` note in §1 |
188
+ | A path still answers a bare `request entity too large` | A path-scoped parser of your own refused it | Raise that parser's limit in `main.ts` |
189
+ | `pnpm audit` still reports `@graphql-tools/utils` 12.0.0 | The override is missing, or its key was copied without the range | Add the entry from §3 verbatim |
190
+ | `pnpm audit` still reports `@graphql-tools/utils` 11.x | An older `@graphql-tools/schema` 10.0.x requests `^11.1.0`, outside the override key | `pnpm update --depth Infinity @graphql-tools/schema @graphql-tools/merge` (§3) — needs Node >=22.15 |
191
+ | `pnpm audit` still reports `proxy-addr` 2.0.7 | The lockfile was not refreshed | `pnpm update --depth Infinity proxy-addr` (§3) |
192
+ | `/iam/token` or MCP authorize answers 401 right after sign-in | 11.42.3 or older without `betterAuth.secret` and with short JWT secrets | Update to 11.42.4 (§4); for anything beyond local development set `betterAuth.secret` |
193
+
194
+ ## Module Documentation
195
+
196
+ - [Request lifecycle — 0d. Body parsers](../docs/REQUEST-LIFECYCLE.md#0d-body-parsers)
197
+ - [Error codes — LTNS_0304](../docs/error-codes.md#ltns_0304-requestbodytoolarge)
198
+ - [Security overrides — why yours are not inherited](../docs/security-overrides.md)
199
+ - [Better-Auth README](../src/core/modules/better-auth/README.md) — secret resolution and fallback chain
@@ -0,0 +1,104 @@
1
+ # Migration Guide: 11.42.4 → 11.42.5
2
+
3
+ ## Overview
4
+
5
+ | Category | Details |
6
+ | -------------------- | ----------------------------------------------------------------------------------------------------------- |
7
+ | **New feature** | `TestHelper.mcpSession()` / `TestHelper.mcp()` — MCP tests without hand-written SSE parsing (§1) |
8
+ | **Tooling** | `check:overrides` accepts a declared "fix exists, but this consumer cannot use it" suppression that expires by itself (§2) |
9
+ | **Docs** | The 11.42.3 → 11.42.4 guide gained three steps; check them if you already applied it (§3) |
10
+ | **Breaking Changes** | None |
11
+ | **Migration Effort** | None required — `pnpm update`. Adopting the helpers is optional |
12
+
13
+ ## Quick Migration
14
+
15
+ ```bash
16
+ pnpm update @lenne.tech/nest-server
17
+ ```
18
+
19
+ Vendor-mode projects pick up the core update; the new helpers arrive with `src/test/`. No configuration
20
+ change is required.
21
+
22
+ ## 1. TestHelper: MCP tests without SSE parsing
23
+
24
+ Projects that enable the AI module's MCP server (`ai.mcp` in `config.env.ts`) should test their tools over
25
+ MCP as well as over REST/GraphQL: MCP has its own role filter and sessions, and a refused tool comes back as
26
+ a result with `isError: true`, not as an HTTP error. Until now such a test had to set the right `Accept`
27
+ header, carry the `mcp-session-id` header, and parse the SSE stream by hand. `TestHelper` now does that:
28
+
29
+ ```typescript
30
+ const session = await testHelper.mcpSession({ token: userToken });
31
+
32
+ expect((await session.listTools()).map((tool) => tool.name)).not.toContain('delete_user');
33
+
34
+ const refused = await session.callTool('delete_user', { id });
35
+ expect(refused.isError).toBe(true);
36
+
37
+ const result = await session.callTool('find_users', { query: '@test.com' });
38
+ expect(result.json.data.length).toBeGreaterThan(0);
39
+
40
+ await session.close();
41
+ ```
42
+
43
+ `testHelper.mcp(message, { statusCode, sessionId, token })` sends a single JSON-RPC message for the
44
+ HTTP-level cases (401 without a token, 404 for another user's session). Full reference: `src/test/README.md`
45
+ → "MCP Testing".
46
+
47
+ The helpers are additive and build on `testHelper.rest()`, so `token` and `cookies` behave as there. They add
48
+ no dependency, so vendor-mode projects receive them with `src/test/`. A project with its own MCP module (not
49
+ the AI module's `/ai/mcp`) passes its endpoint as `path`.
50
+
51
+ ## 2. `check:overrides`: a suppression for a fix the consumer cannot use
52
+
53
+ `auditConfig.ignoreGhsas` used to be usable only for advisories with no fix at all — the moment any fix
54
+ appeared, `check:overrides` failed with `FIX AVAILABLE`. That is right in the common case and a dead end in
55
+ one: the fix exists, but the package that pulls in the vulnerable version cannot use it (for example, a
56
+ patched major removed an export the consumer imports).
57
+
58
+ That case can now be declared next to the suppression:
59
+
60
+ ```yaml
61
+ auditConfig:
62
+ ignoreGhsas:
63
+ - GHSA-x6jw-m9v5-85vh
64
+ unusableFixConsumers:
65
+ GHSA-x6jw-m9v5-85vh: '@nuxt/devtools@3.4.1 cannot use 4.0.1'
66
+ ```
67
+
68
+ The declaration expires by itself: `check:overrides` fails with `RE-TEST` as soon as the consumer leaves the
69
+ tree or changes version, the advisory offers a patched version nobody assessed (a backport), or anything other
70
+ than exactly that consumer version pulls the vulnerable package — including a direct dependency of your own
71
+ project. An undeclared suppression of a fixed advisory still fails on sight.
72
+
73
+ This is a change to the guard script, not to the published runtime: projects created from
74
+ nest-server-starter or lt-monorepo receive it with their template update. Exhaust the remedies first (raise
75
+ the vulnerable package, raise or patch the consumer); the rules and the reasoning are in
76
+ `.claude/rules/package-management.md` → "When the fix exists and the consumer cannot use it".
77
+
78
+ ## 3. Three steps added to the 11.42.3 → 11.42.4 guide
79
+
80
+ If you already applied [11.42.3 → 11.42.4](./11.42.3-to-11.42.4.md), check these — they were added after its
81
+ release:
82
+
83
+ 1. **`source-map-js` < 1.2.2** ([GHSA-68fv-2mgg-jv7q](https://github.com/advisories/GHSA-68fv-2mgg-jv7q),
84
+ high, dev tooling only) is also reported by `pnpm audit`: `pnpm update --depth Infinity source-map-js`.
85
+ 2. **Commit `pnpm-workspace.yaml` and `pnpm-lock.yaml` together.** An override only reaches the lockfile through
86
+ the `pnpm install` that follows it; a workspace file committed without the re-installed lockfile fails every
87
+ `pnpm install --frozen-lockfile`, typically the Docker image build.
88
+ 3. **Starter-based projects:** set `bodyParser` where both `localConfig()` and `deployedConfig()` see it (the
89
+ starter ships a shared `PROJECT_BODY_PARSER` constant), or via `NSC__BODY_PARSER__JSON__LIMIT`. A value only
90
+ in the deployed block leaves the e2e suite and `lt dev up` at 100 kB.
91
+
92
+ ## Troubleshooting
93
+
94
+ | Symptom | Cause | Fix |
95
+ | ------- | ----- | --- |
96
+ | `check:overrides` fails with `RE-TEST` on a declared suppression | The consumer moved, a new patched version appeared, or another package (or your project directly) now pulls the vulnerable package | Re-assess: take the fix if it is now usable, otherwise update the declaration with what you re-checked (§2) |
97
+ | `check:overrides` fails with `FIX AVAILABLE` | A suppressed advisory gained a fix, and no declaration explains why it cannot be used | Take the fix; declare only after the remedies are exhausted (§2) |
98
+ | `session.callTool()` throws instead of returning `isError` | The JSON-RPC call itself failed (protocol error), not the tool | Assert on `session.request(...)` for protocol-level errors (§1) |
99
+
100
+ ## Module Documentation
101
+
102
+ - [TestHelper README — MCP Testing](../src/test/README.md)
103
+ - [AI module README](../src/core/modules/ai/README.md)
104
+ - `.claude/rules/package-management.md` — suppressions, overrides and the `check:overrides` guard
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.42.3",
3
+ "version": "11.42.5",
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",
@@ -129,7 +129,7 @@
129
129
  "mongoose": "9.10.4",
130
130
  "multer": "2.4.0",
131
131
  "node-mailjet": "6.0.11",
132
- "nodemailer": "10.0.13",
132
+ "nodemailer": "10.0.14",
133
133
  "passport": "0.7.0",
134
134
  "passport-jwt": "4.0.1",
135
135
  "reflect-metadata": "0.2.2",
@@ -214,7 +214,7 @@
214
214
  "oxfmt": "0.71.0",
215
215
  "oxlint": "1.86.0",
216
216
  "rimraf": "6.1.3",
217
- "smtp-server": "3.19.16",
217
+ "smtp-server": "3.19.17",
218
218
  "ts-node": "10.9.2",
219
219
  "tsconfig-paths": "4.2.0",
220
220
  "tsx": "4.23.15",
@@ -1051,6 +1051,53 @@ export interface IBetterAuthUserField {
1051
1051
  type: BetterAuthFieldType;
1052
1052
  }
1053
1053
 
1054
+ /**
1055
+ * Size limits for the two body parsers NestJS registers globally (`application/json` and
1056
+ * `application/x-www-form-urlencoded`).
1057
+ *
1058
+ * @see IServerOptions.bodyParser
1059
+ * @since 11.42.4
1060
+ */
1061
+ export interface IBodyParserConfig {
1062
+ /**
1063
+ * Limit for `application/json` bodies — REST and GraphQL alike, since Apollo reads the body the
1064
+ * global parser produced.
1065
+ *
1066
+ * A number is bytes; a string is a size such as `'500kb'` or `'2mb'` (1024-based, units
1067
+ * `b` / `kb` / `mb` / `gb`, case-insensitive).
1068
+ *
1069
+ * @default '100kb' (body-parser's own default — unset leaves the parser untouched)
1070
+ */
1071
+ json?: IBodyParserTypeConfig;
1072
+
1073
+ /**
1074
+ * Limit for `application/x-www-form-urlencoded` bodies. Same value format as `json`.
1075
+ *
1076
+ * Configured separately on purpose: form posts rarely need more, and raising one limit should not
1077
+ * silently raise the other.
1078
+ *
1079
+ * @default '100kb' (body-parser's own default — unset leaves the parser untouched)
1080
+ */
1081
+ urlencoded?: IBodyParserTypeConfig;
1082
+ }
1083
+
1084
+ /**
1085
+ * Settings for one body parser.
1086
+ *
1087
+ * @see IBodyParserConfig
1088
+ * @since 11.42.4
1089
+ */
1090
+ export interface IBodyParserTypeConfig {
1091
+ /**
1092
+ * Maximum body size: bytes as a number, or a size string such as `'2mb'`.
1093
+ *
1094
+ * A value that is not a positive size (`0`, negative, `NaN`, `'lots'`) is REJECTED with an error
1095
+ * log and the parser keeps its default — it never becomes "no limit". The parser runs before
1096
+ * authentication, so an unbounded limit would be an unbounded pre-auth allocation.
1097
+ */
1098
+ limit?: number | string;
1099
+ }
1100
+
1054
1101
  /**
1055
1102
  * Interface for Error Code module configuration
1056
1103
  *
@@ -2264,6 +2311,48 @@ export interface IServerOptions {
2264
2311
  */
2265
2312
  betterAuth?: boolean | IBetterAuth;
2266
2313
 
2314
+ /**
2315
+ * Size limits for request bodies.
2316
+ *
2317
+ * NestJS registers a JSON and a URL-encoded body parser on every application, and neither has a
2318
+ * limit anybody chose: body-parser's default of **100 kB** applies. A request over it is answered
2319
+ * with 413 in middleware — before authentication, before routing — so the whole request fails,
2320
+ * every retry fails identically, and a document that grew past the limit can no longer be saved.
2321
+ *
2322
+ * Unset, both parsers stay exactly as NestJS registered them. Set a limit only where the data
2323
+ * model needs it, and keep it as small as that need allows: the parser runs before
2324
+ * authentication, and `JSON.parse` blocks the event loop for the whole body — measured on a
2325
+ * realistic document shape, about 0.5 ms for 100 kB, 16 ms for 1 MB and 55 ms for 2 MB per
2326
+ * request. That is why the default is not raised for everybody.
2327
+ *
2328
+ * Applied by `CoreModule` during module init, which replaces the global parsers NestJS registered
2329
+ * IN PLACE — same position in the middleware chain, same options otherwise (`extended: true` for
2330
+ * URL-encoded bodies, `req.rawBody` when the app was created with `NestFactory.create(…, {
2331
+ * rawBody: true })`). Because this happens in `app.init()`, it holds under
2332
+ * `Test.createTestingModule(...).createNestApplication()` too, which never runs `main.ts` — a
2333
+ * `useBodyParser()` call in `main.ts` does not reach the e2e suite, this does. A configured value
2334
+ * also replaces a global parser registered in `main.ts`; a path-scoped one
2335
+ * (`server.use('/upload', json({ limit }))`) is left alone, and stays the way to give ONE path a
2336
+ * different limit, since the parser runs before routing and GraphQL shares a single path anyway.
2337
+ *
2338
+ * One caveat: under the testing module the options passed to `createNestApplication()` are not
2339
+ * visible to providers, so a test app created with `{ rawBody: true }` gets no `req.rawBody` from
2340
+ * a CONFIGURED parser. Applications created with `NestFactory.create()` are not affected.
2341
+ *
2342
+ * Independent of this setting, a 413 from either parser is answered with
2343
+ * `#LTNS_0304: Request body too large [<size>, limit <limit>]` instead of a bare
2344
+ * "request entity too large", and logged as a warning naming the route and the config key.
2345
+ *
2346
+ * @since 11.42.4
2347
+ *
2348
+ * @example
2349
+ * ```typescript
2350
+ * // Documents carry HTML: allow 2 MB of JSON, keep form posts at the default
2351
+ * bodyParser: { json: { limit: '2mb' } },
2352
+ * ```
2353
+ */
2354
+ bodyParser?: IBodyParserConfig;
2355
+
2267
2356
  /**
2268
2357
  * Configuration for Brevo
2269
2358
  * See: https://developers.brevo.com/