@bymax-one/nest-config 1.0.0 → 1.0.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,23 @@ as the GitHub Release body, so each released version needs a matching
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.0.1] - 2026-08-04
15
+
16
+ ### Security
17
+
18
+ - `ConfigService` no longer discloses configured values when an instance is serialized.
19
+ The validated root and its flattened lookup moved from TypeScript `private` properties —
20
+ which are erased at runtime, leaving enumerable own properties — to ECMAScript private
21
+ fields. `JSON.stringify`, `Object.entries`, object spread and `util.inspect` (including
22
+ `showHidden`) previously emitted every secret the application declared in plaintext,
23
+ which is what code that renders an injected provider incidentally does: a logger
24
+ formatting its arguments, an error reporter capturing the scope of a throw.
25
+ - `ConfigService.toJSON` reports the declared namespace names and nothing else, so a
26
+ serialized instance stays informative for debugging without carrying values.
27
+
28
+ Reading on purpose is unchanged. `get`, `has` and `getAll` return the same values as
29
+ before; only incidental serialization is affected. No API was removed.
30
+
14
31
  ## [1.0.0] - 2026-08-03
15
32
 
16
33
  First published release. Everything below ships in it.
@@ -103,4 +120,5 @@ have regressed from. They are kept because the reasoning is worth having.
103
120
  ever carried the permissive range. No runtime behaviour changed.
104
121
 
105
122
  [1.0.0]: https://github.com/bymaxone/nest-config/releases/tag/v1.0.0
106
- [Unreleased]: https://github.com/bymaxone/nest-config/compare/v1.0.0...HEAD
123
+ [1.0.1]: https://github.com/bymaxone/nest-config/compare/v1.0.0...v1.0.1
124
+ [Unreleased]: https://github.com/bymaxone/nest-config/compare/v1.0.1...HEAD
package/README.md CHANGED
@@ -404,6 +404,16 @@ from the schema, the bound — and never the received value. `env-validator` and
404
404
  `report-formatter` each say so in their own contracts, and property-based tests hold them
405
405
  to it.
406
406
 
407
+ ### The service does not serialize its configuration
408
+
409
+ `ConfigService` keeps the validated root in an ECMAScript private field, so
410
+ `JSON.stringify`, `Object.entries`, object spread and `util.inspect` cannot reach the
411
+ values. Those are the paths taken by code that renders an injected provider it was handed
412
+ incidentally — a logger formatting its arguments, an error reporter capturing the scope of
413
+ a throw. `toJSON` returns the declared namespace names, so the omission reads as deliberate
414
+ rather than as an empty object. Reading on purpose is unaffected: `get`, `has` and the
415
+ `getAll` escape hatch return the real values.
416
+
407
417
  ### Failing fast is the security property
408
418
 
409
419
  A service that starts with a missing `JWT_SECRET` and discovers it at the first login is a
@@ -437,6 +447,7 @@ schema includes a value, that message is printed as written.
437
447
  | Failure mode | Refuse to start; the whole schema is validated before the context finishes building |
438
448
  | Aggregation | Every failure reported at once, so a fix cycle is one restart rather than one per variable |
439
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 |
440
451
  | Sources | `process.env` only — no file, no network, no remote provider |
441
452
  | Type surface | Dot paths checked against the schema at compile time; a typo is a build error, not a runtime `undefined` |
442
453
  | Supply chain | `dependencies: {}`; third-party Actions pinned by commit SHA (org-internal reusables by tag); CodeQL and OpenSSF Scorecard |
@@ -4,6 +4,9 @@ var common = require('@nestjs/common');
4
4
  var zod = require('zod');
5
5
 
6
6
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __typeError = (msg) => {
8
+ throw TypeError(msg);
9
+ };
7
10
  var __decorateClass = (decorators, target, key, kind) => {
8
11
  var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
9
12
  for (var i = decorators.length - 1, decorator; i >= 0; i--)
@@ -12,6 +15,10 @@ var __decorateClass = (decorators, target, key, kind) => {
12
15
  return result;
13
16
  };
14
17
  var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
18
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
19
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
20
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
21
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), member.set(obj, value), value);
15
22
 
