@bymax-one/nest-config 1.0.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/README.md ADDED
@@ -0,0 +1,572 @@
1
+ <p align="center">
2
+ <img src="https://img.shields.io/badge/%40bymax--one-nest--config-000000?style=for-the-badge&logo=nestjs&logoColor=E0234E" alt="@bymax-one/nest-config" />
3
+ </p>
4
+
5
+ <h1 align="center">@bymax-one/nest-config</h1>
6
+
7
+ <p align="center">
8
+ <strong>Typed and validated environment configuration for NestJS</strong><br />
9
+ <sub>Zod v4 · Fail-fast at bootstrap · Value-free errors · Typed dot-path access · Testing helpers · Zero Runtime Dependencies</sub>
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://www.npmjs.com/package/@bymax-one/nest-config"><img src="https://img.shields.io/npm/v/@bymax-one/nest-config?style=flat-square&colorA=000000&colorB=000000" alt="npm version" /></a>
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
+ <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
+ <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-95.72%25-brightgreen?style=flat-square&colorA=000000" alt="mutation score" /></a>
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
+ <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
+ <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>
21
+ <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-24%2B-339933?style=flat-square&logo=node.js&logoColor=white" alt="Node.js" /></a>
22
+ </p>
23
+
24
+ <p align="center">
25
+ <a href="https://github.com/bymaxone/nest-config">GitHub</a> ·
26
+ <a href="https://github.com/bymaxone/nest-config/issues">Issues</a> ·
27
+ <a href="#-quick-start">Quick Start</a> ·
28
+ <a href="#-api-reference">API Reference</a> ·
29
+ <a href="https://github.com/bymaxone/nest-config-example">Example App</a>
30
+ </p>
31
+
32
+ ---
33
+
34
+ ## ✨ Overview
35
+
36
+ `@bymax-one/nest-config` gives a NestJS application a single, typed, validated entry point
37
+ for environment configuration. It validates `process.env` exactly once at bootstrap against a
38
+ [Zod](https://zod.dev) v4 schema, fails fast with one aggregated report, and exposes the
39
+ result as a frozen object reached through typed dot paths.
40
+
41
+ The library has **zero direct dependencies** — `zod`, `reflect-metadata` and `@nestjs/*`
42
+ arrive as peer dependencies, so you control exact versions and the supply-chain surface stays
43
+ minimal.
44
+
45
+ ### Why nest-config?
46
+
47
+ - **A misconfigured process does not boot.** Validation runs before the application context
48
+ finishes building, so a service that is missing a secret never accepts the first request
49
+ that would need it.
50
+ - **One round trip to fix the environment.** Every violation is collected and reported
51
+ together, not the first one that fails.
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 carry the
54
+ expected constraint and the schema's own enum options, never the received value.
55
+ - **The schema is the type.** `get('database.url')` is `string` and `get('server.port')` is
56
+ `number` because the schema says so; a typo in a path is a build error, not a runtime
57
+ `undefined`.
58
+
59
+ ---
60
+
61
+ ## 🔥 Features
62
+
63
+ ### ✅ Validation
64
+
65
+ - ✅ **Validate once, at bootstrap** — parsed and validated a single time, before the
66
+ application serves traffic; there is no lazy path that could discover a problem later
67
+ - ✅ **Fail fast, fail complete** — every violation reported together in one aggregated
68
+ 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
71
+ - ✅ **Zod v4 schemas** — the full type language, including coercion, refinements and
72
+ defaults, with `defineEnv` shaping namespaces and leaves
73
+
74
+ ### 🔒 The Validated Object
75
+
76
+ - ✅ **Immutable by construction** — deep-frozen before it enters the DI container;
77
+ configuration is a fact about the process, not a mutable bag
78
+ - ✅ **Typed dot-path access** — `get('database.url')` is `string`, `get('server.port')` is
79
+ `number`, with no casts and no `any` in the chain
80
+ - ✅ **Injectable environment** — the raw source defaults to `process.env` but is a plain
81
+ injectable record, so a test validates any input without touching the real environment
82
+
83
+ ### 🧩 Developer Experience
84
+
85
+ - ✅ **Zero runtime dependencies** — `zod`, `reflect-metadata` and `@nestjs/*` all arrive as
86
+ peers, so you pin the versions
87
+ - ✅ **Testing entry point** — `./testing` builds the same validated object from an explicit
88
+ record, so a test never passes because a variable happened to be set on the machine
89
+ - ✅ **One class identity** — the module and its testing entry share a single `./internal`
90
+ bundle, so `ConfigService` is the same injection token from either, in ESM and CommonJS
91
+ - ✅ **Dual-format output** — ESM + CJS with declarations for each format, verified against
92
+ the packed tarball on every run
93
+ - ✅ **Typed end to end** — TypeScript `strict` with `exactOptionalPropertyTypes` and
94
+ `noUncheckedIndexedAccess`; zero `any`
95
+
96
+ ---
97
+
98
+ ## 📦 Subpath Exports
99
+
100
+ | Subpath | Contents |
101
+ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
102
+ | `.` | `BymaxConfigModule`, `ConfigService`, `defineEnv`, the DI tokens, `BymaxConfigValidationError` and every option type |
103
+ | `./testing` | `configTestingModule`, `createTestConfig` — a validated config for a test without booting the application's own |
104
+ | `./internal` | The shared runtime both entry points build on. **Not public API**: it is in the `exports` map because it has to resolve at runtime, not because it is meant to be imported |
105
+
106
+ `.` and `./testing` are separate bundles, and a module reached from both by a
107
+ relative path would be _copied_ into each — a copied class is a different injection
108
+ token and a different `instanceof` target. `./internal` is the one bundle they both
109
+ import by package specifier, which is what gives `ConfigService` a single identity in
110
+ ESM and CommonJS alike.
111
+
112
+ ### Install
113
+
114
+ ```bash
115
+ pnpm add @bymax-one/nest-config @nestjs/common @nestjs/core reflect-metadata zod
116
+ ```
117
+
118
+ ### Peer requirements
119
+
120
+ | Package | Version |
121
+ | ------------------ | ---------- |
122
+ | Node.js | `>=24.0.0` |
123
+ | `@nestjs/common` | `^11.0.16` |
124
+ | `@nestjs/core` | `^11.1.18` |
125
+ | `reflect-metadata` | `^0.2.0` |
126
+ | `zod` | `^4.0.0` |
127
+
128
+ ## 🚀 Quick Start
129
+
130
+ Define a schema with `defineEnv`. Top-level keys are namespaces, leaves are
131
+ environment-derived values; each leaf reads a `SCREAMING_SNAKE_CASE` variable
132
+ derived from its path (`database.url` reads `DATABASE_URL`).
133
+
134
+ <!-- Source: test/e2e/fixtures/env.schema.ts -->
135
+
136
+ ```typescript
137
+ import { z } from 'zod'
138
+ import { defineEnv } from '@bymax-one/nest-config'
139
+
140
+ export const envSchema = defineEnv({
141
+ server: z.object({
142
+ port: z.coerce.number().int().min(1).max(65535).default(3000),
143
+ env: z.enum(['development', 'test', 'production']).default('development')
144
+ }),
145
+ database: z.object({
146
+ url: z.url()
147
+ }),
148
+ redis: z.object({
149
+ url: z.url()
150
+ }),
151
+ log: z.object({
152
+ level: z.enum(['trace', 'debug', 'info', 'warn', 'error']).default('info')
153
+ })
154
+ })
155
+
156
+ export type AppConfig = typeof envSchema.infer
157
+ ```
158
+
159
+ Register `BymaxConfigModule` once in the root module (it is global by
160
+ default):
161
+
162
+ ```typescript
163
+ import { Module } from '@nestjs/common'
164
+ import { BymaxConfigModule } from '@bymax-one/nest-config'
165
+ import { envSchema } from './config/env.schema'
166
+
167
+ @Module({
168
+ imports: [BymaxConfigModule.forRoot({ schema: envSchema })]
169
+ })
170
+ export class AppModule {}
171
+ ```
172
+
173
+ Inject the typed `ConfigService` wherever configuration is needed, no casts:
174
+
175
+ <!-- Source: test/e2e/fixtures/feature.provider.ts -->
176
+
177
+ ```typescript
178
+ import { Inject, Injectable } from '@nestjs/common'
179
+ import { ConfigService } from '@bymax-one/nest-config'
180
+ import type { AppConfig } from './config/env.schema'
181
+
182
+ @Injectable()
183
+ export class FeatureProvider {
184
+ public constructor(@Inject(ConfigService) private readonly config: ConfigService<AppConfig>) {}
185
+
186
+ public describeConnection(): string {
187
+ return `${this.config.get('database.url')}::${this.config.get('server.port')}`
188
+ }
189
+ }
190
+ ```
191
+
192
+ A process started with an incomplete environment exits immediately with the
193
+ aggregated report, never one violation at a time:
194
+
195
+ ```
196
+ BymaxConfigValidationError: environment validation failed (3 issues)
197
+
198
+ DATABASE_URL missing required value (expected: url)
199
+ AUTH_JWT_SECRET too short (expected: string, minimum 32 characters)
200
+ SERVER_PORT invalid value (expected: integer between 1 and 65535)
201
+
202
+ Fix the variables above and restart the process.
203
+ ```
204
+
205
+ Raw values are never printed, not truncated, not masked, absent. This is a
206
+ hard guarantee of the package, verified by tests.
207
+
208
+ ## 📖 API Reference
209
+
210
+ ### `defineEnv(shape)`
211
+
212
+ A thin, typed factory over `z.object(...)` that establishes the two-level
213
+ namespace convention: top-level keys are namespaces, leaves are
214
+ environment-derived values. It composes the caller's schemas as-is, never
215
+ rewriting, cloning, or wrapping them, so coercion and defaults stay explicit
216
+ consumer choices. Leaves use `z.coerce.*` because the source arrives as
217
+ strings (the shape of `process.env`).
218
+
219
+ A leaf can override its derived variable name through schema metadata when a
220
+ legacy name must be preserved:
221
+
222
+ ```typescript
223
+ database: z.object({
224
+ // Reads DB_CONNECTION_STRING instead of the derived DATABASE_URL.
225
+ url: z.url().meta({ env: 'DB_CONNECTION_STRING' })
226
+ })
227
+ ```
228
+
229
+ ### `BymaxConfigModule`
230
+
231
+ | Method | Signature |
232
+ | ----------------------- | ----------------------------------------------------------------- |
233
+ | `forRoot(options)` | `(options: BymaxConfigModuleOptions) => DynamicModule` |
234
+ | `forRootAsync(options)` | `(options: ConfigurableModuleAsyncOptions<...>) => DynamicModule` |
235
+
236
+ `BymaxConfigModuleOptions`:
237
+
238
+ | Field | Type | Required | Notes |
239
+ | ------------------- | ---------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
240
+ | `schema` | `EnvSchema` | yes | Produced by `defineEnv`. |
241
+ | `source` | `Record<string, string \| undefined>` | no | Defaults to `process.env` in the provider factory. Injectable for tests. |
242
+ | `onValidationError` | `(issues: ReadonlyArray<ConfigIssue>) => void` | no | Observability hook invoked before the fail-fast throw. Cannot suppress the failure. |
243
+ | `strict` | `boolean` | no | When true, source variables matching a namespace prefix but no declared leaf raise `BYMAX_CONFIG_UNKNOWN_KEY`. Defaults to `false`. |
244
+
245
+ `forRootAsync` resolves the source through another provider, for example a
246
+ secrets-manager client composed with `process.env`:
247
+
248
+ <!-- Source: test/e2e/fixtures/secrets.provider.ts, secrets.module.ts, config-boot-async.e2e-spec.ts -->
249
+
250
+ ```typescript
251
+ BymaxConfigModule.forRootAsync({
252
+ imports: [SecretsModule],
253
+ useFactory: (secrets: SecretsProvider) => ({
254
+ schema: envSchema,
255
+ source: { ...process.env, ...secrets.asEnvRecord() }
256
+ }),
257
+ inject: [SecretsProvider]
258
+ })
259
+ ```
260
+
261
+ `isGlobal` defaults to `true` and can be set to `false` through the standard
262
+ extras when an application intentionally scopes configuration to a submodule.
263
+
264
+ ### `ConfigService<TConfig>`
265
+
266
+ The typed, injectable accessor over the frozen configuration object.
267
+
268
+ | Method | Signature | Notes |
269
+ | ----------- | ------------------------------------------------------------- | ------------------------------------------------ |
270
+ | `get(path)` | `<P extends Path<TConfig>>(path: P) => PathValue<TConfig, P>` | Dot-path access, type inferred from the schema. |
271
+ | `getAll()` | `() => Readonly<TConfig>` | The deep-frozen root object. |
272
+ | `has(path)` | `(path: Path<TConfig>) => boolean` | True when the resolved value is not `undefined`. |
273
+
274
+ Because validation completed at bootstrap, `get` never throws for a declared
275
+ path: every leaf either passed validation, received its default, or the
276
+ process did not start.
277
+
278
+ ### `BymaxConfigValidationError` and `ConfigIssue`
279
+
280
+ ```typescript
281
+ export class BymaxConfigValidationError extends Error {
282
+ public readonly code: ConfigValidationCode // 'BYMAX_CONFIG_VALIDATION'
283
+ public readonly issues: ReadonlyArray<ConfigIssue>
284
+ }
285
+
286
+ export interface ConfigIssue {
287
+ readonly path: string // e.g. "database.url"
288
+ readonly variable: string // e.g. "DATABASE_URL"
289
+ readonly code: ConfigIssueCode
290
+ readonly message: string // value-free constraint description
291
+ }
292
+ ```
293
+
294
+ `ConfigErrorCode` is the frozen catalog backing `ConfigValidationCode` (the
295
+ error's own `code`) and `ConfigIssueCode` (each issue's `code`); see the
296
+ error catalog below for every value.
297
+
298
+ ### Injection tokens
299
+
300
+ `BYMAX_CONFIG` and `BYMAX_CONFIG_OPTIONS` are module-local `Symbol` tokens,
301
+ exported for advanced factory-style wiring that needs the raw frozen config
302
+ or the resolved options directly instead of going through `ConfigService`:
303
+
304
+ ```typescript
305
+ import { Inject, Injectable } from '@nestjs/common'
306
+ import { BYMAX_CONFIG } from '@bymax-one/nest-config'
307
+ import type { AppConfig } from './config/env.schema'
308
+
309
+ @Injectable()
310
+ export class RawConfigConsumer {
311
+ public constructor(@Inject(BYMAX_CONFIG) private readonly config: Readonly<AppConfig>) {}
312
+ }
313
+ ```
314
+
315
+ ### Type utilities
316
+
317
+ `EnvSchema`, `EnvShape`, `EnvOutput`, `EnvLeaf`, and `EnvNamespace` type the
318
+ two-level schema shape produced by `defineEnv`; `Path<T>` and `PathValue<T, P>`
319
+ are the template-literal utilities behind `ConfigService.get`'s compile-time
320
+ dot-path inference, limited to the two-level namespace convention. All five
321
+ are exported for advanced generic code (custom decorators, typed wrappers)
322
+ that needs to reference the schema shape directly.
323
+
324
+ ## 🚨 Error Catalog
325
+
326
+ | Code | Meaning |
327
+ | -------------------------- | ------------------------------------------------------ |
328
+ | `BYMAX_CONFIG_VALIDATION` | One or more schema violations (top-level error code). |
329
+ | `BYMAX_CONFIG_MISSING` | Required variable absent from the source (issue code). |
330
+ | `BYMAX_CONFIG_INVALID` | Present but failed its constraint (issue code). |
331
+ | `BYMAX_CONFIG_UNKNOWN_KEY` | Strict mode: source variable matches no declared leaf. |
332
+
333
+ ## 🏗️ Architecture
334
+
335
+ ```
336
+ process.env
337
+ (or the record you inject)
338
+ │
339
+ ▼
340
+ ┌───────────────────────────────────────┐
341
+ │ defineEnv │
342
+ │ namespaces at the top, Zod v4 leaves │
343
+ └───────────────────┬───────────────────┘
344
+ │
345
+ ▼
346
+ ┌───────────────────────────────────────┐
347
+ │ createValidatedConfig │
348
+ │ runs ONCE, at bootstrap, and │
349
+ │ collects EVERY failure first │
350
+ └─────────┬─────────────────┬───────────┘
351
+ │ │
352
+ all valid any invalid
353
+ │ │
354
+ ▼ ▼
355
+ deep-frozen BymaxConfigValidationError
356
+ BYMAX_CONFIG one aggregated, value-free report
357
+ │ │
358
+ ▼ ▼
359
+ ConfigService the process does not start
360
+ get('db.host') typed │
361
+ from the schema itself no request is ever served
362
+ │ with a missing secret
363
+ ▼
364
+ ┌─────────────────────────────┐
365
+ │ ./testing │
366
+ │ same pipeline, explicit │
367
+ │ source — a test cannot │
368
+ │ pass because a variable │
369
+ │ happened to be set │
370
+ └─────────────────────────────┘
371
+ ```
372
+
373
+ Validation happens at bootstrap and nowhere else. There is no lazy read, no cache to
374
+ invalidate, and no path where a request-time lookup can discover that an environment
375
+ variable was missing — by then the process would already have refused to start.
376
+
377
+ `./testing` builds the same validated object from an explicit record, so a test that
378
+ needs configuration does not need the application's real environment, and cannot
379
+ accidentally pass because a variable happened to be set on the machine.
380
+
381
+ ### Design Principles
382
+
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: the message carries the expected constraint and the schema's own options, never the received value |
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 |
392
+
393
+ ---
394
+
395
+ ## 🔐 Security Model
396
+
397
+ Configuration is where the secrets are, and a validation report is the one artifact
398
+ guaranteed to be printed, logged, and pasted into an issue. That shapes the whole contract.
399
+
400
+ ### Errors are value-free by contract
401
+
402
+ Every message states the _expected_ constraint — the type, the allowed enum options taken
403
+ from the schema, the bound — and never the received value. `env-validator` and
404
+ `report-formatter` each say so in their own contracts, and property-based tests hold them
405
+ to it.
406
+
407
+ ### Failing fast is the security property
408
+
409
+ A service that starts with a missing `JWT_SECRET` and discovers it at the first login is a
410
+ service that has already accepted traffic it cannot authenticate. The whole schema is
411
+ validated before the application context finishes building, so a misconfigured deployment
412
+ does not serve one request.
413
+
414
+ ### The validated object is frozen
415
+
416
+ It is deep-frozen before it enters the container, so nothing in the running process
417
+ rewrites configuration under a component that already read it.
418
+
419
+ ### Nothing is read from anywhere but the source you name
420
+
421
+ `process.env` by default, or the injectable record you provide. No file, no network, no
422
+ remote provider — there is no path by which configuration arrives from somewhere you did
423
+ not choose.
424
+
425
+ ### Your own messages are yours
426
+
427
+ Value-free applies to this library's reports. If a `.refine` or a custom message in your
428
+ schema includes a value, that message is printed as written.
429
+
430
+ ---
431
+
432
+ ## 🛡️ Security Table
433
+
434
+ | Layer | Implementation |
435
+ | -------------- | -------------------------------------------------------------------------------------------------------------------------- |
436
+ | Error messages | Expected constraint only — type, schema-declared enum options, bounds; never the received value |
437
+ | Failure mode | Refuse to start; the whole schema is validated before the context finishes building |
438
+ | Aggregation | Every failure reported at once, so a fix cycle is one restart rather than one per variable |
439
+ | Immutability | The validated result is frozen before it is provided |
440
+ | Sources | `process.env` only — no file, no network, no remote provider |
441
+ | Type surface | Dot paths checked against the schema at compile time; a typo is a build error, not a runtime `undefined` |
442
+ | Supply chain | `dependencies: {}`; third-party Actions pinned by commit SHA (org-internal reusables by tag); CodeQL and OpenSSF Scorecard |
443
+
444
+ > [!IMPORTANT]
445
+ > **Value-free applies to this library's own reports.** If your schema's `.refine`
446
+ > or a custom message includes a value, that message is printed as written.
447
+
448
+ ---
449
+
450
+ ## 🧱 Tech Stack
451
+
452
+ - **Runtime:** Node.js 24+
453
+ - **Framework:** NestJS 11 (`ConfigurableModuleBuilder`, `@Global()`, `Symbol()` tokens)
454
+ - **Validation:** Zod `^4` (peer) — the schema language, the coercion and the error source
455
+ - **Build:** tsup — ESM + CJS per subpath, with `.d.ts` _and_ `.d.cts` declarations,
456
+ and one shared `./internal` bundle so the module and its testing entry point hold
457
+ a single class identity
458
+ - **Tests:** Jest, including property-based tests over the report formatter + Stryker (mutation)
459
+ - **TypeScript:** 5.x strict (`noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`), zero `any`
460
+
461
+ ---
462
+
463
+ ## 🧪 Testing & Quality
464
+
465
+ Configuration decides whether a process may start at all, so the suite is held to a bar
466
+ beyond "the tests pass".
467
+
468
+ - ✅ **100% line coverage** — statements, branches, functions and lines, enforced as a gate
469
+ - ✅ **95.72% mutation score** — verified with [Stryker](https://stryker-mutator.io/) at
470
+ `break: 95`; the eleven survivors are provable equivalents, documented in the
471
+ [report](./docs/mutation_testing_results.md)
472
+ - ✅ **The value-free guarantee is tested, not assumed** — a sentinel secret is placed in a
473
+ source and the assertion is that it appears nowhere in the rendered report or in the thrown
474
+ error, because a guarantee about what is _absent_ is the one thing an ordinary assertion
475
+ about output does not cover
476
+ - ✅ **Published-artifact gates** — `check:exports` resolves the types the way each module
477
+ system does, `check:runtime` loads every subpath from the packed tarball in ESM and
478
+ CommonJS, and `check:published` compiles this README's snippets against `dist/`
479
+ - ✅ **Zero suppressions** — no coverage or mutation directives in the production source
480
+
481
+ ```bash
482
+ pnpm test # unit suite
483
+ pnpm test:cov # unit suite with the 100% coverage gate
484
+ pnpm mutation # Stryker mutation testing (break: 95)
485
+ pnpm typecheck # tsc strict check
486
+ pnpm lint # ESLint
487
+ ```
488
+
489
+ ### Testing helpers
490
+
491
+ `@bymax-one/nest-config/testing` removes every excuse for touching
492
+ `process.env` in tests. `createTestConfig` synthesizes a complete valid
493
+ source from the schema (defaults where declared, deterministic placeholder
494
+ values elsewhere), applies selective overrides, then runs the exact
495
+ production pipeline (validate, freeze), so a test exercises the same code
496
+ path a running application does.
497
+
498
+ ```typescript
499
+ import { createTestConfig } from '@bymax-one/nest-config/testing'
500
+ import { envSchema } from './config/env.schema'
501
+
502
+ const config = createTestConfig(envSchema, {
503
+ database: { url: 'postgres://localhost:5432/test' }
504
+ })
505
+ ```
506
+
507
+ `configTestingModule` registers the production `BymaxConfigModule` with a
508
+ synthesized source, ready to drop into a Nest `TestingModule` graph:
509
+
510
+ ```typescript
511
+ import { Test } from '@nestjs/testing'
512
+ import { configTestingModule } from '@bymax-one/nest-config/testing'
513
+ import { envSchema } from './config/env.schema'
514
+ import { FeatureProvider } from './feature.provider'
515
+
516
+ const moduleRef = await Test.createTestingModule({
517
+ imports: [configTestingModule(envSchema, { server: { port: 0 } })],
518
+ providers: [FeatureProvider]
519
+ }).compile()
520
+ ```
521
+
522
+ Placeholder synthesis never fabricates values that could accidentally pass a
523
+ secret-strength constraint; length and format constraints are honored. The
524
+ subpath has no Jest dependency and works with any runner, though the family
525
+ standard is Jest.
526
+
527
+ ## 🚫 Known Limitations
528
+
529
+ 1. **Two-level namespace convention.** The path-inference utilities
530
+ (`Path<T>`, `PathValue<T, P>`) target `namespace.leaf` schemas. Deeper
531
+ nesting validates correctly but is not covered by `get()` path inference;
532
+ `getAll()` remains fully typed for arbitrary depth.
533
+ 2. **String-shaped sources only.** The source record is
534
+ `Record<string, string | undefined>` by design, the shape of a process
535
+ environment. Structured sources must be serialized into that shape before
536
+ validation.
537
+ 3. **No `.env` file loading.** Process environment population is the
538
+ platform's job: Node's native `--env-file`, container orchestrators, or CI
539
+ secrets. Bundling a loader would add a dependency and a second
540
+ source-of-truth.
541
+ 4. **No multi-source precedence.** One source record per module
542
+ registration. Precedence between layers (defaults, env, secrets) is the
543
+ caller's composition (`{ ...a, ...b }`), kept explicit on purpose.
544
+
545
+ ## 🤝 Contributing
546
+
547
+ Pull requests are welcome. Please open an issue first for significant changes.
548
+
549
+ - Read [`docs/technical_specification.md`](./docs/technical_specification.md) for architecture decisions.
550
+ - Run the full gate listed in [`CONTRIBUTING.md`](./CONTRIBUTING.md) before opening a PR.
551
+ - Conventional Commits are enforced by `commitlint.config.cjs`.
552
+
553
+ ---
554
+
555
+ ## 🔒 Security Policy
556
+
557
+ If you discover a security vulnerability, please **do not** open a public
558
+ issue. Instead, email us at **support@bymax.one** with `[security]
559
+ @bymax-one/nest-config` in the subject. We take security seriously and will
560
+ respond promptly. See [`SECURITY.md`](./SECURITY.md) for the full policy.
561
+
562
+ ---
563
+
564
+ ## 📄 License
565
+
566
+ [MIT](./LICENSE) © [Bymax One](https://github.com/bymaxone)
567
+
568
+ ---
569
+
570
+ <p align="center">
571
+ <sub>Built with ❤️ by <a href="https://github.com/bymaxone">Bymax One</a></sub>
572
+ </p>
package/dist/index.cjs ADDED
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ var internal = require('@bymax-one/nest-config/internal');
4
+
5
+
6
+
7
+ Object.defineProperty(exports, "BYMAX_CONFIG", {
8
+ enumerable: true,
9
+ get: function () { return internal.BYMAX_CONFIG; }
10
+ });
11
+ Object.defineProperty(exports, "BYMAX_CONFIG_OPTIONS", {
12
+ enumerable: true,
13
+ get: function () { return internal.BYMAX_CONFIG_OPTIONS; }
14
+ });
15
+ Object.defineProperty(exports, "BymaxConfigModule", {
16
+ enumerable: true,
17
+ get: function () { return internal.BymaxConfigModule; }
18
+ });
19
+ Object.defineProperty(exports, "BymaxConfigValidationError", {
20
+ enumerable: true,
21
+ get: function () { return internal.BymaxConfigValidationError; }
22
+ });
23
+ Object.defineProperty(exports, "ConfigErrorCode", {
24
+ enumerable: true,
25
+ get: function () { return internal.ConfigErrorCode; }
26
+ });
27
+ Object.defineProperty(exports, "ConfigService", {
28
+ enumerable: true,
29
+ get: function () { return internal.ConfigService; }
30
+ });
31
+ Object.defineProperty(exports, "defineEnv", {
32
+ enumerable: true,
33
+ get: function () { return internal.defineEnv; }
34
+ });
@@ -0,0 +1 @@
1
+ export { BYMAX_CONFIG, BYMAX_CONFIG_OPTIONS, BymaxConfigModule, BymaxConfigModuleOptions, BymaxConfigValidationError, ConfigErrorCode, ConfigIssue, ConfigIssueCode, ConfigService, ConfigValidationCode, EnvLeaf, EnvNamespace, EnvOutput, EnvSchema, EnvShape, Path, PathValue, defineEnv } from '@bymax-one/nest-config/internal';
@@ -0,0 +1 @@
1
+ export { BYMAX_CONFIG, BYMAX_CONFIG_OPTIONS, BymaxConfigModule, BymaxConfigModuleOptions, BymaxConfigValidationError, ConfigErrorCode, ConfigIssue, ConfigIssueCode, ConfigService, ConfigValidationCode, EnvLeaf, EnvNamespace, EnvOutput, EnvSchema, EnvShape, Path, PathValue, defineEnv } from '@bymax-one/nest-config/internal';
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ export { BYMAX_CONFIG, BYMAX_CONFIG_OPTIONS, BymaxConfigModule, BymaxConfigValidationError, ConfigErrorCode, ConfigService, defineEnv } from '@bymax-one/nest-config/internal';