@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.
@@ -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. Must never include secrets, connection
53
- * strings, credentials, or a raw internal error object: only safe,
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,
@@ -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. Must never include secrets, connection
53
- * strings, credentials, or a raw internal error object: only safe,
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 };