@bymax-one/nest-core 1.6.6 → 1.6.8
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 +114 -1
- package/README.md +554 -898
- package/dist/health/index.d.cts +6 -3
- package/dist/health/index.d.ts +6 -3
- package/dist/http/index.cjs +73 -0
- package/dist/http/index.d.cts +59 -0
- package/dist/http/index.d.ts +59 -0
- package/dist/http/index.mjs +71 -0
- package/dist/index.cjs +206 -79
- package/dist/index.d.cts +62 -9
- package/dist/index.d.ts +62 -9
- package/dist/index.mjs +208 -81
- package/package.json +25 -3
package/dist/health/index.d.cts
CHANGED
|
@@ -49,8 +49,9 @@ interface HealthIndicatorResult {
|
|
|
49
49
|
/** Whether the checked dependency is reachable and healthy. */
|
|
50
50
|
status: 'up' | 'down';
|
|
51
51
|
/**
|
|
52
|
-
* Optional diagnostic detail
|
|
53
|
-
*
|
|
52
|
+
* Optional diagnostic detail, served in the readiness response only when
|
|
53
|
+
* `health.exposeIndicatorErrors` is on. Must never include secrets,
|
|
54
|
+
* connection strings, credentials, or a raw internal error object: only safe,
|
|
54
55
|
* human-readable fields belong here.
|
|
55
56
|
*/
|
|
56
57
|
details?: Record<string, unknown>;
|
|
@@ -66,11 +67,13 @@ interface IHealthIndicator {
|
|
|
66
67
|
/**
|
|
67
68
|
* Perform the check.
|
|
68
69
|
*
|
|
70
|
+
* @param signal - Aborted when the aggregator stops waiting for this check. An indicator that
|
|
71
|
+
* can cancel its query should pass it on; one that cannot may ignore it.
|
|
69
72
|
* @returns The indicator's outcome.
|
|
70
73
|
* @throws Any error; a rejection is converted to a `down` entry by the
|
|
71
74
|
* aggregator and never propagates to the caller.
|
|
72
75
|
*/
|
|
73
|
-
check(): Promise<HealthIndicatorResult>;
|
|
76
|
+
check(signal?: AbortSignal): Promise<HealthIndicatorResult>;
|
|
74
77
|
}
|
|
75
78
|
/**
|
|
76
79
|
* One entry in a {@link HealthResponse.checks} array: an indicator's result,
|
package/dist/health/index.d.ts
CHANGED
|
@@ -49,8 +49,9 @@ interface HealthIndicatorResult {
|
|
|
49
49
|
/** Whether the checked dependency is reachable and healthy. */
|
|
50
50
|
status: 'up' | 'down';
|
|
51
51
|
/**
|
|
52
|
-
* Optional diagnostic detail
|
|
53
|
-
*
|
|
52
|
+
* Optional diagnostic detail, served in the readiness response only when
|
|
53
|
+
* `health.exposeIndicatorErrors` is on. Must never include secrets,
|
|
54
|
+
* connection strings, credentials, or a raw internal error object: only safe,
|
|
54
55
|
* human-readable fields belong here.
|
|
55
56
|
*/
|
|
56
57
|
details?: Record<string, unknown>;
|
|
@@ -66,11 +67,13 @@ interface IHealthIndicator {
|
|
|
66
67
|
/**
|
|
67
68
|
* Perform the check.
|
|
68
69
|
*
|
|
70
|
+
* @param signal - Aborted when the aggregator stops waiting for this check. An indicator that
|
|
71
|
+
* can cancel its query should pass it on; one that cannot may ignore it.
|
|
69
72
|
* @returns The indicator's outcome.
|
|
70
73
|
* @throws Any error; a rejection is converted to a `down` entry by the
|
|
71
74
|
* aggregator and never propagates to the caller.
|
|
72
75
|
*/
|
|
73
|
-
check(): Promise<HealthIndicatorResult>;
|
|
76
|
+
check(signal?: AbortSignal): Promise<HealthIndicatorResult>;
|
|
74
77
|
}
|
|
75
78
|
/**
|
|
76
79
|
* One entry in a {@link HealthResponse.checks} array: an indicator's result,
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
function assertBootstrapState(express) {
|
|
33
|
+
if (express.getHttpAdapter().getType?.() !== "express") {
|
|
34
|
+
throw new Error("applyHttpFloors supports the Express adapter only");
|
|
35
|
+
}
|
|
36
|
+
const server = express.getHttpServer();
|
|
37
|
+
if (server.listening) {
|
|
38
|
+
throw new Error("applyHttpFloors must run before listen(): the server is already listening");
|
|
39
|
+
}
|
|
40
|
+
if (express.isInitialized === true) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"applyHttpFloors must run before app.init(): the application is already initialised"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return server;
|
|
46
|
+
}
|
|
47
|
+
async function applyHttpFloors(app, options = {}) {
|
|
48
|
+
const headersTimeoutMs = options.headersTimeoutMs ?? DEFAULT_HEADERS_TIMEOUT_MS;
|
|
49
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
50
|
+
assertPositiveInteger("headersTimeoutMs", headersTimeoutMs);
|
|
51
|
+
assertPositiveInteger("requestTimeoutMs", requestTimeoutMs);
|
|
52
|
+
if (headersTimeoutMs > requestTimeoutMs) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`headersTimeoutMs (${headersTimeoutMs}) must not exceed requestTimeoutMs (${requestTimeoutMs})`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const checkingIntervalMs = options.connectionsCheckingIntervalMs ?? Math.min(DEFAULT_CONNECTIONS_CHECKING_INTERVAL_MS, headersTimeoutMs);
|
|
58
|
+
assertPositiveInteger("connectionsCheckingIntervalMs", checkingIntervalMs);
|
|
59
|
+
const express = app;
|
|
60
|
+
const server = assertBootstrapState(express);
|
|
61
|
+
const given = options.helmet ?? {};
|
|
62
|
+
if (given !== false) {
|
|
63
|
+
const helmet = await loadHelmet();
|
|
64
|
+
express.use(helmet("hsts" in given ? given : { strictTransportSecurity: false, ...given }));
|
|
65
|
+
}
|
|
66
|
+
express.useBodyParser("json", { inflate: false });
|
|
67
|
+
express.useBodyParser("urlencoded", { inflate: false, extended: true });
|
|
68
|
+
server.headersTimeout = headersTimeoutMs;
|
|
69
|
+
server.requestTimeout = requestTimeoutMs;
|
|
70
|
+
server.connectionsCheckingInterval = checkingIntervalMs;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
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 before `app.init()`, 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 before `app.init()`, 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,71 @@
|
|
|
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
|
+
function assertBootstrapState(express) {
|
|
31
|
+
if (express.getHttpAdapter().getType?.() !== "express") {
|
|
32
|
+
throw new Error("applyHttpFloors supports the Express adapter only");
|
|
33
|
+
}
|
|
34
|
+
const server = express.getHttpServer();
|
|
35
|
+
if (server.listening) {
|
|
36
|
+
throw new Error("applyHttpFloors must run before listen(): the server is already listening");
|
|
37
|
+
}
|
|
38
|
+
if (express.isInitialized === true) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"applyHttpFloors must run before app.init(): the application is already initialised"
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return server;
|
|
44
|
+
}
|
|
45
|
+
async function applyHttpFloors(app, options = {}) {
|
|
46
|
+
const headersTimeoutMs = options.headersTimeoutMs ?? DEFAULT_HEADERS_TIMEOUT_MS;
|
|
47
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
48
|
+
assertPositiveInteger("headersTimeoutMs", headersTimeoutMs);
|
|
49
|
+
assertPositiveInteger("requestTimeoutMs", requestTimeoutMs);
|
|
50
|
+
if (headersTimeoutMs > requestTimeoutMs) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`headersTimeoutMs (${headersTimeoutMs}) must not exceed requestTimeoutMs (${requestTimeoutMs})`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
const checkingIntervalMs = options.connectionsCheckingIntervalMs ?? Math.min(DEFAULT_CONNECTIONS_CHECKING_INTERVAL_MS, headersTimeoutMs);
|
|
56
|
+
assertPositiveInteger("connectionsCheckingIntervalMs", checkingIntervalMs);
|
|
57
|
+
const express = app;
|
|
58
|
+
const server = assertBootstrapState(express);
|
|
59
|
+
const given = options.helmet ?? {};
|
|
60
|
+
if (given !== false) {
|
|
61
|
+
const helmet = await loadHelmet();
|
|
62
|
+
express.use(helmet("hsts" in given ? given : { strictTransportSecurity: false, ...given }));
|
|
63
|
+
}
|
|
64
|
+
express.useBodyParser("json", { inflate: false });
|
|
65
|
+
express.useBodyParser("urlencoded", { inflate: false, extended: true });
|
|
66
|
+
server.headersTimeout = headersTimeoutMs;
|
|
67
|
+
server.requestTimeout = requestTimeoutMs;
|
|
68
|
+
server.connectionsCheckingInterval = checkingIntervalMs;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export { applyHttpFloors };
|