@bymax-one/nest-config 1.1.1 → 1.1.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,93 @@ as the GitHub Release body, so each released version needs a matching
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.1.3] - 2026-08-18
15
+
16
+ **Documentation.** The runtime bundles are byte-identical to `1.1.2`. Five files differ: the
17
+ two `./internal` declaration files, by the JSDoc noted below; `README.md` and `CHANGELOG.md`;
18
+ and `package.json`, by its `version` field alone. Verified by hashing every file in the
19
+ published `1.1.2` tarball against this build.
20
+
21
+ ### Documentation
22
+
23
+ - **How to report the failure is documented, because the two obvious ways are not
24
+ equivalent.** `onValidationError` said what the hook is and nothing about logging what it
25
+ hands you, which left the choice at the call site with no way to know the cost. `code` and
26
+ `issues` are the only two own enumerable properties of `BymaxConfigValidationError`,
27
+ while `name`, `message` and `stack` are non-enumerable as on any `Error`. That splits the
28
+ outcome three ways by representation: an error object keeps everything when the serializer
29
+ extracts the standard fields and copies the own enumerables, which are two separate steps; `error.stack` keeps the report and drops `code` and `issues`;
30
+ and a plain `JSON.stringify(error)` keeps `code` and `issues` and drops the report.
31
+ Which representation reaches the sink is decided by the logging call, and that call is
32
+ library-specific — Pino serializes an error only as the merging object or under `err`,
33
+ and drops it entirely when it is passed as a trailing argument. The machine-readable half is the easy one to
34
+ lose, precisely because the report keeps arriving — `code` separates a configuration
35
+ failure from any other boot failure, and `issues` is what an alert keys on per variable.
36
+
37
+ Wrapping the error as a `cause` also keeps all of it, wherever the serializer walks the
38
+ chain the same way. Measured against `@bymax-one/nest-logger` 1.2.7 and 1.2.9 rather
39
+ than assumed: a fifteen-issue report crosses the chain with every issue and the full multi-line
40
+ `message` intact.
41
+
42
+ Versions 1.2.6 through 1.2.9 carry a defect the README states in full: handed this error
43
+ **directly**, the redactor dropped the whole `err` field, leaving `_redactionFailed: true`
44
+ and no report at all. The trigger was `issues` rather than `code`, and the property
45
+ definition rather than freezing: this error is not frozen — it defines `code` and `issues`
46
+ as non-writable, non-configurable own properties and freezes the issue list, while the
47
+ instance stays extensible. Only the locked object property failed, because the redactor's
48
+ clone inherited the locked descriptors and redefining a non-configurable property fails
49
+ exactly when the value differs, which a structurally copied object does and an identical
50
+ string does not.
51
+
52
+ **Fixed upstream in `1.3.0`**, measured against the published package rather than taken on
53
+ report: a real failure logged directly now arrives with `type, message, stack, code,
54
+ issues`, the full issue array and the multi-line report intact, and this error is left
55
+ untouched. On an affected version the workaround stands, and it is something a consumer
56
+ writes: a `catch` receives this error unchanged, since the module rethrows the instance it
57
+ caught, so wrapping is an explicit `new Error(message, { cause: error })`.
58
+
59
+ The rule also covers the call site where this failure usually lands: a `catch` in
60
+ `main.ts` runs before any logging module is registered, so it reports through
61
+ `console.error`. Node's inspector appends an error's own enumerables after the stack,
62
+ so `console.error(message, error)` prints the report, the `code` and the expanded `issues`, while
63
+ `console.error(message, error.stack)` prints the report alone — the same split, one layer
64
+ earlier.
65
+
66
+ The same fact is now stated on `BymaxConfigValidationError` itself, so it reaches a
67
+ consumer through the published types and not only through the README.
68
+
69
+ ## [1.1.2] - 2026-08-14
70
+
71
+ **Documentation only.** `dist/` is byte-identical to `1.1.1`, verified by hashing every file in
72
+ the published tarball against a local build. Inside the package only `README.md`, `CHANGELOG.md`
73
+ and the `version` field of `package.json` differ.
74
+
75
+ ### Documentation
76
+
77
+ - **The `1.1.1` apply-note understated the work for a consumer with a mutation gate.** It said
78
+ a derived backend has nothing to change, which is true for correctness and wrong as an
79
+ estimate. Under Stryker, a message string literal is a mutant: authoring messages for rules
80
+ that had none converts operator-facing prose into tested behavior, and a repo gating at
81
+ `break: 100` fails until an assertion pins each message. Reported by a consumer adopting
82
+ `1.1.1`, which added one assertion per message to keep its gate green.
83
+
84
+ The honest version of the note: **upgrading changes nothing** — a rule that carries no
85
+ message renders exactly as it did before, verified byte-for-byte on a real boot. **Writing
86
+ the messages the release makes worth writing is real work**: wherever mutation testing is a
87
+ gate, every message needs an assertion that covers it — one assertion on the rendered report
88
+ can cover several — and that is the work the release exists to enable.
89
+
90
+ Also worth knowing before writing them: a rule whose variable is _absent_ rendered
91
+ `missing required value` on `1.1.0`, not `invalid value`. For a conditionally-required
92
+ variable that is the more misleading of the two — nothing was forgotten, another variable
93
+ made it required — so the before/after is larger than the `invalid value` case suggests.
94
+
95
+ - The mutation badge read **99.59%**; the score measured as the `v1.1.1` release gate is
96
+ **99.61%**. The denominator grew with the new code — 14 more killed mutants in
97
+ `env-validator.ts`, the only file the release changed executably, and the same single
98
+ counted survivor — so the badge, the README claim and `docs/mutation_testing_results.md`
99
+ now state the measured number, with a dated re-run entry recording it.
100
+
14
101
  ## [1.1.1] - 2026-08-14
