@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/dist/index.cjs CHANGED
@@ -18,6 +18,114 @@ var __decorateClass = (decorators, target, key, kind) => {
18
18
  };
19
19
  var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
20
20
 
21
+ // src/core.tokens.ts
22
+ var BYMAX_CORE_OPTIONS = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:core-options");
23
+ var BYMAX_CORRELATION_PROVIDER = /* @__PURE__ */ Symbol.for(
24
+ "@bymax-one/nest-core:correlation-provider"
25
+ );
26
+ var BYMAX_TIMING_SINK = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:timing-sink");
27
+ var BYMAX_HEALTH_INDICATORS = /* @__PURE__ */ Symbol.for(
28
+ "@bymax-one/nest-core:health-indicators"
29
+ );
30
+ var BYMAX_HEALTH_TRANSITION_SINK = /* @__PURE__ */ Symbol.for(
31
+ "@bymax-one/nest-core:health-transition-sink"
32
+ );
33
+ var BYMAX_UNEXPECTED_ERROR_SINK = /* @__PURE__ */ Symbol.for(
34
+ "@bymax-one/nest-core:unexpected-error-sink"
35
+ );
36
+ var BYMAX_METRICS_REGISTRY = /* @__PURE__ */ Symbol.for(
37
+ "@bymax-one/nest-core:metrics-registry"
38
+ );
39
+ var BYMAX_TRACE_CONTEXT = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:trace-context");
40
+
41
+ // src/envelope/error-codes.ts
42
+ var BYMAX_BAD_REQUEST = "BYMAX_BAD_REQUEST";
43
+ var BYMAX_VALIDATION_FAILED = "BYMAX_VALIDATION_FAILED";
44
+ var BYMAX_UNAUTHORIZED = "BYMAX_UNAUTHORIZED";
45
+ var BYMAX_FORBIDDEN = "BYMAX_FORBIDDEN";
46
+ var BYMAX_NOT_FOUND = "BYMAX_NOT_FOUND";
47
+ var BYMAX_CONFLICT = "BYMAX_CONFLICT";
48
+ var BYMAX_PAYLOAD_TOO_LARGE = "BYMAX_PAYLOAD_TOO_LARGE";
49
+ var BYMAX_UNSUPPORTED_MEDIA_TYPE = "BYMAX_UNSUPPORTED_MEDIA_TYPE";
50
+ var BYMAX_UNPROCESSABLE_ENTITY = "BYMAX_UNPROCESSABLE_ENTITY";
51
+ var BYMAX_TOO_MANY_REQUESTS = "BYMAX_TOO_MANY_REQUESTS";
52
+ var BYMAX_CLIENT_ERROR = "BYMAX_CLIENT_ERROR";
53
+ var BYMAX_INTERNAL_ERROR = "BYMAX_INTERNAL_ERROR";
54
+ var BYMAX_NOT_IMPLEMENTED = "BYMAX_NOT_IMPLEMENTED";
55
+ var BYMAX_BAD_GATEWAY = "BYMAX_BAD_GATEWAY";
56
+ var BYMAX_SERVICE_UNAVAILABLE = "BYMAX_SERVICE_UNAVAILABLE";
57
+ var BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
58
+ var STATUS_CODES = /* @__PURE__ */ new Map([
59
+ [400, BYMAX_BAD_REQUEST],
60
+ [401, BYMAX_UNAUTHORIZED],
61
+ [403, BYMAX_FORBIDDEN],
62
+ [404, BYMAX_NOT_FOUND],
63
+ [409, BYMAX_CONFLICT],
64
+ [413, BYMAX_PAYLOAD_TOO_LARGE],
65
+ [415, BYMAX_UNSUPPORTED_MEDIA_TYPE],
66
+ [422, BYMAX_UNPROCESSABLE_ENTITY],
67
+ [429, BYMAX_TOO_MANY_REQUESTS],
68
+ [500, BYMAX_INTERNAL_ERROR],
69
+ [501, BYMAX_NOT_IMPLEMENTED],
70
+ [502, BYMAX_BAD_GATEWAY],
71
+ [503, BYMAX_SERVICE_UNAVAILABLE],
72
+ [504, BYMAX_GATEWAY_TIMEOUT]
73
+ ]);
74
+ var CLIENT_ERROR_MIN = 400;
75
+ var CLIENT_ERROR_MAX = 500;
76
+ function codeForStatus(status) {
77
+ const catalogued = STATUS_CODES.get(status);
78
+ if (catalogued !== void 0) {
79
+ return catalogued;
80
+ }
81
+ if (status >= CLIENT_ERROR_MIN && status < CLIENT_ERROR_MAX) {
82
+ return BYMAX_CLIENT_ERROR;
83
+ }
84
+ return BYMAX_INTERNAL_ERROR;
85
+ }
86
+
87
+ // src/body-depth/body-depth.middleware.ts
88
+ var BODY_TOO_DEEP_MESSAGE = "Request body is nested too deeply";
89
+ function exceedsDepth(value, maxDepth) {
90
+ let level = [value];
91
+ for (let depth = 0; level.length > 0; depth += 1) {
92
+ const next = [];
93
+ for (const node of level) {
94
+ if (typeof node === "object" && node !== null) {
95
+ if (depth >= maxDepth) {
96
+ return true;
97
+ }
98
+ for (const child of Object.values(node)) {
99
+ next.push(child);
100
+ }
101
+ }
102
+ }
103
+ level = next;
104
+ }
105
+ return false;
106
+ }
107
+ exports.BymaxBodyDepthMiddleware = class BymaxBodyDepthMiddleware {
108
+ constructor(options) {
109
+ this.options = options;
110
+ }
111
+ /**
112
+ * @param req - The request, whose `body` the parser has already filled.
113
+ * @param _res - Unused.
114
+ * @param next - Continues the chain.
115
+ * @throws BadRequestException When the body is nested too deeply.
116
+ */
117
+ use(req, _res, next) {
118
+ if (exceedsDepth(req.body, this.options.bodyDepth.maxDepth)) {
119
+ throw new common.BadRequestException({ code: BYMAX_BAD_REQUEST, message: BODY_TOO_DEEP_MESSAGE });
120
+ }
121
+ next();
122
+ }
123
+ };
124
+ exports.BymaxBodyDepthMiddleware = __decorateClass([
125
+ common.Injectable(),
126
+ __decorateParam(0, common.Inject(BYMAX_CORE_OPTIONS))
127
+ ], exports.BymaxBodyDepthMiddleware);
128
+
21
129
  // src/route-defaults.ts
