@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/dist/index.cjs CHANGED
@@ -18,6 +18,121 @@ 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 isContainer(node) {
90
+ if (typeof node !== "object" || node === null) {
91
+ return false;
92
+ }
93
+ const proto = Object.getPrototypeOf(node);
94
+ return Array.isArray(node) || proto === Object.prototype || proto === null;
95
+ }
96
+ function exceedsDepth(value, maxDepth) {
97
+ let level = [value];
98
+ for (let depth = 0; level.length > 0; depth += 1) {
99
+ const next = [];
100
+ for (const node of level) {
101
+ if (isContainer(node)) {
102
+ if (depth >= maxDepth) {
103
+ return true;
104
+ }
105
+ for (const child of Object.values(node)) {
106
+ next.push(child);
107
+ }
108
+ }
109
+ }
110
+ level = next;
111
+ }
112
+ return false;
113
+ }
114
+ exports.BymaxBodyDepthMiddleware = class BymaxBodyDepthMiddleware {
115
+ constructor(options) {
116
+ this.options = options;
117
+ }
118
+ /**
119
+ * @param req - The request, whose `body` the parser has already filled.
120
+ * @param _res - Unused.
121
+ * @param next - Continues the chain.
122
+ * @throws BadRequestException When the body is nested too deeply.
123
+ */
124
+ use(req, _res, next) {
125
+ if (exceedsDepth(req.body, this.options.bodyDepth.maxDepth)) {
126
+ throw new common.BadRequestException({ code: BYMAX_BAD_REQUEST, message: BODY_TOO_DEEP_MESSAGE });
127
+ }
128
+ next();
129
+ }
130
+ };
131
+ exports.BymaxBodyDepthMiddleware = __decorateClass([
132
+ common.Injectable(),
133
+ __decorateParam(0, common.Inject(BYMAX_CORE_OPTIONS))
134
+ ], exports.BymaxBodyDepthMiddleware);
135
+
21
136
  // src/route-defaults.ts
22
137
  var DEFAULT_HEALTH_PATH = "health";
23
138
  var DEFAULT_METRICS_PATH = "metrics";
@@ -40,6 +155,8 @@ function runtimeEnvironmentName(declared) {
40
155
 
41
156
  // src/core.options.ts
42
157
  var DEFAULT_INDICATOR_TIMEOUT_MS = 5e3;
158
+ var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
159
+ var DEFAULT_READINESS_CACHE_MS = 1e3;
43
160
  var DEFAULT_OPENAPI_PATH = "docs";
44
161
  var DEFAULT_OPENAPI_JSON_PATH = "docs-json";
45
162
  var DEFAULT_OPENAPI_TITLE = "API";
@@ -59,17 +176,48 @@ function resolveEnvelope(raw) {
59
176
  exposeInternals: raw?.exposeInternals ?? false
60
177
  };
61
178
  }
179
+ function positiveSafeInteger(name, value, max) {
180
+ if (!Number.isSafeInteger(value) || value < 1) {
181
+ throw new Error(`${name} must be a positive safe integer, received ${String(value)}`);
182
+ }
183
+ if (max !== void 0 && value > max) {
184
+ throw new Error(`${name} must be a positive integer of at most ${max}, received ${value}`);
185
+ }
186
+ return value;
187
+ }
62
188
  function resolveTiming(raw) {
63
189
  const enabled = raw?.enabled ?? true;
64
190
  const threshold = raw?.slowRequestThresholdMs;
65
- return threshold === void 0 ? { enabled } : { enabled, slowRequestThresholdMs: threshold };
191
+ return threshold === void 0 ? { enabled } : {
192
+ enabled,
193
+ slowRequestThresholdMs: positiveSafeInteger("timing.slowRequestThresholdMs", threshold)
194
+ };
195
+ }
196
+ var DEFAULT_MAX_BODY_DEPTH = 32;
197
+ function resolveBodyDepth(raw) {
198
+ const maxDepth = positiveSafeInteger(
199
+ "bodyDepth.maxDepth",
200
+ raw?.maxDepth ?? DEFAULT_MAX_BODY_DEPTH
201
+ );
202
+ return { enabled: raw?.enabled ?? false, maxDepth };
66
203
  }