15
102
 
16
103
  ### Fixed
@@ -231,4 +318,6 @@ have regressed from. They are kept because the reasoning is worth having.
231
318
  [1.0.3]: https://github.com/bymaxone/nest-config/compare/v1.0.2...v1.0.3
232
319
  [1.1.0]: https://github.com/bymaxone/nest-config/compare/v1.0.3...v1.1.0
233
320
  [1.1.1]: https://github.com/bymaxone/nest-config/compare/v1.1.0...v1.1.1
234
- [Unreleased]: https://github.com/bymaxone/nest-config/compare/v1.1.1...HEAD
321
+ [1.1.2]: https://github.com/bymaxone/nest-config/compare/v1.1.1...v1.1.2
322
+ [1.1.3]: https://github.com/bymaxone/nest-config/compare/v1.1.2...v1.1.3
323
+ [Unreleased]: https://github.com/bymaxone/nest-config/compare/v1.1.3...HEAD
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
  <a href="https://www.npmjs.com/package/@bymax-one/nest-config"><img src="https://img.shields.io/npm/dm/@bymax-one/nest-config?style=flat-square&colorA=000000&colorB=000000" alt="npm downloads" /></a>
15
15
  <a href="https://github.com/bymaxone/nest-config/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/bymaxone/nest-config/ci.yml?branch=main&style=flat-square&colorA=000000&label=CI" alt="CI status" /></a>
16
16
  <a href="https://github.com/bymaxone/nest-config/actions/workflows/ci.yml"><img src="https://img.shields.io/badge/coverage-100%25-brightgreen?style=flat-square&colorA=000000" alt="coverage" /></a>
17
- <a href="https://github.com/bymaxone/nest-config/blob/main/docs/mutation_testing_results.md"><img src="https://img.shields.io/badge/mutation-99.59%25-brightgreen?style=flat-square&colorA=000000" alt="mutation score" /></a>
17
+ <a href="https://github.com/bymaxone/nest-config/blob/main/docs/mutation_testing_results.md"><img src="https://img.shields.io/badge/mutation-99.61%25-brightgreen?style=flat-square&colorA=000000" alt="mutation score" /></a>
18
18
  <a href="https://scorecard.dev/viewer/?uri=github.com/bymaxone/nest-config"><img src="https://api.scorecard.dev/projects/github.com/bymaxone/nest-config/badge?style=flat-square" alt="OpenSSF Scorecard" /></a>