22
130
  var DEFAULT_HEALTH_PATH = "health";
23
131
  var DEFAULT_METRICS_PATH = "metrics";
@@ -64,6 +172,16 @@ function resolveTiming(raw) {
64
172
  const threshold = raw?.slowRequestThresholdMs;
65
173
  return threshold === void 0 ? { enabled } : { enabled, slowRequestThresholdMs: threshold };
66
174
  }
175
+ var DEFAULT_MAX_BODY_DEPTH = 32;
176
+ function resolveBodyDepth(raw) {
177
+ const maxDepth = raw?.maxDepth ?? DEFAULT_MAX_BODY_DEPTH;
178
+ if (!Number.isSafeInteger(maxDepth) || maxDepth < 1) {
179
+ throw new Error(
180
+ `bodyDepth.maxDepth must be a positive safe integer, received ${String(maxDepth)}`
181
+ );
182
+ }
183
+ return { enabled: raw?.enabled ?? false, maxDepth };
184
+ }
67
185
  function resolveHealth(raw) {
68
186
  return {
69
187
  enabled: raw?.enabled ?? true,
@@ -136,6 +254,7 @@ function normalizeCoreOptions(raw) {
136
254
  return deepFreeze({
137
255
  envelope: resolveEnvelope(raw?.envelope),
138
256
  timing: resolveTiming(raw?.timing),
257
+ bodyDepth: resolveBodyDepth(raw?.bodyDepth),
139
258
  health: resolveHealth(raw?.health),
140
259
  metrics: resolveMetrics(raw?.metrics),
141
260
  openapi: resolveOpenApi(raw?.openapi, raw?.environment),
@@ -148,26 +267,6 @@ function normalizeCoreOptions(raw) {
148
267
  }
149
268
  normalizeCoreOptions();
150
269
 
151
- // src/core.tokens.ts
152
- var BYMAX_CORE_OPTIONS = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:core-options");
153
- var BYMAX_CORRELATION_PROVIDER = /* @__PURE__ */ Symbol.for(
154
- "@bymax-one/nest-core:correlation-provider"
155
- );
156
- var BYMAX_TIMING_SINK = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:timing-sink");
157
- var BYMAX_HEALTH_INDICATORS = /* @__PURE__ */ Symbol.for(
158
- "@bymax-one/nest-core:health-indicators"
159
- );
160
- var BYMAX_HEALTH_TRANSITION_SINK = /* @__PURE__ */ Symbol.for(
161
- "@bymax-one/nest-core:health-transition-sink"
162
- );
163
- var BYMAX_UNEXPECTED_ERROR_SINK = /* @__PURE__ */ Symbol.for(
164
- "@bymax-one/nest-core:unexpected-error-sink"
165
- );
166
- var BYMAX_METRICS_REGISTRY = /* @__PURE__ */ Symbol.for(
167
- "@bymax-one/nest-core:metrics-registry"
168
- );
169
- var BYMAX_TRACE_CONTEXT = /* @__PURE__ */ Symbol.for("@bymax-one/nest-core:trace-context");
170
-
171
270
  // src/optional-peer.ts
172
271
  function isMissingModuleError(cause) {
173
272
  const code = cause.code;
@@ -275,52 +374,6 @@ function containRejection(returned, onFailure) {
275
374
  }
276
375
  }
277
376
 
278
- // src/envelope/error-codes.ts
279
- var BYMAX_BAD_REQUEST = "BYMAX_BAD_REQUEST";
280
- var BYMAX_VALIDATION_FAILED = "BYMAX_VALIDATION_FAILED";
281
- var BYMAX_UNAUTHORIZED = "BYMAX_UNAUTHORIZED";
282
- var BYMAX_FORBIDDEN = "BYMAX_FORBIDDEN";
283
- var BYMAX_NOT_FOUND = "BYMAX_NOT_FOUND";
284
- var BYMAX_CONFLICT = "BYMAX_CONFLICT";
285
- var BYMAX_PAYLOAD_TOO_LARGE = "BYMAX_PAYLOAD_TOO_LARGE";
286
- var BYMAX_UNSUPPORTED_MEDIA_TYPE = "BYMAX_UNSUPPORTED_MEDIA_TYPE";
287
- var BYMAX_UNPROCESSABLE_ENTITY = "BYMAX_UNPROCESSABLE_ENTITY";
288
- var BYMAX_TOO_MANY_REQUESTS = "BYMAX_TOO_MANY_REQUESTS";
289
- var BYMAX_CLIENT_ERROR = "BYMAX_CLIENT_ERROR";
290
- var BYMAX_INTERNAL_ERROR = "BYMAX_INTERNAL_ERROR";
291
- var BYMAX_NOT_IMPLEMENTED = "BYMAX_NOT_IMPLEMENTED";
292
- var BYMAX_BAD_GATEWAY = "BYMAX_BAD_GATEWAY";
293
- var BYMAX_SERVICE_UNAVAILABLE = "BYMAX_SERVICE_UNAVAILABLE";
294
- var BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
295
- var STATUS_CODES = /* @__PURE__ */ new Map([
296
- [400, BYMAX_BAD_REQUEST],
297
- [401, BYMAX_UNAUTHORIZED],
298
- [403, BYMAX_FORBIDDEN],
299
- [404, BYMAX_NOT_FOUND],
300
- [409, BYMAX_CONFLICT],
301
- [413, BYMAX_PAYLOAD_TOO_LARGE],
302
- [415, BYMAX_UNSUPPORTED_MEDIA_TYPE],
303
- [422, BYMAX_UNPROCESSABLE_ENTITY],
304
- [429, BYMAX_TOO_MANY_REQUESTS],
305
- [500, BYMAX_INTERNAL_ERROR],
306
- [501, BYMAX_NOT_IMPLEMENTED],
307
- [502, BYMAX_BAD_GATEWAY],
308
- [503, BYMAX_SERVICE_UNAVAILABLE],
309
- [504, BYMAX_GATEWAY_TIMEOUT]
310
- ]);
311
- var CLIENT_ERROR_MIN = 400;
312
- var CLIENT_ERROR_MAX = 500;
313
- function codeForStatus(status) {
314
- const catalogued = STATUS_CODES.get(status);
315
- if (catalogued !== void 0) {
316
- return catalogued;
317
- }
318
- if (status >= CLIENT_ERROR_MIN && status < CLIENT_ERROR_MAX) {
319
- return BYMAX_CLIENT_ERROR;
320
- }
321
- return BYMAX_INTERNAL_ERROR;
322
- }
323
-
324
377
  // src/envelope/error-envelope.ts
325
378
  function buildErrorEnvelope(input) {
326
379
  const base = {
@@ -359,6 +412,12 @@ function extractRequestInfo(context) {
359
412
  var INTERNAL_ERROR_STATUS = 500;
360
413
  var INTERNAL_ERROR_MESSAGE = "Internal server error";
361
414
  var VALIDATION_FAILED_MESSAGE = "Validation failed";
415
+ var MALFORMED_REQUEST_MESSAGE = "Malformed request";
416
+ var PRE_ROUTING_MESSAGES = /* @__PURE__ */ new Map([
417
+ [400, MALFORMED_REQUEST_MESSAGE],
418
+ [413, "Request entity too large"],
419
+ [415, "Unsupported media type"]
420
+ ]);
362
421
  var ABSOLUTE_FORM_PREFIX = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
363
422
  function pathOf(url) {
364
423
  let target = String(url);
@@ -409,8 +468,9 @@ function resolveExposedClientError(exception) {
409
468
  if (typeof status !== "number" || !Number.isInteger(status) || status < CLIENT_ERROR_MIN2 || status >= CLIENT_ERROR_MAX2) {
410
469
  return void 0;
411
470
  }
471
+ const fixed = typeof candidate.type === "string" ? PRE_ROUTING_MESSAGES.get(status) : void 0;
412
472
  const message = exception instanceof Error ? exception.message : "";
413
- return { status, message: message === "" ? INTERNAL_ERROR_MESSAGE : message };
473
+ return { status, message: fixed ?? (message === "" ? INTERNAL_ERROR_MESSAGE : message) };
414
474
  }
415
475
  function isValidationResponse(response) {
416
476
  return typeof response === "object" && response !== null && "message" in response && Array.isArray(response.message);
@@ -611,6 +671,9 @@ exports.BymaxExceptionFilter = class BymaxExceptionFilter {
611
671
  toValidationDetails(response.message)
612
672
  );
613
673
  }
674
+ if (status === 400 && readRequestInfo(request).route === UNMATCHED_ROUTE) {
675
+ return this.toEnvelope(status, codeForStatus(status), MALFORMED_REQUEST_MESSAGE, context);
676
+ }
614
677
  const message = extractHttpMessage(response, exception);
615
678
  const rawUrl = String(this.adapterHost.httpAdapter.getRequestUrl(request));
616
679
  return this.toEnvelope(
@@ -1687,12 +1750,14 @@ exports.BymaxCoreModule = class BymaxCoreModule extends BymaxCoreModuleBase {
1687
1750
  * @param consumer - Nest's middleware consumer.
1688
1751
  */
1689
1752
  configure(consumer) {
1690
- if (!this.resolved.timing.enabled) {
1691
- return;
1692
- }
1693
1753
  const adapter = this.adapterHost?.httpAdapter;
1694
- const onFastify = bridgeFastifyRouteMetadata(adapter);
1695
- consumer.apply(exports.BymaxTimingMiddleware).forRoutes(onFastify ? "{*splat}" : "/");
1754
+ if (this.resolved.timing.enabled) {
1755
+ const onFastify = bridgeFastifyRouteMetadata(adapter);
1756
+ consumer.apply(exports.BymaxTimingMiddleware).forRoutes(onFastify ? "{*splat}" : "/");
1757
+ }
1758
+ if (this.resolved.bodyDepth.enabled && adapter?.getType?.() !== "fastify") {
1759
+ consumer.apply(exports.BymaxBodyDepthMiddleware).forRoutes("/");
1760
+ }
1696
1761
  }
1697
1762
  /**
1698
1763
  * Register the module synchronously. Options are known now, so disabled
package/dist/index.d.cts CHANGED
@@ -22,6 +22,25 @@ interface TimingOptions {
22
22
  /** Samples above this threshold are flagged as slow. No default (unset). */
23
23
  slowRequestThresholdMs?: number;
24
24
  }
25
+ /** Request-body nesting limit, applied before the application's own validation runs. */
26
+ interface BodyDepthOptions {
27
+ /**
28
+ * Refuse a parsed request body nested deeper than {@link BodyDepthOptions.maxDepth} with a
29
+ * `400` through the error envelope. Default: `false`.
30
+ *
31
+ * Off by default because turning it on rejects bodies that are accepted today. Enable it:
32
+ * a body of a few thousand nested arrays is only a few kilobytes, and a recursive validator
33
+ * or transformer overflows the stack on it, so the request is answered `500`, with no
34
+ * credential needed. The check runs before routing, on the body the parser already produced, so it covers the Express adapter and a body parsed by
35
+ * Nest's own parsers; it does not cover Fastify.
36
+ */
37
+ enabled?: boolean;
38
+ /**
39
+ * The deepest nesting a body may have: `[[1]]` is depth 2, a flat object or array is depth 1,
40
+ * and a scalar is depth 0. Must be a positive safe integer. Default: `32`.
41
+ */
42
+ maxDepth?: number;
43
+ }
25
44
  /** Liveness and readiness endpoint configuration. */
26
45
  interface HealthOptions {
27
46
  /** Register the health controller. Default: `true`. */
@@ -267,6 +286,8 @@ interface BymaxCoreModuleOptions {
267
286
  envelope?: EnvelopeOptions;
268
287
  /** Request-timing middleware. Default: enabled. */
269
288
  timing?: TimingOptions;
289
+ /** Request-body nesting limit. Default: disabled. */
290
+ bodyDepth?: BodyDepthOptions;
270
291
  /** Liveness and readiness endpoints. Default: enabled. */
271
292
  health?: HealthOptions;
272
293
  /** Prometheus metrics endpoint. Default: disabled. */
@@ -308,6 +329,11 @@ interface ResolvedTimingOptions {
308
329
  enabled: boolean;
309
330
  slowRequestThresholdMs?: number;
310
331
  }
332
+ /** Fully-resolved body-depth options. */
333
+ interface ResolvedBodyDepthOptions {
334
+ enabled: boolean;
335
+ maxDepth: number;
336
+ }
311
337
  /** Fully-resolved health options. */
312
338
  interface ResolvedHealthOptions {
313
339
  enabled: boolean;
@@ -365,6 +391,7 @@ interface ResolvedOpenApiOptions {
365
391
  interface ResolvedCoreOptions {
366
392
  envelope: ResolvedEnvelopeOptions;
367
393
  timing: ResolvedTimingOptions;
394
+ bodyDepth: ResolvedBodyDepthOptions;
368
395
  health: ResolvedHealthOptions;
369
396
  metrics: ResolvedMetricsOptions;
370
397
  openapi: ResolvedOpenApiOptions;
@@ -1115,6 +1142,21 @@ declare class TimingInterceptor implements NestInterceptor {
1115
1142
  private recordSample;
1116
1143
  }
1117
1144
 
1145
+ /** Refuses a body nested deeper than `bodyDepth.maxDepth` with a coded `400`. */
1146
+ declare class BymaxBodyDepthMiddleware implements NestMiddleware {
1147
+ private readonly options;
1148
+ constructor(options: ResolvedCoreOptions);
1149
+ /**
1150
+ * @param req - The request, whose `body` the parser has already filled.
1151
+ * @param _res - Unused.
1152
+ * @param next - Continues the chain.
1153
+ * @throws BadRequestException When the body is nested too deeply.
1154
+ */
1155
+ use(req: {
1156
+ body?: unknown;
1157
+ }, _res: unknown, next: () => void): void;
1158
+ }
1159
+
1118
1160
  /**
1119
1161
  * @fileoverview Neutral request-info accessor for request timing. Reads the
1120
1162
  * HTTP method and the route template off a request without assuming Express or
@@ -1296,5 +1338,5 @@ declare const BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
1296
1338
  */
1297
1339
  declare function codeForStatus(status: number): string;
1298
1340
 
1299
- export { BYMAX_BAD_GATEWAY, BYMAX_BAD_REQUEST, BYMAX_CLIENT_ERROR, BYMAX_CONFLICT, BYMAX_CORE_OPTIONS, BYMAX_CORRELATION_PROVIDER, BYMAX_FORBIDDEN, BYMAX_GATEWAY_TIMEOUT, BYMAX_HEALTH_INDICATORS, BYMAX_HEALTH_TRANSITION_SINK, BYMAX_INTERNAL_ERROR, BYMAX_METRICS_REGISTRY, BYMAX_NOT_FOUND, BYMAX_NOT_IMPLEMENTED, BYMAX_PAYLOAD_TOO_LARGE, BYMAX_SERVICE_UNAVAILABLE, BYMAX_TIMING_SINK, BYMAX_TOO_MANY_REQUESTS, BYMAX_TRACE_CONTEXT, BYMAX_UNAUTHORIZED, BYMAX_UNEXPECTED_ERROR_SINK, BYMAX_UNPROCESSABLE_ENTITY, BYMAX_UNSUPPORTED_MEDIA_TYPE, BYMAX_VALIDATION_FAILED, BymaxCoreModule, BymaxExceptionFilter, BymaxTimingMiddleware, TimingInterceptor, UNMATCHED_ROUTE, buildErrorEnvelope, codeForStatus };
1300
- export type { BuildErrorEnvelopeInput, BymaxCoreModuleOptions, EnvelopeOptions, ErrorDetails, ErrorEnvelope, FilterErrorContext, HealthOptions, ICorrelationIdProvider, ITimingSink, ITraceContextProvider, IUnexpectedErrorSink, MetricsOptions, OpenApiHttpMethod, OpenApiOperationIdFactory, OpenApiOperationKey, OpenApiOptions, OpenApiSecurityRequirement, OpenApiSecurityScheme, OpenApiServerDescriptor, OperationSecurityMap, RequestTimingSample, ResolvedCoreOptions, TelemetryOptions, TimingOptions, TraceContext, UnexpectedErrorReport };
1341
+ export { BYMAX_BAD_GATEWAY, BYMAX_BAD_REQUEST, BYMAX_CLIENT_ERROR, BYMAX_CONFLICT, BYMAX_CORE_OPTIONS, BYMAX_CORRELATION_PROVIDER, BYMAX_FORBIDDEN, BYMAX_GATEWAY_TIMEOUT, BYMAX_HEALTH_INDICATORS, BYMAX_HEALTH_TRANSITION_SINK, BYMAX_INTERNAL_ERROR, BYMAX_METRICS_REGISTRY, BYMAX_NOT_FOUND, BYMAX_NOT_IMPLEMENTED, BYMAX_PAYLOAD_TOO_LARGE, BYMAX_SERVICE_UNAVAILABLE, BYMAX_TIMING_SINK, BYMAX_TOO_MANY_REQUESTS, BYMAX_TRACE_CONTEXT, BYMAX_UNAUTHORIZED, BYMAX_UNEXPECTED_ERROR_SINK, BYMAX_UNPROCESSABLE_ENTITY, BYMAX_UNSUPPORTED_MEDIA_TYPE, BYMAX_VALIDATION_FAILED, BymaxBodyDepthMiddleware, BymaxCoreModule, BymaxExceptionFilter, BymaxTimingMiddleware, TimingInterceptor, UNMATCHED_ROUTE, buildErrorEnvelope, codeForStatus };
1342
+ export type { BodyDepthOptions, BuildErrorEnvelopeInput, BymaxCoreModuleOptions, EnvelopeOptions, ErrorDetails, ErrorEnvelope, FilterErrorContext, HealthOptions, ICorrelationIdProvider, ITimingSink, ITraceContextProvider, IUnexpectedErrorSink, MetricsOptions, OpenApiHttpMethod, OpenApiOperationIdFactory, OpenApiOperationKey, OpenApiOptions, OpenApiSecurityRequirement, OpenApiSecurityScheme, OpenApiServerDescriptor, OperationSecurityMap, RequestTimingSample, ResolvedCoreOptions, TelemetryOptions, TimingOptions, TraceContext, UnexpectedErrorReport };
package/dist/index.d.ts CHANGED
@@ -22,6 +22,25 @@ interface TimingOptions {
22
22
  /** Samples above this threshold are flagged as slow. No default (unset). */
23
23
  slowRequestThresholdMs?: number;
24
24
  }
25
+ /** Request-body nesting limit, applied before the application's own validation runs. */
26
+ interface BodyDepthOptions {
27
+ /**
28
+ * Refuse a parsed request body nested deeper than {@link BodyDepthOptions.maxDepth} with a
29
+ * `400` through the error envelope. Default: `false`.
30
+ *
31
+ * Off by default because turning it on rejects bodies that are accepted today. Enable it:
32
+ * a body of a few thousand nested arrays is only a few kilobytes, and a recursive validator
33
+ * or transformer overflows the stack on it, so the request is answered `500`, with no
34
+ * credential needed. The check runs before routing, on the body the parser already produced, so it covers the Express adapter and a body parsed by
35
+ * Nest's own parsers; it does not cover Fastify.
36
+ */
37
+ enabled?: boolean;
38
+ /**
39
+ * The deepest nesting a body may have: `[[1]]` is depth 2, a flat object or array is depth 1,
40
+ * and a scalar is depth 0. Must be a positive safe integer. Default: `32`.
41
+ */
42
+ maxDepth?: number;
43
+ }
25
44
  /** Liveness and readiness endpoint configuration. */
26
45
  interface HealthOptions {
27
46
  /** Register the health controller. Default: `true`. */
@@ -267,6 +286,8 @@ interface BymaxCoreModuleOptions {
267
286
  envelope?: EnvelopeOptions;
268
287
  /** Request-timing middleware. Default: enabled. */
269
288
  timing?: TimingOptions;
289
+ /** Request-body nesting limit. Default: disabled. */
290
+ bodyDepth?: BodyDepthOptions;
270
291
  /** Liveness and readiness endpoints. Default: enabled. */
271
292
  health?: HealthOptions;
272
293
  /** Prometheus metrics endpoint. Default: disabled. */
@@ -308,6 +329,11 @@ interface ResolvedTimingOptions {
308
329
  enabled: boolean;
309
330
  slowRequestThresholdMs?: number;
310
331
  }
332
+ /** Fully-resolved body-depth options. */
333
+ interface ResolvedBodyDepthOptions {
334
+ enabled: boolean;
335
+ maxDepth: number;
336
+ }
311
337
  /** Fully-resolved health options. */
312
338
  interface ResolvedHealthOptions {
313
339
  enabled: boolean;
@@ -365,6 +391,7 @@ interface ResolvedOpenApiOptions {
365
391
  interface ResolvedCoreOptions {
366
392
  envelope: ResolvedEnvelopeOptions;
367
393
  timing: ResolvedTimingOptions;
394
+ bodyDepth: ResolvedBodyDepthOptions;
368
395
  health: ResolvedHealthOptions;
369
396
  metrics: ResolvedMetricsOptions;
370
397
  openapi: ResolvedOpenApiOptions;
@@ -1115,6 +1142,21 @@ declare class TimingInterceptor implements NestInterceptor {
1115
1142
  private recordSample;
1116
1143
  }
1117
1144
 
1145
+ /** Refuses a body nested deeper than `bodyDepth.maxDepth` with a coded `400`. */
1146
+ declare class BymaxBodyDepthMiddleware implements NestMiddleware {
1147
+ private readonly options;
1148
+ constructor(options: ResolvedCoreOptions);
1149
+ /**
1150
+ * @param req - The request, whose `body` the parser has already filled.
1151
+ * @param _res - Unused.
1152
+ * @param next - Continues the chain.
1153
+ * @throws BadRequestException When the body is nested too deeply.
1154
+ */
1155
+ use(req: {
1156
+ body?: unknown;
1157
+ }, _res: unknown, next: () => void): void;
1158
+ }
1159
+
1118
1160
  /**
1119
1161
  * @fileoverview Neutral request-info accessor for request timing. Reads the
1120
1162
  * HTTP method and the route template off a request without assuming Express or
@@ -1296,5 +1338,5 @@ declare const BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
1296
1338
  */
1297
1339
  declare function codeForStatus(status: number): string;
1298
1340
 
1299
- export { BYMAX_BAD_GATEWAY, BYMAX_BAD_REQUEST, BYMAX_CLIENT_ERROR, BYMAX_CONFLICT, BYMAX_CORE_OPTIONS, BYMAX_CORRELATION_PROVIDER, BYMAX_FORBIDDEN, BYMAX_GATEWAY_TIMEOUT, BYMAX_HEALTH_INDICATORS, BYMAX_HEALTH_TRANSITION_SINK, BYMAX_INTERNAL_ERROR, BYMAX_METRICS_REGISTRY, BYMAX_NOT_FOUND, BYMAX_NOT_IMPLEMENTED, BYMAX_PAYLOAD_TOO_LARGE, BYMAX_SERVICE_UNAVAILABLE, BYMAX_TIMING_SINK, BYMAX_TOO_MANY_REQUESTS, BYMAX_TRACE_CONTEXT, BYMAX_UNAUTHORIZED, BYMAX_UNEXPECTED_ERROR_SINK, BYMAX_UNPROCESSABLE_ENTITY, BYMAX_UNSUPPORTED_MEDIA_TYPE, BYMAX_VALIDATION_FAILED, BymaxCoreModule, BymaxExceptionFilter, BymaxTimingMiddleware, TimingInterceptor, UNMATCHED_ROUTE, buildErrorEnvelope, codeForStatus };
1300
- export type { BuildErrorEnvelopeInput, BymaxCoreModuleOptions, EnvelopeOptions, ErrorDetails, ErrorEnvelope, FilterErrorContext, HealthOptions, ICorrelationIdProvider, ITimingSink, ITraceContextProvider, IUnexpectedErrorSink, MetricsOptions, OpenApiHttpMethod, OpenApiOperationIdFactory, OpenApiOperationKey, OpenApiOptions, OpenApiSecurityRequirement, OpenApiSecurityScheme, OpenApiServerDescriptor, OperationSecurityMap, RequestTimingSample, ResolvedCoreOptions, TelemetryOptions, TimingOptions, TraceContext, UnexpectedErrorReport };
1341
+ export { BYMAX_BAD_GATEWAY, BYMAX_BAD_REQUEST, BYMAX_CLIENT_ERROR, BYMAX_CONFLICT, BYMAX_CORE_OPTIONS, BYMAX_CORRELATION_PROVIDER, BYMAX_FORBIDDEN, BYMAX_GATEWAY_TIMEOUT, BYMAX_HEALTH_INDICATORS, BYMAX_HEALTH_TRANSITION_SINK, BYMAX_INTERNAL_ERROR, BYMAX_METRICS_REGISTRY, BYMAX_NOT_FOUND, BYMAX_NOT_IMPLEMENTED, BYMAX_PAYLOAD_TOO_LARGE, BYMAX_SERVICE_UNAVAILABLE, BYMAX_TIMING_SINK, BYMAX_TOO_MANY_REQUESTS, BYMAX_TRACE_CONTEXT, BYMAX_UNAUTHORIZED, BYMAX_UNEXPECTED_ERROR_SINK, BYMAX_UNPROCESSABLE_ENTITY, BYMAX_UNSUPPORTED_MEDIA_TYPE, BYMAX_VALIDATION_FAILED, BymaxBodyDepthMiddleware, BymaxCoreModule, BymaxExceptionFilter, BymaxTimingMiddleware, TimingInterceptor, UNMATCHED_ROUTE, buildErrorEnvelope, codeForStatus };
1342
+ export type { BodyDepthOptions, BuildErrorEnvelopeInput, BymaxCoreModuleOptions, EnvelopeOptions, ErrorDetails, ErrorEnvelope, FilterErrorContext, HealthOptions, ICorrelationIdProvider, ITimingSink, ITraceContextProvider, IUnexpectedErrorSink, MetricsOptions, OpenApiHttpMethod, OpenApiOperationIdFactory, OpenApiOperationKey, OpenApiOptions, OpenApiSecurityRequirement, OpenApiSecurityScheme, OpenApiServerDescriptor, OperationSecurityMap, RequestTimingSample, ResolvedCoreOptions, TelemetryOptions, TimingOptions, TraceContext, UnexpectedErrorReport };