@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 +21 -0
- package/README.md +290 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/json-provider.d.ts +212 -0
- package/dist/json-provider.d.ts.map +1 -0
- package/dist/json-provider.js +357 -0
- package/dist/json-provider.js.map +1 -0
- package/package.json +64 -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/README.md
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# @pawells/config-provider-json
|
|
2
|
+
|
|
3
|
+
[](https://github.com/PhillipAWells/config/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@pawells/config-provider-json)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](../../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.
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|