@pawells/config-provider-env 3.0.0-rc1

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/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/README.md ADDED
@@ -0,0 +1,210 @@
1
+ # @pawells/config-provider-env
2
+
3
+ [![CI](https://github.com/PhillipAWells/config/actions/workflows/ci.yml/badge.svg)](https://github.com/PhillipAWells/config/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/@pawells/config-provider-env)](https://www.npmjs.com/package/@pawells/config-provider-env)
5
+ [![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](../../LICENSE)
7
+
8
+ ## Description
9
+
10
+ `@pawells/config-provider-env` is a configuration provider for `@pawells/config` that loads settings from `process.env` and an optional `.env` file. Values in the dotenv file overwrite `process.env` for the same key. Environment variable strings are parsed to their native JavaScript types (numbers, booleans, arrays, `null`) before being handed to `ConfigManager` for schema validation.
11
+
12
+ The provider also supports writing configuration back to disk (`Save`), making it suitable for generating `.env.example` templates and snapshotting live values.
13
+
14
+ See the [workspace README](../../README.md) for an end-to-end quick start. See [CHANGELOG.md](../../CHANGELOG.md) for version history.
15
+
16
+ ## Requirements
17
+
18
+ - Node.js `>=22.0.0`
19
+ - `@pawells/config` (peer dependency, installed as a direct dependency via `workspace:*` in the monorepo)
20
+
21
+ ## Installation
22
+
23
+ ```sh
24
+ yarn add @pawells/config-provider-env @pawells/config
25
+ ```
26
+
27
+ All packages are ESM-only (`"type": "module"`).
28
+
29
+ ## Quick Start
30
+
31
+ ```typescript
32
+ import { ConfigManager, RegisterConfigSchema, Secret } from '@pawells/config';
33
+ import { ConfigEnvironmentProvider } from '@pawells/config-provider-env';
34
+ import { z } from 'zod';
35
+
36
+ // Register the provider BEFORE importing any schema modules.
37
+ // The factory method creates, registers, and returns the provider instance.
38
+ const envProvider = await ConfigEnvironmentProvider.Register({ path: '.env' });
39
+
40
+ // Define a schema — provider values are already available.
41
+ const AppConfig = RegisterConfigSchema('App', z.object({
42
+ HOST: z.string().min(1).default('localhost'),
43
+ PORT: z.coerce.number().int().positive().default(3000),
44
+ API_KEY: Secret(z.string().min(1)).default(''),
45
+ }));
46
+
47
+ // Read typed values.
48
+ const host = AppConfig.Get('HOST'); // string
49
+ const port = AppConfig.Get('PORT'); // number
50
+
51
+ // Generate .env.example — defaults written; secrets blank.
52
+ await ConfigManager.Save(envProvider, { path: '.env.example' });
53
+
54
+ // Snapshot current runtime values — secrets included.
55
+ await ConfigManager.Save(envProvider, { path: '.env.snapshot', useCurrentValues: true });
56
+ ```
57
+
58
+ ## API Reference
59
+
60
+ ### `ConfigEnvironmentProvider`
61
+
62
+ An async configuration provider that extends `ConfigProvider` from `@pawells/config`.
63
+
64
+ #### `static Register(options?)`
65
+
66
+ Convenience factory: creates a `ConfigEnvironmentProvider` instance, registers it with `ConfigManager`, and returns it.
67
+
68
+ ```typescript
69
+ static async Register(
70
+ options?: Partial<TConfigENVProviderOptions>
71
+ ): Promise<ConfigEnvironmentProvider>
72
+ ```
73
+
74
+ Unspecified options use schema defaults (`name: 'environment'`, `path: <cwd>/.env`).
75
+
76
+ ```typescript
77
+ // Register with all defaults
78
+ const provider = await ConfigEnvironmentProvider.Register();
79
+
80
+ // Register with a custom path
81
+ const provider = await ConfigEnvironmentProvider.Register({
82
+ path: '.env.production'
83
+ });
84
+ ```
85
+
86
+ #### Constructor
87
+
88
+ ```typescript
89
+ new ConfigEnvironmentProvider(options: TConfigENVProviderOptions)
90
+ ```
91
+
92
+ After constructing, pass the instance to `ConfigManager.RegisterProvider` manually if you need the instance before registration:
93
+
94
+ ```typescript
95
+ import { ConfigManager } from '@pawells/config';
96
+ import { ConfigEnvironmentProvider } from '@pawells/config-provider-env';
97
+
98
+ const provider = new ConfigEnvironmentProvider({ name: 'env', path: '.env' });
99
+ await ConfigManager.RegisterProvider(provider);
100
+ ```
101
+
102
+ #### `Load()`
103
+
104
+ ```typescript
105
+ async Load(): Promise<Record<string, unknown>>
106
+ ```
107
+
108
+ Loads configuration from `process.env` and optionally the `.env` file at `options.path`:
109
+
110
+ 1. All entries in `process.env` are read first.
111
+ 2. If `options.path` is set, the dotenv file is parsed and its entries overwrite `process.env` values for the same key.
112
+ 3. All string values are passed through an internal parser that converts JSON-encoded booleans, numbers, arrays, and `null` to their native JavaScript types. Plain strings are returned unchanged.
113
+ 4. If the dotenv file is absent (`ENOENT`), it is silently skipped — only `process.env` is returned.
114
+ 5. Security exceptions (symlink path, path-traversal sequence) are propagated as errors.
115
+
116
+ ```typescript
117
+ // .env: APP_PORT=8080
118
+ // process.env: APP_HOST=prod.example.com
119
+ const values = await provider.Load();
120
+ // → { APP_HOST: 'prod.example.com', APP_PORT: 8080 }
121
+ ```
122
+
123
+ **Throws** `ConfigError` when the dotenv path is a symlink or contains a `..` traversal sequence.
124
+
125
+ #### `Save(entries, options?)`
126
+
127
+ ```typescript
128
+ async Save(
129
+ entries: readonly ConfigSaveEntry[],
130
+ options?: TConfigENVProviderSaveOptions
131
+ ): Promise<void>
132
+ ```
133
+
134
+ Writes configuration entries to a `.env`-format file. Call via `ConfigManager.Save` rather than directly.
135
+
136
+ **Template mode** (`useCurrentValues: false`, the default):
137
+ - Each entry is written as `KEY=<default value>`.
138
+ - Entries where `entry.isSecret` is `true` are written as `KEY=` (blank value), regardless of their default.
139
+ - This is suitable for generating `.env.example` files safe to commit.
140
+
141
+ **Current-values mode** (`useCurrentValues: true`):
142
+ - All entries including secrets are written with their live resolved values.
143
+ - Use for runtime snapshots or debugging.
144
+
145
+ If a Zod `.describe()` annotation is present on the field, it is emitted as a `# comment` line immediately before the key.
146
+
147
+ ```typescript
148
+ // Template output in .env.example:
149
+ // # Server port
150
+ // APP_PORT=3000
151
+ // # API key
152
+ // APP_API_KEY=
153
+ ```
154
+
155
+ **Options:**
156
+
157
+ | Field | Type | Default | Description |
158
+ |---|---|---|---|
159
+ | `path` | `string` (optional) | `this.options.path` | Output file path; overrides the constructor path if provided. |
160
+ | `useCurrentValues` | `boolean` (optional) | `false` | `false` = registered defaults, secrets blank; `true` = live resolved values. |
161
+
162
+ ---
163
+
164
+ ### Options types
165
+
166
+ #### `TConfigENVProviderOptions`
167
+
168
+ ```typescript
169
+ type TConfigENVProviderOptions = {
170
+ name: string // Default: 'environment'
171
+ path: string // Default: <cwd>/.env; '..' sequences are rejected
172
+ }
173
+ ```
174
+
175
+ Validated by `CONFIG_ENV_PROVIDER_OPTIONS_SCHEMA`.
176
+
177
+ #### `TConfigENVProviderSaveOptions`
178
+
179
+ ```typescript
180
+ type TConfigENVProviderSaveOptions = {
181
+ path?: string // Overrides constructor path if provided; '..' sequences are rejected
182
+ useCurrentValues?: boolean // Default: false
183
+ }
184
+ ```
185
+
186
+ Validated by `CONFIG_ENV_PROVIDER_SAVE_OPTIONS_SCHEMA`.
187
+
188
+ ---
189
+
190
+ ### Assertion and validation utilities
191
+
192
+ | Export | Description |
193
+ |---|---|
194
+ | `CONFIG_ENV_PROVIDER_OPTIONS_SCHEMA` | Zod schema for `TConfigENVProviderOptions` |
195
+ | `AssertConfigENVProviderOptions(options)` | Asserts conformance; throws `ZodError` otherwise |
196
+ | `ValidateConfigENVProviderOptions(options)` | Returns `true` if valid; `false` otherwise |
197
+ | `CONFIG_ENV_PROVIDER_SAVE_OPTIONS_SCHEMA` | Zod schema for `TConfigENVProviderSaveOptions` |
198
+ | `AssertConfigENVProviderSaveOptions(options)` | Asserts conformance; throws `ZodError` otherwise |
199
+ | `ValidateConfigENVProviderSaveOptions(options)` | Returns `true` if valid; `false` otherwise |
200
+
201
+ ---
202
+
203
+ ### Security
204
+
205
+ - **Symlink rejection** — The dotenv path is checked; if it resolves to a symlink, `Load()` throws a `ConfigError`.
206
+ - **Path-traversal protection** — Any path containing `..` is rejected by the options schema at construction time and at save time.
207
+
208
+ ## License
209
+
210
+ MIT — See [LICENSE](../../LICENSE) for details.
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Intelligently parses an environment variable string to its native JavaScript type.
3
+ *
4
+ * Attempts `JSON.parse()` to convert encoded booleans (`'true'`), numbers (`'42'`),
5
+ * arrays (`'["a","b"]'`), and `null` to their native JavaScript equivalents.
6
+ * Falls back to returning the original string unchanged if JSON parsing fails.
7
+ * Uses a fast-path optimization to skip `JSON.parse()` for plain strings.
8
+ *
9
+ * @param envVarValue - Raw string value read from `process.env`
10
+ * @returns Parsed value; a native JS type if JSON-parseable, otherwise the original string
11
+ *
12
+ * @remarks
13
+ * The string `"null"` is parsed by `JSON.parse()` and returns the JavaScript `null` value
14
+ * (not the string `"null"`). Schemas that expect a string will reject this value via `safeParse()`.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * ParseEnvVarValue('true') // → true (boolean)
19
+ * ParseEnvVarValue('42') // → 42 (number)
20
+ * ParseEnvVarValue('["a","b"]') // → ['a', 'b'] (string[])
21
+ * ParseEnvVarValue('hello world') // → 'hello world' (string, unchanged)
22
+ * ```
23
+ */
24
+ export declare function ParseEnvVarValue(envVarValue: string): unknown;
25
+ /**
26
+ * Parses a `.env` file from disk into a flat key/value record.
27
+ *
28
+ * Processing rules (applied per line):
29
+ * - Lines beginning with `#` (after trimming whitespace) are treated as comments and skipped
30
+ * - Blank lines are skipped
31
+ * - Lines containing `=` are split on the first `=`; the key is trimmed; the value is trimmed
32
+ * and surrounding single- or double-quotes are stripped if present
33
+ * - Inline comments (e.g. `KEY=value # comment`) are stripped from unquoted values
34
+ * - Lines without `=` are skipped
35
+ * - Windows-style `\r\n` line endings are normalized automatically
36
+ * - Paths containing `..` traversal sequences are rejected for security
37
+ * - Symbolic links are not permitted for security (both final component and parent directories)
38
+ *
39
+ * @param path - Path to the `.env` file to read
40
+ * @returns A promise resolving to a record mapping each key to its raw string value
41
+ * @throws {ConfigError} When the path contains `..` directory traversal sequences
42
+ * @throws {ConfigError} When the path is a symbolic link (final component or parent directory)
43
+ * @throws {Error} If the file cannot be read (e.g. not found, permission denied)
44
+ *
45
+ * @remarks
46
+ * Parent directories are canonicalized via realpath to detect symlinked ancestor directories.
47
+ * The final path component is checked via O_NOFOLLOW for atomic symlink rejection.
48
+ * Inline comments must be preceded by a space to be recognized (e.g., `KEY=value # comment`).
49
+ * A `#` that is not preceded by a space is treated as part of the value.
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * // .env contents:
54
+ * // # Database configuration
55
+ * // HOST=localhost
56
+ * // PORT=3000
57
+ * // SECRET="my-token"
58
+ * const values = await ParseDotEnvFileAsync('./.env');
59
+ * // → { HOST: 'localhost', PORT: '3000', SECRET: 'my-token' }
60
+ * ```
61
+ */
62
+ export declare function ParseDotEnvFileAsync(path: string): Promise<Record<string, string>>;
63
+ /**
64
+ * Serializes a configuration value to its `.env` string representation.
65
+ *
66
+ * Conversion rules:
67
+ * - `null` or `undefined` → `''` (blank)
68
+ * - Arrays → JSON-stringified (e.g. `["a","b"]`)
69
+ * - Plain objects → JSON-stringified
70
+ * - `Date` → ISO 8601 string
71
+ * - All other values → `String(value)`
72
+ *
73
+ * @param value - The configuration value to serialize
74
+ * @returns The serialized string ready for inclusion in a `.env` file
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * SerializeConfigValue('hello') // → 'hello'
79
+ * SerializeConfigValue(42) // → '42'
80
+ * SerializeConfigValue(true) // → 'true'
81
+ * SerializeConfigValue(['a', 'b']) // → '["a","b"]'
82
+ * SerializeConfigValue({ key: 'value' }) // → '{"key":"value"}'
83
+ * SerializeConfigValue(new Date('2024-01-01T00:00:00.000Z'))
84
+ * // → '2024-01-01T00:00:00.000Z'
85
+ * SerializeConfigValue(null) // → ''
86
+ * SerializeConfigValue(undefined) // → ''
87
+ * ```
88
+ */
89
+ export declare function SerializeConfigValue(value: unknown): string;
90
+ //# sourceMappingURL=env-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-utils.d.ts","sourceRoot":"","sources":["../src/env-utils.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAqB7D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CA8ExF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAM3D"}
@@ -0,0 +1,185 @@
1
+ import { promises as fs, constants as fsConstants } from 'node:fs';
2
+ import { normalize, dirname, basename, join } from 'node:path';
3
+ import { ConfigError } from '@pawells/config';
4
+ /**
5
+ * Intelligently parses an environment variable string to its native JavaScript type.
6
+ *
7
+ * Attempts `JSON.parse()` to convert encoded booleans (`'true'`), numbers (`'42'`),
8
+ * arrays (`'["a","b"]'`), and `null` to their native JavaScript equivalents.
9
+ * Falls back to returning the original string unchanged if JSON parsing fails.
10
+ * Uses a fast-path optimization to skip `JSON.parse()` for plain strings.
11
+ *
12
+ * @param envVarValue - Raw string value read from `process.env`
13
+ * @returns Parsed value; a native JS type if JSON-parseable, otherwise the original string
14
+ *
15
+ * @remarks
16
+ * The string `"null"` is parsed by `JSON.parse()` and returns the JavaScript `null` value
17
+ * (not the string `"null"`). Schemas that expect a string will reject this value via `safeParse()`.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * ParseEnvVarValue('true') // → true (boolean)
22
+ * ParseEnvVarValue('42') // → 42 (number)
23
+ * ParseEnvVarValue('["a","b"]') // → ['a', 'b'] (string[])
24
+ * ParseEnvVarValue('hello world') // → 'hello world' (string, unchanged)
25
+ * ```
26
+ */
27
+ export function ParseEnvVarValue(envVarValue) {
28
+ // Fast path: skip JSON.parse for plain strings that cannot be valid JSON
29
+ const firstChar = envVarValue[0];
30
+ if (firstChar !== '{'
31
+ && firstChar !== '['
32
+ && firstChar !== '"'
33
+ && envVarValue !== 'true'
34
+ && envVarValue !== 'false'
35
+ && envVarValue !== 'null'
36
+ && !/^-?\d/.test(envVarValue)) {
37
+ return envVarValue;
38
+ }
39
+ try {
40
+ return JSON.parse(envVarValue);
41
+ }
42
+ catch {
43
+ return envVarValue;
44
+ }
45
+ }
46
+ /**
47
+ * Parses a `.env` file from disk into a flat key/value record.
48
+ *
49
+ * Processing rules (applied per line):
50
+ * - Lines beginning with `#` (after trimming whitespace) are treated as comments and skipped
51
+ * - Blank lines are skipped
52
+ * - Lines containing `=` are split on the first `=`; the key is trimmed; the value is trimmed
53
+ * and surrounding single- or double-quotes are stripped if present
54
+ * - Inline comments (e.g. `KEY=value # comment`) are stripped from unquoted values
55
+ * - Lines without `=` are skipped
56
+ * - Windows-style `\r\n` line endings are normalized automatically
57
+ * - Paths containing `..` traversal sequences are rejected for security
58
+ * - Symbolic links are not permitted for security (both final component and parent directories)
59
+ *
60
+ * @param path - Path to the `.env` file to read
61
+ * @returns A promise resolving to a record mapping each key to its raw string value
62
+ * @throws {ConfigError} When the path contains `..` directory traversal sequences
63
+ * @throws {ConfigError} When the path is a symbolic link (final component or parent directory)
64
+ * @throws {Error} If the file cannot be read (e.g. not found, permission denied)
65
+ *
66
+ * @remarks
67
+ * Parent directories are canonicalized via realpath to detect symlinked ancestor directories.
68
+ * The final path component is checked via O_NOFOLLOW for atomic symlink rejection.
69
+ * Inline comments must be preceded by a space to be recognized (e.g., `KEY=value # comment`).
70
+ * A `#` that is not preceded by a space is treated as part of the value.
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * // .env contents:
75
+ * // # Database configuration
76
+ * // HOST=localhost
77
+ * // PORT=3000
78
+ * // SECRET="my-token"
79
+ * const values = await ParseDotEnvFileAsync('./.env');
80
+ * // → { HOST: 'localhost', PORT: '3000', SECRET: 'my-token' }
81
+ * ```
82
+ */
83
+ export async function ParseDotEnvFileAsync(path) {
84
+ if (normalize(path).includes('..'))
85
+ throw new ConfigError(`Path traversal sequences ("..") are not permitted. Received: "${path}"`);
86
+ try {
87
+ // Canonicalize parent directory to detect symlinked ancestors
88
+ const realParent = await fs.realpath(dirname(path));
89
+ const safePath = join(realParent, basename(path));
90
+ // Open file with O_NOFOLLOW to reject symlink final component atomically
91
+ const filehandle = await fs.open(safePath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
92
+ let raw;
93
+ try {
94
+ const buffer = await filehandle.readFile();
95
+ raw = buffer.toString('utf-8');
96
+ }
97
+ finally {
98
+ await filehandle.close();
99
+ }
100
+ const result = {};
101
+ for (const rawLine of raw.split('\n')) {
102
+ const line = rawLine.replace(/\r$/, '').trim();
103
+ if (line === '' || line.startsWith('#'))
104
+ continue;
105
+ const eqIdx = line.indexOf('=');
106
+ if (eqIdx === -1)
107
+ continue;
108
+ const key = line.slice(0, eqIdx).trim();
109
+ let value = line.slice(eqIdx + 1).trim();
110
+ if ((value.startsWith('"') && value.endsWith('"'))
111
+ || (value.startsWith('\'') && value.endsWith('\''))) {
112
+ value = value.slice(1, -1);
113
+ }
114
+ else {
115
+ // Strip inline # comments (only if value is not quoted)
116
+ const commentIdx = value.indexOf(' #');
117
+ if (commentIdx !== -1) {
118
+ value = value.slice(0, commentIdx).trim();
119
+ }
120
+ }
121
+ if (key !== '') {
122
+ result[key] = value;
123
+ }
124
+ }
125
+ return result;
126
+ }
127
+ catch (error) {
128
+ // If it's already a ConfigError, rethrow it
129
+ if (error instanceof ConfigError) {
130
+ throw error;
131
+ }
132
+ // Check error codes from fs.open and realpath
133
+ const errorCode = error instanceof Error ? error.code : undefined;
134
+ // ELOOP (Linux) or ENOTDIR (macOS) indicate symlink in final component
135
+ if (errorCode === 'ELOOP' || errorCode === 'ENOTDIR') {
136
+ throw new ConfigError('Symlink paths are not permitted.');
137
+ }
138
+ // Check if it's ENOENT (file not found) — allow optional dotenv files to return empty
139
+ const isNotFound = errorCode === 'ENOENT';
140
+ // If file is missing, return empty config (dotenv files are optional by nature)
141
+ if (isNotFound) {
142
+ return {};
143
+ }
144
+ // All other errors (permission, etc.) are rethrown
145
+ throw error;
146
+ }
147
+ }
148
+ /**
149
+ * Serializes a configuration value to its `.env` string representation.
150
+ *
151
+ * Conversion rules:
152
+ * - `null` or `undefined` → `''` (blank)
153
+ * - Arrays → JSON-stringified (e.g. `["a","b"]`)
154
+ * - Plain objects → JSON-stringified
155
+ * - `Date` → ISO 8601 string
156
+ * - All other values → `String(value)`
157
+ *
158
+ * @param value - The configuration value to serialize
159
+ * @returns The serialized string ready for inclusion in a `.env` file
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * SerializeConfigValue('hello') // → 'hello'
164
+ * SerializeConfigValue(42) // → '42'
165
+ * SerializeConfigValue(true) // → 'true'
166
+ * SerializeConfigValue(['a', 'b']) // → '["a","b"]'
167
+ * SerializeConfigValue({ key: 'value' }) // → '{"key":"value"}'
168
+ * SerializeConfigValue(new Date('2024-01-01T00:00:00.000Z'))
169
+ * // → '2024-01-01T00:00:00.000Z'
170
+ * SerializeConfigValue(null) // → ''
171
+ * SerializeConfigValue(undefined) // → ''
172
+ * ```
173
+ */
174
+ export function SerializeConfigValue(value) {
175
+ if (value === null || value === undefined)
176
+ return '';
177
+ if (value instanceof Date)
178
+ return value.toISOString();
179
+ if (Array.isArray(value))
180
+ return JSON.stringify(value);
181
+ if (value !== null && typeof value === 'object')
182
+ return JSON.stringify(value);
183
+ return String(value);
184
+ }
185
+ //# sourceMappingURL=env-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-utils.js","sourceRoot":"","sources":["../src/env-utils.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,226 @@
1
+ import { z } from 'zod/v4';
2
+ import { ConfigProvider, type ConfigSaveEntry } from '@pawells/config';
3
+ /**
4
+ * Zod schema for validating ConfigEnvironmentProvider constructor options.
5
+ *
6
+ * @remarks
7
+ * This schema extends the base {@link CONFIG_PROVIDER_OPTIONS_SCHEMA} with environment-specific fields:
8
+ * - `name`: Unique provider identifier (default: `'environment'`)
9
+ * - `path`: Path to the `.env` file to load (default: `.env` in current working directory);
10
+ * paths containing `..` directory traversal sequences are rejected for security
11
+ */
12
+ export declare const CONFIG_ENV_PROVIDER_OPTIONS_SCHEMA: z.ZodObject<{
13
+ name: z.ZodDefault<z.ZodString>;
14
+ path: z.ZodDefault<z.ZodString>;
15
+ }, z.core.$strip>;
16
+ /**
17
+ * Runtime type extracted from {@link CONFIG_ENV_PROVIDER_OPTIONS_SCHEMA}.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * const options: TConfigENVProviderOptions = {
22
+ * name: 'my-env',
23
+ * path: '.env.local'
24
+ * };
25
+ * ```
26
+ */
27
+ export type TConfigENVProviderOptions = z.infer<typeof CONFIG_ENV_PROVIDER_OPTIONS_SCHEMA>;
28
+ /**
29
+ * Asserts that a value conforms to {@link TConfigENVProviderOptions}.
30
+ *
31
+ * @param options - The value to validate
32
+ * @throws {ZodError} When validation fails
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * AssertConfigENVProviderOptions(userInput);
37
+ * // If no error, userInput is safely typed as TConfigENVProviderOptions
38
+ * ```
39
+ */
40
+ export declare function AssertConfigENVProviderOptions(options: unknown): asserts options is TConfigENVProviderOptions;
41
+ /**
42
+ * Validates whether a value conforms to {@link TConfigENVProviderOptions}.
43
+ *
44
+ * @param options - The value to validate
45
+ * @returns `true` if valid; `false` otherwise
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * if (ValidateConfigENVProviderOptions(userInput)) {
50
+ * // userInput is valid
51
+ * }
52
+ * ```
53
+ */
54
+ export declare function ValidateConfigENVProviderOptions(options: unknown): boolean;
55
+ /**
56
+ * Zod schema for validating ConfigEnvironmentProvider Save options.
57
+ *
58
+ * @remarks
59
+ * Extends the base {@link CONFIG_PROVIDER_SAVE_OPTIONS_SCHEMA} with:
60
+ * - `path`: Optional output file path; overrides constructor path if provided
61
+ */
62
+ export declare const CONFIG_ENV_PROVIDER_SAVE_OPTIONS_SCHEMA: z.ZodObject<{
63
+ useCurrentValues: z.ZodOptional<z.ZodBoolean>;
64
+ path: z.ZodOptional<z.ZodString>;
65
+ }, z.core.$strip>;
66
+ /**
67
+ * Runtime type extracted from {@link CONFIG_ENV_PROVIDER_SAVE_OPTIONS_SCHEMA}.
68
+ *
69
+ * @example
70
+ * ```typescript
71
+ * const options: TConfigENVProviderSaveOptions = {
72
+ * path: '.env.example',
73
+ * useCurrentValues: false
74
+ * };
75
+ * ```
76
+ */
77
+ export type TConfigENVProviderSaveOptions = z.infer<typeof CONFIG_ENV_PROVIDER_SAVE_OPTIONS_SCHEMA>;
78
+ /**
79
+ * Asserts that a value conforms to {@link TConfigENVProviderSaveOptions}.
80
+ *
81
+ * @param options - The value to validate
82
+ * @throws {ZodError} When validation fails
83
+ */
84
+ export declare function AssertConfigENVProviderSaveOptions(options: unknown): asserts options is TConfigENVProviderSaveOptions;
85
+ /**
86
+ * Validates whether a value conforms to {@link TConfigENVProviderSaveOptions}.
87
+ *
88
+ * @param options - The value to validate
89
+ * @returns `true` if valid; `false` otherwise
90
+ */
91
+ export declare function ValidateConfigENVProviderSaveOptions(options: unknown): boolean;
92
+ /**
93
+ * Configuration provider for environment variables and `.env` files.
94
+ *
95
+ * Loads configuration from `process.env` and an optional `.env` file. This provider
96
+ * implements the {@link ConfigProvider} interface, supporting both {@link Load} and
97
+ * {@link Save} operations. It is suitable for development environments and containerized
98
+ * deployments where configuration is typically provided via environment variables.
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * // Register with defaults
103
+ * const provider = await ConfigEnvironmentProvider.Register();
104
+ *
105
+ * // Register with custom path
106
+ * const provider = await ConfigEnvironmentProvider.Register({
107
+ * path: '.env.production'
108
+ * });
109
+ *
110
+ * // Or instantiate directly
111
+ * const provider = new ConfigEnvironmentProvider({
112
+ * name: 'app-env',
113
+ * path: '.env'
114
+ * });
115
+ * await ConfigManager.RegisterProvider(provider);
116
+ * ```
117
+ */
118
+ export declare class ConfigEnvironmentProvider extends ConfigProvider<TConfigENVProviderOptions, unknown, TConfigENVProviderSaveOptions> {
119
+ /**
120
+ * Initialize a configuration provider that loads from environment variables and a `.env` file.
121
+ *
122
+ * @param options - Provider configuration object with `name` and optional `path`
123
+ * @param options.name - Unique provider name (default: `'environment'`)
124
+ * @param options.path - Path to the `.env` file to load (default: `.env` in current working directory)
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * const provider = new ConfigEnvironmentProvider({
129
+ * name: 'my-env-provider',
130
+ * path: './.env.local'
131
+ * });
132
+ * ```
133
+ */
134
+ constructor(options: TConfigENVProviderOptions);
135
+ /**
136
+ * Load configuration values from `process.env` and, optionally, a `.env` file.
137
+ *
138
+ * Reads all entries in `process.env` first, then (if `options.path` is set)
139
+ * reads the dotenv file and overwrites any overlapping keys. All values are passed
140
+ * through {@link ParseEnvVarValue} before being returned.
141
+ *
142
+ * If the dotenv file is missing or cannot be read due to permission errors or other
143
+ * file system issues, the file is silently skipped and only environment variables are
144
+ * returned. The dotenv file is optional and its absence or read failure does not prevent
145
+ * configuration loading.
146
+ *
147
+ * Security rejections (symlink detection, path traversal) are propagated as errors.
148
+ *
149
+ * @returns A record of fully-qualified config key names to their parsed values
150
+ * @throws {ConfigError} When the dotenv path is a symlink or contains path traversal sequences
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * // process.env = { KEYCLOAK_HOST: 'prod.example.com' }
155
+ * // .env = { KEYCLOAK_HOST: 'localhost', KEYCLOAK_PORT: '8080' }
156
+ * const provider = new ConfigEnvironmentProvider({
157
+ * name: 'env',
158
+ * path: '.env'
159
+ * });
160
+ * const config = await provider.Load();
161
+ * // → { KEYCLOAK_HOST: 'localhost', KEYCLOAK_PORT: 8080 }
162
+ * ```
163
+ */
164
+ Load(): Promise<Record<string, unknown>>;
165
+ /**
166
+ * Save configuration values to a `.env`-format file.
167
+ *
168
+ * In template mode (`useCurrentValues: false`, the default), each entry is
169
+ * written using its registered default value. Secret fields (where
170
+ * `entry.isSecret` is `true`) are always written with a blank value in
171
+ * template mode, regardless of their default — making this suitable for
172
+ * generating `.env.example` files.
173
+ *
174
+ * In current-values mode (`useCurrentValues: true`), the live resolved value
175
+ * is used for every field including secrets — suitable for snapshotting the
176
+ * active runtime configuration.
177
+ *
178
+ * If a field carries a description (from a Zod `.describe()` annotation) it
179
+ * is emitted as a `# comment` line immediately before the key–value pair.
180
+ *
181
+ * @param entries - All registered config entries supplied by {@link ConfigManager.Save}
182
+ * @param options - Save options
183
+ * @param options.path - Output file path; overrides `this.options.path` if provided
184
+ * @param options.useCurrentValues - When `true`, emit current live values; when `false` (default), emit registered defaults
185
+ *
186
+ * @example
187
+ * ```typescript
188
+ * const provider = new ConfigEnvironmentProvider({
189
+ * name: 'env',
190
+ * path: '.env'
191
+ * });
192
+ *
193
+ * // Generate a .env.example template (secrets blank)
194
+ * await ConfigManager.Save(provider, { path: '.env.example' });
195
+ *
196
+ * // Snapshot current runtime config (secrets included)
197
+ * await ConfigManager.Save(provider, {
198
+ * path: '.env.snapshot',
199
+ * useCurrentValues: true,
200
+ * });
201
+ * ```
202
+ */
203
+ Save(entries: readonly ConfigSaveEntry[], options?: TConfigENVProviderSaveOptions): Promise<void>;
204
+ /**
205
+ * Creates a ConfigEnvironmentProvider instance and registers it with ConfigManager.
206
+ *
207
+ * This is a convenience factory method that combines instantiation and registration in one call.
208
+ * Unspecified options use their schema defaults (`name: 'environment'`, `path: '.env'` in cwd).
209
+ *
210
+ * @param options - Optional partial provider configuration; unspecified fields use schema defaults
211
+ * @returns A promise resolving to the registered provider instance
212
+ *
213
+ * @example
214
+ * ```typescript
215
+ * // Register with all defaults
216
+ * const provider = await ConfigEnvironmentProvider.Register();
217
+ *
218
+ * // Register with custom path
219
+ * const provider = await ConfigEnvironmentProvider.Register({
220
+ * path: '.env.production'
221
+ * });
222
+ * ```
223
+ */
224
+ static Register(options?: Partial<TConfigENVProviderOptions>): Promise<ConfigEnvironmentProvider>;
225
+ }
226
+ //# sourceMappingURL=environment-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environment-provider.d.ts","sourceRoot":"","sources":["../src/environment-provider.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAuE,KAAK,eAAe,EAA8B,MAAM,iBAAiB,CAAC;AAGxK;;;;;;;;GAQG;AACH,eAAO,MAAM,kCAAkC;;;iBAK7C,CAAC;AAEH;;;;;;;;;;GAUG;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kCAAkC,CAAC,CAAC;AAE3F;;;;;;;;;;;GAWG;AACH,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,yBAAyB,CAE7G;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gCAAgC,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAQ1E;AAED;;;;;;GAMG;AACH,eAAO,MAAM,uCAAuC;;;iBAIlD,CAAC;AAEH;;;;;;;;;;GAUG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uCAAuC,CAAC,CAAC;AAEpG;;;;;GAKG;AACH,wBAAgB,kCAAkC,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,6BAA6B,CAErH;AAED;;;;;GAKG;AACH,wBAAgB,oCAAoC,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAQ9E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,yBAA0B,SAAQ,cAAc,CAAC,yBAAyB,EAAE,OAAO,EAAE,6BAA6B,CAAC;IAC/H;;;;;;;;;;;;;;OAcG;gBACS,OAAO,EAAE,yBAAyB;IAK9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACU,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IA6BrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAqCG;IACU,IAAI,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,EAAE,OAAO,CAAC,EAAE,6BAA6B,GAAG,OAAO,CAAC,IAAI,CAAC;IAuB9G;;;;;;;;;;;;;;;;;;;OAmBG;WACiB,QAAQ,CAAC,OAAO,GAAE,OAAO,CAAC,yBAAyB,CAAM,GAAG,OAAO,CAAC,yBAAyB,CAAC;CAKlH"}
@@ -0,0 +1,278 @@
1
+ import { promises as FS } from 'node:fs';
2
+ import * as PATH from 'node:path';
3
+ import { z } from 'zod/v4';
4
+ import { ConfigProvider, CONFIG_PROVIDER_OPTIONS_SCHEMA, CONFIG_PROVIDER_SAVE_OPTIONS_SCHEMA, ConfigManager, ConfigError } from '@pawells/config';
5
+ import { ParseEnvVarValue, ParseDotEnvFileAsync, SerializeConfigValue } from './env-utils.js';
6
+ /**
7
+ * Zod schema for validating ConfigEnvironmentProvider constructor options.
8
+ *
9
+ * @remarks
10
+ * This schema extends the base {@link CONFIG_PROVIDER_OPTIONS_SCHEMA} with environment-specific fields:
11
+ * - `name`: Unique provider identifier (default: `'environment'`)
12
+ * - `path`: Path to the `.env` file to load (default: `.env` in current working directory);
13
+ * paths containing `..` directory traversal sequences are rejected for security
14
+ */
15
+ export const CONFIG_ENV_PROVIDER_OPTIONS_SCHEMA = CONFIG_PROVIDER_OPTIONS_SCHEMA.extend({
16
+ name: z.string().min(1).default('environment'),
17
+ path: z.string().min(1).default(PATH.join(process.cwd(), '.env')).refine((path) => !PATH.normalize(path).includes('..'), {
18
+ message: 'Path traversal sequences ("..") are not permitted.'
19
+ })
20
+ });
21
+ /**
22
+ * Asserts that a value conforms to {@link TConfigENVProviderOptions}.
23
+ *
24
+ * @param options - The value to validate
25
+ * @throws {ZodError} When validation fails
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * AssertConfigENVProviderOptions(userInput);
30
+ * // If no error, userInput is safely typed as TConfigENVProviderOptions
31
+ * ```
32
+ */
33
+ export function AssertConfigENVProviderOptions(options) {
34
+ CONFIG_ENV_PROVIDER_OPTIONS_SCHEMA.parse(options);
35
+ }
36
+ /**
37
+ * Validates whether a value conforms to {@link TConfigENVProviderOptions}.
38
+ *
39
+ * @param options - The value to validate
40
+ * @returns `true` if valid; `false` otherwise
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * if (ValidateConfigENVProviderOptions(userInput)) {
45
+ * // userInput is valid
46
+ * }
47
+ * ```
48
+ */
49
+ export function ValidateConfigENVProviderOptions(options) {
50
+ try {
51
+ AssertConfigENVProviderOptions(options);
52
+ return true;
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
58
+ /**
59
+ * Zod schema for validating ConfigEnvironmentProvider Save options.
60
+ *
61
+ * @remarks
62
+ * Extends the base {@link CONFIG_PROVIDER_SAVE_OPTIONS_SCHEMA} with:
63
+ * - `path`: Optional output file path; overrides constructor path if provided
64
+ */
65
+ export const CONFIG_ENV_PROVIDER_SAVE_OPTIONS_SCHEMA = CONFIG_PROVIDER_SAVE_OPTIONS_SCHEMA.extend({
66
+ path: z.string().min(1).refine((path) => !PATH.normalize(path).includes('..'), {
67
+ message: 'Path traversal sequences ("..") are not permitted.'
68
+ }).optional()
69
+ });
70
+ /**
71
+ * Asserts that a value conforms to {@link TConfigENVProviderSaveOptions}.
72
+ *
73
+ * @param options - The value to validate
74
+ * @throws {ZodError} When validation fails
75
+ */
76
+ export function AssertConfigENVProviderSaveOptions(options) {
77
+ CONFIG_ENV_PROVIDER_SAVE_OPTIONS_SCHEMA.parse(options);
78
+ }
79
+ /**
80
+ * Validates whether a value conforms to {@link TConfigENVProviderSaveOptions}.
81
+ *
82
+ * @param options - The value to validate
83
+ * @returns `true` if valid; `false` otherwise
84
+ */
85
+ export function ValidateConfigENVProviderSaveOptions(options) {
86
+ try {
87
+ AssertConfigENVProviderSaveOptions(options);
88
+ return true;
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ }
94
+ /**
95
+ * Configuration provider for environment variables and `.env` files.
96
+ *
97
+ * Loads configuration from `process.env` and an optional `.env` file. This provider
98
+ * implements the {@link ConfigProvider} interface, supporting both {@link Load} and
99
+ * {@link Save} operations. It is suitable for development environments and containerized
100
+ * deployments where configuration is typically provided via environment variables.
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * // Register with defaults
105
+ * const provider = await ConfigEnvironmentProvider.Register();
106
+ *
107
+ * // Register with custom path
108
+ * const provider = await ConfigEnvironmentProvider.Register({
109
+ * path: '.env.production'
110
+ * });
111
+ *
112
+ * // Or instantiate directly
113
+ * const provider = new ConfigEnvironmentProvider({
114
+ * name: 'app-env',
115
+ * path: '.env'
116
+ * });
117
+ * await ConfigManager.RegisterProvider(provider);
118
+ * ```
119
+ */
120
+ export class ConfigEnvironmentProvider extends ConfigProvider {
121
+ /**
122
+ * Initialize a configuration provider that loads from environment variables and a `.env` file.
123
+ *
124
+ * @param options - Provider configuration object with `name` and optional `path`
125
+ * @param options.name - Unique provider name (default: `'environment'`)
126
+ * @param options.path - Path to the `.env` file to load (default: `.env` in current working directory)
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * const provider = new ConfigEnvironmentProvider({
131
+ * name: 'my-env-provider',
132
+ * path: './.env.local'
133
+ * });
134
+ * ```
135
+ */
136
+ constructor(options) {
137
+ super(options);
138
+ AssertConfigENVProviderOptions(options);
139
+ }
140
+ /**
141
+ * Load configuration values from `process.env` and, optionally, a `.env` file.
142
+ *
143
+ * Reads all entries in `process.env` first, then (if `options.path` is set)
144
+ * reads the dotenv file and overwrites any overlapping keys. All values are passed
145
+ * through {@link ParseEnvVarValue} before being returned.
146
+ *
147
+ * If the dotenv file is missing or cannot be read due to permission errors or other
148
+ * file system issues, the file is silently skipped and only environment variables are
149
+ * returned. The dotenv file is optional and its absence or read failure does not prevent
150
+ * configuration loading.
151
+ *
152
+ * Security rejections (symlink detection, path traversal) are propagated as errors.
153
+ *
154
+ * @returns A record of fully-qualified config key names to their parsed values
155
+ * @throws {ConfigError} When the dotenv path is a symlink or contains path traversal sequences
156
+ *
157
+ * @example
158
+ * ```typescript
159
+ * // process.env = { KEYCLOAK_HOST: 'prod.example.com' }
160
+ * // .env = { KEYCLOAK_HOST: 'localhost', KEYCLOAK_PORT: '8080' }
161
+ * const provider = new ConfigEnvironmentProvider({
162
+ * name: 'env',
163
+ * path: '.env'
164
+ * });
165
+ * const config = await provider.Load();
166
+ * // → { KEYCLOAK_HOST: 'localhost', KEYCLOAK_PORT: 8080 }
167
+ * ```
168
+ */
169
+ async Load() {
170
+ const result = {};
171
+ for (const [key, value] of Object.entries(process.env)) {
172
+ if (value !== undefined) {
173
+ result[key] = ParseEnvVarValue(value);
174
+ }
175
+ }
176
+ if (this.options.path !== undefined) {
177
+ try {
178
+ const dotenv = await ParseDotEnvFileAsync(this.options.path);
179
+ for (const [key, value] of Object.entries(dotenv)) {
180
+ result[key] = ParseEnvVarValue(value);
181
+ }
182
+ }
183
+ catch (error) {
184
+ // Re-throw ConfigError (security rejections: symlink, path traversal)
185
+ if (error instanceof ConfigError) {
186
+ throw error;
187
+ }
188
+ // Silently skip other file errors (ENOENT, EACCES, etc.);
189
+ // dotenv is optional and fallback uses process.env values already collected
190
+ }
191
+ }
192
+ return result;
193
+ }
194
+ /**
195
+ * Save configuration values to a `.env`-format file.
196
+ *
197
+ * In template mode (`useCurrentValues: false`, the default), each entry is
198
+ * written using its registered default value. Secret fields (where
199
+ * `entry.isSecret` is `true`) are always written with a blank value in
200
+ * template mode, regardless of their default — making this suitable for
201
+ * generating `.env.example` files.
202
+ *
203
+ * In current-values mode (`useCurrentValues: true`), the live resolved value
204
+ * is used for every field including secrets — suitable for snapshotting the
205
+ * active runtime configuration.
206
+ *
207
+ * If a field carries a description (from a Zod `.describe()` annotation) it
208
+ * is emitted as a `# comment` line immediately before the key–value pair.
209
+ *
210
+ * @param entries - All registered config entries supplied by {@link ConfigManager.Save}
211
+ * @param options - Save options
212
+ * @param options.path - Output file path; overrides `this.options.path` if provided
213
+ * @param options.useCurrentValues - When `true`, emit current live values; when `false` (default), emit registered defaults
214
+ *
215
+ * @example
216
+ * ```typescript
217
+ * const provider = new ConfigEnvironmentProvider({
218
+ * name: 'env',
219
+ * path: '.env'
220
+ * });
221
+ *
222
+ * // Generate a .env.example template (secrets blank)
223
+ * await ConfigManager.Save(provider, { path: '.env.example' });
224
+ *
225
+ * // Snapshot current runtime config (secrets included)
226
+ * await ConfigManager.Save(provider, {
227
+ * path: '.env.snapshot',
228
+ * useCurrentValues: true,
229
+ * });
230
+ * ```
231
+ */
232
+ async Save(entries, options) {
233
+ if (options !== undefined)
234
+ AssertConfigENVProviderSaveOptions(options);
235
+ const useCurrentValues = options?.useCurrentValues ?? false;
236
+ const path = options?.path ?? this.options.path;
237
+ const lines = [];
238
+ for (const entry of entries) {
239
+ if (entry.description !== undefined) {
240
+ const safeDescription = entry.description.replace(/[\r\n]/g, ' ').replace(/#/g, '(hash)');
241
+ lines.push(`# ${safeDescription}`);
242
+ }
243
+ if (!useCurrentValues && entry.isSecret) {
244
+ lines.push(`${entry.key}=`);
245
+ }
246
+ else {
247
+ lines.push(`${entry.key}=${SerializeConfigValue(entry.value)}`);
248
+ }
249
+ }
250
+ await FS.writeFile(path, lines.join('\n'), 'utf-8');
251
+ }
252
+ /**
253
+ * Creates a ConfigEnvironmentProvider instance and registers it with ConfigManager.
254
+ *
255
+ * This is a convenience factory method that combines instantiation and registration in one call.
256
+ * Unspecified options use their schema defaults (`name: 'environment'`, `path: '.env'` in cwd).
257
+ *
258
+ * @param options - Optional partial provider configuration; unspecified fields use schema defaults
259
+ * @returns A promise resolving to the registered provider instance
260
+ *
261
+ * @example
262
+ * ```typescript
263
+ * // Register with all defaults
264
+ * const provider = await ConfigEnvironmentProvider.Register();
265
+ *
266
+ * // Register with custom path
267
+ * const provider = await ConfigEnvironmentProvider.Register({
268
+ * path: '.env.production'
269
+ * });
270
+ * ```
271
+ */
272
+ static async Register(options = {}) {
273
+ const provider = new ConfigEnvironmentProvider(CONFIG_ENV_PROVIDER_OPTIONS_SCHEMA.parse(options));
274
+ await ConfigManager.RegisterProvider(provider);
275
+ return provider;
276
+ }
277
+ }
278
+ //# sourceMappingURL=environment-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environment-provider.js","sourceRoot":"","sources":["../src/environment-provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export * from './environment-provider.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './environment-provider.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC"}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@pawells/config-provider-env",
3
+ "displayName": "Config ENV Provider",
4
+ "version": "3.0.0-rc1",
5
+ "description": "Environment variable and dotenv file configuration provider for @pawells/config",
6
+ "keywords": [
7
+ "config",
8
+ "configuration",
9
+ "environment",
10
+ "environment-variables",
11
+ "dotenv",
12
+ "typescript",
13
+ "nodejs"
14
+ ],
15
+ "license": "MIT",
16
+ "author": {
17
+ "name": "Phillip Aaron Wells",
18
+ "email": "69355326+PhillipAWells@users.noreply.github.com"
19
+ },
20
+ "homepage": "https://github.com/PhillipAWells/config",
21
+ "bugs": {
22
+ "url": "https://github.com/PhillipAWells/config/issues"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/PhillipAWells/config.git",
27
+ "directory": "packages/config-provider-env"
28
+ },
29
+ "funding": {
30
+ "type": "github",
31
+ "url": "https://github.com/sponsors/PhillipAWells"
32
+ },
33
+ "engines": {
34
+ "node": ">=22.0.0"
35
+ },
36
+ "type": "module",
37
+ "types": "./dist/index.d.ts",
38
+ "exports": {
39
+ "./package.json": "./package.json",
40
+ ".": {
41
+ "local": "./src/index.ts",
42
+ "types": "./dist/index.d.ts",
43
+ "import": "./dist/index.js",
44
+ "default": "./dist/index.js"
45
+ }
46
+ },
47
+ "files": [
48
+ "dist",
49
+ "!**/*.tsbuildinfo",
50
+ "LICENSE"
51
+ ],
52
+ "sideEffects": false,
53
+ "scripts": {
54
+ "test:coverage": "yarn nx test config-provider-env -- --coverage"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "dependencies": {
60
+ "@pawells/config": "workspace:*",
61
+ "tslib": "^2.8.1",
62
+ "zod": "^4.4.3"
63
+ }
64
+ }