@pawells/config 2.2.0 → 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 +19 -0
- package/LICENSE +21 -0
- package/dist/errors.d.ts +2 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/manager.d.ts +56 -2
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +133 -2
- package/dist/schema.factory.d.ts +53 -0
- package/dist/schema.factory.d.ts.map +1 -1
- package/dist/schema.factory.js +45 -17
- package/dist/secret.d.ts +44 -0
- package/dist/secret.d.ts.map +1 -0
- package/dist/secret.js +65 -0
- package/package.json +3 -2
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
|
-
* @
|
|
5
|
-
* @
|
|
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/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA
|
|
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
|
-
* @
|
|
5
|
-
* @
|
|
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
package/dist/index.d.ts.map
CHANGED
|
@@ -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
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
|
-
*
|
|
35
|
-
*
|
|
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
|
package/dist/manager.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../src/manager.ts"],"names":[],"mappings":"
|
|
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
|
-
*
|
|
57
|
-
*
|
|
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
|
}
|
package/dist/schema.factory.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/schema.factory.js
CHANGED
|
@@ -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
|
}
|
package/dist/secret.d.ts
ADDED
|
@@ -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.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Phillip Aaron Wells",
|
|
@@ -29,10 +29,11 @@
|
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
31
|
"dist",
|
|
32
|
+
"CHANGELOG.md",
|
|
32
33
|
"!**/*.tsbuildinfo"
|
|
33
34
|
],
|
|
34
35
|
"dependencies": {
|
|
35
|
-
"tslib": "^2.
|
|
36
|
+
"tslib": "^2.8.1",
|
|
36
37
|
"zod": "^4.4.3"
|
|
37
38
|
},
|
|
38
39
|
"nx": {
|