16
23
  // src/config.tokens.ts
17
24
  var BYMAX_CONFIG_OPTIONS = /* @__PURE__ */ Symbol("BYMAX_CONFIG_OPTIONS");
@@ -275,6 +282,7 @@ var bymaxConfigProvider = {
275
282
  useFactory: createValidatedConfig,
276
283
  inject: [BYMAX_CONFIG_OPTIONS]
277
284
  };
285
+ var _config, _leaves;
278
286
  exports.ConfigService = class ConfigService {
279
287
  /**
280
288
  * Bind the accessor to the frozen configuration object.
@@ -286,14 +294,53 @@ exports.ConfigService = class ConfigService {
286
294
  * @param config - The validated, deep-frozen config injected via {@link BYMAX_CONFIG}.
287
295
  */
288
296
  constructor(config) {
289
- this.config = config;
297
+ /**
298
+ * The frozen configuration root.
299
+ *
300
+ * Held in an ECMAScript private field rather than a TypeScript `private`
301
+ * property: `private` is erased at runtime, leaving an enumerable own
302
+ * property that `JSON.stringify`, `Object.entries`, object spread and
303
+ * `util.inspect` all walk into. This object aggregates every secret the
304
+ * application declared, so any code that incidentally serializes the
305
+ * injected service — a logger that renders its arguments, an observability
306
+ * SDK capturing the scope of a thrown error, a stray `console.log` — would
307
+ * emit them in plaintext. A `#` field is unreachable to all of those.
308
+ */
309
+ __privateAdd(this, _config);
310
+ /**
311
+ * Precomputed `namespace.leaf` to value lookup, built once at construction.
312
+ *
313
+ * An ECMAScript private field for the same reason as the config root: it
314
+ * holds the same values, flattened.
315
+ */
316
+ __privateAdd(this, _leaves);
317
+ __privateSet(this, _config, config);
290
318
  const leaves = /* @__PURE__ */ new Map();
291
319
  for (const [namespace, value] of Object.entries(config)) {
292
320
  for (const [leaf, leafValue] of Object.entries(value)) {
293
321
  leaves.set(`${namespace}.${leaf}`, leafValue);
294
322
  }
295
323
  }
296
- this.leaves = leaves;
324
+ __privateSet(this, _leaves, leaves);
325
+ }
326
+ /**
327
+ * Describe the service for serialization without disclosing any value.
328
+ *
329
+ * `JSON.stringify` on an instance would otherwise yield `{}`, which reads as
330
+ * a bug rather than as a deliberate omission. Returning the namespace names
331
+ * keeps the output useful for debugging while the values stay unreachable.
332
+ *
333
+ * @returns The declared namespace names, never their contents.
334
+ * @example
335
+ * ```typescript
336
+ * JSON.stringify(config); // {"service":"ConfigService","namespaces":["server","database"]}
337
+ * ```
338
+ */
339
+ toJSON() {
340
+ return {
341
+ service: "ConfigService",
342
+ namespaces: Object.keys(__privateGet(this, _config))
343
+ };
297
344
  }
298
345
  /**
299
346
  * Read a leaf value by its `namespace.leaf` dot-path.
@@ -328,7 +375,7 @@ exports.ConfigService = class ConfigService {
328
375
  * ```
329
376
  */
330
377
  getAll() {
331
- return this.config;
378
+ return __privateGet(this, _config);
332
379
  }
333
380
  /**
334
381
  * Report whether a declared path resolves to a defined value.
@@ -357,9 +404,11 @@ exports.ConfigService = class ConfigService {
357
404
  * @returns The leaf value as `unknown`, or `undefined` when the leaf is unset.
358
405
  */
359
406
  resolve(path) {
360
- return this.leaves.get(String(path));
407
+ return __privateGet(this, _leaves).get(String(path));
361
408
  }
362
409
  };