67
204
  function resolveHealth(raw) {
205
+ const readinessCacheMs = raw?.readinessCacheMs ?? DEFAULT_READINESS_CACHE_MS;
206
+ if (!Number.isSafeInteger(readinessCacheMs) || readinessCacheMs < 0) {
207
+ throw new Error(
208
+ `health.readinessCacheMs must be a non-negative safe integer, received ${String(readinessCacheMs)}`
209
+ );
210
+ }
68
211
  return {
212
+ readinessCacheMs,
69
213
  enabled: raw?.enabled ?? true,
70
214
  path: raw?.path ?? DEFAULT_HEALTH_PATH,
71
215
  exposeIndicatorErrors: raw?.exposeIndicatorErrors ?? false,
72
- indicatorTimeoutMs: raw?.indicatorTimeoutMs ?? DEFAULT_INDICATOR_TIMEOUT_MS,
216
+ indicatorTimeoutMs: positiveSafeInteger(
217
+ "health.indicatorTimeoutMs",
218
+ raw?.indicatorTimeoutMs ?? DEFAULT_INDICATOR_TIMEOUT_MS,
219
+ MAX_TIMER_DELAY_MS
220
+ ),
73
221
  autoDiscover: raw?.autoDiscover ?? false
74
222
  };
75
223
  }
