@bymax-one/nest-config 1.1.3 → 1.2.1

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,94 @@ as the GitHub Release body, so each released version needs a matching
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.2.1] - 2026-10-30
15
+
16
+ ### Security
17
+
18
+ - **The `zod` peer range now starts at `4.6.6`, the first release without
19
+ GHSA-mrhx-c7xc-m3w5** (high: memory exhaustion while validating oversized constrained
20
+ collections). The range `^4.0.0` declared installs below it as supported, and the
21
+ advisory applies to them in an application that validates untrusted collection input
22
+ through its own schemas. **Apply to a derived backend:** raise `zod` to `4.6.6` or later
23
+ (`pnpm up zod`); an install below it is outside the declared peer range.
24
+
25
+ ### Documentation
26
+
27
+ - **Support policy: only the latest minor receives security fixes, and the Discussions
28
+ link is gone.** `SECURITY.md` supports the latest minor alone; earlier versions get no fix and no
29
+ backport, and the guidance is to upgrade. The issue-template chooser no longer links to
30
+ Discussions; the security contact by e-mail remains.
31
+
32
+ - **`strict` names the two prefixed-variable sources a search will not find.** Both came
33
+ from an adopter enabling it in a real deployment. A container's own `environment:` block
34
+ assigns a variable whose host-side name is a different one, so a search over
35
+ interpolations misses it — and it fails on the default path, because Compose assigns an
36
+ unset interpolation the empty string rather than absent, and validation skips only
37
+ `undefined`. Kubernetes service links appear in no file at all: at the default
38
+ `enableServiceLinks`, a Service named `redis` supplies `REDIS_PORT`,
39
+ `REDIS_SERVICE_HOST`, `REDIS_SERVICE_PORT` and the `REDIS_PORT_6379_TCP*` family into a
40
+ closed `redis` namespace — a crash loop in production that never appears in CI.
41
+
42
+ The cost of an open namespace is also stated in its sharpest form rather than its
43
+ general one: the typo it cannot catch may be in that namespace's own declared variable,
44
+ so `OTEL_ENABLE` is waived, the leaf falls back to its default, and the feature stays
45
+ off with nothing reported.
46
+
47
+ ## [1.2.0] - 2026-09-04
48
+
49
+ ### Added
50
+
51
+ - **A namespace can declare itself open, so `strict` is usable next to a library that
52
+ reads its own environment.** `strict` claims a declared namespace's entire variable
53
+ prefix: a namespace named `otel` rejects the OpenTelemetry SDK's own
54
+ `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_TRACES_SAMPLER`, and one named `redis` rejects a
55
+ `REDIS_PORT` only a compose file reads. A prefix matching no declared namespace
56
+ (`POSTGRES_*` with no `postgres` namespace) is never inspected, so the failure lands
57
+ hardest exactly where a schema shares a prefix with another program — which is the common
58
+ case. `OTEL_` is an open standard; a consumer cannot rename around it.
59
+
60
+ `z.object({ ... }).meta({ open: true })` on the namespace waives unknown-key detection
61
+ for the remainder of that prefix. The waiver is narrow by construction: declared leaves
62
+ stay bound and validated (an open `otel` still reads and coerces `OTEL_ENABLED`), it
63
+ applies per namespace rather than per schema, and a more specific closed namespace still
64
+ claims its own variables (`otelExporter` beside an open `otel` still reports
65
+ `OTEL_EXPORTER_TYPO`). Only the literal `true` opens a namespace.
66
+
67
+ **The cost is stated rather than designed away:** an open namespace cannot distinguish a
68
+ foreign variable from a misspelled local one, so a typo of `OTEL_ENABLED` passes and the
69
+ leaf falls back to its default. The two cases are indistinguishable by name. Open the
70
+ namespaces whose prefix genuinely belongs to another program, and leave the rest closed.
71
+
72
+ Filtering the foreign names out of `source` is not an equivalent workaround and is worse
73
+ in a way that is easy to miss: removing `OTEL_*` from the source also unbinds the declared
74
+ `OTEL_ENABLED`, whose `.default()` then manufactures a plausible value while the variable
75
+ silently stops being read.
76
+
77
+ ### Security
78
+
79
+ - **Development-only advisories cleared; the published package was never affected.** Eight
80
+ advisories landed against transitive development dependencies across this cycle:
81
+ `browserslist` (GHSA-73wf-gq98-2v4g, GHSA-c83g-rgw3-j3cx, CVSS 7.5 each), `fast-uri`
82
+ (GHSA-5jgf-p345-68v8, GHSA-jqff-g426-hqxp — host confusion; GHSA-f65p-4m7j-42xc,
83
+ GHSA-fph4-wmhf-6fwf — server-side request forgery; CVSS 7.5 each) and `qs`
84
+ (GHSA-4mjr-xmp4-gh2g, GHSA-x5fp-wj9c-mxmx, CVSS 6.3 each). All three are reached through
85
+ the mutation-testing and commit-tooling chains, none is a runtime dependency, and the
86
+ package ships only `dist` as runtime code — so no consumer resolved a vulnerable version
87
+ through it.
88
+
89
+ Recorded here rather than omitted as internal because a consumer reading an advisory feed
90
+ needs to know whether a library in their tree pulled one in, and "it did not" is an answer
91
+ only the maintainer can give. The floors now sit above every affected range and the
92
+ lockfile resolves `browserslist@4.28.8`, `fast-uri@3.1.6` and `qs@6.16.0`.
93
+
94
+ ### Documentation
95
+
96
+ - **`strict` now documents what it claims before you enable it.** The option read as an
97
+ unambiguous improvement and gave no warning that a declared namespace claims its whole
98
+ prefix — the behaviour was measured by a consumer only after enabling it broke a boot on
99
+ five correct variables. The API reference gains a `Strict mode and shared prefixes`
100
+ section covering the claim, the open-namespace opt-out, and its cost.
101
+
14
102
  ## [1.1.3] - 2026-08-18
