@bymax-one/nest-core 1.6.6 → 1.6.7
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 +31 -1
- package/README.md +107 -24
- package/dist/http/index.cjs +64 -0
- package/dist/http/index.d.cts +59 -0
- package/dist/http/index.d.ts +59 -0
- package/dist/http/index.mjs +62 -0
- package/dist/index.cjs +126 -71
- package/dist/index.d.cts +44 -2
- package/dist/index.d.ts +44 -2
- package/dist/index.mjs +128 -73
- package/package.json +25 -3
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,35 @@ heading here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.6.7] - 2026-11-12
|
|
15
|
+
|
|
16
|
+
A patch release. Two floors that every backend re-implemented move into the library; both are additive
|
|
17
|
+
and nothing changes for an application that does not turn them on.
|
|
18
|
+
|
|
19
|
+
**Apply to a derived backend:** `pnpm up @bymax-one/nest-core`, then replace a local body-depth
|
|
20
|
+
middleware with `bodyDepth: { enabled: true }` and a local helmet, parser and timeout block with
|
|
21
|
+
`await applyHttpFloors(app)` from `@bymax-one/nest-core/http` (add `helmet`). Call it right after
|
|
22
|
+
`applyAccessLog` and before the cookie parser, trusted proxies and `applyBymaxOpenApi`.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **`bodyDepth` refuses a request body nested deeper than a limit with a `400`.** A body of about
|
|
27
|
+
2,000 nested arrays is 4 KB, and the recursion in a validator or transformer overflows the stack
|
|
28
|
+
on it, so the filter answered `500` for a request that was only malformed, with no credential
|
|
29
|
+
needed (measured with Nest's `ValidationPipe`). The new `BymaxBodyDepthMiddleware` checks
|
|
30
|
+
the body Nest's parsers already produced, before the application's validation, and answers
|
|
31
|
+
`BYMAX_BAD_REQUEST` ("Request body is nested too deeply"). **Off by default** (`enabled: false`,
|
|
32
|
+
`maxDepth: 32`) because turning it on rejects bodies that are accepted today. Express only; it is
|
|
33
|
+
not mounted on Fastify, whose middleware runs before the body is parsed.
|
|
34
|
+
- **`applyHttpFloors` in the new `./http` subpath.** One bootstrap call for `helmet` without HSTS
|
|
35
|
+
(options pass through, `helmet: false` leaves it out), JSON and urlencoded parsers that refuse a
|
|
36
|
+
compressed body with `415` instead of decompressing it, and the header and request timeouts
|
|
37
|
+
(15 s and 30 s), enforced within a second: it also sets the server's connection sweep to one
|
|
38
|
+
second (`connectionsCheckingIntervalMs`), since Node's default of thirty would let a 15 s timeout
|
|
39
|
+
cut anywhere up to 45 s; it throws when the server is already listening. `helmet` is an optional peer, loaded only when the helper runs. Express only.
|
|
40
|
+
- The root bundle's brotli budget moves from 17.25 to 17.75 KiB for the body-depth code; `./http`
|
|
41
|
+
has a budget of 1 KiB.
|
|
42
|
+
|
|
14
43
|
## [1.6.6] - 2026-11-07
|
|
15
44
|
|
|
16
45
|
A patch release. The error envelope no longer returns the parser's own text for a request refused before
|
|
@@ -1336,4 +1365,5 @@ have regressed from. They are kept because the reasoning is worth having.
|
|
|
1336
1365
|
[1.6.4]: https://github.com/bymaxone/nest-core/compare/v1.6.3...v1.6.4
|
|
1337
1366
|
[1.6.5]: https://github.com/bymaxone/nest-core/compare/v1.6.4...v1.6.5
|
|
1338
1367
|
[1.6.6]: https://github.com/bymaxone/nest-core/compare/v1.6.5...v1.6.6
|
|
1339
|
-
[
|
|
1368
|
+
[1.6.7]: https://github.com/bymaxone/nest-core/compare/v1.6.6...v1.6.7
|
|
1369
|
+
[Unreleased]: https://github.com/bymaxone/nest-core/compare/v1.6.7...HEAD
|
package/README.md
CHANGED
|
@@ -126,6 +126,7 @@ never loads its peer, which the release gate asserts against the packed tarball.
|
|
|
126
126
|
| `./pagination` | `normalizePageQuery`, `buildPageResult`, `normalizeCursorQuery`, `buildCursorResult`, `encodeCursor`, `decodeCursor` and their types — pure functions, no NestJS provider involved |
|
|
127
127
|
| `./health` | `IHealthIndicator`, `HealthResponse`, the indicator contracts and the `@BymaxHealthIndicator()` marker, so a package that only implements an indicator does not import the module |
|
|
128
128
|
| `./metrics` | `IMetricsContributor` and the `@BymaxMetricsContributor()` marker, so a package that only publishes metrics imports neither the module nor its DI tokens. The one subpath whose types name `prom-client`, which anyone implementing the contract already depends on |
|
|
129
|
+
| `./http` | `applyHttpFloors`, the one bootstrap call for the HTTP-level floors: security headers, compressed request bodies refused, and the header and request timeouts — separate so an application that never calls it never loads `helmet` |
|
|
129
130
|
| `./openapi` | `applyBymaxOpenApi`, the one bootstrap call that builds and mounts the OpenAPI document — separate so an application that never documents its API never loads the code that does |
|
|
130
131
|
|
|
131
132
|
Each subpath ships ESM and CommonJS with its own `.d.ts` and `.d.cts`, so
|
|
@@ -138,13 +139,14 @@ pnpm add @bymax-one/nest-core @nestjs/common @nestjs/core reflect-metadata rxjs
|
|
|
138
139
|
```
|
|
139
140
|
|
|
140
141
|
Add `prom-client` if you enable the metrics feature, `@nestjs/swagger` if you
|
|
141
|
-
enable the OpenAPI feature,
|
|
142
|
-
correlation
|
|
143
|
-
loaded, unless you turn its feature on.
|
|
142
|
+
enable the OpenAPI feature, `@opentelemetry/api` if you enable trace
|
|
143
|
+
correlation, and `helmet` if you call `applyHttpFloors`. Each is an optional peer
|
|
144
|
+
dependency: none is required, or ever loaded, unless you turn its feature on.
|
|
144
145
|
|
|
145
146
|
```bash
|
|
146
147
|
pnpm add prom-client
|
|
147
148
|
pnpm add @opentelemetry/api
|
|
149
|
+
pnpm add helmet
|
|
148
150
|
pnpm add -D @nestjs/swagger
|
|
149
151
|
```
|
|
150
152
|
|
|
@@ -232,6 +234,28 @@ and the classification table: [Production is a closed door](#production-is-a-clo
|
|
|
232
234
|
| `enabled` | `boolean` | `true` | Applies the request-timing middleware to every route. |
|
|
233
235
|
| `slowRequestThresholdMs` | `number` | unset | Samples above this duration are flagged `slow: true`. Absent means never slow. |
|
|
234
236
|
|
|
237
|
+
### `bodyDepth`
|
|
238
|
+
|
|
239
|
+
| Option | Type | Default | Description |
|
|
240
|
+
| ---------- | --------- | ------- | -------------------------------------------------------------------------------------------------- |
|
|
241
|
+
| `enabled` | `boolean` | `false` | Refuses a request body nested deeper than `maxDepth` with a `400` through the envelope. See below. |
|
|
242
|
+
| `maxDepth` | `number` | `32` | The deepest nesting a body may have. A flat object or array is depth 1. A positive safe integer. |
|
|
243
|
+
|
|
244
|
+
**Off by default, and worth turning on.** It is off because turning it on rejects bodies that
|
|
245
|
+
are accepted today, and a patch release must not start doing that. Turn it on: a body of about
|
|
246
|
+
2,000 nested arrays is only 4 KB, and the recursion in a validator or transformer
|
|
247
|
+
(`class-validator`, `class-transformer`) overflows the stack on it. The filter then answers
|
|
248
|
+
`500` for a request that was only malformed — measured with Nest's `ValidationPipe` on an
|
|
249
|
+
application route, needing no credential, so it is also a way to make a healthy service look
|
|
250
|
+
broken to a monitor. This package ships no validation of its own, so the limit sits in front of
|
|
251
|
+
yours: the check runs on the body Nest's parsers already produced and answers
|
|
252
|
+
`400 BYMAX_BAD_REQUEST` ("Request body is nested too deeply") before routing, so it protects
|
|
253
|
+
every route, this package's own included, before any pipe runs.
|
|
254
|
+
|
|
255
|
+
It covers the Express adapter. It does nothing when the application disables Nest's body
|
|
256
|
+
parsers, or parses bodies after module middleware has run, and it is not mounted on Fastify,
|
|
257
|
+
whose middleware runs before the body is parsed.
|
|
258
|
+
|
|
235
259
|
### `health`
|
|
236
260
|
|
|
237
261
|
| Option | Type | Default | Description |
|
|
@@ -1681,6 +1705,55 @@ same `@Global()`-module pattern is how every pluggable token in this package
|
|
|
1681
1705
|
is overridden: `BYMAX_TIMING_SINK` and `BYMAX_HEALTH_INDICATORS` follow it
|
|
1682
1706
|
identically.
|
|
1683
1707
|
|
|
1708
|
+
## 🧱 HTTP Floors
|
|
1709
|
+
|
|
1710
|
+
`applyHttpFloors` (from `@bymax-one/nest-core/http`) is the HTTP-level protection that every
|
|
1711
|
+
backend wants and none should tune per deployment. One call, before `app.listen()`:
|
|
1712
|
+
|
|
1713
|
+
```typescript
|
|
1714
|
+
import { NestFactory } from '@nestjs/core'
|
|
1715
|
+
import { applyHttpFloors } from '@bymax-one/nest-core/http'
|
|
1716
|
+
import { AppModule } from './app.module'
|
|
1717
|
+
|
|
1718
|
+
async function bootstrap(): Promise<void> {
|
|
1719
|
+
const app = await NestFactory.create(AppModule)
|
|
1720
|
+
await applyHttpFloors(app)
|
|
1721
|
+
await app.listen(3000)
|
|
1722
|
+
}
|
|
1723
|
+
void bootstrap()
|
|
1724
|
+
```
|
|
1725
|
+
|
|
1726
|
+
| What | Default |
|
|
1727
|
+
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1728
|
+
| Security headers | `helmet()` with `strictTransportSecurity: false` (TLS is normally ended in front of the app, which owns HSTS). Pass `helmet: { ... }` to tune it, for example the CSP of a browser-serving backend, or `helmet: false` to leave it out |
|
|
1729
|
+
| Request bodies | The JSON and urlencoded parsers refuse a compressed body (`Content-Encoding`) with `415` instead of decompressing it, on every route and before routing. Size limits and the extended form syntax keep the parsers' defaults |
|
|
1730
|
+
| `headersTimeoutMs` | `15000`: the longest a client may take to send its complete header block. Node's own default is 60 s, which leaves a header block sent a byte at a time open as a cheap denial of service |
|
|
1731
|
+
| `requestTimeoutMs` | `30000`: the longest a single request, headers and body together, may take. Node's default is 5 minutes. Must not be below `headersTimeoutMs` |
|
|
1732
|
+
|
|
1733
|
+
`keepAliveTimeout` is left at Node's 5 s: raising it only lengthens idle-socket retention.
|
|
1734
|
+
|
|
1735
|
+
Node enforces both timeouts from a sweep over the open connections, not from a timer per
|
|
1736
|
+
connection, and the sweep runs every 30 s by default: measured, a 1 s header timeout left a
|
|
1737
|
+
dribbled header block open for 30 s. The helper therefore also sets the server's
|
|
1738
|
+
`connectionsCheckingInterval` to one second (or the header timeout, if that is shorter), so a
|
|
1739
|
+
connection is cut within a second of its timeout expiring. Nest creates the server without
|
|
1740
|
+
exposing its options, so this is a property set before `listen()`: Node reads it when the server
|
|
1741
|
+
starts listening, and the helper throws if the server is already listening. The sweep walks every
|
|
1742
|
+
open connection each time, so a server holding very many sockets can set
|
|
1743
|
+
`connectionsCheckingIntervalMs` higher, at the cost of cutting up to that long after the timeout.
|
|
1744
|
+
|
|
1745
|
+
**Order is part of the contract.** Call it right after `applyAccessLog` from
|
|
1746
|
+
`@bymax-one/nest-logger`, and before everything that reads the request: helmet is mounted first,
|
|
1747
|
+
so even a request the body parsers refuse carries the headers, and the parsers are registered
|
|
1748
|
+
here so Nest's own defaults, which accept gzip, are not (an application that needs a raw body on a route mounts that parser before calling it). Cookie parsing, the trusted-proxy
|
|
1749
|
+
setting and `applyBymaxOpenApi` stay with you and go after it:
|
|
1750
|
+
`applyAccessLog` → `applyHttpFloors` → cookie parser → trusted proxies → `applyBymaxOpenApi`.
|
|
1751
|
+
|
|
1752
|
+
Express only; on another adapter it throws before applying anything. `helmet` is an optional
|
|
1753
|
+
peer, loaded only when the helper runs, and a missing one fails with the install command. An
|
|
1754
|
+
unusable timeout (zero, fractional, or a header timeout above the request timeout) throws
|
|
1755
|
+
rather than leaving a socket limit switched off.
|
|
1756
|
+
|
|
1684
1757
|
## 🏗️ Architecture
|
|
1685
1758
|
|
|
1686
1759
|
```
|
|
@@ -1836,6 +1909,8 @@ the deployment nobody configured is the one most likely to be exposed.
|
|
|
1836
1909
|
| Health output | The response names which indicator is down and nothing more; the reason goes to the logger. `exposeIndicatorErrors` (default `false`) puts it back in the response for debugging |
|
|
1837
1910
|
| Slow indicators | Converted to `down` by the aggregator, so a probe cannot hang on one |
|
|
1838
1911
|
| Correlation | Resolved through `BYMAX_CORRELATION_PROVIDER` — the app decides where the id comes from |
|
|
1912
|
+
| Body nesting | Opt-in `bodyDepth` refuses a body nested deeper than `maxDepth` with a `400`, so a few kilobytes of nested arrays cannot overflow a recursive validator into a `500` |
|
|
1913
|
+
| HTTP floors | `applyHttpFloors`: compressed request bodies refused with `415`, header and request timeouts of 15 s and 30 s, `helmet` headers without HSTS |
|
|
1839
1914
|
| Request accounting | Every closed request is counted, including the ones a guard rejected (`401`/`403`/`429`) and the ones that matched no route (`404`), so an attack in progress moves the graph |
|
|
1840
1915
|
| Route labels | Taken from the matched template; an unmatched request records the constant `<unmatched>`, never the requested path, so a scan cannot grow the metric registry without bound |
|
|
1841
1916
|
| Pagination cursors | Opaque, not authenticated; treated as client-supplied input on the way back in |
|
|
@@ -1909,27 +1984,28 @@ in the sections above.
|
|
|
1909
1984
|
|
|
1910
1985
|
### `.` (root)
|
|
1911
1986
|
|
|
1912
|
-
| Export
|
|
1913
|
-
|
|
|
1914
|
-
| `BymaxCoreModule`
|
|
1915
|
-
| `BymaxCoreModuleOptions`, `EnvelopeOptions`, `TimingOptions`, `HealthOptions`, `MetricsOptions`, `TelemetryOptions`, `OpenApiOptions`, `OpenApiServerDescriptor`, `OpenApiSecurityScheme`, `ResolvedCoreOptions`
|
|
1916
|
-
| `OpenApiSecurityRequirement`, `OpenApiHttpMethod`, `OpenApiOperationKey`, `OperationSecurityMap`
|
|
1917
|
-
| `OpenApiOperationIdFactory`
|
|
1918
|
-
| `BYMAX_CORE_OPTIONS`, `BYMAX_CORRELATION_PROVIDER`, `BYMAX_TIMING_SINK`, `BYMAX_HEALTH_INDICATORS`, `BYMAX_HEALTH_TRANSITION_SINK`, `BYMAX_UNEXPECTED_ERROR_SINK`, `BYMAX_METRICS_REGISTRY`, `BYMAX_TRACE_CONTEXT`
|
|
1919
|
-
| `IHealthTransitionSink`, `HealthTransition`, `HealthTransitionCause`
|
|
1920
|
-
| `ICorrelationIdProvider`
|
|
1921
|
-
| `ITraceContextProvider`, `TraceContext`
|
|
1922
|
-
| `BymaxExceptionFilter`
|
|
1923
|
-
| `FilterErrorContext`
|
|
1924
|
-
| `IUnexpectedErrorSink`, `UnexpectedErrorReport`
|
|
1925
|
-
| `buildErrorEnvelope`
|
|
1926
|
-
| `ErrorEnvelope`, `ErrorDetails`, `BuildErrorEnvelopeInput`
|
|
1927
|
-
| `BymaxTimingMiddleware`
|
|
1928
|
-
| `
|
|
1929
|
-
| `
|
|
1930
|
-
| `
|
|
1931
|
-
| `
|
|
1932
|
-
| `
|
|
1987
|
+
| Export | Kind | Description |
|
|
1988
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1989
|
+
| `BymaxCoreModule` | class | The dynamic module: `forRoot` and `forRootAsync`. |
|
|
1990
|
+
| `BymaxCoreModuleOptions`, `EnvelopeOptions`, `TimingOptions`, `BodyDepthOptions`, `HealthOptions`, `MetricsOptions`, `TelemetryOptions`, `OpenApiOptions`, `OpenApiServerDescriptor`, `OpenApiSecurityScheme`, `ResolvedCoreOptions` | types | The options surface and its resolved shape. |
|
|
1991
|
+
| `OpenApiSecurityRequirement`, `OpenApiHttpMethod`, `OpenApiOperationKey`, `OperationSecurityMap` | types | The operation-key contract a sibling library targets to ship its own security map. |
|
|
1992
|
+
| `OpenApiOperationIdFactory` | type | Names the operations in the generated document. |
|
|
1993
|
+
| `BYMAX_CORE_OPTIONS`, `BYMAX_CORRELATION_PROVIDER`, `BYMAX_TIMING_SINK`, `BYMAX_HEALTH_INDICATORS`, `BYMAX_HEALTH_TRANSITION_SINK`, `BYMAX_UNEXPECTED_ERROR_SINK`, `BYMAX_METRICS_REGISTRY`, `BYMAX_TRACE_CONTEXT` | tokens | The DI tokens; see the [token table](#-di-tokens). |
|
|
1994
|
+
| `IHealthTransitionSink`, `HealthTransition`, `HealthTransitionCause` | types | The readiness-transition contract; also on `./health`. |
|
|
1995
|
+
| `ICorrelationIdProvider` | type | The correlation-provider contract. |
|
|
1996
|
+
| `ITraceContextProvider`, `TraceContext` | types | The trace-context contract and the identifiers it resolves. |
|
|
1997
|
+
| `BymaxExceptionFilter` | class | The envelope exception filter. |
|
|
1998
|
+
| `FilterErrorContext` | type | The neutral request context passed to the filter's observability seam. |
|
|
1999
|
+
| `IUnexpectedErrorSink`, `UnexpectedErrorReport` | types | Receives the cause behind each collapsed `500 BYMAX_INTERNAL_ERROR`; see [Recording the cause of a 500](#recording-the-cause-of-a-500). |
|
|
2000
|
+
| `buildErrorEnvelope` | function | Pure builder assembling an `ErrorEnvelope`. |
|
|
2001
|
+
| `ErrorEnvelope`, `ErrorDetails`, `BuildErrorEnvelopeInput` | types | The envelope contract and its builder input. |
|
|
2002
|
+
| `BymaxTimingMiddleware` | class | The request-timing middleware, applied to every route when timing is enabled. |
|
|
2003
|
+
| `BymaxBodyDepthMiddleware` | class | Refuses a body nested deeper than `bodyDepth.maxDepth` with a `400`; applied to every route, after the timing middleware, only when `bodyDepth.enabled` is true. |
|
|
2004
|
+
| `UNMATCHED_ROUTE` | constant | The bounded label recorded when a request matched no route (`<unmatched>`). |
|
|
2005
|
+
| `TimingInterceptor` | class | **Deprecated, registered nowhere.** `BymaxTimingMiddleware` replaced it as the recorder because guards run before interceptors, so it never saw a request a guard rejected; wiring it by hand next to the middleware records a second sample for every request that reaches a handler. Kept so code that wired it by hand keeps compiling. |
|
|
2006
|
+
| `ITimingSink`, `RequestTimingSample` | types | The timing-sink contract and its sample shape. |
|
|
2007
|
+
| `BYMAX_BAD_GATEWAY` … `BYMAX_VALIDATION_FAILED` | constants | The full error-code catalog (see [Error envelope](#-error-envelope)). |
|
|
2008
|
+
| `codeForStatus` | function | Derives a catalog code from an HTTP status. |
|
|
1933
2009
|
|
|
1934
2010
|
### `./pagination`
|
|
1935
2011
|
|
|
@@ -1963,6 +2039,13 @@ in the sections above.
|
|
|
1963
2039
|
| `IMetricsContributor` | type | The contract: `registerMetrics(registry)`. |
|
|
1964
2040
|
| `MetricsRegistry` | type | The `prom-client` registry the scrape endpoint serves. |
|
|
1965
2041
|
|
|
2042
|
+
### `./http`
|
|
2043
|
+
|
|
2044
|
+
| Export | Kind | Description |
|
|
2045
|
+
| ---------------------------------------------- | -------- | --------------------------------------------------------------------------------------- |
|
|
2046
|
+
| `applyHttpFloors` | function | Applies the headers, the body-parser floor and the timeouts; call it before `listen()`. |
|
|
2047
|
+
| `HttpFloorsOptions`, `HttpFloorsHelmetOptions` | types | The `helmet`, `headersTimeoutMs` and `requestTimeoutMs` options. |
|
|
2048
|
+
|
|
1966
2049
|
### `./openapi`
|
|
1967
2050
|
|
|
1968
2051
|
| Export | Kind | Description |
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/optional-peer.ts
|
|
4
|
+
function isMissingModuleError(cause) {
|
|
5
|
+
const code = cause.code;
|
|
6
|
+
return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// src/http/http.loader.ts
|
|
10
|
+
var MISSING_PEER_MESSAGE = "applyHttpFloors needs the optional peer helmet, which is not installed. Run: pnpm add helmet";
|
|
11
|
+
async function loadHelmet() {
|
|
12
|
+
try {
|
|
13
|
+
const loaded = await import('helmet');
|
|
14
|
+
return loaded.default;
|
|
15
|
+
} catch (cause) {
|
|
16
|
+
if (isMissingModuleError(cause)) {
|
|
17
|
+
throw new Error(MISSING_PEER_MESSAGE, { cause });
|
|
18
|
+
}
|
|
19
|
+
throw cause;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/http/http-floors.ts
|
|
24
|
+
var DEFAULT_HEADERS_TIMEOUT_MS = 15e3;
|
|
25
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
26
|
+
var DEFAULT_CONNECTIONS_CHECKING_INTERVAL_MS = 1e3;
|
|
27
|
+
function assertPositiveInteger(name, value) {
|
|
28
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
29
|
+
throw new Error(`${name} must be a positive integer, received ${String(value)}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function applyHttpFloors(app, options = {}) {
|
|
33
|
+
const headersTimeoutMs = options.headersTimeoutMs ?? DEFAULT_HEADERS_TIMEOUT_MS;
|
|
34
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
35
|
+
assertPositiveInteger("headersTimeoutMs", headersTimeoutMs);
|
|
36
|
+
assertPositiveInteger("requestTimeoutMs", requestTimeoutMs);
|
|
37
|
+
if (headersTimeoutMs > requestTimeoutMs) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`headersTimeoutMs (${headersTimeoutMs}) must not exceed requestTimeoutMs (${requestTimeoutMs})`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const checkingIntervalMs = options.connectionsCheckingIntervalMs ?? Math.min(DEFAULT_CONNECTIONS_CHECKING_INTERVAL_MS, headersTimeoutMs);
|
|
43
|
+
assertPositiveInteger("connectionsCheckingIntervalMs", checkingIntervalMs);
|
|
44
|
+
const express = app;
|
|
45
|
+
if (express.getHttpAdapter().getType?.() !== "express") {
|
|
46
|
+
throw new Error("applyHttpFloors supports the Express adapter only");
|
|
47
|
+
}
|
|
48
|
+
const server = express.getHttpServer();
|
|
49
|
+
if (server.listening) {
|
|
50
|
+
throw new Error("applyHttpFloors must run before listen(): the server is already listening");
|
|
51
|
+
}
|
|
52
|
+
const given = options.helmet ?? {};
|
|
53
|
+
if (given !== false) {
|
|
54
|
+
const helmet = await loadHelmet();
|
|
55
|
+
express.use(helmet("hsts" in given ? given : { strictTransportSecurity: false, ...given }));
|
|
56
|
+
}
|
|
57
|
+
express.useBodyParser("json", { inflate: false });
|
|
58
|
+
express.useBodyParser("urlencoded", { inflate: false, extended: true });
|
|
59
|
+
server.headersTimeout = headersTimeoutMs;
|
|
60
|
+
server.requestTimeout = requestTimeoutMs;
|
|
61
|
+
server.connectionsCheckingInterval = checkingIntervalMs;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
exports.applyHttpFloors = applyHttpFloors;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { INestApplication } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview `applyHttpFloors`, the HTTP-level protections every backend wants and none should
|
|
5
|
+
* tune per deployment: security headers without HSTS, compressed request bodies refused, and
|
|
6
|
+
* timeouts that bound how long a client may hold a socket, enforced to within a second.
|
|
7
|
+
*
|
|
8
|
+
* Order is part of the contract. Call it right after the access log is installed
|
|
9
|
+
* (`applyAccessLog` from `@bymax-one/nest-logger`) and before anything that reads the request:
|
|
10
|
+
* helmet is mounted first so even a request the body parsers refuse carries the headers, and the
|
|
11
|
+
* parsers are registered here so Nest's defaults (which accept gzip) are not. Cookie parsing, the
|
|
12
|
+
* trusted-proxy setting and OpenAPI stay with the caller and go after this call.
|
|
13
|
+
*
|
|
14
|
+
* Express only. `helmet` is an optional peer, loaded when this runs.
|
|
15
|
+
* @layer Bootstrap
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** The `helmet()` options object; any option `helmet` accepts. */
|
|
19
|
+
type HttpFloorsHelmetOptions = Readonly<Record<string, unknown>>;
|
|
20
|
+
/** Options for {@link applyHttpFloors}. */
|
|
21
|
+
interface HttpFloorsOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Options handed to `helmet()`, over `{ strictTransportSecurity: false }` (TLS is normally
|
|
24
|
+
* terminated in front of the application, which owns HSTS). A browser-serving backend tunes
|
|
25
|
+
* `contentSecurityPolicy` here. `false` leaves helmet out entirely.
|
|
26
|
+
*/
|
|
27
|
+
readonly helmet?: HttpFloorsHelmetOptions | false;
|
|
28
|
+
/**
|
|
29
|
+
* Longest a client may take to send its complete header block before the socket is closed.
|
|
30
|
+
* Positive integer, at most `requestTimeoutMs`. Default: `15000`.
|
|
31
|
+
*/
|
|
32
|
+
readonly headersTimeoutMs?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Longest a single request may take, headers and body together. Positive integer.
|
|
35
|
+
* Default: `30000`.
|
|
36
|
+
*/
|
|
37
|
+
readonly requestTimeoutMs?: number;
|
|
38
|
+
/**
|
|
39
|
+
* How often the server sweeps its open connections for expired timeouts. Positive integer.
|
|
40
|
+
* Default: `1000`, or `headersTimeoutMs` when that is shorter. Node enforces both timeouts from
|
|
41
|
+
* this sweep rather than from a timer per connection, and its own default is `30000`, so a
|
|
42
|
+
* `headersTimeoutMs` of 15 s would cut anywhere between 15 and 45 s. The sweep walks every open
|
|
43
|
+
* connection each time, so a server holding very many sockets may set it higher, at the cost
|
|
44
|
+
* of cutting up to that long after the timeout.
|
|
45
|
+
*/
|
|
46
|
+
readonly connectionsCheckingIntervalMs?: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Apply the HTTP floors to an Express application. Call it before `app.init()` or `listen`.
|
|
50
|
+
*
|
|
51
|
+
* @param app - The Nest application, created on the Express adapter.
|
|
52
|
+
* @param options - Overrides for the helmet options and the two timeouts.
|
|
53
|
+
* @throws Error When a timeout is unusable, the application is not on Express, or `helmet` is
|
|
54
|
+
* needed and not installed. Nothing is applied in those cases.
|
|
55
|
+
*/
|
|
56
|
+
declare function applyHttpFloors(app: INestApplication, options?: HttpFloorsOptions): Promise<void>;
|
|
57
|
+
|
|
58
|
+
export { applyHttpFloors };
|
|
59
|
+
export type { HttpFloorsHelmetOptions, HttpFloorsOptions };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { INestApplication } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview `applyHttpFloors`, the HTTP-level protections every backend wants and none should
|
|
5
|
+
* tune per deployment: security headers without HSTS, compressed request bodies refused, and
|
|
6
|
+
* timeouts that bound how long a client may hold a socket, enforced to within a second.
|
|
7
|
+
*
|
|
8
|
+
* Order is part of the contract. Call it right after the access log is installed
|
|
9
|
+
* (`applyAccessLog` from `@bymax-one/nest-logger`) and before anything that reads the request:
|
|
10
|
+
* helmet is mounted first so even a request the body parsers refuse carries the headers, and the
|
|
11
|
+
* parsers are registered here so Nest's defaults (which accept gzip) are not. Cookie parsing, the
|
|
12
|
+
* trusted-proxy setting and OpenAPI stay with the caller and go after this call.
|
|
13
|
+
*
|
|
14
|
+
* Express only. `helmet` is an optional peer, loaded when this runs.
|
|
15
|
+
* @layer Bootstrap
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** The `helmet()` options object; any option `helmet` accepts. */
|
|
19
|
+
type HttpFloorsHelmetOptions = Readonly<Record<string, unknown>>;
|
|
20
|
+
/** Options for {@link applyHttpFloors}. */
|
|
21
|
+
interface HttpFloorsOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Options handed to `helmet()`, over `{ strictTransportSecurity: false }` (TLS is normally
|
|
24
|
+
* terminated in front of the application, which owns HSTS). A browser-serving backend tunes
|
|
25
|
+
* `contentSecurityPolicy` here. `false` leaves helmet out entirely.
|
|
26
|
+
*/
|
|
27
|
+
readonly helmet?: HttpFloorsHelmetOptions | false;
|
|
28
|
+
/**
|
|
29
|
+
* Longest a client may take to send its complete header block before the socket is closed.
|
|
30
|
+
* Positive integer, at most `requestTimeoutMs`. Default: `15000`.
|
|
31
|
+
*/
|
|
32
|
+
readonly headersTimeoutMs?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Longest a single request may take, headers and body together. Positive integer.
|
|
35
|
+
* Default: `30000`.
|
|
36
|
+
*/
|
|
37
|
+
readonly requestTimeoutMs?: number;
|
|
38
|
+
/**
|
|
39
|
+
* How often the server sweeps its open connections for expired timeouts. Positive integer.
|
|
40
|
+
* Default: `1000`, or `headersTimeoutMs` when that is shorter. Node enforces both timeouts from
|
|
41
|
+
* this sweep rather than from a timer per connection, and its own default is `30000`, so a
|
|
42
|
+
* `headersTimeoutMs` of 15 s would cut anywhere between 15 and 45 s. The sweep walks every open
|
|
43
|
+
* connection each time, so a server holding very many sockets may set it higher, at the cost
|
|
44
|
+
* of cutting up to that long after the timeout.
|
|
45
|
+
*/
|
|
46
|
+
readonly connectionsCheckingIntervalMs?: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Apply the HTTP floors to an Express application. Call it before `app.init()` or `listen`.
|
|
50
|
+
*
|
|
51
|
+
* @param app - The Nest application, created on the Express adapter.
|
|
52
|
+
* @param options - Overrides for the helmet options and the two timeouts.
|
|
53
|
+
* @throws Error When a timeout is unusable, the application is not on Express, or `helmet` is
|
|
54
|
+
* needed and not installed. Nothing is applied in those cases.
|
|
55
|
+
*/
|
|
56
|
+
declare function applyHttpFloors(app: INestApplication, options?: HttpFloorsOptions): Promise<void>;
|
|
57
|
+
|
|
58
|
+
export { applyHttpFloors };
|
|
59
|
+
export type { HttpFloorsHelmetOptions, HttpFloorsOptions };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/optional-peer.ts
|
|
2
|
+
function isMissingModuleError(cause) {
|
|
3
|
+
const code = cause.code;
|
|
4
|
+
return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// src/http/http.loader.ts
|
|
8
|
+
var MISSING_PEER_MESSAGE = "applyHttpFloors needs the optional peer helmet, which is not installed. Run: pnpm add helmet";
|
|
9
|
+
async function loadHelmet() {
|
|
10
|
+
try {
|
|
11
|
+
const loaded = await import('helmet');
|
|
12
|
+
return loaded.default;
|
|
13
|
+
} catch (cause) {
|
|
14
|
+
if (isMissingModuleError(cause)) {
|
|
15
|
+
throw new Error(MISSING_PEER_MESSAGE, { cause });
|
|
16
|
+
}
|
|
17
|
+
throw cause;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/http/http-floors.ts
|
|
22
|
+
var DEFAULT_HEADERS_TIMEOUT_MS = 15e3;
|
|
23
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
24
|
+
var DEFAULT_CONNECTIONS_CHECKING_INTERVAL_MS = 1e3;
|
|
25
|
+
function assertPositiveInteger(name, value) {
|
|
26
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
27
|
+
throw new Error(`${name} must be a positive integer, received ${String(value)}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function applyHttpFloors(app, options = {}) {
|
|
31
|
+
const headersTimeoutMs = options.headersTimeoutMs ?? DEFAULT_HEADERS_TIMEOUT_MS;
|
|
32
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
33
|
+
assertPositiveInteger("headersTimeoutMs", headersTimeoutMs);
|
|
34
|
+
assertPositiveInteger("requestTimeoutMs", requestTimeoutMs);
|
|
35
|
+
if (headersTimeoutMs > requestTimeoutMs) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`headersTimeoutMs (${headersTimeoutMs}) must not exceed requestTimeoutMs (${requestTimeoutMs})`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const checkingIntervalMs = options.connectionsCheckingIntervalMs ?? Math.min(DEFAULT_CONNECTIONS_CHECKING_INTERVAL_MS, headersTimeoutMs);
|
|
41
|
+
assertPositiveInteger("connectionsCheckingIntervalMs", checkingIntervalMs);
|
|
42
|
+
const express = app;
|
|
43
|
+
if (express.getHttpAdapter().getType?.() !== "express") {
|
|
44
|
+
throw new Error("applyHttpFloors supports the Express adapter only");
|
|
45
|
+
}
|
|
46
|
+
const server = express.getHttpServer();
|
|
47
|
+
if (server.listening) {
|
|
48
|
+
throw new Error("applyHttpFloors must run before listen(): the server is already listening");
|
|
49
|
+
}
|
|
50
|
+
const given = options.helmet ?? {};
|
|
51
|
+
if (given !== false) {
|
|
52
|
+
const helmet = await loadHelmet();
|
|
53
|
+
express.use(helmet("hsts" in given ? given : { strictTransportSecurity: false, ...given }));
|
|
54
|
+
}
|
|
55
|
+
express.useBodyParser("json", { inflate: false });
|
|
56
|
+
express.useBodyParser("urlencoded", { inflate: false, extended: true });
|
|
57
|
+
server.headersTimeout = headersTimeoutMs;
|
|
58
|
+
server.requestTimeout = requestTimeoutMs;
|
|
59
|
+
server.connectionsCheckingInterval = checkingIntervalMs;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { applyHttpFloors };
|
package/dist/index.cjs
CHANGED
|
@@ -18,6 +18,114 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
18
18
|
};
|
|
19
19
|
var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
|
|
20
20
|
|
|
21
|
+
// src/core.tokens.ts
|
|
22
|
+
var BYMAX_CORE_OPTIONS = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:core-options");
|
|
23
|
+
var BYMAX_CORRELATION_PROVIDER = /* @__PURE__ */ Symbol.for(
|
|
24
|
+
"@bymax-one/nest-core:correlation-provider"
|
|
25
|
+
);
|
|
26
|
+
var BYMAX_TIMING_SINK = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:timing-sink");
|
|
27
|
+
var BYMAX_HEALTH_INDICATORS = /* @__PURE__ */ Symbol.for(
|
|
28
|
+
"@bymax-one/nest-core:health-indicators"
|
|
29
|
+
);
|
|
30
|
+
var BYMAX_HEALTH_TRANSITION_SINK = /* @__PURE__ */ Symbol.for(
|
|
31
|
+
"@bymax-one/nest-core:health-transition-sink"
|
|
32
|
+
);
|
|
33
|
+
var BYMAX_UNEXPECTED_ERROR_SINK = /* @__PURE__ */ Symbol.for(
|
|
34
|
+
"@bymax-one/nest-core:unexpected-error-sink"
|
|
35
|
+
);
|
|
36
|
+
var BYMAX_METRICS_REGISTRY = /* @__PURE__ */ Symbol.for(
|
|
37
|
+
"@bymax-one/nest-core:metrics-registry"
|
|
38
|
+
);
|
|
39
|
+
var BYMAX_TRACE_CONTEXT = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:trace-context");
|
|
40
|
+
|
|
41
|
+
// src/envelope/error-codes.ts
|
|
42
|
+
var BYMAX_BAD_REQUEST = "BYMAX_BAD_REQUEST";
|
|
43
|
+
var BYMAX_VALIDATION_FAILED = "BYMAX_VALIDATION_FAILED";
|
|
44
|
+
var BYMAX_UNAUTHORIZED = "BYMAX_UNAUTHORIZED";
|
|
45
|
+
var BYMAX_FORBIDDEN = "BYMAX_FORBIDDEN";
|
|
46
|
+
var BYMAX_NOT_FOUND = "BYMAX_NOT_FOUND";
|
|
47
|
+
var BYMAX_CONFLICT = "BYMAX_CONFLICT";
|
|
48
|
+
var BYMAX_PAYLOAD_TOO_LARGE = "BYMAX_PAYLOAD_TOO_LARGE";
|
|
49
|
+
var BYMAX_UNSUPPORTED_MEDIA_TYPE = "BYMAX_UNSUPPORTED_MEDIA_TYPE";
|
|
50
|
+
var BYMAX_UNPROCESSABLE_ENTITY = "BYMAX_UNPROCESSABLE_ENTITY";
|
|
51
|
+
var BYMAX_TOO_MANY_REQUESTS = "BYMAX_TOO_MANY_REQUESTS";
|
|
52
|
+
var BYMAX_CLIENT_ERROR = "BYMAX_CLIENT_ERROR";
|
|
53
|
+
var BYMAX_INTERNAL_ERROR = "BYMAX_INTERNAL_ERROR";
|
|
54
|
+
var BYMAX_NOT_IMPLEMENTED = "BYMAX_NOT_IMPLEMENTED";
|
|
55
|
+
var BYMAX_BAD_GATEWAY = "BYMAX_BAD_GATEWAY";
|
|
56
|
+
var BYMAX_SERVICE_UNAVAILABLE = "BYMAX_SERVICE_UNAVAILABLE";
|
|
57
|
+
var BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
|
|
58
|
+
var STATUS_CODES = /* @__PURE__ */ new Map([
|
|
59
|
+
[400, BYMAX_BAD_REQUEST],
|
|
60
|
+
[401, BYMAX_UNAUTHORIZED],
|
|
61
|
+
[403, BYMAX_FORBIDDEN],
|
|
62
|
+
[404, BYMAX_NOT_FOUND],
|
|
63
|
+
[409, BYMAX_CONFLICT],
|
|
64
|
+
[413, BYMAX_PAYLOAD_TOO_LARGE],
|
|
65
|
+
[415, BYMAX_UNSUPPORTED_MEDIA_TYPE],
|
|
66
|
+
[422, BYMAX_UNPROCESSABLE_ENTITY],
|
|
67
|
+
[429, BYMAX_TOO_MANY_REQUESTS],
|
|
68
|
+
[500, BYMAX_INTERNAL_ERROR],
|
|
69
|
+
[501, BYMAX_NOT_IMPLEMENTED],
|
|
70
|
+
[502, BYMAX_BAD_GATEWAY],
|
|
71
|
+
[503, BYMAX_SERVICE_UNAVAILABLE],
|
|
72
|
+
[504, BYMAX_GATEWAY_TIMEOUT]
|
|
73
|
+
]);
|
|
74
|
+
var CLIENT_ERROR_MIN = 400;
|
|
75
|
+
var CLIENT_ERROR_MAX = 500;
|
|
76
|
+
function codeForStatus(status) {
|
|
77
|
+
const catalogued = STATUS_CODES.get(status);
|
|
78
|
+
if (catalogued !== void 0) {
|
|
79
|
+
return catalogued;
|
|
80
|
+
}
|
|
81
|
+
if (status >= CLIENT_ERROR_MIN && status < CLIENT_ERROR_MAX) {
|
|
82
|
+
return BYMAX_CLIENT_ERROR;
|
|
83
|
+
}
|
|
84
|
+
return BYMAX_INTERNAL_ERROR;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/body-depth/body-depth.middleware.ts
|
|
88
|
+
var BODY_TOO_DEEP_MESSAGE = "Request body is nested too deeply";
|
|
89
|
+
function exceedsDepth(value, maxDepth) {
|
|
90
|
+
let level = [value];
|
|
91
|
+
for (let depth = 0; level.length > 0; depth += 1) {
|
|
92
|
+
const next = [];
|
|
93
|
+
for (const node of level) {
|
|
94
|
+
if (typeof node === "object" && node !== null) {
|
|
95
|
+
if (depth >= maxDepth) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
for (const child of Object.values(node)) {
|
|
99
|
+
next.push(child);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
level = next;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
exports.BymaxBodyDepthMiddleware = class BymaxBodyDepthMiddleware {
|
|
108
|
+
constructor(options) {
|
|
109
|
+
this.options = options;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* @param req - The request, whose `body` the parser has already filled.
|
|
113
|
+
* @param _res - Unused.
|
|
114
|
+
* @param next - Continues the chain.
|
|
115
|
+
* @throws BadRequestException When the body is nested too deeply.
|
|
116
|
+
*/
|
|
117
|
+
use(req, _res, next) {
|
|
118
|
+
if (exceedsDepth(req.body, this.options.bodyDepth.maxDepth)) {
|
|
119
|
+
throw new common.BadRequestException({ code: BYMAX_BAD_REQUEST, message: BODY_TOO_DEEP_MESSAGE });
|
|
120
|
+
}
|
|
121
|
+
next();
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
exports.BymaxBodyDepthMiddleware = __decorateClass([
|
|
125
|
+
common.Injectable(),
|
|
126
|
+
__decorateParam(0, common.Inject(BYMAX_CORE_OPTIONS))
|
|
127
|
+
], exports.BymaxBodyDepthMiddleware);
|
|
128
|
+
|
|
21
129
|
// src/route-defaults.ts
|
|
22
130
|
var DEFAULT_HEALTH_PATH = "health";
|
|
23
131
|
var DEFAULT_METRICS_PATH = "metrics";
|
|
@@ -64,6 +172,16 @@ function resolveTiming(raw) {
|
|
|
64
172
|
const threshold = raw?.slowRequestThresholdMs;
|
|
65
173
|
return threshold === void 0 ? { enabled } : { enabled, slowRequestThresholdMs: threshold };
|
|
66
174
|
}
|
|
175
|
+
var DEFAULT_MAX_BODY_DEPTH = 32;
|
|
176
|
+
function resolveBodyDepth(raw) {
|
|
177
|
+
const maxDepth = raw?.maxDepth ?? DEFAULT_MAX_BODY_DEPTH;
|
|
178
|
+
if (!Number.isSafeInteger(maxDepth) || maxDepth < 1) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`bodyDepth.maxDepth must be a positive safe integer, received ${String(maxDepth)}`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return { enabled: raw?.enabled ?? false, maxDepth };
|
|
184
|
+
}
|
|
67
185
|
function resolveHealth(raw) {
|
|
68
186
|
return {
|
|
69
187
|
enabled: raw?.enabled ?? true,
|
|
@@ -136,6 +254,7 @@ function normalizeCoreOptions(raw) {
|
|
|
136
254
|
return deepFreeze({
|
|
137
255
|
envelope: resolveEnvelope(raw?.envelope),
|
|
138
256
|
timing: resolveTiming(raw?.timing),
|
|
257
|
+
bodyDepth: resolveBodyDepth(raw?.bodyDepth),
|
|
139
258
|
health: resolveHealth(raw?.health),
|
|
140
259
|
metrics: resolveMetrics(raw?.metrics),
|
|
141
260
|
openapi: resolveOpenApi(raw?.openapi, raw?.environment),
|
|
@@ -148,26 +267,6 @@ function normalizeCoreOptions(raw) {
|
|
|
148
267
|
}
|
|
149
268
|
normalizeCoreOptions();
|
|
150
269
|
|
|
151
|
-
// src/core.tokens.ts
|
|
152
|
-
var BYMAX_CORE_OPTIONS = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:core-options");
|
|
153
|
-
var BYMAX_CORRELATION_PROVIDER = /* @__PURE__ */ Symbol.for(
|
|
154
|
-
"@bymax-one/nest-core:correlation-provider"
|
|
155
|
-
);
|
|
156
|
-
var BYMAX_TIMING_SINK = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:timing-sink");
|
|
157
|
-
var BYMAX_HEALTH_INDICATORS = /* @__PURE__ */ Symbol.for(
|
|
158
|
-
"@bymax-one/nest-core:health-indicators"
|
|
159
|
-
);
|
|
160
|
-
var BYMAX_HEALTH_TRANSITION_SINK = /* @__PURE__ */ Symbol.for(
|
|
161
|
-
"@bymax-one/nest-core:health-transition-sink"
|
|
162
|
-
);
|
|
163
|
-
var BYMAX_UNEXPECTED_ERROR_SINK = /* @__PURE__ */ Symbol.for(
|
|
164
|
-
"@bymax-one/nest-core:unexpected-error-sink"
|
|
165
|
-
);
|
|
166
|
-
var BYMAX_METRICS_REGISTRY = /* @__PURE__ */ Symbol.for(
|
|
167
|
-
"@bymax-one/nest-core:metrics-registry"
|
|
168
|
-
);
|
|
169
|
-
var BYMAX_TRACE_CONTEXT = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:trace-context");
|
|
170
|
-
|
|
171
270
|
// src/optional-peer.ts
|
|
172
271
|
function isMissingModuleError(cause) {
|
|
173
272
|
const code = cause.code;
|
|
@@ -275,52 +374,6 @@ function containRejection(returned, onFailure) {
|
|
|
275
374
|
}
|
|
276
375
|
}
|
|
277
376
|
|
|
278
|
-
// src/envelope/error-codes.ts
|
|
279
|
-
var BYMAX_BAD_REQUEST = "BYMAX_BAD_REQUEST";
|
|
280
|
-
var BYMAX_VALIDATION_FAILED = "BYMAX_VALIDATION_FAILED";
|
|
281
|
-
var BYMAX_UNAUTHORIZED = "BYMAX_UNAUTHORIZED";
|
|
282
|
-
var BYMAX_FORBIDDEN = "BYMAX_FORBIDDEN";
|
|
283
|
-
var BYMAX_NOT_FOUND = "BYMAX_NOT_FOUND";
|
|
284
|
-
var BYMAX_CONFLICT = "BYMAX_CONFLICT";
|
|
285
|
-
var BYMAX_PAYLOAD_TOO_LARGE = "BYMAX_PAYLOAD_TOO_LARGE";
|
|
286
|
-
var BYMAX_UNSUPPORTED_MEDIA_TYPE = "BYMAX_UNSUPPORTED_MEDIA_TYPE";
|
|
287
|
-
var BYMAX_UNPROCESSABLE_ENTITY = "BYMAX_UNPROCESSABLE_ENTITY";
|
|
288
|
-
var BYMAX_TOO_MANY_REQUESTS = "BYMAX_TOO_MANY_REQUESTS";
|
|
289
|
-
var BYMAX_CLIENT_ERROR = "BYMAX_CLIENT_ERROR";
|
|
290
|
-
var BYMAX_INTERNAL_ERROR = "BYMAX_INTERNAL_ERROR";
|
|
291
|
-
var BYMAX_NOT_IMPLEMENTED = "BYMAX_NOT_IMPLEMENTED";
|
|
292
|
-
var BYMAX_BAD_GATEWAY = "BYMAX_BAD_GATEWAY";
|
|
293
|
-
var BYMAX_SERVICE_UNAVAILABLE = "BYMAX_SERVICE_UNAVAILABLE";
|
|
294
|
-
var BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
|
|
295
|
-
var STATUS_CODES = /* @__PURE__ */ new Map([
|
|
296
|
-
[400, BYMAX_BAD_REQUEST],
|
|
297
|
-
[401, BYMAX_UNAUTHORIZED],
|
|
298
|
-
[403, BYMAX_FORBIDDEN],
|
|
299
|
-
[404, BYMAX_NOT_FOUND],
|
|
300
|
-
[409, BYMAX_CONFLICT],
|
|
301
|
-
[413, BYMAX_PAYLOAD_TOO_LARGE],
|
|
302
|
-
[415, BYMAX_UNSUPPORTED_MEDIA_TYPE],
|
|
303
|
-
[422, BYMAX_UNPROCESSABLE_ENTITY],
|
|
304
|
-
[429, BYMAX_TOO_MANY_REQUESTS],
|
|
305
|
-
[500, BYMAX_INTERNAL_ERROR],
|
|
306
|
-
[501, BYMAX_NOT_IMPLEMENTED],
|
|
307
|
-
[502, BYMAX_BAD_GATEWAY],
|
|
308
|
-
[503, BYMAX_SERVICE_UNAVAILABLE],
|
|
309
|
-
[504, BYMAX_GATEWAY_TIMEOUT]
|
|
310
|
-
]);
|
|
311
|
-
var CLIENT_ERROR_MIN = 400;
|
|
312
|
-
var CLIENT_ERROR_MAX = 500;
|
|
313
|
-
function codeForStatus(status) {
|
|
314
|
-
const catalogued = STATUS_CODES.get(status);
|
|
315
|
-
if (catalogued !== void 0) {
|
|
316
|
-
return catalogued;
|
|
317
|
-
}
|
|
318
|
-
if (status >= CLIENT_ERROR_MIN && status < CLIENT_ERROR_MAX) {
|
|
319
|
-
return BYMAX_CLIENT_ERROR;
|
|
320
|
-
}
|
|
321
|
-
return BYMAX_INTERNAL_ERROR;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
377
|
// src/envelope/error-envelope.ts
|
|
325
378
|
function buildErrorEnvelope(input) {
|
|
326
379
|
const base = {
|
|
@@ -1697,12 +1750,14 @@ exports.BymaxCoreModule = class BymaxCoreModule extends BymaxCoreModuleBase {
|
|
|
1697
1750
|
* @param consumer - Nest's middleware consumer.
|
|
1698
1751
|
*/
|
|
1699
1752
|
configure(consumer) {
|
|
1700
|
-
if (!this.resolved.timing.enabled) {
|
|
1701
|
-
return;
|
|
1702
|
-
}
|
|
1703
1753
|
const adapter = this.adapterHost?.httpAdapter;
|
|
1704
|
-
|
|
1705
|
-
|
|
1754
|
+
if (this.resolved.timing.enabled) {
|
|
1755
|
+
const onFastify = bridgeFastifyRouteMetadata(adapter);
|
|
1756
|
+
consumer.apply(exports.BymaxTimingMiddleware).forRoutes(onFastify ? "{*splat}" : "/");
|
|
1757
|
+
}
|
|
1758
|
+
if (this.resolved.bodyDepth.enabled && adapter?.getType?.() !== "fastify") {
|
|
1759
|
+
consumer.apply(exports.BymaxBodyDepthMiddleware).forRoutes("/");
|
|
1760
|
+
}
|
|
1706
1761
|
}
|
|
1707
1762
|
/**
|
|
1708
1763
|
* Register the module synchronously. Options are known now, so disabled
|
package/dist/index.d.cts
CHANGED
|
@@ -22,6 +22,25 @@ interface TimingOptions {
|
|
|
22
22
|
/** Samples above this threshold are flagged as slow. No default (unset). */
|
|
23
23
|
slowRequestThresholdMs?: number;
|
|
24
24
|
}
|
|
25
|
+
/** Request-body nesting limit, applied before the application's own validation runs. */
|
|
26
|
+
interface BodyDepthOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Refuse a parsed request body nested deeper than {@link BodyDepthOptions.maxDepth} with a
|
|
29
|
+
* `400` through the error envelope. Default: `false`.
|
|
30
|
+
*
|
|
31
|
+
* Off by default because turning it on rejects bodies that are accepted today. Enable it:
|
|
32
|
+
* a body of a few thousand nested arrays is only a few kilobytes, and a recursive validator
|
|
33
|
+
* or transformer overflows the stack on it, so the request is answered `500`, with no
|
|
34
|
+
* credential needed. The check runs before routing, on the body the parser already produced, so it covers the Express adapter and a body parsed by
|
|
35
|
+
* Nest's own parsers; it does not cover Fastify.
|
|
36
|
+
*/
|
|
37
|
+
enabled?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The deepest nesting a body may have: `[[1]]` is depth 2, a flat object or array is depth 1,
|
|
40
|
+
* and a scalar is depth 0. Must be a positive safe integer. Default: `32`.
|
|
41
|
+
*/
|
|
42
|
+
maxDepth?: number;
|
|
43
|
+
}
|
|
25
44
|
/** Liveness and readiness endpoint configuration. */
|
|
26
45
|
interface HealthOptions {
|
|
27
46
|
/** Register the health controller. Default: `true`. */
|
|
@@ -267,6 +286,8 @@ interface BymaxCoreModuleOptions {
|
|
|
267
286
|
envelope?: EnvelopeOptions;
|
|
268
287
|
/** Request-timing middleware. Default: enabled. */
|
|
269
288
|
timing?: TimingOptions;
|
|
289
|
+
/** Request-body nesting limit. Default: disabled. */
|
|
290
|
+
bodyDepth?: BodyDepthOptions;
|
|
270
291
|
/** Liveness and readiness endpoints. Default: enabled. */
|
|
271
292
|
health?: HealthOptions;
|
|
272
293
|
/** Prometheus metrics endpoint. Default: disabled. */
|
|
@@ -308,6 +329,11 @@ interface ResolvedTimingOptions {
|
|
|
308
329
|
enabled: boolean;
|
|
309
330
|
slowRequestThresholdMs?: number;
|
|
310
331
|
}
|
|
332
|
+
/** Fully-resolved body-depth options. */
|
|
333
|
+
interface ResolvedBodyDepthOptions {
|
|
334
|
+
enabled: boolean;
|
|
335
|
+
maxDepth: number;
|
|
336
|
+
}
|
|
311
337
|
/** Fully-resolved health options. */
|
|
312
338
|
interface ResolvedHealthOptions {
|
|
313
339
|
enabled: boolean;
|
|
@@ -365,6 +391,7 @@ interface ResolvedOpenApiOptions {
|
|
|
365
391
|
interface ResolvedCoreOptions {
|
|
366
392
|
envelope: ResolvedEnvelopeOptions;
|
|
367
393
|
timing: ResolvedTimingOptions;
|
|
394
|
+
bodyDepth: ResolvedBodyDepthOptions;
|
|
368
395
|
health: ResolvedHealthOptions;
|
|
369
396
|
metrics: ResolvedMetricsOptions;
|
|
370
397
|
openapi: ResolvedOpenApiOptions;
|
|
@@ -1115,6 +1142,21 @@ declare class TimingInterceptor implements NestInterceptor {
|
|
|
1115
1142
|
private recordSample;
|
|
1116
1143
|
}
|
|
1117
1144
|
|
|
1145
|
+
/** Refuses a body nested deeper than `bodyDepth.maxDepth` with a coded `400`. */
|
|
1146
|
+
declare class BymaxBodyDepthMiddleware implements NestMiddleware {
|
|
1147
|
+
private readonly options;
|
|
1148
|
+
constructor(options: ResolvedCoreOptions);
|
|
1149
|
+
/**
|
|
1150
|
+
* @param req - The request, whose `body` the parser has already filled.
|
|
1151
|
+
* @param _res - Unused.
|
|
1152
|
+
* @param next - Continues the chain.
|
|
1153
|
+
* @throws BadRequestException When the body is nested too deeply.
|
|
1154
|
+
*/
|
|
1155
|
+
use(req: {
|
|
1156
|
+
body?: unknown;
|
|
1157
|
+
}, _res: unknown, next: () => void): void;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1118
1160
|
/**
|
|
1119
1161
|
* @fileoverview Neutral request-info accessor for request timing. Reads the
|
|
1120
1162
|
* HTTP method and the route template off a request without assuming Express or
|
|
@@ -1296,5 +1338,5 @@ declare const BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
|
|
|
1296
1338
|
*/
|
|
1297
1339
|
declare function codeForStatus(status: number): string;
|
|
1298
1340
|
|
|
1299
|
-
export { BYMAX_BAD_GATEWAY, BYMAX_BAD_REQUEST, BYMAX_CLIENT_ERROR, BYMAX_CONFLICT, BYMAX_CORE_OPTIONS, BYMAX_CORRELATION_PROVIDER, BYMAX_FORBIDDEN, BYMAX_GATEWAY_TIMEOUT, BYMAX_HEALTH_INDICATORS, BYMAX_HEALTH_TRANSITION_SINK, BYMAX_INTERNAL_ERROR, BYMAX_METRICS_REGISTRY, BYMAX_NOT_FOUND, BYMAX_NOT_IMPLEMENTED, BYMAX_PAYLOAD_TOO_LARGE, BYMAX_SERVICE_UNAVAILABLE, BYMAX_TIMING_SINK, BYMAX_TOO_MANY_REQUESTS, BYMAX_TRACE_CONTEXT, BYMAX_UNAUTHORIZED, BYMAX_UNEXPECTED_ERROR_SINK, BYMAX_UNPROCESSABLE_ENTITY, BYMAX_UNSUPPORTED_MEDIA_TYPE, BYMAX_VALIDATION_FAILED, BymaxCoreModule, BymaxExceptionFilter, BymaxTimingMiddleware, TimingInterceptor, UNMATCHED_ROUTE, buildErrorEnvelope, codeForStatus };
|
|
1300
|
-
export type { BuildErrorEnvelopeInput, BymaxCoreModuleOptions, EnvelopeOptions, ErrorDetails, ErrorEnvelope, FilterErrorContext, HealthOptions, ICorrelationIdProvider, ITimingSink, ITraceContextProvider, IUnexpectedErrorSink, MetricsOptions, OpenApiHttpMethod, OpenApiOperationIdFactory, OpenApiOperationKey, OpenApiOptions, OpenApiSecurityRequirement, OpenApiSecurityScheme, OpenApiServerDescriptor, OperationSecurityMap, RequestTimingSample, ResolvedCoreOptions, TelemetryOptions, TimingOptions, TraceContext, UnexpectedErrorReport };
|
|
1341
|
+
export { BYMAX_BAD_GATEWAY, BYMAX_BAD_REQUEST, BYMAX_CLIENT_ERROR, BYMAX_CONFLICT, BYMAX_CORE_OPTIONS, BYMAX_CORRELATION_PROVIDER, BYMAX_FORBIDDEN, BYMAX_GATEWAY_TIMEOUT, BYMAX_HEALTH_INDICATORS, BYMAX_HEALTH_TRANSITION_SINK, BYMAX_INTERNAL_ERROR, BYMAX_METRICS_REGISTRY, BYMAX_NOT_FOUND, BYMAX_NOT_IMPLEMENTED, BYMAX_PAYLOAD_TOO_LARGE, BYMAX_SERVICE_UNAVAILABLE, BYMAX_TIMING_SINK, BYMAX_TOO_MANY_REQUESTS, BYMAX_TRACE_CONTEXT, BYMAX_UNAUTHORIZED, BYMAX_UNEXPECTED_ERROR_SINK, BYMAX_UNPROCESSABLE_ENTITY, BYMAX_UNSUPPORTED_MEDIA_TYPE, BYMAX_VALIDATION_FAILED, BymaxBodyDepthMiddleware, BymaxCoreModule, BymaxExceptionFilter, BymaxTimingMiddleware, TimingInterceptor, UNMATCHED_ROUTE, buildErrorEnvelope, codeForStatus };
|
|
1342
|
+
export type { BodyDepthOptions, BuildErrorEnvelopeInput, BymaxCoreModuleOptions, EnvelopeOptions, ErrorDetails, ErrorEnvelope, FilterErrorContext, HealthOptions, ICorrelationIdProvider, ITimingSink, ITraceContextProvider, IUnexpectedErrorSink, MetricsOptions, OpenApiHttpMethod, OpenApiOperationIdFactory, OpenApiOperationKey, OpenApiOptions, OpenApiSecurityRequirement, OpenApiSecurityScheme, OpenApiServerDescriptor, OperationSecurityMap, RequestTimingSample, ResolvedCoreOptions, TelemetryOptions, TimingOptions, TraceContext, UnexpectedErrorReport };
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,25 @@ interface TimingOptions {
|
|
|
22
22
|
/** Samples above this threshold are flagged as slow. No default (unset). */
|
|
23
23
|
slowRequestThresholdMs?: number;
|
|
24
24
|
}
|
|
25
|
+
/** Request-body nesting limit, applied before the application's own validation runs. */
|
|
26
|
+
interface BodyDepthOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Refuse a parsed request body nested deeper than {@link BodyDepthOptions.maxDepth} with a
|
|
29
|
+
* `400` through the error envelope. Default: `false`.
|
|
30
|
+
*
|
|
31
|
+
* Off by default because turning it on rejects bodies that are accepted today. Enable it:
|
|
32
|
+
* a body of a few thousand nested arrays is only a few kilobytes, and a recursive validator
|
|
33
|
+
* or transformer overflows the stack on it, so the request is answered `500`, with no
|
|
34
|
+
* credential needed. The check runs before routing, on the body the parser already produced, so it covers the Express adapter and a body parsed by
|
|
35
|
+
* Nest's own parsers; it does not cover Fastify.
|
|
36
|
+
*/
|
|
37
|
+
enabled?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The deepest nesting a body may have: `[[1]]` is depth 2, a flat object or array is depth 1,
|
|
40
|
+
* and a scalar is depth 0. Must be a positive safe integer. Default: `32`.
|
|
41
|
+
*/
|
|
42
|
+
maxDepth?: number;
|
|
43
|
+
}
|
|
25
44
|
/** Liveness and readiness endpoint configuration. */
|
|
26
45
|
interface HealthOptions {
|
|
27
46
|
/** Register the health controller. Default: `true`. */
|
|
@@ -267,6 +286,8 @@ interface BymaxCoreModuleOptions {
|
|
|
267
286
|
envelope?: EnvelopeOptions;
|
|
268
287
|
/** Request-timing middleware. Default: enabled. */
|
|
269
288
|
timing?: TimingOptions;
|
|
289
|
+
/** Request-body nesting limit. Default: disabled. */
|
|
290
|
+
bodyDepth?: BodyDepthOptions;
|
|
270
291
|
/** Liveness and readiness endpoints. Default: enabled. */
|
|
271
292
|
health?: HealthOptions;
|
|
272
293
|
/** Prometheus metrics endpoint. Default: disabled. */
|
|
@@ -308,6 +329,11 @@ interface ResolvedTimingOptions {
|
|
|
308
329
|
enabled: boolean;
|
|
309
330
|
slowRequestThresholdMs?: number;
|
|
310
331
|
}
|
|
332
|
+
/** Fully-resolved body-depth options. */
|
|
333
|
+
interface ResolvedBodyDepthOptions {
|
|
334
|
+
enabled: boolean;
|
|
335
|
+
maxDepth: number;
|
|
336
|
+
}
|
|
311
337
|
/** Fully-resolved health options. */
|
|
312
338
|
interface ResolvedHealthOptions {
|
|
313
339
|
enabled: boolean;
|
|
@@ -365,6 +391,7 @@ interface ResolvedOpenApiOptions {
|
|
|
365
391
|
interface ResolvedCoreOptions {
|
|
366
392
|
envelope: ResolvedEnvelopeOptions;
|
|
367
393
|
timing: ResolvedTimingOptions;
|
|
394
|
+
bodyDepth: ResolvedBodyDepthOptions;
|
|
368
395
|
health: ResolvedHealthOptions;
|
|
369
396
|
metrics: ResolvedMetricsOptions;
|
|
370
397
|
openapi: ResolvedOpenApiOptions;
|
|
@@ -1115,6 +1142,21 @@ declare class TimingInterceptor implements NestInterceptor {
|
|
|
1115
1142
|
private recordSample;
|
|
1116
1143
|
}
|
|
1117
1144
|
|
|
1145
|
+
/** Refuses a body nested deeper than `bodyDepth.maxDepth` with a coded `400`. */
|
|
1146
|
+
declare class BymaxBodyDepthMiddleware implements NestMiddleware {
|
|
1147
|
+
private readonly options;
|
|
1148
|
+
constructor(options: ResolvedCoreOptions);
|
|
1149
|
+
/**
|
|
1150
|
+
* @param req - The request, whose `body` the parser has already filled.
|
|
1151
|
+
* @param _res - Unused.
|
|
1152
|
+
* @param next - Continues the chain.
|
|
1153
|
+
* @throws BadRequestException When the body is nested too deeply.
|
|
1154
|
+
*/
|
|
1155
|
+
use(req: {
|
|
1156
|
+
body?: unknown;
|
|
1157
|
+
}, _res: unknown, next: () => void): void;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1118
1160
|
/**
|
|
1119
1161
|
* @fileoverview Neutral request-info accessor for request timing. Reads the
|
|
1120
1162
|
* HTTP method and the route template off a request without assuming Express or
|
|
@@ -1296,5 +1338,5 @@ declare const BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
|
|
|
1296
1338
|
*/
|
|
1297
1339
|
declare function codeForStatus(status: number): string;
|
|
1298
1340
|
|
|
1299
|
-
export { BYMAX_BAD_GATEWAY, BYMAX_BAD_REQUEST, BYMAX_CLIENT_ERROR, BYMAX_CONFLICT, BYMAX_CORE_OPTIONS, BYMAX_CORRELATION_PROVIDER, BYMAX_FORBIDDEN, BYMAX_GATEWAY_TIMEOUT, BYMAX_HEALTH_INDICATORS, BYMAX_HEALTH_TRANSITION_SINK, BYMAX_INTERNAL_ERROR, BYMAX_METRICS_REGISTRY, BYMAX_NOT_FOUND, BYMAX_NOT_IMPLEMENTED, BYMAX_PAYLOAD_TOO_LARGE, BYMAX_SERVICE_UNAVAILABLE, BYMAX_TIMING_SINK, BYMAX_TOO_MANY_REQUESTS, BYMAX_TRACE_CONTEXT, BYMAX_UNAUTHORIZED, BYMAX_UNEXPECTED_ERROR_SINK, BYMAX_UNPROCESSABLE_ENTITY, BYMAX_UNSUPPORTED_MEDIA_TYPE, BYMAX_VALIDATION_FAILED, BymaxCoreModule, BymaxExceptionFilter, BymaxTimingMiddleware, TimingInterceptor, UNMATCHED_ROUTE, buildErrorEnvelope, codeForStatus };
|
|
1300
|
-
export type { BuildErrorEnvelopeInput, BymaxCoreModuleOptions, EnvelopeOptions, ErrorDetails, ErrorEnvelope, FilterErrorContext, HealthOptions, ICorrelationIdProvider, ITimingSink, ITraceContextProvider, IUnexpectedErrorSink, MetricsOptions, OpenApiHttpMethod, OpenApiOperationIdFactory, OpenApiOperationKey, OpenApiOptions, OpenApiSecurityRequirement, OpenApiSecurityScheme, OpenApiServerDescriptor, OperationSecurityMap, RequestTimingSample, ResolvedCoreOptions, TelemetryOptions, TimingOptions, TraceContext, UnexpectedErrorReport };
|
|
1341
|
+
export { BYMAX_BAD_GATEWAY, BYMAX_BAD_REQUEST, BYMAX_CLIENT_ERROR, BYMAX_CONFLICT, BYMAX_CORE_OPTIONS, BYMAX_CORRELATION_PROVIDER, BYMAX_FORBIDDEN, BYMAX_GATEWAY_TIMEOUT, BYMAX_HEALTH_INDICATORS, BYMAX_HEALTH_TRANSITION_SINK, BYMAX_INTERNAL_ERROR, BYMAX_METRICS_REGISTRY, BYMAX_NOT_FOUND, BYMAX_NOT_IMPLEMENTED, BYMAX_PAYLOAD_TOO_LARGE, BYMAX_SERVICE_UNAVAILABLE, BYMAX_TIMING_SINK, BYMAX_TOO_MANY_REQUESTS, BYMAX_TRACE_CONTEXT, BYMAX_UNAUTHORIZED, BYMAX_UNEXPECTED_ERROR_SINK, BYMAX_UNPROCESSABLE_ENTITY, BYMAX_UNSUPPORTED_MEDIA_TYPE, BYMAX_VALIDATION_FAILED, BymaxBodyDepthMiddleware, BymaxCoreModule, BymaxExceptionFilter, BymaxTimingMiddleware, TimingInterceptor, UNMATCHED_ROUTE, buildErrorEnvelope, codeForStatus };
|
|
1342
|
+
export type { BodyDepthOptions, BuildErrorEnvelopeInput, BymaxCoreModuleOptions, EnvelopeOptions, ErrorDetails, ErrorEnvelope, FilterErrorContext, HealthOptions, ICorrelationIdProvider, ITimingSink, ITraceContextProvider, IUnexpectedErrorSink, MetricsOptions, OpenApiHttpMethod, OpenApiOperationIdFactory, OpenApiOperationKey, OpenApiOptions, OpenApiSecurityRequirement, OpenApiSecurityScheme, OpenApiServerDescriptor, OperationSecurityMap, RequestTimingSample, ResolvedCoreOptions, TelemetryOptions, TimingOptions, TraceContext, UnexpectedErrorReport };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Injectable, Inject, Catch, Optional, ConfigurableModuleBuilder, Module, BadRequestException, HttpException, Logger, Get, Res, Controller, Req, HttpStatus, UnauthorizedException, NotFoundException } from '@nestjs/common';
|
|
2
2
|
import { HttpAdapterHost, DiscoveryService, Reflector, BaseExceptionFilter, APP_FILTER, DiscoveryModule } from '@nestjs/core';
|
|
3
3
|
import { createHash, timingSafeEqual } from 'crypto';
|
|
4
4
|
import { AsyncResource } from 'async_hooks';
|
|
@@ -16,6 +16,114 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
16
16
|
};
|
|
17
17
|
var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
|
|
18
18
|
|
|
19
|
+
// src/core.tokens.ts
|
|
20
|
+
var BYMAX_CORE_OPTIONS = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:core-options");
|
|
21
|
+
var BYMAX_CORRELATION_PROVIDER = /* @__PURE__ */ Symbol.for(
|
|
22
|
+
"@bymax-one/nest-core:correlation-provider"
|
|
23
|
+
);
|
|
24
|
+
var BYMAX_TIMING_SINK = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:timing-sink");
|
|
25
|
+
var BYMAX_HEALTH_INDICATORS = /* @__PURE__ */ Symbol.for(
|
|
26
|
+
"@bymax-one/nest-core:health-indicators"
|
|
27
|
+
);
|
|
28
|
+
var BYMAX_HEALTH_TRANSITION_SINK = /* @__PURE__ */ Symbol.for(
|
|
29
|
+
"@bymax-one/nest-core:health-transition-sink"
|
|
30
|
+
);
|
|
31
|
+
var BYMAX_UNEXPECTED_ERROR_SINK = /* @__PURE__ */ Symbol.for(
|
|
32
|
+
"@bymax-one/nest-core:unexpected-error-sink"
|
|
33
|
+
);
|
|
34
|
+
var BYMAX_METRICS_REGISTRY = /* @__PURE__ */ Symbol.for(
|
|
35
|
+
"@bymax-one/nest-core:metrics-registry"
|
|
36
|
+
);
|
|
37
|
+
var BYMAX_TRACE_CONTEXT = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:trace-context");
|
|
38
|
+
|
|
39
|
+
// src/envelope/error-codes.ts
|
|
40
|
+
var BYMAX_BAD_REQUEST = "BYMAX_BAD_REQUEST";
|
|
41
|
+
var BYMAX_VALIDATION_FAILED = "BYMAX_VALIDATION_FAILED";
|
|
42
|
+
var BYMAX_UNAUTHORIZED = "BYMAX_UNAUTHORIZED";
|
|
43
|
+
var BYMAX_FORBIDDEN = "BYMAX_FORBIDDEN";
|
|
44
|
+
var BYMAX_NOT_FOUND = "BYMAX_NOT_FOUND";
|
|
45
|
+
var BYMAX_CONFLICT = "BYMAX_CONFLICT";
|
|
46
|
+
var BYMAX_PAYLOAD_TOO_LARGE = "BYMAX_PAYLOAD_TOO_LARGE";
|
|
47
|
+
var BYMAX_UNSUPPORTED_MEDIA_TYPE = "BYMAX_UNSUPPORTED_MEDIA_TYPE";
|
|
48
|
+
var BYMAX_UNPROCESSABLE_ENTITY = "BYMAX_UNPROCESSABLE_ENTITY";
|
|
49
|
+
var BYMAX_TOO_MANY_REQUESTS = "BYMAX_TOO_MANY_REQUESTS";
|
|
50
|
+
var BYMAX_CLIENT_ERROR = "BYMAX_CLIENT_ERROR";
|
|
51
|
+
var BYMAX_INTERNAL_ERROR = "BYMAX_INTERNAL_ERROR";
|
|
52
|
+
var BYMAX_NOT_IMPLEMENTED = "BYMAX_NOT_IMPLEMENTED";
|
|
53
|
+
var BYMAX_BAD_GATEWAY = "BYMAX_BAD_GATEWAY";
|
|
54
|
+
var BYMAX_SERVICE_UNAVAILABLE = "BYMAX_SERVICE_UNAVAILABLE";
|
|
55
|
+
var BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
|
|
56
|
+
var STATUS_CODES = /* @__PURE__ */ new Map([
|
|
57
|
+
[400, BYMAX_BAD_REQUEST],
|
|
58
|
+
[401, BYMAX_UNAUTHORIZED],
|
|
59
|
+
[403, BYMAX_FORBIDDEN],
|
|
60
|
+
[404, BYMAX_NOT_FOUND],
|
|
61
|
+
[409, BYMAX_CONFLICT],
|
|
62
|
+
[413, BYMAX_PAYLOAD_TOO_LARGE],
|
|
63
|
+
[415, BYMAX_UNSUPPORTED_MEDIA_TYPE],
|
|
64
|
+
[422, BYMAX_UNPROCESSABLE_ENTITY],
|
|
65
|
+
[429, BYMAX_TOO_MANY_REQUESTS],
|
|
66
|
+
[500, BYMAX_INTERNAL_ERROR],
|
|
67
|
+
[501, BYMAX_NOT_IMPLEMENTED],
|
|
68
|
+
[502, BYMAX_BAD_GATEWAY],
|
|
69
|
+
[503, BYMAX_SERVICE_UNAVAILABLE],
|
|
70
|
+
[504, BYMAX_GATEWAY_TIMEOUT]
|
|
71
|
+
]);
|
|
72
|
+
var CLIENT_ERROR_MIN = 400;
|
|
73
|
+
var CLIENT_ERROR_MAX = 500;
|
|
74
|
+
function codeForStatus(status) {
|
|
75
|
+
const catalogued = STATUS_CODES.get(status);
|
|
76
|
+
if (catalogued !== void 0) {
|
|
77
|
+
return catalogued;
|
|
78
|
+
}
|
|
79
|
+
if (status >= CLIENT_ERROR_MIN && status < CLIENT_ERROR_MAX) {
|
|
80
|
+
return BYMAX_CLIENT_ERROR;
|
|
81
|
+
}
|
|
82
|
+
return BYMAX_INTERNAL_ERROR;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/body-depth/body-depth.middleware.ts
|
|
86
|
+
var BODY_TOO_DEEP_MESSAGE = "Request body is nested too deeply";
|
|
87
|
+
function exceedsDepth(value, maxDepth) {
|
|
88
|
+
let level = [value];
|
|
89
|
+
for (let depth = 0; level.length > 0; depth += 1) {
|
|
90
|
+
const next = [];
|
|
91
|
+
for (const node of level) {
|
|
92
|
+
if (typeof node === "object" && node !== null) {
|
|
93
|
+
if (depth >= maxDepth) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
for (const child of Object.values(node)) {
|
|
97
|
+
next.push(child);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
level = next;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
var BymaxBodyDepthMiddleware = class {
|
|
106
|
+
constructor(options) {
|
|
107
|
+
this.options = options;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* @param req - The request, whose `body` the parser has already filled.
|
|
111
|
+
* @param _res - Unused.
|
|
112
|
+
* @param next - Continues the chain.
|
|
113
|
+
* @throws BadRequestException When the body is nested too deeply.
|
|
114
|
+
*/
|
|
115
|
+
use(req, _res, next) {
|
|
116
|
+
if (exceedsDepth(req.body, this.options.bodyDepth.maxDepth)) {
|
|
117
|
+
throw new BadRequestException({ code: BYMAX_BAD_REQUEST, message: BODY_TOO_DEEP_MESSAGE });
|
|
118
|
+
}
|
|
119
|
+
next();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
BymaxBodyDepthMiddleware = __decorateClass([
|
|
123
|
+
Injectable(),
|
|
124
|
+
__decorateParam(0, Inject(BYMAX_CORE_OPTIONS))
|
|
125
|
+
], BymaxBodyDepthMiddleware);
|
|
126
|
+
|
|
19
127
|
// src/route-defaults.ts
|
|
20
128
|
var DEFAULT_HEALTH_PATH = "health";
|
|
21
129
|
var DEFAULT_METRICS_PATH = "metrics";
|
|
@@ -62,6 +170,16 @@ function resolveTiming(raw) {
|
|
|
62
170
|
const threshold = raw?.slowRequestThresholdMs;
|
|
63
171
|
return threshold === void 0 ? { enabled } : { enabled, slowRequestThresholdMs: threshold };
|
|
64
172
|
}
|
|
173
|
+
var DEFAULT_MAX_BODY_DEPTH = 32;
|
|
174
|
+
function resolveBodyDepth(raw) {
|
|
175
|
+
const maxDepth = raw?.maxDepth ?? DEFAULT_MAX_BODY_DEPTH;
|
|
176
|
+
if (!Number.isSafeInteger(maxDepth) || maxDepth < 1) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`bodyDepth.maxDepth must be a positive safe integer, received ${String(maxDepth)}`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return { enabled: raw?.enabled ?? false, maxDepth };
|
|
182
|
+
}
|
|
65
183
|
function resolveHealth(raw) {
|
|
66
184
|
return {
|
|
67
185
|
enabled: raw?.enabled ?? true,
|
|
@@ -134,6 +252,7 @@ function normalizeCoreOptions(raw) {
|
|
|
134
252
|
return deepFreeze({
|
|
135
253
|
envelope: resolveEnvelope(raw?.envelope),
|
|
136
254
|
timing: resolveTiming(raw?.timing),
|
|
255
|
+
bodyDepth: resolveBodyDepth(raw?.bodyDepth),
|
|
137
256
|
health: resolveHealth(raw?.health),
|
|
138
257
|
metrics: resolveMetrics(raw?.metrics),
|
|
139
258
|
openapi: resolveOpenApi(raw?.openapi, raw?.environment),
|
|
@@ -146,26 +265,6 @@ function normalizeCoreOptions(raw) {
|
|
|
146
265
|
}
|
|
147
266
|
normalizeCoreOptions();
|
|
148
267
|
|
|
149
|
-
// src/core.tokens.ts
|
|
150
|
-
var BYMAX_CORE_OPTIONS = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:core-options");
|
|
151
|
-
var BYMAX_CORRELATION_PROVIDER = /* @__PURE__ */ Symbol.for(
|
|
152
|
-
"@bymax-one/nest-core:correlation-provider"
|
|
153
|
-
);
|
|
154
|
-
var BYMAX_TIMING_SINK = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:timing-sink");
|
|
155
|
-
var BYMAX_HEALTH_INDICATORS = /* @__PURE__ */ Symbol.for(
|
|
156
|
-
"@bymax-one/nest-core:health-indicators"
|
|
157
|
-
);
|
|
158
|
-
var BYMAX_HEALTH_TRANSITION_SINK = /* @__PURE__ */ Symbol.for(
|
|
159
|
-
"@bymax-one/nest-core:health-transition-sink"
|
|
160
|
-
);
|
|
161
|
-
var BYMAX_UNEXPECTED_ERROR_SINK = /* @__PURE__ */ Symbol.for(
|
|
162
|
-
"@bymax-one/nest-core:unexpected-error-sink"
|
|
163
|
-
);
|
|
164
|
-
var BYMAX_METRICS_REGISTRY = /* @__PURE__ */ Symbol.for(
|
|
165
|
-
"@bymax-one/nest-core:metrics-registry"
|
|
166
|
-
);
|
|
167
|
-
var BYMAX_TRACE_CONTEXT = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:trace-context");
|
|
168
|
-
|
|
169
268
|
// src/optional-peer.ts
|
|
170
269
|
function isMissingModuleError(cause) {
|
|
171
270
|
const code = cause.code;
|
|
@@ -273,52 +372,6 @@ function containRejection(returned, onFailure) {
|
|
|
273
372
|
}
|
|
274
373
|
}
|
|
275
374
|
|
|
276
|
-
// src/envelope/error-codes.ts
|
|
277
|
-
var BYMAX_BAD_REQUEST = "BYMAX_BAD_REQUEST";
|
|
278
|
-
var BYMAX_VALIDATION_FAILED = "BYMAX_VALIDATION_FAILED";
|
|
279
|
-
var BYMAX_UNAUTHORIZED = "BYMAX_UNAUTHORIZED";
|
|
280
|
-
var BYMAX_FORBIDDEN = "BYMAX_FORBIDDEN";
|
|
281
|
-
var BYMAX_NOT_FOUND = "BYMAX_NOT_FOUND";
|
|
282
|
-
var BYMAX_CONFLICT = "BYMAX_CONFLICT";
|
|
283
|
-
var BYMAX_PAYLOAD_TOO_LARGE = "BYMAX_PAYLOAD_TOO_LARGE";
|
|
284
|
-
var BYMAX_UNSUPPORTED_MEDIA_TYPE = "BYMAX_UNSUPPORTED_MEDIA_TYPE";
|
|
285
|
-
var BYMAX_UNPROCESSABLE_ENTITY = "BYMAX_UNPROCESSABLE_ENTITY";
|
|
286
|
-
var BYMAX_TOO_MANY_REQUESTS = "BYMAX_TOO_MANY_REQUESTS";
|
|
287
|
-
var BYMAX_CLIENT_ERROR = "BYMAX_CLIENT_ERROR";
|
|
288
|
-
var BYMAX_INTERNAL_ERROR = "BYMAX_INTERNAL_ERROR";
|
|
289
|
-
var BYMAX_NOT_IMPLEMENTED = "BYMAX_NOT_IMPLEMENTED";
|
|
290
|
-
var BYMAX_BAD_GATEWAY = "BYMAX_BAD_GATEWAY";
|
|
291
|
-
var BYMAX_SERVICE_UNAVAILABLE = "BYMAX_SERVICE_UNAVAILABLE";
|
|
292
|
-
var BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
|
|
293
|
-
var STATUS_CODES = /* @__PURE__ */ new Map([
|
|
294
|
-
[400, BYMAX_BAD_REQUEST],
|
|
295
|
-
[401, BYMAX_UNAUTHORIZED],
|
|
296
|
-
[403, BYMAX_FORBIDDEN],
|
|
297
|
-
[404, BYMAX_NOT_FOUND],
|
|
298
|
-
[409, BYMAX_CONFLICT],
|
|
299
|
-
[413, BYMAX_PAYLOAD_TOO_LARGE],
|
|
300
|
-
[415, BYMAX_UNSUPPORTED_MEDIA_TYPE],
|
|
301
|
-
[422, BYMAX_UNPROCESSABLE_ENTITY],
|
|
302
|
-
[429, BYMAX_TOO_MANY_REQUESTS],
|
|
303
|
-
[500, BYMAX_INTERNAL_ERROR],
|
|
304
|
-
[501, BYMAX_NOT_IMPLEMENTED],
|
|
305
|
-
[502, BYMAX_BAD_GATEWAY],
|
|
306
|
-
[503, BYMAX_SERVICE_UNAVAILABLE],
|
|
307
|
-
[504, BYMAX_GATEWAY_TIMEOUT]
|
|
308
|
-
]);
|
|
309
|
-
var CLIENT_ERROR_MIN = 400;
|
|
310
|
-
var CLIENT_ERROR_MAX = 500;
|
|
311
|
-
function codeForStatus(status) {
|
|
312
|
-
const catalogued = STATUS_CODES.get(status);
|
|
313
|
-
if (catalogued !== void 0) {
|
|
314
|
-
return catalogued;
|
|
315
|
-
}
|
|
316
|
-
if (status >= CLIENT_ERROR_MIN && status < CLIENT_ERROR_MAX) {
|
|
317
|
-
return BYMAX_CLIENT_ERROR;
|
|
318
|
-
}
|
|
319
|
-
return BYMAX_INTERNAL_ERROR;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
375
|
// src/envelope/error-envelope.ts
|
|
323
376
|
function buildErrorEnvelope(input) {
|
|
324
377
|
const base = {
|
|
@@ -1695,12 +1748,14 @@ var BymaxCoreModule = class extends BymaxCoreModuleBase {
|
|
|
1695
1748
|
* @param consumer - Nest's middleware consumer.
|
|
1696
1749
|
*/
|
|
1697
1750
|
configure(consumer) {
|
|
1698
|
-
if (!this.resolved.timing.enabled) {
|
|
1699
|
-
return;
|
|
1700
|
-
}
|
|
1701
1751
|
const adapter = this.adapterHost?.httpAdapter;
|
|
1702
|
-
|
|
1703
|
-
|
|
1752
|
+
if (this.resolved.timing.enabled) {
|
|
1753
|
+
const onFastify = bridgeFastifyRouteMetadata(adapter);
|
|
1754
|
+
consumer.apply(BymaxTimingMiddleware).forRoutes(onFastify ? "{*splat}" : "/");
|
|
1755
|
+
}
|
|
1756
|
+
if (this.resolved.bodyDepth.enabled && adapter?.getType?.() !== "fastify") {
|
|
1757
|
+
consumer.apply(BymaxBodyDepthMiddleware).forRoutes("/");
|
|
1758
|
+
}
|
|
1704
1759
|
}
|
|
1705
1760
|
/**
|
|
1706
1761
|
* Register the module synchronously. Options are known now, so disabled
|
|
@@ -1892,4 +1947,4 @@ TimingInterceptor = __decorateClass([
|
|
|
1892
1947
|
__decorateParam(3, Inject(BYMAX_TRACE_CONTEXT))
|
|
1893
1948
|
], TimingInterceptor);
|
|
1894
1949
|
|
|
1895
|
-
export { BYMAX_BAD_GATEWAY, BYMAX_BAD_REQUEST, BYMAX_CLIENT_ERROR, BYMAX_CONFLICT, BYMAX_CORE_OPTIONS, BYMAX_CORRELATION_PROVIDER, BYMAX_FORBIDDEN, BYMAX_GATEWAY_TIMEOUT, BYMAX_HEALTH_INDICATORS, BYMAX_HEALTH_TRANSITION_SINK, BYMAX_INTERNAL_ERROR, BYMAX_METRICS_REGISTRY, BYMAX_NOT_FOUND, BYMAX_NOT_IMPLEMENTED, BYMAX_PAYLOAD_TOO_LARGE, BYMAX_SERVICE_UNAVAILABLE, BYMAX_TIMING_SINK, BYMAX_TOO_MANY_REQUESTS, BYMAX_TRACE_CONTEXT, BYMAX_UNAUTHORIZED, BYMAX_UNEXPECTED_ERROR_SINK, BYMAX_UNPROCESSABLE_ENTITY, BYMAX_UNSUPPORTED_MEDIA_TYPE, BYMAX_VALIDATION_FAILED, BymaxCoreModule, BymaxExceptionFilter, BymaxTimingMiddleware, TimingInterceptor, UNMATCHED_ROUTE, buildErrorEnvelope, codeForStatus };
|
|
1950
|
+
export { BYMAX_BAD_GATEWAY, BYMAX_BAD_REQUEST, BYMAX_CLIENT_ERROR, BYMAX_CONFLICT, BYMAX_CORE_OPTIONS, BYMAX_CORRELATION_PROVIDER, BYMAX_FORBIDDEN, BYMAX_GATEWAY_TIMEOUT, BYMAX_HEALTH_INDICATORS, BYMAX_HEALTH_TRANSITION_SINK, BYMAX_INTERNAL_ERROR, BYMAX_METRICS_REGISTRY, BYMAX_NOT_FOUND, BYMAX_NOT_IMPLEMENTED, BYMAX_PAYLOAD_TOO_LARGE, BYMAX_SERVICE_UNAVAILABLE, BYMAX_TIMING_SINK, BYMAX_TOO_MANY_REQUESTS, BYMAX_TRACE_CONTEXT, BYMAX_UNAUTHORIZED, BYMAX_UNEXPECTED_ERROR_SINK, BYMAX_UNPROCESSABLE_ENTITY, BYMAX_UNSUPPORTED_MEDIA_TYPE, BYMAX_VALIDATION_FAILED, BymaxBodyDepthMiddleware, BymaxCoreModule, BymaxExceptionFilter, BymaxTimingMiddleware, TimingInterceptor, UNMATCHED_ROUTE, buildErrorEnvelope, codeForStatus };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bymax-one/nest-core",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.7",
|
|
4
4
|
"description": "Zero-dependency NestJS 11 application foundation kit: error-envelope exception filter, request-timing middleware, pagination helpers, health endpoints with indicator discovery, an optional Prometheus metrics endpoint with a contribution contract, OpenAPI documents in development, and OpenTelemetry trace correlation.",
|
|
5
5
|
"author": "Bymax One <support@bymax.one>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -63,6 +63,16 @@
|
|
|
63
63
|
"default": "./dist/metrics/index.cjs"
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
|
+
"./http": {
|
|
67
|
+
"import": {
|
|
68
|
+
"types": "./dist/http/index.d.ts",
|
|
69
|
+
"default": "./dist/http/index.mjs"
|
|
70
|
+
},
|
|
71
|
+
"require": {
|
|
72
|
+
"types": "./dist/http/index.d.cts",
|
|
73
|
+
"default": "./dist/http/index.cjs"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
66
76
|
"./openapi": {
|
|
67
77
|
"import": {
|
|
68
78
|
"types": "./dist/openapi/index.d.ts",
|
|
@@ -77,6 +87,7 @@
|
|
|
77
87
|
},
|
|
78
88
|
"scripts": {
|
|
79
89
|
"build": "pnpm clean && tsup",
|
|
90
|
+
"check:file-length": "node scripts/check-file-length.mjs",
|
|
80
91
|
"check:exports": "attw --pack . --profile strict",
|
|
81
92
|
"check:mutants": "node scripts/check-mutation-directives.mjs",
|
|
82
93
|
"check:published": "node scripts/check-published-surface.mjs",
|
|
@@ -94,8 +105,8 @@
|
|
|
94
105
|
"size": "node scripts/check-size.mjs",
|
|
95
106
|
"test": "jest",
|
|
96
107
|
"test:cov": "jest --coverage",
|
|
97
|
-
"test:cov:all": "jest --config jest.coverage.config.ts --coverage",
|
|
98
|
-
"test:e2e": "jest --config jest.e2e.config.ts",
|
|
108
|
+
"test:cov:all": "node --experimental-vm-modules --disable-warning=ExperimentalWarning node_modules/jest/bin/jest.js --config jest.coverage.config.ts --coverage",
|
|
109
|
+
"test:e2e": "node --experimental-vm-modules --disable-warning=ExperimentalWarning node_modules/jest/bin/jest.js --config jest.e2e.config.ts",
|
|
99
110
|
"test:watch": "jest --watch",
|
|
100
111
|
"typecheck": "tsc --noEmit"
|
|
101
112
|
},
|
|
@@ -113,6 +124,7 @@
|
|
|
113
124
|
"@nestjs/core": "^11.1.18",
|
|
114
125
|
"@nestjs/swagger": "^11.0.0",
|
|
115
126
|
"@opentelemetry/api": "^1.9.0",
|
|
127
|
+
"helmet": "^8.0.0",
|
|
116
128
|
"prom-client": "^15.0.0",
|
|
117
129
|
"reflect-metadata": "^0.2.0",
|
|
118
130
|
"rxjs": "^7.0.0"
|
|
@@ -124,6 +136,9 @@
|
|
|
124
136
|
"@opentelemetry/api": {
|
|
125
137
|
"optional": true
|
|
126
138
|
},
|
|
139
|
+
"helmet": {
|
|
140
|
+
"optional": true
|
|
141
|
+
},
|
|
127
142
|
"prom-client": {
|
|
128
143
|
"optional": true
|
|
129
144
|
}
|
|
@@ -150,6 +165,8 @@
|
|
|
150
165
|
"@types/supertest": "^7.2.0",
|
|
151
166
|
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
|
152
167
|
"@typescript-eslint/parser": "^8.59.3",
|
|
168
|
+
"class-transformer": "^0.5.1",
|
|
169
|
+
"class-validator": "^0.15.1",
|
|
153
170
|
"eslint": "^9.39.4",
|
|
154
171
|
"eslint-config-prettier": "^10.1.8",
|
|
155
172
|
"eslint-import-resolver-typescript": "^4.4.4",
|
|
@@ -158,6 +175,7 @@
|
|
|
158
175
|
"eslint-plugin-security": "^4.0.0",
|
|
159
176
|
"fastify": "^5.12.5",
|
|
160
177
|
"globals": "^17.6.0",
|
|
178
|
+
"helmet": "^8.3.0",
|
|
161
179
|
"husky": "^9.1.7",
|
|
162
180
|
"jest": "^30.4.2",
|
|
163
181
|
"lint-staged": "^17.2.0",
|
|
@@ -182,6 +200,7 @@
|
|
|
182
200
|
"metrics",
|
|
183
201
|
"openapi",
|
|
184
202
|
"swagger",
|
|
203
|
+
"helmet",
|
|
185
204
|
"opentelemetry",
|
|
186
205
|
"tracing",
|
|
187
206
|
"typescript"
|
|
@@ -209,6 +228,9 @@
|
|
|
209
228
|
],
|
|
210
229
|
"metrics": [
|
|
211
230
|
"./dist/metrics/index.d.cts"
|
|
231
|
+
],
|
|
232
|
+
"http": [
|
|
233
|
+
"./dist/http/index.d.cts"
|
|
212
234
|
]
|
|
213
235
|
}
|
|
214
236
|
}
|