@bymax-one/nest-core 1.6.3 → 1.6.5

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,125 @@ heading here.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.6.5] - 2026-10-31
15
+
16
+ A documentation release. The compiled JavaScript is byte-identical to `1.6.4`: what changes in the
17
+ package is the README and comments in the type declarations.
18
+
19
+ **Apply to a derived backend:** `pnpm up @bymax-one/nest-core`. No code change is needed.
20
+
21
+ ### Documentation
22
+
23
+ - **README snippets are corrected so they compile.** The metrics `authToken` example no longer passes
24
+ `string | undefined` where `exactOptionalPropertyTypes` requires `string`; the `environment` example
25
+ reads the validated value with `getOrThrow`; the contributor example imports `Injectable`; and the
26
+ `authOperationSecurity` example is a complete function and a complete `forRoot` call.
27
+ - **The README lists every error-code constant and the OpenAPI contract version.** Each code is in a
28
+ catalog beside the status it maps from, and `BYMAX_OPENAPI_CONTRACT_VERSION` is documented and used
29
+ by the contributor example instead of a literal `1`.
30
+ - **The README says this package mints no request id.** `correlationId` is whatever the bound
31
+ `BYMAX_CORRELATION_PROVIDER` returns, and the id comes from what the application binds, such as the
32
+ `@bymax-one/nest-logger` request-id middleware.
33
+ - **The README explains why `TimingInterceptor` is deprecated.** It records a second sample for every
34
+ request that reaches a handler when wired next to the middleware, and it never saw a request a guard
35
+ rejected.
36
+ - **The `TimingOptions` declarations name the middleware.** `timing.enabled` registers
37
+ `BymaxTimingMiddleware`, not an interceptor.
38
+ - **The README's `RequestTimingSample` snippet lists the two optional trace fields.** The interface
39
+ carries `traceId?` and `spanId?`, present when telemetry is enabled and a span was recording.
40
+
41
+ ### Tests
42
+
43
+ - **`pnpm check:published` compiles every README snippet through the TypeScript API.** Syntax and type
44
+ errors are reported together, snippets that do not import the package are checked too, a relative
45
+ import resolves to the snippet that exports the name, and the names a snippet takes from its prose
46
+ are typed in `scripts/readme-prelude.d.ts`, so a wrong method, option or field fails the gate
47
+ instead of resolving to `any`. An option misspelled in a `forRootAsync` factory fails too.
48
+ - **`pnpm check:published` fails on three more kinds of README defect.** A syntax error in a snippet,
49
+ a prelude import that stopped resolving, and a snippet that restates a shipped interface with a
50
+ member added, dropped or retyped.
51
+ - **The README snippet gate no longer exempts a type error on an imported type or a union.** The
52
+ exemption for an abbreviated local class now reads only the snippet's own declarations, accepts a
53
+ plain identifier only, and handles a `$` in a class name.
54
+ - **The README's error-code catalog and contract version are pinned to the code.** A spec fails when an
55
+ exported code constant has no row, a row names a status `codeForStatus` does not map to it, or the
56
+ README states a contract version other than `BYMAX_OPENAPI_CONTRACT_VERSION`.
57
+ - **The README's option tables are pinned to the resolved defaults.** A spec parses every table under
58
+ the Configuration section and fails when a documented default, or a documented option, differs from
59
+ `DEFAULT_CORE_OPTIONS`, or when a resolved option has no row.
60
+
61
+ ## [1.6.4] - 2026-10-30
62
+
63
+ A 5xx envelope no longer carries `details` unless `envelope.exposeInternals` is on, and the
64
+ message of the router's own 404 no longer carries the query string or the credentials of an
65
+ absolute-form target.
66
+
67
+ **Apply to a derived backend:**
68
+
69
+ - **`details` is absent from a response with status 500 or above, by default.** The filter used to pass the
70
+ `details` of any `HttpException` through verbatim, so a library that put a payload fragment
71
+ there (a cache preview, a query excerpt) published it in the body of a 5xx. The `code` and
72
+ `message` are unchanged, and so is every 4xx, where `details` is the contract. A backend
73
+ that read `details` from a 5xx body should log that context where the error is thrown instead.
74
+ `envelope.exposeInternals` keeps the details, for local debugging only. The filter owns no
75
+ logger and records nothing about them itself.
76
+ - **The router's 404 `message` names the path, not the URL.** For a request that matches no
77
+ route, Nest answers `Cannot GET /orders?token=…` with the raw URL. A test or a client that
78
+ matches that text for a request with a query string now reads `Cannot GET /orders`, and the
79
+ userinfo of an absolute-form target (`GET http://user:pw@host/orders`) is gone from it too. A
80
+ `NotFoundException` the application throws is not touched.
81
+
82
+ ### Security
83
+
84
+ - **The router's 404 `message` no longer carries the query string or credentials.** 1.6.3 took
85
+ the query out of `path`, "where reset codes and tokens travel", but the `message` of Nest's own
86
+ 404 (`Cannot ${method} ${url}`, built in `@nestjs/core` from the adapter's raw URL) still held
87
+ it, and held the userinfo of an absolute-form target as well. Both now read
88
+ `Cannot GET /path`, as `path` does. Only that exact framework message is rewritten, so a
89
+ message the application wrote, and a domain error's message, stay as written.
90
+
91
+ - **A server error no longer publishes its `details` by default.** Both the flat `{ code, message, details }`
92
+ and the nested `{ error: { code, message, details } }` shapes are cut for a status of 500 or
93
+ above, as is a validation-shaped 5xx, because every mapping rule shares one builder.
94
+
95
+ - **Development dependencies lifted past four advisories.** `body-parser` 2.4.0
96
+ (GHSA-27r3-jxmh-p828), `multer` 2.5.0 (GHSA-q7qp-rjg7-q26m) and `qs` 6.16.1
97
+ (GHSA-6pq8-jhm2-2vcw) are now the only resolved copies, through `pnpm`
98
+ overrides. `deepmerge` (GHSA-gcqx-w48m-w2h4) has no patched release and is
99
+ reached only through `jest-config`, so it is ignored in `osv-scanner.toml`
100
+ until 2027-01-27. All of them sit in the development tree: the package
101
+ declares no `dependencies`, so none of them reaches a consumer.
102
+
103
+ ### Changed
104
+
105
+ - Development tooling: `tsup` 9.0.0, `@opentelemetry/context-async-hooks`
106
+ 3.0.0, `@nestjs/common`, `core`, `platform-express`, `platform-fastify` and
107
+ `testing` 11.2.8, `@opentelemetry/api` 1.10.0 and the CodeQL upload action
108
+ 4.39.1. None of them changes the published JavaScript. With `tsup` 9 the
109
+ declaration files list type-only exports in a separate `export type { … }`
110
+ statement instead of inline `type` modifiers; the exported names are the
111
+ same. `SECURITY.md` and `CONTRIBUTING.md` now match the current release line
112
+ and the pinned `pnpm`.
113
+
114
+ ### Documentation
115
+
116
+ - **Support policy.** `SECURITY.md` states that only the latest minor (`1.6.x`)
117
+ receives security fixes, and that earlier versions are not supported and
118
+ get no backports: the guidance is to upgrade.
119
+ - **Issue template contact links.** The "Questions and discussion" link is
120
+ removed from the issue chooser, since the repository has GitHub Discussions
121
+ turned off. The private security contact stays.
122
+ - **Subpath lists.** `SECURITY.md` and both issue templates name the
123
+ subpaths, including `./metrics` and `./openapi`, and `SECURITY.md` calls the
124
+ request-timing component a middleware.
125
+ - **Unknown errors and the logger.** The README no longer says the message and
126
+ stack of an unknown error are "captured for your logger". They go to the
127
+ `BYMAX_UNEXPECTED_ERROR_SINK` seam when one is bound, and reach the response
128
+ only under `envelope.exposeInternals`. It also no longer says the body of that
129
+ 500 carries only the code and the correlation id: it carries the fixed
130
+ envelope fields and the generic message. The `package.json` description calls
131
+ the request-timing component a middleware, as the module registers it.
132
+
14
133
  ## [1.6.3] - 2026-10-23
15
134
 
16
135
  The error envelope's `path` carried the query string, and the documented
@@ -1193,4 +1312,6 @@ have regressed from. They are kept because the reasoning is worth having.
1193
1312
  [1.6.1]: https://github.com/bymaxone/nest-core/compare/v1.6.0...v1.6.1
1194
1313
  [1.6.2]: https://github.com/bymaxone/nest-core/compare/v1.6.1...v1.6.2
1195
1314
  [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
1315
+ [1.6.4]: https://github.com/bymaxone/nest-core/compare/v1.6.3...v1.6.4
1316
+ [1.6.5]: https://github.com/bymaxone/nest-core/compare/v1.6.4...v1.6.5
1317
+ [Unreleased]: https://github.com/bymaxone/nest-core/compare/v1.6.5...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
 
@@ -271,8 +273,9 @@ is rejected at boot rather than silently ignored, so a mistyped secret fails lou
271
273
  instead of leaving the endpoint open:
272
274
 
273
275
  ```ts
276
+ const token = process.env.METRICS_TOKEN
274
277
  BymaxCoreModule.forRoot({
275
- metrics: { enabled: true, authToken: process.env.METRICS_TOKEN }
278
+ metrics: { enabled: true, ...(token === undefined ? {} : { authToken: token }) }
276
279
  })
277
280
  // Scrape: curl -H "Authorization: Bearer $METRICS_TOKEN" http://host/metrics
278
281
  ```
@@ -354,10 +357,14 @@ place that knows it:
354
357
 
355
358
  ```ts
356
359
  // in the library
357
- export function authOperationSecurity(prefix = ''): OperationSecurityMap { /* … */ }
360
+ export function authOperationSecurity(prefix = ''): OperationSecurityMap {
361
+ return {} /* … */
362
+ }
358
363
 
359
364
  // in the application
360
- operationSecurity: { ...authOperationSecurity('api'), ...myOwnOverrides }
365
+ BymaxCoreModule.forRoot({
366
+ openapi: { operationSecurity: { ...authOperationSecurity('api'), ...myOwnOverrides } }
367
+ })
361
368
  ```
362
369
 
363
370
  A requirement naming a scheme that is not declared **fails the document build**
@@ -557,15 +564,36 @@ follows this exact, versioned shape:
557
564
  }
558
565
  ```
559
566
 
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. |
567
+ | Field | Type | Presence | Notes |
568
+ | --------------- | ----------------- | -------- | -------------------------------------------------------------------------------------------------- |
569
+ | `statusCode` | number | always | HTTP status. |
570
+ | `code` | string | always | Stable, machine-readable code. |
571
+ | `message` | string | always | Human-readable, safe for end users. |
572
+ | `details` | array or object | optional | Structured context, for example validation issues. Absent on a 5xx unless `exposeInternals` is on. |
573
+ | `correlationId` | string | optional | Present when a correlation provider is bound. |
574
+ | `timestamp` | string (ISO 8601) | always | Time the error was formatted. |
575
+ | `path` | string | always | Request URL path, never the query string. The router's own 404 `message` names the same path. |
576
+
577
+ **This package mints no request id.** `correlationId` is whatever the bound
578
+ `ICorrelationIdProvider` (`BYMAX_CORRELATION_PROVIDER`) returns: the filter only
579
+ reads it, and with no provider bound the field is absent. The id has to come from
580
+ something you bind. With `@bymax-one/nest-logger` that is its request-id
581
+ middleware; see
582
+ [Integration with `@bymax-one/nest-logger`](#-integration-with-bymax-onenest-logger).
583
+
584
+ **A server error carries no `details` by default.** For a status of 500 or above, the filter omits `details`
585
+ from the body, whichever library threw the `HttpException` and whether it wrote them flat or
586
+ under `error`; the `code` and `message` stay, because they are text the thrower chose for the
587
+ caller. `envelope.exposeInternals` keeps the details, for local debugging only. The filter owns no
588
+ logger and does not record a 5xx's `details` itself, so whatever throws such an error should log
589
+ that context on the server and must not interpolate internal data into its `message`. Client
590
+ errors are unchanged: their details are the contract. The reasoning follows the OWASP
591
+ [Error Handling Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html)
592
+ (a generic client response, details logged server side), RFC 9457
593
+ [sections 3.1.4 and 5](https://www.rfc-editor.org/rfc/rfc9457.html) (a problem detail helps the client
594
+ correct the problem rather than debug, and no implementation details leave through the HTTP
595
+ interface), and the NestJS [exception filters](https://docs.nestjs.com/exception-filters) behaviour
596
+ of serializing an `HttpException` response object as the whole body.
569
597
 
570
598
  Codes are stable strings under a reserved `BYMAX_` prefix, derived from the
571
599
  HTTP status: `BYMAX_NOT_FOUND` for 404, `BYMAX_VALIDATION_FAILED` for the
@@ -579,6 +607,29 @@ import { BadRequestException } from '@nestjs/common'
579
607
  throw new BadRequestException({ code: 'INVOICE_OVERDUE', message: 'Invoice is overdue' })
580
608
  ```
581
609
 
610
+ Every code the package assigns is exported from `@bymax-one/nest-core` as a constant:
611
+
612
+ | Code | Status | When |
613
+ | ------------------------------ | --------- | -------------------------------------------------------- |
614
+ | `BYMAX_BAD_REQUEST` | 400 | A bad request that is not a validation failure. |
615
+ | `BYMAX_VALIDATION_FAILED` | 400 | The shape a validation pipe produces. |
616
+ | `BYMAX_UNAUTHORIZED` | 401 | No or invalid credentials. |
617
+ | `BYMAX_FORBIDDEN` | 403 | Authenticated, but not allowed. |
618
+ | `BYMAX_NOT_FOUND` | 404 | Unknown resource, including a route no handler matched. |
619
+ | `BYMAX_CONFLICT` | 409 | A state conflict. |
620
+ | `BYMAX_PAYLOAD_TOO_LARGE` | 413 | A body over the size limit. |
621
+ | `BYMAX_UNSUPPORTED_MEDIA_TYPE` | 415 | A content type the route does not accept. |
622
+ | `BYMAX_UNPROCESSABLE_ENTITY` | 422 | A well-formed body that is semantically invalid. |
623
+ | `BYMAX_TOO_MANY_REQUESTS` | 429 | A rate limit. |
624
+ | `BYMAX_CLIENT_ERROR` | other 4xx | A client error with no code of its own. |
625
+ | `BYMAX_INTERNAL_ERROR` | 500 | An unknown error, and any non-4xx status without a code. |
626
+ | `BYMAX_NOT_IMPLEMENTED` | 501 | A handler that is not implemented. |
627
+ | `BYMAX_BAD_GATEWAY` | 502 | A failure of an upstream service. |
628
+ | `BYMAX_SERVICE_UNAVAILABLE` | 503 | Temporary unavailability. |
629
+ | `BYMAX_GATEWAY_TIMEOUT` | 504 | An upstream service that did not answer in time. |
630
+
631
+ `codeForStatus(status)` returns the code for a status.
632
+
582
633
  ### What the filter classifies from, and what it cannot
583
634
 
584
635
  An error raised before any handler ran — a malformed JSON body, a payload over
@@ -737,9 +788,17 @@ export interface RequestTimingSample {
737
788
  statusCode: number
738
789
  durationMs: number
739
790
  slow: boolean
791
+ traceId?: string
792
+ spanId?: string
740
793
  }
741
794
  ```
742
795
 
796
+ `TimingInterceptor` is still exported and is **deprecated**: nothing registers it. It
797
+ was the recorder until `BymaxTimingMiddleware` replaced it, because guards run
798
+ before interceptors and a request a guard rejected never reached it. Registering it
799
+ as well records a second sample for every request that reaches a handler, so leave
800
+ it out.
801
+
743
802
  ### Rejected requests are counted too
744
803
 
745
804
  The recorder is middleware (`BymaxTimingMiddleware`), applied to every route by
@@ -1280,7 +1339,7 @@ BymaxCoreModule.forRootAsync({
1280
1339
  inject: [ConfigService],
1281
1340
  useFactory: (config: ConfigService) => ({
1282
1341
  // Your validated value, not a second copy of NODE_ENV.
1283
- environment: config.get('APP_ENV'),
1342
+ environment: config.getOrThrow<string>('APP_ENV'),
1284
1343
  openapi: { enabled: true }
1285
1344
  })
1286
1345
  })
@@ -1383,7 +1442,11 @@ So a library marks a provider and returns fragments keyed by **handler
1383
1442
  identity**, which survives every prefix, version and mount point:
1384
1443
 
1385
1444
  ```ts
1386
- import { BymaxOpenApiContributor } from '@bymax-one/nest-core/openapi'
1445
+ import { Injectable } from '@nestjs/common'
1446
+ import {
1447
+ BymaxOpenApiContributor,
1448
+ BYMAX_OPENAPI_CONTRACT_VERSION
1449
+ } from '@bymax-one/nest-core/openapi'
1387
1450
  import type { IOpenApiContributor, OpenApiFragment } from '@bymax-one/nest-core/openapi'
1388
1451
 
1389
1452
  @BymaxOpenApiContributor()
@@ -1396,7 +1459,7 @@ export class AuthOpenApi implements IOpenApiContributor {
1396
1459
  // Required, and declared rather than inferred: a fragment crosses a
1397
1460
  // boundary between independently released packages, where each side
1398
1461
  // type-checked against its own copy.
1399
- contractVersion: 1,
1462
+ contractVersion: BYMAX_OPENAPI_CONTRACT_VERSION,
1400
1463
  components: {
1401
1464
  securitySchemes: {
1402
1465
  // Derived from resolved options — which is why this cannot be a
@@ -1425,14 +1488,15 @@ library that is never imported contributes nothing.
1425
1488
 
1426
1489
  #### What the merge guarantees
1427
1490
 
1428
- | Rule | Behavior |
1429
- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1430
- | Marked, not shaped | Only providers carrying the marker are called. A class that merely exposes `contributeOpenApi` is never touched. |
1431
- | Called once | While the document is built, after options resolve — so a contributor may derive its contribution from its own configuration. |
1432
- | Stable order | Contributors run sorted by class name, so two libraries describing the same operation resolve the same way on every boot. |
1433
- | Data, not mutation | A contributor returns fragments; this package decides what to write. That is what makes precedence enforceable. |
1434
- | Named failures | A marked class that cannot contribute, one that throws, or a fragment addressing a handler the application does not have all fail the document build naming the contributor. |
1435
- | Off with the document | With `openapi.enabled` false, no contributor runs. |
1491
+ | Rule | Behavior |
1492
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1493
+ | Marked, not shaped | Only providers carrying the marker are called. A class that merely exposes `contributeOpenApi` is never touched. |
1494
+ | Called once | While the document is built, after options resolve — so a contributor may derive its contribution from its own configuration. |
1495
+ | Stable order | Contributors run sorted by class name, so two libraries describing the same operation resolve the same way on every boot. |
1496
+ | Data, not mutation | A contributor returns fragments; this package decides what to write. That is what makes precedence enforceable. |
1497
+ | Named failures | A marked class that cannot contribute, one that throws, or a fragment addressing a handler the application does not have all fail the document build naming the contributor. |
1498
+ | Versioned contract | A fragment declares `contractVersion` as `BYMAX_OPENAPI_CONTRACT_VERSION`, currently `1`. A fragment declaring another version fails the document build, naming both versions. |
1499
+ | Off with the document | With `openapi.enabled` false, no contributor runs. |
1436
1500
 
1437
1501
  **Precedence, weakest first:** what this package infers about its own routes,
1438
1502
  then what a library contributed, then what the consumer configured, and above
@@ -1684,8 +1748,9 @@ disclose.
1684
1748
  ### An error envelope is an exfiltration surface
1685
1749
 
1686
1750
  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.
1751
+ becomes a generic 500 whose body carries the fixed envelope fields and the generic message,
1752
+ and not the cause unless `exposeInternals` is on. The original message and stack go to the
1753
+ `BYMAX_UNEXPECTED_ERROR_SINK` seam when you bind one.
1689
1754
  `envelope.exposeInternals` puts them in the body and exists for local debugging — its own
1690
1755
  documentation says never to enable it in production, and it defaults to `false`.
1691
1756
 
@@ -1761,7 +1826,7 @@ the deployment nobody configured is the one most likely to be exposed.
1761
1826
  | Layer | Implementation |
1762
1827
  | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1763
1828
  | 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`) |
1829
+ | Internals | Message and stack handed to the unexpected-error sink when bound, in the body only under `exposeInternals` (default `false`) |
1765
1830
  | 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
1831
  | Slow indicators | Converted to `down` by the aggregator, so a probe cannot hang on one |
1767
1832
  | Correlation | Resolved through `BYMAX_CORRELATION_PROVIDER` — the app decides where the id comes from |
@@ -1777,8 +1842,9 @@ the deployment nobody configured is the one most likely to be exposed.
1777
1842
 
1778
1843
  > [!IMPORTANT]
1779
1844
  > **`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.
1845
+ > the body of a 500 carries the original message and stack of whatever failed, and the
1846
+ > `details` of any 5xx `HttpException` — including anything a driver, an SDK or a
1847
+ > template put in them.
1782
1848
 
1783
1849
  ---
1784
1850
 
@@ -1837,27 +1903,27 @@ in the sections above.
1837
1903
 
1838
1904
  ### `.` (root)
1839
1905
 
1840
- | Export | Kind | Description |
1841
- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | --------------------------------------------------------------------------------------------------------------------------------------- |
1842
- | `BymaxCoreModule` | class | The dynamic module: `forRoot` and `forRootAsync`. |
1843
- | `BymaxCoreModuleOptions`, `EnvelopeOptions`, `TimingOptions`, `HealthOptions`, `MetricsOptions`, `TelemetryOptions`, `OpenApiOptions`, `OpenApiServerDescriptor`, `OpenApiSecurityScheme`, `ResolvedCoreOptions` | types | The options surface and its resolved shape. |
1844
- | `OpenApiSecurityRequirement`, `OpenApiHttpMethod`, `OpenApiOperationKey`, `OperationSecurityMap` | types | The operation-key contract a sibling library targets to ship its own security map. |
1845
- | `OpenApiOperationIdFactory` | type | Names the operations in the generated document. |
1846
- | `BYMAX_CORE_OPTIONS`, `BYMAX_CORRELATION_PROVIDER`, `BYMAX_TIMING_SINK`, `BYMAX_HEALTH_INDICATORS`, `BYMAX_HEALTH_TRANSITION_SINK`, `BYMAX_UNEXPECTED_ERROR_SINK`, `BYMAX_METRICS_REGISTRY`, `BYMAX_TRACE_CONTEXT` | tokens | The DI tokens; see the [token table](#-di-tokens). |
1847
- | `IHealthTransitionSink`, `HealthTransition`, `HealthTransitionCause` | types | The readiness-transition contract; also on `./health`. |
1848
- | `ICorrelationIdProvider` | type | The correlation-provider contract. |
1849
- | `ITraceContextProvider`, `TraceContext` | types | The trace-context contract and the identifiers it resolves. |
1850
- | `BymaxExceptionFilter` | class | The envelope exception filter. |
1851
- | `FilterErrorContext` | type | The neutral request context passed to the filter's observability seam. |
1852
- | `IUnexpectedErrorSink`, `UnexpectedErrorReport` | types | Receives the cause behind each collapsed `500 BYMAX_INTERNAL_ERROR`; see [Recording the cause of a 500](#recording-the-cause-of-a-500). |
1853
- | `buildErrorEnvelope` | function | Pure builder assembling an `ErrorEnvelope`. |
1854
- | `ErrorEnvelope`, `ErrorDetails`, `BuildErrorEnvelopeInput` | types | The envelope contract and its builder input. |
1855
- | `BymaxTimingMiddleware` | class | The request-timing middleware, applied to every route when timing is enabled. |
1856
- | `UNMATCHED_ROUTE` | constant | The bounded label recorded when a request matched no route (`<unmatched>`). |
1857
- | `TimingInterceptor` | class | Deprecated: superseded by `BymaxTimingMiddleware`, which the module registers. |
1858
- | `ITimingSink`, `RequestTimingSample` | types | The timing-sink contract and its sample shape. |
1859
- | `BYMAX_BAD_GATEWAY` … `BYMAX_VALIDATION_FAILED` | constants | The full error-code catalog (see [Error envelope](#-error-envelope)). |
1860
- | `codeForStatus` | function | Derives a catalog code from an HTTP status. |
1906
+ | Export | Kind | Description |
1907
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1908
+ | `BymaxCoreModule` | class | The dynamic module: `forRoot` and `forRootAsync`. |
1909
+ | `BymaxCoreModuleOptions`, `EnvelopeOptions`, `TimingOptions`, `HealthOptions`, `MetricsOptions`, `TelemetryOptions`, `OpenApiOptions`, `OpenApiServerDescriptor`, `OpenApiSecurityScheme`, `ResolvedCoreOptions` | types | The options surface and its resolved shape. |
1910
+ | `OpenApiSecurityRequirement`, `OpenApiHttpMethod`, `OpenApiOperationKey`, `OperationSecurityMap` | types | The operation-key contract a sibling library targets to ship its own security map. |
1911
+ | `OpenApiOperationIdFactory` | type | Names the operations in the generated document. |
1912
+ | `BYMAX_CORE_OPTIONS`, `BYMAX_CORRELATION_PROVIDER`, `BYMAX_TIMING_SINK`, `BYMAX_HEALTH_INDICATORS`, `BYMAX_HEALTH_TRANSITION_SINK`, `BYMAX_UNEXPECTED_ERROR_SINK`, `BYMAX_METRICS_REGISTRY`, `BYMAX_TRACE_CONTEXT` | tokens | The DI tokens; see the [token table](#-di-tokens). |
1913
+ | `IHealthTransitionSink`, `HealthTransition`, `HealthTransitionCause` | types | The readiness-transition contract; also on `./health`. |
1914
+ | `ICorrelationIdProvider` | type | The correlation-provider contract. |
1915
+ | `ITraceContextProvider`, `TraceContext` | types | The trace-context contract and the identifiers it resolves. |
1916
+ | `BymaxExceptionFilter` | class | The envelope exception filter. |
1917
+ | `FilterErrorContext` | type | The neutral request context passed to the filter's observability seam. |
1918
+ | `IUnexpectedErrorSink`, `UnexpectedErrorReport` | types | Receives the cause behind each collapsed `500 BYMAX_INTERNAL_ERROR`; see [Recording the cause of a 500](#recording-the-cause-of-a-500). |
1919
+ | `buildErrorEnvelope` | function | Pure builder assembling an `ErrorEnvelope`. |
1920
+ | `ErrorEnvelope`, `ErrorDetails`, `BuildErrorEnvelopeInput` | types | The envelope contract and its builder input. |
1921
+ | `BymaxTimingMiddleware` | class | The request-timing middleware, applied to every route when timing is enabled. |
1922
+ | `UNMATCHED_ROUTE` | constant | The bounded label recorded when a request matched no route (`<unmatched>`). |
1923
+ | `TimingInterceptor` | class | **Deprecated, registered nowhere.** `BymaxTimingMiddleware` replaced it as the recorder because guards run before interceptors, so it never saw a request a guard rejected; wiring it by hand next to the middleware records a second sample for every request that reaches a handler. Kept so code that wired it by hand keeps compiling. |
1924
+ | `ITimingSink`, `RequestTimingSample` | types | The timing-sink contract and its sample shape. |
1925
+ | `BYMAX_BAD_GATEWAY` … `BYMAX_VALIDATION_FAILED` | constants | The full error-code catalog (see [Error envelope](#-error-envelope)). |
1926
+ | `codeForStatus` | function | Derives a catalog code from an HTTP status. |
1861
1927
 
1862
1928
  ### `./pagination`
1863
1929
 
@@ -1893,14 +1959,15 @@ in the sections above.
1893
1959
 
1894
1960
  ### `./openapi`
1895
1961
 
1896
- | Export | Kind | Description |
1897
- | -------------------------------------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------ |
1898
- | `applyBymaxOpenApi` | function | Builds and mounts the document; call it before `app.listen()`. |
1899
- | `OpenApiMountOutcome` | type | What the helper did: mounted at a path, or skipped with a reason. |
1900
- | `DiscardedComponent` | type | A contributed component entry the document already declared. |
1901
- | `OpenApiSkipReason` | type | Why it was skipped: `'disabled'` or `'production'`. |
1902
- | `BymaxOpenApiContributor`, `BYMAX_OPENAPI_CONTRIBUTOR_METADATA` | decorator, constant | Marks a provider as describing its own routes, and the metadata key behind it. |
1903
- | `IOpenApiContributor`, `OpenApiFragment`, `OpenApiFragmentObject`, `OpenApiHandlerKey` | types | The contributor contract and the shape of what it returns. |
1962
+ | Export | Kind | Description |
1963
+ | -------------------------------------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------- |
1964
+ | `applyBymaxOpenApi` | function | Builds and mounts the document; call it before `app.listen()`. |
1965
+ | `OpenApiMountOutcome` | type | What the helper did: mounted at a path, or skipped with a reason. |
1966
+ | `DiscardedComponent` | type | A contributed component entry the document already declared. |
1967
+ | `OpenApiSkipReason` | type | Why it was skipped: `'disabled'` or `'production'`. |
1968
+ | `BymaxOpenApiContributor`, `BYMAX_OPENAPI_CONTRIBUTOR_METADATA` | decorator, constant | Marks a provider as describing its own routes, and the metadata key behind it. |
1969
+ | `BYMAX_OPENAPI_CONTRACT_VERSION` | constant | The version of the fragment contract, currently `1`; a fragment declares it as `contractVersion`. |
1970
+ | `IOpenApiContributor`, `OpenApiFragment`, `OpenApiFragmentObject`, `OpenApiHandlerKey` | types | The contributor contract and the shape of what it returns. |
1904
1971
 
1905
1972
  ## 🧩 Compatibility
1906
1973
 
@@ -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,13 +10,14 @@ 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
  }
17
- /** Request-timing interceptor configuration. */
18
+ /** Request-timing middleware configuration. */
18
19
  interface TimingOptions {
19
- /** Register the timing interceptor. Default: `true`. */
20
+ /** Register the request-timing middleware (`BymaxTimingMiddleware`). Default: `true`. */
20
21
  enabled?: boolean;
21
22
  /** Samples above this threshold are flagged as slow. No default (unset). */
22
23
  slowRequestThresholdMs?: number;
@@ -264,7 +265,7 @@ interface MetricsOptions {
264
265
  interface BymaxCoreModuleOptions {
265
266
  /** Error-envelope exception filter. Default: enabled. */
266
267
  envelope?: EnvelopeOptions;
267
- /** Request timing interceptor. Default: enabled. */
268
+ /** Request-timing middleware. Default: enabled. */
268
269
  timing?: TimingOptions;
269
270
  /** Liveness and readiness endpoints. Default: enabled. */
270
271
  health?: HealthOptions;
@@ -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,13 +10,14 @@ 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
  }
17
- /** Request-timing interceptor configuration. */
18
+ /** Request-timing middleware configuration. */
18
19
  interface TimingOptions {
19
- /** Register the timing interceptor. Default: `true`. */
20
+ /** Register the request-timing middleware (`BymaxTimingMiddleware`). Default: `true`. */
20
21
  enabled?: boolean;
21
22
  /** Samples above this threshold are flagged as slow. No default (unset). */
22
23
  slowRequestThresholdMs?: number;
@@ -264,7 +265,7 @@ interface MetricsOptions {
264
265
  interface BymaxCoreModuleOptions {
265
266
  /** Error-envelope exception filter. Default: enabled. */
266
267
  envelope?: EnvelopeOptions;
267
- /** Request timing interceptor. Default: enabled. */
268
+ /** Request-timing middleware. Default: enabled. */
268
269
  timing?: TimingOptions;
269
270
  /** Liveness and readiness endpoints. Default: enabled. */
270
271
  health?: HealthOptions;
@@ -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.5",
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": [