@bymax-one/nest-core 1.6.5 → 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 +53 -1
- package/README.md +113 -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 +137 -72
- package/dist/index.d.cts +44 -2
- package/dist/index.d.ts +44 -2
- package/dist/index.mjs +139 -74
- package/package.json +25 -3
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,56 @@ 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
|
+
|
|
43
|
+
## [1.6.6] - 2026-11-07
|
|
44
|
+
|
|
45
|
+
A patch release. The error envelope no longer returns the parser's own text for a request refused before
|
|
46
|
+
routing; see Fixed.
|
|
47
|
+
|
|
48
|
+
**Apply to a derived backend:** `pnpm up @bymax-one/nest-core`. A client that matched on those
|
|
49
|
+
message strings must match `code` instead. A middleware that throws a plain `BadRequestException`
|
|
50
|
+
before routing now answers `Malformed request` with the same 400; to keep its message, throw it with
|
|
51
|
+
a `code` (`new HttpException({ code: 'TENANT_REQUIRED', message }, 400)`).
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- **A request refused before routing no longer has the parser's own text returned.** The message of
|
|
56
|
+
a body-parser, router or decoding refusal quotes the request: V8's JSON excerpt (on a login body
|
|
57
|
+
with a missing value it ends in the password's last characters), a charset or content-encoding
|
|
58
|
+
header value, or the path. The status is unchanged and the envelope keeps its shape, but the
|
|
59
|
+
message is now fixed per status. It is chosen from the error's own `type` (the body pipeline's
|
|
60
|
+
refusals carry one) or, for the `SyntaxError` and `URIError` that Nest rewrites into a plain
|
|
61
|
+
`BadRequestException`, from a 400 raised while the request matched no route. Not covered: Fastify,
|
|
62
|
+
whose body errors take another path and were not measured here.
|
|
63
|
+
|
|
14
64
|
## [1.6.5] - 2026-10-31
|
|
15
65
|
|
|
16
66
|
A documentation release. The compiled JavaScript is byte-identical to `1.6.4`: what changes in the
|
|
@@ -1314,4 +1364,6 @@ have regressed from. They are kept because the reasoning is worth having.
|
|
|
1314
1364
|
[1.6.3]: https://github.com/bymaxone/nest-core/compare/v1.6.2...v1.6.3
|
|
1315
1365
|
[1.6.4]: https://github.com/bymaxone/nest-core/compare/v1.6.3...v1.6.4
|
|
1316
1366
|
[1.6.5]: https://github.com/bymaxone/nest-core/compare/v1.6.4...v1.6.5
|
|
1317
|
-
[
|
|
1367
|
+
[1.6.6]: https://github.com/bymaxone/nest-core/compare/v1.6.5...v1.6.6
|
|
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 |
|
|
@@ -639,6 +663,12 @@ marks itself `expose: true` with a `4xx` status, which is the `http-errors`
|
|
|
639
663
|
convention Node's body pipeline follows. Nothing in the application failed, so
|
|
640
664
|
it is not routed through the unexpected-error seam either.
|
|
641
665
|
|
|
666
|
+
The message of such a refusal is fixed per status — `Malformed request` (400),
|
|
667
|
+
`Request entity too large` (413), `Unsupported media type` (415) — because the
|
|
668
|
+
parser's own text quotes the request: a JSON excerpt, a header value, the path.
|
|
669
|
+
A `BadRequestException` a middleware throws before routing gets the same 400
|
|
670
|
+
message; throw it with a `code` to keep its own. Fastify is not covered.
|
|
671
|
+
|
|
642
672
|
**An error that carries no such marking is a 500, including when a client
|
|
643
673
|
caused it.** The clearest case is depth: a body of a few kilobytes nested
|
|
644
674
|
thousands of levels deep overflows the stack during validation and surfaces as
|
|
@@ -1675,6 +1705,55 @@ same `@Global()`-module pattern is how every pluggable token in this package
|
|
|
1675
1705
|
is overridden: `BYMAX_TIMING_SINK` and `BYMAX_HEALTH_INDICATORS` follow it
|
|
1676
1706
|
identically.
|
|
1677
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
|
+
|
|
1678
1757
|
## 🏗️ Architecture
|
|
1679
1758
|
|
|
1680
1759
|
```
|
|
@@ -1830,6 +1909,8 @@ the deployment nobody configured is the one most likely to be exposed.
|
|
|
1830
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 |
|
|
1831
1910
|
| Slow indicators | Converted to `down` by the aggregator, so a probe cannot hang on one |
|
|
1832
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 |
|
|
1833
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 |
|
|
1834
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 |
|
|
1835
1916
|
| Pagination cursors | Opaque, not authenticated; treated as client-supplied input on the way back in |
|
|
@@ -1903,27 +1984,28 @@ in the sections above.
|
|
|
1903
1984
|
|
|
1904
1985
|
### `.` (root)
|
|
1905
1986
|
|
|
1906
|
-
| Export
|
|
1907
|
-
|
|
|
1908
|
-
| `BymaxCoreModule`
|
|
1909
|
-
| `BymaxCoreModuleOptions`, `EnvelopeOptions`, `TimingOptions`, `HealthOptions`, `MetricsOptions`, `TelemetryOptions`, `OpenApiOptions`, `OpenApiServerDescriptor`, `OpenApiSecurityScheme`, `ResolvedCoreOptions`
|
|
1910
|
-
| `OpenApiSecurityRequirement`, `OpenApiHttpMethod`, `OpenApiOperationKey`, `OperationSecurityMap`
|
|
1911
|
-
| `OpenApiOperationIdFactory`
|
|
1912
|
-
| `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`
|
|
1913
|
-
| `IHealthTransitionSink`, `HealthTransition`, `HealthTransitionCause`
|
|
1914
|
-
| `ICorrelationIdProvider`
|
|
1915
|
-
| `ITraceContextProvider`, `TraceContext`
|
|
1916
|
-
| `BymaxExceptionFilter`
|
|
1917
|
-
| `FilterErrorContext`
|
|
1918
|
-
| `IUnexpectedErrorSink`, `UnexpectedErrorReport`
|
|
1919
|
-
| `buildErrorEnvelope`
|
|
1920
|
-
| `ErrorEnvelope`, `ErrorDetails`, `BuildErrorEnvelopeInput`
|
|
1921
|
-
| `BymaxTimingMiddleware`
|
|
1922
|
-
| `
|
|
1923
|
-
| `
|
|
1924
|
-
| `
|
|
1925
|
-
| `
|
|
1926
|
-
| `
|
|
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. |
|
|
1927
2009
|
|
|
1928
2010
|
### `./pagination`
|
|
1929
2011
|
|
|
@@ -1957,6 +2039,13 @@ in the sections above.
|
|
|
1957
2039
|
| `IMetricsContributor` | type | The contract: `registerMetrics(registry)`. |
|
|
1958
2040
|
| `MetricsRegistry` | type | The `prom-client` registry the scrape endpoint serves. |
|
|
1959
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
|
+
|
|
1960
2049
|
### `./openapi`
|
|
1961
2050
|
|
|
1962
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 };
|