@pawells/config-provider-json 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,290 @@
1
+ # @pawells/config-provider-json
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-json)](https://www.npmjs.com/package/@pawells/config-provider-json)
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-json` is an async configuration provider for `@pawells/config` that reads values from a JSON file and writes configuration snapshots or templates back to disk.
11
+
12
+ The file must contain a JSON object at the top level. Nested objects are flattened one level deep using `_` as the separator, so a key registered as `KEYCLOAK_HOST` is found at `{ "KEYCLOAK": { "HOST": "..." } }`. Top-level non-object values are kept at their own key unchanged. Native JSON types (numbers, booleans, arrays, `null`) are passed directly to Zod schema validation without string pre-processing.
13
+
14
+ The provider enforces a 10 MB file-size limit and rejects symlinks and path-traversal sequences (`..`) for security.
15
+
16
+ See the [workspace README](../../README.md) for an end-to-end quick start. See [CHANGELOG.md](../../CHANGELOG.md) for version history.
17
+
18
+ ## Requirements
19
+
20
+ - Node.js `>=22.0.0`
21
+ - `@pawells/config` (peer dependency, installed as a direct dependency via `workspace:*` in the monorepo)
22
+
23
+ ## Installation
24
+
25
+ ```sh
26
+ yarn add @pawells/config-provider-json @pawells/config
27
+ ```
28
+
29
+ All packages are ESM-only (`"type": "module"`).
30
+
31
+ ## Quick Start
32
+
33
+ ```typescript
34
+ import { ConfigManager, RegisterConfigSchema, Secret } from '@pawells/config';
35
+ import { ConfigJSONProvider } from '@pawells/config-provider-json';
36
+ import { z } from 'zod';
37
+
38
+ // Register providers BEFORE importing any schema modules.
39
+ const jsonProvider = await ConfigJSONProvider.Register({
40
+ name: 'json',
41
+ path: './config.json',
42
+ required: false // OK if the file does not exist yet
43
+ });
44
+
45
+ // config.json: { "APP": { "HOST": "localhost", "PORT": 3000 } }
46
+ // Flattens to: APP_HOST='localhost', APP_PORT=3000
47
+
48
+ const AppConfig = RegisterConfigSchema('App', z.object({
49
+ HOST: z.string().min(1).default('localhost'),
50
+ PORT: z.coerce.number().int().positive().default(3000),
51
+ SECRET_KEY: Secret(z.string().min(32)).default(''),
52
+ }));
53
+
54
+ const host = AppConfig.Get('HOST'); // string
55
+ const port = AppConfig.Get('PORT'); // number
56
+
57
+ // Generate config.example.json — defaults; secrets written as null.
58
+ await ConfigManager.Save(jsonProvider, { path: './config.example.json' });
59
+
60
+ // Snapshot current runtime values.
61
+ await ConfigManager.Save(jsonProvider, {
62
+ path: './config.snapshot.json',
63
+ useCurrentValues: true
64
+ });
65
+ ```
66
+
67
+ ### JSON file format
68
+
69
+ Nested objects are flattened one level deep:
70
+
71
+ ```json
72
+ {
73
+ "APP": {
74
+ "HOST": "localhost",
75
+ "PORT": 3000,
76
+ "DEBUG": false
77
+ },
78
+ "KEYCLOAK": {
79
+ "HOST": "keycloak.example.com",
80
+ "PORT": 8080
81
+ }
82
+ }
83
+ ```
84
+
85
+ Produces the configuration keys: `APP_HOST`, `APP_PORT`, `APP_DEBUG`, `KEYCLOAK_HOST`, `KEYCLOAK_PORT`.
86
+
87
+ Only one level of nesting is flattened. Deeper objects are skipped. Top-level non-object values (strings, numbers, booleans, arrays, `null`) are kept at their own key:
88
+
89
+ ```json
90
+ { "TOP_LEVEL_KEY": "value" }
91
+ ```
92
+
93
+ Produces `TOP_LEVEL_KEY='value'`.
94
+
95
+ ## API Reference
96
+
97
+ ### `ConfigJSONProvider`
98
+
99
+ An async configuration provider that extends `ConfigProvider` from `@pawells/config`.
100
+
101
+ #### `static Register(options?)`
102
+
103
+ Convenience factory: creates a `ConfigJSONProvider` instance, registers it with `ConfigManager`, and returns it.
104
+
105
+ ```typescript
106
+ static async Register(
107
+ options?: Partial<TConfigJSONProviderOptions>
108
+ ): Promise<ConfigJSONProvider>
109
+ ```
110
+
111
+ Unspecified options use schema defaults (`path: <cwd>/config.json`, `required: false`). Note: `name` has no default and must be supplied.
112
+
113
+ ```typescript
114
+ // Register with a specific path
115
+ const provider = await ConfigJSONProvider.Register({
116
+ name: 'json',
117
+ path: './config.json',
118
+ required: false
119
+ });
120
+
121
+ // Register an optional local override file
122
+ const localProvider = await ConfigJSONProvider.Register({
123
+ name: 'json-local',
124
+ path: './config.local.json',
125
+ required: false
126
+ });
127
+ ```
128
+
129
+ #### Constructor
130
+
131
+ ```typescript
132
+ new ConfigJSONProvider(options: TConfigJSONProviderOptions)
133
+ ```
134
+
135
+ After constructing, pass the instance to `ConfigManager.RegisterProvider` manually if you need the instance before registration:
136
+
137
+ ```typescript
138
+ import { ConfigManager } from '@pawells/config';
139
+ import { ConfigJSONProvider } from '@pawells/config-provider-json';
140
+
141
+ const provider = new ConfigJSONProvider({
142
+ name: 'json',
143
+ path: './config.json',
144
+ required: true
145
+ });
146
+ await ConfigManager.RegisterProvider(provider);
147
+ ```
148
+
149
+ **Throws** `ConfigError` if `options` fail schema validation (e.g. the path contains `..`).
150
+
151
+ #### `Load()`
152
+
153
+ ```typescript
154
+ async Load(): Promise<Record<string, unknown>>
155
+ ```
156
+
157
+ Reads and flattens the JSON configuration file:
158
+
159
+ 1. The parent directory of `options.path` is canonicalized via `fs.realpath` to detect symlinked ancestor directories.
160
+ 2. The file is opened with `O_NOFOLLOW` to atomically reject a symlinked final path component.
161
+ 3. Content is read and the size is checked against the 10 MB limit.
162
+ 4. The JSON is parsed; malformed JSON throws `ConfigError`.
163
+ 5. Top-level properties are flattened one level deep (see format above).
164
+ 6. Prototype-pollution keys (`__proto__`, `constructor`, `prototype`) are silently skipped.
165
+
166
+ If the file is absent (`ENOENT`) and `required` is `false`, returns `{}`. If `required` is `true`, throws `ConfigError`.
167
+
168
+ ```typescript
169
+ // config.json: { "APP": { "HOST": "localhost", "PORT": 3000, "DEBUG": false } }
170
+ const values = await provider.Load();
171
+ // → { APP_HOST: 'localhost', APP_PORT: 3000, APP_DEBUG: false }
172
+ ```
173
+
174
+ **Throws:**
175
+ - `ConfigError` — The file is a symlink (final component or parent directory).
176
+ - `ConfigError` — The file cannot be read and `required` is `true`.
177
+ - `ConfigError` — The file exceeds 10 MB.
178
+ - `ConfigError` — The file content is not valid JSON.
179
+
180
+ #### `Save(entries, options?)`
181
+
182
+ ```typescript
183
+ async Save(
184
+ entries: readonly ConfigSaveEntry[],
185
+ options?: TConfigJSONProviderSaveOptions
186
+ ): Promise<void>
187
+ ```
188
+
189
+ Writes configuration entries to a nested JSON file. Call via `ConfigManager.Save` rather than directly.
190
+
191
+ **Template mode** (`useCurrentValues: false`, the default):
192
+ - Each entry is written using its registered default value.
193
+ - Entries where `entry.isSecret` is `true` are written as `null`, indicating that a value is required but intentionally absent.
194
+ - This is suitable for generating `config.example.json` files safe to commit.
195
+
196
+ **Current-values mode** (`useCurrentValues: true`):
197
+ - All entries including secrets are written with their live resolved values.
198
+
199
+ The output structure mirrors the input structure that `Load()` flattens: entries with a section (registered via a namespace) are nested under their section key. Entries without a section are written at the top level.
200
+
201
+ Output is pretty-printed with tab indentation.
202
+
203
+ **Options:**
204
+
205
+ | Field | Type | Default | Description |
206
+ |---|---|---|---|
207
+ | `path` | `string` (optional) | `this.options.path` | Output file path; overrides the constructor path if provided. `..` sequences are rejected. |
208
+ | `useCurrentValues` | `boolean` (optional) | `false` | `false` = registered defaults, secrets as `null`; `true` = live resolved values. |
209
+
210
+ ---
211
+
212
+ ### Options types
213
+
214
+ #### `TConfigJSONProviderOptions`
215
+
216
+ ```typescript
217
+ type TConfigJSONProviderOptions = {
218
+ name: string // Required; unique identifier for diagnostics
219
+ path: string // Default: <cwd>/config.json; '..' sequences are rejected
220
+ required: boolean // Default: false; when true, missing file throws ConfigError
221
+ }
222
+ ```
223
+
224
+ Validated by `CONFIG_JSON_PROVIDER_OPTIONS_SCHEMA`.
225
+
226
+ #### `TConfigJSONProviderSaveOptions`
227
+
228
+ ```typescript
229
+ type TConfigJSONProviderSaveOptions = {
230
+ path?: string // Overrides constructor path if provided; '..' sequences are rejected
231
+ useCurrentValues?: boolean // Default: false
232
+ }
233
+ ```
234
+
235
+ Validated by `CONFIG_JSON_PROVIDER_SAVE_OPTIONS_SCHEMA`.
236
+
237
+ ---
238
+
239
+ ### Assertion and validation utilities
240
+
241
+ | Export | Description |
242
+ |---|---|
243
+ | `CONFIG_JSON_PROVIDER_OPTIONS_SCHEMA` | Zod schema for `TConfigJSONProviderOptions` |
244
+ | `AssertConfigJSONProviderOptions(options)` | Asserts conformance; throws `ZodError` otherwise |
245
+ | `ValidateConfigJSONProviderOptions(options)` | Returns `true` if valid; `false` otherwise |
246
+ | `CONFIG_JSON_PROVIDER_SAVE_OPTIONS_SCHEMA` | Zod schema for `TConfigJSONProviderSaveOptions` |
247
+ | `AssertConfigJSONProviderSaveOptions(options)` | Asserts conformance; throws `ZodError` otherwise |
248
+ | `ValidateConfigJSONProviderSaveOptions(options)` | Returns `true` if valid; `false` otherwise |
249
+ | `CONFIG_JSON_PROVIDER_PATH_SCHEMA` | Zod schema for an individual path value |
250
+
251
+ ---
252
+
253
+ ### Error handling
254
+
255
+ ```typescript
256
+ import { ConfigError } from '@pawells/config';
257
+ import { ConfigJSONProvider } from '@pawells/config-provider-json';
258
+
259
+ try {
260
+ const provider = new ConfigJSONProvider({
261
+ name: 'json',
262
+ path: './config.json',
263
+ required: true
264
+ });
265
+ await ConfigManager.RegisterProvider(provider);
266
+ } catch (error) {
267
+ if (error instanceof ConfigError) {
268
+ // Symlink detected, path traversal, JSON parse failure, file not found (required),
269
+ // or size limit exceeded
270
+ console.error('Configuration error:', error.message);
271
+ } else {
272
+ // File system permission error or other I/O error
273
+ console.error('I/O error:', error);
274
+ }
275
+ }
276
+ ```
277
+
278
+ ---
279
+
280
+ ### Security
281
+
282
+ - **Symlink rejection** — The final path component is opened with `O_NOFOLLOW`. Parent directories are canonicalized via `fs.realpath`. Either form of symlink throws `ConfigError`.
283
+ - **Path-traversal protection** — Any path containing `..` is rejected by the options schema at construction time and at save time.
284
+ - **Prototype-pollution protection** — Keys `__proto__`, `constructor`, and `prototype` are silently skipped in both `Load()` and `Save()`.
285
+ - **Size limit** — Files larger than 10 MB (byte-accurate) throw `ConfigError` before parsing.
286
+ - **No string coercion** — Native JSON types are passed directly to Zod; no implicit string-to-number conversion occurs.
287
+
288
+ ## License
289
+
290
+ MIT — See [LICENSE](../../LICENSE) for details.
@@ -0,0 +1,2 @@
1
+ export * from './json-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,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './json-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,oBAAoB,CAAC"}
@@ -0,0 +1,212 @@
1
+ import { z } from 'zod/v4';
2
+ import { ConfigProvider, type ConfigSaveEntry } from '@pawells/config';
3
+ /**
4
+ * Zod schema for validating JSON provider file paths.
5
+ *
6
+ * Enforces non-empty string with no path traversal sequences ("..").
7
+ * Defaults to `./config.json` in the current working directory.
8
+ */
9
+ export declare const CONFIG_JSON_PROVIDER_PATH_SCHEMA: z.ZodDefault<z.ZodString>;
10
+ /**
11
+ * Zod schema for validating ConfigJSONProvider constructor options.
12
+ *
13
+ * Extends the base provider options with path and required flag.
14
+ */
15
+ export declare const CONFIG_JSON_PROVIDER_OPTIONS_SCHEMA: z.ZodObject<{
16
+ name: z.ZodString;
17
+ path: z.ZodDefault<z.ZodString>;
18
+ required: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
19
+ }, z.core.$strip>;
20
+ /**
21
+ * Type-safe representation of ConfigJSONProvider options.
22
+ */
23
+ export type TConfigJSONProviderOptions = z.infer<typeof CONFIG_JSON_PROVIDER_OPTIONS_SCHEMA>;
24
+ /**
25
+ * Assert that a value conforms to TConfigJSONProviderOptions.
26
+ *
27
+ * @param options - The value to validate.
28
+ * @throws {ZodError} If validation fails.
29
+ */
30
+ export declare function AssertConfigJSONProviderOptions(options: unknown): asserts options is TConfigJSONProviderOptions;
31
+ /**
32
+ * Validate whether a value conforms to TConfigJSONProviderOptions.
33
+ *
34
+ * @param options - The value to validate.
35
+ * @returns `true` if validation succeeds; `false` otherwise.
36
+ */
37
+ export declare function ValidateConfigJSONProviderOptions(options: unknown): boolean;
38
+ /**
39
+ * Zod schema for validating ConfigJSONProvider Save options.
40
+ *
41
+ * Allows optional path override and value-selection mode.
42
+ */
43
+ export declare const CONFIG_JSON_PROVIDER_SAVE_OPTIONS_SCHEMA: z.ZodObject<{
44
+ useCurrentValues: z.ZodOptional<z.ZodBoolean>;
45
+ path: z.ZodOptional<z.ZodDefault<z.ZodString>>;
46
+ }, z.core.$strip>;
47
+ /**
48
+ * Type-safe representation of ConfigJSONProvider Save options.
49
+ */
50
+ export type TConfigJSONProviderSaveOptions = z.infer<typeof CONFIG_JSON_PROVIDER_SAVE_OPTIONS_SCHEMA>;
51
+ /**
52
+ * Assert that a value conforms to TConfigJSONProviderSaveOptions.
53
+ *
54
+ * @param options - The value to validate.
55
+ * @throws {ZodError} If validation fails.
56
+ */
57
+ export declare function AssertConfigJSONProviderSaveOptions(options: unknown): asserts options is TConfigJSONProviderSaveOptions;
58
+ /**
59
+ * Validate whether a value conforms to TConfigJSONProviderSaveOptions.
60
+ *
61
+ * @param options - The value to validate.
62
+ * @returns `true` if validation succeeds; `false` otherwise.
63
+ */
64
+ export declare function ValidateConfigJSONProviderSaveOptions(options: unknown): boolean;
65
+ /**
66
+ * Configuration provider that reads values from a JSON file.
67
+ *
68
+ * The file must contain a JSON object at the top level. Nested objects are
69
+ * flattened using the pattern `SECTION_FIELD` so that a config registered
70
+ * as `KEYCLOAK_HOST` is found under `{ "KEYCLOAK": { "HOST": "..." } }`.
71
+ * Top-level non-object values are kept under their own key unchanged.
72
+ * Only one level of nesting is flattened; deeper nesting is skipped.
73
+ *
74
+ * Native JSON types (numbers, booleans, arrays, `null`) are passed directly
75
+ * to Zod schema validation without any string pre-processing.
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * // config.json: { "KEYCLOAK": { "HOST": "localhost", "PORT": 8080 } }
80
+ * const provider = new ConfigJSONProvider({
81
+ * name: 'json',
82
+ * path: './config.json',
83
+ * required: true
84
+ * });
85
+ * await ConfigManager.RegisterProvider(provider);
86
+ * // Registers: KEYCLOAK_HOST='localhost', KEYCLOAK_PORT=8080
87
+ *
88
+ * // Optional file — no throw if missing
89
+ * const optProvider = new ConfigJSONProvider({
90
+ * name: 'json-local',
91
+ * path: './config.local.json',
92
+ * required: false
93
+ * });
94
+ * await ConfigManager.RegisterProvider(optProvider);
95
+ * ```
96
+ */
97
+ export declare class ConfigJSONProvider extends ConfigProvider<TConfigJSONProviderOptions, Record<string, unknown>, TConfigJSONProviderSaveOptions> {
98
+ /**
99
+ * Construct a JSON configuration provider.
100
+ *
101
+ * @param options - Provider configuration options.
102
+ * @param options.name - Unique name identifying this provider instance (used for diagnostics).
103
+ * @param options.path - Path to the JSON configuration file; defaults to `./config.json`.
104
+ * @param options.required - When `true` (default `false`), a missing or unreadable file throws an error; when `false`, returns `{}`.
105
+ * @throws {ConfigError} If options fail schema validation or path contains path traversal sequences (..).
106
+ */
107
+ constructor(options: TConfigJSONProviderOptions);
108
+ /**
109
+ * Load and flatten the JSON configuration file asynchronously.
110
+ *
111
+ * Reads the file at `options.path` asynchronously, parses it as JSON, and flattens
112
+ * nested objects (one level deep) into fully-qualified keys using `_` as the separator.
113
+ * If the file cannot be read and `options.required` is `false`, returns `{}`.
114
+ *
115
+ * Symlink paths are not permitted and will throw a ConfigError. Both the final path
116
+ * component and parent directories are validated: the final component is checked via
117
+ * O_NOFOLLOW, and parent directories are canonicalized via realpath to reject symlinked
118
+ * ancestor directories.
119
+ *
120
+ * Input size (and thus practical nesting depth) is bounded by the existing 10 MB file-size limit.
121
+ *
122
+ * @returns A flat record of fully-qualified config key names to their native-typed values.
123
+ * @throws {ConfigError} If the file is a symlink (final component or parent directory).
124
+ * @throws {ConfigError} If the file cannot be read and `options.required` is `true`.
125
+ * @throws {ConfigError} When the config file exceeds 10MB.
126
+ * @throws {ConfigError} If the loaded JSON fails to parse.
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * // config.json:
131
+ * // { "APP": { "HOST": "localhost", "PORT": 3000, "DEBUG": false } }
132
+ * const provider = new ConfigJSONProvider({
133
+ * name: 'json',
134
+ * path: './config.json',
135
+ * required: true
136
+ * });
137
+ * const values = await provider.Load();
138
+ * // → { APP_HOST: 'localhost', APP_PORT: 3000, APP_DEBUG: false }
139
+ * ```
140
+ */
141
+ Load(): Promise<Record<string, unknown>>;
142
+ /**
143
+ * Save configuration values to a nested JSON file asynchronously.
144
+ *
145
+ * Entries that belong to a registered namespace (i.e. `entry.section` is
146
+ * non-empty) are grouped under their section key, reproducing the nested
147
+ * structure that {@link Load} flattens on read:
148
+ * `KEYCLOAK_HOST` → `{ "KEYCLOAK": { "HOST": "..." } }`.
149
+ *
150
+ * Entries with no section (registered directly with {@link ConfigManager.Register}
151
+ * rather than via {@link ConfigManager.RegisterNamespace}) are written as top-level keys.
152
+ *
153
+ * In template mode (`useCurrentValues: false`, the default), secret fields
154
+ * (`entry.isSecret === true`) are written as `null`, clearly indicating that
155
+ * a value is required but intentionally absent — suitable for committed
156
+ * `config.example.json` template files.
157
+ *
158
+ * In current-values mode (`useCurrentValues: true`), all values including
159
+ * secrets are written as-is, capturing the current live state.
160
+ *
161
+ * The output file is pretty-printed with tab indentation.
162
+ *
163
+ * @param entries - All registered config entries supplied by {@link ConfigManager.Save}.
164
+ * @param options - Save options including output file path and value-selection mode.
165
+ * @param options.path - Output file path; defaults to `this.options.path` if not provided.
166
+ * @param options.useCurrentValues - When `true`, write current live values; when `false` (default), write defaults and `null` for secrets.
167
+ * @throws {ConfigError} If options validation fails, JSON serialization fails, or file write fails.
168
+ *
169
+ * @example
170
+ * ```typescript
171
+ * // Generate config.example.json (secrets as null, for committing)
172
+ * const provider = new ConfigJSONProvider({
173
+ * name: 'json',
174
+ * path: './config.json'
175
+ * });
176
+ * await provider.Save(entries, { path: './config.example.json' });
177
+ *
178
+ * // Snapshot current runtime config (with live secrets)
179
+ * await provider.Save(entries, {
180
+ * path: './config.snapshot.json',
181
+ * useCurrentValues: true
182
+ * });
183
+ * ```
184
+ */
185
+ Save(entries: readonly ConfigSaveEntry[], options?: TConfigJSONProviderSaveOptions): Promise<void>;
186
+ /**
187
+ * Create and register a ConfigJSONProvider with {@link ConfigManager}.
188
+ *
189
+ * A convenience factory that combines construction and registration.
190
+ * The provider is immediately registered with the global ConfigManager
191
+ * and returned for optional use.
192
+ *
193
+ * @param options - Partial provider options; missing fields use schema defaults.
194
+ * @returns A promise that resolves to the created and registered provider instance.
195
+ * @throws {ZodError} If options fail schema validation.
196
+ *
197
+ * @example
198
+ * ```typescript
199
+ * // Register with defaults (./config.json, required: false)
200
+ * await ConfigJSONProvider.Register({ name: 'json' });
201
+ *
202
+ * // Register with custom path
203
+ * const provider = await ConfigJSONProvider.Register({
204
+ * name: 'json-local',
205
+ * path: './config.local.json',
206
+ * required: false
207
+ * });
208
+ * ```
209
+ */
210
+ static Register(options?: Partial<TConfigJSONProviderOptions>): Promise<ConfigJSONProvider>;
211
+ }
212
+ //# sourceMappingURL=json-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-provider.d.ts","sourceRoot":"","sources":["../src/json-provider.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AAC3B,OAAO,EAAmG,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAUxK;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,2BAI1C,CAAC;AAEJ;;;;GAIG;AACH,eAAO,MAAM,mCAAmC;;;;iBAG9C,CAAC;AAEH;;GAEG;AACH,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mCAAmC,CAAC,CAAC;AAE7F;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,0BAA0B,CAE/G;AAED;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAQ3E;AAED;;;;GAIG;AACH,eAAO,MAAM,wCAAwC;;;iBAEnD,CAAC;AAEH;;GAEG;AACH,MAAM,MAAM,8BAA8B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wCAAwC,CAAC,CAAC;AAEtG;;;;;GAKG;AACH,wBAAgB,mCAAmC,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,8BAA8B,CAEvH;AAED;;;;;GAKG;AACH,wBAAgB,qCAAqC,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAQ/E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,qBAAa,kBAAmB,SAAQ,cAAc,CACrD,0BAA0B,EAC1B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvB,8BAA8B,CAC9B;IACA;;;;;;;;OAQG;gBACS,OAAO,EAAE,0BAA0B;IAK/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;IACU,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAsFrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA0CG;IACU,IAAI,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,EAAE,OAAO,CAAC,EAAE,8BAA8B,GAAG,OAAO,CAAC,IAAI,CAAC;IAkD/G;;;;;;;;;;;;;;;;;;;;;;;OAuBG;WACiB,QAAQ,CAAC,OAAO,GAAE,OAAO,CAAC,0BAA0B,CAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAK5G"}
@@ -0,0 +1,357 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import { constants as fsConstants } from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { z } from 'zod/v4';
5
+ import { CONFIG_PROVIDER_OPTIONS_SCHEMA, CONFIG_PROVIDER_SAVE_OPTIONS_SCHEMA, ConfigError, ConfigManager, ConfigProvider } from '@pawells/config';
6
+ import { GetErrorMessage } from '@pawells/typescript-common';
7
+ /**
8
+ * Set of keys that pose prototype pollution risks and must be filtered.
9
+ *
10
+ * @internal
11
+ */
12
+ const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
13
+ /**
14
+ * Zod schema for validating JSON provider file paths.
15
+ *
16
+ * Enforces non-empty string with no path traversal sequences ("..").
17
+ * Defaults to `./config.json` in the current working directory.
18
+ */
19
+ export const CONFIG_JSON_PROVIDER_PATH_SCHEMA = z.string().min(1)
20
+ .default(path.join(process.cwd(), 'config.json'))
21
+ .refine((filePath) => !path.normalize(filePath).includes('..'), {
22
+ message: 'Path traversal sequences ("..") are not permitted.'
23
+ });
24
+ /**
25
+ * Zod schema for validating ConfigJSONProvider constructor options.
26
+ *
27
+ * Extends the base provider options with path and required flag.
28
+ */
29
+ export const CONFIG_JSON_PROVIDER_OPTIONS_SCHEMA = CONFIG_PROVIDER_OPTIONS_SCHEMA.extend({
30
+ path: CONFIG_JSON_PROVIDER_PATH_SCHEMA,
31
+ required: z.boolean().optional().default(false)
32
+ });
33
+ /**
34
+ * Assert that a value conforms to TConfigJSONProviderOptions.
35
+ *
36
+ * @param options - The value to validate.
37
+ * @throws {ZodError} If validation fails.
38
+ */
39
+ export function AssertConfigJSONProviderOptions(options) {
40
+ CONFIG_JSON_PROVIDER_OPTIONS_SCHEMA.parse(options);
41
+ }
42
+ /**
43
+ * Validate whether a value conforms to TConfigJSONProviderOptions.
44
+ *
45
+ * @param options - The value to validate.
46
+ * @returns `true` if validation succeeds; `false` otherwise.
47
+ */
48
+ export function ValidateConfigJSONProviderOptions(options) {
49
+ try {
50
+ CONFIG_JSON_PROVIDER_OPTIONS_SCHEMA.parse(options);
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ /**
58
+ * Zod schema for validating ConfigJSONProvider Save options.
59
+ *
60
+ * Allows optional path override and value-selection mode.
61
+ */
62
+ export const CONFIG_JSON_PROVIDER_SAVE_OPTIONS_SCHEMA = CONFIG_PROVIDER_SAVE_OPTIONS_SCHEMA.extend({
63
+ path: CONFIG_JSON_PROVIDER_PATH_SCHEMA.optional()
64
+ });
65
+ /**
66
+ * Assert that a value conforms to TConfigJSONProviderSaveOptions.
67
+ *
68
+ * @param options - The value to validate.
69
+ * @throws {ZodError} If validation fails.
70
+ */
71
+ export function AssertConfigJSONProviderSaveOptions(options) {
72
+ CONFIG_JSON_PROVIDER_SAVE_OPTIONS_SCHEMA.parse(options);
73
+ }
74
+ /**
75
+ * Validate whether a value conforms to TConfigJSONProviderSaveOptions.
76
+ *
77
+ * @param options - The value to validate.
78
+ * @returns `true` if validation succeeds; `false` otherwise.
79
+ */
80
+ export function ValidateConfigJSONProviderSaveOptions(options) {
81
+ try {
82
+ CONFIG_JSON_PROVIDER_SAVE_OPTIONS_SCHEMA.parse(options);
83
+ return true;
84
+ }
85
+ catch {
86
+ return false;
87
+ }
88
+ }
89
+ /**
90
+ * Configuration provider that reads values from a JSON file.
91
+ *
92
+ * The file must contain a JSON object at the top level. Nested objects are
93
+ * flattened using the pattern `SECTION_FIELD` so that a config registered
94
+ * as `KEYCLOAK_HOST` is found under `{ "KEYCLOAK": { "HOST": "..." } }`.
95
+ * Top-level non-object values are kept under their own key unchanged.
96
+ * Only one level of nesting is flattened; deeper nesting is skipped.
97
+ *
98
+ * Native JSON types (numbers, booleans, arrays, `null`) are passed directly
99
+ * to Zod schema validation without any string pre-processing.
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * // config.json: { "KEYCLOAK": { "HOST": "localhost", "PORT": 8080 } }
104
+ * const provider = new ConfigJSONProvider({
105
+ * name: 'json',
106
+ * path: './config.json',
107
+ * required: true
108
+ * });
109
+ * await ConfigManager.RegisterProvider(provider);
110
+ * // Registers: KEYCLOAK_HOST='localhost', KEYCLOAK_PORT=8080
111
+ *
112
+ * // Optional file — no throw if missing
113
+ * const optProvider = new ConfigJSONProvider({
114
+ * name: 'json-local',
115
+ * path: './config.local.json',
116
+ * required: false
117
+ * });
118
+ * await ConfigManager.RegisterProvider(optProvider);
119
+ * ```
120
+ */
121
+ export class ConfigJSONProvider extends ConfigProvider {
122
+ /**
123
+ * Construct a JSON configuration provider.
124
+ *
125
+ * @param options - Provider configuration options.
126
+ * @param options.name - Unique name identifying this provider instance (used for diagnostics).
127
+ * @param options.path - Path to the JSON configuration file; defaults to `./config.json`.
128
+ * @param options.required - When `true` (default `false`), a missing or unreadable file throws an error; when `false`, returns `{}`.
129
+ * @throws {ConfigError} If options fail schema validation or path contains path traversal sequences (..).
130
+ */
131
+ constructor(options) {
132
+ AssertConfigJSONProviderOptions(options);
133
+ super(options);
134
+ }
135
+ /**
136
+ * Load and flatten the JSON configuration file asynchronously.
137
+ *
138
+ * Reads the file at `options.path` asynchronously, parses it as JSON, and flattens
139
+ * nested objects (one level deep) into fully-qualified keys using `_` as the separator.
140
+ * If the file cannot be read and `options.required` is `false`, returns `{}`.
141
+ *
142
+ * Symlink paths are not permitted and will throw a ConfigError. Both the final path
143
+ * component and parent directories are validated: the final component is checked via
144
+ * O_NOFOLLOW, and parent directories are canonicalized via realpath to reject symlinked
145
+ * ancestor directories.
146
+ *
147
+ * Input size (and thus practical nesting depth) is bounded by the existing 10 MB file-size limit.
148
+ *
149
+ * @returns A flat record of fully-qualified config key names to their native-typed values.
150
+ * @throws {ConfigError} If the file is a symlink (final component or parent directory).
151
+ * @throws {ConfigError} If the file cannot be read and `options.required` is `true`.
152
+ * @throws {ConfigError} When the config file exceeds 10MB.
153
+ * @throws {ConfigError} If the loaded JSON fails to parse.
154
+ *
155
+ * @example
156
+ * ```typescript
157
+ * // config.json:
158
+ * // { "APP": { "HOST": "localhost", "PORT": 3000, "DEBUG": false } }
159
+ * const provider = new ConfigJSONProvider({
160
+ * name: 'json',
161
+ * path: './config.json',
162
+ * required: true
163
+ * });
164
+ * const values = await provider.Load();
165
+ * // → { APP_HOST: 'localhost', APP_PORT: 3000, APP_DEBUG: false }
166
+ * ```
167
+ */
168
+ async Load() {
169
+ try {
170
+ // Canonicalize parent directory to detect symlinked ancestors
171
+ const realParentDir = await fs.realpath(path.dirname(this.options.path));
172
+ const fileName = path.basename(this.options.path);
173
+ const resolvedPath = path.join(realParentDir, fileName);
174
+ // Open file with O_NOFOLLOW to reject symlink final component atomically
175
+ const filehandle = await fs.open(resolvedPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
176
+ let buffer;
177
+ try {
178
+ buffer = await filehandle.readFile();
179
+ }
180
+ finally {
181
+ await filehandle.close();
182
+ }
183
+ // Use byte-accurate size check: 10 MB = 10 * 1024 * 1024 bytes
184
+ if (buffer.byteLength > 10 * 1024 * 1024) {
185
+ throw new ConfigError('Config file exceeds 10MB limit');
186
+ }
187
+ const content = buffer.toString('utf-8');
188
+ let parsed;
189
+ try {
190
+ parsed = JSON.parse(content);
191
+ }
192
+ catch (error) {
193
+ throw new ConfigError('Failed to parse JSON config file: ' + (error instanceof Error ? error.message : String(error)), {
194
+ cause: error instanceof Error ? error : undefined
195
+ });
196
+ }
197
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
198
+ return {};
199
+ const result = {};
200
+ for (const [topKey, value] of Object.entries(parsed)) {
201
+ if (DANGEROUS_KEYS.has(topKey))
202
+ continue;
203
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
204
+ for (const [fieldKey, fieldValue] of Object.entries(value)) {
205
+ if (DANGEROUS_KEYS.has(fieldKey))
206
+ continue;
207
+ result[`${topKey}_${fieldKey}`] = fieldValue;
208
+ }
209
+ }
210
+ else {
211
+ result[topKey] = value;
212
+ }
213
+ }
214
+ return result;
215
+ }
216
+ catch (error) {
217
+ // If it's already a ConfigError, rethrow it
218
+ if (error instanceof ConfigError) {
219
+ throw error;
220
+ }
221
+ // Check error codes from fs.open and realpath
222
+ const errorCode = error instanceof Error ? error.code : undefined;
223
+ // ELOOP (Linux) or ENOTDIR (macOS) indicate symlink in final component
224
+ if (errorCode === 'ELOOP' || errorCode === 'ENOTDIR') {
225
+ throw new ConfigError('Symlink paths are not permitted.');
226
+ }
227
+ // Check if it's ENOENT (file not found) or parent directory not found
228
+ const isNotFound = errorCode === 'ENOENT';
229
+ // If file is missing and not required, return empty config
230
+ if (isNotFound && !this.options.required) {
231
+ return {};
232
+ }
233
+ // If file is missing and required, wrap error in ConfigError (sanitize message)
234
+ if (isNotFound && this.options.required) {
235
+ throw new ConfigError('Config file not found.', {
236
+ cause: error instanceof Error ? error : undefined
237
+ });
238
+ }
239
+ // All other errors (permission, etc.) are rethrown
240
+ throw error;
241
+ }
242
+ }
243
+ /**
244
+ * Save configuration values to a nested JSON file asynchronously.
245
+ *
246
+ * Entries that belong to a registered namespace (i.e. `entry.section` is
247
+ * non-empty) are grouped under their section key, reproducing the nested
248
+ * structure that {@link Load} flattens on read:
249
+ * `KEYCLOAK_HOST` → `{ "KEYCLOAK": { "HOST": "..." } }`.
250
+ *
251
+ * Entries with no section (registered directly with {@link ConfigManager.Register}
252
+ * rather than via {@link ConfigManager.RegisterNamespace}) are written as top-level keys.
253
+ *
254
+ * In template mode (`useCurrentValues: false`, the default), secret fields
255
+ * (`entry.isSecret === true`) are written as `null`, clearly indicating that
256
+ * a value is required but intentionally absent — suitable for committed
257
+ * `config.example.json` template files.
258
+ *
259
+ * In current-values mode (`useCurrentValues: true`), all values including
260
+ * secrets are written as-is, capturing the current live state.
261
+ *
262
+ * The output file is pretty-printed with tab indentation.
263
+ *
264
+ * @param entries - All registered config entries supplied by {@link ConfigManager.Save}.
265
+ * @param options - Save options including output file path and value-selection mode.
266
+ * @param options.path - Output file path; defaults to `this.options.path` if not provided.
267
+ * @param options.useCurrentValues - When `true`, write current live values; when `false` (default), write defaults and `null` for secrets.
268
+ * @throws {ConfigError} If options validation fails, JSON serialization fails, or file write fails.
269
+ *
270
+ * @example
271
+ * ```typescript
272
+ * // Generate config.example.json (secrets as null, for committing)
273
+ * const provider = new ConfigJSONProvider({
274
+ * name: 'json',
275
+ * path: './config.json'
276
+ * });
277
+ * await provider.Save(entries, { path: './config.example.json' });
278
+ *
279
+ * // Snapshot current runtime config (with live secrets)
280
+ * await provider.Save(entries, {
281
+ * path: './config.snapshot.json',
282
+ * useCurrentValues: true
283
+ * });
284
+ * ```
285
+ */
286
+ async Save(entries, options) {
287
+ // Validate options and wrap ZodError in ConfigError
288
+ try {
289
+ AssertConfigJSONProviderSaveOptions(options);
290
+ }
291
+ catch (error) {
292
+ throw new ConfigError('Invalid save options.', {
293
+ cause: error instanceof Error ? error : undefined
294
+ });
295
+ }
296
+ const filePath = options.path ?? this.options.path;
297
+ const useCurrentValues = options.useCurrentValues ?? false;
298
+ const result = {};
299
+ for (const entry of entries) {
300
+ const value = (!useCurrentValues && entry.isSecret) ? null : entry.value;
301
+ if (DANGEROUS_KEYS.has(entry.section) || DANGEROUS_KEYS.has(entry.field)) {
302
+ continue; // Skip prototype-pollution-risk keys
303
+ }
304
+ if (entry.section !== '') {
305
+ // Guard against clobbering primitives with objects
306
+ if (result[entry.section] !== undefined && (typeof result[entry.section] !== 'object' || result[entry.section] === null)) {
307
+ continue; // Skip this entry to avoid clobbering
308
+ }
309
+ if (typeof result[entry.section] !== 'object' || result[entry.section] === null) {
310
+ result[entry.section] = {};
311
+ }
312
+ result[entry.section][entry.field] = value;
313
+ }
314
+ else {
315
+ result[entry.field] = value;
316
+ }
317
+ }
318
+ let jsonOutput;
319
+ try {
320
+ jsonOutput = JSON.stringify(result, null, '\t');
321
+ }
322
+ catch (error) {
323
+ throw new ConfigError(`Failed to serialize configuration to JSON: ${GetErrorMessage(error)}`, { cause: error instanceof Error ? error : undefined });
324
+ }
325
+ await fs.writeFile(filePath, jsonOutput, 'utf-8');
326
+ }
327
+ /**
328
+ * Create and register a ConfigJSONProvider with {@link ConfigManager}.
329
+ *
330
+ * A convenience factory that combines construction and registration.
331
+ * The provider is immediately registered with the global ConfigManager
332
+ * and returned for optional use.
333
+ *
334
+ * @param options - Partial provider options; missing fields use schema defaults.
335
+ * @returns A promise that resolves to the created and registered provider instance.
336
+ * @throws {ZodError} If options fail schema validation.
337
+ *
338
+ * @example
339
+ * ```typescript
340
+ * // Register with defaults (./config.json, required: false)
341
+ * await ConfigJSONProvider.Register({ name: 'json' });
342
+ *
343
+ * // Register with custom path
344
+ * const provider = await ConfigJSONProvider.Register({
345
+ * name: 'json-local',
346
+ * path: './config.local.json',
347
+ * required: false
348
+ * });
349
+ * ```
350
+ */
351
+ static async Register(options = {}) {
352
+ const provider = new ConfigJSONProvider(CONFIG_JSON_PROVIDER_OPTIONS_SCHEMA.parse(options));
353
+ await ConfigManager.RegisterProvider(provider);
354
+ return provider;
355
+ }
356
+ }
357
+ //# sourceMappingURL=json-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-provider.js","sourceRoot":"","sources":["../src/json-provider.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@pawells/config-provider-json",
3
+ "displayName": "Config JSON Provider",
4
+ "version": "3.0.0-rc1",
5
+ "description": "JSON file configuration provider for @pawells/config",
6
+ "keywords": [
7
+ "config",
8
+ "configuration",
9
+ "json",
10
+ "file",
11
+ "typescript",
12
+ "nodejs"
13
+ ],
14
+ "license": "MIT",
15
+ "author": {
16
+ "name": "Phillip Aaron Wells",
17
+ "email": "69355326+PhillipAWells@users.noreply.github.com"
18
+ },
19
+ "homepage": "https://github.com/PhillipAWells/config",
20
+ "bugs": {
21
+ "url": "https://github.com/PhillipAWells/config/issues"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/PhillipAWells/config.git",
26
+ "directory": "packages/config-provider-json"
27
+ },
28
+ "funding": {
29
+ "type": "github",
30
+ "url": "https://github.com/sponsors/PhillipAWells"
31
+ },
32
+ "engines": {
33
+ "node": ">=22.0.0"
34
+ },
35
+ "type": "module",
36
+ "types": "./dist/index.d.ts",
37
+ "exports": {
38
+ "./package.json": "./package.json",
39
+ ".": {
40
+ "local": "./src/index.ts",
41
+ "types": "./dist/index.d.ts",
42
+ "import": "./dist/index.js",
43
+ "default": "./dist/index.js"
44
+ }
45
+ },
46
+ "files": [
47
+ "dist",
48
+ "!**/*.tsbuildinfo",
49
+ "LICENSE"
50
+ ],
51
+ "sideEffects": false,
52
+ "scripts": {
53
+ "test:coverage": "yarn nx test config-provider-json -- --coverage"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "dependencies": {
59
+ "@pawells/config": "workspace:*",
60
+ "@pawells/typescript-common": "^3.0.5",
61
+ "tslib": "^2.8.1",
62
+ "zod": "^4.4.3"
63
+ }
64
+ }