@bymax-one/nest-core 1.1.1 → 1.2.0

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,43 @@ heading here.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.2.0] - 2026-08-08
15
+
16
+ Both entries change what a caller receives, which is why this is a minor rather than a patch: an
17
+ application wiring `@bymax-one/nest-auth` starts seeing that library's own error codes where it
18
+ previously saw one collapsed `BYMAX_BAD_REQUEST`, and a deployment with a feature disabled starts
19
+ answering `404` where it answered `500`.
20
+
21
+ ### Fixed
22
+
23
+ - **A feature disabled on the `forRootAsync` path answers `404` instead of `500`.** Route metadata
24
+ is fixed before the async options resolve, so the health and metrics controllers register
25
+ regardless and guard at request time. That guard threw a plain `Error`, which the envelope
26
+ renders as `BYMAX_INTERNAL_ERROR` — so every consumer registering asynchronously with
27
+ `metrics: { enabled: false }`, which is the ordinary configuration and the one that keeps the
28
+ optional `prom-client` peer unloaded, served an unauthenticated `/metrics` that answered a
29
+ server error to anyone who asked. It counted as a real failure in alerting, in error budgets and
30
+ in any uptime check pointed at the service, describing a state nothing was wrong with.
31
+
32
+ The route now reads as absent, which is what the caller would have seen had the framework been
33
+ able to skip the registration. Only the feature's _absence_ is normalised: a resolved path that
34
+ disagrees with the route the controller was registered at is a genuine misconfiguration and
35
+ still throws.
36
+
37
+ - **A domain error's `details` reach the caller, and a nested `{ error: { … } }` body is read as
38
+ readily as a flat one.** The filter passed an explicit `code` through but dropped the structured
39
+ context beside it, and recognised the fields only when they sat directly on the response.
40
+
41
+ `@bymax-one/nest-auth` builds `{ error: { code, message, details } }`, so a backend wiring both
42
+ libraries rendered every distinct auth failure identically — a duplicate e-mail, a password below
43
+ the policy floor, a missing field all arrived as `BYMAX_BAD_REQUEST` / `"Auth Exception"` with no
44
+ details. A client could not branch on the failure, and neither could whoever was debugging it.
45
+
46
+ A nested object is followed only when it carries a string `code`, since `error` is an ordinary
47
+ word for a response body to use; a flat code still wins over a nested one; and a `details` value
48
+ that is neither an array nor an object — including the `null` `AuthException` writes to mean
49
+ "none" — is omitted rather than reshaped, so the field stays present only when context exists.
50
+
14
51
  ## [1.1.1] - 2026-08-07
15
52
 
16
53
  **Documentation and tooling.** `dist/` differs from `1.1.0` only in the text of the comments
@@ -264,4 +301,5 @@ have regressed from. They are kept because the reasoning is worth having.
264
301
  [1.0.1]: https://github.com/bymaxone/nest-core/compare/v1.0.0...v1.0.1
265
302
  [1.0.0]: https://github.com/bymaxone/nest-core/releases/tag/v1.0.0
266
303
  [1.1.1]: https://github.com/bymaxone/nest-core/compare/v1.1.0...v1.1.1