410
+ _config = new WeakMap();
411
+ _leaves = new WeakMap();
363
412
  exports.ConfigService = __decorateClass([
364
413
  common.Injectable(),
365
414
  __decorateParam(0, common.Inject(BYMAX_CONFIG))
@@ -351,9 +351,7 @@ declare class BymaxConfigModule extends ConfigurableModuleClass {
351
351
  * ```
352
352
  */
353
353
  declare class ConfigService<TConfig> {
354
- private readonly config;
355
- /** Precomputed `namespace.leaf` to value lookup, built once at construction. */
356
- private readonly leaves;
354
+ #private;
357
355
  /**
358
356
  * Bind the accessor to the frozen configuration object.
359
357
  *
@@ -364,6 +362,23 @@ declare class ConfigService<TConfig> {
364
362
  * @param config - The validated, deep-frozen config injected via {@link BYMAX_CONFIG}.
365
363
  */
366
364
  constructor(config: Readonly<TConfig>);
365
+ /**
366
+ * Describe the service for serialization without disclosing any value.
367
+ *
368
+ * `JSON.stringify` on an instance would otherwise yield `{}`, which reads as
369
+ * a bug rather than as a deliberate omission. Returning the namespace names
370
+ * keeps the output useful for debugging while the values stay unreachable.
371
+ *
372
+ * @returns The declared namespace names, never their contents.
373
+ * @example
374
+ * ```typescript
375
+ * JSON.stringify(config); // {"service":"ConfigService","namespaces":["server","database"]}
376
+ * ```
377
+ */
378
+ toJSON(): {
379
+ readonly service: string;
380
+ readonly namespaces: readonly string[];
381
+ };
367
382
  /**
368
383
  * Read a leaf value by its `namespace.leaf` dot-path.
369
384
  *
@@ -351,9 +351,7 @@ declare class BymaxConfigModule extends ConfigurableModuleClass {
351
351
  * ```
352
352
  */
353
353
  declare class ConfigService<TConfig> {
354
- private readonly config;
355
- /** Precomputed `namespace.leaf` to value lookup, built once at construction. */
356
- private readonly leaves;
354
+ #private;
357
355
  /**
358
356
  * Bind the accessor to the frozen configuration object.
359
357
  *
@@ -364,6 +362,23 @@ declare class ConfigService<TConfig> {
364
362
  * @param config - The validated, deep-frozen config injected via {@link BYMAX_CONFIG}.
365
363
  */
366
364
  constructor(config: Readonly<TConfig>);
365
+ /**
366
+ * Describe the service for serialization without disclosing any value.
367
+ *
368
+ * `JSON.stringify` on an instance would otherwise yield `{}`, which reads as
369
+ * a bug rather than as a deliberate omission. Returning the namespace names
370
+ * keeps the output useful for debugging while the values stay unreachable.
371
+ *
372
+ * @returns The declared namespace names, never their contents.
373
+ * @example
374
+ * ```typescript
375
+ * JSON.stringify(config); // {"service":"ConfigService","namespaces":["server","database"]}
376
+ * ```
377
+ */
378
+ toJSON(): {
379
+ readonly service: string;
380
+ readonly namespaces: readonly string[];
381
+ };
367
382
  /**
368
383
  * Read a leaf value by its `namespace.leaf` dot-path.
369
384
  *
@@ -2,6 +2,9 @@ import { ConfigurableModuleBuilder, Injectable, Inject, Module } from '@nestjs/c
2
2
  import { z } from 'zod';
3
3
 
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __typeError = (msg) => {
6
+ throw TypeError(msg);
7
+ };
5
8
  var __decorateClass = (decorators, target, key, kind) => {
6
9
  var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
7
10
  for (var i = decorators.length - 1, decorator; i >= 0; i--)
@@ -10,6 +13,10 @@ var __decorateClass = (decorators, target, key, kind) => {
10
13
  return result;
11
14
  };
12
15
  var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
16
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
17
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
18
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
19
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), member.set(obj, value), value);
13
20
 
14
21
  // src/config.tokens.ts
15
22
  var BYMAX_CONFIG_OPTIONS = /* @__PURE__ */ Symbol("BYMAX_CONFIG_OPTIONS");
@@ -273,6 +280,7 @@ var bymaxConfigProvider = {
273
280
  useFactory: createValidatedConfig,
274
281
  inject: [BYMAX_CONFIG_OPTIONS]
275
282
  };
283
+ var _config, _leaves;
276
284
  var ConfigService = class {
277
285
  /**
278
286
  * Bind the accessor to the frozen configuration object.
@@ -284,14 +292,53 @@ var ConfigService = class {
284
292
  * @param config - The validated, deep-frozen config injected via {@link BYMAX_CONFIG}.
285
293
  */
286
294
  constructor(config) {
287
- this.config = config;
295
+ /**
296
+ * The frozen configuration root.
297
+ *
298
+ * Held in an ECMAScript private field rather than a TypeScript `private`
299
+ * property: `private` is erased at runtime, leaving an enumerable own
300
+ * property that `JSON.stringify`, `Object.entries`, object spread and
301
+ * `util.inspect` all walk into. This object aggregates every secret the
302
+ * application declared, so any code that incidentally serializes the
303
+ * injected service — a logger that renders its arguments, an observability
304
+ * SDK capturing the scope of a thrown error, a stray `console.log` — would
305
+ * emit them in plaintext. A `#` field is unreachable to all of those.
306
+ */
307
+ __privateAdd(this, _config);
308
+ /**
309
+ * Precomputed `namespace.leaf` to value lookup, built once at construction.
310
+ *
311
+ * An ECMAScript private field for the same reason as the config root: it
312
+ * holds the same values, flattened.
313
+ */
314
+ __privateAdd(this, _leaves);
315
+ __privateSet(this, _config, config);
288
316
  const leaves = /* @__PURE__ */ new Map();
289
317
  for (const [namespace, value] of Object.entries(config)) {
290
318
  for (const [leaf, leafValue] of Object.entries(value)) {
291
319
  leaves.set(`${namespace}.${leaf}`, leafValue);
292
320
  }
293
321
  }
294
- this.leaves = leaves;
322
+ __privateSet(this, _leaves, leaves);
323
+ }
324
+ /**
325
+ * Describe the service for serialization without disclosing any value.
326
+ *
327
+ * `JSON.stringify` on an instance would otherwise yield `{}`, which reads as
328
+ * a bug rather than as a deliberate omission. Returning the namespace names
329
+ * keeps the output useful for debugging while the values stay unreachable.
330
+ *
331
+ * @returns The declared namespace names, never their contents.
332
+ * @example
333
+ * ```typescript
334
+ * JSON.stringify(config); // {"service":"ConfigService","namespaces":["server","database"]}
335
+ * ```
336
+ */
337
+ toJSON() {
338
+ return {
339
+ service: "ConfigService",
340
+ namespaces: Object.keys(__privateGet(this, _config))
341
+ };
295
342
  }
296
343
  /**
297
344
  * Read a leaf value by its `namespace.leaf` dot-path.
@@ -326,7 +373,7 @@ var ConfigService = class {
326
373
  * ```
327
374
  */
328
375
  getAll() {
329
- return this.config;
376
+ return __privateGet(this, _config);
330
377
  }
331
378
  /**
332
379
  * Report whether a declared path resolves to a defined value.
@@ -355,9 +402,11 @@ var ConfigService = class {
355
402
  * @returns The leaf value as `unknown`, or `undefined` when the leaf is unset.
356
403
  */
357
404
  resolve(path) {
358
- return this.leaves.get(String(path));
405
+ return __privateGet(this, _leaves).get(String(path));
359
406
  }
360
407
  };
408
+ _config = new WeakMap();
409
+ _leaves = new WeakMap();
361
410
  ConfigService = __decorateClass([
362
411
  Injectable(),
363
412
  __decorateParam(0, Inject(BYMAX_CONFIG))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-config",
3
- "version": "1.0.0",
3
+ "version": "1.0.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",
@@ -53,6 +53,29 @@
53
53
  },
54
54
  "./package.json": "./package.json"
55
55
  },
56
+ "scripts": {
57
+ "build": "pnpm clean && tsup",
58
+ "check:exports": "attw --pack . --profile strict",
59
+ "check:published": "node scripts/check-published-surface.mjs",
60
+ "check:runtime": "node scripts/check-consumer-runtime.mjs",
61
+ "clean": "node -e \"const fs=require('node:fs');for(const d of ['dist','coverage'])fs.rmSync(d,{recursive:true,force:true})\"",
62
+ "dogfood": "node scripts/dogfood-smoke-test.mjs",
63
+ "lint": "eslint src",
64
+ "lint:fix": "eslint src --fix",
65
+ "mutation": "stryker run",
66
+ "mutation:dry-run": "stryker run --dryRunOnly",
67
+ "mutation:incremental": "stryker run --incremental",
68
+ "prepare": "husky",
69
+ "prepublishOnly": "pnpm typecheck && pnpm lint && pnpm test:cov:all && pnpm build && pnpm size && pnpm check:published",
70
+ "release": "npm publish --provenance --access public",
71
+ "size": "node scripts/check-size.mjs",
72
+ "test": "jest",
73
+ "test:cov": "jest --coverage",
74
+ "test:cov:all": "jest --config jest.coverage.config.ts --coverage",
75
+ "test:e2e": "jest --config jest.e2e.config.ts",
76
+ "test:watch": "jest --watch",
77
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.build.json"
78
+ },
56
79
  "lint-staged": {
57
80
  "*.{ts,tsx,js,mjs,cjs}": [
58
81
  "eslint --fix",
@@ -104,6 +127,17 @@
104
127
  "typescript": "^5.9.3",
105
128
  "zod": "^4.0.0"
106
129
  },
130
+ "pnpm": {
131
+ "overrides": {
132
+ "brace-expansion@1": "^1.1.17",
133
+ "brace-expansion@2": "^2.1.3",
134
+ "brace-expansion@5": "^5.0.8",
135
+ "esbuild": "^0.28.1",
136
+ "fast-uri": "^3.1.4",
137
+ "protobufjs": "^7.6.5",
138
+ "qs": "^6.15.2"
139
+ }
140
+ },
107
141
  "keywords": [
108
142
  "nestjs",
109
143
  "config",
@@ -116,6 +150,7 @@
116
150
  "fail-fast",
117
151
  "configuration"
118
152
  ],
153
+ "packageManager": "pnpm@10.8.1",
119
154
  "engines": {
120
155
  "node": ">=24.0.0"
121
156
  },
@@ -136,26 +171,5 @@
136
171
  "./dist/internal/index.d.cts"
137
172
  ]
138
173
  }
139
- },
140
- "scripts": {
141
- "build": "pnpm clean && tsup",
142
- "check:exports": "attw --pack . --profile strict",
143
- "check:published": "node scripts/check-published-surface.mjs",
144
- "check:runtime": "node scripts/check-consumer-runtime.mjs",
145
- "clean": "node -e \"const fs=require('node:fs');for(const d of ['dist','coverage'])fs.rmSync(d,{recursive:true,force:true})\"",
146
- "dogfood": "node scripts/dogfood-smoke-test.mjs",
147
- "lint": "eslint src",
148
- "lint:fix": "eslint src --fix",
149
- "mutation": "stryker run",
150
- "mutation:dry-run": "stryker run --dryRunOnly",
151
- "mutation:incremental": "stryker run --incremental",
152
- "release": "npm publish --provenance --access public",
153
- "size": "node scripts/check-size.mjs",
154
- "test": "jest",
155
- "test:cov": "jest --coverage",
156
- "test:cov:all": "jest --config jest.coverage.config.ts --coverage",
157
- "test:e2e": "jest --config jest.e2e.config.ts",
158
- "test:watch": "jest --watch",
159
- "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.build.json"
160
174
  }
161
- }
175
+ }