@pawells/config 2.1.7 → 2.3.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/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ [Unreleased]: https://github.com/PhillipAWells/common/compare/v2.3.0...HEAD
11
+
12
+ ## [2.3.0] - 2026-06-01
13
+
14
+ ### Added
15
+
16
+ - **Secret** — `Secret<T>` wrapper type for marking config values as sensitive; `IsSecret()` type guard for runtime detection; `GetSecretKeys()` for retrieving all secret field names from a config object; `Redact()` for producing a copy of a config with all secret values masked
17
+ - **ConfigManager.GenerateEnv** — new method for serializing a registered config to `.env` file format, with options to target specific keys and to unmask secret values
18
+
19
+ [2.3.0]: https://github.com/PhillipAWells/common/releases/tag/v2.3.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Phillip Aaron Wells
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/errors.d.ts CHANGED
@@ -1,10 +1,8 @@
1
1
  /**
2
2
  * Custom error classes for configuration management.
3
3
  *
4
- * @throws {ConfigurationAlreadyRegisteredError} When a configuration key is registered twice
5
- * @throws {ConfigurationNotRegisteredError} When accessing an unregistered configuration key
6
- * @throws {ConfigurationError} When configuration validation fails
7
- * @throws {ConfigurationNotSetError} When a required configuration value is not set
4
+ * Exports: {@link ConfigurationAlreadyRegisteredError}, {@link ConfigurationNotRegisteredError},
5
+ * {@link ConfigurationError}, and {@link ConfigurationNotSetError}.
8
6
  */
9
7
  /**
10
8
  * Abstract base class for configuration errors.
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;GAGG;AACH,uBAAe,sBAAuB,SAAQ,KAAK;IAClD,kBAAyB,IAAI,EAAE,MAAM,CAAC;gBAE1B,OAAO,EAAE,MAAM;CAI3B;AAED;;;;;;;GAOG;AACH,qBAAa,mCAAoC,SAAQ,sBAAsB;IAC9E,SAAgB,IAAI,sCAAsC;gBAE9C,GAAG,EAAE,MAAM;CAGvB;AAED;;;;;;;GAOG;AACH,qBAAa,+BAAgC,SAAQ,sBAAsB;IAC1E,SAAgB,IAAI,kCAAkC;gBAE1C,GAAG,EAAE,MAAM;CAGvB;AAED;;;;;;;;;GASG;AACH,qBAAa,kBAAmB,SAAQ,sBAAsB;IAC7D,SAAgB,IAAI,yBAAyB;gBAEjC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE;CAMrE;AAED;;;;;;;GAOG;AACH,qBAAa,wBAAyB,SAAQ,sBAAsB;IACnE,SAAgB,IAAI,2BAA2B;gBAEnC,GAAG,EAAE,MAAM;CAGvB"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;GAGG;AACH,uBAAe,sBAAuB,SAAQ,KAAK;IAClD,kBAAyB,IAAI,EAAE,MAAM,CAAC;gBAE1B,OAAO,EAAE,MAAM;CAI3B;AAED;;;;;;;GAOG;AACH,qBAAa,mCAAoC,SAAQ,sBAAsB;IAC9E,SAAgB,IAAI,sCAAsC;gBAE9C,GAAG,EAAE,MAAM;CAGvB;AAED;;;;;;;GAOG;AACH,qBAAa,+BAAgC,SAAQ,sBAAsB;IAC1E,SAAgB,IAAI,kCAAkC;gBAE1C,GAAG,EAAE,MAAM;CAGvB;AAED;;;;;;;;;GASG;AACH,qBAAa,kBAAmB,SAAQ,sBAAsB;IAC7D,SAAgB,IAAI,yBAAyB;gBAEjC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE;CAMrE;AAED;;;;;;;GAOG;AACH,qBAAa,wBAAyB,SAAQ,sBAAsB;IACnE,SAAgB,IAAI,2BAA2B;gBAEnC,GAAG,EAAE,MAAM;CAGvB"}
package/dist/errors.js CHANGED
@@ -1,10 +1,8 @@
1
1
  /**
2
2
  * Custom error classes for configuration management.
3
3
  *
4
- * @throws {ConfigurationAlreadyRegisteredError} When a configuration key is registered twice
5
- * @throws {ConfigurationNotRegisteredError} When accessing an unregistered configuration key
6
- * @throws {ConfigurationError} When configuration validation fails
7
- * @throws {ConfigurationNotSetError} When a required configuration value is not set
4
+ * Exports: {@link ConfigurationAlreadyRegisteredError}, {@link ConfigurationNotRegisteredError},
5
+ * {@link ConfigurationError}, and {@link ConfigurationNotSetError}.
8
6
  */