19
19
  <a href="https://github.com/bymaxone/nest-config/blob/main/LICENSE"><img src="https://img.shields.io/github/license/bymaxone/nest-config?style=flat-square&colorA=000000&colorB=000000" alt="license" /></a>
20
20
  <a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-strict-3178C6?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript" /></a>
@@ -327,6 +327,83 @@ export interface ConfigIssue {
327
327
  error's own `code`) and `ConfigIssueCode` (each issue's `code`); see the
328
328
  error catalog below for every value.
329
329
 
330
+ ### Reporting the failure
331
+
332
+ `onValidationError` receives `ReadonlyArray<ConfigIssue>` — four string fields per
333
+ issue, JSON-serializable, with no error to unwrap. Every description this package
334
+ generates is value-free, so it can go straight into a log as structured data. The
335
+ exception is the one described above: a `custom` message written by your own schema
336
+ is printed as written, so if one of yours interpolates a value, reviewing it before
337
+ it reaches a log is the schema author's job, not this package's.
338
+
339
+ If you also log the thrown error from a bootstrap `catch`, how you pass it decides
340
+ what survives. `code` and `issues` are the **only two own enumerable properties** of
341
+ `BymaxConfigValidationError`; `name`, `message` and `stack` are non-enumerable, as
342
+ on any `Error`. That splits the outcome three ways — all three measured:
343
+
344
+ | Which representation reaches the sink | The report (`message`) | `code` and `issues` |
345
+ | ------------------------------------------------------------------------------------------------------- | ---------------------- | ------------------- |
346
+ | the error object, read by a serializer that extracts the standard fields **and** copies own enumerables | ✅ | ✅ |
347
+ | `error.stack` | ✅ inside the string | ❌ |
348
+ | `JSON.stringify(error)`, `{ ...error }` | ❌ | ✅ |
349
+
350
+ Recognizing an `Error` is not enough on its own: extracting `name`, `message` and
351
+ `stack` is what preserves the report, and copying the own enumerables is a separate
352
+ step that preserves `code` and `issues`. A serializer that does only the second is
353
+ the third row — `code` and `issues` and no report at all — so if you build the log
354
+ record by hand, add `message` explicitly.
355
+
356
+ **Which representation the sink sees is decided by your logging call, and that call
357
+ is library-specific.** These are measured, not assumed:
358
+
359
+ ```typescript
360
+ console.error('configuration invalid', error) // report, code and issues
361
+ pino.error({ err: error }, 'configuration invalid') // report, code and issues
362
+ pino.error('configuration invalid', error) // the error is dropped entirely
363
+
364
+ // @bymax-one/nest-logger 1.2.6-1.2.9: wrap it, explicitly (see the note below)
365
+ nestLogger.error('BOOT_FAILED', new Error('bootstrap failed', { cause: error }))
366
+ ```
367
+
368
+ Pino only applies error serialization when the error is the merging object or sits
369
+ under `err`; passed as a trailing argument it is treated as an interpolation value,
370
+ and nothing about the failure reaches the entry.
371
+
372
+ > [!NOTE]
373
+ > **`@bymax-one/nest-logger` 1.2.6 through 1.2.9 dropped this error when it was logged
374
+ > directly.** Its redactor left `_redactionFailed: true` and no `err` field at all —
375
+ > no report, no `code`, no `issues`. The trigger was the definition rather than any
376
+ > freezing: this error defines `code` and `issues` as non-writable, non-configurable
377
+ > own properties, and the redactor's clone inherited those descriptors, so rewriting
378
+ > the object-valued one with a walked copy failed. A locked scalar was unaffected,
379
+ > which is why `code` alone never reproduced it.
380
+ >
381
+ > Fixed in **1.3.0**, measured rather than taken on report: a real failure logged
382
+ > directly now arrives with `type, message, stack, code, issues`, the full issue array
383
+ > and the report intact, and this error is left untouched. Measured affected: 1.2.6, 1.2.7
384
+ > and 1.2.9 — 1.2.8 was never published, and on 1.2.2 and earlier this call shape does not
385
+ > attach the error at all, so the defect does not arise there in this form. On an affected
386
+ > version, wrap it — a `catch` receives this error unchanged, since the
387
+ > module rethrows the instance it caught, so wrapping is an explicit
388
+ > `new Error(message, { cause: error })` rather than a side effect of catching. A
389
+ > lockfile pinned below 1.3.0 does not pick the fix up from an ordinary install, so the
390
+ > update has to be deliberate.
391
+
392
+ Wrapping the error as the `cause` of an error you construct keeps all of it wherever the
393
+ serializer walks the chain — measured against `@bymax-one/nest-logger` 1.2.7 and
394
+ 1.2.9, where a fifteen-issue report crosses the chain with `code`, every issue and
395
+ the full multi-line `message` intact.
396
+
397
+ Before any logger exists — which is where this failure usually lands, since a `catch`
398
+ in `main.ts` runs before the logging module is registered — `console.error` is the
399
+ reporter, and Node's inspector prints the stack and then appends the own enumerables.
400
+ So `console.error(message, error)` shows the report, the `code` and the expanded
401
+ `issues` list, while `console.error(message, error.stack)` shows the report alone.
402
+
403
+ Losing the machine-readable half is easy to miss precisely because the report keeps
404
+ arriving: `code` is what separates a configuration failure from any other boot
405
+ failure, and `issues` is what an alert or a dashboard keys on per variable.
406
+
330
407
  ### Injection tokens