267
- [Unreleased]: https://github.com/bymaxone/nest-core/compare/v1.1.1...HEAD
304
+ [1.2.0]: https://github.com/bymaxone/nest-core/compare/v1.1.1...v1.2.0
305
+ [Unreleased]: https://github.com/bymaxone/nest-core/compare/v1.2.0...HEAD
package/dist/index.cjs CHANGED
@@ -287,12 +287,28 @@ function buildErrorEnvelope(input) {
287
287
  var INTERNAL_ERROR_STATUS = 500;
288
288
  var INTERNAL_ERROR_MESSAGE = "Internal server error";
289
289
  var VALIDATION_FAILED_MESSAGE = "Validation failed";
290
- function extractExplicitCode(response) {
291
- if (typeof response !== "object" || response === null || !("code" in response)) {
290
+ function resolveErrorCarrier(response) {
291
+ if (typeof response !== "object" || response === null) {
292
292
  return void 0;
293
293
  }
294
- const code = response.code;
295
- return typeof code === "string" ? code : void 0;
294
+ if (hasStringCode(response)) {
295
+ return response;
296
+ }
297
+ const nested = response.error;
298
+ if (typeof nested === "object" && nested !== null && hasStringCode(nested)) {
299
+ return nested;
300
+ }
301
+ return void 0;
302
+ }
303
+ function hasStringCode(value) {
304
+ return "code" in value && typeof value.code === "string";
305
+ }
306
+ function extractExplicitCode(carrier) {
307
+ return carrier.code;
308
+ }
309
+ function extractExplicitDetails(carrier) {
310
+ const details = carrier.details;
311
+ return typeof details === "object" && details !== null ? details : void 0;
296
312
  }
297
313
  function isValidationResponse(response) {
298
314
  return typeof response === "object" && response !== null && "message" in response && Array.isArray(response.message);
@@ -302,12 +318,13 @@ function toValidationDetails(violations) {
302
318
  (violation) => typeof violation === "string" ? { issue: violation } : violation
303
319
  );
304
320
  }
305
- function extractHttpMessage(response, exception) {
321
+ function extractHttpMessage(response, exception, carrier) {
306
322
  if (typeof response === "string") {
307
323
  return response;
308
324
  }
309
- if (typeof response === "object" && response !== null && "message" in response) {
310
- const message = response.message;
325
+ const source = carrier ?? response;
326
+ if (typeof source === "object" && source !== null && "message" in source) {
327
+ const message = source.message;
311
328
  if (typeof message === "string") {
312
329
  return message;
313
330
  }
@@ -414,9 +431,10 @@ exports.BymaxExceptionFilter = class BymaxExceptionFilter {
414
431
  return this.mapUnknown(exception, context);
415
432
  }
416
433
  /**
417
- * Map an `HttpException` to the envelope. Explicit domain codes pass through;
418
- * the validation shape becomes `BYMAX_VALIDATION_FAILED` with structured
419
- * details; everything else derives its code from the status.
434
+ * Map an `HttpException` to the envelope. A domain error passes its own code,
435
+ * message and details through, whether it wrote them flat on the response or
436
+ * nested under `error`; the validation shape becomes `BYMAX_VALIDATION_FAILED`
437
+ * with structured details; everything else derives its code from the status.
420
438
  *
421
439
  * @param exception - The HTTP exception to format.
422
440
  * @param context - The neutral request context.
@@ -425,9 +443,15 @@ exports.BymaxExceptionFilter = class BymaxExceptionFilter {
425
443
  mapHttpException(exception, context) {
426
444
  const status = exception.getStatus();
427
445
  const response = exception.getResponse();
428
- const explicitCode = extractExplicitCode(response);
429
- if (explicitCode !== void 0) {
430
- return this.toEnvelope(status, explicitCode, extractHttpMessage(response, exception), context);
446
+ const carrier = resolveErrorCarrier(response);
447
+ if (carrier !== void 0) {
448
+ return this.toEnvelope(
449
+ status,
450
+ extractExplicitCode(carrier),
451
+ extractHttpMessage(response, exception, carrier),
452
+ context,
453
+ extractExplicitDetails(carrier)
454
+ );
431
455
  }
432
456
  if (isValidationResponse(response)) {
433
457
  return this.toEnvelope(
@@ -682,8 +706,8 @@ var PassThroughInterceptor = class {
682
706
  };
683
707
  function assertAsyncFeatureEnabled(feature, enabled) {
684
708
  if (!enabled) {
685
- throw new Error(
686
- `[BymaxCoreModule] The "${feature}" controller was reached while the feature is disabled. On the forRootAsync path this controller is always registered because options resolve after the module is defined; enable "${feature}" in the resolved options, or do not expose this controller while the feature is disabled.`
709
+ throw new common.NotFoundException(
710
+ `[BymaxCoreModule] The "${feature}" feature is disabled, so this route does not exist.`
687
711
  );
688
712
  }
689
713
  }
package/dist/index.d.cts CHANGED
@@ -464,9 +464,10 @@ declare class BymaxExceptionFilter implements ExceptionFilter {
464
464
  */
465
465
  private buildEnvelope;
466
466
  /**
467
- * Map an `HttpException` to the envelope. Explicit domain codes pass through;
468
- * the validation shape becomes `BYMAX_VALIDATION_FAILED` with structured
469
- * details; everything else derives its code from the status.
467
+ * Map an `HttpException` to the envelope. A domain error passes its own code,
468
+ * message and details through, whether it wrote them flat on the response or
469
+ * nested under `error`; the validation shape becomes `BYMAX_VALIDATION_FAILED`
470
+ * with structured details; everything else derives its code from the status.
470
471
  *
471
472
  * @param exception - The HTTP exception to format.
472
473
  * @param context - The neutral request context.
package/dist/index.d.ts CHANGED
@@ -464,9 +464,10 @@ declare class BymaxExceptionFilter implements ExceptionFilter {
464
464
  */
465
465
  private buildEnvelope;
466
466
  /**
467
- * Map an `HttpException` to the envelope. Explicit domain codes pass through;
468
- * the validation shape becomes `BYMAX_VALIDATION_FAILED` with structured
469
- * details; everything else derives its code from the status.
467
+ * Map an `HttpException` to the envelope. A domain error passes its own code,
468
+ * message and details through, whether it wrote them flat on the response or
469
+ * nested under `error`; the validation shape becomes `BYMAX_VALIDATION_FAILED`
470
+ * with structured details; everything else derives its code from the status.
470
471
  *
471
472
  * @param exception - The HTTP exception to format.
472
473
  * @param context - The neutral request context.
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { Catch, Inject, Optional, Injectable, ConfigurableModuleBuilder, Module, HttpException, Logger, Get, Res, Controller, HttpStatus } from '@nestjs/common';
1
+ import { Catch, Inject, Optional, Injectable, ConfigurableModuleBuilder, Module, HttpException, Logger, Get, Res, Controller, HttpStatus, NotFoundException } from '@nestjs/common';
2
2
  import { HttpAdapterHost, DiscoveryService, Reflector, BaseExceptionFilter, APP_FILTER, APP_INTERCEPTOR, DiscoveryModule } from '@nestjs/core';
3
3
  import { tap, catchError, throwError } from 'rxjs';
4
4
 
@@ -285,12 +285,28 @@ function buildErrorEnvelope(input) {
285
285
  var INTERNAL_ERROR_STATUS = 500;
286
286
  var INTERNAL_ERROR_MESSAGE = "Internal server error";
287
287
  var VALIDATION_FAILED_MESSAGE = "Validation failed";
288
- function extractExplicitCode(response) {
289
- if (typeof response !== "object" || response === null || !("code" in response)) {
288
+ function resolveErrorCarrier(response) {
289
+ if (typeof response !== "object" || response === null) {
290
290
  return void 0;
291
291
  }
292
- const code = response.code;
293
- return typeof code === "string" ? code : void 0;
292
+ if (hasStringCode(response)) {
293
+ return response;
294
+ }
295
+ const nested = response.error;
296
+ if (typeof nested === "object" && nested !== null && hasStringCode(nested)) {
297
+ return nested;
298
+ }
299
+ return void 0;
300
+ }
301
+ function hasStringCode(value) {
302
+ return "code" in value && typeof value.code === "string";
303
+ }
304
+ function extractExplicitCode(carrier) {
305
+ return carrier.code;
306
+ }
307
+ function extractExplicitDetails(carrier) {
308
+ const details = carrier.details;
309
+ return typeof details === "object" && details !== null ? details : void 0;
294
310
  }
295
311
  function isValidationResponse(response) {
296
312
  return typeof response === "object" && response !== null && "message" in response && Array.isArray(response.message);
@@ -300,12 +316,13 @@ function toValidationDetails(violations) {
300
316
  (violation) => typeof violation === "string" ? { issue: violation } : violation
301
317
  );
302
318
  }
303
- function extractHttpMessage(response, exception) {
319
+ function extractHttpMessage(response, exception, carrier) {
304
320
  if (typeof response === "string") {
305
321
  return response;
306
322
  }
307
- if (typeof response === "object" && response !== null && "message" in response) {
308
- const message = response.message;
323
+ const source = carrier ?? response;
324
+ if (typeof source === "object" && source !== null && "message" in source) {
325
+ const message = source.message;
309
326
  if (typeof message === "string") {
310
327
  return message;
311
328
  }
@@ -412,9 +429,10 @@ var BymaxExceptionFilter = class {
412
429
  return this.mapUnknown(exception, context);
413
430
  }
414
431
  /**
415
- * Map an `HttpException` to the envelope. Explicit domain codes pass through;
416
- * the validation shape becomes `BYMAX_VALIDATION_FAILED` with structured
417
- * details; everything else derives its code from the status.
432
+ * Map an `HttpException` to the envelope. A domain error passes its own code,
433
+ * message and details through, whether it wrote them flat on the response or
434
+ * nested under `error`; the validation shape becomes `BYMAX_VALIDATION_FAILED`
435
+ * with structured details; everything else derives its code from the status.
418
436
  *
419
437
  * @param exception - The HTTP exception to format.
420
438
  * @param context - The neutral request context.
@@ -423,9 +441,15 @@ var BymaxExceptionFilter = class {
423
441
  mapHttpException(exception, context) {
424
442
  const status = exception.getStatus();
425
443
  const response = exception.getResponse();
426
- const explicitCode = extractExplicitCode(response);
427
- if (explicitCode !== void 0) {
428
- return this.toEnvelope(status, explicitCode, extractHttpMessage(response, exception), context);
444
+ const carrier = resolveErrorCarrier(response);
445
+ if (carrier !== void 0) {
446
+ return this.toEnvelope(
447
+ status,
448
+ extractExplicitCode(carrier),
449
+ extractHttpMessage(response, exception, carrier),
450
+ context,
451
+ extractExplicitDetails(carrier)
452
+ );
429
453
  }
430
454
  if (isValidationResponse(response)) {
431
455
  return this.toEnvelope(
@@ -680,8 +704,8 @@ var PassThroughInterceptor = class {
680
704
  };
681
705
  function assertAsyncFeatureEnabled(feature, enabled) {
682
706
  if (!enabled) {
683
- throw new Error(
684
- `[BymaxCoreModule] The "${feature}" controller was reached while the feature is disabled. On the forRootAsync path this controller is always registered because options resolve after the module is defined; enable "${feature}" in the resolved options, or do not expose this controller while the feature is disabled.`
707
+ throw new NotFoundException(
708
+ `[BymaxCoreModule] The "${feature}" feature is disabled, so this route does not exist.`
685
709
  );
686
710
  }
687
711
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-core",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
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",
@@ -86,8 +86,8 @@
86
86
  "lint": "eslint --no-error-on-unmatched-pattern src scripts test",
87
87
  "lint:fix": "eslint --no-error-on-unmatched-pattern src scripts test --fix",
88
88
  "mutation": "stryker run",
89
+ "mutation:full": "node -e \"require('node:fs').rmSync('reports/stryker-incremental.json',{force:true,recursive:true})\" && stryker run",
89
90
  "mutation:dry-run": "stryker run --dryRunOnly",
90
- "mutation:incremental": "stryker run --incremental",
91
91
  "prepare": "husky",
92
92
  "prepublishOnly": "pnpm clean && pnpm typecheck && pnpm lint && pnpm check:mutants && pnpm test:cov:all && pnpm build && pnpm size && pnpm check:published",
93
93
  "release": "npm publish --provenance --access public",