@bymax-one/nest-core 1.3.1 → 1.4.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 +251 -1
- package/README.md +418 -48
- package/dist/index.cjs +344 -163
- package/dist/index.d.cts +327 -18
- package/dist/index.d.ts +327 -18
- package/dist/index.mjs +345 -165
- package/dist/openapi/index.cjs +401 -15
- package/dist/openapi/index.d.cts +159 -4
- package/dist/openapi/index.d.ts +159 -4
- package/dist/openapi/index.mjs +400 -17
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,254 @@ heading here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.4.0] - 2026-08-13
|
|
15
|
+
|
|
16
|
+
HTTP metrics were blind to every request that did not reach a handler. Nest runs
|
|
17
|
+
**middleware → guards → interceptors → pipes → handler**, and the recorder was an
|
|
18
|
+
interceptor, so authentication failures, authorization failures, throttled
|
|
19
|
+
requests and unknown paths were never counted. Measured on a running
|
|
20
|
+
application, three requests — a handler success, a guard rejection, an unknown
|
|
21
|
+
path — produced **one** sample. A deployment could be under a credential-stuffing
|
|
22
|
+
run, a privilege probe or route enumeration with a flat error graph, which makes
|
|
23
|
+
this a security fix rather than an observability improvement.
|
|
24
|
+
|
|
25
|
+
**Apply to a derived backend:** bump the dependency. No code change is needed.
|
|
26
|
+
Expect new `401`/`403`/`429` series on routes that already existed, a new
|
|
27
|
+
`route="<unmatched>"` series for `404`s, and a **lower success rate** — the
|
|
28
|
+
denominator finally includes the rejections. Existing `status_code="200"` series
|
|
29
|
+
keep their values.
|
|
30
|
+
|
|
31
|
+
### Security
|
|
32
|
+
|
|
33
|
+
- **Requests rejected before a handler are now counted.** The recorder moved
|
|
34
|
+
from `APP_INTERCEPTOR` to middleware (`BymaxTimingMiddleware`), applied to
|
|
35
|
+
every route when `timing.enabled` is `true`. Nest runs middleware, then
|
|
36
|
+
guards, then interceptors: a request a guard rejects never reached
|
|
37
|
+
`intercept()`, and a request matching no route never reached a controller.
|
|
38
|
+
Measured on a real application, three requests — a handler success, a guard
|
|
39
|
+
rejection and an unknown path — produced exactly **one** sample. `401`, `403`,
|
|
40
|
+
`429` and `404` were all invisible, which is to say a deployment could be under
|
|
41
|
+
a credential-stuffing run, a privilege probe or route enumeration with a flat
|
|
42
|
+
error graph. All six cases are now counted, and the sample is emitted on the
|
|
43
|
+
response's `'close'` event rather than `'finish'`, so a client that hangs up
|
|
44
|
+
mid-request — what a scanner does — is counted too.
|
|
45
|
+
- **The root path is recorded.** On Express the middleware is mounted at `'/'`
|
|
46
|
+
rather than through a wildcard pattern. The unbraced `'*splat'` skips the root outright,
|
|
47
|
+
and the braced `'{*splat}'` that Nest 11's migration guide prescribes stops
|
|
48
|
+
matching the _prefixed_ root once an application calls `setGlobalPrefix` —
|
|
49
|
+
which production applications almost always do. That was reported as
|
|
50
|
+
nest#14520 and fixed by nest#14522, whose regression test covers Fastify;
|
|
51
|
+
measured on `@nestjs/core` 11.1.28 with the Express adapter, the prefixed root
|
|
52
|
+
still reaches no middleware while the route itself answers `200`. Both
|
|
53
|
+
patterns were measured against the mount, which matched every path in both
|
|
54
|
+
configurations. Fastify needs the opposite choice — see the next entry. One
|
|
55
|
+
limit remains and is documented: module middleware is scoped to the global
|
|
56
|
+
prefix, so a request outside it entirely reaches no middleware.
|
|
57
|
+
- **Fastify records the same labels as Express**, which the documented support
|
|
58
|
+
for both platforms had been promising without any test behind it. Nest runs
|
|
59
|
+
middleware on Fastify through `@fastify/middie`, whose `runMiddie` calls
|
|
60
|
+
`run(req.raw, reply.raw, next)` and copies only `id`, `hostname`, `protocol`,
|
|
61
|
+
`ip`, `ips`, `log`, `query` and `body` onto that raw request — never
|
|
62
|
+
`routeOptions`. The recorder therefore saw no route metadata at all and would
|
|
63
|
+
have labelled every Fastify request `<unmatched>`, destroying the per-route
|
|
64
|
+
breakdown and making a scan indistinguishable from ordinary traffic. Worse,
|
|
65
|
+
`forRoutes('/')` is a mount on Express but an **exact match** on Fastify, so
|
|
66
|
+
most requests produced no sample whatsoever. The module now selects the mount
|
|
67
|
+
per adapter and registers an `onRequest` hook on Fastify that carries the
|
|
68
|
+
resolved template to the recorder. Covered by a new Fastify end-to-end suite.
|
|
69
|
+
- **Unmatched requests record a bounded label.** A request that matched no route
|
|
70
|
+
is recorded as `<unmatched>` (exported as `UNMATCHED_ROUTE`), never the
|
|
71
|
+
requested path. The previous raw-URL fallback would have let anyone mint one
|
|
72
|
+
Prometheus time series per probe, so counting scanner traffic under it would
|
|
73
|
+
have turned this fix into a memory-exhaustion vector.
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
|
|
77
|
+
- **`ITimingSink` implementations now receive more samples**, including requests
|
|
78
|
+
that never reached a handler. Sinks that assumed "one sample per completed
|
|
79
|
+
request" should expect "one sample per closed request". The built-in metrics
|
|
80
|
+
bridge needs no change; a dashboard filtering on `2xx` sees its numbers
|
|
81
|
+
unchanged and its error rates become correct.
|
|
82
|
+
- **No status is relabelled**, and that is deliberate. A client that hangs up
|
|
83
|
+
mid-handler was already counted — destroying the socket does not cancel the
|
|
84
|
+
JavaScript already running, so the handler finished and the interceptor
|
|
85
|
+
recorded an ordinary `200` — and it still is, once, under the same `200`.
|
|
86
|
+
Introducing a sentinel status for aborts would rewrite the value of
|
|
87
|
+
`status_code="200"` series that already exist in every deployment, moving
|
|
88
|
+
error-rate panels with no change in traffic. Whether an abort deserves its own
|
|
89
|
+
status is a separate decision from whether the request is counted at all, and
|
|
90
|
+
this release makes only the second one.
|
|
91
|
+
- **The timing recorder is registered once, not twice.** The middleware
|
|
92
|
+
**replaced** the interceptor rather than joining it — two recorders would
|
|
93
|
+
double every rate an alert threshold is tuned against, which is a quieter
|
|
94
|
+
failure than the one being fixed.
|
|
95
|
+
|
|
96
|
+
### Deprecated
|
|
97
|
+
|
|
98
|
+
- **`TimingInterceptor`** is superseded by `BymaxTimingMiddleware` and is no
|
|
99
|
+
longer registered by `BymaxCoreModule`. It stays exported so an application
|
|
100
|
+
that wired it by hand keeps compiling; registering it alongside the middleware
|
|
101
|
+
records a second sample for every request that reaches a handler.
|
|
102
|
+
|
|
103
|
+
### Added
|
|
104
|
+
|
|
105
|
+
- **`BymaxTimingMiddleware` and `UNMATCHED_ROUTE`** are exported from the package
|
|
106
|
+
root.
|
|
107
|
+
- **A library can describe its own routes in a consumer's document.** A provider
|
|
108
|
+
marked `@BymaxOpenApiContributor()` returns OpenAPI fragments keyed by handler
|
|
109
|
+
identity — `'AuthController.login'` — and they are merged onto the operations
|
|
110
|
+
those handlers produced. It exists because the two obvious alternatives do
|
|
111
|
+
not work: decorating a library's controllers with `@nestjs/swagger` would load
|
|
112
|
+
that peer in every application importing the library, and a consumer-side map
|
|
113
|
+
keyed by path cannot be written by a library mounted through
|
|
114
|
+
`RouterModule.register`, which does not know its own final paths.
|
|
115
|
+
- **`openapi.operationIdFactory`**, plus the exported `OpenApiOperationIdFactory`
|
|
116
|
+
type. This package now always installs a factory so it can learn which handler
|
|
117
|
+
produced which operation, and **delegates the id string** — to this option when
|
|
118
|
+
set, to the format `@nestjs/swagger` itself produces otherwise. Choosing the id
|
|
119
|
+
instead would have renamed every operation in every published document and
|
|
120
|
+
broken any client generated from one; a test compares both documents to keep
|
|
121
|
+
that true if the peer's format ever changes.
|
|
122
|
+
- **The contract types** `IOpenApiContributor`, `OpenApiFragment`,
|
|
123
|
+
`OpenApiFragmentObject` and `OpenApiHandlerKey`, exported from `./openapi` so a
|
|
124
|
+
sibling library can target them at its own compile time.
|
|
125
|
+
- **`BYMAX_OPENAPI_CONTRACT_VERSION`**, and a required `contractVersion` on every
|
|
126
|
+
fragment. A fragment crosses a boundary between independently released
|
|
127
|
+
packages, and on that boundary compile-time types protect nothing: each side
|
|
128
|
+
type-checks against its own installed copy, so only the value travelling at
|
|
129
|
+
runtime can say which shape it is. A revision this package does not speak fails
|
|
130
|
+
the build naming both. Required rather than inferred from absence, following
|
|
131
|
+
the pattern Kubernetes objects use for `apiVersion` — an optional discriminator
|
|
132
|
+
is unambiguous only while exactly one revision exists, which is precisely when
|
|
133
|
+
nobody checks it.
|
|
134
|
+
|
|
135
|
+
### Fixed
|
|
136
|
+
|
|
137
|
+
- **`DiscoveryModule` is imported when only the document is enabled.** It was
|
|
138
|
+
imported on the synchronous registration path only when readiness discovery or
|
|
139
|
+
metrics could scan, so an application enabling nothing but OpenAPI had no
|
|
140
|
+
scanner — and a library's description of its own routes would have been
|
|
141
|
+
silently dropped.
|
|
142
|
+
|
|
143
|
+
### Notes
|
|
144
|
+
|
|
145
|
+
- Deriving fragments from validation decorators is deliberately **not** in this
|
|
146
|
+
package: it takes no dependency on any validation library. A library that
|
|
147
|
+
wants its schemas to track its own decorators generates them in its own build,
|
|
148
|
+
where that dependency already exists, and commits the result with a test
|
|
149
|
+
asserting generated matches committed — so drift fails in the repository that
|
|
150
|
+
caused it. An application's own DTOs need none of this; `@nestjs/swagger`'s CLI
|
|
151
|
+
plugin already derives them, which is the route a precompiled library lacks.
|
|
152
|
+
|
|
153
|
+
## [1.3.2] - 2026-08-12
|
|
154
|
+
|
|
155
|
+
A consumer audit of the served document found that it described the library's
|
|
156
|
+
promises rather than the deployment: routes of features that were switched off
|
|
157
|
+
were still listed, the contributed schemas were never referenced by any
|
|
158
|
+
operation, and nothing said which operations needed authentication. Everything
|
|
159
|
+
below is additive — no option changes meaning, no existing document loses an
|
|
160
|
+
entry it had.
|
|
161
|
+
|
|
162
|
+
**Apply to a derived backend:** bump the dependency. The document improves with
|
|
163
|
+
no code change; the two new options are opt-in.
|
|
164
|
+
|
|
165
|
+
### Added
|
|
166
|
+
|
|
167
|
+
- **`openapi.security` and `openapi.operationSecurity`.** A document-level
|
|
168
|
+
default requirement, plus per-operation overrides keyed `"<METHOD> <path>"`.
|
|
169
|
+
An empty array marks an operation public, which is the specification's own way
|
|
170
|
+
of overriding the default — and it matters for generated clients, since an
|
|
171
|
+
operation with _absent_ security inherits the document default and a client
|
|
172
|
+
would attach credentials to a public registration endpoint.
|
|
173
|
+
- **The operation key is a documented contract**, with `OpenApiOperationKey` and
|
|
174
|
+
`OperationSecurityMap` exported as types so a sibling library can ship a
|
|
175
|
+
plain-data map of its own operations and have it checked at its own compile
|
|
176
|
+
time, with no runtime coupling. The path is written exactly as documented,
|
|
177
|
+
**including any global prefix** — `@nestjs/swagger` puts `setGlobalPrefix` into
|
|
178
|
+
the documented paths, so a library shipping such a map should expose a function
|
|
179
|
+
taking the prefix rather than a frozen constant.
|
|
180
|
+
- **A key addressing no operation fails the document build**, listing both the
|
|
181
|
+
keys that missed and the operations that exist. A stale key would otherwise
|
|
182
|
+
leave a route silently documented as authenticated when it is not, or the
|
|
183
|
+
reverse. Failing is safe here: the document is only ever built outside
|
|
184
|
+
production.
|
|
185
|
+
- **A requirement naming an undeclared security scheme fails the same way.** A
|
|
186
|
+
requirement is a reference, and a reference to nothing produces a document
|
|
187
|
+
whose security cannot be resolved — a client generator looks the name up in
|
|
188
|
+
`components.securitySchemes`, finds nothing, and either fails or emits an
|
|
189
|
+
unauthenticated client. Configuring the requirement and forgetting the scheme
|
|
190
|
+
is one edit apart. A scheme the document itself declares counts as declared,
|
|
191
|
+
and marking an operation public names no scheme, so it needs none.
|
|
192
|
+
|
|
193
|
+
### Fixed
|
|
194
|
+
|
|
195
|
+
- **A disabled feature's routes are no longer documented.** With
|
|
196
|
+
`metrics: { enabled: false }` the runtime answers `GET /metrics` with a 404
|
|
197
|
+
envelope — on `forRootAsync` the controller is mounted unconditionally and
|
|
198
|
+
guards each request, because route metadata is fixed before the async options
|
|
199
|
+
resolve — while the document still advertised it. The filter reads the same
|
|
200
|
+
resolved snapshot the guard reads, so the two cannot drift. `@nestjs/swagger`
|
|
201
|
+
documents paths as the application serves them — the global prefix, and under
|
|
202
|
+
`enableVersioning({ type: URI })` the version segment that follows it, so
|
|
203
|
+
`/api/v1/metrics` — and both are **read from the application** rather than
|
|
204
|
+
inferred from the document. Versioning matters as much as the prefix: without
|
|
205
|
+
it, every versioned application kept advertising the routes of a feature it
|
|
206
|
+
had switched off, and its health probes lost the payload schema and the public
|
|
207
|
+
marking this package contributes. Inference is the trap: an application whose routes all sit under one
|
|
208
|
+
controller prefix would have that treated as the global one, and a consumer
|
|
209
|
+
route ending in `/health/live` deleted as though this package owned it. What
|
|
210
|
+
leaves is also the **operation**, not the path item — a method the consumer
|
|
211
|
+
mounted on the same path survives, and the path disappears only once nothing
|
|
212
|
+
is left under it.
|
|
213
|
+
- **The automatic security policy is stated for `GET` alone.** These controllers
|
|
214
|
+
expose no other method, so a consumer's `POST` on the same path is theirs and
|
|
215
|
+
no longer inherits a requirement written for ours.
|
|
216
|
+
- **The envelope response follows the envelope feature.** With
|
|
217
|
+
`envelope.enabled` off, errors are shaped by Nest or by the consumer's own
|
|
218
|
+
handler, so documenting this package's envelope described a body the
|
|
219
|
+
deployment never sends. The health response is a separate feature and is
|
|
220
|
+
unaffected.
|
|
221
|
+
- **A response written as a bare `$ref` is a declaration.** It carries no
|
|
222
|
+
`content`, so the placeholder rule would have overwritten it — discarding the
|
|
223
|
+
reference and leaving `$ref` beside sibling keys, which is not a valid
|
|
224
|
+
response object.
|
|
225
|
+
- **`BymaxMetricsAuth` is reserved while a scrape token is configured.** The
|
|
226
|
+
name was silently overwritten or silently lost depending on where the other
|
|
227
|
+
definition came from, and the losing case left the scrape operation pointing
|
|
228
|
+
at a scheme that is not the bearer token the runtime checks. It now fails the
|
|
229
|
+
document build with the collision named.
|
|
230
|
+
- **The contributed schemas are referenced by the operations that return them.**
|
|
231
|
+
They shipped orphaned: `components.schemas` carried the envelope, the health
|
|
232
|
+
response and the pagination shapes while no operation pointed at any of them,
|
|
233
|
+
so a generated client had no error type at all. Every operation now carries a
|
|
234
|
+
`default` response referencing `BymaxErrorEnvelope`, and the health endpoints
|
|
235
|
+
an explicit `200` referencing `BymaxHealthResponse`. Gated by
|
|
236
|
+
`includeCoreSchemas`, because referencing a schema that was not contributed
|
|
237
|
+
would leave a dangling `$ref`.
|
|
238
|
+
- **A response is judged by whether it declares a shape.** `@nestjs/swagger`
|
|
239
|
+
emits a placeholder `200` with a description and no content for every handler,
|
|
240
|
+
so a plain "existing always wins" rule would never have written a contributed
|
|
241
|
+
schema. A response carrying `content` is a real declaration and is untouched;
|
|
242
|
+
one without it is filled in, keeping any description already written.
|
|
243
|
+
- **The library documents the security of its own three routes.** The health
|
|
244
|
+
probes are marked public — an orchestrator polls them holding no credential —
|
|
245
|
+
and the scrape endpoint carries a bearer requirement, with its scheme, exactly
|
|
246
|
+
when `metrics.authToken` is set. The library owns both the routes and the
|
|
247
|
+
option, so no consumer should have to restate either.
|
|
248
|
+
|
|
249
|
+
### Documentation
|
|
250
|
+
|
|
251
|
+
- The metrics naming rules are framed as an adoption guideline for sibling
|
|
252
|
+
libraries, with the reason the rules live here: a Prometheus registry is a flat
|
|
253
|
+
namespace, so two libraries picking the same metric name collide at the
|
|
254
|
+
_consumer's_ boot, in an application neither library's CI ever assembles. A
|
|
255
|
+
contributing library is asked to publish its own metric list; this package
|
|
256
|
+
deliberately keeps no central catalogue.
|
|
257
|
+
- `applyBymaxOpenApi` documents that testing its enabled path under Jest needs
|
|
258
|
+
`NODE_OPTIONS=--experimental-vm-modules`, because the optional peer is reached
|
|
259
|
+
through a dynamic `import()`. Only the enabled case fails without it, which is
|
|
260
|
+
what makes the omission confusing.
|
|
261
|
+
|
|
14
262
|
## [1.3.1] - 2026-08-11
|
|
15
263
|
|
|
16
264
|
A patch fixing a defect that existed only in the published artifact: `applyBymaxOpenApi` threw on
|
|
@@ -394,9 +642,11 @@ have regressed from. They are kept because the reasoning is worth having.
|
|
|
394
642
|
[1.0.1]: https://github.com/bymaxone/nest-core/compare/v1.0.0...v1.0.1
|
|
395
643
|
[1.0.0]: https://github.com/bymaxone/nest-core/releases/tag/v1.0.0
|
|
396
644
|
[1.1.1]: https://github.com/bymaxone/nest-core/compare/v1.1.0...v1.1.1
|
|
645
|
+
[1.3.2]: https://github.com/bymaxone/nest-core/compare/v1.3.1...v1.3.2
|
|
397
646
|
[1.3.1]: https://github.com/bymaxone/nest-core/compare/v1.3.0...v1.3.1
|
|
398
647
|
[1.3.0]: https://github.com/bymaxone/nest-core/compare/v1.2.2...v1.3.0
|
|
399
648
|
[1.2.2]: https://github.com/bymaxone/nest-core/compare/v1.2.1...v1.2.2
|
|
400
649
|
[1.2.1]: https://github.com/bymaxone/nest-core/compare/v1.2.0...v1.2.1
|
|
401
650
|
[1.2.0]: https://github.com/bymaxone/nest-core/compare/v1.1.1...v1.2.0
|
|
402
|
-
[
|
|
651
|
+
[1.4.0]: https://github.com/bymaxone/nest-core/compare/v1.3.2...v1.4.0
|
|
652
|
+
[Unreleased]: https://github.com/bymaxone/nest-core/compare/v1.4.0...HEAD
|