15
103
 
16
104
  **Documentation.** The runtime bundles are byte-identical to `1.1.2`. Five files differ: the
@@ -320,4 +408,6 @@ have regressed from. They are kept because the reasoning is worth having.
320
408
  [1.1.1]: https://github.com/bymaxone/nest-config/compare/v1.1.0...v1.1.1
321
409
  [1.1.2]: https://github.com/bymaxone/nest-config/compare/v1.1.1...v1.1.2
322
410
  [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
411
+ [1.2.0]: https://github.com/bymaxone/nest-config/compare/v1.1.3...v1.2.0
412
+ [1.2.1]: https://github.com/bymaxone/nest-config/compare/v1.2.0...v1.2.1
413
+ [Unreleased]: https://github.com/bymaxone/nest-config/compare/v1.2.1...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.61%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.62%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>
@@ -125,7 +125,7 @@ pnpm add @bymax-one/nest-config @nestjs/common @nestjs/core reflect-metadata zod
125
125
  | `@nestjs/common` | `^11.0.16` |
126
126
  | `@nestjs/core` | `^11.1.18` |
127
127
  | `reflect-metadata` | `^0.2.0` |
128
- | `zod` | `^4.0.0` |
128
+ | `zod` | `^4.6.6` |
129
129
 
130
130
  ## 🚀 Quick Start
131
131
 
@@ -258,6 +258,20 @@ database: z.object({
258
258
  })
259
259
  ```
260
260
 
261
+ A namespace can declare itself **open**, which waives strict unknown-key
262
+ detection for the rest of its prefix. Use it when the prefix is shared with
263
+ another program that reads its own variables — see
264
+ [Strict mode and shared prefixes](#strict-mode-and-shared-prefixes):
265
+
266
+ ```typescript
267
+ // OTEL_ENABLED is read and validated; every other OTEL_* variable belongs to
268
+ // the OpenTelemetry SDK, which reads it natively.
269
+ otel: z.object({ enabled: z.stringbool().default(false) }).meta({ open: true })
270
+ ```
271
+
272
+ Only the literal `true` opens a namespace. Any other value, and any metadata
273
+ without the key, leaves detection in force.
274
+
261
275
  ### `BymaxConfigModule`
262
276
 
263
277
  | Method | Signature |
@@ -267,12 +281,12 @@ database: z.object({
267
281
 
268
282
  `BymaxConfigModuleOptions`:
269
283
 
270
- | Field | Type | Required | Notes |
271
- | ------------------- | ---------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
272
- | `schema` | `EnvSchema` | yes | Produced by `defineEnv`. |
273
- | `source` | `Record<string, string \| undefined>` | no | Defaults to `process.env` in the provider factory. Injectable for tests. |
274
- | `onValidationError` | `(issues: ReadonlyArray<ConfigIssue>) => void` | no | Observability hook invoked before the fail-fast throw. Cannot suppress the failure. |
275
- | `strict` | `boolean` | no | When true, source variables matching a namespace prefix but no declared leaf raise `BYMAX_CONFIG_UNKNOWN_KEY`. Defaults to `false`. |
284
+ | Field | Type | Required | Notes |
285
+ | ------------------- | ---------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
286
+ | `schema` | `EnvSchema` | yes | Produced by `defineEnv`. |
287
+ | `source` | `Record<string, string \| undefined>` | no | Defaults to `process.env` in the provider factory. Injectable for tests. |
288
+ | `onValidationError` | `(issues: ReadonlyArray<ConfigIssue>) => void` | no | Observability hook invoked before the fail-fast throw. Cannot suppress the failure. |
289
+ | `strict` | `boolean` | no | When true, source variables matching a namespace prefix but no declared leaf raise `BYMAX_CONFIG_UNKNOWN_KEY`. Defaults to `false`. Read [Strict mode and shared prefixes](#strict-mode-and-shared-prefixes) before enabling it. |
276
290
 
277
291
  `forRootAsync` resolves the source through another provider, for example a
278
292
  secrets-manager client composed with `process.env`:
@@ -293,6 +307,102 @@ BymaxConfigModule.forRootAsync({
293
307
  `isGlobal` defaults to `true` and can be set to `false` through the standard
294
308
  extras when an application intentionally scopes configuration to a submodule.
295
309
 
310
+ #### Strict mode and shared prefixes
311
+
312
+ **A declared namespace claims its entire variable prefix.** Under `strict`, any
313
+ source variable starting with that prefix and matching no declared leaf fails
314
+ the boot — including variables this application never reads because _another
315
+ program_ reads them. A namespace named `otel` claims `OTEL_*`, so the
316
+ OpenTelemetry SDK's own `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_TRACES_SAMPLER`
317
+ are rejected; a namespace named `redis` claims `REDIS_*`, so a `REDIS_PORT` that
318
+ only a compose file reads is rejected too. A prefix matching no declared
319
+ namespace at all (`POSTGRES_*` with no `postgres` namespace) is never inspected,
320
+ which is why the failure lands hardest on schemas that share a prefix with a
321
+ library or a tool — the common case, not the edge one.
322
+
323
+ Declare such a namespace **open** to waive the check for the remainder of its
324
+ prefix:
325
+
326
+ ```typescript
327
+ export const envSchema = defineEnv({
328
+ // OTEL_ENABLED is declared, read and validated. Every other OTEL_* variable
329
+ // is the SDK's, and strict leaves it alone.
330
+ otel: z.object({ enabled: z.stringbool().default(false) }).meta({ open: true }),
331
+ database: z.object({ url: z.url() })
332
+ })
333
+ ```
334
+
335
+ What the waiver does and does not do:
336
+
337
+ - **Declared leaves stay bound.** `OTEL_ENABLED` is still read, coerced and
338
+ validated. Opening a namespace waives unknown-key detection only; it never
339
+ weakens the validation the schema does perform.
340
+ - **The waiver is per namespace.** A stray `DATABASE_TYPO` is still reported
341
+ while `otel` is open. One open namespace does not disable strict mode.
342
+ - **The most specific namespace wins.** If a schema declares both `otel` (open)
343
+ and `otelExporter` (closed), `OTEL_EXPORTER_TYPO` belongs to `otelExporter`
344
+ and is still reported.
345
+
346
+ #### Where prefixed variables come from that a sweep will miss
347
+
348
+ Before enabling `strict`, know that reading your own repository does not enumerate
349
+ what reaches `process.env`. Two sources defeat a migration sweep in different ways
350
+ — one because the search you would naturally run is the wrong one, the other
351
+ because there is nothing to search — and both were found by adopters rather than
352
+ by reading manifests.
353
+
354
+ **A container's own `environment:` block.** Compose assigns variables to the
355
+ container with no `${...}` on the host side to give them away:
356
+
357
+ ```yaml
358
+ services:
359
+ app:
360
+ environment:
361
+ REDIS_PASSWORD: ${STACK_REDIS_PASSWORD:-}
362
+ ```
363
+
364
+ `REDIS_PASSWORD` lands in the application's environment. A repository-wide search
365
+ for the prefix does find this line — but the search a migration actually reaches
366
+ for is over interpolations, `${REDIS_`, and that one finds nothing, because the
367
+ interpolated name is the _other_ one. The variable the container receives and the
368
+ variable the host supplies are different names on the same line.
369
+
370
+ This one fails on the default path rather than only when someone opts in, and the
371
+ reason is worth stating: **Compose assigns an unset interpolation the empty string,
372
+ not absent.** Validation skips a variable whose value is `undefined` — an absent
373
+ variable is the schema's business, not strict mode's — and `''` is not `undefined`.
374
+ So the boot fails even when nobody set a password. That is deliberate: a validator
375
+ that ignored empty strings would also ignore a real misconfiguration that sets one.
376
+
377
+ **Kubernetes service links, which appear in no file at all.** With
378
+ `enableServiceLinks` at its default, a pod receives Docker-link-compatible
379
+ variables for every Service in its namespace. A Service named `redis` supplies
380
+ `REDIS_PORT`, `REDIS_SERVICE_HOST`, `REDIS_SERVICE_PORT` and the
381
+ `REDIS_PORT_6379_TCP*` family — all inside a closed `redis` namespace, none
382
+ declared, none written down anywhere you can search. Under `strict` that is a crash
383
+ loop in production over variables the application never reads, and it will not have
384
+ appeared in CI.
385
+
386
+ Set `enableServiceLinks: false` on the pod spec, or open the namespace. Managed
387
+ add-ons injecting names like `REDIS_PUBLIC_URL` or `DATABASE_URL_UNPOOLED` are the
388
+ same family.
389
+
390
+ **The cost, stated plainly:** an open namespace cannot tell a foreign variable
391
+ from a misspelled local one. No design removes this — the two cases are
392
+ indistinguishable by name — so open the namespaces whose prefix genuinely belongs
393
+ to someone else, and leave the rest closed.
394
+
395
+ The sharpest form of that cost is worth seeing before you accept it. The typo can
396
+ be in the open namespace's **own** declared variable: write `OTEL_ENABLE` instead
397
+ of `OTEL_ENABLED` and it is waived like any other unclaimed name, the leaf falls
398
+ back to its default, and the feature you meant to switch on stays off with nothing
399
+ reported. A closed namespace would have named that variable and refused to boot.
400
+
401
+ > Filtering the foreign names out of `source` instead is **not** equivalent, and
402
+ > is worse in a way that is easy to miss: removing `OTEL_*` from the source also
403
+ > unbinds the declared `OTEL_ENABLED`, whose `.default()` then manufactures a
404
+ > plausible value while the variable silently stops being read.
405
+
296
406
  ### `ConfigService<TConfig>`
297
407
 
298
408
  The typed, injectable accessor over the frozen configuration object.
@@ -672,6 +782,10 @@ standard is Jest.
672
782
  4. **No multi-source precedence.** One source record per module
673
783
  registration. Precedence between layers (defaults, env, secrets) is the
674
784
  caller's composition (`{ ...a, ...b }`), kept explicit on purpose.
785
+ 5. **An open namespace cannot detect a typo in its own prefix.** A foreign
786
+ variable and a misspelled local one are indistinguishable by name, so
787
+ `meta({ open: true })` accepts both. See
788
+ [Strict mode and shared prefixes](#strict-mode-and-shared-prefixes).
675
789
 
676
790
  ## 🤝 Contributing
677
791
 
@@ -125,6 +125,9 @@ function readEnvOverride(leaf) {
125
125
  const override = leaf.meta()?.env;
126
126
  return typeof override === "string" && override.length > 0 ? override : void 0;
127
127
  }
128
+ function readOpenFlag(namespace) {
129
+ return namespace.meta()?.open === true;
130
+ }
128
131
  function deriveVariable(namespace, leafKey) {
129
132
  return `${toScreamingSnake(namespace)}_${toScreamingSnake(leafKey)}`;
130
133
  }
@@ -137,9 +140,10 @@ function resolveSourceNames(schema) {
137
140
  );
138
141
  }
139
142
  function resolveNamespacePrefixes(schema) {
140
- return Object.keys(schema.shape).map((namespace) => ({
143
+ return Object.entries(schema.shape).map(([namespace, namespaceSchema]) => ({
141
144
  namespace,
142
- prefix: `${toScreamingSnake(namespace)}_`
145
+ prefix: `${toScreamingSnake(namespace)}_`,
146
+ open: readOpenFlag(namespaceSchema)
143
147
  }));
144
148
  }
145
149
 
@@ -240,6 +244,7 @@ function detectUnknownKeys(prefixes, bindings, source) {
240
244
  if (source.get(key) === void 0) continue;
241
245
  const match = longestMatchingPrefix(prefixes, key);
242
246
  if (match === void 0) continue;
247
+ if (match.open) continue;
243
248
  issues.push({
244
249
  path: match.namespace,
245
250
  variable: key,
@@ -240,6 +240,12 @@ interface BymaxConfigModuleOptions<TSchema extends EnvSchema = EnvSchema> {
240
240
  /**
241
241
  * When true, source variables that match a declared namespace prefix but no
242
242
  * declared leaf produce `BYMAX_CONFIG_UNKNOWN_KEY` issues. Defaults to false.
243
+ *
244
+ * The prefix is claimed whole, so a namespace sharing its prefix with another
245
+ * program (`OTEL_` with an OpenTelemetry SDK, `REDIS_` with a compose file)
246
+ * would reject that program's variables. Declare such a namespace
247
+ * `meta({ open: true })` to waive the check for the rest of its prefix while
248
+ * its own leaves stay bound and validated.
243
249
  */
244
250
  readonly strict?: boolean;
245
251
  }
@@ -562,8 +568,11 @@ declare function createValidatedConfig<TShape extends EnvShape>(options: Omit<By
562
568
  * @fileoverview Deterministic mapping between nested config paths and flat
563
569
  * environment variable names. Each leaf resolves to exactly one variable:
564
570
  * SCREAMING_SNAKE_CASE of the joined path (`database.url` reads `DATABASE_URL`),
565
- * unless the leaf declares a `meta({ env })` override, which wins. This is an
566
- * internal module consumed by the validator; it is not part of the public API.
571
+ * unless the leaf declares a `meta({ env })` override, which wins. It also
572
+ * resolves the shared prefix of each namespace and whether that namespace
573
+ * declared `meta({ open: true })`, the two facts strict unknown-key detection
574
+ * runs on. This is an internal module consumed by the validator; it is not part
575
+ * of the public API.
567
576
  * @layer Utility
568
577
  */
569
578
 
@@ -240,6 +240,12 @@ interface BymaxConfigModuleOptions<TSchema extends EnvSchema = EnvSchema> {
240
240
  /**
241
241
  * When true, source variables that match a declared namespace prefix but no
242
242
  * declared leaf produce `BYMAX_CONFIG_UNKNOWN_KEY` issues. Defaults to false.
243
+ *
244
+ * The prefix is claimed whole, so a namespace sharing its prefix with another
245
+ * program (`OTEL_` with an OpenTelemetry SDK, `REDIS_` with a compose file)
246
+ * would reject that program's variables. Declare such a namespace
247
+ * `meta({ open: true })` to waive the check for the rest of its prefix while
248
+ * its own leaves stay bound and validated.
243
249
  */
244
250
  readonly strict?: boolean;
245
251
  }
@@ -562,8 +568,11 @@ declare function createValidatedConfig<TShape extends EnvShape>(options: Omit<By
562
568
  * @fileoverview Deterministic mapping between nested config paths and flat
563
569
  * environment variable names. Each leaf resolves to exactly one variable:
564
570
  * SCREAMING_SNAKE_CASE of the joined path (`database.url` reads `DATABASE_URL`),
565
- * unless the leaf declares a `meta({ env })` override, which wins. This is an
566
- * internal module consumed by the validator; it is not part of the public API.
571
+ * unless the leaf declares a `meta({ env })` override, which wins. It also
572
+ * resolves the shared prefix of each namespace and whether that namespace
573
+ * declared `meta({ open: true })`, the two facts strict unknown-key detection
574
+ * runs on. This is an internal module consumed by the validator; it is not part
575
+ * of the public API.
567
576
  * @layer Utility
568
577
  */
569
578
 
@@ -123,6 +123,9 @@ function readEnvOverride(leaf) {
123
123
  const override = leaf.meta()?.env;
124
124
  return typeof override === "string" && override.length > 0 ? override : void 0;
125
125
  }
126
+ function readOpenFlag(namespace) {
127
+ return namespace.meta()?.open === true;
128
+ }
126
129
  function deriveVariable(namespace, leafKey) {
127
130
  return `${toScreamingSnake(namespace)}_${toScreamingSnake(leafKey)}`;
128
131
  }
@@ -135,9 +138,10 @@ function resolveSourceNames(schema) {
135
138
  );
136
139
  }
137
140
  function resolveNamespacePrefixes(schema) {
138
- return Object.keys(schema.shape).map((namespace) => ({
141
+ return Object.entries(schema.shape).map(([namespace, namespaceSchema]) => ({
139
142
  namespace,
140
- prefix: `${toScreamingSnake(namespace)}_`
143
+ prefix: `${toScreamingSnake(namespace)}_`,
144
+ open: readOpenFlag(namespaceSchema)
141
145
  }));
142
146
  }
143
147
 
@@ -238,6 +242,7 @@ function detectUnknownKeys(prefixes, bindings, source) {
238
242
  if (source.get(key) === void 0) continue;
239
243
  const match = longestMatchingPrefix(prefixes, key);
240
244
  if (match === void 0) continue;
245
+ if (match.open) continue;
241
246
  issues.push({
242
247
  path: match.namespace,
243
248
  variable: key,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-config",
3
- "version": "1.1.3",
3
+ "version": "1.2.1",
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",
@@ -56,16 +56,17 @@
56
56
  "scripts": {
57
57
  "build": "pnpm clean && tsup",
58
58
  "check:exports": "attw --pack . --profile strict",
59
+ "check:lint-graph": "node scripts/check-lint-graph-rules.mjs",
59
60
  "check:mutants": "node scripts/check-mutation-directives.mjs",
60
61
  "check:published": "node scripts/check-published-surface.mjs",
61
62
  "check:runtime": "node scripts/check-consumer-runtime.mjs",
62
63
  "clean": "node -e \"const fs=require('node:fs');for(const d of ['dist','coverage'])fs.rmSync(d,{recursive:true,force:true})\"",
63
64
  "dogfood": "node scripts/dogfood-smoke-test.mjs",
64
- "lint": "eslint src",
65
- "lint:fix": "eslint src --fix",
65
+ "lint": "eslint src scripts",
66
+ "lint:fix": "eslint src scripts --fix",
66
67
  "mutation": "stryker run",
67
- "mutation:full": "node -e \"require('node:fs').rmSync('reports/stryker-incremental.json',{force:true,recursive:true})\" && stryker run",
68
68
  "mutation:dry-run": "stryker run --dryRunOnly",
69
+ "mutation:full": "node -e \"require('node:fs').rmSync('reports/stryker-incremental.json',{force:true,recursive:true})\" && stryker run",
69
70
  "prepare": "husky",
70
71
  "prepublishOnly": "pnpm typecheck && pnpm lint && pnpm check:mutants && pnpm test:cov:all && pnpm build && pnpm size && pnpm check:published",
71
72
  "release": "npm publish --provenance --access public",
@@ -86,18 +87,17 @@
86
87
  "prettier --write"
87
88
  ]
88
89
  },
89
- "dependencies": {},
90
90
  "peerDependencies": {
91
91
  "@nestjs/common": "^11.0.16",
92
92
  "@nestjs/core": "^11.1.18",
93
93
  "reflect-metadata": "^0.2.0",
94
- "zod": "^4.0.0"
94
+ "zod": "^4.6.6"
95
95
  },
96
96
  "devDependencies": {
97
97
  "@arethetypeswrong/cli": "^0.18.2",
98
98
  "@commitlint/cli": "^21.0.2",
99
99
  "@commitlint/config-conventional": "^21.0.2",
100
- "@eslint/js": "^9.39.4",
100
+ "@eslint/js": "^10.0.1",
101
101
  "@nestjs/common": "^11.1.20",
102
102
  "@nestjs/core": "^11.1.20",
103
103
  "@nestjs/platform-express": "^11.1.20",
@@ -110,10 +110,10 @@
110
110
  "@types/node": "^25",
111
111
  "@typescript-eslint/eslint-plugin": "^8.59.3",
112
112
  "@typescript-eslint/parser": "^8.59.3",
113
- "eslint": "^9.39.4",
113
+ "eslint": "^10.9.1",
114
114
  "eslint-config-prettier": "^10.1.8",
115
- "eslint-import-resolver-typescript": "^4.4.4",
116
- "eslint-plugin-import": "^2.32.0",
115
+ "eslint-import-resolver-typescript": "^4.4.5",
116
+ "eslint-plugin-import-x": "^4.17.1",
117
117
  "eslint-plugin-prettier": "^5.5.5",
118
118
  "eslint-plugin-security": "^4.0.0",
119
119
  "globals": "^17.6.0",
@@ -126,7 +126,7 @@
126
126
  "ts-node": "^10.9.2",
127
127
  "tsup": "^8.5.1",
128
128
  "typescript": "^5.9.3",
129
- "zod": "^4.0.0"
129
+ "zod": "^4.6.7"
130
130
  },
131
131
  "keywords": [
132
132
  "nestjs",