9
7
  /**
10
8
  * Abstract base class for configuration errors.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './errors.js';
2
2
  export * from './manager.js';
3
3
  export * from './schema.factory.js';
4
+ export { Secret } from './secret.js';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './errors.js';
2
2
  export * from './manager.js';
3
3
  export * from './schema.factory.js';
4
+ export { Secret } from './secret.js';
package/dist/manager.d.ts CHANGED
@@ -1,8 +1,25 @@
1
1
  import { z } from 'zod/v4';
2
+ /**
3
+ * Zod schema for all supported configuration value types.
4
+ * Accepts string, number, boolean, Date, string[], number[], boolean[], and undefined — nullable and optional.
5
+ */
2
6
  export declare const CONFIG_VALUES_TYPES_SCHEMA: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodDate, z.ZodArray<z.ZodString>, z.ZodArray<z.ZodNumber>, z.ZodArray<z.ZodBoolean>, z.ZodUndefined]>>>;
7
+ /**
8
+ * Asserts that a value conforms to the supported configuration value types.
9
+ *
10
+ * @param value - The value to assert
11
+ * @throws {ZodError} If the value does not match any supported type
12
+ * @example
13
+ * AssertConfigValueType('hello'); // passes
14
+ * AssertConfigValueType(42); // passes
15
+ * AssertConfigValueType({}); // throws ZodError
16
+ */
3
17
  export declare function AssertConfigValueType(value: unknown): asserts value is TConfigValueTypes;
18
+ /** Union of all supported configuration value types. */
4
19
  export type TConfigValueTypes = z.infer<typeof CONFIG_VALUES_TYPES_SCHEMA>;
20
+ /** Map of configuration keys to their typed values. */
5
21
  export type TConfig = Map<string, TConfigValueTypes>;
22
+ /** Identifies whether a configuration value comes from the registered default or a runtime override. */
6
23
  export type TConfigSource = 'DEFAULT' | 'OVERRIDE';
7
24
  /**
8
25
  * Runtime configuration manager with Zod schema validation.
@@ -31,14 +48,15 @@ export declare class ConfigManager {
31
48
  * @param defaultValue - Initial value for the configuration key, must satisfy the schema
32
49
  * @throws {ConfigurationAlreadyRegisteredError} If key is already registered
33
50
  * @example
34
- * manager.register('PORT', z.coerce.number().positive());
35
- * manager.register('JWT_SECRET', z.string().min(32));
51
+ * ConfigManager.Register('PORT', z.coerce.number().positive(), 3000);
52
+ * ConfigManager.Register('JWT_SECRET', z.string().min(32), 'default-secret');
36
53
  */
37
54
  static Register(key: string, schema: z.ZodType<TConfigValueTypes>, defaultValue: unknown): void;
38
55
  /**
39
56
  * Set a configuration value and validate against its schema.
40
57
  * @param key - Configuration key
41
58
  * @param value - Value to set and validate
59
+ * @param target - Whether to set the default store or the override store; defaults to `'OVERRIDE'`
42
60
  * @throws {ConfigurationNotRegisteredError} If schema is not registered for key
43
61
  * @throws {ConfigurationError} If validation fails
44
62
  * @example
@@ -50,6 +68,7 @@ export declare class ConfigManager {
50
68
  * Retrieve a configuration value by key.
51
69
  * Returns the value parsed by its registered schema.
52
70
  * @param key - Configuration key
71
+ * @param source - Optional — filter to a specific store (`'DEFAULT'` or `'OVERRIDE'`); omit to return the resolved value (overrides take precedence over defaults)
53
72
  * @returns The typed configuration value
54
73
  * @throws {ConfigurationNotSetError} If value was not set
55
74
  * @throws {ConfigurationNotRegisteredError} If schema is not registered
@@ -69,5 +88,40 @@ export declare class ConfigManager {
69
88
  * const parsed = schema.safeParse(value);
70
89
  */
71
90
  static GetSchema(key: string): z.ZodTypeAny;
