@bymax-one/nest-core 1.3.2 → 1.5.0

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/CHANGELOG.md CHANGED
@@ -11,7 +11,186 @@ heading here.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
- ## [1.3.2] - 2026-08-11
14
+ ## [1.5.0] - 2026-08-15
15
+
16
+ An OpenAPI document could stop requiring credentials without anything saying
17
+ so. Deleting a document-level `security` default — typically alongside the
18
+ per-operation entries a library has taken over describing — leaves every route
19
+ the backend itself owns with no requirement from any source, and the document
20
+ stays valid, no requirement dangles, and the runtime still answers `401`. The
21
+ only observable change is that a client generated from the document stops
22
+ sending credentials.
23
+
24
+ **Apply to a derived backend:** bump the dependency. No code change is needed.
25
+ If the boot log now names operations, they are the ones a generated client will
26
+ call without credentials — set `openapi.security`, or mark each public with an
27
+ explicit `[]` in `openapi.operationSecurity`.
28
+
29
+ ### Added
30
+
31
+ - **The document build warns when an operation ends up requiring no credential
32
+ at all.** Deleting a document-level `security` default — typically alongside
33
+ the per-operation entries a library has taken over describing — leaves every
34
+ route the backend itself owns with no requirement from any source. Nothing
35
+ catches it today: the document is valid, no requirement dangles so
36
+ `assertSchemesDeclared` is satisfied, the runtime still answers `401` so a
37
+ status-code probe finds nothing, and a consumer's document test stays green if
38
+ it asserts only the operations it enumerated. The only observable change is
39
+ that a client generated from the document sends no credentials.
40
+ `applyBymaxOpenApi` now emits one warning per build naming the affected
41
+ operations, capped at ten with a count of the rest.
42
+
43
+ It warns and never throws — an API that is public on purpose is legitimate —
44
+ and the trigger is narrow so the line stays worth reading: only when the
45
+ document declares no top-level `security`, **and** at least one other
46
+ operation does state a requirement, **and** the operation is not one of the
47
+ three this package registers. An explicit `[]` — from `operationSecurity`, a
48
+ decorator, or a library's fragment — states the intent and stops the report.
49
+ The known limit is documented rather than closed: a document with nothing
50
+ explicit anywhere is indistinguishable from an API that is public on purpose,
51
+ so removing _every_ requirement at once is not warned. Render the document
52
+ with and without your libraries and diff the operations you mount.
53
+
54
+ ## [1.4.0] - 2026-08-13
55
+
56
+ HTTP metrics were blind to every request that did not reach a handler. Nest runs
57
+ **middleware → guards → interceptors → pipes → handler**, and the recorder was an
58
+ interceptor, so authentication failures, authorization failures, throttled
59
+ requests and unknown paths were never counted. Measured on a running
60
+ application, three requests — a handler success, a guard rejection, an unknown
61
+ path — produced **one** sample. A deployment could be under a credential-stuffing
62
+ run, a privilege probe or route enumeration with a flat error graph, which makes
63
+ this a security fix rather than an observability improvement.
64
+
65
+ **Apply to a derived backend:** bump the dependency. No code change is needed.
66
+ Expect new `401`/`403`/`429` series on routes that already existed, a new
67
+ `route="<unmatched>"` series for `404`s, and a **lower success rate** — the
68
+ denominator finally includes the rejections. Existing `status_code="200"` series
69
+ keep their values.
70
+
71
+ ### Security
72
+
73
+ - **Requests rejected before a handler are now counted.** The recorder moved
74
+ from `APP_INTERCEPTOR` to middleware (`BymaxTimingMiddleware`), applied to
75
+ every route when `timing.enabled` is `true`. Nest runs middleware, then
76
+ guards, then interceptors: a request a guard rejects never reached
77
+ `intercept()`, and a request matching no route never reached a controller.
78
+ Measured on a real application, three requests — a handler success, a guard
79
+ rejection and an unknown path — produced exactly **one** sample. `401`, `403`,
80
+ `429` and `404` were all invisible, which is to say a deployment could be under
81
+ a credential-stuffing run, a privilege probe or route enumeration with a flat
82
+ error graph. All six cases are now counted, and the sample is emitted on the
83
+ response's `'close'` event rather than `'finish'`, so a client that hangs up
84
+ mid-request — what a scanner does — is counted too.
85
+ - **The root path is recorded.** On Express the middleware is mounted at `'/'`
86
+ rather than through a wildcard pattern. The unbraced `'*splat'` skips the root outright,
87
+ and the braced `'{*splat}'` that Nest 11's migration guide prescribes stops
88
+ matching the _prefixed_ root once an application calls `setGlobalPrefix` —
89
+ which production applications almost always do. That was reported as
90
+ nest#14520 and fixed by nest#14522, whose regression test covers Fastify;
91
+ measured on `@nestjs/core` 11.1.28 with the Express adapter, the prefixed root
92
+ still reaches no middleware while the route itself answers `200`. Both
93
+ patterns were measured against the mount, which matched every path in both
94
+ configurations. Fastify needs the opposite choice — see the next entry. One
95
+ limit remains and is documented: module middleware is scoped to the global
96
+ prefix, so a request outside it entirely reaches no middleware.
97
+ - **Fastify records the same labels as Express**, which the documented support
98
+ for both platforms had been promising without any test behind it. Nest runs
99
+ middleware on Fastify through `@fastify/middie`, whose `runMiddie` calls
100
+ `run(req.raw, reply.raw, next)` and copies only `id`, `hostname`, `protocol`,
101
+ `ip`, `ips`, `log`, `query` and `body` onto that raw request — never
102
+ `routeOptions`. The recorder therefore saw no route metadata at all and would
103
+ have labelled every Fastify request `<unmatched>`, destroying the per-route
104
+ breakdown and making a scan indistinguishable from ordinary traffic. Worse,
105
+ `forRoutes('/')` is a mount on Express but an **exact match** on Fastify, so
106
+ most requests produced no sample whatsoever. The module now selects the mount
107
+ per adapter and registers an `onRequest` hook on Fastify that carries the
108
+ resolved template to the recorder. Covered by a new Fastify end-to-end suite.
109
+ - **Unmatched requests record a bounded label.** A request that matched no route
110
+ is recorded as `<unmatched>` (exported as `UNMATCHED_ROUTE`), never the
111
+ requested path. The previous raw-URL fallback would have let anyone mint one
112
+ Prometheus time series per probe, so counting scanner traffic under it would
113
+ have turned this fix into a memory-exhaustion vector.
114
+
115
+ ### Changed
116
+
117
+ - **`ITimingSink` implementations now receive more samples**, including requests
118
+ that never reached a handler. Sinks that assumed "one sample per completed
119
+ request" should expect "one sample per closed request". The built-in metrics
120
+ bridge needs no change; a dashboard filtering on `2xx` sees its numbers
121
+ unchanged and its error rates become correct.
122
+ - **No status is relabelled**, and that is deliberate. A client that hangs up
123
+ mid-handler was already counted — destroying the socket does not cancel the
124
+ JavaScript already running, so the handler finished and the interceptor
125
+ recorded an ordinary `200` — and it still is, once, under the same `200`.
126
+ Introducing a sentinel status for aborts would rewrite the value of
127
+ `status_code="200"` series that already exist in every deployment, moving
128
+ error-rate panels with no change in traffic. Whether an abort deserves its own
129
+ status is a separate decision from whether the request is counted at all, and
130
+ this release makes only the second one.
131
+ - **The timing recorder is registered once, not twice.** The middleware
132
+ **replaced** the interceptor rather than joining it — two recorders would
133
+ double every rate an alert threshold is tuned against, which is a quieter
134
+ failure than the one being fixed.
135
+
136
+ ### Deprecated
137
+
138
+ - **`TimingInterceptor`** is superseded by `BymaxTimingMiddleware` and is no
139
+ longer registered by `BymaxCoreModule`. It stays exported so an application
140
+ that wired it by hand keeps compiling; registering it alongside the middleware
141
+ records a second sample for every request that reaches a handler.
142
+
143
+ ### Added
144
+
145
+ - **`BymaxTimingMiddleware` and `UNMATCHED_ROUTE`** are exported from the package
146
+ root.
147
+ - **A library can describe its own routes in a consumer's document.** A provider
148
+ marked `@BymaxOpenApiContributor()` returns OpenAPI fragments keyed by handler
149
+ identity — `'AuthController.login'` — and they are merged onto the operations
150
+ those handlers produced. It exists because the two obvious alternatives do
151
+ not work: decorating a library's controllers with `@nestjs/swagger` would load
152
+ that peer in every application importing the library, and a consumer-side map
153
+ keyed by path cannot be written by a library mounted through
154
+ `RouterModule.register`, which does not know its own final paths.
155
+ - **`openapi.operationIdFactory`**, plus the exported `OpenApiOperationIdFactory`
156
+ type. This package now always installs a factory so it can learn which handler
157
+ produced which operation, and **delegates the id string** — to this option when
158
+ set, to the format `@nestjs/swagger` itself produces otherwise. Choosing the id
159
+ instead would have renamed every operation in every published document and
160
+ broken any client generated from one; a test compares both documents to keep
161
+ that true if the peer's format ever changes.
162
+ - **The contract types** `IOpenApiContributor`, `OpenApiFragment`,
163
+ `OpenApiFragmentObject` and `OpenApiHandlerKey`, exported from `./openapi` so a
164
+ sibling library can target them at its own compile time.
165
+ - **`BYMAX_OPENAPI_CONTRACT_VERSION`**, and a required `contractVersion` on every
166
+ fragment. A fragment crosses a boundary between independently released
167
+ packages, and on that boundary compile-time types protect nothing: each side
168
+ type-checks against its own installed copy, so only the value travelling at
169
+ runtime can say which shape it is. A revision this package does not speak fails
170
+ the build naming both. Required rather than inferred from absence, following
171
+ the pattern Kubernetes objects use for `apiVersion` — an optional discriminator
172
+ is unambiguous only while exactly one revision exists, which is precisely when
173
+ nobody checks it.
174
+
175
+ ### Fixed
176
+
177
+ - **`DiscoveryModule` is imported when only the document is enabled.** It was
178
+ imported on the synchronous registration path only when readiness discovery or
179
+ metrics could scan, so an application enabling nothing but OpenAPI had no
180
+ scanner — and a library's description of its own routes would have been
181
+ silently dropped.
182
+
183
+ ### Notes
184
+
185
+ - Deriving fragments from validation decorators is deliberately **not** in this
186
+ package: it takes no dependency on any validation library. A library that
187
+ wants its schemas to track its own decorators generates them in its own build,
188
+ where that dependency already exists, and commits the result with a test
189
+ asserting generated matches committed — so drift fails in the repository that
190
+ caused it. An application's own DTOs need none of this; `@nestjs/swagger`'s CLI
191
+ plugin already derives them, which is the route a precompiled library lacks.
192
+
193
+ ## [1.3.2] - 2026-08-12
15
194
 
16
195
  A consumer audit of the served document found that it described the library's
17
196
  promises rather than the deployment: routes of features that were switched off
@@ -509,4 +688,6 @@ have regressed from. They are kept because the reasoning is worth having.
509
688
  [1.2.2]: https://github.com/bymaxone/nest-core/compare/v1.2.1...v1.2.2
510
689
  [1.2.1]: https://github.com/bymaxone/nest-core/compare/v1.2.0...v1.2.1
511
690
  [1.2.0]: https://github.com/bymaxone/nest-core/compare/v1.1.1...v1.2.0
512
- [Unreleased]: https://github.com/bymaxone/nest-core/compare/v1.3.2...HEAD
691
+ [1.4.0]: https://github.com/bymaxone/nest-core/compare/v1.3.2...v1.4.0
692
+ [1.5.0]: https://github.com/bymaxone/nest-core/compare/v1.4.0...v1.5.0
693
+ [Unreleased]: https://github.com/bymaxone/nest-core/compare/v1.5.0...HEAD