@bymax-one/nest-core 1.6.1 → 1.6.3
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 +107 -1
- package/README.md +132 -33
- package/dist/index.cjs +107 -40
- package/dist/index.d.cts +119 -5
- package/dist/index.d.ts +119 -5
- package/dist/index.mjs +107 -41
- package/dist/openapi/index.cjs +8 -9
- package/dist/openapi/index.mjs +8 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,110 @@ heading here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.6.3] - 2026-10-23
|
|
15
|
+
|
|
16
|
+
The error envelope's `path` carried the query string, and the documented
|
|
17
|
+
recipe for pairing with `@bymax-one/nest-logger` bound nothing.
|
|
18
|
+
|
|
19
|
+
**Apply to a derived backend:**
|
|
20
|
+
|
|
21
|
+
- **`path` changes in the response body.** An application that reads `path`
|
|
22
|
+
from the envelope now gets the request path alone. `/invoices/42?token=x`
|
|
23
|
+
and `http://host/invoices/42` both now read `/invoices/42`. The query string was never part of the documented
|
|
24
|
+
contract, and is where reset codes and tokens travel.
|
|
25
|
+
- **The nest-logger correlation recipe changes.** An application that
|
|
26
|
+
followed the previous README recipe bound
|
|
27
|
+
`BYMAX_CORRELATION_PROVIDER` to `LogContextService` with `useExisting`, and
|
|
28
|
+
got no correlation id at all. Replace it with the `useFactory` in the
|
|
29
|
+
README's "Integration with `@bymax-one/nest-logger`", and make sure
|
|
30
|
+
nest-logger's request-id middleware is mounted (`applyAccessLog(app)` or
|
|
31
|
+
`applyRequestIdMiddleware(consumer)`). Without it there is no id to read.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **The envelope `path` no longer carries the query string.** Both adapters
|
|
36
|
+
report the original URL, query included: Express returns `originalUrl`, and
|
|
37
|
+
Fastify the raw URL. The filter copied that into `path`, which is documented
|
|
38
|
+
as the request URL path. So a query-string secret reached the client and any
|
|
39
|
+
log that stores the envelope. An absolute-form target, as a proxy sends it,
|
|
40
|
+
also carried its scheme, authority and any credentials; only the path is
|
|
41
|
+
kept now. The `onUnexpectedError` context now receives the same path. The unexpected-error sink already reported the route
|
|
42
|
+
template and is unaffected.
|
|
43
|
+
- **The nest-logger integration recipe binds what it promises.** It imported
|
|
44
|
+
`NestLoggerModule`, which does not exist; the module is `BymaxLoggerModule`.
|
|
45
|
+
It also aliased `LogContextService` with `useExisting`, but that service
|
|
46
|
+
has no `getCorrelationId()`. The binding was inert, and silently so: the
|
|
47
|
+
filter treats a failing correlation read as "no id". The README and the
|
|
48
|
+
specification now adapt the `requestId` key with a factory; the README
|
|
49
|
+
binds `PinoUnexpectedErrorSink` (nest-logger 1.4.1) beside it. The snippet
|
|
50
|
+
passed the published-surface gate before only because that gate ignores
|
|
51
|
+
diagnostics from a peer the fixture does not install.
|
|
52
|
+
- **A thrown value that cannot be described no longer breaks the response.**
|
|
53
|
+
With `envelope.exposeInternals` on, a value with no prototype, or an `Error`
|
|
54
|
+
whose `stack` getter throws, threw out of the filter. The internals dump now
|
|
55
|
+
degrades to a fixed message, and the hooks still run.
|
|
56
|
+
- **The discard warning no longer claims a mismatch for identical entries.**
|
|
57
|
+
It now says a generated client will not match the library's own types only
|
|
58
|
+
where the two definitions differ.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- **Served component order (since 1.6.1).** Since 1.6.1, a document's own
|
|
63
|
+
`components.schemas` and `components.parameters` entries are listed before
|
|
64
|
+
this package's catalogue and before contributed entries. The 1.6.1 entry did
|
|
65
|
+
not say so. Order in a components map carries no meaning in OpenAPI, but it
|
|
66
|
+
shows in Swagger UI's model list and in a stored `openapi.json` snapshot.
|
|
67
|
+
|
|
68
|
+
## [1.6.2] - 2026-10-22
|
|
69
|
+
|
|
70
|
+
An unexpected error became `500 BYMAX_INTERNAL_ERROR` and the application had no
|
|
71
|
+
way to record what it was. `BymaxCoreModule` builds the filter itself, and the
|
|
72
|
+
only seam — the protected `onUnexpectedError` — is reachable only by subclassing
|
|
73
|
+
a filter the module never lets you substitute. So the access log saw the status
|
|
74
|
+
and nothing else, and a 500 on a read endpoint left no recoverable cause.
|
|
75
|
+
|
|
76
|
+
**This patch adds public surface**, which a patch number cannot tell you: the
|
|
77
|
+
`BYMAX_UNEXPECTED_ERROR_SINK` token and the `IUnexpectedErrorSink` and
|
|
78
|
+
`UnexpectedErrorReport` types. All are additive and nothing is removed or
|
|
79
|
+
renamed, so the digit stays at a patch under this project's versioning policy.
|
|
80
|
+
|
|
81
|
+
**Apply to a derived backend:** nothing is required; with no sink bound the
|
|
82
|
+
filter behaves as before. To record each unexpected error's cause, bind an
|
|
83
|
+
`IUnexpectedErrorSink` under `BYMAX_UNEXPECTED_ERROR_SINK` from a `@Global()`
|
|
84
|
+
module of your own, as the README's "Recording the cause of a 500" shows. A
|
|
85
|
+
backend registered through `forRootAsync` with `telemetry.exposeTraceId` on
|
|
86
|
+
starts serving `traceId` in its error envelopes, which it was configured to do
|
|
87
|
+
and did not; see Fixed.
|
|
88
|
+
|
|
89
|
+
### Added
|
|
90
|
+
|
|
91
|
+
- **`BYMAX_UNEXPECTED_ERROR_SINK`, `IUnexpectedErrorSink` and
|
|
92
|
+
`UnexpectedErrorReport`.** The filter calls `record(report)` once per
|
|
93
|
+
unexpected error, before replying, with the original thrown value, the
|
|
94
|
+
method, the matched route template (`<unmatched>` before routing), and the
|
|
95
|
+
correlation and trace ids when they resolve. An `HttpException` and an
|
|
96
|
+
exposed `4xx` from the body pipeline never reach it.
|
|
97
|
+
|
|
98
|
+
The report carries no raw URL, query string, headers or body: a log outlives
|
|
99
|
+
the request and a query string is where tokens end up. The thrown value is
|
|
100
|
+
passed uncut whatever `envelope.exposeInternals` says, since that option
|
|
101
|
+
governs the response and the sink is not the response.
|
|
102
|
+
|
|
103
|
+
The response never depends on the sink. A synchronous throw is caught and an
|
|
104
|
+
`async record()` rejection is contained, both silently. The interface is
|
|
105
|
+
structural, so a logging library can ship a compatible class without
|
|
106
|
+
importing this package. The protected `onUnexpectedError` seam is unchanged
|
|
107
|
+
and runs alongside the sink.
|
|
108
|
+
|
|
109
|
+
### Fixed
|
|
110
|
+
|
|
111
|
+
- **`forRootAsync` never put the trace id in the error envelope.** The async
|
|
112
|
+
filter slot is built by a factory whose injection list did not include
|
|
113
|
+
`BYMAX_TRACE_CONTEXT`, so the filter fell back to its no-op reader: with
|
|
114
|
+
`telemetry.exposeTraceId` on, `forRoot` served `traceId` and `forRootAsync`
|
|
115
|
+
silently did not. The factory now injects every dependency the sync path
|
|
116
|
+
resolves through the class.
|
|
117
|
+
|
|
14
118
|
## [1.6.1] - 2026-09-05
|
|
15
119
|
|
|
16
120
|
A library contributing an OpenAPI component under a name the application already
|
|
@@ -1087,4 +1191,6 @@ have regressed from. They are kept because the reasoning is worth having.
|
|
|
1087
1191
|
[1.5.3]: https://github.com/bymaxone/nest-core/compare/v1.5.2...v1.5.3
|
|
1088
1192
|
[1.6.0]: https://github.com/bymaxone/nest-core/compare/v1.5.3...v1.6.0
|
|
1089
1193
|
[1.6.1]: https://github.com/bymaxone/nest-core/compare/v1.6.0...v1.6.1
|
|
1090
|
-
[
|
|
1194
|
+
[1.6.2]: https://github.com/bymaxone/nest-core/compare/v1.6.1...v1.6.2
|
|
1195
|
+
[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
|
package/README.md
CHANGED
|
@@ -516,8 +516,9 @@ requirement.
|
|
|
516
516
|
## 🔑 DI Tokens
|
|
517
517
|
|
|
518
518
|
Every token is a `Symbol`. `BYMAX_CORRELATION_PROVIDER`,
|
|
519
|
-
`BYMAX_HEALTH_INDICATORS
|
|
520
|
-
`@Optional()` and are not bound
|
|
519
|
+
`BYMAX_HEALTH_INDICATORS`, `BYMAX_HEALTH_TRANSITION_SINK` and
|
|
520
|
+
`BYMAX_UNEXPECTED_ERROR_SINK` are consumed with `@Optional()` and are not bound
|
|
521
|
+
by the module: provide any of them from your own module to supply your own
|
|
521
522
|
implementation, otherwise the internal fallback in the last column applies.
|
|
522
523
|
`BYMAX_TIMING_SINK` and `BYMAX_METRICS_REGISTRY` behave differently on
|
|
523
524
|
`forRootAsync`, where options resolve after the module is defined: there the
|
|
@@ -535,6 +536,7 @@ below.
|
|
|
535
536
|
| `BYMAX_TIMING_SINK` | `ITimingSink` | internal no-op, or the metrics bridge when timing and metrics are both enabled |
|
|
536
537
|
| `BYMAX_HEALTH_INDICATORS` | `IHealthIndicator[]` | treated as an empty indicator set |
|
|
537
538
|
| `BYMAX_HEALTH_TRANSITION_SINK` | `IHealthTransitionSink` | no sink; readiness transitions go to Nest's logger instead (binding one stands that line down) |
|
|
539
|
+
| `BYMAX_UNEXPECTED_ERROR_SINK` | `IUnexpectedErrorSink` | no sink; the cause of a `500 BYMAX_INTERNAL_ERROR` is not recorded anywhere |
|
|
538
540
|
| `BYMAX_METRICS_REGISTRY` | the `prom-client` `Registry` | bound when metrics are enabled; on `forRootAsync` always registered, guarded-placeholder when off |
|
|
539
541
|
| `BYMAX_TRACE_CONTEXT` | `ITraceContextProvider` | bound on every path: the OpenTelemetry reader when telemetry is enabled, a no-op that resolves no trace otherwise |
|
|
540
542
|
|
|
@@ -667,6 +669,62 @@ what exhausts the stack leaves a wide margin: one consumer runs `32`, against
|
|
|
667
669
|
the ~2000 levels that overflow. Walk the parsed body iteratively — a recursive
|
|
668
670
|
depth check on a hostile payload overflows the stack it was written to protect.
|
|
669
671
|
|
|
672
|
+
### Recording the cause of a 500
|
|
673
|
+
|
|
674
|
+
An error the filter does not recognise collapses to a fixed
|
|
675
|
+
`500 BYMAX_INTERNAL_ERROR`, and that collapse is the point: the client learns
|
|
676
|
+
nothing about your internals. It also means that once the response is written,
|
|
677
|
+
the thrown value is gone — an access log sees only the status. Bind an
|
|
678
|
+
`IUnexpectedErrorSink` and the filter hands it the original value first:
|
|
679
|
+
|
|
680
|
+
```typescript
|
|
681
|
+
import { Global, Logger, Module } from '@nestjs/common'
|
|
682
|
+
import { BYMAX_UNEXPECTED_ERROR_SINK } from '@bymax-one/nest-core'
|
|
683
|
+
import type { IUnexpectedErrorSink, UnexpectedErrorReport } from '@bymax-one/nest-core'
|
|
684
|
+
|
|
685
|
+
export class UnexpectedErrorLogger implements IUnexpectedErrorSink {
|
|
686
|
+
private readonly logger = new Logger('UnexpectedError')
|
|
687
|
+
|
|
688
|
+
record({ error, method, route, correlationId }: UnexpectedErrorReport): void {
|
|
689
|
+
this.logger.error(
|
|
690
|
+
`${method} ${route} failed (correlationId=${correlationId ?? '-'})`,
|
|
691
|
+
error instanceof Error ? error.stack : String(error)
|
|
692
|
+
)
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
@Global()
|
|
697
|
+
@Module({
|
|
698
|
+
providers: [{ provide: BYMAX_UNEXPECTED_ERROR_SINK, useClass: UnexpectedErrorLogger }],
|
|
699
|
+
exports: [BYMAX_UNEXPECTED_ERROR_SINK]
|
|
700
|
+
})
|
|
701
|
+
export class ErrorReportingModule {}
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
The sink is called once per unexpected error, on both registration paths, and
|
|
705
|
+
for nothing else: an `HttpException` is a deliberate answer and an exposed `4xx`
|
|
706
|
+
from the body pipeline is a client mistake, so neither reaches it. The report
|
|
707
|
+
carries the thrown value as it was thrown, the method, the route **template**
|
|
708
|
+
(`/invoices/:id`, or `<unmatched>` for an error raised before routing), and the
|
|
709
|
+
correlation and trace ids when they resolve. It carries no raw URL, query
|
|
710
|
+
string, headers or body.
|
|
711
|
+
|
|
712
|
+
The response never depends on the sink. A sink that throws, or an
|
|
713
|
+
`async record()` that rejects, is absorbed and the client receives the same
|
|
714
|
+
envelope it would have received with no sink bound, so a broken logger cannot
|
|
715
|
+
turn one failure into two. It runs synchronously before the reply, so hand the
|
|
716
|
+
report off and return.
|
|
717
|
+
|
|
718
|
+
The value is passed uncut whatever `envelope.exposeInternals` says, since that
|
|
719
|
+
option governs the response body and the sink is not the response. A driver's
|
|
720
|
+
error message can carry hosts and query fragments: send the report where access
|
|
721
|
+
is already controlled.
|
|
722
|
+
|
|
723
|
+
`IUnexpectedErrorSink` is matched by shape, so a library can ship a compatible
|
|
724
|
+
class without importing this package. Subclassing `BymaxExceptionFilter` and
|
|
725
|
+
overriding `onUnexpectedError` still works and runs alongside the sink; the
|
|
726
|
+
sink is what reaches the filter `BymaxCoreModule` registers itself.
|
|
727
|
+
|
|
670
728
|
## ⏱️ Request Timing
|
|
671
729
|
|
|
672
730
|
One `RequestTimingSample` is delivered to whatever implements `ITimingSink` for
|
|
@@ -1488,27 +1546,67 @@ your samples and your logs.
|
|
|
1488
1546
|
|
|
1489
1547
|
## 🔗 Integration with `@bymax-one/nest-logger`
|
|
1490
1548
|
|
|
1491
|
-
Pairing this package with `@bymax-one/nest-logger`
|
|
1492
|
-
error responses
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1549
|
+
Pairing this package with `@bymax-one/nest-logger` gives correlated logs and
|
|
1550
|
+
error responses without either package importing the other. nest-logger's
|
|
1551
|
+
request-id middleware stores the id under the `requestId` key of
|
|
1552
|
+
`LogContextService`. That service is not itself an `ICorrelationIdProvider` —
|
|
1553
|
+
it has no `getCorrelationId()` — so adapt it with a factory. `useExisting` on
|
|
1554
|
+
it binds an object without that method, and the filter then reads no id and
|
|
1555
|
+
reports nothing. Bind both tokens from a `@Global()` module of your own so they
|
|
1556
|
+
are visible outside the module that declares them:
|
|
1496
1557
|
|
|
1497
1558
|
```typescript
|
|
1498
1559
|
import { Global, Module } from '@nestjs/common'
|
|
1499
|
-
import { BYMAX_CORRELATION_PROVIDER } from '@bymax-one/nest-core'
|
|
1500
|
-
import {
|
|
1560
|
+
import { BYMAX_CORRELATION_PROVIDER, BYMAX_UNEXPECTED_ERROR_SINK } from '@bymax-one/nest-core'
|
|
1561
|
+
import type { ICorrelationIdProvider } from '@bymax-one/nest-core'
|
|
1562
|
+
import {
|
|
1563
|
+
BymaxLoggerModule,
|
|
1564
|
+
LogContextService,
|
|
1565
|
+
PinoUnexpectedErrorSink
|
|
1566
|
+
} from '@bymax-one/nest-logger'
|
|
1501
1567
|
|
|
1502
1568
|
@Global()
|
|
1503
1569
|
@Module({
|
|
1504
|
-
imports: [
|
|
1505
|
-
providers: [
|
|
1506
|
-
|
|
1570
|
+
imports: [BymaxLoggerModule.forRoot({ service: { name: 'api', version: '1.0.0' } })],
|
|
1571
|
+
providers: [
|
|
1572
|
+
{
|
|
1573
|
+
provide: BYMAX_CORRELATION_PROVIDER,
|
|
1574
|
+
inject: [LogContextService],
|
|
1575
|
+
useFactory: (context: LogContextService): ICorrelationIdProvider => ({
|
|
1576
|
+
getCorrelationId: () => context.get<string>('requestId')
|
|
1577
|
+
})
|
|
1578
|
+
},
|
|
1579
|
+
{ provide: BYMAX_UNEXPECTED_ERROR_SINK, useClass: PinoUnexpectedErrorSink }
|
|
1580
|
+
],
|
|
1581
|
+
exports: [BYMAX_CORRELATION_PROVIDER, BYMAX_UNEXPECTED_ERROR_SINK]
|
|
1507
1582
|
})
|
|
1508
1583
|
export class ObservabilityModule {}
|
|
1509
1584
|
```
|
|
1510
1585
|
|
|
1511
|
-
|
|
1586
|
+
The id exists only once nest-logger's request-id middleware runs, and
|
|
1587
|
+
`BymaxLoggerModule` mounts none on its own. Mount it in `main.ts`, or call
|
|
1588
|
+
`applyRequestIdMiddleware(consumer)` from a module's `configure()`:
|
|
1589
|
+
|
|
1590
|
+
> **Express only.** nest-logger's request-id middleware supports
|
|
1591
|
+
> `@nestjs/platform-express`; the Fastify adapter is unsupported, so do not
|
|
1592
|
+
> rely on this correlation provider there.
|
|
1593
|
+
|
|
1594
|
+
```typescript
|
|
1595
|
+
import { NestFactory } from '@nestjs/core'
|
|
1596
|
+
import { applyAccessLog } from '@bymax-one/nest-logger'
|
|
1597
|
+
import { AppModule } from './app.module'
|
|
1598
|
+
|
|
1599
|
+
async function bootstrap(): Promise<void> {
|
|
1600
|
+
const app = await NestFactory.create(AppModule)
|
|
1601
|
+
applyAccessLog(app) // opens the request context that carries `requestId`
|
|
1602
|
+
await app.listen(3000)
|
|
1603
|
+
}
|
|
1604
|
+
void bootstrap()
|
|
1605
|
+
```
|
|
1606
|
+
|
|
1607
|
+
Every error envelope now carries the same correlation id your logs do, and the
|
|
1608
|
+
cause of every collapsed `500` is logged with it (`PinoUnexpectedErrorSink`
|
|
1609
|
+
needs `@bymax-one/nest-logger` 1.4.1 or later). The
|
|
1512
1610
|
same `@Global()`-module pattern is how every pluggable token in this package
|
|
1513
1611
|
is overridden: `BYMAX_TIMING_SINK` and `BYMAX_HEALTH_INDICATORS` follow it
|
|
1514
1612
|
identically.
|
|
@@ -1739,26 +1837,27 @@ in the sections above.
|
|
|
1739
1837
|
|
|
1740
1838
|
### `.` (root)
|
|
1741
1839
|
|
|
1742
|
-
| Export
|
|
1743
|
-
|
|
|
1744
|
-
| `BymaxCoreModule`
|
|
1745
|
-
| `BymaxCoreModuleOptions`, `EnvelopeOptions`, `TimingOptions`, `HealthOptions`, `MetricsOptions`, `TelemetryOptions`, `OpenApiOptions`, `OpenApiServerDescriptor`, `OpenApiSecurityScheme`, `ResolvedCoreOptions`
|
|
1746
|
-
| `OpenApiSecurityRequirement`, `OpenApiHttpMethod`, `OpenApiOperationKey`, `OperationSecurityMap`
|
|
1747
|
-
| `OpenApiOperationIdFactory`
|
|
1748
|
-
| `BYMAX_CORE_OPTIONS`, `BYMAX_CORRELATION_PROVIDER`, `BYMAX_TIMING_SINK`, `BYMAX_HEALTH_INDICATORS`, `BYMAX_HEALTH_TRANSITION_SINK`, `BYMAX_METRICS_REGISTRY`, `BYMAX_TRACE_CONTEXT`
|
|
1749
|
-
| `IHealthTransitionSink`, `HealthTransition`, `HealthTransitionCause`
|
|
1750
|
-
| `ICorrelationIdProvider`
|
|
1751
|
-
| `ITraceContextProvider`, `TraceContext`
|
|
1752
|
-
| `BymaxExceptionFilter`
|
|
1753
|
-
| `FilterErrorContext`
|
|
1754
|
-
| `
|
|
1755
|
-
| `
|
|
1756
|
-
| `
|
|
1757
|
-
| `
|
|
1758
|
-
| `
|
|
1759
|
-
| `
|
|
1760
|
-
| `
|
|
1761
|
-
| `
|
|
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. |
|
|
1762
1861
|
|
|
1763
1862
|
### `./pagination`
|
|
1764
1863
|
|
package/dist/index.cjs
CHANGED
|
@@ -160,6 +160,9 @@ var BYMAX_HEALTH_INDICATORS = /* @__PURE__ */ Symbol.for(
|
|
|
160
160
|
var BYMAX_HEALTH_TRANSITION_SINK = /* @__PURE__ */ Symbol.for(
|
|
161
161
|
"@bymax-one/nest-core:health-transition-sink"
|
|
162
162
|
);
|
|
163
|
+
var BYMAX_UNEXPECTED_ERROR_SINK = /* @__PURE__ */ Symbol.for(
|
|
164
|
+
"@bymax-one/nest-core:unexpected-error-sink"
|
|
165
|
+
);
|
|
163
166
|
var BYMAX_METRICS_REGISTRY = /* @__PURE__ */ Symbol.for(
|
|
164
167
|
"@bymax-one/nest-core:metrics-registry"
|
|
165
168
|
);
|
|
@@ -265,6 +268,13 @@ function buildTraceContextProvider() {
|
|
|
265
268
|
};
|
|
266
269
|
}
|
|
267
270
|
|
|
271
|
+
// src/contain-rejection.ts
|
|
272
|
+
function containRejection(returned, onFailure) {
|
|
273
|
+
if (typeof returned?.then === "function") {
|
|
274
|
+
Promise.resolve(returned).catch(onFailure);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
268
278
|
// src/envelope/error-codes.ts
|
|
269
279
|
var BYMAX_BAD_REQUEST = "BYMAX_BAD_REQUEST";
|
|
270
280
|
var BYMAX_VALIDATION_FAILED = "BYMAX_VALIDATION_FAILED";
|
|
@@ -328,10 +338,40 @@ function buildErrorEnvelope(input) {
|
|
|
328
338
|
};
|
|
329
339
|
}
|
|
330
340
|
|
|
341
|
+
// src/timing/request-info.accessor.ts
|
|
342
|
+
var UNMATCHED_ROUTE = "<unmatched>";
|
|
343
|
+
function readExpressTemplate(request) {
|
|
344
|
+
const path = request.route?.path;
|
|
345
|
+
return path === void 0 ? void 0 : `${request.baseUrl ?? ""}${path}`;
|
|
346
|
+
}
|
|
347
|
+
function readFastifyTemplate(request) {
|
|
348
|
+
return request.routeOptions?.url;
|
|
349
|
+
}
|
|
350
|
+
function readRequestInfo(request) {
|
|
351
|
+
const template = readExpressTemplate(request) ?? readFastifyTemplate(request);
|
|
352
|
+
return { method: request.method ?? "", route: template ?? UNMATCHED_ROUTE };
|
|
353
|
+
}
|
|
354
|
+
function extractRequestInfo(context) {
|
|
355
|
+
return readRequestInfo(context.switchToHttp().getRequest());
|
|
356
|
+
}
|
|
357
|
+
|
|
331
358
|
// src/envelope/exception.filter.ts
|
|
332
359
|
var INTERNAL_ERROR_STATUS = 500;
|
|
333
360
|
var INTERNAL_ERROR_MESSAGE = "Internal server error";
|
|
334
361
|
var VALIDATION_FAILED_MESSAGE = "Validation failed";
|
|
362
|
+
var ABSOLUTE_FORM_PREFIX = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
|
|
363
|
+
function pathOf(url) {
|
|
364
|
+
let target = String(url);
|
|
365
|
+
const prefix = ABSOLUTE_FORM_PREFIX.exec(target);
|
|
366
|
+
if (prefix !== null) {
|
|
367
|
+
const afterScheme = target.slice(prefix[0].length);
|
|
368
|
+
const authorityEnd = afterScheme.search(/[/?#]/);
|
|
369
|
+
target = authorityEnd === -1 ? "" : afterScheme.slice(authorityEnd);
|
|
370
|
+
}
|
|
371
|
+
const pathEnd = target.search(/[?#]/);
|
|
372
|
+
const path = pathEnd === -1 ? target : target.slice(0, pathEnd);
|
|
373
|
+
return path === "" ? "/" : path;
|
|
374
|
+
}
|
|
335
375
|
function resolveErrorCarrier(response) {
|
|
336
376
|
if (typeof response !== "object" || response === null) {
|
|
337
377
|
return void 0;
|
|
@@ -393,11 +433,16 @@ function extractHttpMessage(response, exception, carrier) {
|
|
|
393
433
|
}
|
|
394
434
|
return exception.message;
|
|
395
435
|
}
|
|
436
|
+
var UNREPRESENTABLE_MESSAGE = "Unrepresentable thrown value";
|
|
396
437
|
function buildInternalDetails(exception) {
|
|
397
|
-
|
|
398
|
-
|
|
438
|
+
try {
|
|
439
|
+
if (exception instanceof Error) {
|
|
440
|
+
return exception.stack !== void 0 ? { message: exception.message, stack: exception.stack } : { message: exception.message };
|
|
441
|
+
}
|
|
442
|
+
return { message: String(exception) };
|
|
443
|
+
} catch {
|
|
444
|
+
return { message: UNREPRESENTABLE_MESSAGE };
|
|
399
445
|
}
|
|
400
|
-
return { message: String(exception) };
|
|
401
446
|
}
|
|
402
447
|
exports.BymaxExceptionFilter = class BymaxExceptionFilter {
|
|
403
448
|
/**
|
|
@@ -408,10 +453,13 @@ exports.BymaxExceptionFilter = class BymaxExceptionFilter {
|
|
|
408
453
|
* (from their own, globally-visible module) is not shadowed by one; when
|
|
409
454
|
* nothing is bound, this falls back to a no-op that omits `correlationId`.
|
|
410
455
|
* @param adapterHost - Host of the live HTTP adapter, resolved lazily per catch.
|
|
456
|
+
* @param traceContext - Reader for the active trace; a no-op when unbound.
|
|
457
|
+
* @param unexpectedErrorSink - Receives every unexpected error; unbound by default.
|
|
411
458
|
*/
|
|
412
|
-
constructor(options, correlation, adapterHost, traceContext) {
|
|
459
|
+
constructor(options, correlation, adapterHost, traceContext, unexpectedErrorSink) {
|
|
413
460
|
this.options = options;
|
|
414
461
|
this.adapterHost = adapterHost;
|
|
462
|
+
this.unexpectedErrorSink = unexpectedErrorSink;
|
|
415
463
|
/** Clock used to stamp the envelope timestamp; overridable in tests via fake timers. */
|
|
416
464
|
this.now = () => /* @__PURE__ */ new Date();
|
|
417
465
|
this.correlation = correlation ?? new NoopCorrelationIdProvider();
|
|
@@ -466,23 +514,24 @@ exports.BymaxExceptionFilter = class BymaxExceptionFilter {
|
|
|
466
514
|
const traceId = this.readAnnotation(() => this.traceContext.getTraceContext()?.traceId);
|
|
467
515
|
const context = {
|
|
468
516
|
method: String(httpAdapter.getRequestMethod(request)),
|
|
469
|
-
path:
|
|
517
|
+
path: pathOf(httpAdapter.getRequestUrl(request)),
|
|
470
518
|
...correlationId !== void 0 ? { correlationId } : {},
|
|
471
519
|
...traceId !== void 0 ? { traceId } : {}
|
|
472
520
|
};
|
|
473
|
-
const envelope = this.buildEnvelope(exception, context);
|
|
521
|
+
const envelope = this.buildEnvelope(exception, context, request);
|
|
474
522
|
httpAdapter.reply(response, envelope, envelope.statusCode);
|
|
475
523
|
}
|
|
476
524
|
/**
|
|
477
525
|
* Select the mapping rule for the exception and build its envelope. An
|
|
478
|
-
* unknown error is handed to the observability seam
|
|
479
|
-
* an integration can record the original error with the
|
|
526
|
+
* unknown error is handed to the observability seam once its envelope is
|
|
527
|
+
* built, so an integration can record the original error with the context.
|
|
480
528
|
*
|
|
481
529
|
* @param exception - The error that escaped the handler.
|
|
482
530
|
* @param context - The neutral request context.
|
|
531
|
+
* @param request - The framework request, read only for the sink's route template.
|
|
483
532
|
* @returns The formatted envelope.
|
|
484
533
|
*/
|
|
485
|
-
buildEnvelope(exception, context) {
|
|
534
|
+
buildEnvelope(exception, context, request) {
|
|
486
535
|
if (exception instanceof common.HttpException) {
|
|
487
536
|
return this.mapHttpException(exception, context);
|
|
488
537
|
}
|
|
@@ -495,11 +544,39 @@ exports.BymaxExceptionFilter = class BymaxExceptionFilter {
|
|
|
495
544
|
context
|
|
496
545
|
);
|
|
497
546
|
}
|
|
547
|
+
const envelope = this.mapUnknown(exception, context);
|
|
498
548
|
try {
|
|
499
549
|
this.onUnexpectedError(exception, context);
|
|
500
550
|
} catch {
|
|
501
551
|
}
|
|
502
|
-
|
|
552
|
+
this.reportUnexpected(exception, context, request);
|
|
553
|
+
return envelope;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Hand an unexpected error to the bound sink, if any. Whatever the sink does
|
|
557
|
+
* — throw, or return a promise that rejects — is absorbed: the response must
|
|
558
|
+
* not depend on it. See `IUnexpectedErrorSink` for the contract.
|
|
559
|
+
*
|
|
560
|
+
* @param error - The original thrown value.
|
|
561
|
+
* @param context - The neutral request context.
|
|
562
|
+
* @param request - The framework request, read for its route template only.
|
|
563
|
+
*/
|
|
564
|
+
reportUnexpected(error, context, request) {
|
|
565
|
+
const sink = this.unexpectedErrorSink;
|
|
566
|
+
if (sink === void 0) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
const report = {
|
|
571
|
+
error,
|
|
572
|
+
method: context.method,
|
|
573
|
+
route: readRequestInfo(request).route,
|
|
574
|
+
...context.correlationId !== void 0 ? { correlationId: context.correlationId } : {},
|
|
575
|
+
...context.traceId !== void 0 ? { traceId: context.traceId } : {}
|
|
576
|
+
};
|
|
577
|
+
containRejection(sink.record(report), () => void 0);
|
|
578
|
+
} catch {
|
|
579
|
+
}
|
|
503
580
|
}
|
|
504
581
|
/**
|
|
505
582
|
* Map an `HttpException` to the envelope. A domain error passes its own code,
|
|
@@ -589,7 +666,7 @@ exports.BymaxExceptionFilter = class BymaxExceptionFilter {
|
|
|
589
666
|
}
|
|
590
667
|
/**
|
|
591
668
|
* Observability seam invoked for every unexpected (non-`HttpException`) error
|
|
592
|
-
*
|
|
669
|
+
* collapsed to the generic 500. The base implementation is a no-op:
|
|
593
670
|
* this library owns no logger. An integration (for example
|
|
594
671
|
* `@bymax-one/nest-logger`) subclasses the filter and overrides this to record
|
|
595
672
|
* the original error with the current request context and correlation id.
|
|
@@ -608,7 +685,9 @@ exports.BymaxExceptionFilter = __decorateClass([
|
|
|
608
685
|
__decorateParam(1, common.Inject(BYMAX_CORRELATION_PROVIDER)),
|
|
609
686
|
__decorateParam(2, common.Inject(core.HttpAdapterHost)),
|
|
610
687
|
__decorateParam(3, common.Optional()),
|
|
611
|
-
__decorateParam(3, common.Inject(BYMAX_TRACE_CONTEXT))
|
|
688
|
+
__decorateParam(3, common.Inject(BYMAX_TRACE_CONTEXT)),
|
|
689
|
+
__decorateParam(4, common.Optional()),
|
|
690
|
+
__decorateParam(4, common.Inject(BYMAX_UNEXPECTED_ERROR_SINK))
|
|
612
691
|
], exports.BymaxExceptionFilter);
|
|
613
692
|
var PassThroughExceptionFilter = class {
|
|
614
693
|
constructor(adapterHost) {
|
|
@@ -635,15 +714,8 @@ function assertAsyncFeatureEnabled(feature, enabled) {
|
|
|
635
714
|
);
|
|
636
715
|
}
|
|
637
716
|
}
|
|
638
|
-
function selectAsyncExceptionFilter(options, correlation, adapterHost) {
|
|
639
|
-
return options.envelope.enabled ? new exports.BymaxExceptionFilter(options, correlation, adapterHost) : new PassThroughExceptionFilter(adapterHost);
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// src/contain-rejection.ts
|
|
643
|
-
function containRejection(returned, onFailure) {
|
|
644
|
-
if (typeof returned?.then === "function") {
|
|
645
|
-
Promise.resolve(returned).catch(onFailure);
|
|
646
|
-
}
|
|
717
|
+
function selectAsyncExceptionFilter(options, correlation, adapterHost, traceContext, unexpectedErrorSink) {
|
|
718
|
+
return options.envelope.enabled ? new exports.BymaxExceptionFilter(options, correlation, adapterHost, traceContext, unexpectedErrorSink) : new PassThroughExceptionFilter(adapterHost);
|
|
647
719
|
}
|
|
648
720
|
|
|
649
721
|
// src/discovery.ts
|
|
@@ -1354,23 +1426,6 @@ function isFastifyInstance(instance) {
|
|
|
1354
1426
|
return typeof instance?.addHook === "function";
|
|
1355
1427
|
}
|
|
1356
1428
|
|
|
1357
|
-
// src/timing/request-info.accessor.ts
|
|
1358
|
-
var UNMATCHED_ROUTE = "<unmatched>";
|
|
1359
|
-
function readExpressTemplate(request) {
|
|
1360
|
-
const path = request.route?.path;
|
|
1361
|
-
return path === void 0 ? void 0 : `${request.baseUrl ?? ""}${path}`;
|
|
1362
|
-
}
|
|
1363
|
-
function readFastifyTemplate(request) {
|
|
1364
|
-
return request.routeOptions?.url;
|
|
1365
|
-
}
|
|
1366
|
-
function readRequestInfo(request) {
|
|
1367
|
-
const template = readExpressTemplate(request) ?? readFastifyTemplate(request);
|
|
1368
|
-
return { method: request.method ?? "", route: template ?? UNMATCHED_ROUTE };
|
|
1369
|
-
}
|
|
1370
|
-
function extractRequestInfo(context) {
|
|
1371
|
-
return readRequestInfo(context.switchToHttp().getRequest());
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
1429
|
// src/timing/timing.sample.ts
|
|
1375
1430
|
function readTraceContext(traceContext) {
|
|
1376
1431
|
try {
|
|
@@ -1529,11 +1584,22 @@ function buildAsyncSlots() {
|
|
|
1529
1584
|
return [
|
|
1530
1585
|
{
|
|
1531
1586
|
provide: core.APP_FILTER,
|
|
1532
|
-
useFactory: (options, correlation, adapterHost) => selectAsyncExceptionFilter(
|
|
1587
|
+
useFactory: (options, correlation, adapterHost, traceContext, unexpectedErrorSink) => selectAsyncExceptionFilter(
|
|
1588
|
+
options,
|
|
1589
|
+
correlation,
|
|
1590
|
+
adapterHost,
|
|
1591
|
+
traceContext,
|
|
1592
|
+
unexpectedErrorSink
|
|
1593
|
+
),
|
|
1594
|
+
// Every constructor dependency the sync path resolves through `useClass`
|
|
1595
|
+
// has to be listed here by hand; one left out reaches the filter as its
|
|
1596
|
+
// no-op fallback and nothing fails.
|
|
1533
1597
|
inject: [
|
|
1534
1598
|
BYMAX_CORE_OPTIONS,
|
|
1535
1599
|
{ token: BYMAX_CORRELATION_PROVIDER, optional: true },
|
|
1536
|
-
core.HttpAdapterHost
|
|
1600
|
+
core.HttpAdapterHost,
|
|
1601
|
+
BYMAX_TRACE_CONTEXT,
|
|
1602
|
+
{ token: BYMAX_UNEXPECTED_ERROR_SINK, optional: true }
|
|
1537
1603
|
]
|
|
1538
1604
|
}
|
|
1539
1605
|
];
|
|
@@ -1833,6 +1899,7 @@ exports.BYMAX_TIMING_SINK = BYMAX_TIMING_SINK;
|
|
|
1833
1899
|
exports.BYMAX_TOO_MANY_REQUESTS = BYMAX_TOO_MANY_REQUESTS;
|
|
1834
1900
|
exports.BYMAX_TRACE_CONTEXT = BYMAX_TRACE_CONTEXT;
|
|
1835
1901
|
exports.BYMAX_UNAUTHORIZED = BYMAX_UNAUTHORIZED;
|
|
1902
|
+
exports.BYMAX_UNEXPECTED_ERROR_SINK = BYMAX_UNEXPECTED_ERROR_SINK;
|
|
1836
1903
|
exports.BYMAX_UNPROCESSABLE_ENTITY = BYMAX_UNPROCESSABLE_ENTITY;
|
|
1837
1904
|
exports.BYMAX_UNSUPPORTED_MEDIA_TYPE = BYMAX_UNSUPPORTED_MEDIA_TYPE;
|
|
1838
1905
|
exports.BYMAX_VALIDATION_FAILED = BYMAX_VALIDATION_FAILED;
|
package/dist/index.d.cts
CHANGED
|
@@ -564,6 +564,12 @@ declare const BYMAX_HEALTH_INDICATORS: unique symbol;
|
|
|
564
564
|
* stands that line down in favour of the sink.
|
|
565
565
|
*/
|
|
566
566
|
declare const BYMAX_HEALTH_TRANSITION_SINK: unique symbol;
|
|
567
|
+
/**
|
|
568
|
+
* Provide the `IUnexpectedErrorSink` that receives the cause of each collapsed
|
|
569
|
+
* `500 BYMAX_INTERNAL_ERROR`. Nothing is bound by default, and with no
|
|
570
|
+
* sink the filter records nothing.
|
|
571
|
+
*/
|
|
572
|
+
declare const BYMAX_UNEXPECTED_ERROR_SINK: unique symbol;
|
|
567
573
|
/**
|
|
568
574
|
* Provide the `prom-client` `Registry` backing the metrics endpoint. Bound
|
|
569
575
|
* lazily and only when the metrics feature is enabled.
|
|
@@ -636,6 +642,100 @@ interface ITraceContextProvider {
|
|
|
636
642
|
getTraceContext(): TraceContext | undefined;
|
|
637
643
|
}
|
|
638
644
|
|
|
645
|
+
/**
|
|
646
|
+
* @fileoverview The unexpected-error contract: a sink that receives the
|
|
647
|
+
* original value behind every collapsed `500 BYMAX_INTERNAL_ERROR`, with the
|
|
648
|
+
* request it failed.
|
|
649
|
+
*
|
|
650
|
+
* This exists because the collapse is what makes the response safe, and that
|
|
651
|
+
* same collapse is what makes the cause unrecoverable. The envelope carries a
|
|
652
|
+
* fixed message by design, an access log sees only the status, and the thrown
|
|
653
|
+
* value is gone the moment the filter replies. Nothing downstream of the filter
|
|
654
|
+
* can get it back, so the filter has to hand it over before it goes.
|
|
655
|
+
*
|
|
656
|
+
* **Structural on purpose.** A sink is matched by shape, not by inheritance, so
|
|
657
|
+
* a logging library can ship a compatible class without importing this package
|
|
658
|
+
* or declaring it as a peer. Nothing here is a class or a value.
|
|
659
|
+
*
|
|
660
|
+
* **What the report leaves out.** No request body, no headers, no query string
|
|
661
|
+
* and no raw URL: `route` is the template the router matched, the same
|
|
662
|
+
* bounded label the timing sample carries. A sink usually writes to a log that
|
|
663
|
+
* outlives the request and is read by more people than the request's sender,
|
|
664
|
+
* and a query string is where tokens and personal data end up. A sink that
|
|
665
|
+
* needs more than this reads it from its own request context.
|
|
666
|
+
*
|
|
667
|
+
* The thrown value itself is passed as is, uncut and unsanitised, whatever
|
|
668
|
+
* `envelope.exposeInternals` says. That option controls the response body,
|
|
669
|
+
* which goes to an untrusted client; a sink is application code at the same
|
|
670
|
+
* trust level as the logger, and a cause cut down to make it safe is not much
|
|
671
|
+
* of a cause. Treat what reaches it as possibly carrying hosts, queries and
|
|
672
|
+
* credentials from a driver's error message, and send it where access is
|
|
673
|
+
* already controlled.
|
|
674
|
+
* @layer Contract
|
|
675
|
+
*/
|
|
676
|
+
/**
|
|
677
|
+
* One unexpected error, as the filter saw it when collapsing it.
|
|
678
|
+
*
|
|
679
|
+
* Optional fields are **absent** when unknown, never present holding
|
|
680
|
+
* `undefined`, so a sink that spreads the report into a structured log line
|
|
681
|
+
* writes no empty keys.
|
|
682
|
+
*/
|
|
683
|
+
interface UnexpectedErrorReport {
|
|
684
|
+
/** The original thrown value: an `Error`, or anything else that was thrown. */
|
|
685
|
+
readonly error: unknown;
|
|
686
|
+
/** HTTP method, read through the adapter (Express and Fastify neutral). */
|
|
687
|
+
readonly method: string;
|
|
688
|
+
/**
|
|
689
|
+
* Route template the request matched, for example `"/invoices/:id"`, or
|
|
690
|
+
* `UNMATCHED_ROUTE` when it matched none: an error raised by middleware
|
|
691
|
+
* before routing. Never the raw URL.
|
|
692
|
+
*/
|
|
693
|
+
readonly route: string;
|
|
694
|
+
/** Correlation id for the request; absent when no provider resolves one. */
|
|
695
|
+
readonly correlationId?: string;
|
|
696
|
+
/**
|
|
697
|
+
* Trace the request ran under; absent when telemetry is off or nothing was
|
|
698
|
+
* recording. Present whatever `telemetry.exposeTraceId` says, because that
|
|
699
|
+
* option controls the response body and the sink is not the response.
|
|
700
|
+
*/
|
|
701
|
+
readonly traceId?: string;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Receives the cause of each collapsed `500 BYMAX_INTERNAL_ERROR`.
|
|
705
|
+
*
|
|
706
|
+
* Bind an implementation under `BYMAX_UNEXPECTED_ERROR_SINK` from a module of
|
|
707
|
+
* your own marked `@Global()`. Nothing is bound by default, and with no sink the
|
|
708
|
+
* filter records nothing: this package owns no logger.
|
|
709
|
+
*
|
|
710
|
+
* Called once per unexpected error, never for an `HttpException` and never for
|
|
711
|
+
* a 4xx the framework's body pipeline raised (a malformed JSON body, a payload
|
|
712
|
+
* over the limit) — those are not unexpected, and a sink would otherwise get a
|
|
713
|
+
* flood of client mistakes mixed in with server failures.
|
|
714
|
+
*
|
|
715
|
+
* Called synchronously from the exception filter, before the response is
|
|
716
|
+
* written, so an implementation must be cheap and must not block: hand the
|
|
717
|
+
* report to a logger and return. Whatever it does, the response does not
|
|
718
|
+
* change. A synchronous throw is caught, and an `async record()` that rejects
|
|
719
|
+
* is contained, and both are dropped silently: the filter is the last thing
|
|
720
|
+
* between an error and the client, and anything it reported about its own sink
|
|
721
|
+
* would push aside the failure the caller actually needs to see.
|
|
722
|
+
*
|
|
723
|
+
* Runs alongside the protected `BymaxExceptionFilter.onUnexpectedError` seam,
|
|
724
|
+
* not instead of it; a subclass overriding that seam still receives every call.
|
|
725
|
+
*
|
|
726
|
+
* Takes a single object, matching `ITimingSink.record` and
|
|
727
|
+
* `IHealthTransitionSink.record`, so a later field is an additive change rather
|
|
728
|
+
* than a new positional parameter.
|
|
729
|
+
*/
|
|
730
|
+
interface IUnexpectedErrorSink {
|
|
731
|
+
/**
|
|
732
|
+
* Record one unexpected error.
|
|
733
|
+
*
|
|
734
|
+
* @param report - The original thrown value and the request it failed.
|
|
735
|
+
*/
|
|
736
|
+
record(report: UnexpectedErrorReport): void;
|
|
737
|
+
}
|
|
738
|
+
|
|
639
739
|
/**
|
|
640
740
|
* Neutral view of the current request handed to {@link BymaxExceptionFilter}
|
|
641
741
|
* mappers and to the {@link BymaxExceptionFilter.onUnexpectedError} seam. It
|
|
@@ -665,6 +765,7 @@ interface FilterErrorContext {
|
|
|
665
765
|
declare class BymaxExceptionFilter implements ExceptionFilter {
|
|
666
766
|
private readonly options;
|
|
667
767
|
private readonly adapterHost;
|
|
768
|
+
private readonly unexpectedErrorSink?;
|
|
668
769
|
/** Clock used to stamp the envelope timestamp; overridable in tests via fake timers. */
|
|
669
770
|
private readonly now;
|
|
670
771
|
/** The resolved correlation provider, or the no-op fallback when none is bound. */
|
|
@@ -679,8 +780,10 @@ declare class BymaxExceptionFilter implements ExceptionFilter {
|
|
|
679
780
|
* (from their own, globally-visible module) is not shadowed by one; when
|
|
680
781
|
* nothing is bound, this falls back to a no-op that omits `correlationId`.
|
|
681
782
|
* @param adapterHost - Host of the live HTTP adapter, resolved lazily per catch.
|
|
783
|
+
* @param traceContext - Reader for the active trace; a no-op when unbound.
|
|
784
|
+
* @param unexpectedErrorSink - Receives every unexpected error; unbound by default.
|
|
682
785
|
*/
|
|
683
|
-
constructor(options: ResolvedCoreOptions, correlation: ICorrelationIdProvider | undefined, adapterHost: HttpAdapterHost, traceContext?: ITraceContextProvider);
|
|
786
|
+
constructor(options: ResolvedCoreOptions, correlation: ICorrelationIdProvider | undefined, adapterHost: HttpAdapterHost, traceContext?: ITraceContextProvider, unexpectedErrorSink?: IUnexpectedErrorSink | undefined);
|
|
684
787
|
/**
|
|
685
788
|
* Format the exception into the stable envelope and reply with it.
|
|
686
789
|
*
|
|
@@ -715,14 +818,25 @@ declare class BymaxExceptionFilter implements ExceptionFilter {
|
|
|
715
818
|
catch(exception: unknown, host: ArgumentsHost): void;
|
|
716
819
|
/**
|
|
717
820
|
* Select the mapping rule for the exception and build its envelope. An
|
|
718
|
-
* unknown error is handed to the observability seam
|
|
719
|
-
* an integration can record the original error with the
|
|
821
|
+
* unknown error is handed to the observability seam once its envelope is
|
|
822
|
+
* built, so an integration can record the original error with the context.
|
|
720
823
|
*
|
|
721
824
|
* @param exception - The error that escaped the handler.
|
|
722
825
|
* @param context - The neutral request context.
|
|
826
|
+
* @param request - The framework request, read only for the sink's route template.
|
|
723
827
|
* @returns The formatted envelope.
|
|
724
828
|
*/
|
|
725
829
|
private buildEnvelope;
|
|
830
|
+
/**
|
|
831
|
+
* Hand an unexpected error to the bound sink, if any. Whatever the sink does
|
|
832
|
+
* — throw, or return a promise that rejects — is absorbed: the response must
|
|
833
|
+
* not depend on it. See `IUnexpectedErrorSink` for the contract.
|
|
834
|
+
*
|
|
835
|
+
* @param error - The original thrown value.
|
|
836
|
+
* @param context - The neutral request context.
|
|
837
|
+
* @param request - The framework request, read for its route template only.
|
|
838
|
+
*/
|
|
839
|
+
private reportUnexpected;
|
|
726
840
|
/**
|
|
727
841
|
* Map an `HttpException` to the envelope. A domain error passes its own code,
|
|
728
842
|
* message and details through, whether it wrote them flat on the response or
|
|
@@ -758,7 +872,7 @@ declare class BymaxExceptionFilter implements ExceptionFilter {
|
|
|
758
872
|
private toEnvelope;
|
|
759
873
|
/**
|
|
760
874
|
* Observability seam invoked for every unexpected (non-`HttpException`) error
|
|
761
|
-
*
|
|
875
|
+
* collapsed to the generic 500. The base implementation is a no-op:
|
|
762
876
|
* this library owns no logger. An integration (for example
|
|
763
877
|
* `@bymax-one/nest-logger`) subclasses the filter and overrides this to record
|
|
764
878
|
* the original error with the current request context and correlation id.
|
|
@@ -1179,4 +1293,4 @@ declare const BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
|
|
|
1179
1293
|
*/
|
|
1180
1294
|
declare function codeForStatus(status: number): string;
|
|
1181
1295
|
|
|
1182
|
-
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_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 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, buildErrorEnvelope, codeForStatus };
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -564,6 +564,12 @@ declare const BYMAX_HEALTH_INDICATORS: unique symbol;
|
|
|
564
564
|
* stands that line down in favour of the sink.
|
|
565
565
|
*/
|
|
566
566
|
declare const BYMAX_HEALTH_TRANSITION_SINK: unique symbol;
|
|
567
|
+
/**
|
|
568
|
+
* Provide the `IUnexpectedErrorSink` that receives the cause of each collapsed
|
|
569
|
+
* `500 BYMAX_INTERNAL_ERROR`. Nothing is bound by default, and with no
|
|
570
|
+
* sink the filter records nothing.
|
|
571
|
+
*/
|
|
572
|
+
declare const BYMAX_UNEXPECTED_ERROR_SINK: unique symbol;
|
|
567
573
|
/**
|
|
568
574
|
* Provide the `prom-client` `Registry` backing the metrics endpoint. Bound
|
|
569
575
|
* lazily and only when the metrics feature is enabled.
|
|
@@ -636,6 +642,100 @@ interface ITraceContextProvider {
|
|
|
636
642
|
getTraceContext(): TraceContext | undefined;
|
|
637
643
|
}
|
|
638
644
|
|
|
645
|
+
/**
|
|
646
|
+
* @fileoverview The unexpected-error contract: a sink that receives the
|
|
647
|
+
* original value behind every collapsed `500 BYMAX_INTERNAL_ERROR`, with the
|
|
648
|
+
* request it failed.
|
|
649
|
+
*
|
|
650
|
+
* This exists because the collapse is what makes the response safe, and that
|
|
651
|
+
* same collapse is what makes the cause unrecoverable. The envelope carries a
|
|
652
|
+
* fixed message by design, an access log sees only the status, and the thrown
|
|
653
|
+
* value is gone the moment the filter replies. Nothing downstream of the filter
|
|
654
|
+
* can get it back, so the filter has to hand it over before it goes.
|
|
655
|
+
*
|
|
656
|
+
* **Structural on purpose.** A sink is matched by shape, not by inheritance, so
|
|
657
|
+
* a logging library can ship a compatible class without importing this package
|
|
658
|
+
* or declaring it as a peer. Nothing here is a class or a value.
|
|
659
|
+
*
|
|
660
|
+
* **What the report leaves out.** No request body, no headers, no query string
|
|
661
|
+
* and no raw URL: `route` is the template the router matched, the same
|
|
662
|
+
* bounded label the timing sample carries. A sink usually writes to a log that
|
|
663
|
+
* outlives the request and is read by more people than the request's sender,
|
|
664
|
+
* and a query string is where tokens and personal data end up. A sink that
|
|
665
|
+
* needs more than this reads it from its own request context.
|
|
666
|
+
*
|
|
667
|
+
* The thrown value itself is passed as is, uncut and unsanitised, whatever
|
|
668
|
+
* `envelope.exposeInternals` says. That option controls the response body,
|
|
669
|
+
* which goes to an untrusted client; a sink is application code at the same
|
|
670
|
+
* trust level as the logger, and a cause cut down to make it safe is not much
|
|
671
|
+
* of a cause. Treat what reaches it as possibly carrying hosts, queries and
|
|
672
|
+
* credentials from a driver's error message, and send it where access is
|
|
673
|
+
* already controlled.
|
|
674
|
+
* @layer Contract
|
|
675
|
+
*/
|
|
676
|
+
/**
|
|
677
|
+
* One unexpected error, as the filter saw it when collapsing it.
|
|
678
|
+
*
|
|
679
|
+
* Optional fields are **absent** when unknown, never present holding
|
|
680
|
+
* `undefined`, so a sink that spreads the report into a structured log line
|
|
681
|
+
* writes no empty keys.
|
|
682
|
+
*/
|
|
683
|
+
interface UnexpectedErrorReport {
|
|
684
|
+
/** The original thrown value: an `Error`, or anything else that was thrown. */
|
|
685
|
+
readonly error: unknown;
|
|
686
|
+
/** HTTP method, read through the adapter (Express and Fastify neutral). */
|
|
687
|
+
readonly method: string;
|
|
688
|
+
/**
|
|
689
|
+
* Route template the request matched, for example `"/invoices/:id"`, or
|
|
690
|
+
* `UNMATCHED_ROUTE` when it matched none: an error raised by middleware
|
|
691
|
+
* before routing. Never the raw URL.
|
|
692
|
+
*/
|
|
693
|
+
readonly route: string;
|
|
694
|
+
/** Correlation id for the request; absent when no provider resolves one. */
|
|
695
|
+
readonly correlationId?: string;
|
|
696
|
+
/**
|
|
697
|
+
* Trace the request ran under; absent when telemetry is off or nothing was
|
|
698
|
+
* recording. Present whatever `telemetry.exposeTraceId` says, because that
|
|
699
|
+
* option controls the response body and the sink is not the response.
|
|
700
|
+
*/
|
|
701
|
+
readonly traceId?: string;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Receives the cause of each collapsed `500 BYMAX_INTERNAL_ERROR`.
|
|
705
|
+
*
|
|
706
|
+
* Bind an implementation under `BYMAX_UNEXPECTED_ERROR_SINK` from a module of
|
|
707
|
+
* your own marked `@Global()`. Nothing is bound by default, and with no sink the
|
|
708
|
+
* filter records nothing: this package owns no logger.
|
|
709
|
+
*
|
|
710
|
+
* Called once per unexpected error, never for an `HttpException` and never for
|
|
711
|
+
* a 4xx the framework's body pipeline raised (a malformed JSON body, a payload
|
|
712
|
+
* over the limit) — those are not unexpected, and a sink would otherwise get a
|
|
713
|
+
* flood of client mistakes mixed in with server failures.
|
|
714
|
+
*
|
|
715
|
+
* Called synchronously from the exception filter, before the response is
|
|
716
|
+
* written, so an implementation must be cheap and must not block: hand the
|
|
717
|
+
* report to a logger and return. Whatever it does, the response does not
|
|
718
|
+
* change. A synchronous throw is caught, and an `async record()` that rejects
|
|
719
|
+
* is contained, and both are dropped silently: the filter is the last thing
|
|
720
|
+
* between an error and the client, and anything it reported about its own sink
|
|
721
|
+
* would push aside the failure the caller actually needs to see.
|
|
722
|
+
*
|
|
723
|
+
* Runs alongside the protected `BymaxExceptionFilter.onUnexpectedError` seam,
|
|
724
|
+
* not instead of it; a subclass overriding that seam still receives every call.
|
|
725
|
+
*
|
|
726
|
+
* Takes a single object, matching `ITimingSink.record` and
|
|
727
|
+
* `IHealthTransitionSink.record`, so a later field is an additive change rather
|
|
728
|
+
* than a new positional parameter.
|
|
729
|
+
*/
|
|
730
|
+
interface IUnexpectedErrorSink {
|
|
731
|
+
/**
|
|
732
|
+
* Record one unexpected error.
|
|
733
|
+
*
|
|
734
|
+
* @param report - The original thrown value and the request it failed.
|
|
735
|
+
*/
|
|
736
|
+
record(report: UnexpectedErrorReport): void;
|
|
737
|
+
}
|
|
738
|
+
|
|
639
739
|
/**
|
|
640
740
|
* Neutral view of the current request handed to {@link BymaxExceptionFilter}
|
|
641
741
|
* mappers and to the {@link BymaxExceptionFilter.onUnexpectedError} seam. It
|
|
@@ -665,6 +765,7 @@ interface FilterErrorContext {
|
|
|
665
765
|
declare class BymaxExceptionFilter implements ExceptionFilter {
|
|
666
766
|
private readonly options;
|
|
667
767
|
private readonly adapterHost;
|
|
768
|
+
private readonly unexpectedErrorSink?;
|
|
668
769
|
/** Clock used to stamp the envelope timestamp; overridable in tests via fake timers. */
|
|
669
770
|
private readonly now;
|
|
670
771
|
/** The resolved correlation provider, or the no-op fallback when none is bound. */
|
|
@@ -679,8 +780,10 @@ declare class BymaxExceptionFilter implements ExceptionFilter {
|
|
|
679
780
|
* (from their own, globally-visible module) is not shadowed by one; when
|
|
680
781
|
* nothing is bound, this falls back to a no-op that omits `correlationId`.
|
|
681
782
|
* @param adapterHost - Host of the live HTTP adapter, resolved lazily per catch.
|
|
783
|
+
* @param traceContext - Reader for the active trace; a no-op when unbound.
|
|
784
|
+
* @param unexpectedErrorSink - Receives every unexpected error; unbound by default.
|
|
682
785
|
*/
|
|
683
|
-
constructor(options: ResolvedCoreOptions, correlation: ICorrelationIdProvider | undefined, adapterHost: HttpAdapterHost, traceContext?: ITraceContextProvider);
|
|
786
|
+
constructor(options: ResolvedCoreOptions, correlation: ICorrelationIdProvider | undefined, adapterHost: HttpAdapterHost, traceContext?: ITraceContextProvider, unexpectedErrorSink?: IUnexpectedErrorSink | undefined);
|
|
684
787
|
/**
|
|
685
788
|
* Format the exception into the stable envelope and reply with it.
|
|
686
789
|
*
|
|
@@ -715,14 +818,25 @@ declare class BymaxExceptionFilter implements ExceptionFilter {
|
|
|
715
818
|
catch(exception: unknown, host: ArgumentsHost): void;
|
|
716
819
|
/**
|
|
717
820
|
* Select the mapping rule for the exception and build its envelope. An
|
|
718
|
-
* unknown error is handed to the observability seam
|
|
719
|
-
* an integration can record the original error with the
|
|
821
|
+
* unknown error is handed to the observability seam once its envelope is
|
|
822
|
+
* built, so an integration can record the original error with the context.
|
|
720
823
|
*
|
|
721
824
|
* @param exception - The error that escaped the handler.
|
|
722
825
|
* @param context - The neutral request context.
|
|
826
|
+
* @param request - The framework request, read only for the sink's route template.
|
|
723
827
|
* @returns The formatted envelope.
|
|
724
828
|
*/
|
|
725
829
|
private buildEnvelope;
|
|
830
|
+
/**
|
|
831
|
+
* Hand an unexpected error to the bound sink, if any. Whatever the sink does
|
|
832
|
+
* — throw, or return a promise that rejects — is absorbed: the response must
|
|
833
|
+
* not depend on it. See `IUnexpectedErrorSink` for the contract.
|
|
834
|
+
*
|
|
835
|
+
* @param error - The original thrown value.
|
|
836
|
+
* @param context - The neutral request context.
|
|
837
|
+
* @param request - The framework request, read for its route template only.
|
|
838
|
+
*/
|
|
839
|
+
private reportUnexpected;
|
|
726
840
|
/**
|
|
727
841
|
* Map an `HttpException` to the envelope. A domain error passes its own code,
|
|
728
842
|
* message and details through, whether it wrote them flat on the response or
|
|
@@ -758,7 +872,7 @@ declare class BymaxExceptionFilter implements ExceptionFilter {
|
|
|
758
872
|
private toEnvelope;
|
|
759
873
|
/**
|
|
760
874
|
* Observability seam invoked for every unexpected (non-`HttpException`) error
|
|
761
|
-
*
|
|
875
|
+
* collapsed to the generic 500. The base implementation is a no-op:
|
|
762
876
|
* this library owns no logger. An integration (for example
|
|
763
877
|
* `@bymax-one/nest-logger`) subclasses the filter and overrides this to record
|
|
764
878
|
* the original error with the current request context and correlation id.
|
|
@@ -1179,4 +1293,4 @@ declare const BYMAX_GATEWAY_TIMEOUT = "BYMAX_GATEWAY_TIMEOUT";
|
|
|
1179
1293
|
*/
|
|
1180
1294
|
declare function codeForStatus(status: number): string;
|
|
1181
1295
|
|
|
1182
|
-
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_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 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, buildErrorEnvelope, codeForStatus };
|
|
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 };
|
package/dist/index.mjs
CHANGED
|
@@ -158,6 +158,9 @@ var BYMAX_HEALTH_INDICATORS = /* @__PURE__ */ Symbol.for(
|
|
|
158
158
|
var BYMAX_HEALTH_TRANSITION_SINK = /* @__PURE__ */ Symbol.for(
|
|
159
159
|
"@bymax-one/nest-core:health-transition-sink"
|
|
160
160
|
);
|
|
161
|
+
var BYMAX_UNEXPECTED_ERROR_SINK = /* @__PURE__ */ Symbol.for(
|
|
162
|
+
"@bymax-one/nest-core:unexpected-error-sink"
|
|
163
|
+
);
|
|
161
164
|
var BYMAX_METRICS_REGISTRY = /* @__PURE__ */ Symbol.for(
|
|
162
165
|
"@bymax-one/nest-core:metrics-registry"
|
|
163
166
|
);
|
|
@@ -263,6 +266,13 @@ function buildTraceContextProvider() {
|
|
|
263
266
|
};
|
|
264
267
|
}
|
|
265
268
|
|
|
269
|
+
// src/contain-rejection.ts
|
|
270
|
+
function containRejection(returned, onFailure) {
|
|
271
|
+
if (typeof returned?.then === "function") {
|
|
272
|
+
Promise.resolve(returned).catch(onFailure);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
266
276
|
// src/envelope/error-codes.ts
|
|
267
277
|
var BYMAX_BAD_REQUEST = "BYMAX_BAD_REQUEST";
|
|
268
278
|
var BYMAX_VALIDATION_FAILED = "BYMAX_VALIDATION_FAILED";
|
|
@@ -326,10 +336,40 @@ function buildErrorEnvelope(input) {
|
|
|
326
336
|
};
|
|
327
337
|
}
|
|
328
338
|
|
|
339
|
+
// src/timing/request-info.accessor.ts
|
|
340
|
+
var UNMATCHED_ROUTE = "<unmatched>";
|
|
341
|
+
function readExpressTemplate(request) {
|
|
342
|
+
const path = request.route?.path;
|
|
343
|
+
return path === void 0 ? void 0 : `${request.baseUrl ?? ""}${path}`;
|
|
344
|
+
}
|
|
345
|
+
function readFastifyTemplate(request) {
|
|
346
|
+
return request.routeOptions?.url;
|
|
347
|
+
}
|
|
348
|
+
function readRequestInfo(request) {
|
|
349
|
+
const template = readExpressTemplate(request) ?? readFastifyTemplate(request);
|
|
350
|
+
return { method: request.method ?? "", route: template ?? UNMATCHED_ROUTE };
|
|
351
|
+
}
|
|
352
|
+
function extractRequestInfo(context) {
|
|
353
|
+
return readRequestInfo(context.switchToHttp().getRequest());
|
|
354
|
+
}
|
|
355
|
+
|
|
329
356
|
// src/envelope/exception.filter.ts
|
|
330
357
|
var INTERNAL_ERROR_STATUS = 500;
|
|
331
358
|
var INTERNAL_ERROR_MESSAGE = "Internal server error";
|
|
332
359
|
var VALIDATION_FAILED_MESSAGE = "Validation failed";
|
|
360
|
+
var ABSOLUTE_FORM_PREFIX = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
|
|
361
|
+
function pathOf(url) {
|
|
362
|
+
let target = String(url);
|
|
363
|
+
const prefix = ABSOLUTE_FORM_PREFIX.exec(target);
|
|
364
|
+
if (prefix !== null) {
|
|
365
|
+
const afterScheme = target.slice(prefix[0].length);
|
|
366
|
+
const authorityEnd = afterScheme.search(/[/?#]/);
|
|
367
|
+
target = authorityEnd === -1 ? "" : afterScheme.slice(authorityEnd);
|
|
368
|
+
}
|
|
369
|
+
const pathEnd = target.search(/[?#]/);
|
|
370
|
+
const path = pathEnd === -1 ? target : target.slice(0, pathEnd);
|
|
371
|
+
return path === "" ? "/" : path;
|
|
372
|
+
}
|
|
333
373
|
function resolveErrorCarrier(response) {
|
|
334
374
|
if (typeof response !== "object" || response === null) {
|
|
335
375
|
return void 0;
|
|
@@ -391,11 +431,16 @@ function extractHttpMessage(response, exception, carrier) {
|
|
|
391
431
|
}
|
|
392
432
|
return exception.message;
|
|
393
433
|
}
|
|
434
|
+
var UNREPRESENTABLE_MESSAGE = "Unrepresentable thrown value";
|
|
394
435
|
function buildInternalDetails(exception) {
|
|
395
|
-
|
|
396
|
-
|
|
436
|
+
try {
|
|
437
|
+
if (exception instanceof Error) {
|
|
438
|
+
return exception.stack !== void 0 ? { message: exception.message, stack: exception.stack } : { message: exception.message };
|
|
439
|
+
}
|
|
440
|
+
return { message: String(exception) };
|
|
441
|
+
} catch {
|
|
442
|
+
return { message: UNREPRESENTABLE_MESSAGE };
|
|
397
443
|
}
|
|
398
|
-
return { message: String(exception) };
|
|
399
444
|
}
|
|
400
445
|
var BymaxExceptionFilter = class {
|
|
401
446
|
/**
|
|
@@ -406,10 +451,13 @@ var BymaxExceptionFilter = class {
|
|
|
406
451
|
* (from their own, globally-visible module) is not shadowed by one; when
|
|
407
452
|
* nothing is bound, this falls back to a no-op that omits `correlationId`.
|
|
408
453
|
* @param adapterHost - Host of the live HTTP adapter, resolved lazily per catch.
|
|
454
|
+
* @param traceContext - Reader for the active trace; a no-op when unbound.
|
|
455
|
+
* @param unexpectedErrorSink - Receives every unexpected error; unbound by default.
|
|
409
456
|
*/
|
|
410
|
-
constructor(options, correlation, adapterHost, traceContext) {
|
|
457
|
+
constructor(options, correlation, adapterHost, traceContext, unexpectedErrorSink) {
|
|
411
458
|
this.options = options;
|
|
412
459
|
this.adapterHost = adapterHost;
|
|
460
|
+
this.unexpectedErrorSink = unexpectedErrorSink;
|
|
413
461
|
/** Clock used to stamp the envelope timestamp; overridable in tests via fake timers. */
|
|
414
462
|
this.now = () => /* @__PURE__ */ new Date();
|
|
415
463
|
this.correlation = correlation ?? new NoopCorrelationIdProvider();
|
|
@@ -464,23 +512,24 @@ var BymaxExceptionFilter = class {
|
|
|
464
512
|
const traceId = this.readAnnotation(() => this.traceContext.getTraceContext()?.traceId);
|
|
465
513
|
const context = {
|
|
466
514
|
method: String(httpAdapter.getRequestMethod(request)),
|
|
467
|
-
path:
|
|
515
|
+
path: pathOf(httpAdapter.getRequestUrl(request)),
|
|
468
516
|
...correlationId !== void 0 ? { correlationId } : {},
|
|
469
517
|
...traceId !== void 0 ? { traceId } : {}
|
|
470
518
|
};
|
|
471
|
-
const envelope = this.buildEnvelope(exception, context);
|
|
519
|
+
const envelope = this.buildEnvelope(exception, context, request);
|
|
472
520
|
httpAdapter.reply(response, envelope, envelope.statusCode);
|
|
473
521
|
}
|
|
474
522
|
/**
|
|
475
523
|
* Select the mapping rule for the exception and build its envelope. An
|
|
476
|
-
* unknown error is handed to the observability seam
|
|
477
|
-
* an integration can record the original error with the
|
|
524
|
+
* unknown error is handed to the observability seam once its envelope is
|
|
525
|
+
* built, so an integration can record the original error with the context.
|
|
478
526
|
*
|
|
479
527
|
* @param exception - The error that escaped the handler.
|
|
480
528
|
* @param context - The neutral request context.
|
|
529
|
+
* @param request - The framework request, read only for the sink's route template.
|
|
481
530
|
* @returns The formatted envelope.
|
|
482
531
|
*/
|
|
483
|
-
buildEnvelope(exception, context) {
|
|
532
|
+
buildEnvelope(exception, context, request) {
|
|
484
533
|
if (exception instanceof HttpException) {
|
|
485
534
|
return this.mapHttpException(exception, context);
|
|
486
535
|
}
|
|
@@ -493,11 +542,39 @@ var BymaxExceptionFilter = class {
|
|
|
493
542
|
context
|
|
494
543
|
);
|
|
495
544
|
}
|
|
545
|
+
const envelope = this.mapUnknown(exception, context);
|
|
496
546
|
try {
|
|
497
547
|
this.onUnexpectedError(exception, context);
|
|
498
548
|
} catch {
|
|
499
549
|
}
|
|
500
|
-
|
|
550
|
+
this.reportUnexpected(exception, context, request);
|
|
551
|
+
return envelope;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Hand an unexpected error to the bound sink, if any. Whatever the sink does
|
|
555
|
+
* — throw, or return a promise that rejects — is absorbed: the response must
|
|
556
|
+
* not depend on it. See `IUnexpectedErrorSink` for the contract.
|
|
557
|
+
*
|
|
558
|
+
* @param error - The original thrown value.
|
|
559
|
+
* @param context - The neutral request context.
|
|
560
|
+
* @param request - The framework request, read for its route template only.
|
|
561
|
+
*/
|
|
562
|
+
reportUnexpected(error, context, request) {
|
|
563
|
+
const sink = this.unexpectedErrorSink;
|
|
564
|
+
if (sink === void 0) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
const report = {
|
|
569
|
+
error,
|
|
570
|
+
method: context.method,
|
|
571
|
+
route: readRequestInfo(request).route,
|
|
572
|
+
...context.correlationId !== void 0 ? { correlationId: context.correlationId } : {},
|
|
573
|
+
...context.traceId !== void 0 ? { traceId: context.traceId } : {}
|
|
574
|
+
};
|
|
575
|
+
containRejection(sink.record(report), () => void 0);
|
|
576
|
+
} catch {
|
|
577
|
+
}
|
|
501
578
|
}
|
|
502
579
|
/**
|
|
503
580
|
* Map an `HttpException` to the envelope. A domain error passes its own code,
|
|
@@ -587,7 +664,7 @@ var BymaxExceptionFilter = class {
|
|
|
587
664
|
}
|
|
588
665
|
/**
|
|
589
666
|
* Observability seam invoked for every unexpected (non-`HttpException`) error
|
|
590
|
-
*
|
|
667
|
+
* collapsed to the generic 500. The base implementation is a no-op:
|
|
591
668
|
* this library owns no logger. An integration (for example
|
|
592
669
|
* `@bymax-one/nest-logger`) subclasses the filter and overrides this to record
|
|
593
670
|
* the original error with the current request context and correlation id.
|
|
@@ -606,7 +683,9 @@ BymaxExceptionFilter = __decorateClass([
|
|
|
606
683
|
__decorateParam(1, Inject(BYMAX_CORRELATION_PROVIDER)),
|
|
607
684
|
__decorateParam(2, Inject(HttpAdapterHost)),
|
|
608
685
|
__decorateParam(3, Optional()),
|
|
609
|
-
__decorateParam(3, Inject(BYMAX_TRACE_CONTEXT))
|
|
686
|
+
__decorateParam(3, Inject(BYMAX_TRACE_CONTEXT)),
|
|
687
|
+
__decorateParam(4, Optional()),
|
|
688
|
+
__decorateParam(4, Inject(BYMAX_UNEXPECTED_ERROR_SINK))
|
|
610
689
|
], BymaxExceptionFilter);
|
|
611
690
|
var PassThroughExceptionFilter = class {
|
|
612
691
|
constructor(adapterHost) {
|
|
@@ -633,15 +712,8 @@ function assertAsyncFeatureEnabled(feature, enabled) {
|
|
|
633
712
|
);
|
|
634
713
|
}
|
|
635
714
|
}
|
|
636
|
-
function selectAsyncExceptionFilter(options, correlation, adapterHost) {
|
|
637
|
-
return options.envelope.enabled ? new BymaxExceptionFilter(options, correlation, adapterHost) : new PassThroughExceptionFilter(adapterHost);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// src/contain-rejection.ts
|
|
641
|
-
function containRejection(returned, onFailure) {
|
|
642
|
-
if (typeof returned?.then === "function") {
|
|
643
|
-
Promise.resolve(returned).catch(onFailure);
|
|
644
|
-
}
|
|
715
|
+
function selectAsyncExceptionFilter(options, correlation, adapterHost, traceContext, unexpectedErrorSink) {
|
|
716
|
+
return options.envelope.enabled ? new BymaxExceptionFilter(options, correlation, adapterHost, traceContext, unexpectedErrorSink) : new PassThroughExceptionFilter(adapterHost);
|
|
645
717
|
}
|
|
646
718
|
|
|
647
719
|
// src/discovery.ts
|
|
@@ -1352,23 +1424,6 @@ function isFastifyInstance(instance) {
|
|
|
1352
1424
|
return typeof instance?.addHook === "function";
|
|
1353
1425
|
}
|
|
1354
1426
|
|
|
1355
|
-
// src/timing/request-info.accessor.ts
|
|
1356
|
-
var UNMATCHED_ROUTE = "<unmatched>";
|
|
1357
|
-
function readExpressTemplate(request) {
|
|
1358
|
-
const path = request.route?.path;
|
|
1359
|
-
return path === void 0 ? void 0 : `${request.baseUrl ?? ""}${path}`;
|
|
1360
|
-
}
|
|
1361
|
-
function readFastifyTemplate(request) {
|
|
1362
|
-
return request.routeOptions?.url;
|
|
1363
|
-
}
|
|
1364
|
-
function readRequestInfo(request) {
|
|
1365
|
-
const template = readExpressTemplate(request) ?? readFastifyTemplate(request);
|
|
1366
|
-
return { method: request.method ?? "", route: template ?? UNMATCHED_ROUTE };
|
|
1367
|
-
}
|
|
1368
|
-
function extractRequestInfo(context) {
|
|
1369
|
-
return readRequestInfo(context.switchToHttp().getRequest());
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
1427
|
// src/timing/timing.sample.ts
|
|
1373
1428
|
function readTraceContext(traceContext) {
|
|
1374
1429
|
try {
|
|
@@ -1527,11 +1582,22 @@ function buildAsyncSlots() {
|
|
|
1527
1582
|
return [
|
|
1528
1583
|
{
|
|
1529
1584
|
provide: APP_FILTER,
|
|
1530
|
-
useFactory: (options, correlation, adapterHost) => selectAsyncExceptionFilter(
|
|
1585
|
+
useFactory: (options, correlation, adapterHost, traceContext, unexpectedErrorSink) => selectAsyncExceptionFilter(
|
|
1586
|
+
options,
|
|
1587
|
+
correlation,
|
|
1588
|
+
adapterHost,
|
|
1589
|
+
traceContext,
|
|
1590
|
+
unexpectedErrorSink
|
|
1591
|
+
),
|
|
1592
|
+
// Every constructor dependency the sync path resolves through `useClass`
|
|
1593
|
+
// has to be listed here by hand; one left out reaches the filter as its
|
|
1594
|
+
// no-op fallback and nothing fails.
|
|
1531
1595
|
inject: [
|
|
1532
1596
|
BYMAX_CORE_OPTIONS,
|
|
1533
1597
|
{ token: BYMAX_CORRELATION_PROVIDER, optional: true },
|
|
1534
|
-
HttpAdapterHost
|
|
1598
|
+
HttpAdapterHost,
|
|
1599
|
+
BYMAX_TRACE_CONTEXT,
|
|
1600
|
+
{ token: BYMAX_UNEXPECTED_ERROR_SINK, optional: true }
|
|
1535
1601
|
]
|
|
1536
1602
|
}
|
|
1537
1603
|
];
|
|
@@ -1811,4 +1877,4 @@ TimingInterceptor = __decorateClass([
|
|
|
1811
1877
|
__decorateParam(3, Inject(BYMAX_TRACE_CONTEXT))
|
|
1812
1878
|
], TimingInterceptor);
|
|
1813
1879
|
|
|
1814
|
-
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_UNPROCESSABLE_ENTITY, BYMAX_UNSUPPORTED_MEDIA_TYPE, BYMAX_VALIDATION_FAILED, BymaxCoreModule, BymaxExceptionFilter, BymaxTimingMiddleware, TimingInterceptor, UNMATCHED_ROUTE, buildErrorEnvelope, codeForStatus };
|
|
1880
|
+
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 };
|
package/dist/openapi/index.cjs
CHANGED
|
@@ -594,14 +594,13 @@ function augmentDocument(document, options, pathPrefixes = [""], contributions =
|
|
|
594
594
|
const merged = new Map(Object.entries(components));
|
|
595
595
|
if (openapi.includeCoreSchemas) {
|
|
596
596
|
const keep = () => void 0;
|
|
597
|
-
|
|
598
|
-
"schemas",
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
);
|
|
597
|
+
const catalogue = [
|
|
598
|
+
["schemas", CORE_SCHEMAS],
|
|
599
|
+
["parameters", CORE_PARAMETERS]
|
|
600
|
+
];
|
|
601
|
+
for (const [member, entries] of catalogue) {
|
|
602
|
+
merged.set(member, placeComponentEntries(merged.get(member), member, entries, keep));
|
|
603
|
+
}
|
|
605
604
|
}
|
|
606
605
|
const scrapeScheme = options.metrics.authToken === void 0 ? {} : {
|
|
607
606
|
[METRICS_SCHEME_NAME]: {
|
|
@@ -738,7 +737,7 @@ function warnDiscardedComponents(logger, discarded) {
|
|
|
738
737
|
const elided = discarded.length - MAX_WARNED_OPERATIONS;
|
|
739
738
|
const listed = discarded.slice(0, MAX_WARNED_OPERATIONS).map(({ contributor, member, name }) => `${contributor} -> ${member}.${name}`).join(", ");
|
|
740
739
|
logger.warn(
|
|
741
|
-
`the served OpenAPI document already defines ${discarded.length} component entry(ies) a library also contributes: ${listed}${elided > 0 ? `, and ${elided} more` : ""}. The contributed version is not served, so a client generated from this document will not match that library's own types. What holds the name may be the application's own declaration, a core schema under openapi.includeCoreSchemas, or a contributor that sorted earlier \u2014 read the document before deciding what to remove.`
|
|
740
|
+
`the served OpenAPI document already defines ${discarded.length} component entry(ies) a library also contributes: ${listed}${elided > 0 ? `, and ${elided} more` : ""}. The contributed version is not served, so where the two differ a client generated from this document will not match that library's own types. What holds the name may be the application's own declaration, a core schema under openapi.includeCoreSchemas, or a contributor that sorted earlier \u2014 read the document before deciding what to remove.`
|
|
742
741
|
);
|
|
743
742
|
}
|
|
744
743
|
function buildConfig(builder, options) {
|
package/dist/openapi/index.mjs
CHANGED
|
@@ -592,14 +592,13 @@ function augmentDocument(document, options, pathPrefixes = [""], contributions =
|
|
|
592
592
|
const merged = new Map(Object.entries(components));
|
|
593
593
|
if (openapi.includeCoreSchemas) {
|
|
594
594
|
const keep = () => void 0;
|
|
595
|
-
|
|
596
|
-
"schemas",
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
);
|
|
595
|
+
const catalogue = [
|
|
596
|
+
["schemas", CORE_SCHEMAS],
|
|
597
|
+
["parameters", CORE_PARAMETERS]
|
|
598
|
+
];
|
|
599
|
+
for (const [member, entries] of catalogue) {
|
|
600
|
+
merged.set(member, placeComponentEntries(merged.get(member), member, entries, keep));
|
|
601
|
+
}
|
|
603
602
|
}
|
|
604
603
|
const scrapeScheme = options.metrics.authToken === void 0 ? {} : {
|
|
605
604
|
[METRICS_SCHEME_NAME]: {
|
|
@@ -736,7 +735,7 @@ function warnDiscardedComponents(logger, discarded) {
|
|
|
736
735
|
const elided = discarded.length - MAX_WARNED_OPERATIONS;
|
|
737
736
|
const listed = discarded.slice(0, MAX_WARNED_OPERATIONS).map(({ contributor, member, name }) => `${contributor} -> ${member}.${name}`).join(", ");
|
|
738
737
|
logger.warn(
|
|
739
|
-
`the served OpenAPI document already defines ${discarded.length} component entry(ies) a library also contributes: ${listed}${elided > 0 ? `, and ${elided} more` : ""}. The contributed version is not served, so a client generated from this document will not match that library's own types. What holds the name may be the application's own declaration, a core schema under openapi.includeCoreSchemas, or a contributor that sorted earlier \u2014 read the document before deciding what to remove.`
|
|
738
|
+
`the served OpenAPI document already defines ${discarded.length} component entry(ies) a library also contributes: ${listed}${elided > 0 ? `, and ${elided} more` : ""}. The contributed version is not served, so where the two differ a client generated from this document will not match that library's own types. What holds the name may be the application's own declaration, a core schema under openapi.includeCoreSchemas, or a contributor that sorted earlier \u2014 read the document before deciding what to remove.`
|
|
740
739
|
);
|
|
741
740
|
}
|
|
742
741
|
function buildConfig(builder, options) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bymax-one/nest-core",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.3",
|
|
4
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.",
|
|
5
5
|
"author": "Bymax One <support@bymax.one>",
|
|
6
6
|
"license": "MIT",
|