91
+ /**
92
+ * Generates a `.env` file string from all currently registered configuration keys.
93
+ *
94
+ * In template mode (default), each key is emitted with its registered default value.
95
+ * Secret fields (marked with `Secret()`) are always emitted with a blank value in template
96
+ * mode, regardless of their registered default.
97
+ * In current-values mode, the live resolved value from `Get(key)` is used; keys that are
98
+ * unset or produce a configuration error are emitted as commented-out blank lines (`# KEY=`).
99
+ *
100
+ * If a field has a Zod `.describe()` annotation, it is emitted as a `# comment` line
101
+ * immediately before the key–value pair.
102
+ *
103
+ * @param options - Optional configuration for generation behavior
104
+ * @param options.useCurrentValues - When `true`, emit current live values from `Get(key)`.
105
+ * Defaults to `false` (template mode using registered defaults).
106
+ * @param options.path - When provided, write the generated string to this file path in UTF-8.
107
+ * @returns - The generated `.env` content as a string.
108
+ *
109
+ * @example
110
+ * ```typescript
111
+ * // Template mode — defaults shown, secrets blank
112
+ * const template = ConfigManager.GenerateEnv();
113
+ * // "APP_HOST=localhost\nAPP_PORT=3000\nAPP_SECRET_KEY="
114
+ * ```
115
+ *
116
+ * @example
117
+ * ```typescript
118
+ * // Save current runtime settings to a file
119
+ * ConfigManager.GenerateEnv({ useCurrentValues: true, path: '.env.snapshot' });
120
+ * ```
121
+ */
122
+ static GenerateEnv(options?: {
123
+ useCurrentValues?: boolean;
124
+ path?: string;
125
+ }): string;
72
126
  }
73
127
  //# sourceMappingURL=manager.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../src/manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AAG3B,eAAO,MAAM,0BAA0B,oMASf,CAAC;AACzB,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,iBAAiB,CAExF;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAC3E,MAAM,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;AACrD,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,UAAU,CAAC;AAEnD;;;;;;;;GAQG;AACH,qBAAa,aAAa;IACzB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAwC;IAGxE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAsB;IAG3D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAsB;IAG5D,OAAO,CAAC,MAAM,KAAK,KAAK,GAMvB;IAED;;;OAGG;WACW,KAAK,IAAI,IAAI;IAM3B,OAAO;IAEP;;;;;;;;;OASG;WACW,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAAC,EAAE,YAAY,EAAE,OAAO,GAAG,IAAI;IAUtG;;;;;;;;;OASG;WACW,GAAG,CAAC,CAAC,SAAS,iBAAiB,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,GAAE,aAA0B,GAAG,IAAI;IAkB/G;;;;;;;;;;;OAWG;WACW,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,iBAAiB;IAgBzE;;;;;;;;OAQG;WACW,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,CAAC,UAAU;CAKlD"}
