@bymax-one/nest-config 1.1.0 → 1.1.2
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 +72 -1
- package/README.md +76 -38
- package/dist/internal/index.cjs +10 -2
- package/dist/internal/index.d.cts +28 -18
- package/dist/internal/index.d.ts +28 -18
- package/dist/internal/index.mjs +10 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,75 @@ as the GitHub Release body, so each released version needs a matching
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.1.2] - 2026-08-14
|
|
15
|
+
|
|
16
|
+
**Documentation only.** `dist/` is byte-identical to `1.1.1`, verified by hashing every file in
|
|
17
|
+
the published tarball against a local build. Inside the package only `README.md`, `CHANGELOG.md`
|
|
18
|
+
and the `version` field of `package.json` differ.
|
|
19
|
+
|
|
20
|
+
### Documentation
|
|
21
|
+
|
|
22
|
+
- **The `1.1.1` apply-note understated the work for a consumer with a mutation gate.** It said
|
|
23
|
+
a derived backend has nothing to change, which is true for correctness and wrong as an
|
|
24
|
+
estimate. Under Stryker, a message string literal is a mutant: authoring messages for rules
|
|
25
|
+
that had none converts operator-facing prose into tested behavior, and a repo gating at
|
|
26
|
+
`break: 100` fails until an assertion pins each message. Reported by a consumer adopting
|
|
27
|
+
`1.1.1`, which added one assertion per message to keep its gate green.
|
|
28
|
+
|
|
29
|
+
The honest version of the note: **upgrading changes nothing** — a rule that carries no
|
|
30
|
+
message renders exactly as it did before, verified byte-for-byte on a real boot. **Writing
|
|
31
|
+
the messages the release makes worth writing is real work**: wherever mutation testing is a
|
|
32
|
+
gate, every message needs an assertion that covers it — one assertion on the rendered report
|
|
33
|
+
can cover several — and that is the work the release exists to enable.
|
|
34
|
+
|
|
35
|
+
Also worth knowing before writing them: a rule whose variable is _absent_ rendered
|
|
36
|
+
`missing required value` on `1.1.0`, not `invalid value`. For a conditionally-required
|
|
37
|
+
variable that is the more misleading of the two — nothing was forgotten, another variable
|
|
38
|
+
made it required — so the before/after is larger than the `invalid value` case suggests.
|
|
39
|
+
|
|
40
|
+
- The mutation badge read **99.59%**; the score measured as the `v1.1.1` release gate is
|
|
41
|
+
**99.61%**. The denominator grew with the new code — 14 more killed mutants in
|
|
42
|
+
`env-validator.ts`, the only file the release changed executably, and the same single
|
|
43
|
+
counted survivor — so the badge, the README claim and `docs/mutation_testing_results.md`
|
|
44
|
+
now state the measured number, with a dated re-run entry recording it.
|
|
45
|
+
|
|
46
|
+
## [1.1.1] - 2026-08-14
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- **A `custom` issue is reported with the message its author wrote.** `.check`, `.refine`
|
|
51
|
+
and `.superRefine` all raise Zod's `custom` code, which carries no structural constraint
|
|
52
|
+
to translate, so every one of them rendered as the literal `invalid value` and the rule's
|
|
53
|
+
own explanation was discarded. Built-in codes were unaffected, which is what made it easy
|
|
54
|
+
to miss: the report looked correct until a rule stopped being trivial. A conditional
|
|
55
|
+
requirement, a cross-field rule or a security floor states itself only through that
|
|
56
|
+
message, and the aggregated report is the one artifact an operator gets for a failure that
|
|
57
|
+
stops the process.
|
|
58
|
+
|
|
59
|
+
The message is used whether the variable is absent or present-but-invalid — "required
|
|
60
|
+
when `X` is enabled" explains an absent variable better than `missing required value`
|
|
61
|
+
does — while `issue.code` still classifies it as `BYMAX_CONFIG_MISSING` or
|
|
62
|
+
`BYMAX_CONFIG_INVALID`, so machine consumers are unaffected. Whitespace runs collapse to
|
|
63
|
+
single spaces to keep the one-line-per-variable layout; the column layout and the
|
|
64
|
+
resolved `variable` name are unchanged.
|
|
65
|
+
|
|
66
|
+
A `custom` issue raised without a message of its own still reads `invalid value`. Zod
|
|
67
|
+
fills a message-less `custom` issue with its locale default before the validator sees it,
|
|
68
|
+
so "no authored message" is recognized by matching that default (`Invalid input`) rather
|
|
69
|
+
than by absence. Two consequences, both deliberate: the string is reserved, so a schema
|
|
70
|
+
that authors it verbatim reports `invalid value`; and under a configured non-English Zod
|
|
71
|
+
locale or a global custom error map the localized default does not match and is reported
|
|
72
|
+
as written.
|
|
73
|
+
|
|
74
|
+
Value-free stays a contract for the text this library generates. An authored message is
|
|
75
|
+
schema text and is printed as written, including a value interpolated into it. The
|
|
76
|
+
exception is now stated everywhere the guarantee is made: the `ConfigIssue`,
|
|
77
|
+
`BymaxConfigValidationError` and `onValidationError` contracts, `env-validator`'s own
|
|
78
|
+
contract, the README (including the security table) and spec 6.1.
|
|
79
|
+
|
|
80
|
+
**Apply to a derived backend:** nothing to change. A schema that already raises `custom`
|
|
81
|
+
issues with messages starts reporting them on the next boot.
|
|
82
|
+
|
|
14
83
|
## [1.1.0] - 2026-08-11
|
|
15
84
|
|
|
16
85
|
Coordinated ecosystem release aligning every `@bymax-one/*` package after the ioredis 6 /
|
|
@@ -193,4 +262,6 @@ have regressed from. They are kept because the reasoning is worth having.
|
|
|
193
262
|
[1.0.1]: https://github.com/bymaxone/nest-config/compare/v1.0.0...v1.0.1
|
|
194
263
|
[1.0.3]: https://github.com/bymaxone/nest-config/compare/v1.0.2...v1.0.3
|
|
195
264
|
[1.1.0]: https://github.com/bymaxone/nest-config/compare/v1.0.3...v1.1.0
|
|
196
|
-
[
|
|
265
|
+
[1.1.1]: https://github.com/bymaxone/nest-config/compare/v1.1.0...v1.1.1
|
|
266
|
+
[1.1.2]: https://github.com/bymaxone/nest-config/compare/v1.1.1...v1.1.2
|
|
267
|
+
[Unreleased]: https://github.com/bymaxone/nest-config/compare/v1.1.2...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.
|
|
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>
|
|
@@ -50,8 +50,9 @@ minimal.
|
|
|
50
50
|
- **One round trip to fix the environment.** Every violation is collected and reported
|
|
51
51
|
together, not the first one that fails.
|
|
52
52
|
- **Errors are value-free by contract.** Configuration is where the secrets are, and the
|
|
53
|
-
report is the artifact that gets logged and pasted into issues — so messages
|
|
54
|
-
expected constraint and the schema's own enum options, never the received
|
|
53
|
+
report is the artifact that gets logged and pasted into issues — so generated messages
|
|
54
|
+
carry the expected constraint and the schema's own enum options, never the received
|
|
55
|
+
value. Your own `custom` messages are the documented exception: they print as written.
|
|
55
56
|
- **The schema is the type.** `get('database.url')` is `string` and `get('server.port')` is
|
|
56
57
|
`number` because the schema says so; a typo in a path is a build error, not a runtime
|
|
57
58
|
`undefined`.
|
|
@@ -66,8 +67,9 @@ minimal.
|
|
|
66
67
|
application serves traffic; there is no lazy path that could discover a problem later
|
|
67
68
|
- ✅ **Fail fast, fail complete** — every violation reported together in one aggregated
|
|
68
69
|
error, so an operator fixes the environment in one round trip
|
|
69
|
-
- ✅ **Never echo values** — messages carry variable names, paths and constraint
|
|
70
|
-
descriptions only, so a secret cannot reach a log or a crash report through this library
|
|
70
|
+
- ✅ **Never echo values** — generated messages carry variable names, paths and constraint
|
|
71
|
+
descriptions only, so a secret cannot reach a log or a crash report through this library;
|
|
72
|
+
a `custom` message you write yourself is printed as written and is yours to keep value-free
|
|
71
73
|
- ✅ **Zod v4 schemas** — the full type language, including coercion, refinements and
|
|
72
74
|
defaults, with `defineEnv` shaping namespaces and leaves
|
|
73
75
|
|
|
@@ -202,8 +204,38 @@ BymaxConfigValidationError: environment validation failed (3 issues)
|
|
|
202
204
|
Fix the variables above and restart the process.
|
|
203
205
|
```
|
|
204
206
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
+
In a message this package generates, raw values are never printed — not
|
|
208
|
+
truncated, not masked, absent. That is a hard guarantee, verified by tests. The
|
|
209
|
+
one exception is a message you write yourself, described next.
|
|
210
|
+
|
|
211
|
+
A rule you write yourself — a `.check`, `.refine` or `.superRefine` raising a
|
|
212
|
+
`custom` issue — has no structural constraint to describe, so its own message is
|
|
213
|
+
what the report prints:
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
BymaxConfigValidationError: environment validation failed (2 issues)
|
|
217
|
+
|
|
218
|
+
STORAGE_ENDPOINT STORAGE_ENDPOINT is required when STORAGE_ENABLED is true.
|
|
219
|
+
LOG_PRETTY LOG_PRETTY must be false when NODE_ENV is production.
|
|
220
|
+
|
|
221
|
+
Fix the variables above and restart the process.
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The message is used whether the variable is absent or present-but-invalid — a
|
|
225
|
+
conditional rule explains an absent variable better than "missing required
|
|
226
|
+
value" does — while `issue.code` still classifies it as
|
|
227
|
+
`BYMAX_CONFIG_MISSING` or `BYMAX_CONFIG_INVALID`. Whitespace runs collapse to
|
|
228
|
+
single spaces so a message wrapped across source lines keeps the one-line
|
|
229
|
+
layout.
|
|
230
|
+
|
|
231
|
+
A `custom` issue raised without a message falls back to `invalid value`. Zod
|
|
232
|
+
fills a message-less `custom` issue with its own default text before the
|
|
233
|
+
validator sees it, so "no message" is recognized by matching that default
|
|
234
|
+
(`Invalid input`) rather than by its absence. Two consequences, both deliberate:
|
|
235
|
+
that exact string is reserved — a schema that authors it verbatim reports
|
|
236
|
+
`invalid value` — and under a configured non-English Zod locale (or a global
|
|
237
|
+
custom error map) the default no longer matches, so the localized default is
|
|
238
|
+
reported as written instead.
|
|
207
239
|
|
|
208
240
|
## 📖 API Reference
|
|
209
241
|
|
|
@@ -287,7 +319,7 @@ export interface ConfigIssue {
|
|
|
287
319
|
readonly path: string // e.g. "database.url"
|
|
288
320
|
readonly variable: string // e.g. "DATABASE_URL"
|
|
289
321
|
readonly code: ConfigIssueCode
|
|
290
|
-
readonly message: string //
|
|
322
|
+
readonly message: string // constraint description, or your own `custom` message
|
|
291
323
|
}
|
|
292
324
|
```
|
|
293
325
|
|
|
@@ -353,7 +385,7 @@ that needs to reference the schema shape directly.
|
|
|
353
385
|
│ │
|
|
354
386
|
▼ ▼
|
|
355
387
|
deep-frozen BymaxConfigValidationError
|
|
356
|
-
BYMAX_CONFIG one aggregated
|
|
388
|
+
BYMAX_CONFIG one aggregated boot-time report
|
|
357
389
|
│ │
|
|
358
390
|
▼ ▼
|
|
359
391
|
ConfigService the process does not start
|
|
@@ -380,15 +412,15 @@ accidentally pass because a variable happened to be set on the machine.
|
|
|
380
412
|
|
|
381
413
|
### Design Principles
|
|
382
414
|
|
|
383
|
-
| Principle | Description
|
|
384
|
-
| ------------------------------------ |
|
|
385
|
-
| 💥 **Refuse to start** | A misconfigured process is stopped before it serves a request, rather than discovering the gap at the first call that needs the value
|
|
386
|
-
| 🤐 **Value-free by contract** | `errors.ts`, `env-validator.ts` and `report-formatter.ts` each state it for themselves:
|
|
387
|
-
| 📋 **Report everything at once** | Validation collects every failure before throwing, so fixing an environment is one cycle instead of one per variable
|
|
388
|
-
| 🧊 **Frozen after validation** | Nothing in the running process rewrites configuration under a component that already read it
|
|
389
|
-
| 🔤 **The schema is the type** | Dot paths are checked against the schema at compile time, so a typo is a build error rather than a runtime `undefined`
|
|
390
|
-
| ⚙️ **Configuration over convention** | Everything goes through `forRoot`/`forRootAsync`. No file discovery, no magic paths, no hidden precedence rules
|
|
391
|
-
| 🧩 **One class identity** | The module and `./testing` import the shared runtime by package specifier, so `ConfigService` is one injection token in ESM and CommonJS alike
|
|
415
|
+
| Principle | Description |
|
|
416
|
+
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
417
|
+
| 💥 **Refuse to start** | A misconfigured process is stopped before it serves a request, rather than discovering the gap at the first call that needs the value |
|
|
418
|
+
| 🤐 **Value-free by contract** | `errors.ts`, `env-validator.ts` and `report-formatter.ts` each state it for themselves: a generated message carries the expected constraint and the schema's own options, never the received value |
|
|
419
|
+
| 📋 **Report everything at once** | Validation collects every failure before throwing, so fixing an environment is one cycle instead of one per variable |
|
|
420
|
+
| 🧊 **Frozen after validation** | Nothing in the running process rewrites configuration under a component that already read it |
|
|
421
|
+
| 🔤 **The schema is the type** | Dot paths are checked against the schema at compile time, so a typo is a build error rather than a runtime `undefined` |
|
|
422
|
+
| ⚙️ **Configuration over convention** | Everything goes through `forRoot`/`forRootAsync`. No file discovery, no magic paths, no hidden precedence rules |
|
|
423
|
+
| 🧩 **One class identity** | The module and `./testing` import the shared runtime by package specifier, so `ConfigService` is one injection token in ESM and CommonJS alike |
|
|
392
424
|
|
|
393
425
|
---
|
|
394
426
|
|
|
@@ -399,10 +431,11 @@ guaranteed to be printed, logged, and pasted into an issue. That shapes the whol
|
|
|
399
431
|
|
|
400
432
|
### Errors are value-free by contract
|
|
401
433
|
|
|
402
|
-
Every message states the _expected_ constraint — the type, the
|
|
403
|
-
from the schema, the bound — and never the received value.
|
|
404
|
-
`report-formatter` each say so in their own contracts, and
|
|
405
|
-
to it.
|
|
434
|
+
Every message this library generates states the _expected_ constraint — the type, the
|
|
435
|
+
allowed enum options taken from the schema, the bound — and never the received value.
|
|
436
|
+
`env-validator` and `report-formatter` each say so in their own contracts, and
|
|
437
|
+
property-based tests hold them to it. The messages you write yourself are the exception,
|
|
438
|
+
covered below.
|
|
406
439
|
|
|
407
440
|
### The service does not serialize its configuration
|
|
408
441
|
|
|
@@ -434,27 +467,31 @@ not choose.
|
|
|
434
467
|
|
|
435
468
|
### Your own messages are yours
|
|
436
469
|
|
|
437
|
-
Value-free applies to this library's reports.
|
|
438
|
-
|
|
470
|
+
Value-free applies to this library's reports. A `custom` issue from your own `.check`,
|
|
471
|
+
`.refine` or `.superRefine` is printed as written — it is schema text, and it is the only
|
|
472
|
+
place a conditional or cross-field rule can state itself. That also means the message is
|
|
473
|
+
yours to keep value-free: if it interpolates the received value, that value reaches the
|
|
474
|
+
report.
|
|
439
475
|
|
|
440
476
|
---
|
|
441
477
|
|
|
442
478
|
## 🛡️ Security Table
|
|
443
479
|
|
|
444
|
-
| Layer | Implementation
|
|
445
|
-
| -------------- |
|
|
446
|
-
| Error messages | Expected constraint only — type, schema-declared enum options, bounds; never the received value
|
|
447
|
-
| Failure mode | Refuse to start; the whole schema is validated before the context finishes building
|
|
448
|
-
| Aggregation | Every failure reported at once, so a fix cycle is one restart rather than one per variable
|
|
449
|
-
| Immutability | The validated result is frozen before it is provided
|
|
450
|
-
| Service state | The root lives in a private field; serializing the injected service yields namespace names only
|
|
451
|
-
| Sources | `process.env` only — no file, no network, no remote provider
|
|
452
|
-
| Type surface | Dot paths checked against the schema at compile time; a typo is a build error, not a runtime `undefined`
|
|
453
|
-
| Supply chain | `dependencies: {}`; third-party Actions pinned by commit SHA (org-internal reusables by tag); CodeQL and OpenSSF Scorecard
|
|
480
|
+
| Layer | Implementation |
|
|
481
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
482
|
+
| Error messages | Expected constraint only — type, schema-declared enum options, bounds; never the received value. A `custom` message from your own schema is the exception: it prints as written |
|
|
483
|
+
| Failure mode | Refuse to start; the whole schema is validated before the context finishes building |
|
|
484
|
+
| Aggregation | Every failure reported at once, so a fix cycle is one restart rather than one per variable |
|
|
485
|
+
| Immutability | The validated result is frozen before it is provided |
|
|
486
|
+
| Service state | The root lives in a private field; serializing the injected service yields namespace names only |
|
|
487
|
+
| Sources | `process.env` only — no file, no network, no remote provider |
|
|
488
|
+
| Type surface | Dot paths checked against the schema at compile time; a typo is a build error, not a runtime `undefined` |
|
|
489
|
+
| Supply chain | `dependencies: {}`; third-party Actions pinned by commit SHA (org-internal reusables by tag); CodeQL and OpenSSF Scorecard |
|
|
454
490
|
|
|
455
491
|
> [!IMPORTANT]
|
|
456
|
-
> **Value-free applies to this library's own reports.**
|
|
457
|
-
>
|
|
492
|
+
> **Value-free applies to this library's own reports.** A `custom` message from your
|
|
493
|
+
> schema's `.check`, `.refine` or `.superRefine` is printed as written — including a
|
|
494
|
+
> value, if you interpolate one into it.
|
|
458
495
|
|
|
459
496
|
---
|
|
460
497
|
|
|
@@ -477,14 +514,15 @@ Configuration decides whether a process may start at all, so the suite is held t
|
|
|
477
514
|
beyond "the tests pass".
|
|
478
515
|
|
|
479
516
|
- ✅ **100% line coverage** — statements, branches, functions and lines, enforced as a gate
|
|
480
|
-
- ✅ **99.
|
|
517
|
+
- ✅ **99.61% mutation score** — verified with [Stryker](https://stryker-mutator.io/) at
|
|
481
518
|
`break: 95`; ten provable equivalents carry their reason inline, and the one survivor left
|
|
482
519
|
is an equivalent no directive can reach, so it stays counted rather than silenced
|
|
483
520
|
([report](./docs/mutation_testing_results.md))
|
|
484
521
|
- ✅ **The value-free guarantee is tested, not assumed** — a sentinel secret is placed in a
|
|
485
522
|
source and the assertion is that it appears nowhere in the rendered report or in the thrown
|
|
486
523
|
error, because a guarantee about what is _absent_ is the one thing an ordinary assertion
|
|
487
|
-
about output does not cover
|
|
524
|
+
about output does not cover; the schema under test writes no `custom` message, so what is
|
|
525
|
+
proven is the guarantee this package makes for its own generated text
|
|
488
526
|
- ✅ **Published-artifact gates** — `check:exports` resolves the types the way each module
|
|
489
527
|
system does, `check:runtime` loads every subpath from the packed tarball in ESM and
|
|
490
528
|
CommonJS, and `check:published` compiles this README's snippets against `dist/`
|
package/dist/internal/index.cjs
CHANGED
|
@@ -87,7 +87,7 @@ var ConfigErrorCode = Object.freeze({
|
|
|
87
87
|
});
|
|
88
88
|
var BymaxConfigValidationError = class _BymaxConfigValidationError extends Error {
|
|
89
89
|
/**
|
|
90
|
-
* Build the aggregated error from
|
|
90
|
+
* Build the aggregated error from the collected issues.
|
|
91
91
|
*
|
|
92
92
|
* @param issues - Every collected violation; copied and frozen so neither the
|
|
93
93
|
* caller nor consumer code can mutate the reported list.
|
|
@@ -146,6 +146,7 @@ function resolveNamespacePrefixes(schema) {
|
|
|
146
146
|
// src/env-validator.ts
|
|
147
147
|
var UNKNOWN_KEY_MESSAGE = "unknown variable not declared in the schema";
|
|
148
148
|
var MISSING_MESSAGE = "missing required value";
|
|
149
|
+
var ZOD_DEFAULT_CUSTOM_MESSAGE = "Invalid input";
|
|
149
150
|
function collectPresentLeaves(bindings, source) {
|
|
150
151
|
const present = [];
|
|
151
152
|
for (const binding of bindings) {
|
|
@@ -189,13 +190,20 @@ function describeConstraint(issue) {
|
|
|
189
190
|
return "invalid value";
|
|
190
191
|
}
|
|
191
192
|
}
|
|
193
|
+
function authoredMessage(issue) {
|
|
194
|
+
if (issue.code !== "custom") return void 0;
|
|
195
|
+
const collapsed = issue.message.replace(/\s+/g, " ").trim();
|
|
196
|
+
if (collapsed.length === 0 || collapsed === ZOD_DEFAULT_CUSTOM_MESSAGE) return void 0;
|
|
197
|
+
return collapsed;
|
|
198
|
+
}
|
|
192
199
|
function toConfigIssue(binding, issue, source) {
|
|
193
200
|
const isMissing = source.get(binding.variable) === void 0;
|
|
201
|
+
const authored = authoredMessage(issue);
|
|
194
202
|
return {
|
|
195
203
|
path: binding.path,
|
|
196
204
|
variable: binding.variable,
|
|
197
205
|
code: isMissing ? ConfigErrorCode.MISSING : ConfigErrorCode.INVALID,
|
|
198
|
-
message: isMissing ? MISSING_MESSAGE : describeConstraint(issue)
|
|
206
|
+
message: authored ?? (isMissing ? MISSING_MESSAGE : describeConstraint(issue))
|
|
199
207
|
};
|
|
200
208
|
}
|
|
201
209
|
function translateIssues(zodIssues, bindings, source) {
|
|
@@ -99,11 +99,12 @@ type Path<TConfig> = {
|
|
|
99
99
|
type PathValue<TConfig, TPath extends Path<TConfig>> = TPath extends `${infer Namespace}.${infer Leaf}` ? Namespace extends keyof TConfig ? Leaf extends keyof TConfig[Namespace] ? TConfig[Namespace][Leaf] : never : never : never;
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
* @fileoverview
|
|
103
|
-
*
|
|
102
|
+
* @fileoverview Error model for the configuration validation pipeline: the
|
|
103
|
+
* frozen issue-code catalog, the ConfigIssue shape, and the
|
|
104
104
|
* BymaxConfigValidationError aggregate. The error carries every violation as a
|
|
105
105
|
* structured, immutable issue list and renders a multi-line report that never
|
|
106
|
-
* echoes a raw source value
|
|
106
|
+
* echoes a raw source value of its own — see {@link ConfigIssue} for the one
|
|
107
|
+
* exception, a message written by the schema author.
|
|
107
108
|
* @layer Error
|
|
108
109
|
*/
|
|
109
110
|
/**
|
|
@@ -135,11 +136,17 @@ type ConfigValidationCode = typeof ConfigErrorCode.VALIDATION;
|
|
|
135
136
|
/** Machine-readable code classifying a single {@link ConfigIssue}. */
|
|
136
137
|
type ConfigIssueCode = typeof ConfigErrorCode.MISSING | typeof ConfigErrorCode.INVALID | typeof ConfigErrorCode.UNKNOWN_KEY;
|
|
137
138
|
/**
|
|
138
|
-
* A single configuration violation, described
|
|
139
|
+
* A single configuration violation, described for the operator who must fix it.
|
|
139
140
|
*
|
|
140
|
-
* Every
|
|
141
|
-
* constraint, never the received input, so an issue can be
|
|
142
|
-
* without leaking a secret.
|
|
141
|
+
* Every description this package generates is value-free by contract: `message`
|
|
142
|
+
* states the expected constraint, never the received input, so an issue can be
|
|
143
|
+
* logged or serialized without leaking a secret.
|
|
144
|
+
*
|
|
145
|
+
* The one exception is the message a schema author attaches to a `custom` issue
|
|
146
|
+
* (`.check`, `.refine`, `.superRefine`), which is reported as written because it
|
|
147
|
+
* is the only statement of a rule that has no structural constraint to describe.
|
|
148
|
+
* A message that interpolates the received value therefore carries that value
|
|
149
|
+
* into the report; keeping an authored message value-free is the author's call.
|
|
143
150
|
*/
|
|
144
151
|
interface ConfigIssue {
|
|
145
152
|
/** Nested config path, e.g. `database.url`; the namespace alone (e.g. `database`) for an unknown-key issue. */
|
|
@@ -148,16 +155,17 @@ interface ConfigIssue {
|
|
|
148
155
|
readonly variable: string;
|
|
149
156
|
/** Stable machine-readable classification. */
|
|
150
157
|
readonly code: ConfigIssueCode;
|
|
151
|
-
/** Human-readable constraint description
|
|
158
|
+
/** Human-readable constraint description; value-free unless a schema author's own `custom` message says otherwise. */
|
|
152
159
|
readonly message: string;
|
|
153
160
|
}
|
|
154
161
|
/**
|
|
155
|
-
* Aggregated
|
|
162
|
+
* Aggregated configuration validation error.
|
|
156
163
|
*
|
|
157
164
|
* Thrown once at bootstrap when the source fails the schema. It carries every
|
|
158
165
|
* violation in an immutable, structured {@link ConfigIssue} list and a
|
|
159
166
|
* human-readable message that lists the offending variables and their expected
|
|
160
|
-
* constraints, never their received values
|
|
167
|
+
* constraints, never their received values — except where a schema author's own
|
|
168
|
+
* `custom` message says otherwise, as described on {@link ConfigIssue}.
|
|
161
169
|
*
|
|
162
170
|
* @example
|
|
163
171
|
* ```typescript
|
|
@@ -176,7 +184,7 @@ declare class BymaxConfigValidationError extends Error {
|
|
|
176
184
|
/** The immutable, aggregated list of collected violations. */
|
|
177
185
|
readonly issues: ReadonlyArray<ConfigIssue>;
|
|
178
186
|
/**
|
|
179
|
-
* Build the aggregated error from
|
|
187
|
+
* Build the aggregated error from the collected issues.
|
|
180
188
|
*
|
|
181
189
|
* @param issues - Every collected violation; copied and frozen so neither the
|
|
182
190
|
* caller nor consumer code can mutate the reported list.
|
|
@@ -187,7 +195,7 @@ declare class BymaxConfigValidationError extends Error {
|
|
|
187
195
|
/**
|
|
188
196
|
* @fileoverview Public options contract for the BymaxConfigModule dynamic
|
|
189
197
|
* module: the schema to validate against, an optional raw source (defaulting to
|
|
190
|
-
* the process environment inside the provider factory), an optional
|
|
198
|
+
* the process environment inside the provider factory), an optional
|
|
191
199
|
* observability hook invoked before the fail-fast throw, and an optional strict
|
|
192
200
|
* unknown-key flag. This file declares only types and carries no runtime code.
|
|
193
201
|
* @layer Types
|
|
@@ -217,10 +225,12 @@ interface BymaxConfigModuleOptions<TSchema extends EnvSchema = EnvSchema> {
|
|
|
217
225
|
*/
|
|
218
226
|
readonly source?: Record<string, string | undefined>;
|
|
219
227
|
/**
|
|
220
|
-
* Observability hook invoked once with the structured
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
228
|
+
* Observability hook invoked once with the structured issue list immediately
|
|
229
|
+
* before the validation error is thrown. The issues are value-free on the
|
|
230
|
+
* terms stated by {@link ConfigIssue}: generated descriptions never carry a
|
|
231
|
+
* received value, while a schema author's own `custom` message is passed
|
|
232
|
+
* through as written. It is observability only and cannot suppress the
|
|
233
|
+
* failure: the error still propagates even when the hook itself throws.
|
|
224
234
|
*/
|
|
225
235
|
readonly onValidationError?: (issues: ReadonlyArray<ConfigIssue>) => void;
|
|
226
236
|
/**
|
|
@@ -514,8 +524,8 @@ declare function defineEnv<TShape extends EnvShape>(shape: TShape): EnvSchema<TS
|
|
|
514
524
|
* @fileoverview The BYMAX_CONFIG provider factory: the fail-fast heart of the
|
|
515
525
|
* module. It resolves the source (defaulting to `process.env`, using a custom
|
|
516
526
|
* source verbatim), runs the single-pass validator, and on success returns the
|
|
517
|
-
* deep-frozen typed config. On failure it invokes the
|
|
518
|
-
*
|
|
527
|
+
* deep-frozen typed config. On failure it invokes the observability hook
|
|
528
|
+
* exactly once before rethrowing the aggregated error; the hook is
|
|
519
529
|
* observability only and can never suppress or replace the failure, so a
|
|
520
530
|
* misconfigured process is stopped before any consumer provider is constructed.
|
|
521
531
|
* @layer Provider
|
package/dist/internal/index.d.ts
CHANGED
|
@@ -99,11 +99,12 @@ type Path<TConfig> = {
|
|
|
99
99
|
type PathValue<TConfig, TPath extends Path<TConfig>> = TPath extends `${infer Namespace}.${infer Leaf}` ? Namespace extends keyof TConfig ? Leaf extends keyof TConfig[Namespace] ? TConfig[Namespace][Leaf] : never : never : never;
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
* @fileoverview
|
|
103
|
-
*
|
|
102
|
+
* @fileoverview Error model for the configuration validation pipeline: the
|
|
103
|
+
* frozen issue-code catalog, the ConfigIssue shape, and the
|
|
104
104
|
* BymaxConfigValidationError aggregate. The error carries every violation as a
|
|
105
105
|
* structured, immutable issue list and renders a multi-line report that never
|
|
106
|
-
* echoes a raw source value
|
|
106
|
+
* echoes a raw source value of its own — see {@link ConfigIssue} for the one
|
|
107
|
+
* exception, a message written by the schema author.
|
|
107
108
|
* @layer Error
|
|
108
109
|
*/
|
|
109
110
|
/**
|
|
@@ -135,11 +136,17 @@ type ConfigValidationCode = typeof ConfigErrorCode.VALIDATION;
|
|
|
135
136
|
/** Machine-readable code classifying a single {@link ConfigIssue}. */
|
|
136
137
|
type ConfigIssueCode = typeof ConfigErrorCode.MISSING | typeof ConfigErrorCode.INVALID | typeof ConfigErrorCode.UNKNOWN_KEY;
|
|
137
138
|
/**
|
|
138
|
-
* A single configuration violation, described
|
|
139
|
+
* A single configuration violation, described for the operator who must fix it.
|
|
139
140
|
*
|
|
140
|
-
* Every
|
|
141
|
-
* constraint, never the received input, so an issue can be
|
|
142
|
-
* without leaking a secret.
|
|
141
|
+
* Every description this package generates is value-free by contract: `message`
|
|
142
|
+
* states the expected constraint, never the received input, so an issue can be
|
|
143
|
+
* logged or serialized without leaking a secret.
|
|
144
|
+
*
|
|
145
|
+
* The one exception is the message a schema author attaches to a `custom` issue
|
|
146
|
+
* (`.check`, `.refine`, `.superRefine`), which is reported as written because it
|
|
147
|
+
* is the only statement of a rule that has no structural constraint to describe.
|
|
148
|
+
* A message that interpolates the received value therefore carries that value
|
|
149
|
+
* into the report; keeping an authored message value-free is the author's call.
|
|
143
150
|
*/
|
|
144
151
|
interface ConfigIssue {
|
|
145
152
|
/** Nested config path, e.g. `database.url`; the namespace alone (e.g. `database`) for an unknown-key issue. */
|
|
@@ -148,16 +155,17 @@ interface ConfigIssue {
|
|
|
148
155
|
readonly variable: string;
|
|
149
156
|
/** Stable machine-readable classification. */
|
|
150
157
|
readonly code: ConfigIssueCode;
|
|
151
|
-
/** Human-readable constraint description
|
|
158
|
+
/** Human-readable constraint description; value-free unless a schema author's own `custom` message says otherwise. */
|
|
152
159
|
readonly message: string;
|
|
153
160
|
}
|
|
154
161
|
/**
|
|
155
|
-
* Aggregated
|
|
162
|
+
* Aggregated configuration validation error.
|
|
156
163
|
*
|
|
157
164
|
* Thrown once at bootstrap when the source fails the schema. It carries every
|
|
158
165
|
* violation in an immutable, structured {@link ConfigIssue} list and a
|
|
159
166
|
* human-readable message that lists the offending variables and their expected
|
|
160
|
-
* constraints, never their received values
|
|
167
|
+
* constraints, never their received values — except where a schema author's own
|
|
168
|
+
* `custom` message says otherwise, as described on {@link ConfigIssue}.
|
|
161
169
|
*
|
|
162
170
|
* @example
|
|
163
171
|
* ```typescript
|
|
@@ -176,7 +184,7 @@ declare class BymaxConfigValidationError extends Error {
|
|
|
176
184
|
/** The immutable, aggregated list of collected violations. */
|
|
177
185
|
readonly issues: ReadonlyArray<ConfigIssue>;
|
|
178
186
|
/**
|
|
179
|
-
* Build the aggregated error from
|
|
187
|
+
* Build the aggregated error from the collected issues.
|
|
180
188
|
*
|
|
181
189
|
* @param issues - Every collected violation; copied and frozen so neither the
|
|
182
190
|
* caller nor consumer code can mutate the reported list.
|
|
@@ -187,7 +195,7 @@ declare class BymaxConfigValidationError extends Error {
|
|
|
187
195
|
/**
|
|
188
196
|
* @fileoverview Public options contract for the BymaxConfigModule dynamic
|
|
189
197
|
* module: the schema to validate against, an optional raw source (defaulting to
|
|
190
|
-
* the process environment inside the provider factory), an optional
|
|
198
|
+
* the process environment inside the provider factory), an optional
|
|
191
199
|
* observability hook invoked before the fail-fast throw, and an optional strict
|
|
192
200
|
* unknown-key flag. This file declares only types and carries no runtime code.
|
|
193
201
|
* @layer Types
|
|
@@ -217,10 +225,12 @@ interface BymaxConfigModuleOptions<TSchema extends EnvSchema = EnvSchema> {
|
|
|
217
225
|
*/
|
|
218
226
|
readonly source?: Record<string, string | undefined>;
|
|
219
227
|
/**
|
|
220
|
-
* Observability hook invoked once with the structured
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
228
|
+
* Observability hook invoked once with the structured issue list immediately
|
|
229
|
+
* before the validation error is thrown. The issues are value-free on the
|
|
230
|
+
* terms stated by {@link ConfigIssue}: generated descriptions never carry a
|
|
231
|
+
* received value, while a schema author's own `custom` message is passed
|
|
232
|
+
* through as written. It is observability only and cannot suppress the
|
|
233
|
+
* failure: the error still propagates even when the hook itself throws.
|
|
224
234
|
*/
|
|
225
235
|
readonly onValidationError?: (issues: ReadonlyArray<ConfigIssue>) => void;
|
|
226
236
|
/**
|
|
@@ -514,8 +524,8 @@ declare function defineEnv<TShape extends EnvShape>(shape: TShape): EnvSchema<TS
|
|
|
514
524
|
* @fileoverview The BYMAX_CONFIG provider factory: the fail-fast heart of the
|
|
515
525
|
* module. It resolves the source (defaulting to `process.env`, using a custom
|
|
516
526
|
* source verbatim), runs the single-pass validator, and on success returns the
|
|
517
|
-
* deep-frozen typed config. On failure it invokes the
|
|
518
|
-
*
|
|
527
|
+
* deep-frozen typed config. On failure it invokes the observability hook
|
|
528
|
+
* exactly once before rethrowing the aggregated error; the hook is
|
|
519
529
|
* observability only and can never suppress or replace the failure, so a
|
|
520
530
|
* misconfigured process is stopped before any consumer provider is constructed.
|
|
521
531
|
* @layer Provider
|
package/dist/internal/index.mjs
CHANGED
|
@@ -85,7 +85,7 @@ var ConfigErrorCode = Object.freeze({
|
|
|
85
85
|
});
|
|
86
86
|
var BymaxConfigValidationError = class _BymaxConfigValidationError extends Error {
|
|
87
87
|
/**
|
|
88
|
-
* Build the aggregated error from
|
|
88
|
+
* Build the aggregated error from the collected issues.
|
|
89
89
|
*
|
|
90
90
|
* @param issues - Every collected violation; copied and frozen so neither the
|
|
91
91
|
* caller nor consumer code can mutate the reported list.
|
|
@@ -144,6 +144,7 @@ function resolveNamespacePrefixes(schema) {
|
|
|
144
144
|
// src/env-validator.ts
|
|
145
145
|
var UNKNOWN_KEY_MESSAGE = "unknown variable not declared in the schema";
|
|
146
146
|
var MISSING_MESSAGE = "missing required value";
|
|
147
|
+
var ZOD_DEFAULT_CUSTOM_MESSAGE = "Invalid input";
|
|
147
148
|
function collectPresentLeaves(bindings, source) {
|
|
148
149
|
const present = [];
|
|
149
150
|
for (const binding of bindings) {
|
|
@@ -187,13 +188,20 @@ function describeConstraint(issue) {
|
|
|
187
188
|
return "invalid value";
|
|
188
189
|
}
|
|
189
190
|
}
|
|
191
|
+
function authoredMessage(issue) {
|
|
192
|
+
if (issue.code !== "custom") return void 0;
|
|
193
|
+
const collapsed = issue.message.replace(/\s+/g, " ").trim();
|
|
194
|
+
if (collapsed.length === 0 || collapsed === ZOD_DEFAULT_CUSTOM_MESSAGE) return void 0;
|
|
195
|
+
return collapsed;
|
|
196
|
+
}
|
|
190
197
|
function toConfigIssue(binding, issue, source) {
|
|
191
198
|
const isMissing = source.get(binding.variable) === void 0;
|
|
199
|
+
const authored = authoredMessage(issue);
|
|
192
200
|
return {
|
|
193
201
|
path: binding.path,
|
|
194
202
|
variable: binding.variable,
|
|
195
203
|
code: isMissing ? ConfigErrorCode.MISSING : ConfigErrorCode.INVALID,
|
|
196
|
-
message: isMissing ? MISSING_MESSAGE : describeConstraint(issue)
|
|
204
|
+
message: authored ?? (isMissing ? MISSING_MESSAGE : describeConstraint(issue))
|
|
197
205
|
};
|
|
198
206
|
}
|
|
199
207
|
function translateIssues(zodIssues, bindings, source) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bymax-one/nest-config",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
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",
|