@bymax-one/nest-core 1.6.3 → 1.6.4

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 CHANGED
@@ -11,6 +11,78 @@ heading here.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.6.4] - 2026-10-30
15
+
16
+ A 5xx envelope no longer carries `details` unless `envelope.exposeInternals` is on, and the
17
+ message of the router's own 404 no longer carries the query string or the credentials of an
18
+ absolute-form target.
19
+
20
+ **Apply to a derived backend:**
21
+
22
+ - **`details` is absent from a response with status 500 or above, by default.** The filter used to pass the
23
+ `details` of any `HttpException` through verbatim, so a library that put a payload fragment
24
+ there (a cache preview, a query excerpt) published it in the body of a 5xx. The `code` and
25
+ `message` are unchanged, and so is every 4xx, where `details` is the contract. A backend
26
+ that read `details` from a 5xx body should log that context where the error is thrown instead.
27
+ `envelope.exposeInternals` keeps the details, for local debugging only. The filter owns no
28
+ logger and records nothing about them itself.
29
+ - **The router's 404 `message` names the path, not the URL.** For a request that matches no
30
+ route, Nest answers `Cannot GET /orders?token=…` with the raw URL. A test or a client that
31
+ matches that text for a request with a query string now reads `Cannot GET /orders`, and the
32
+ userinfo of an absolute-form target (`GET http://user:pw@host/orders`) is gone from it too. A
33
+ `NotFoundException` the application throws is not touched.
34
+
35
+ ### Security
36
+
37
+ - **The router's 404 `message` no longer carries the query string or credentials.** 1.6.3 took
38
+ the query out of `path`, "where reset codes and tokens travel", but the `message` of Nest's own
39
+ 404 (`Cannot ${method} ${url}`, built in `@nestjs/core` from the adapter's raw URL) still held
40
+ it, and held the userinfo of an absolute-form target as well. Both now read
41
+ `Cannot GET /path`, as `path` does. Only that exact framework message is rewritten, so a
42
+ message the application wrote, and a domain error's message, stay as written.
43
+
44
+ - **A server error no longer publishes its `details` by default.** Both the flat `{ code, message, details }`
45
+ and the nested `{ error: { code, message, details } }` shapes are cut for a status of 500 or
46
+ above, as is a validation-shaped 5xx, because every mapping rule shares one builder.
47
+
48
+ - **Development dependencies lifted past four advisories.** `body-parser` 2.4.0
49
+ (GHSA-27r3-jxmh-p828), `multer` 2.5.0 (GHSA-q7qp-rjg7-q26m) and `qs` 6.16.1
50
+ (GHSA-6pq8-jhm2-2vcw) are now the only resolved copies, through `pnpm`
51
+ overrides. `deepmerge` (GHSA-gcqx-w48m-w2h4) has no patched release and is
52
+ reached only through `jest-config`, so it is ignored in `osv-scanner.toml`
53
+ until 2027-01-27. All of them sit in the development tree: the package
54
+ declares no `dependencies`, so none of them reaches a consumer.
55
+
56
+ ### Changed
57
+
58
+ - Development tooling: `tsup` 9.0.0, `@opentelemetry/context-async-hooks`
59
+ 3.0.0, `@nestjs/common`, `core`, `platform-express`, `platform-fastify` and
60
+ `testing` 11.2.8, `@opentelemetry/api` 1.10.0 and the CodeQL upload action
61
+ 4.39.1. None of them changes the published JavaScript. With `tsup` 9 the
62
+ declaration files list type-only exports in a separate `export type { … }`
63
+ statement instead of inline `type` modifiers; the exported names are the
64
+ same. `SECURITY.md` and `CONTRIBUTING.md` now match the current release line
65
+ and the pinned `pnpm`.
66
+
67
+ ### Documentation
68
+
69
+ - **Support policy.** `SECURITY.md` states that only the latest minor (`1.6.x`)
70
+ receives security fixes, and that earlier versions are not supported and
71
+ get no backports: the guidance is to upgrade.
72
+ - **Issue template contact links.** The "Questions and discussion" link is
73
+ removed from the issue chooser, since the repository has GitHub Discussions
74
+ turned off. The private security contact stays.
75
+ - **Subpath lists.** `SECURITY.md` and both issue templates name the
76
+ subpaths, including `./metrics` and `./openapi`, and `SECURITY.md` calls the
77
+ request-timing component a middleware.
78
+ - **Unknown errors and the logger.** The README no longer says the message and
79
+ stack of an unknown error are "captured for your logger". They go to the
80
+ `BYMAX_UNEXPECTED_ERROR_SINK` seam when one is bound, and reach the response
81
+ only under `envelope.exposeInternals`. It also no longer says the body of that
82
+ 500 carries only the code and the correlation id: it carries the fixed
83
+ envelope fields and the generic message. The `package.json` description calls
84
+ the request-timing component a middleware, as the module registers it.
85
+
14
86
  ## [1.6.3] - 2026-10-23
15
87
 
16
88
  The error envelope's `path` carried the query string, and the documented
@@ -1193,4 +1265,5 @@ have regressed from. They are kept because the reasoning is worth having.
1193
1265
  [1.6.1]: https://github.com/bymaxone/nest-core/compare/v1.6.0...v1.6.1
1194
1266
  [1.6.2]: https://github.com/bymaxone/nest-core/compare/v1.6.1...v1.6.2
1195
1267
  [1.6.3]: https://github.com/bymaxone/nest-core/compare/v1.6.2...v1.6.3
1196
- [Unreleased]: https://github.com/bymaxone/nest-core/compare/v1.6.3...HEAD
1268
+ [1.6.4]: https://github.com/bymaxone/nest-core/compare/v1.6.3...v1.6.4
1269
+ [Unreleased]: https://github.com/bymaxone/nest-core/compare/v1.6.4...HEAD
package/README.md CHANGED
@@ -69,7 +69,9 @@ never loads its peer, which the release gate asserts against the packed tarball.
69
69
  - ✅ **Versioned code catalog** — `BYMAX_NOT_FOUND`, `BYMAX_CONFLICT`, `BYMAX_BAD_GATEWAY`
70
70
  and the rest, exported as constants so a client maps a `code` rather than a message string
71
71
  - ✅ **Internals stay internal** — an unknown error becomes a generic 500; its message and
72
- stack are captured for your logger, and reach the body only under `exposeInternals`
72
+ stack go to the `BYMAX_UNEXPECTED_ERROR_SINK` seam when you bind one, and reach the body
73
+ only under `exposeInternals`. A 5xx
74
+ never carries `details` either, whichever library threw it, unless `exposeInternals` is on
73
75
  - ✅ **Correlation id** — resolved through `BYMAX_CORRELATION_PROVIDER`, so the id comes from
74
76
  wherever your request context already keeps it
75
77
 
@@ -218,10 +220,10 @@ and the classification table: [Production is a closed door](#production-is-a-clo
218
220
 
219
221
  ### `envelope`
220
222
 
221
- | Option | Type | Default | Description |
222
- | ----------------- | --------- | ------- | ---------------------------------------------------------------------------------------------- |
223
- | `enabled` | `boolean` | `true` | Registers the global exception filter. |
224
- | `exposeInternals` | `boolean` | `false` | Includes the original message and stack of unknown errors. Development only, never production. |
223
+ | Option | Type | Default | Description |
224
+ | ----------------- | --------- | ------- | -------------------------------------------------------------------------------------------------------------------------- |
225
+ | `enabled` | `boolean` | `true` | Registers the global exception filter. |
226
+ | `exposeInternals` | `boolean` | `false` | Includes the original message and stack of unknown errors, and the `details` of a 5xx. Development only, never production. |
225
227
 
226
228
  ### `timing`
227
229
 
@@ -557,15 +559,29 @@ follows this exact, versioned shape:
557
559
  }
558
560
  ```
559
561
 
560
- | Field | Type | Presence | Notes |
561
- | --------------- | ----------------- | -------- | -------------------------------------------------- |
562
- | `statusCode` | number | always | HTTP status. |
563
- | `code` | string | always | Stable, machine-readable code. |
564
- | `message` | string | always | Human-readable, safe for end users. |
565
- | `details` | array or object | optional | Structured context, for example validation issues. |
566
- | `correlationId` | string | optional | Present when a correlation provider is bound. |
567
- | `timestamp` | string (ISO 8601) | always | Time the error was formatted. |
568
- | `path` | string | always | Request URL path. |
562
+ | Field | Type | Presence | Notes |
563
+ | --------------- | ----------------- | -------- | -------------------------------------------------------------------------------------------------- |
564
+ | `statusCode` | number | always | HTTP status. |
565
+ | `code` | string | always | Stable, machine-readable code. |
566
+ | `message` | string | always | Human-readable, safe for end users. |
567
+ | `details` | array or object | optional | Structured context, for example validation issues. Absent on a 5xx unless `exposeInternals` is on. |
568
+ | `correlationId` | string | optional | Present when a correlation provider is bound. |
569
+ | `timestamp` | string (ISO 8601) | always | Time the error was formatted. |
570
+ | `path` | string | always | Request URL path, never the query string. The router's own 404 `message` names the same path. |
571
+
572
+ **A server error carries no `details` by default.** For a status of 500 or above, the filter omits `details`
573
+ from the body, whichever library threw the `HttpException` and whether it wrote them flat or
574
+ under `error`; the `code` and `message` stay, because they are text the thrower chose for the
575
+ caller. `envelope.exposeInternals` keeps the details, for local debugging only. The filter owns no
576
+ logger and does not record a 5xx's `details` itself, so whatever throws such an error should log
577
+ that context on the server and must not interpolate internal data into its `message`. Client
578
+ errors are unchanged: their details are the contract. The reasoning follows the OWASP
579
+ [Error Handling Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html)
580
+ (a generic client response, details logged server side), RFC 9457
581
+ [sections 3.1.4 and 5](https://www.rfc-editor.org/rfc/rfc9457.html) (a problem detail helps the client
582
+ correct the problem rather than debug, and no implementation details leave through the HTTP
583
+ interface), and the NestJS [exception filters](https://docs.nestjs.com/exception-filters) behaviour
584
+ of serializing an `HttpException` response object as the whole body.
569
585
 
570
586
  Codes are stable strings under a reserved `BYMAX_` prefix, derived from the
571
587
  HTTP status: `BYMAX_NOT_FOUND` for 404, `BYMAX_VALIDATION_FAILED` for the
@@ -1684,8 +1700,9 @@ disclose.
1684
1700
  ### An error envelope is an exfiltration surface
1685
1701
 
1686
1702
  The filter's job is to make every failure look the same to a client, so an unknown error
1687
- becomes a generic 500 whose body carries the code, the correlation id and nothing else. The
1688
- original message and stack are captured for your logger, not for the response.
1703
+ becomes a generic 500 whose body carries the fixed envelope fields and the generic message,
1704
+ and not the cause unless `exposeInternals` is on. The original message and stack go to the
1705
+ `BYMAX_UNEXPECTED_ERROR_SINK` seam when you bind one.
1689
1706
  `envelope.exposeInternals` puts them in the body and exists for local debugging — its own
1690
1707
  documentation says never to enable it in production, and it defaults to `false`.
1691
1708
 
@@ -1761,7 +1778,7 @@ the deployment nobody configured is the one most likely to be exposed.
1761
1778
  | Layer | Implementation |
1762
1779
  | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1763
1780
  | Error responses | One shape for everything; unknown errors become a generic 500 |
1764
- | Internals | Message and stack captured for logging, in the body only under `exposeInternals` (default `false`) |
1781
+ | Internals | Message and stack handed to the unexpected-error sink when bound, in the body only under `exposeInternals` (default `false`) |
1765
1782
  | Health output | The response names which indicator is down and nothing more; the reason goes to the logger. `exposeIndicatorErrors` (default `false`) puts it back in the response for debugging |
1766
1783
  | Slow indicators | Converted to `down` by the aggregator, so a probe cannot hang on one |
1767
1784
  | Correlation | Resolved through `BYMAX_CORRELATION_PROVIDER` — the app decides where the id comes from |
@@ -1777,8 +1794,9 @@ the deployment nobody configured is the one most likely to be exposed.
1777
1794
 
1778
1795
  > [!IMPORTANT]
1779
1796
  > **`exposeInternals` is a debugging switch, not a verbosity setting.** With it on,
1780
- > the body of a 500 carries the original message and stack of whatever failed —
1781
- > including anything a driver, an SDK or a template put in them.
1797
+ > the body of a 500 carries the original message and stack of whatever failed, and the
1798
+ > `details` of any 5xx `HttpException` — including anything a driver, an SDK or a
1799
+ > template put in them.
1782
1800
 
1783
1801
  ---
1784
1802
 
@@ -94,4 +94,5 @@ interface HealthResponse {
94
94
  checks: HealthCheckEntry[];
95
95
  }
96
96
 
97
- export { BYMAX_HEALTH_INDICATOR_METADATA, BymaxHealthIndicator, type HealthCheckEntry, type HealthIndicatorResult, type HealthResponse, type IHealthIndicator };
97
+ export { BYMAX_HEALTH_INDICATOR_METADATA, BymaxHealthIndicator };
98
+ export type { HealthCheckEntry, HealthIndicatorResult, HealthResponse, IHealthIndicator };
@@ -94,4 +94,5 @@ interface HealthResponse {
94
94
  checks: HealthCheckEntry[];
95
95
  }
96
96
 
97
- export { BYMAX_HEALTH_INDICATOR_METADATA, BymaxHealthIndicator, type HealthCheckEntry, type HealthIndicatorResult, type HealthResponse, type IHealthIndicator };
97
+ export { BYMAX_HEALTH_INDICATOR_METADATA, BymaxHealthIndicator };
98
+ export type { HealthCheckEntry, HealthIndicatorResult, HealthResponse, IHealthIndicator };
package/dist/index.cjs CHANGED
@@ -533,7 +533,7 @@ exports.BymaxExceptionFilter = class BymaxExceptionFilter {
533
533
  */
534
534
  buildEnvelope(exception, context, request) {
535
535
  if (exception instanceof common.HttpException) {
536
- return this.mapHttpException(exception, context);
536
+ return this.mapHttpException(exception, context, request);
537
537
  }
538
538
  const exposed = resolveExposedClientError(exception);
539
539
  if (exposed !== void 0) {
@@ -580,15 +580,16 @@ exports.BymaxExceptionFilter = class BymaxExceptionFilter {
580
580
  }
581
581
  /**
582
582
  * Map an `HttpException` to the envelope. A domain error passes its own code,
583
- * message and details through, whether it wrote them flat on the response or
584
- * nested under `error`; the validation shape becomes `BYMAX_VALIDATION_FAILED`
585
- * with structured details; everything else derives its code from the status.
583
+ * message and details through (a 5xx's details are withheld by default in
584
+ * `toEnvelope`), whether it wrote them flat on the response or nested under
585
+ * `error`; the validation shape becomes `BYMAX_VALIDATION_FAILED` with
586
+ * structured details; everything else derives its code from the status.
586
587
  *
587
588
  * @param exception - The HTTP exception to format.
588
589
  * @param context - The neutral request context.
589
590
  * @returns The formatted envelope.
590
591
  */
591
- mapHttpException(exception, context) {
592
+ mapHttpException(exception, context, request) {
592
593
  const status = exception.getStatus();
593
594
  const response = exception.getResponse();
594
595
  const carrier = resolveErrorCarrier(response);
@@ -610,10 +611,12 @@ exports.BymaxExceptionFilter = class BymaxExceptionFilter {
610
611
  toValidationDetails(response.message)
611
612
  );
612
613
  }
614
+ const message = extractHttpMessage(response, exception);
615
+ const rawUrl = String(this.adapterHost.httpAdapter.getRequestUrl(request));
613
616
  return this.toEnvelope(
614
617
  status,
615
618
  codeForStatus(status),
616
- extractHttpMessage(response, exception),
619
+ message === `Cannot ${context.method} ${rawUrl}` ? `Cannot ${context.method} ${context.path}` : message,
617
620
  context
618
621
  );
619
622
  }
@@ -638,7 +641,8 @@ exports.BymaxExceptionFilter = class BymaxExceptionFilter {
638
641
  }
639
642
  /**
640
643
  * Assemble the envelope through the pure builder, threading the shared clock
641
- * and omitting absent optional details.
644
+ * and omitting absent optional details. A server-error status carries none
645
+ * unless `exposeInternals` is on, whichever mapping rule produced them.
642
646
  *
643
647
  * @param statusCode - HTTP status for the envelope.
644
648
  * @param code - Stable machine-readable code.
@@ -648,13 +652,14 @@ exports.BymaxExceptionFilter = class BymaxExceptionFilter {
648
652
  * @returns The formatted envelope.
649
653
  */
650
654
  toEnvelope(statusCode, code, message, context, details) {
655
+ const withheld = statusCode >= CLIENT_ERROR_MAX2 && !this.options.envelope.exposeInternals;
651
656
  return buildErrorEnvelope({
652
657
  statusCode,
653
658
  code,
654
659
  message,
655
660
  path: context.path,
656
661
  now: this.now,
657
- ...details !== void 0 ? { details } : {},
662
+ ...details !== void 0 && !withheld ? { details } : {},
658
663
  // Stryker disable next-line ConditionalExpression: equivalent — the always-spread form hands the builder `correlationId: undefined`, and `buildErrorEnvelope` re-guards `input.correlationId !== undefined` and omits an undefined value, so the emitted envelope is byte-for-byte identical whether or not a correlation id is present.
659
664
  ...context.correlationId !== void 0 ? { correlationId: context.correlationId } : {},
660
665
  // Gated separately from the context above: the trace id reaches the
package/dist/index.d.cts CHANGED
@@ -10,7 +10,8 @@ interface EnvelopeOptions {
10
10
  enabled?: boolean;
11
11
  /**
12
12
  * Include the original message and stack of unknown errors in the envelope
13
- * details. Never enable in production. Default: `false`.
13
+ * details, and keep the `details` of a 5xx `HttpException`, which are
14
+ * otherwise withheld from the body. Never enable in production. Default: `false`.
14
15
  */
15
16
  exposeInternals?: boolean;
16
17
  }
@@ -839,9 +840,10 @@ declare class BymaxExceptionFilter implements ExceptionFilter {
839
840
  private reportUnexpected;
840
841
  /**
841
842
  * Map an `HttpException` to the envelope. A domain error passes its own code,
842
- * message and details through, whether it wrote them flat on the response or
843
- * nested under `error`; the validation shape becomes `BYMAX_VALIDATION_FAILED`
844
- * with structured details; everything else derives its code from the status.
843
+ * message and details through (a 5xx's details are withheld by default in
844
+ * `toEnvelope`), whether it wrote them flat on the response or nested under
845
+ * `error`; the validation shape becomes `BYMAX_VALIDATION_FAILED` with
846
+ * structured details; everything else derives its code from the status.
845
847
  *
846
848
  * @param exception - The HTTP exception to format.
847
849
  * @param context - The neutral request context.
@@ -860,7 +862,8 @@ declare class BymaxExceptionFilter implements ExceptionFilter {
860
862
  private mapUnknown;
861
863
  /**
862
864
  * Assemble the envelope through the pure builder, threading the shared clock
863
- * and omitting absent optional details.
865
+ * and omitting absent optional details. A server-error status carries none
866
+ * unless `exposeInternals` is on, whichever mapping rule produced them.
864
867
  *
865
868
  * @param statusCode - HTTP status for the envelope.
866
869
  * @param code - Stable machine-readable code.
@@ -1293,4 +1296,5 @@ declare const BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
1293
1296
  */
1294
1297
  declare function codeForStatus(status: number): string;
1295
1298
 
1296
- 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, type BuildErrorEnvelopeInput, BymaxCoreModule, type BymaxCoreModuleOptions, BymaxExceptionFilter, BymaxTimingMiddleware, type EnvelopeOptions, type ErrorDetails, type ErrorEnvelope, type FilterErrorContext, type HealthOptions, type ICorrelationIdProvider, type ITimingSink, type ITraceContextProvider, type IUnexpectedErrorSink, type MetricsOptions, type OpenApiHttpMethod, type OpenApiOperationIdFactory, type OpenApiOperationKey, type OpenApiOptions, type OpenApiSecurityRequirement, type OpenApiSecurityScheme, type OpenApiServerDescriptor, type OperationSecurityMap, type RequestTimingSample, type ResolvedCoreOptions, type TelemetryOptions, TimingInterceptor, type TimingOptions, type TraceContext, UNMATCHED_ROUTE, type UnexpectedErrorReport, buildErrorEnvelope, codeForStatus };
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 };
package/dist/index.d.ts CHANGED
@@ -10,7 +10,8 @@ interface EnvelopeOptions {
10
10
  enabled?: boolean;
11
11
  /**
12
12
  * Include the original message and stack of unknown errors in the envelope
13
- * details. Never enable in production. Default: `false`.
13
+ * details, and keep the `details` of a 5xx `HttpException`, which are
14
+ * otherwise withheld from the body. Never enable in production. Default: `false`.
14
15
  */
15
16
  exposeInternals?: boolean;
16
17
  }
@@ -839,9 +840,10 @@ declare class BymaxExceptionFilter implements ExceptionFilter {
839
840
  private reportUnexpected;
840
841
  /**
841
842
  * Map an `HttpException` to the envelope. A domain error passes its own code,
842
- * message and details through, whether it wrote them flat on the response or
843
- * nested under `error`; the validation shape becomes `BYMAX_VALIDATION_FAILED`
844
- * with structured details; everything else derives its code from the status.
843
+ * message and details through (a 5xx's details are withheld by default in
844
+ * `toEnvelope`), whether it wrote them flat on the response or nested under
845
+ * `error`; the validation shape becomes `BYMAX_VALIDATION_FAILED` with
846
+ * structured details; everything else derives its code from the status.
845
847
  *
846
848
  * @param exception - The HTTP exception to format.
847
849
  * @param context - The neutral request context.
@@ -860,7 +862,8 @@ declare class BymaxExceptionFilter implements ExceptionFilter {
860
862
  private mapUnknown;
861
863
  /**
862
864
  * Assemble the envelope through the pure builder, threading the shared clock
863
- * and omitting absent optional details.
865
+ * and omitting absent optional details. A server-error status carries none
866
+ * unless `exposeInternals` is on, whichever mapping rule produced them.
864
867
  *
865
868
  * @param statusCode - HTTP status for the envelope.
866
869
  * @param code - Stable machine-readable code.
@@ -1293,4 +1296,5 @@ declare const BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
1293
1296
  */
1294
1297
  declare function codeForStatus(status: number): string;
1295
1298
 
1296
- 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, type BuildErrorEnvelopeInput, BymaxCoreModule, type BymaxCoreModuleOptions, BymaxExceptionFilter, BymaxTimingMiddleware, type EnvelopeOptions, type ErrorDetails, type ErrorEnvelope, type FilterErrorContext, type HealthOptions, type ICorrelationIdProvider, type ITimingSink, type ITraceContextProvider, type IUnexpectedErrorSink, type MetricsOptions, type OpenApiHttpMethod, type OpenApiOperationIdFactory, type OpenApiOperationKey, type OpenApiOptions, type OpenApiSecurityRequirement, type OpenApiSecurityScheme, type OpenApiServerDescriptor, type OperationSecurityMap, type RequestTimingSample, type ResolvedCoreOptions, type TelemetryOptions, TimingInterceptor, type TimingOptions, type TraceContext, UNMATCHED_ROUTE, type UnexpectedErrorReport, buildErrorEnvelope, codeForStatus };
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 };
package/dist/index.mjs CHANGED
@@ -531,7 +531,7 @@ var BymaxExceptionFilter = class {
531
531
  */
532
532
  buildEnvelope(exception, context, request) {
533
533
  if (exception instanceof HttpException) {
534
- return this.mapHttpException(exception, context);
534
+ return this.mapHttpException(exception, context, request);
535
535
  }
536
536
  const exposed = resolveExposedClientError(exception);
537
537
  if (exposed !== void 0) {
@@ -578,15 +578,16 @@ var BymaxExceptionFilter = class {
578
578
  }
579
579
  /**
580
580
  * Map an `HttpException` to the envelope. A domain error passes its own code,
581
- * message and details through, whether it wrote them flat on the response or
582
- * nested under `error`; the validation shape becomes `BYMAX_VALIDATION_FAILED`
583
- * with structured details; everything else derives its code from the status.
581
+ * message and details through (a 5xx's details are withheld by default in
582
+ * `toEnvelope`), whether it wrote them flat on the response or nested under
583
+ * `error`; the validation shape becomes `BYMAX_VALIDATION_FAILED` with
584
+ * structured details; everything else derives its code from the status.
584
585
  *
585
586
  * @param exception - The HTTP exception to format.
586
587
  * @param context - The neutral request context.
587
588
  * @returns The formatted envelope.
588
589
  */
589
- mapHttpException(exception, context) {
590
+ mapHttpException(exception, context, request) {
590
591
  const status = exception.getStatus();
591
592
  const response = exception.getResponse();
592
593
  const carrier = resolveErrorCarrier(response);
@@ -608,10 +609,12 @@ var BymaxExceptionFilter = class {
608
609
  toValidationDetails(response.message)
609
610
  );
610
611
  }
612
+ const message = extractHttpMessage(response, exception);
613
+ const rawUrl = String(this.adapterHost.httpAdapter.getRequestUrl(request));
611
614
  return this.toEnvelope(
612
615
  status,
613
616
  codeForStatus(status),
614
- extractHttpMessage(response, exception),
617
+ message === `Cannot ${context.method} ${rawUrl}` ? `Cannot ${context.method} ${context.path}` : message,
615
618
  context
616
619
  );
617
620
  }
@@ -636,7 +639,8 @@ var BymaxExceptionFilter = class {
636
639
  }
637
640
  /**
638
641
  * Assemble the envelope through the pure builder, threading the shared clock
639
- * and omitting absent optional details.
642
+ * and omitting absent optional details. A server-error status carries none
643
+ * unless `exposeInternals` is on, whichever mapping rule produced them.
640
644
  *
641
645
  * @param statusCode - HTTP status for the envelope.
642
646
  * @param code - Stable machine-readable code.
@@ -646,13 +650,14 @@ var BymaxExceptionFilter = class {
646
650
  * @returns The formatted envelope.
647
651
  */
648
652
  toEnvelope(statusCode, code, message, context, details) {
653
+ const withheld = statusCode >= CLIENT_ERROR_MAX2 && !this.options.envelope.exposeInternals;
649
654
  return buildErrorEnvelope({
650
655
  statusCode,
651
656
  code,
652
657
  message,
653
658
  path: context.path,
654
659
  now: this.now,
655
- ...details !== void 0 ? { details } : {},
660
+ ...details !== void 0 && !withheld ? { details } : {},
656
661
  // Stryker disable next-line ConditionalExpression: equivalent — the always-spread form hands the builder `correlationId: undefined`, and `buildErrorEnvelope` re-guards `input.correlationId !== undefined` and omits an undefined value, so the emitted envelope is byte-for-byte identical whether or not a correlation id is present.
657
662
  ...context.correlationId !== void 0 ? { correlationId: context.correlationId } : {},
658
663
  // Gated separately from the context above: the trace id reaches the
@@ -54,4 +54,5 @@ declare const BYMAX_METRICS_CONTRIBUTOR_METADATA = "bymax-one:metrics-contributo
54
54
  */
55
55
  declare function BymaxMetricsContributor(): CustomDecorator<string>;
56
56
 
57
- export { BYMAX_METRICS_CONTRIBUTOR_METADATA, BymaxMetricsContributor, type IMetricsContributor, type MetricsRegistry };
57
+ export { BYMAX_METRICS_CONTRIBUTOR_METADATA, BymaxMetricsContributor };
58
+ export type { IMetricsContributor, MetricsRegistry };
@@ -54,4 +54,5 @@ declare const BYMAX_METRICS_CONTRIBUTOR_METADATA = "bymax-one:metrics-contributo
54
54
  */
55
55
  declare function BymaxMetricsContributor(): CustomDecorator<string>;
56
56
 
57
- export { BYMAX_METRICS_CONTRIBUTOR_METADATA, BymaxMetricsContributor, type IMetricsContributor, type MetricsRegistry };
57
+ export { BYMAX_METRICS_CONTRIBUTOR_METADATA, BymaxMetricsContributor };
58
+ export type { IMetricsContributor, MetricsRegistry };
@@ -240,4 +240,5 @@ interface OpenApiMountOutcome {
240
240
  */
241
241
  declare function applyBymaxOpenApi(app: INestApplication): Promise<OpenApiMountOutcome>;
242
242
 
243
- export { BYMAX_OPENAPI_CONTRACT_VERSION, BYMAX_OPENAPI_CONTRIBUTOR_METADATA, BymaxOpenApiContributor, type DiscardedComponent, type IOpenApiContributor, type OpenApiFragment, type OpenApiFragmentObject, type OpenApiHandlerKey, type OpenApiMountOutcome, type OpenApiSkipReason, applyBymaxOpenApi };
243
+ export { BYMAX_OPENAPI_CONTRACT_VERSION, BYMAX_OPENAPI_CONTRIBUTOR_METADATA, BymaxOpenApiContributor, applyBymaxOpenApi };
244
+ export type { DiscardedComponent, IOpenApiContributor, OpenApiFragment, OpenApiFragmentObject, OpenApiHandlerKey, OpenApiMountOutcome, OpenApiSkipReason };
@@ -240,4 +240,5 @@ interface OpenApiMountOutcome {
240
240
  */
241
241
  declare function applyBymaxOpenApi(app: INestApplication): Promise<OpenApiMountOutcome>;
242
242
 
243
- export { BYMAX_OPENAPI_CONTRACT_VERSION, BYMAX_OPENAPI_CONTRIBUTOR_METADATA, BymaxOpenApiContributor, type DiscardedComponent, type IOpenApiContributor, type OpenApiFragment, type OpenApiFragmentObject, type OpenApiHandlerKey, type OpenApiMountOutcome, type OpenApiSkipReason, applyBymaxOpenApi };
243
+ export { BYMAX_OPENAPI_CONTRACT_VERSION, BYMAX_OPENAPI_CONTRIBUTOR_METADATA, BymaxOpenApiContributor, applyBymaxOpenApi };
244
+ export type { DiscardedComponent, IOpenApiContributor, OpenApiFragment, OpenApiFragmentObject, OpenApiHandlerKey, OpenApiMountOutcome, OpenApiSkipReason };
@@ -175,4 +175,5 @@ declare function normalizeCursorQuery(raw: {
175
175
  */
176
176
  declare function buildCursorResult<T>(items: T[], limit: number, toCursor: (lastItem: T) => Record<string, string | number>): CursorResult<T>;
177
177
 
178
- export { type CursorQuery, type CursorResult, type PageMeta, type PageQuery, type PageResult, buildCursorResult, buildPageResult, decodeCursor, encodeCursor, normalizeCursorQuery, normalizePageQuery };
178
+ export { buildCursorResult, buildPageResult, decodeCursor, encodeCursor, normalizeCursorQuery, normalizePageQuery };
179
+ export type { CursorQuery, CursorResult, PageMeta, PageQuery, PageResult };
@@ -175,4 +175,5 @@ declare function normalizeCursorQuery(raw: {
175
175
  */
176
176
  declare function buildCursorResult<T>(items: T[], limit: number, toCursor: (lastItem: T) => Record<string, string | number>): CursorResult<T>;
177
177
 
178
- export { type CursorQuery, type CursorResult, type PageMeta, type PageQuery, type PageResult, buildCursorResult, buildPageResult, decodeCursor, encodeCursor, normalizeCursorQuery, normalizePageQuery };
178
+ export { buildCursorResult, buildPageResult, decodeCursor, encodeCursor, normalizeCursorQuery, normalizePageQuery };
179
+ export type { CursorQuery, CursorResult, PageMeta, PageQuery, PageResult };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-core",
3
- "version": "1.6.3",
4
- "description": "Zero-dependency NestJS 11 application foundation kit: error-envelope exception filter, request-timing interceptor, pagination helpers, health endpoints with indicator discovery, an optional Prometheus metrics endpoint with a contribution contract, OpenAPI documents in development, and OpenTelemetry trace correlation.",
3
+ "version": "1.6.4",
4
+ "description": "Zero-dependency NestJS 11 application foundation kit: error-envelope exception filter, request-timing middleware, pagination helpers, health endpoints with indicator discovery, an optional Prometheus metrics endpoint with a contribution contract, OpenAPI documents in development, and OpenTelemetry trace correlation.",
5
5
  "author": "Bymax One <support@bymax.one>",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/bymaxone/nest-core#readme",
@@ -140,7 +140,7 @@
140
140
  "@nestjs/swagger": "^11.4.6",
141
141
  "@nestjs/testing": "^11.1.20",
142
142
  "@opentelemetry/api": "^1.9.1",
143
- "@opentelemetry/context-async-hooks": "^2.10.0",
143
+ "@opentelemetry/context-async-hooks": "^3.0.0",
144
144
  "@stryker-mutator/core": "^9",
145
145
  "@stryker-mutator/jest-runner": "^9",
146
146
  "@stryker-mutator/typescript-checker": "^9",
@@ -168,7 +168,7 @@
168
168
  "supertest": "^7.2.2",
169
169
  "ts-jest": "^29.4.9",
170
170
  "ts-node": "^10.9.2",
171
- "tsup": "^8.5.1",
171
+ "tsup": "^9.0.0",
172
172
  "typescript": "^5.9.3"
173
173
  },
174
174
  "keywords": [