1
+ {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../src/manager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AAI3B;;;GAGG;AACH,eAAO,MAAM,0BAA0B,oMASf,CAAC;AAEzB;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,iBAAiB,CAExF;AAED,wDAAwD;AACxD,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAE3E,uDAAuD;AACvD,MAAM,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;AAErD,wGAAwG;AACxG,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,UAAU,CAAC;AA8CnD;;;;;;;;GAQG;AACH,qBAAa,aAAa;IACzB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAwC;IAGxE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAsB;IAG3D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAsB;IAG5D,OAAO,CAAC,MAAM,KAAK,KAAK,GAMvB;IAED;;;OAGG;WACW,KAAK,IAAI,IAAI;IAM3B,OAAO;IAEP;;;;;;;;;OASG;WACW,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAAC,EAAE,YAAY,EAAE,OAAO,GAAG,IAAI;IAUtG;;;;;;;;;;OAUG;WACW,GAAG,CAAC,CAAC,SAAS,iBAAiB,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,GAAE,aAA0B,GAAG,IAAI;IAkB/G;;;;;;;;;;;;OAYG;WACW,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,iBAAiB;IAgBzE;;;;;;;;OAQG;WACW,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,CAAC,UAAU;IAMlD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8BG;WACW,WAAW,CAAC,OAAO,CAAC,EAAE;QAAE,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM;CA6C1F"}
package/dist/manager.js CHANGED
@@ -1,5 +1,11 @@
1
+ import { writeFileSync } from 'node:fs';
1
2
  import { z } from 'zod/v4';
2
3
  import { ConfigurationAlreadyRegisteredError, ConfigurationNotRegisteredError, ConfigurationError, ConfigurationNotSetError } from './errors.js';
4
+ import { IsMarkedSecret } from './secret.js';
5
+ /**
6
+ * Zod schema for all supported configuration value types.
7
+ * Accepts string, number, boolean, Date, string[], number[], boolean[], and undefined — nullable and optional.
8
+ */
3
9
  export const CONFIG_VALUES_TYPES_SCHEMA = z.union([
4
10
  z.string(),
5
11
  z.number(),
@@ -10,9 +16,60 @@ export const CONFIG_VALUES_TYPES_SCHEMA = z.union([
10
16
  z.array(z.boolean()),
11
17
  z.undefined(),
12
18
  ]).nullable().optional();
19
+ /**
20
+ * Asserts that a value conforms to the supported configuration value types.
21
+ *
22
+ * @param value - The value to assert
23
+ * @throws {ZodError} If the value does not match any supported type
24
+ * @example
25
+ * AssertConfigValueType('hello'); // passes
26
+ * AssertConfigValueType(42); // passes
27
+ * AssertConfigValueType({}); // throws ZodError
28
+ */
13
29
  export function AssertConfigValueType(value) {
14
30
  CONFIG_VALUES_TYPES_SCHEMA.parse(value);
15
31
  }
32
+ /**
33
+ * Traverses the Zod schema's innerType chain to find a description in globalRegistry.
34
+ *
35
+ * @param schema - Zod schema to inspect
36
+ * @returns - The description string if found, otherwise undefined
37
+ * @internal
38
+ */
39
+ function GetFieldDescription(schema) {
40
+ let current = schema;
41
+ while (current != null) {
42
+ try {
43
+ const meta = z.globalRegistry.get(current);
44
+ if (typeof meta?.description === 'string') {
45
+ return meta.description;
46
+ }
47
+ }
48
+ catch {
49
+ break;
50
+ }
51
+ current = current._def?.innerType;
52
+ }
53
+ return undefined;
54
+ }
55
+ /**
56
+ * Serializes a configuration value to its .env string representation.
57
+ * Arrays are JSON-stringified; Dates use ISO 8601; all others use String().
58
+ * Returns '' for null or undefined.
59
+ *
60
+ * @param value - The configuration value to serialize
61
+ * @returns - Serialized string representation
62
+ * @internal
63
+ */
64
+ function SerializeConfigValue(value) {
65
+ if (value === null || value === undefined)
66
+ return '';
67
+ if (Array.isArray(value))
68
+ return JSON.stringify(value);
69
+ if (value instanceof Date)
70
+ return value.toISOString();
71
+ return String(value);
72
+ }
16
73
  /**
17
74
  * Runtime configuration manager with Zod schema validation.
18
75
  * Provides a singleton instance to register and retrieve typed configuration values.
@@ -53,8 +110,8 @@ export class ConfigManager {
53
110
  * @param defaultValue - Initial value for the configuration key, must satisfy the schema
54
111
  * @throws {ConfigurationAlreadyRegisteredError} If key is already registered
55
112
  * @example
56
- * manager.register('PORT', z.coerce.number().positive());
57
- * manager.register('JWT_SECRET', z.string().min(32));
113
+ * ConfigManager.Register('PORT', z.coerce.number().positive(), 3000);
114
+ * ConfigManager.Register('JWT_SECRET', z.string().min(32), 'default-secret');
58
115
  */
59
116
  static Register(key, schema, defaultValue) {
60
117
  // Ensure key is unique, but it's fine when the schemas match.
@@ -71,6 +128,7 @@ export class ConfigManager {
71
128
  * Set a configuration value and validate against its schema.
72
129
  * @param key - Configuration key
73
130
  * @param value - Value to set and validate
131
+ * @param target - Whether to set the default store or the override store; defaults to `'OVERRIDE'`
74
132
  * @throws {ConfigurationNotRegisteredError} If schema is not registered for key
75
133
  * @throws {ConfigurationError} If validation fails
76
134
  * @example
@@ -99,6 +157,7 @@ export class ConfigManager {
99
157
  * Retrieve a configuration value by key.
100
158
  * Returns the value parsed by its registered schema.
101
159
  * @param key - Configuration key
160
+ * @param source - Optional — filter to a specific store (`'DEFAULT'` or `'OVERRIDE'`); omit to return the resolved value (overrides take precedence over defaults)
102
161
  * @returns The typed configuration value
103
162
  * @throws {ConfigurationNotSetError} If value was not set
104
163
  * @throws {ConfigurationNotRegisteredError} If schema is not registered
@@ -138,4 +197,76 @@ export class ConfigManager {
138
197
  throw new ConfigurationNotRegisteredError(key);
139
198
  return schema;
140
199
  }
200
+ /**
201
+ * Generates a `.env` file string from all currently registered configuration keys.
202
+ *
203
+ * In template mode (default), each key is emitted with its registered default value.
204
+ * Secret fields (marked with `Secret()`) are always emitted with a blank value in template
205
+ * mode, regardless of their registered default.
206
+ * In current-values mode, the live resolved value from `Get(key)` is used; keys that are
207
+ * unset or produce a configuration error are emitted as commented-out blank lines (`# KEY=`).
208
+ *
209
+ * If a field has a Zod `.describe()` annotation, it is emitted as a `# comment` line
210
+ * immediately before the key–value pair.
211
+ *
212
+ * @param options - Optional configuration for generation behavior
213
+ * @param options.useCurrentValues - When `true`, emit current live values from `Get(key)`.
214
+ * Defaults to `false` (template mode using registered defaults).
215
+ * @param options.path - When provided, write the generated string to this file path in UTF-8.
216
+ * @returns - The generated `.env` content as a string.
217
+ *
218
+ * @example
219
+ * ```typescript
220
+ * // Template mode — defaults shown, secrets blank
221
+ * const template = ConfigManager.GenerateEnv();
222
+ * // "APP_HOST=localhost\nAPP_PORT=3000\nAPP_SECRET_KEY="
223
+ * ```
224
+ *
225
+ * @example
226
+ * ```typescript
227
+ * // Save current runtime settings to a file
228
+ * ConfigManager.GenerateEnv({ useCurrentValues: true, path: '.env.snapshot' });
229
+ * ```
230
+ */
231
+ static GenerateEnv(options) {
232
+ const useCurrentValues = options?.useCurrentValues ?? false;
233
+ const lines = [];
234
+ for (const [key, schema] of this._Schemas) {
235
+ const description = GetFieldDescription(schema);
236
+ const isSecret = IsMarkedSecret(schema);
237
+ if (description != null) {
238
+ lines.push(`# ${description}`);
239
+ }
240
+ if (useCurrentValues) {
241
+ try {
242
+ const value = this.Get(key);
243
+ lines.push(`${key}=${SerializeConfigValue(value)}`);
244
+ }
245
+ catch (e) {
246
+ if (e instanceof ConfigurationNotSetError ||
247
+ e instanceof ConfigurationNotRegisteredError ||
248
+ e instanceof ConfigurationError) {
249
+ lines.push(`# ${key}=`);
250
+ }
251
+ else {
252
+ throw e;
253
+ }
254
+ }
255
+ }
256
+ else {
257
+ if (isSecret) {
258
+ lines.push(`${key}=`);
259
+ }
260
+ else {
261
+ const defaultValue = this._DataDefaults.get(key);
262
+ lines.push(`${key}=${SerializeConfigValue(defaultValue)}`);
263
+ }
264
+ }
265
+ }
266
+ const result = lines.join('\n');
267
+ if (options?.path != null) {
268
+ writeFileSync(options.path, result, 'utf8');
269
+ }
270
+ return result;
271
+ }
141
272
  }
@@ -56,11 +56,64 @@ export interface IConfigSchemaObject<TConfig extends Record<string, unknown>, TK
56
56
  * Parse environment variables matching the prefix and set them as overrides.
57
57
  * Logs warnings for invalid values but continues processing remaining vars.
58
58
  *
59
+ * @remarks
60
+ * The concrete implementation also accepts an optional `throwOnError` boolean parameter
61
+ * (defaulting to `false`) that is not exposed in this interface.
62
+ *
59
63
  * @example
60
64
  * MongoDBConfig.ParseENV();
61
65
  * // Reads MONGODB_HOST, MONGODB_PORT, etc. from process.env
62
66
  */
63
67
  ParseENV(): void;
68
+ /**
69
+ * Returns whether the given key was marked as a secret field using Secret().
70
+ *
71
+ * @param key - The configuration key to check
72
+ * @returns true if the key was marked with Secret() at schema construction time
73
+ */
74
+ IsSecret(key: TKeys): boolean;
75
+ /**
76
+ * Returns an array of all keys that were marked as secret using Secret(),
77
+ * in schema shape insertion order.
78
+ *
79
+ * @returns Array of secret key names
80
+ */
81
+ GetSecretKeys(): Array<TKeys>;
82
+ /**
83
+ * Returns a snapshot of all currently resolved config values where secret
84
+ * fields are replaced with '***'. Keys that have not been registered yet
85
+ * are omitted from the result.
86
+ *
87
+ * @returns Record of config values with secrets redacted
88
+ */
89
+ Redact(): Record<string, unknown>;
64
90
  }
91
+ /**
92
+ * Creates a configuration schema object from a Zod schema and prefix.
93
+ *
94
+ * Generic factory that eliminates duplication between concrete config classes.
95
+ * Returns an object with Register, Get, Set, Validate, and ParseENV methods
96
+ * that manage config values for a specific namespace.
97
+ *
98
+ * @param schema - Zod schema defining the config shape and validation rules
99
+ * @param prefix - Prefix for environment variable names (e.g., 'MONGODB_')
100
+ * @returns ConfigSchemaObject with static-like methods for the config namespace
101
+ * @template TSchema - The Zod schema shape
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * const MONGODB_SCHEMA = z.object({
106
+ * HOST: z.string().default('localhost'),
107
+ * PORT: z.number().default(27017),
108
+ * });
109
+ *
110
+ * export const MongoDBConfig = createConfigSchema(MONGODB_SCHEMA, 'MONGODB_');
111
+ * // Usage:
112
+ * // MongoDBConfig.Register();
113
+ * // const host = MongoDBConfig.Get('HOST');
114
+ * // MongoDBConfig.Set('PORT', 27018);
115
+ * // MongoDBConfig.ParseENV();
116
+ * ```
117
+ */
65
118
  export declare function CreateConfigSchema<TSchema extends z.ZodRawShape>(schema: z.ZodObject<TSchema>, prefix: string): IConfigSchemaObject<z.infer<typeof schema>>;
66
119
  //# sourceMappingURL=schema.factory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.factory.d.ts","sourceRoot":"","sources":["../src/schema.factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AA0BjE;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB,CACnC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,KAAK,SAAS,MAAM,OAAO,GAAG,MAAM,OAAO;IAE3C;;;OAGG;IACH,QAAQ,IAAI,IAAI,CAAC;IAEjB;;;;;;;;;;OAUG;IACH,GAAG,CAAC,CAAC,SAAS,KAAK,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAEzC;;;;;;;;;;;OAWG;IACH,GAAG,CAAC,CAAC,SAAS,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IAE9E;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,CAAC,SAAS,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;IAE3D;;;;;;;OAOG;IACH,QAAQ,IAAI,IAAI,CAAC;CACjB;AA+CD,wBAAgB,kBAAkB,CAAC,OAAO,SAAS,CAAC,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,mBAAmB,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC,CAkE3J"}
1
+ {"version":3,"file":"schema.factory.d.ts","sourceRoot":"","sources":["../src/schema.factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AA4BjE;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB,CACnC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,KAAK,SAAS,MAAM,OAAO,GAAG,MAAM,OAAO;IAE3C;;;OAGG;IACH,QAAQ,IAAI,IAAI,CAAC;IAEjB;;;;;;;;;;OAUG;IACH,GAAG,CAAC,CAAC,SAAS,KAAK,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAEzC;;;;;;;;;;;OAWG;IACH,GAAG,CAAC,CAAC,SAAS,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IAE9E;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,CAAC,SAAS,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;IAE3D;;;;;;;;;;;OAWG;IACH,QAAQ,IAAI,IAAI,CAAC;IAEjB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,KAAK,GAAG,OAAO,CAAC;IAE9B;;;;;OAKG;IACH,aAAa,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC;IAE9B;;;;;;OAMG;IACH,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAoBD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,SAAS,CAAC,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,mBAAmB,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC,CAqG3J"}
@@ -1,4 +1,6 @@
1
1
  import { ConfigManager } from './manager.js';
2
+ import { IsMarkedSecret } from './secret.js';
3
+ import { ConfigurationNotRegisteredError, ConfigurationNotSetError } from './errors.js';
2
4
  /**
3
5
  * Intelligently parse an environment variable string to a JSON-compatible value.
4
6
  * Attempts to parse as JSON first (handles objects, arrays, booleans, null, numbers).
@@ -22,6 +24,23 @@ function ParseEnvVarValue(envVarValue) {
22
24
  return envVarValue;
23
25
  }
24
26
  }
27
+ /**
28
+ * Safely extracts the default value from a Zod schema, handling both
29
+ * lazy defaults (functions) and eager defaults (plain values).
30
+ *
31
+ * @param schema - The field schema to extract the default from
32
+ * @returns The evaluated default value, or undefined if not present
33
+ */
34
+ function extractDefaultValue(schema) {
35
+ const _def = schema._def;
36
+ if (!_def || typeof _def !== 'object')
37
+ return undefined;
38
+ const dv = _def.defaultValue;
39
+ if (dv === undefined)
40
+ return undefined;
41
+ // Handle both lazy (function) and eager (plain value) defaults
42
+ return typeof dv === 'function' ? dv() : dv;
43
+ }
25
44
  /**
26
45
  * Creates a configuration schema object from a Zod schema and prefix.
27
46
  *
@@ -49,23 +68,6 @@ function ParseEnvVarValue(envVarValue) {
49
68
  * // MongoDBConfig.ParseENV();
50
69
  * ```
51
70
  */
52
- /**
53
- * Safely extracts the default value from a Zod schema, handling both
54
- * lazy defaults (functions) and eager defaults (plain values).
55
- *
56
- * @param schema - The field schema to extract the default from
57
- * @returns The evaluated default value, or undefined if not present
58
- */
59
- function extractDefaultValue(schema) {
60
- const _def = schema._def;
61
- if (!_def || typeof _def !== 'object')
62
- return undefined;
63
- const dv = _def.defaultValue;
64
- if (dv === undefined)
65
- return undefined;
66
- // Handle both lazy (function) and eager (plain value) defaults
67
- return typeof dv === 'function' ? dv() : dv;
68
- }
69
71
  export function CreateConfigSchema(schema, prefix) {
70
72
  // Build a map of key -> prefixed name
71
73
  const prefixedNames = {};
@@ -74,6 +76,8 @@ export function CreateConfigSchema(schema, prefix) {
74
76
  prefixedNames[key] = `${prefix}${key}`;
75
77
  }
76
78
  }
79
+ // Build a set of secret keys by checking each field's metadata
80
+ const secretKeys = new Set(Object.keys(schema.shape).filter((key) => IsMarkedSecret(schema.shape[key])));
77
81
  return {
78
82
  Register() {
79
83
  for (const key in schema.shape) {
@@ -124,5 +128,29 @@ export function CreateConfigSchema(schema, prefix) {
124
128
  }
125
129
  }
126
130
  },
131
+ IsSecret(key) {
132
+ return secretKeys.has(key);
133
+ },
134
+ GetSecretKeys() {
135
+ return Array.from(secretKeys);
136
+ },
137
+ Redact() {
138
+ const result = {};
139
+ for (const key of Object.keys(schema.shape)) {
140
+ try {
141
+ const value = this.Get(key);
142
+ result[key] = secretKeys.has(key) ? '***' : value;
143
+ }
144
+ catch (error) {
145
+ if (error instanceof ConfigurationNotSetError ||
146
+ error instanceof ConfigurationNotRegisteredError) {
147
+ // omit unregistered/unset keys
148
+ continue;
149
+ }
150
+ throw error;
151
+ }
152
+ }
153
+ return result;
154
+ },
127
155
  };
128
156
  }
@@ -0,0 +1,44 @@
1
+ import { z } from 'zod/v4';
2
+ /**
3
+ * Marks a Zod schema as secret using Zod v4's globalRegistry metadata system.
4
+ * This allows configuration to automatically detect sensitive fields that should
5
+ * not be logged or exposed in output.
6
+ *
7
+ * The inferred TypeScript type of the schema is unchanged by this operation.
8
+ * The metadata is stored in Zod's global registry for retrieval by validation logic.
9
+ *
10
+ * @param schema - The Zod schema to mark as secret
11
+ * @returns The same schema type with secret metadata registered
12
+ *
13
+ * @remarks
14
+ * The inferred TypeScript type is unchanged and will not affect type inference
15
+ * for values validated by this schema. CreateConfigSchema automatically detects
16
+ * this marker via the IsMarkedSecret helper to handle sensitive fields specially.
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * // Mark a simple string as secret
21
+ * const secretToken = Secret(z.string());
22
+ * ```
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * // Mark a chained schema with constraints
27
+ * const apiKey = Secret(z.string().min(32)).default('');
28
+ * ```
29
+ */
30
+ export declare function Secret<T extends z.ZodTypeAny>(schema: T): T;
31
+ /**
32
+ * Internal helper function that traverses the Zod schema metadata chain
33
+ * to determine if a schema is marked as secret.
34
+ *
35
+ * Walks through the innerType chain of wrapper schemas (e.g., ZodDefault,
36
+ * ZodOptional) and checks each level's metadata in the global registry.
37
+ *
38
+ * @param schema - The Zod schema to check for secret metadata
39
+ * @returns true if the schema or any of its inner schemas has secret=true metadata
40
+ * @internal
41
+ */
42
+ declare function IsMarkedSecret(schema: z.ZodTypeAny): boolean;
43
+ export { IsMarkedSecret };
44
+ //# sourceMappingURL=secret.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret.d.ts","sourceRoot":"","sources":["../src/secret.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AAE3B;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAG3D;AAED;;;;;;;;;;GAUG;AACH,iBAAS,cAAc,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,GAAG,OAAO,CAsBrD;AAGD,OAAO,EAAE,cAAc,EAAE,CAAC"}
package/dist/secret.js ADDED
@@ -0,0 +1,65 @@
1
+ import { z } from 'zod/v4';
2
+ /**
3
+ * Marks a Zod schema as secret using Zod v4's globalRegistry metadata system.
4
+ * This allows configuration to automatically detect sensitive fields that should
5
+ * not be logged or exposed in output.
6
+ *
7
+ * The inferred TypeScript type of the schema is unchanged by this operation.
8
+ * The metadata is stored in Zod's global registry for retrieval by validation logic.
9
+ *
10
+ * @param schema - The Zod schema to mark as secret
11
+ * @returns The same schema type with secret metadata registered
12
+ *
13
+ * @remarks
14
+ * The inferred TypeScript type is unchanged and will not affect type inference
15
+ * for values validated by this schema. CreateConfigSchema automatically detects
16
+ * this marker via the IsMarkedSecret helper to handle sensitive fields specially.
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * // Mark a simple string as secret
21
+ * const secretToken = Secret(z.string());
22
+ * ```
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * // Mark a chained schema with constraints
27
+ * const apiKey = Secret(z.string().min(32)).default('');
28
+ * ```
29
+ */
30
+ export function Secret(schema) {
31
+ const existingMeta = z.globalRegistry.get(schema) ?? {};
32
+ return schema.meta({ ...existingMeta, secret: true });
33
+ }
34
+ /**
35
+ * Internal helper function that traverses the Zod schema metadata chain
36
+ * to determine if a schema is marked as secret.
37
+ *
38
+ * Walks through the innerType chain of wrapper schemas (e.g., ZodDefault,
39
+ * ZodOptional) and checks each level's metadata in the global registry.
40
+ *
41
+ * @param schema - The Zod schema to check for secret metadata
42
+ * @returns true if the schema or any of its inner schemas has secret=true metadata
43
+ * @internal
44
+ */
45
+ function IsMarkedSecret(schema) {
46
+ let current = schema;
47
+ while (current != null) {
48
+ try {
49
+ const meta = z.globalRegistry.get(current);
50
+ if (meta?.secret === true) {
51
+ return true;
52
+ }
53
+ }
54
+ catch {
55
+ // Schema does not support registry lookup (e.g. mock/partial schema objects).
56
+ // Treat as non-secret and stop traversal.
57
+ break;
58
+ }
59
+ // Traverse to the next level via innerType (present in wrapper schemas)
60
+ current = current._def?.innerType;
61
+ }
62
+ return false;
63
+ }
64
+ // Export for testing and advanced use cases (though marked as internal)
65
+ export { IsMarkedSecret };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pawells/config",
3
- "version": "2.1.7",
3
+ "version": "2.3.0",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "Phillip Aaron Wells",
@@ -29,10 +29,36 @@
29
29
  },
30
30
  "files": [
31
31
  "dist",
32
+ "CHANGELOG.md",
32
33
  "!**/*.tsbuildinfo"
33
34
  ],
34
35
  "dependencies": {
35
- "tslib": "^2.3.0",
36
+ "tslib": "^2.8.1",
36
37
  "zod": "^4.4.3"
38
+ },
39
+ "nx": {
40
+ "name": "@pawells/config",
41
+ "tags": [
42
+ "npm:public"
43
+ ],
44
+ "targets": {
45
+ "publish": {
46
+ "executor": "nx:run-commands",
47
+ "options": {
48
+ "command": "npm publish --provenance --access public",
49
+ "cwd": "{projectRoot}"
50
+ },
51
+ "dependsOn": [
52
+ "build"
53
+ ]
54
+ },
55
+ "clean": {
56
+ "executor": "nx:run-commands",
57
+ "options": {
58
+ "command": "rm -rf dist tsconfig.*.tsbuildinfo",
59
+ "cwd": "packages/typescript-common"
60
+ }
61
+ }
62
+ }
37
63
  }
38
64
  }