@lenne.tech/nest-server 11.42.4 → 11.42.6
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/configurable-features.md +1 -1
- package/.claude/rules/package-management.md +69 -3
- package/.claude/rules/testing.md +3 -3
- package/FRAMEWORK-API.md +1 -1
- package/dist/core/common/services/core-body-parser.initializer.d.ts +2 -0
- package/dist/core/common/services/core-body-parser.initializer.js +23 -3
- package/dist/core/common/services/core-body-parser.initializer.js.map +1 -1
- package/dist/test/test.helper.d.ts +60 -0
- package/dist/test/test.helper.js +104 -0
- package/dist/test/test.helper.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +1 -1
- package/migration-guides/11.42.3-to-11.42.4.md +27 -4
- package/migration-guides/11.42.4-to-11.42.5.md +104 -0
- package/migration-guides/11.42.5-to-11.42.6.md +97 -0
- package/package.json +1 -1
- package/src/core/common/interfaces/server-options.interface.ts +5 -0
- package/src/core/common/services/core-body-parser.initializer.ts +47 -5
- package/src/test/README.md +79 -1
- package/src/test/test.helper.ts +303 -0
|
@@ -569,7 +569,7 @@ Both use body-parser's default limit of **100 kB** unless `bodyParser` says othe
|
|
|
569
569
|
bodyParser: { json: { limit: '2mb' } },
|
|
570
570
|
```
|
|
571
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.
|
|
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. Keep `server.useBodyParser('json')` (or `'urlencoded'`) in `main.ts` next to it: NestJS registers its global parser only when no layer with the same function name exists, so a path-scoped one registered before `app.init()` suppresses it, and every other path receives an empty body. Since 11.42.6 `CoreBodyParserInitializer` warns at boot in that case.
|
|
573
573
|
|
|
574
574
|
The parser runs **before authentication and routing**. Over the limit, the whole request is refused:
|
|
575
575
|
|
|
@@ -19,7 +19,8 @@ pnpm update --depth Infinity proxy-addr
|
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
Then add the `@graphql-tools/utils` override from §3 to `pnpm-workspace.yaml`, run `pnpm install`,
|
|
22
|
-
|
|
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`.
|
|
23
24
|
Vendor-mode projects pick up the core update and need the same two dependency steps. The body limit
|
|
24
25
|
needs no configuration: unset, both parsers keep the 100 kB they had.
|
|
25
26
|
|
|
@@ -62,13 +63,28 @@ server.useBodyParser('json', { limit: '2mb' });
|
|
|
62
63
|
bodyParser: { json: { limit: '2mb' } },
|
|
63
64
|
```
|
|
64
65
|
|
|
66
|
+
**Exception: keep `useBodyParser()` when `main.ts` also registers a path-scoped parser** such as
|
|
67
|
+
`server.use('/upload', json({ limit: '20mb' }))`. NestJS registers its own global parser only if no
|
|
68
|
+
layer with the same function name exists yet, and a path-scoped `json()` registered before
|
|
69
|
+
`app.init()` counts as one. Removing `useBodyParser()` then leaves the app without a global JSON
|
|
70
|
+
parser: every request outside that path reaches its handler with an empty body, and a sign-in fails
|
|
71
|
+
with a validation error. Keep `server.useBodyParser('json')` (the configured limit is applied to it)
|
|
72
|
+
and set the limit in the config as above. Since 11.42.6 the server warns at boot when it finds this.
|
|
73
|
+
|
|
74
|
+
**Starter-based projects:** `config.env.ts` builds `local` / `e2e` / `ci` from `localConfig()` and the
|
|
75
|
+
deployed environments from `deployedConfig()`. Put the limit where BOTH see it — the starter ships a
|
|
76
|
+
shared `PROJECT_BODY_PARSER` constant for this since its 11.42.4 update — or set
|
|
77
|
+
`NSC__BODY_PARSER__JSON__LIMIT` in the environment. A value only in the deployed block leaves the e2e
|
|
78
|
+
suite and `lt dev up` at 100 kB.
|
|
79
|
+
|
|
65
80
|
The config is applied during module init, so it holds in every test that builds the app with
|
|
66
81
|
`Test.createTestingModule(...).createNestApplication()` — which never runs `main.ts`, so a
|
|
67
82
|
`useBodyParser()` call there never reached the e2e suite. Once configured, the value also replaces a
|
|
68
83
|
global parser your `main.ts` registered; keeping both is harmless, but the config wins.
|
|
69
84
|
|
|
70
85
|
A **path-scoped** parser is left alone: `server.use('/upload', json({ limit: '20mb' }))` in `main.ts`
|
|
71
|
-
still gives that one path its own limit
|
|
86
|
+
still gives that one path its own limit, as long as the global parser stays registered next to it
|
|
87
|
+
(see the exception above). The parser runs before routing, so a per-route limit can only
|
|
72
88
|
key on the path — and all GraphQL operations share one.
|
|
73
89
|
|
|
74
90
|
**`rawBody: true`:** `req.rawBody` is kept for apps created with `NestFactory.create(…, { rawBody:
|
|
@@ -101,7 +117,7 @@ refused request, with method, path (without query string) and the config key to
|
|
|
101
117
|
The status code is unchanged. Only clients that compare the message text against
|
|
102
118
|
`'request entity too large'` need adjusting — branch on the status or on `LTNS_0304` instead.
|
|
103
119
|
|
|
104
|
-
## 3.
|
|
120
|
+
## 3. Dependency advisories your project must fix itself
|
|
105
121
|
|
|
106
122
|
`pnpm` overrides apply only to the project that runs the install, so the framework's own fixes do not
|
|
107
123
|
reach your tree — see [docs/security-overrides.md](../docs/security-overrides.md).
|
|
@@ -110,6 +126,7 @@ reach your tree — see [docs/security-overrides.md](../docs/security-overrides.
|
|
|
110
126
|
| ------- | -------- | ----------------------- |
|
|
111
127
|
| `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` |
|
|
112
128
|
| `@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) |
|
|
129
|
+
| `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 |
|
|
113
130
|
|
|
114
131
|
`proxy-addr` is what Express uses to compute `request.ip` from `X-Forwarded-For` as far as
|
|
115
132
|
`trustProxy` allows. With `trustProxy` set, every IP-keyed rate limit — the framework's own and any of
|
|
@@ -146,7 +163,13 @@ The current releases (schema 10.1.3, merge 9.2.6) request utils `^12.0.3`, which
|
|
|
146
163
|
(22.22) and Node 24 satisfy that; if your `engines.node` still declares a lower floor, raise it — the
|
|
147
164
|
override above cannot hold this copy back, because `^12.0.3` does not intersect its key.
|
|
148
165
|
|
|
149
|
-
Then `pnpm install` and `pnpm audit` —
|
|
166
|
+
Then `pnpm install` and `pnpm audit` — the advisories must be gone.
|
|
167
|
+
|
|
168
|
+
**Commit `pnpm-workspace.yaml` and `pnpm-lock.yaml` together.** Every step above leaves a consistent
|
|
169
|
+
lockfile once it has run to the end, but the override only reaches the lockfile through the
|
|
170
|
+
`pnpm install` that follows it. A `pnpm-workspace.yaml` committed with the override and without the
|
|
171
|
+
re-installed lockfile fails every `pnpm install --frozen-lockfile` — typically the Docker image build.
|
|
172
|
+
Check with `pnpm install --frozen-lockfile` locally before you push.
|
|
150
173
|
|
|
151
174
|
## 4. Native Better-Auth endpoints accept the session when the secret is auto-generated
|
|
152
175
|
|
|
@@ -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
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Migration Guide: 11.42.5 → 11.42.6
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Category | Details |
|
|
6
|
+
| -------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
7
|
+
| **Correction** | The 11.42.3 → 11.42.4 guide told every project to drop `useBodyParser()` from `main.ts`. With a path-scoped parser in `main.ts` that removes JSON parsing everywhere else (§1) |
|
|
8
|
+
| **Improvement** | The server warns at boot when NestJS skipped its global body parser because of such a path-scoped one (§1) |
|
|
9
|
+
| **Tooling** | `check:overrides` no longer reports a failed audit as clean, and its probe follows `pnpm_config_registry` (§2) |
|
|
10
|
+
| **Breaking Changes** | None |
|
|
11
|
+
| **Migration Effort** | None for most projects. Projects that removed `useBodyParser()` while registering a path-scoped parser in `main.ts` must add it back (§1) |
|
|
12
|
+
|
|
13
|
+
## Quick Migration
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm update @lenne.tech/nest-server
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then check `main.ts` as described in §1. Vendor-mode projects pick up the core update and need the
|
|
20
|
+
same check.
|
|
21
|
+
|
|
22
|
+
## 1. Keep `useBodyParser()` next to a path-scoped parser
|
|
23
|
+
|
|
24
|
+
§1 of the [11.42.3 → 11.42.4 guide](./11.42.3-to-11.42.4.md) recommended moving the body limit from
|
|
25
|
+
`main.ts` into `bodyParser.json.limit` and deleting the `useBodyParser()` call. That is right, with one
|
|
26
|
+
exception the guide did not name.
|
|
27
|
+
|
|
28
|
+
NestJS registers its own global JSON and URL-encoded parsers inside `app.init()`, but only when no
|
|
29
|
+
layer with the same function name exists yet. It compares the NAME and nothing else. A path-scoped
|
|
30
|
+
parser registered in `main.ts` before `listen()` carries that name:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// main.ts
|
|
34
|
+
server.use('/diagnostics', json({ limit: '256kb' })); // a layer named `jsonParser`
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
With `useBodyParser()` removed, NestJS treats the JSON parser as already applied and registers none.
|
|
38
|
+
Every request outside `/diagnostics` then reaches its handler with an empty body. A sign-in fails with
|
|
39
|
+
a validation error such as "Missing input", and before this release nothing was logged.
|
|
40
|
+
|
|
41
|
+
**Check `main.ts`.** If it registers `json(...)` or `urlencoded(...)` on a path, keep or restore the
|
|
42
|
+
global parser next to it:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// main.ts
|
|
46
|
+
server.use('/diagnostics', json({ limit: '256kb' }));
|
|
47
|
+
server.useBodyParser('json'); // the configured bodyParser.json.limit is applied to it
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Keep the limit itself in the config (`bodyParser.json.limit`), where it also holds under
|
|
51
|
+
`Test.createTestingModule()`. The e2e suite cannot catch the missing parser: the testing module never
|
|
52
|
+
runs `main.ts`, so it never registers the path-scoped parser either.
|
|
53
|
+
|
|
54
|
+
Since 11.42.6 the server logs a warning at boot in exactly this situation:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
No global json body parser is registered: NestJS skips its own when a layer named `jsonParser`
|
|
58
|
+
already exists, and a path-scoped `json()` parser registered before `app.init()` counts as one. …
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The framework only reports it. It does not register the missing parser itself: NestJS would have put
|
|
62
|
+
it after everything `main.ts` registered, and that position is no longer known during module init. A
|
|
63
|
+
raw-body webhook or a proxy registered after the path-scoped parser must keep receiving the unread
|
|
64
|
+
request stream.
|
|
65
|
+
|
|
66
|
+
## 2. `check:overrides`: a failed audit is no longer reported as clean
|
|
67
|
+
|
|
68
|
+
Two fixes in `scripts/check-overrides.mjs` (and the shared registry block in `scripts/check.mjs`):
|
|
69
|
+
|
|
70
|
+
- When pnpm 11 cannot reach the registry, `pnpm audit --json` prints
|
|
71
|
+
`{"error": {"message": "fetch failed"}}` instead of a report. The guard read that as "0 advisories"
|
|
72
|
+
and printed `ok — N override(s) checked … none is failing`. It now prints
|
|
73
|
+
`could not obtain an audit report` (exit 0) on a live run, and fails (exit 1) when the failed report
|
|
74
|
+
was passed in with `--audit-file`, which is the CI path. The suppression checks still run in that
|
|
75
|
+
case, because they ask the GitHub Advisory API rather than the audit: a suppressed advisory that
|
|
76
|
+
gained a fix still fails with `FIX AVAILABLE`.
|
|
77
|
+
- The clean-tree probe reads `pnpm_config_registry`, the variable pnpm 11 audits against. It used to
|
|
78
|
+
read `npm_config_registry`, which pnpm 11 ignores, and could then ask a registry the audit never used.
|
|
79
|
+
|
|
80
|
+
`scripts/` is not part of the npm package. Projects created from nest-server-starter or lt-monorepo
|
|
81
|
+
receive these fixes with their template update.
|
|
82
|
+
|
|
83
|
+
## Troubleshooting
|
|
84
|
+
|
|
85
|
+
| Symptom | Cause | Fix |
|
|
86
|
+
| ------- | ----- | --- |
|
|
87
|
+
| Boot warning "No global json body parser is registered" | `main.ts` registers a path-scoped `json()` and no global parser | Add `server.useBodyParser('json')` to `main.ts` (§1) |
|
|
88
|
+
| Requests arrive with an empty body outside one path; sign-in reports "Missing input" | Same cause, on a version before 11.42.6 | Same fix (§1) |
|
|
89
|
+
| `check:overrides` prints `could not obtain an audit report (fetch failed)` | The npm registry was not reachable during the audit | Re-run with network access, or pass a captured report with `--audit-file` |
|
|
90
|
+
| `check:overrides` fails with `records a failed audit, not a result` | The report passed with `--audit-file` is a pnpm error, not an audit | Capture the report again once the registry answers |
|
|
91
|
+
|
|
92
|
+
## Module Documentation
|
|
93
|
+
|
|
94
|
+
- [REQUEST-LIFECYCLE.md — 0d. Body parsers](../docs/REQUEST-LIFECYCLE.md)
|
|
95
|
+
- [11.42.3 → 11.42.4 guide §1](./11.42.3-to-11.42.4.md)
|
|
96
|
+
- `.claude/rules/configurable-features.md` — Body Parser Limits row
|
|
97
|
+
- `.claude/rules/package-management.md` — 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
|
+
"version": "11.42.6",
|
|
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",
|
|
@@ -2335,6 +2335,11 @@ export interface IServerOptions {
|
|
|
2335
2335
|
* (`server.use('/upload', json({ limit }))`) is left alone, and stays the way to give ONE path a
|
|
2336
2336
|
* different limit, since the parser runs before routing and GraphQL shares a single path anyway.
|
|
2337
2337
|
*
|
|
2338
|
+
* Keep `server.useBodyParser('json')` in `main.ts` next to such a path-scoped parser. NestJS
|
|
2339
|
+
* registers its own global parser only if no layer with the same function name exists yet, so a
|
|
2340
|
+
* path-scoped `json()` registered before `app.init()` makes it skip the global one, and requests
|
|
2341
|
+
* outside that path arrive with an empty body. The initializer warns at boot when it finds that.
|
|
2342
|
+
*
|
|
2338
2343
|
* One caveat: under the testing module the options passed to `createNestApplication()` are not
|
|
2339
2344
|
* visible to providers, so a test app created with `{ rawBody: true }` gets no `req.rawBody` from
|
|
2340
2345
|
* a CONFIGURED parser. Applications created with `NestFactory.create()` are not affected.
|
|
@@ -90,11 +90,8 @@ export class CoreBodyParserInitializer implements OnModuleInit {
|
|
|
90
90
|
const limit = this.resolveLimit(type, config?.[type]?.limit);
|
|
91
91
|
const layers = stack.filter((layer) => this.isGlobalParserLayer(layer, type));
|
|
92
92
|
|
|
93
|
-
if (
|
|
94
|
-
this.
|
|
95
|
-
`bodyParser.${type}.limit is configured, but no global ${type} body parser is registered ` +
|
|
96
|
-
'(was the app created with `bodyParser: false`?) — the limit has no effect.',
|
|
97
|
-
);
|
|
93
|
+
if (!layers.length) {
|
|
94
|
+
this.warnMissingGlobalParser(type, stack, limit);
|
|
98
95
|
continue;
|
|
99
96
|
}
|
|
100
97
|
|
|
@@ -147,6 +144,18 @@ export class CoreBodyParserInitializer implements OnModuleInit {
|
|
|
147
144
|
return layer?.slash === true && typeof layer.handle === 'function' && layer.handle.name === PARSER_NAMES[type];
|
|
148
145
|
}
|
|
149
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Whether the application was created without `bodyParser: false`.
|
|
149
|
+
*
|
|
150
|
+
* Read from the same container options as `rawBody`. Under `Test.createTestingModule()` those
|
|
151
|
+
* options never reach the container, so the answer there is always "enabled".
|
|
152
|
+
*/
|
|
153
|
+
protected isBodyParserEnabled(): boolean {
|
|
154
|
+
const container = (this.moduleRef as unknown as { container?: { contextOptions?: { bodyParser?: boolean } } })
|
|
155
|
+
?.container;
|
|
156
|
+
return container?.contextOptions?.bodyParser !== false;
|
|
157
|
+
}
|
|
158
|
+
|
|
150
159
|
/**
|
|
151
160
|
* Whether the application was created with `rawBody: true`.
|
|
152
161
|
*
|
|
@@ -210,6 +219,39 @@ export class CoreBodyParserInitializer implements OnModuleInit {
|
|
|
210
219
|
);
|
|
211
220
|
}
|
|
212
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Explain why there is no global parser of this type, when there should be one.
|
|
224
|
+
*
|
|
225
|
+
* NestJS registers its own parsers inside `app.init()` only if no layer with the parser's function
|
|
226
|
+
* name exists yet (`ExpressAdapter.isMiddlewareApplied()` compares the NAME and nothing else). A
|
|
227
|
+
* path-scoped `server.use('/upload', json({ limit }))` in `main.ts` therefore counts as "applied",
|
|
228
|
+
* NestJS skips its global parser, and every request outside that path reaches its handler with an
|
|
229
|
+
* empty body — a sign-in fails with a validation error, and nothing says why. Restoring the parser
|
|
230
|
+
* from here is not safe: NestJS would have placed it after everything `main.ts` registered, and that
|
|
231
|
+
* position is no longer knowable during module init (a raw-body webhook or a proxy registered after
|
|
232
|
+
* the path-scoped parser must keep seeing the unread stream). So this reports, with the remedy.
|
|
233
|
+
*/
|
|
234
|
+
protected warnMissingGlobalParser(type: CoreBodyParserType, stack: ExpressLayer[], limit: number | undefined): void {
|
|
235
|
+
const name = PARSER_NAMES[type];
|
|
236
|
+
const shadowed = stack.some((layer) => layer?.slash !== true && layer?.handle?.name === name);
|
|
237
|
+
if (shadowed && this.isBodyParserEnabled()) {
|
|
238
|
+
this.logger.warn(
|
|
239
|
+
`No global ${type} body parser is registered: NestJS skips its own when a layer named \`${name}\` ` +
|
|
240
|
+
`already exists, and a path-scoped \`${type}()\` parser registered before \`app.init()\` counts as one. ` +
|
|
241
|
+
`Every request outside that path reaches its handler with an EMPTY body. Register the global parser ` +
|
|
242
|
+
`in main.ts as well — \`server.useBodyParser('${type}')\` — a configured \`bodyParser.${type}.limit\` ` +
|
|
243
|
+
'is applied to it.',
|
|
244
|
+
);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (limit !== undefined) {
|
|
248
|
+
this.logger.warn(
|
|
249
|
+
`bodyParser.${type}.limit is configured, but no global ${type} body parser is registered ` +
|
|
250
|
+
'(was the app created with `bodyParser: false`?) — the limit has no effect.',
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
213
255
|
/**
|
|
214
256
|
* Wrap a parser so its 413 is translated, keeping the name NestJS identifies the layer by.
|
|
215
257
|
*
|
package/src/test/README.md
CHANGED
|
@@ -243,4 +243,82 @@ const result = await testHelper.graphQl(
|
|
|
243
243
|
);
|
|
244
244
|
```
|
|
245
245
|
|
|
246
|
-
See `TestGraphQLConfig` and `TestGraphQLOptions` interfaces in `test.helper.ts` for full configuration options.
|
|
246
|
+
See `TestGraphQLConfig` and `TestGraphQLOptions` interfaces in `test.helper.ts` for full configuration options.
|
|
247
|
+
|
|
248
|
+
## MCP Testing (`testHelper.mcp()` / `testHelper.mcpSession()`)
|
|
249
|
+
|
|
250
|
+
An MCP client reaches the same services as REST and GraphQL through its own door: the MCP server of the AI module
|
|
251
|
+
(`/ai/mcp`, enabled with `ai.mcp` in `config.env.ts`) has its own role filter, its own sessions, and reports a refused
|
|
252
|
+
or failing tool as a result with `isError: true` instead of an HTTP error. A project that enables MCP therefore tests
|
|
253
|
+
its tools over MCP too, next to its API tests.
|
|
254
|
+
|
|
255
|
+
The transport answers requests as an SSE stream. These helpers handle the stream, the session header and the
|
|
256
|
+
handshake, so a test reads results directly.
|
|
257
|
+
|
|
258
|
+
### `mcpSession(options?)`
|
|
259
|
+
|
|
260
|
+
Opens a session the way every MCP client does (`initialize`, then `notifications/initialized`) for the user of `token`
|
|
261
|
+
or `cookies`:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
const session = await testHelper.mcpSession({ token: userToken });
|
|
265
|
+
|
|
266
|
+
// tools/list — filtered by the user's roles
|
|
267
|
+
const names = (await session.listTools()).map((tool) => tool.name);
|
|
268
|
+
expect(names).toContain('find_users');
|
|
269
|
+
|
|
270
|
+
// tools/call — `json` is the first text content parsed as JSON (the tool's own result)
|
|
271
|
+
const result = await session.callTool('find_users', { query: '@test.com' });
|
|
272
|
+
expect(result.isError).toBeFalsy();
|
|
273
|
+
expect(result.json.data.length).toBeGreaterThan(0);
|
|
274
|
+
|
|
275
|
+
// A tool outside the user's roles is refused, not run
|
|
276
|
+
const refused = await session.callTool('delete_user', { id });
|
|
277
|
+
expect(refused.isError).toBe(true);
|
|
278
|
+
|
|
279
|
+
// Any other JSON-RPC request; returns the whole message, protocol errors included
|
|
280
|
+
const reply = await session.request('prompts/list');
|
|
281
|
+
|
|
282
|
+
// End the session server-side
|
|
283
|
+
await session.close();
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
| Member | Description |
|
|
287
|
+
| ------------------------------------ | ------------------------------------------------------------------------ |
|
|
288
|
+
| `sessionId` | `mcp-session-id` the server assigned |
|
|
289
|
+
| `initializeResult` | Result of `initialize` (`serverInfo`, `capabilities`, `protocolVersion`) |
|
|
290
|
+
| `listTools()` | `tools/list`, returns the tools array |
|
|
291
|
+
| `callTool(name, args?)` | `tools/call`, returns the `CallToolResult` plus `json` |
|
|
292
|
+
| `request(method, params?, options?)` | Any request in the session, returns the JSON-RPC message |
|
|
293
|
+
| `close()` | `DELETE` with the session id |
|
|
294
|
+
|
|
295
|
+
### `mcp(message, options?)`
|
|
296
|
+
|
|
297
|
+
Sends a single JSON-RPC message and returns `{ message, response, sessionId }`. Use it for the HTTP-level cases a
|
|
298
|
+
session hides:
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// No token → 401
|
|
302
|
+
await testHelper.mcp({ id: 1, method: 'initialize', params: {} }, { statusCode: 401 });
|
|
303
|
+
|
|
304
|
+
// Another user's session id → 404 (not 403: confirming the id exists would be an oracle)
|
|
305
|
+
await testHelper.mcp(
|
|
306
|
+
{ id: 2, method: 'tools/list' },
|
|
307
|
+
{ sessionId: adminSession.sessionId, statusCode: 404, token: userToken },
|
|
308
|
+
);
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### TestMcpOptions
|
|
312
|
+
|
|
313
|
+
| Option | Type | Default | Description |
|
|
314
|
+
| ------------------ | ---------------------------------- | ------------------------------- | ----------------------------------------------- |
|
|
315
|
+
| `token` | `string` | - | Bearer token, the same one `rest()` takes |
|
|
316
|
+
| `cookies` | `string \| Record<string, string>` | - | Cookie authentication, same modes as `rest()` |
|
|
317
|
+
| `sessionId` | `string` | - | `mcp-session-id` header (`mcp()` only) |
|
|
318
|
+
| `statusCode` | `number` | `200`, `202` for a notification | Expected HTTP status (`mcp()` only) |
|
|
319
|
+
| `path` | `string` | `'/ai/mcp'` | Endpoint, for a project with its own MCP module |
|
|
320
|
+
| `headers` | `Record<string, string>` | - | Additional headers |
|
|
321
|
+
| `log` / `logError` | `boolean` | `false` | Same as `rest()` |
|
|
322
|
+
|
|
323
|
+
`mcpSession()` additionally takes `clientInfo` and `protocolVersion` for the `initialize` request.
|
|
324
|
+
`TestHelper.parseMcpMessage(response)` is the parser behind both, for tests that send requests themselves.
|