@bymax-one/nest-core 1.6.4 → 1.6.6
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 +71 -1
- package/README.md +98 -43
- package/dist/index.cjs +11 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +11 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,74 @@ heading here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.6.6] - 2026-11-07
|
|
15
|
+
|
|
16
|
+
A patch release. The error envelope no longer returns the parser's own text for a request refused before
|
|
17
|
+
routing; see Fixed.
|
|
18
|
+
|
|
19
|
+
**Apply to a derived backend:** `pnpm up @bymax-one/nest-core`. A client that matched on those
|
|
20
|
+
message strings must match `code` instead. A middleware that throws a plain `BadRequestException`
|
|
21
|
+
before routing now answers `Malformed request` with the same 400; to keep its message, throw it with
|
|
22
|
+
a `code` (`new HttpException({ code: 'TENANT_REQUIRED', message }, 400)`).
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **A request refused before routing no longer has the parser's own text returned.** The message of
|
|
27
|
+
a body-parser, router or decoding refusal quotes the request: V8's JSON excerpt (on a login body
|
|
28
|
+
with a missing value it ends in the password's last characters), a charset or content-encoding
|
|
29
|
+
header value, or the path. The status is unchanged and the envelope keeps its shape, but the
|
|
30
|
+
message is now fixed per status. It is chosen from the error's own `type` (the body pipeline's
|
|
31
|
+
refusals carry one) or, for the `SyntaxError` and `URIError` that Nest rewrites into a plain
|
|
32
|
+
`BadRequestException`, from a 400 raised while the request matched no route. Not covered: Fastify,
|
|
33
|
+
whose body errors take another path and were not measured here.
|
|
34
|
+
|
|
35
|
+
## [1.6.5] - 2026-10-31
|
|
36
|
+
|
|
37
|
+
A documentation release. The compiled JavaScript is byte-identical to `1.6.4`: what changes in the
|
|
38
|
+
package is the README and comments in the type declarations.
|
|
39
|
+
|
|
40
|
+
**Apply to a derived backend:** `pnpm up @bymax-one/nest-core`. No code change is needed.
|
|
41
|
+
|
|
42
|
+
### Documentation
|
|
43
|
+
|
|
44
|
+
- **README snippets are corrected so they compile.** The metrics `authToken` example no longer passes
|
|
45
|
+
`string | undefined` where `exactOptionalPropertyTypes` requires `string`; the `environment` example
|
|
46
|
+
reads the validated value with `getOrThrow`; the contributor example imports `Injectable`; and the
|
|
47
|
+
`authOperationSecurity` example is a complete function and a complete `forRoot` call.
|
|
48
|
+
- **The README lists every error-code constant and the OpenAPI contract version.** Each code is in a
|
|
49
|
+
catalog beside the status it maps from, and `BYMAX_OPENAPI_CONTRACT_VERSION` is documented and used
|
|
50
|
+
by the contributor example instead of a literal `1`.
|
|
51
|
+
- **The README says this package mints no request id.** `correlationId` is whatever the bound
|
|
52
|
+
`BYMAX_CORRELATION_PROVIDER` returns, and the id comes from what the application binds, such as the
|
|
53
|
+
`@bymax-one/nest-logger` request-id middleware.
|
|
54
|
+
- **The README explains why `TimingInterceptor` is deprecated.** It records a second sample for every
|
|
55
|
+
request that reaches a handler when wired next to the middleware, and it never saw a request a guard
|
|
56
|
+
rejected.
|
|
57
|
+
- **The `TimingOptions` declarations name the middleware.** `timing.enabled` registers
|
|
58
|
+
`BymaxTimingMiddleware`, not an interceptor.
|
|
59
|
+
- **The README's `RequestTimingSample` snippet lists the two optional trace fields.** The interface
|
|
60
|
+
carries `traceId?` and `spanId?`, present when telemetry is enabled and a span was recording.
|
|
61
|
+
|
|
62
|
+
### Tests
|
|
63
|
+
|
|
64
|
+
- **`pnpm check:published` compiles every README snippet through the TypeScript API.** Syntax and type
|
|
65
|
+
errors are reported together, snippets that do not import the package are checked too, a relative
|
|
66
|
+
import resolves to the snippet that exports the name, and the names a snippet takes from its prose
|
|
67
|
+
are typed in `scripts/readme-prelude.d.ts`, so a wrong method, option or field fails the gate
|
|
68
|
+
instead of resolving to `any`. An option misspelled in a `forRootAsync` factory fails too.
|
|
69
|
+
- **`pnpm check:published` fails on three more kinds of README defect.** A syntax error in a snippet,
|
|
70
|
+
a prelude import that stopped resolving, and a snippet that restates a shipped interface with a
|
|
71
|
+
member added, dropped or retyped.
|
|
72
|
+
- **The README snippet gate no longer exempts a type error on an imported type or a union.** The
|
|
73
|
+
exemption for an abbreviated local class now reads only the snippet's own declarations, accepts a
|
|
74
|
+
plain identifier only, and handles a `$` in a class name.
|
|
75
|
+
- **The README's error-code catalog and contract version are pinned to the code.** A spec fails when an
|
|
76
|
+
exported code constant has no row, a row names a status `codeForStatus` does not map to it, or the
|
|
77
|
+
README states a contract version other than `BYMAX_OPENAPI_CONTRACT_VERSION`.
|
|
78
|
+
- **The README's option tables are pinned to the resolved defaults.** A spec parses every table under
|
|
79
|
+
the Configuration section and fails when a documented default, or a documented option, differs from
|
|
80
|
+
`DEFAULT_CORE_OPTIONS`, or when a resolved option has no row.
|
|
81
|
+
|
|
14
82
|
## [1.6.4] - 2026-10-30
|
|
15
83
|
|
|
16
84
|
A 5xx envelope no longer carries `details` unless `envelope.exposeInternals` is on, and the
|
|
@@ -1266,4 +1334,6 @@ have regressed from. They are kept because the reasoning is worth having.
|
|
|
1266
1334
|
[1.6.2]: https://github.com/bymaxone/nest-core/compare/v1.6.1...v1.6.2
|
|
1267
1335
|
[1.6.3]: https://github.com/bymaxone/nest-core/compare/v1.6.2...v1.6.3
|
|
1268
1336
|
[1.6.4]: https://github.com/bymaxone/nest-core/compare/v1.6.3...v1.6.4
|
|
1269
|
-
[
|
|
1337
|
+
[1.6.5]: https://github.com/bymaxone/nest-core/compare/v1.6.4...v1.6.5
|
|
1338
|
+
[1.6.6]: https://github.com/bymaxone/nest-core/compare/v1.6.5...v1.6.6
|
|
1339
|
+
[Unreleased]: https://github.com/bymaxone/nest-core/compare/v1.6.6...HEAD
|
package/README.md
CHANGED
|
@@ -273,8 +273,9 @@ is rejected at boot rather than silently ignored, so a mistyped secret fails lou
|
|
|
273
273
|
instead of leaving the endpoint open:
|
|
274
274
|
|
|
275
275
|
```ts
|
|
276
|
+
const token = process.env.METRICS_TOKEN
|
|
276
277
|
BymaxCoreModule.forRoot({
|
|
277
|
-
metrics: { enabled: true, authToken:
|
|
278
|
+
metrics: { enabled: true, ...(token === undefined ? {} : { authToken: token }) }
|
|
278
279
|
})
|
|
279
280
|
// Scrape: curl -H "Authorization: Bearer $METRICS_TOKEN" http://host/metrics
|
|
280
281
|
```
|
|
@@ -356,10 +357,14 @@ place that knows it:
|
|
|
356
357
|
|
|
357
358
|
```ts
|
|
358
359
|
// in the library
|
|
359
|
-
export function authOperationSecurity(prefix = ''): OperationSecurityMap {
|
|
360
|
+
export function authOperationSecurity(prefix = ''): OperationSecurityMap {
|
|
361
|
+
return {} /* … */
|
|
362
|
+
}
|
|
360
363
|
|
|
361
364
|
// in the application
|
|
362
|
-
|
|
365
|
+
BymaxCoreModule.forRoot({
|
|
366
|
+
openapi: { operationSecurity: { ...authOperationSecurity('api'), ...myOwnOverrides } }
|
|
367
|
+
})
|
|
363
368
|
```
|
|
364
369
|
|
|
365
370
|
A requirement naming a scheme that is not declared **fails the document build**
|
|
@@ -569,6 +574,13 @@ follows this exact, versioned shape:
|
|
|
569
574
|
| `timestamp` | string (ISO 8601) | always | Time the error was formatted. |
|
|
570
575
|
| `path` | string | always | Request URL path, never the query string. The router's own 404 `message` names the same path. |
|
|
571
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
|
+
|
|
572
584
|
**A server error carries no `details` by default.** For a status of 500 or above, the filter omits `details`
|
|
573
585
|
from the body, whichever library threw the `HttpException` and whether it wrote them flat or
|
|
574
586
|
under `error`; the `code` and `message` stay, because they are text the thrower chose for the
|
|
@@ -595,6 +607,29 @@ import { BadRequestException } from '@nestjs/common'
|
|
|
595
607
|
throw new BadRequestException({ code: 'INVOICE_OVERDUE', message: 'Invoice is overdue' })
|
|
596
608
|
```
|
|
597
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
|
+
|
|
598
633
|
### What the filter classifies from, and what it cannot
|
|
599
634
|
|
|
600
635
|
An error raised before any handler ran — a malformed JSON body, a payload over
|
|
@@ -604,6 +639,12 @@ marks itself `expose: true` with a `4xx` status, which is the `http-errors`
|
|
|
604
639
|
convention Node's body pipeline follows. Nothing in the application failed, so
|
|
605
640
|
it is not routed through the unexpected-error seam either.
|
|
606
641
|
|
|
642
|
+
The message of such a refusal is fixed per status — `Malformed request` (400),
|
|
643
|
+
`Request entity too large` (413), `Unsupported media type` (415) — because the
|
|
644
|
+
parser's own text quotes the request: a JSON excerpt, a header value, the path.
|
|
645
|
+
A `BadRequestException` a middleware throws before routing gets the same 400
|
|
646
|
+
message; throw it with a `code` to keep its own. Fastify is not covered.
|
|
647
|
+
|
|
607
648
|
**An error that carries no such marking is a 500, including when a client
|
|
608
649
|
caused it.** The clearest case is depth: a body of a few kilobytes nested
|
|
609
650
|
thousands of levels deep overflows the stack during validation and surfaces as
|
|
@@ -753,9 +794,17 @@ export interface RequestTimingSample {
|
|
|
753
794
|
statusCode: number
|
|
754
795
|
durationMs: number
|
|
755
796
|
slow: boolean
|
|
797
|
+
traceId?: string
|
|
798
|
+
spanId?: string
|
|
756
799
|
}
|
|
757
800
|
```
|
|
758
801
|
|
|
802
|
+
`TimingInterceptor` is still exported and is **deprecated**: nothing registers it. It
|
|
803
|
+
was the recorder until `BymaxTimingMiddleware` replaced it, because guards run
|
|
804
|
+
before interceptors and a request a guard rejected never reached it. Registering it
|
|
805
|
+
as well records a second sample for every request that reaches a handler, so leave
|
|
806
|
+
it out.
|
|
807
|
+
|
|
759
808
|
### Rejected requests are counted too
|
|
760
809
|
|
|
761
810
|
The recorder is middleware (`BymaxTimingMiddleware`), applied to every route by
|
|
@@ -1296,7 +1345,7 @@ BymaxCoreModule.forRootAsync({
|
|
|
1296
1345
|
inject: [ConfigService],
|
|
1297
1346
|
useFactory: (config: ConfigService) => ({
|
|
1298
1347
|
// Your validated value, not a second copy of NODE_ENV.
|
|
1299
|
-
environment: config.
|
|
1348
|
+
environment: config.getOrThrow<string>('APP_ENV'),
|
|
1300
1349
|
openapi: { enabled: true }
|
|
1301
1350
|
})
|
|
1302
1351
|
})
|
|
@@ -1399,7 +1448,11 @@ So a library marks a provider and returns fragments keyed by **handler
|
|
|
1399
1448
|
identity**, which survives every prefix, version and mount point:
|
|
1400
1449
|
|
|
1401
1450
|
```ts
|
|
1402
|
-
import {
|
|
1451
|
+
import { Injectable } from '@nestjs/common'
|
|
1452
|
+
import {
|
|
1453
|
+
BymaxOpenApiContributor,
|
|
1454
|
+
BYMAX_OPENAPI_CONTRACT_VERSION
|
|
1455
|
+
} from '@bymax-one/nest-core/openapi'
|
|
1403
1456
|
import type { IOpenApiContributor, OpenApiFragment } from '@bymax-one/nest-core/openapi'
|
|
1404
1457
|
|
|
1405
1458
|
@BymaxOpenApiContributor()
|
|
@@ -1412,7 +1465,7 @@ export class AuthOpenApi implements IOpenApiContributor {
|
|
|
1412
1465
|
// Required, and declared rather than inferred: a fragment crosses a
|
|
1413
1466
|
// boundary between independently released packages, where each side
|
|
1414
1467
|
// type-checked against its own copy.
|
|
1415
|
-
contractVersion:
|
|
1468
|
+
contractVersion: BYMAX_OPENAPI_CONTRACT_VERSION,
|
|
1416
1469
|
components: {
|
|
1417
1470
|
securitySchemes: {
|
|
1418
1471
|
// Derived from resolved options — which is why this cannot be a
|
|
@@ -1441,14 +1494,15 @@ library that is never imported contributes nothing.
|
|
|
1441
1494
|
|
|
1442
1495
|
#### What the merge guarantees
|
|
1443
1496
|
|
|
1444
|
-
| Rule | Behavior
|
|
1445
|
-
| --------------------- |
|
|
1446
|
-
| Marked, not shaped | Only providers carrying the marker are called. A class that merely exposes `contributeOpenApi` is never touched.
|
|
1447
|
-
| Called once | While the document is built, after options resolve — so a contributor may derive its contribution from its own configuration.
|
|
1448
|
-
| Stable order | Contributors run sorted by class name, so two libraries describing the same operation resolve the same way on every boot.
|
|
1449
|
-
| Data, not mutation | A contributor returns fragments; this package decides what to write. That is what makes precedence enforceable.
|
|
1450
|
-
| 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.
|
|
1451
|
-
|
|
|
1497
|
+
| Rule | Behavior |
|
|
1498
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1499
|
+
| Marked, not shaped | Only providers carrying the marker are called. A class that merely exposes `contributeOpenApi` is never touched. |
|
|
1500
|
+
| Called once | While the document is built, after options resolve — so a contributor may derive its contribution from its own configuration. |
|
|
1501
|
+
| Stable order | Contributors run sorted by class name, so two libraries describing the same operation resolve the same way on every boot. |
|
|
1502
|
+
| Data, not mutation | A contributor returns fragments; this package decides what to write. That is what makes precedence enforceable. |
|
|
1503
|
+
| 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. |
|
|
1504
|
+
| 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. |
|
|
1505
|
+
| Off with the document | With `openapi.enabled` false, no contributor runs. |
|
|
1452
1506
|
|
|
1453
1507
|
**Precedence, weakest first:** what this package infers about its own routes,
|
|
1454
1508
|
then what a library contributed, then what the consumer configured, and above
|
|
@@ -1855,27 +1909,27 @@ in the sections above.
|
|
|
1855
1909
|
|
|
1856
1910
|
### `.` (root)
|
|
1857
1911
|
|
|
1858
|
-
| Export | Kind | Description
|
|
1859
|
-
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- |
|
|
1860
|
-
| `BymaxCoreModule` | class | The dynamic module: `forRoot` and `forRootAsync`.
|
|
1861
|
-
| `BymaxCoreModuleOptions`, `EnvelopeOptions`, `TimingOptions`, `HealthOptions`, `MetricsOptions`, `TelemetryOptions`, `OpenApiOptions`, `OpenApiServerDescriptor`, `OpenApiSecurityScheme`, `ResolvedCoreOptions` | types | The options surface and its resolved shape.
|
|
1862
|
-
| `OpenApiSecurityRequirement`, `OpenApiHttpMethod`, `OpenApiOperationKey`, `OperationSecurityMap` | types | The operation-key contract a sibling library targets to ship its own security map.
|
|
1863
|
-
| `OpenApiOperationIdFactory` | type | Names the operations in the generated document.
|
|
1864
|
-
| `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).
|
|
1865
|
-
| `IHealthTransitionSink`, `HealthTransition`, `HealthTransitionCause` | types | The readiness-transition contract; also on `./health`.
|
|
1866
|
-
| `ICorrelationIdProvider` | type | The correlation-provider contract.
|
|
1867
|
-
| `ITraceContextProvider`, `TraceContext` | types | The trace-context contract and the identifiers it resolves.
|
|
1868
|
-
| `BymaxExceptionFilter` | class | The envelope exception filter.
|
|
1869
|
-
| `FilterErrorContext` | type | The neutral request context passed to the filter's observability seam.
|
|
1870
|
-
| `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).
|
|
1871
|
-
| `buildErrorEnvelope` | function | Pure builder assembling an `ErrorEnvelope`.
|
|
1872
|
-
| `ErrorEnvelope`, `ErrorDetails`, `BuildErrorEnvelopeInput` | types | The envelope contract and its builder input.
|
|
1873
|
-
| `BymaxTimingMiddleware` | class | The request-timing middleware, applied to every route when timing is enabled.
|
|
1874
|
-
| `UNMATCHED_ROUTE` | constant | The bounded label recorded when a request matched no route (`<unmatched>`).
|
|
1875
|
-
| `TimingInterceptor` | class | Deprecated
|
|
1876
|
-
| `ITimingSink`, `RequestTimingSample` | types | The timing-sink contract and its sample shape.
|
|
1877
|
-
| `BYMAX_BAD_GATEWAY` … `BYMAX_VALIDATION_FAILED` | constants | The full error-code catalog (see [Error envelope](#-error-envelope)).
|
|
1878
|
-
| `codeForStatus` | function | Derives a catalog code from an HTTP status.
|
|
1912
|
+
| Export | Kind | Description |
|
|
1913
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1914
|
+
| `BymaxCoreModule` | class | The dynamic module: `forRoot` and `forRootAsync`. |
|
|
1915
|
+
| `BymaxCoreModuleOptions`, `EnvelopeOptions`, `TimingOptions`, `HealthOptions`, `MetricsOptions`, `TelemetryOptions`, `OpenApiOptions`, `OpenApiServerDescriptor`, `OpenApiSecurityScheme`, `ResolvedCoreOptions` | types | The options surface and its resolved shape. |
|
|
1916
|
+
| `OpenApiSecurityRequirement`, `OpenApiHttpMethod`, `OpenApiOperationKey`, `OperationSecurityMap` | types | The operation-key contract a sibling library targets to ship its own security map. |
|
|
1917
|
+
| `OpenApiOperationIdFactory` | type | Names the operations in the generated document. |
|
|
1918
|
+
| `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). |
|
|
1919
|
+
| `IHealthTransitionSink`, `HealthTransition`, `HealthTransitionCause` | types | The readiness-transition contract; also on `./health`. |
|
|
1920
|
+
| `ICorrelationIdProvider` | type | The correlation-provider contract. |
|
|
1921
|
+
| `ITraceContextProvider`, `TraceContext` | types | The trace-context contract and the identifiers it resolves. |
|
|
1922
|
+
| `BymaxExceptionFilter` | class | The envelope exception filter. |
|
|
1923
|
+
| `FilterErrorContext` | type | The neutral request context passed to the filter's observability seam. |
|
|
1924
|
+
| `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). |
|
|
1925
|
+
| `buildErrorEnvelope` | function | Pure builder assembling an `ErrorEnvelope`. |
|
|
1926
|
+
| `ErrorEnvelope`, `ErrorDetails`, `BuildErrorEnvelopeInput` | types | The envelope contract and its builder input. |
|
|
1927
|
+
| `BymaxTimingMiddleware` | class | The request-timing middleware, applied to every route when timing is enabled. |
|
|
1928
|
+
| `UNMATCHED_ROUTE` | constant | The bounded label recorded when a request matched no route (`<unmatched>`). |
|
|
1929
|
+
| `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. |
|
|
1930
|
+
| `ITimingSink`, `RequestTimingSample` | types | The timing-sink contract and its sample shape. |
|
|
1931
|
+
| `BYMAX_BAD_GATEWAY` … `BYMAX_VALIDATION_FAILED` | constants | The full error-code catalog (see [Error envelope](#-error-envelope)). |
|
|
1932
|
+
| `codeForStatus` | function | Derives a catalog code from an HTTP status. |
|
|
1879
1933
|
|
|
1880
1934
|
### `./pagination`
|
|
1881
1935
|
|
|
@@ -1911,14 +1965,15 @@ in the sections above.
|
|
|
1911
1965
|
|
|
1912
1966
|
### `./openapi`
|
|
1913
1967
|
|
|
1914
|
-
| Export | Kind | Description
|
|
1915
|
-
| -------------------------------------------------------------------------------------- | ------------------- |
|
|
1916
|
-
| `applyBymaxOpenApi` | function | Builds and mounts the document; call it before `app.listen()`.
|
|
1917
|
-
| `OpenApiMountOutcome` | type | What the helper did: mounted at a path, or skipped with a reason.
|
|
1918
|
-
| `DiscardedComponent` | type | A contributed component entry the document already declared.
|
|
1919
|
-
| `OpenApiSkipReason` | type | Why it was skipped: `'disabled'` or `'production'`.
|
|
1920
|
-
| `BymaxOpenApiContributor`, `BYMAX_OPENAPI_CONTRIBUTOR_METADATA` | decorator, constant | Marks a provider as describing its own routes, and the metadata key behind it.
|
|
1921
|
-
| `
|
|
1968
|
+
| Export | Kind | Description |
|
|
1969
|
+
| -------------------------------------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------- |
|
|
1970
|
+
| `applyBymaxOpenApi` | function | Builds and mounts the document; call it before `app.listen()`. |
|
|
1971
|
+
| `OpenApiMountOutcome` | type | What the helper did: mounted at a path, or skipped with a reason. |
|
|
1972
|
+
| `DiscardedComponent` | type | A contributed component entry the document already declared. |
|
|
1973
|
+
| `OpenApiSkipReason` | type | Why it was skipped: `'disabled'` or `'production'`. |
|
|
1974
|
+
| `BymaxOpenApiContributor`, `BYMAX_OPENAPI_CONTRIBUTOR_METADATA` | decorator, constant | Marks a provider as describing its own routes, and the metadata key behind it. |
|
|
1975
|
+
| `BYMAX_OPENAPI_CONTRACT_VERSION` | constant | The version of the fragment contract, currently `1`; a fragment declares it as `contractVersion`. |
|
|
1976
|
+
| `IOpenApiContributor`, `OpenApiFragment`, `OpenApiFragmentObject`, `OpenApiHandlerKey` | types | The contributor contract and the shape of what it returns. |
|
|
1922
1977
|
|
|
1923
1978
|
## 🧩 Compatibility
|
|
1924
1979
|
|
package/dist/index.cjs
CHANGED
|
@@ -359,6 +359,12 @@ function extractRequestInfo(context) {
|
|
|
359
359
|
var INTERNAL_ERROR_STATUS = 500;
|
|
360
360
|
var INTERNAL_ERROR_MESSAGE = "Internal server error";
|
|
361
361
|
var VALIDATION_FAILED_MESSAGE = "Validation failed";
|
|
362
|
+
var MALFORMED_REQUEST_MESSAGE = "Malformed request";
|
|
363
|
+
var PRE_ROUTING_MESSAGES = /* @__PURE__ */ new Map([
|
|
364
|
+
[400, MALFORMED_REQUEST_MESSAGE],
|
|
365
|
+
[413, "Request entity too large"],
|
|
366
|
+
[415, "Unsupported media type"]
|
|
367
|
+
]);
|
|
362
368
|
var ABSOLUTE_FORM_PREFIX = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
|
|
363
369
|
function pathOf(url) {
|
|
364
370
|
let target = String(url);
|
|
@@ -409,8 +415,9 @@ function resolveExposedClientError(exception) {
|
|
|
409
415
|
if (typeof status !== "number" || !Number.isInteger(status) || status < CLIENT_ERROR_MIN2 || status >= CLIENT_ERROR_MAX2) {
|
|
410
416
|
return void 0;
|
|
411
417
|
}
|
|
418
|
+
const fixed = typeof candidate.type === "string" ? PRE_ROUTING_MESSAGES.get(status) : void 0;
|
|
412
419
|
const message = exception instanceof Error ? exception.message : "";
|
|
413
|
-
return { status, message: message === "" ? INTERNAL_ERROR_MESSAGE : message };
|
|
420
|
+
return { status, message: fixed ?? (message === "" ? INTERNAL_ERROR_MESSAGE : message) };
|
|
414
421
|
}
|
|
415
422
|
function isValidationResponse(response) {
|
|
416
423
|
return typeof response === "object" && response !== null && "message" in response && Array.isArray(response.message);
|
|
@@ -611,6 +618,9 @@ exports.BymaxExceptionFilter = class BymaxExceptionFilter {
|
|
|
611
618
|
toValidationDetails(response.message)
|
|
612
619
|
);
|
|
613
620
|
}
|
|
621
|
+
if (status === 400 && readRequestInfo(request).route === UNMATCHED_ROUTE) {
|
|
622
|
+
return this.toEnvelope(status, codeForStatus(status), MALFORMED_REQUEST_MESSAGE, context);
|
|
623
|
+
}
|
|
614
624
|
const message = extractHttpMessage(response, exception);
|
|
615
625
|
const rawUrl = String(this.adapterHost.httpAdapter.getRequestUrl(request));
|
|
616
626
|
return this.toEnvelope(
|
package/dist/index.d.cts
CHANGED
|
@@ -15,9 +15,9 @@ interface EnvelopeOptions {
|
|
|
15
15
|
*/
|
|
16
16
|
exposeInternals?: boolean;
|
|
17
17
|
}
|
|
18
|
-
/** Request-timing
|
|
18
|
+
/** Request-timing middleware configuration. */
|
|
19
19
|
interface TimingOptions {
|
|
20
|
-
/** Register the timing
|
|
20
|
+
/** Register the request-timing middleware (`BymaxTimingMiddleware`). Default: `true`. */
|
|
21
21
|
enabled?: boolean;
|
|
22
22
|
/** Samples above this threshold are flagged as slow. No default (unset). */
|
|
23
23
|
slowRequestThresholdMs?: number;
|
|
@@ -265,7 +265,7 @@ interface MetricsOptions {
|
|
|
265
265
|
interface BymaxCoreModuleOptions {
|
|
266
266
|
/** Error-envelope exception filter. Default: enabled. */
|
|
267
267
|
envelope?: EnvelopeOptions;
|
|
268
|
-
/** Request
|
|
268
|
+
/** Request-timing middleware. Default: enabled. */
|
|
269
269
|
timing?: TimingOptions;
|
|
270
270
|
/** Liveness and readiness endpoints. Default: enabled. */
|
|
271
271
|
health?: HealthOptions;
|
package/dist/index.d.ts
CHANGED
|
@@ -15,9 +15,9 @@ interface EnvelopeOptions {
|
|
|
15
15
|
*/
|
|
16
16
|
exposeInternals?: boolean;
|
|
17
17
|
}
|
|
18
|
-
/** Request-timing
|
|
18
|
+
/** Request-timing middleware configuration. */
|
|
19
19
|
interface TimingOptions {
|
|
20
|
-
/** Register the timing
|
|
20
|
+
/** Register the request-timing middleware (`BymaxTimingMiddleware`). Default: `true`. */
|
|
21
21
|
enabled?: boolean;
|
|
22
22
|
/** Samples above this threshold are flagged as slow. No default (unset). */
|
|
23
23
|
slowRequestThresholdMs?: number;
|
|
@@ -265,7 +265,7 @@ interface MetricsOptions {
|
|
|
265
265
|
interface BymaxCoreModuleOptions {
|
|
266
266
|
/** Error-envelope exception filter. Default: enabled. */
|
|
267
267
|
envelope?: EnvelopeOptions;
|
|
268
|
-
/** Request
|
|
268
|
+
/** Request-timing middleware. Default: enabled. */
|
|
269
269
|
timing?: TimingOptions;
|
|
270
270
|
/** Liveness and readiness endpoints. Default: enabled. */
|
|
271
271
|
health?: HealthOptions;
|
package/dist/index.mjs
CHANGED
|
@@ -357,6 +357,12 @@ function extractRequestInfo(context) {
|
|
|
357
357
|
var INTERNAL_ERROR_STATUS = 500;
|
|
358
358
|
var INTERNAL_ERROR_MESSAGE = "Internal server error";
|
|
359
359
|
var VALIDATION_FAILED_MESSAGE = "Validation failed";
|
|
360
|
+
var MALFORMED_REQUEST_MESSAGE = "Malformed request";
|
|
361
|
+
var PRE_ROUTING_MESSAGES = /* @__PURE__ */ new Map([
|
|
362
|
+
[400, MALFORMED_REQUEST_MESSAGE],
|
|
363
|
+
[413, "Request entity too large"],
|
|
364
|
+
[415, "Unsupported media type"]
|
|
365
|
+
]);
|
|
360
366
|
var ABSOLUTE_FORM_PREFIX = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
|
|
361
367
|
function pathOf(url) {
|
|
362
368
|
let target = String(url);
|
|
@@ -407,8 +413,9 @@ function resolveExposedClientError(exception) {
|
|
|
407
413
|
if (typeof status !== "number" || !Number.isInteger(status) || status < CLIENT_ERROR_MIN2 || status >= CLIENT_ERROR_MAX2) {
|
|
408
414
|
return void 0;
|
|
409
415
|
}
|
|
416
|
+
const fixed = typeof candidate.type === "string" ? PRE_ROUTING_MESSAGES.get(status) : void 0;
|
|
410
417
|
const message = exception instanceof Error ? exception.message : "";
|
|
411
|
-
return { status, message: message === "" ? INTERNAL_ERROR_MESSAGE : message };
|
|
418
|
+
return { status, message: fixed ?? (message === "" ? INTERNAL_ERROR_MESSAGE : message) };
|
|
412
419
|
}
|
|
413
420
|
function isValidationResponse(response) {
|
|
414
421
|
return typeof response === "object" && response !== null && "message" in response && Array.isArray(response.message);
|
|
@@ -609,6 +616,9 @@ var BymaxExceptionFilter = class {
|
|
|
609
616
|
toValidationDetails(response.message)
|
|
610
617
|
);
|
|
611
618
|
}
|
|
619
|
+
if (status === 400 && readRequestInfo(request).route === UNMATCHED_ROUTE) {
|
|
620
|
+
return this.toEnvelope(status, codeForStatus(status), MALFORMED_REQUEST_MESSAGE, context);
|
|
621
|
+
}
|
|
612
622
|
const message = extractHttpMessage(response, exception);
|
|
613
623
|
const rawUrl = String(this.adapterHost.httpAdapter.getRequestUrl(request));
|
|
614
624
|
return this.toEnvelope(
|
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.6",
|
|
4
4
|
"description": "Zero-dependency NestJS 11 application foundation kit: error-envelope exception filter, request-timing middleware, pagination helpers, health endpoints with indicator discovery, an optional Prometheus metrics endpoint with a contribution contract, OpenAPI documents in development, and OpenTelemetry trace correlation.",
|
|
5
5
|
"author": "Bymax One <support@bymax.one>",
|
|
6
6
|
"license": "MIT",
|