331
408
 
332
409
  `BYMAX_CONFIG` and `BYMAX_CONFIG_OPTIONS` are module-local `Symbol` tokens,
@@ -514,7 +591,7 @@ Configuration decides whether a process may start at all, so the suite is held t
514
591
  beyond "the tests pass".
515
592
 
516
593
  - ✅ **100% line coverage** — statements, branches, functions and lines, enforced as a gate
517
- - ✅ **99.59% mutation score** — verified with [Stryker](https://stryker-mutator.io/) at
594
+ - ✅ **99.61% mutation score** — verified with [Stryker](https://stryker-mutator.io/) at
518
595
  `break: 95`; ten provable equivalents carry their reason inline, and the one survivor left
519
596
  is an equivalent no directive can reach, so it stays counted rather than silenced
520
597
  ([report](./docs/mutation_testing_results.md))
@@ -167,6 +167,10 @@ interface ConfigIssue {
167
167
  * constraints, never their received values — except where a schema author's own
168
168
  * `custom` message says otherwise, as described on {@link ConfigIssue}.
169
169
  *
170
+ * `code` and `issues` are its only own enumerable properties, so a structured
171
+ * logger that copies own enumerables keeps both; passing `error.stack` instead of
172
+ * the error keeps neither.
173
+ *
170
174
  * @example
171
175
  * ```typescript
172
176
  * try {
@@ -167,6 +167,10 @@ interface ConfigIssue {
167
167
  * constraints, never their received values — except where a schema author's own
168
168
  * `custom` message says otherwise, as described on {@link ConfigIssue}.
169
169
  *
170
+ * `code` and `issues` are its only own enumerable properties, so a structured
171
+ * logger that copies own enumerables keeps both; passing `error.stack` instead of
172
+ * the error keeps neither.
173
+ *
170
174
  * @example
171
175
  * ```typescript
172
176
  * try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-config",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Typed and validated environment configuration entry point for NestJS 11 applications, built on Zod v4.",
5
5
  "author": "Bymax One <support@bymax.one>",
6
6
  "license": "MIT",