@bymax-one/nest-core 1.6.2 → 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 CHANGED
@@ -11,6 +11,60 @@ 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
+
14
68
  ## [1.6.2] - 2026-10-22
15
69
 
16
70
  An unexpected error became `500 BYMAX_INTERNAL_ERROR` and the application had no
@@ -1138,4 +1192,5 @@ have regressed from. They are kept because the reasoning is worth having.
1138
1192
  [1.6.0]: https://github.com/bymaxone/nest-core/compare/v1.5.3...v1.6.0
1139
1193
  [1.6.1]: https://github.com/bymaxone/nest-core/compare/v1.6.0...v1.6.1
1140
1194
  [1.6.2]: https://github.com/bymaxone/nest-core/compare/v1.6.1...v1.6.2
1141
- [Unreleased]: https://github.com/bymaxone/nest-core/compare/v1.6.2...HEAD
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
@@ -1546,27 +1546,67 @@ your samples and your logs.
1546
1546
 
1547
1547
  ## 🔗 Integration with `@bymax-one/nest-logger`
1548
1548
 
1549
- Pairing this package with `@bymax-one/nest-logger` yields correlated logs and
1550
- error responses with one binding and no hard coupling: `LogContextService`
1551
- satisfies `ICorrelationIdProvider` out of the box, so `useExisting` aliases it
1552
- onto the shared token. Bind it from a `@Global()` module of your own so the
1553
- binding is visible outside the module that declares it:
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:
1554
1557
 
1555
1558
  ```typescript
1556
1559
  import { Global, Module } from '@nestjs/common'
1557
- import { BYMAX_CORRELATION_PROVIDER } from '@bymax-one/nest-core'
1558
- import { LogContextService, NestLoggerModule } from '@bymax-one/nest-logger'
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'
1559
1567
 
1560
1568
  @Global()
1561
1569
  @Module({
1562
- imports: [NestLoggerModule.forRoot()],
1563
- providers: [{ provide: BYMAX_CORRELATION_PROVIDER, useExisting: LogContextService }],
1564
- exports: [BYMAX_CORRELATION_PROVIDER]
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]
1565
1582
  })
1566
1583
  export class ObservabilityModule {}
1567
1584
  ```
1568
1585
 
1569
- Every error envelope now carries the same correlation id your logs do. The
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
1570
1610
  same `@Global()`-module pattern is how every pluggable token in this package
1571
1611
  is overridden: `BYMAX_TIMING_SINK` and `BYMAX_HEALTH_INDICATORS` follow it
1572
1612
  identically.
package/dist/index.cjs CHANGED
@@ -359,6 +359,19 @@ 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 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
+ }
362
375
  function resolveErrorCarrier(response) {
363
376
  if (typeof response !== "object" || response === null) {
364
377
  return void 0;
@@ -420,11 +433,16 @@ function extractHttpMessage(response, exception, carrier) {
420
433
  }
421
434
  return exception.message;
422
435
  }
436
+ var UNREPRESENTABLE_MESSAGE = "Unrepresentable thrown value";
423
437
  function buildInternalDetails(exception) {
424
- if (exception instanceof Error) {
425
- return exception.stack !== void 0 ? { message: exception.message, stack: exception.stack } : { message: exception.message };
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 };
426
445
  }
427
- return { message: String(exception) };
428
446
  }
429
447
  exports.BymaxExceptionFilter = class BymaxExceptionFilter {
430
448
  /**
@@ -496,7 +514,7 @@ exports.BymaxExceptionFilter = class BymaxExceptionFilter {
496
514
  const traceId = this.readAnnotation(() => this.traceContext.getTraceContext()?.traceId);
497
515
  const context = {
498
516
  method: String(httpAdapter.getRequestMethod(request)),
499
- path: String(httpAdapter.getRequestUrl(request)),
517
+ path: pathOf(httpAdapter.getRequestUrl(request)),
500
518
  ...correlationId !== void 0 ? { correlationId } : {},
501
519
  ...traceId !== void 0 ? { traceId } : {}
502
520
  };
package/dist/index.mjs CHANGED
@@ -357,6 +357,19 @@ 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 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
+ }
360
373
  function resolveErrorCarrier(response) {
361
374
  if (typeof response !== "object" || response === null) {
362
375
  return void 0;
@@ -418,11 +431,16 @@ function extractHttpMessage(response, exception, carrier) {
418
431
  }
419
432
  return exception.message;
420
433
  }
434
+ var UNREPRESENTABLE_MESSAGE = "Unrepresentable thrown value";
421
435
  function buildInternalDetails(exception) {
422
- if (exception instanceof Error) {
423
- return exception.stack !== void 0 ? { message: exception.message, stack: exception.stack } : { message: exception.message };
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 };
424
443
  }
425
- return { message: String(exception) };
426
444
  }
427
445
  var BymaxExceptionFilter = class {
428
446
  /**
@@ -494,7 +512,7 @@ var BymaxExceptionFilter = class {
494
512
  const traceId = this.readAnnotation(() => this.traceContext.getTraceContext()?.traceId);
495
513
  const context = {
496
514
  method: String(httpAdapter.getRequestMethod(request)),
497
- path: String(httpAdapter.getRequestUrl(request)),
515
+ path: pathOf(httpAdapter.getRequestUrl(request)),
498
516
  ...correlationId !== void 0 ? { correlationId } : {},
499
517
  ...traceId !== void 0 ? { traceId } : {}
500
518
  };
@@ -737,7 +737,7 @@ function warnDiscardedComponents(logger, discarded) {
737
737
  const elided = discarded.length - MAX_WARNED_OPERATIONS;
738
738
  const listed = discarded.slice(0, MAX_WARNED_OPERATIONS).map(({ contributor, member, name }) => `${contributor} -> ${member}.${name}`).join(", ");
739
739
  logger.warn(
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 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.`
741
741
  );
742
742
  }
743
743
  function buildConfig(builder, options) {
@@ -735,7 +735,7 @@ function warnDiscardedComponents(logger, discarded) {
735
735
  const elided = discarded.length - MAX_WARNED_OPERATIONS;
736
736
  const listed = discarded.slice(0, MAX_WARNED_OPERATIONS).map(({ contributor, member, name }) => `${contributor} -> ${member}.${name}`).join(", ");
737
737
  logger.warn(
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 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.`
739
739
  );
740
740
  }
741
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.2",
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",