@@ -136,6 +284,7 @@ function normalizeCoreOptions(raw) {
136
284
  return deepFreeze({
137
285
  envelope: resolveEnvelope(raw?.envelope),
138
286
  timing: resolveTiming(raw?.timing),
287
+ bodyDepth: resolveBodyDepth(raw?.bodyDepth),
139
288
  health: resolveHealth(raw?.health),
140
289
  metrics: resolveMetrics(raw?.metrics),
141
290
  openapi: resolveOpenApi(raw?.openapi, raw?.environment),
@@ -148,26 +297,6 @@ function normalizeCoreOptions(raw) {
148
297
  }
149
298
  normalizeCoreOptions();
150
299
 
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
300
  // src/optional-peer.ts
172
301
  function isMissingModuleError(cause) {
173
302
  const code = cause.code;
@@ -275,52 +404,6 @@ function containRejection(returned, onFailure) {
275
404
  }
276
405
  }
277
406
 
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
407
  // src/envelope/error-envelope.ts
325
408
  function buildErrorEnvelope(input) {
326
409
  const base = {
@@ -793,10 +876,12 @@ function summarizeRejection(reason) {
793
876
  }
794
877
  return `${message.slice(0, MAX_ERROR_MESSAGE_LENGTH - TRUNCATION_ELLIPSIS.length)}${TRUNCATION_ELLIPSIS}`;
795
878
  }
796
- async function runIndicator(indicator, timeoutMs, exposeErrors) {
879
+ function startTimeout(indicator, timeoutMs) {
880
+ const controller = new AbortController();
797
881
  let timer;
798
882
  const timedOut = new Promise((resolve) => {
799
883
  timer = setTimeout(() => {
884
+ controller.abort();
800
885
  resolve({
801
886
  entry: { name: indicator.name, status: "down", details: { timedOutAfterMs: timeoutMs } },
802
887
  isUp: false,
@@ -804,8 +889,12 @@ async function runIndicator(indicator, timeoutMs, exposeErrors) {
804
889
  });
805
890
  }, timeoutMs);
806
891
  });
807
- const checked = Promise.resolve().then(() => indicator.check()).then((result) => {
808
- const entry = { ...result, name: indicator.name };
892
+ return { signal: controller.signal, timedOut, clear: () => clearTimeout(timer) };
893
+ }
894
+ async function runIndicator(indicator, timeoutMs, exposeErrors) {
895
+ const { signal, timedOut, clear } = startTimeout(indicator, timeoutMs);
896
+ const checked = Promise.resolve().then(() => indicator.check(signal)).then((result) => {
897
+ const entry = exposeErrors ? { ...result, name: indicator.name } : { name: indicator.name, status: result.status };
809
898
  return result.status === "up" ? { entry, isUp: true } : { entry, isUp: false, cause: { kind: "reported-down" } };
810
899
  }).catch((reason) => {
811
900
  const message = summarizeRejection(reason);
@@ -818,7 +907,7 @@ async function runIndicator(indicator, timeoutMs, exposeErrors) {
818
907
  try {
819
908
  return await Promise.race([checked, timedOut]);
820
909
  } finally {
821
- clearTimeout(timer);
910
+ clear();
822
911
  }
823
912
  }
824
913
  function describeCause(cause) {
@@ -878,6 +967,8 @@ var HealthService = class {
878
967
  * Comparing states alone would then let the stale observation win.
879
968
  */
880
969
  this.probeSequence = 0;
970
+ /** Set once shutdown begins, after which readiness answers not ready without running anything. */
971
+ this.draining = false;
881
972
  }
882
973
  /**
883
974
  * Report one check's current state, emitting only when it differs from the
@@ -1050,6 +1141,14 @@ var HealthService = class {
1050
1141
  );
1051
1142
  return this.effectiveIndicators;
1052
1143
  }
1144
+ /**
1145
+ * Stop reporting ready the moment shutdown begins, so a load balancer stops routing here while
1146
+ * the process drains. Nothing cached or in progress is consulted again: every later probe is answered
1147
+ * before either is read.
1148
+ */
1149
+ beforeApplicationShutdown() {
1150
+ this.draining = true;
1151
+ }
1053
1152
  /**
1054
1153
  * Liveness check: the process is up and able to respond. Runs no
1055
1154
  * indicators, so it never depends on the health of anything else.
@@ -1064,9 +1163,33 @@ var HealthService = class {
1064
1163
  * aggregate the results. `status` is `'ok'` only when every indicator
1065
1164
  * reports `up`; an empty indicator list is vacuously `'ok'`.
1066
1165
  *
1166
+ * Probes share the work: one that arrives while a run is in progress awaits it, and for
1167
+ * `health.readinessCacheMs` after it ends the answer is returned without running anything.
1168
+ * The endpoint is usually unauthenticated, so probe volume must not set dependency volume.
1169
+ * Once shutdown has begun the answer is always not ready. The returned object is shared between
1170
+ * callers and must not be mutated.
1171
+ *
1172
+ * @returns The aggregated health response.
1173
+ */
1174
+ checkReadiness() {
1175
+ if (this.draining) {
1176
+ return Promise.resolve({ status: "error", checks: [{ name: "shutdown", status: "down" }] });
1177
+ }
1178
+ const cached = this.cached;
1179
+ if (cached !== void 0 && performance.now() - cached.at < this.options.health.readinessCacheMs) {
1180
+ return Promise.resolve(cached.response);
1181
+ }
1182
+ this.inflight ??= this.runReadiness().finally(() => {
1183
+ this.inflight = void 0;
1184
+ });
1185
+ return this.inflight;
1186
+ }
1187
+ /**
1188
+ * Run every indicator once and record the answer.
1189
+ *
1067
1190
  * @returns The aggregated health response.
1068
1191
  */
1069
- async checkReadiness() {
1192
+ async runReadiness() {
1070
1193
  const timeoutMs = this.options.health.indicatorTimeoutMs;
1071
1194
  const exposeErrors = this.options.health.exposeIndicatorErrors;
1072
1195
  const seq = ++this.probeSequence;
@@ -1078,7 +1201,9 @@ var HealthService = class {
1078
1201
  }
1079
1202
  const checks = outcomes.map((outcome) => outcome.entry);
1080
1203
  const status = checks.every((check) => check.status === "up") ? "ok" : "error";
1081
- return { status, checks };
1204
+ const response = { status, checks };
1205
+ this.cached = { at: performance.now(), response };
1206
+ return response;
1082
1207
  }
1083
1208
  };
1084
1209
  HealthService = __decorateClass([
@@ -1697,12 +1822,14 @@ exports.BymaxCoreModule = class BymaxCoreModule extends BymaxCoreModuleBase {
1697
1822
  * @param consumer - Nest's middleware consumer.
1698
1823
  */
1699
1824
  configure(consumer) {
1700
- if (!this.resolved.timing.enabled) {
1701
- return;
1702
- }
1703
1825
  const adapter = this.adapterHost?.httpAdapter;
1704
- const onFastify = bridgeFastifyRouteMetadata(adapter);
1705
- consumer.apply(exports.BymaxTimingMiddleware).forRoutes(onFastify ? "{*splat}" : "/");
1826
+ if (this.resolved.timing.enabled) {
1827
+ const onFastify = bridgeFastifyRouteMetadata(adapter);
1828
+ consumer.apply(exports.BymaxTimingMiddleware).forRoutes(onFastify ? "{*splat}" : "/");
1829
+ }
1830
+ if (this.resolved.bodyDepth.enabled && adapter?.getType?.() !== "fastify") {
1831
+ consumer.apply(exports.BymaxBodyDepthMiddleware).forRoutes("/");
1832
+ }
1706
1833
  }
1707
1834
  /**
1708
1835
  * Register the module synchronously. Options are known now, so disabled
package/dist/index.d.cts CHANGED
@@ -19,17 +19,45 @@ interface EnvelopeOptions {
19
19
  interface TimingOptions {
20
20
  /** Register the request-timing middleware (`BymaxTimingMiddleware`). Default: `true`. */
21
21
  enabled?: boolean;
22
- /** Samples above this threshold are flagged as slow. No default (unset). */
22
+ /** Samples above this threshold are flagged as slow. Positive integer. 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`. */
28
47
  enabled?: boolean;
29
48
  /** Route prefix for the health endpoints. Default: `'health'`. */
30
49
  path?: string;
31
- /** Per-indicator timeout before a check is reported as down. Default: `5000`. */
50
+ /** Per-indicator timeout before a check is reported as down. Positive integer up to 2147483647. Default: `5000`. */
32
51
  indicatorTimeoutMs?: number;
52
+ /**
53
+ * How long a finished readiness answer is reused, in milliseconds. `0` turns the cache off.
54
+ * Default: `1000`.
55
+ *
56
+ * Readiness is usually reachable without credentials, and every probe would otherwise reach
57
+ * every dependency. Probes that arrive while one is running always share it, whatever this is set to;
58
+ * this is only how long its answer outlives it.
59
+ */
60
+ readinessCacheMs?: number;
33
61
  /**
34
62
  * Aggregate every provider marked with `@BymaxHealthIndicator()`, anywhere in
35
63
  * the application, in addition to those registered under
@@ -43,15 +71,16 @@ interface HealthOptions {
43
71
  */
44
72
  autoDiscover?: boolean;
45
73
  /**
46
- * Include the failing indicator's message in the readiness response under
47
- * `details.error`. Never enable in production. Default: `false`.
74
+ * Serve what an indicator returned in `details`, and the failing indicator's
75
+ * message under `details.error`, in the readiness response. Never enable in
76
+ * production. Default: `false`.
48
77
  *
49
78
  * Readiness is typically unauthenticated and reachable by whatever probes it,
50
79
  * and an indicator usually does not author its own failure message — it lets a
51
80
  * driver's error propagate, and driver errors carry hosts, ports and sometimes
52
- * credentials. With this off, the response names which indicator is down and
53
- * nothing else; the message goes to the logger, where access is already
54
- * controlled.
81
+ * credentials. With this off, the response carries each indicator's name and
82
+ * status and nothing else, except the timeout the aggregator itself reports;
83
+ * the message goes to the logger, where access is already controlled.
55
84
  */
56
85
  exposeIndicatorErrors?: boolean;
57
86
  }
@@ -267,6 +296,8 @@ interface BymaxCoreModuleOptions {
267
296
  envelope?: EnvelopeOptions;
268
297
  /** Request-timing middleware. Default: enabled. */
269
298
  timing?: TimingOptions;
299
+ /** Request-body nesting limit. Default: disabled. */
300
+ bodyDepth?: BodyDepthOptions;
270
301
  /** Liveness and readiness endpoints. Default: enabled. */
271
302
  health?: HealthOptions;
272
303
  /** Prometheus metrics endpoint. Default: disabled. */
@@ -308,11 +339,17 @@ interface ResolvedTimingOptions {
308
339
  enabled: boolean;
309
340
  slowRequestThresholdMs?: number;
310
341
  }
342
+ /** Fully-resolved body-depth options. */
343
+ interface ResolvedBodyDepthOptions {
344
+ enabled: boolean;
345
+ maxDepth: number;
346
+ }
311
347
  /** Fully-resolved health options. */
312
348
  interface ResolvedHealthOptions {
313
349
  enabled: boolean;
314
350
  path: string;
315
351
  indicatorTimeoutMs: number;
352
+ readinessCacheMs: number;
316
353
  exposeIndicatorErrors: boolean;
317
354
  autoDiscover: boolean;
318
355
  }
@@ -365,6 +402,7 @@ interface ResolvedOpenApiOptions {
365
402
  interface ResolvedCoreOptions {
366
403
  envelope: ResolvedEnvelopeOptions;
367
404
  timing: ResolvedTimingOptions;
405
+ bodyDepth: ResolvedBodyDepthOptions;
368
406
  health: ResolvedHealthOptions;
369
407
  metrics: ResolvedMetricsOptions;
370
408
  openapi: ResolvedOpenApiOptions;
@@ -1115,6 +1153,21 @@ declare class TimingInterceptor implements NestInterceptor {
1115
1153
  private recordSample;
1116
1154
  }
1117
1155
 
1156
+ /** Refuses a body nested deeper than `bodyDepth.maxDepth` with a coded `400`. */
1157
+ declare class BymaxBodyDepthMiddleware implements NestMiddleware {
1158
+ private readonly options;
1159
+ constructor(options: ResolvedCoreOptions);
1160
+ /**
1161
+ * @param req - The request, whose `body` the parser has already filled.
1162
+ * @param _res - Unused.
1163
+ * @param next - Continues the chain.
1164
+ * @throws BadRequestException When the body is nested too deeply.
1165
+ */
1166
+ use(req: {
1167
+ body?: unknown;
1168
+ }, _res: unknown, next: () => void): void;
1169
+ }
1170
+
1118
1171
  /**
1119
1172
  * @fileoverview Neutral request-info accessor for request timing. Reads the
1120
1173
  * HTTP method and the route template off a request without assuming Express or
@@ -1296,5 +1349,5 @@ declare const BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
1296
1349
  */
1297
1350
  declare function codeForStatus(status: number): string;
1298
1351
 
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 };
1352
+ 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 };
1353
+ 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
@@ -19,17 +19,45 @@ interface EnvelopeOptions {
19
19
  interface TimingOptions {
20
20
  /** Register the request-timing middleware (`BymaxTimingMiddleware`). Default: `true`. */
21
21
  enabled?: boolean;
22
- /** Samples above this threshold are flagged as slow. No default (unset). */
22
+ /** Samples above this threshold are flagged as slow. Positive integer. 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`. */
28
47
  enabled?: boolean;
29
48
  /** Route prefix for the health endpoints. Default: `'health'`. */
30
49
  path?: string;
31
- /** Per-indicator timeout before a check is reported as down. Default: `5000`. */
50
+ /** Per-indicator timeout before a check is reported as down. Positive integer up to 2147483647. Default: `5000`. */
32
51
  indicatorTimeoutMs?: number;
52
+ /**
53
+ * How long a finished readiness answer is reused, in milliseconds. `0` turns the cache off.
54
+ * Default: `1000`.
55
+ *
56
+ * Readiness is usually reachable without credentials, and every probe would otherwise reach
57
+ * every dependency. Probes that arrive while one is running always share it, whatever this is set to;
58
+ * this is only how long its answer outlives it.
59
+ */
60
+ readinessCacheMs?: number;
33
61
  /**
34
62
  * Aggregate every provider marked with `@BymaxHealthIndicator()`, anywhere in
35
63
  * the application, in addition to those registered under
@@ -43,15 +71,16 @@ interface HealthOptions {
43
71
  */
44
72
  autoDiscover?: boolean;
45
73
  /**
46
- * Include the failing indicator's message in the readiness response under
47
- * `details.error`. Never enable in production. Default: `false`.
74
+ * Serve what an indicator returned in `details`, and the failing indicator's
75
+ * message under `details.error`, in the readiness response. Never enable in
76
+ * production. Default: `false`.
48
77
  *
49
78
  * Readiness is typically unauthenticated and reachable by whatever probes it,
50
79
  * and an indicator usually does not author its own failure message — it lets a
51
80
  * driver's error propagate, and driver errors carry hosts, ports and sometimes
52
- * credentials. With this off, the response names which indicator is down and
53
- * nothing else; the message goes to the logger, where access is already
54
- * controlled.
81
+ * credentials. With this off, the response carries each indicator's name and
82
+ * status and nothing else, except the timeout the aggregator itself reports;
83
+ * the message goes to the logger, where access is already controlled.
55
84
  */
56
85
  exposeIndicatorErrors?: boolean;
57
86
  }
@@ -267,6 +296,8 @@ interface BymaxCoreModuleOptions {
267
296
  envelope?: EnvelopeOptions;
268
297
  /** Request-timing middleware. Default: enabled. */
269
298
  timing?: TimingOptions;
299
+ /** Request-body nesting limit. Default: disabled. */
300
+ bodyDepth?: BodyDepthOptions;
270
301
  /** Liveness and readiness endpoints. Default: enabled. */
271
302
  health?: HealthOptions;
272
303
  /** Prometheus metrics endpoint. Default: disabled. */
@@ -308,11 +339,17 @@ interface ResolvedTimingOptions {
308
339
  enabled: boolean;
309
340
  slowRequestThresholdMs?: number;
310
341
  }
342
+ /** Fully-resolved body-depth options. */
343
+ interface ResolvedBodyDepthOptions {
344
+ enabled: boolean;
345
+ maxDepth: number;
346
+ }
311
347
  /** Fully-resolved health options. */
312
348
  interface ResolvedHealthOptions {
313
349
  enabled: boolean;
314
350
  path: string;
315
351
  indicatorTimeoutMs: number;
352
+ readinessCacheMs: number;
316
353
  exposeIndicatorErrors: boolean;
317
354
  autoDiscover: boolean;
318
355
  }
@@ -365,6 +402,7 @@ interface ResolvedOpenApiOptions {
365
402
  interface ResolvedCoreOptions {
366
403
  envelope: ResolvedEnvelopeOptions;
367
404
  timing: ResolvedTimingOptions;
405
+ bodyDepth: ResolvedBodyDepthOptions;
368
406
  health: ResolvedHealthOptions;
369
407
  metrics: ResolvedMetricsOptions;
370
408
  openapi: ResolvedOpenApiOptions;
@@ -1115,6 +1153,21 @@ declare class TimingInterceptor implements NestInterceptor {
1115
1153
  private recordSample;
1116
1154
  }
1117
1155
 
1156
+ /** Refuses a body nested deeper than `bodyDepth.maxDepth` with a coded `400`. */
1157
+ declare class BymaxBodyDepthMiddleware implements NestMiddleware {
1158
+ private readonly options;
1159
+ constructor(options: ResolvedCoreOptions);
1160
+ /**
1161
+ * @param req - The request, whose `body` the parser has already filled.
1162
+ * @param _res - Unused.
1163
+ * @param next - Continues the chain.
1164
+ * @throws BadRequestException When the body is nested too deeply.
1165
+ */
1166
+ use(req: {
1167
+ body?: unknown;
1168
+ }, _res: unknown, next: () => void): void;
1169
+ }
1170
+
1118
1171
  /**
1119
1172
  * @fileoverview Neutral request-info accessor for request timing. Reads the
1120
1173
  * HTTP method and the route template off a request without assuming Express or
@@ -1296,5 +1349,5 @@ declare const BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
1296
1349
  */
1297
1350
  declare function codeForStatus(status: number): string;
1298
1351
 
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 };
1352
+ 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 };
1353
+ 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 };