@zudojs/config 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/dist/configLoader/configLoader.core.js +6 -2
- package/dist/configManager/configManager.core.js +14 -5
- package/dist/configSource/configSource.core.d.ts +10 -0
- package/dist/configSource/configSource.core.js +11 -1
- package/dist/configStore/configStore.core.js +7 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -58,6 +58,16 @@ await `manager.ready()` before reading values.
|
|
|
58
58
|
A source provides values plus a priority. Higher priority wins; equal
|
|
59
59
|
priorities are applied in registration order (last registered wins).
|
|
60
60
|
|
|
61
|
+
A source that declares no priority gets `DEFAULT_CONFIG_SOURCE_PRIORITY`
|
|
62
|
+
(`-1`), which is strictly below the priority `initialValues` is seeded
|
|
63
|
+
at (`0`), so an undeclared source layers UNDER the values a manager was
|
|
64
|
+
seeded with. Declare `priority: 0` (or higher) on the source when it
|
|
65
|
+
should override them.
|
|
66
|
+
|
|
67
|
+
Sources are deduplicated by name — the first occurrence of a name wins,
|
|
68
|
+
both when they are passed to the constructor and through `addSource()`
|
|
69
|
+
(which rejects a duplicate outright).
|
|
70
|
+
|
|
61
71
|
- `createDefaultsConfigSource(values, name?)` — low-priority defaults.
|
|
62
72
|
- `createMemoryConfigSource(values, { name, priority?, optional? })` —
|
|
63
73
|
in-memory values.
|
|
@@ -147,12 +157,21 @@ entry is redacted). Individual values can be validated with
|
|
|
147
157
|
including secrets.
|
|
148
158
|
- `manager.toSafeObject()` / `store.toSafeObject()` replace sensitive
|
|
149
159
|
values with `"[REDACTED]"` — use these for logging and diagnostics.
|
|
160
|
+
- An entry is marked sensitive automatically by `ConfigStore.set()`, so
|
|
161
|
+
the same key is redacted however it was written: from a source, from
|
|
162
|
+
`initialValues`, from `set()` / `setMany()` / `replace()`, or from
|
|
163
|
+
`manager.set()`. Detection is `isSensitiveConfigEntry` — the key name
|
|
164
|
+
(`password`, `secret`, `token`, `*_key`, `auth`, `dsn`,
|
|
165
|
+
`database_url`, …) or a value that is a URL with embedded
|
|
166
|
+
`user:password@` credentials. Pass `sensitive: false` explicitly to
|
|
167
|
+
opt a key out.
|
|
150
168
|
|
|
151
169
|
## Reloading
|
|
152
170
|
|
|
153
171
|
`manager.reload()` re-runs all sources. Values set at runtime via
|
|
154
172
|
`manager.set()` and `initialValues` survive loads; sources only
|
|
155
|
-
overwrite entries at equal or higher priority
|
|
173
|
+
overwrite entries at equal or higher priority, and a source that
|
|
174
|
+
declares no priority ranks below both. A reload rebuilds every
|
|
156
175
|
source-provided value from scratch, so a value a source no longer
|
|
157
176
|
provides disappears (letting a lower-priority default show through), and
|
|
158
177
|
the store is only updated once every source has loaded — a failing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cloneConfigValue } from "../configValue/configValue.core.js";
|
|
2
2
|
import { createConfigEntry } from "../configEntry/configEntry.type.js";
|
|
3
|
-
import { loadConfigSourceStrict, sortConfigSources, } from "../configSource/configSource.core.js";
|
|
3
|
+
import { deduplicateConfigSources, loadConfigSourceStrict, sortConfigSources, } from "../configSource/configSource.core.js";
|
|
4
4
|
import { isSensitiveConfigEntry } from "../configSource/configSource.sensitive.js";
|
|
5
5
|
import { commitReloadStaging, createReloadStaging, } from "./configLoader.reload.js";
|
|
6
6
|
import { createConfigStore } from "../configStore/configStore.factory.js";
|
|
@@ -23,7 +23,11 @@ export class ConfigLoader {
|
|
|
23
23
|
disposed = false;
|
|
24
24
|
lastResult;
|
|
25
25
|
constructor(options = {}) {
|
|
26
|
-
|
|
26
|
+
// addSource() rejects a name that is already registered, but the
|
|
27
|
+
// constructor used to accept duplicates silently — and because a
|
|
28
|
+
// later duplicate overwrote the earlier one, the LAST occurrence
|
|
29
|
+
// won, the opposite of the documented "first occurrence wins".
|
|
30
|
+
this.sources = [...deduplicateConfigSources(options.sources ?? [])];
|
|
27
31
|
this.context = options.context ?? {};
|
|
28
32
|
this.store =
|
|
29
33
|
options.store ??
|
|
@@ -38,15 +38,21 @@ export class ConfigManager {
|
|
|
38
38
|
this.store =
|
|
39
39
|
suppliedStore ??
|
|
40
40
|
createConfigStore({
|
|
41
|
-
initialValues: options.initialValues,
|
|
42
41
|
freeze: options.freeze ?? true,
|
|
43
42
|
});
|
|
44
43
|
// initialValues used to be wired ONLY into a store the manager
|
|
45
44
|
// created itself: passing `store` or `loader` silently discarded
|
|
46
|
-
// them.
|
|
47
|
-
|
|
45
|
+
// them. Both paths are seeded here instead, at the baseline
|
|
46
|
+
// priority 0. A source that declares no priority now ranks below
|
|
47
|
+
// that (DEFAULT_CONFIG_SOURCE_PRIORITY), so it can no longer wipe
|
|
48
|
+
// the seeds; a source that declares a priority of 0 or above
|
|
49
|
+
// still overrides them.
|
|
50
|
+
if (options.initialValues) {
|
|
48
51
|
for (const [key, value] of Object.entries(options.initialValues)) {
|
|
49
|
-
|
|
52
|
+
this.store.set(key, value, {
|
|
53
|
+
source: "initialValues",
|
|
54
|
+
priority: 0,
|
|
55
|
+
});
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
58
|
this.name = options.name ?? "config";
|
|
@@ -295,7 +301,10 @@ export class ConfigManager {
|
|
|
295
301
|
return this.store.set(key, value, {
|
|
296
302
|
source: options.source ?? "runtime",
|
|
297
303
|
priority: options.priority ?? Number.MAX_SAFE_INTEGER,
|
|
298
|
-
|
|
304
|
+
// Left undefined so the store's own secret detection runs; an
|
|
305
|
+
// explicit `false` here would have disabled it for every runtime
|
|
306
|
+
// write.
|
|
307
|
+
sensitive: options.sensitive,
|
|
299
308
|
});
|
|
300
309
|
}
|
|
301
310
|
/**
|
|
@@ -77,6 +77,16 @@ export interface ConfigSourceOptions {
|
|
|
77
77
|
* Checks whether a value is a configuration source.
|
|
78
78
|
*/
|
|
79
79
|
export declare function isConfigSource(value: unknown): value is ConfigSource;
|
|
80
|
+
/**
|
|
81
|
+
* Priority given to a source that declares none.
|
|
82
|
+
*
|
|
83
|
+
* Strictly below the priority `initialValues` and other baseline store
|
|
84
|
+
* writes use (0). A source used to default to 0 as well, and because
|
|
85
|
+
* `applySource` overwrites on EQUAL priority, any undeclared source
|
|
86
|
+
* silently wiped the values a manager was seeded with. Declaring a
|
|
87
|
+
* priority of 0 or above still overrides them.
|
|
88
|
+
*/
|
|
89
|
+
export declare const DEFAULT_CONFIG_SOURCE_PRIORITY = -1;
|
|
80
90
|
/**
|
|
81
91
|
* Creates a function-based configuration source.
|
|
82
92
|
*/
|
|
@@ -32,12 +32,22 @@ export function isConfigSource(value) {
|
|
|
32
32
|
typeof source.optional === "boolean" &&
|
|
33
33
|
typeof source.load === "function");
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Priority given to a source that declares none.
|
|
37
|
+
*
|
|
38
|
+
* Strictly below the priority `initialValues` and other baseline store
|
|
39
|
+
* writes use (0). A source used to default to 0 as well, and because
|
|
40
|
+
* `applySource` overwrites on EQUAL priority, any undeclared source
|
|
41
|
+
* silently wiped the values a manager was seeded with. Declaring a
|
|
42
|
+
* priority of 0 or above still overrides them.
|
|
43
|
+
*/
|
|
44
|
+
export const DEFAULT_CONFIG_SOURCE_PRIORITY = -1;
|
|
35
45
|
/**
|
|
36
46
|
* Creates a function-based configuration source.
|
|
37
47
|
*/
|
|
38
48
|
export function createConfigSource(options, loader) {
|
|
39
49
|
const type = options.type ?? ConfigSourceType.CUSTOM;
|
|
40
|
-
const priority = options.priority ??
|
|
50
|
+
const priority = options.priority ?? DEFAULT_CONFIG_SOURCE_PRIORITY;
|
|
41
51
|
if (options.name.trim().length === 0) {
|
|
42
52
|
throw new TypeError("Configuration source name cannot be empty.");
|
|
43
53
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cloneConfigValue, configValuesEqual, defineConfigProperty, freezeConfigValue, } from "../configValue/configValue.core.js";
|
|
2
2
|
import { createConfigEntry, toSafeConfigEntry, } from "../configEntry/configEntry.type.js";
|
|
3
|
+
import { isSensitiveConfigEntry } from "../configSource/configSource.sensitive.js";
|
|
3
4
|
/**
|
|
4
5
|
* Normalizes a configuration key.
|
|
5
6
|
*/
|
|
@@ -97,7 +98,12 @@ export class ConfigStore {
|
|
|
97
98
|
source: options.source ?? "runtime",
|
|
98
99
|
sourceType: options.sourceType,
|
|
99
100
|
priority: options.priority ?? 0,
|
|
100
|
-
|
|
101
|
+
// Secret detection belongs HERE, not at a single caller. It used
|
|
102
|
+
// to run only inside ConfigLoader.applySource, so the very same
|
|
103
|
+
// key was redacted when it arrived from a source and printed in
|
|
104
|
+
// clear when it arrived through initialValues, set(), setMany()
|
|
105
|
+
// or replace(). Pass `sensitive: false` explicitly to opt out.
|
|
106
|
+
sensitive: options.sensitive ?? isSensitiveConfigEntry(normalizedKey, value),
|
|
101
107
|
resolved: options.resolved ?? true,
|
|
102
108
|
});
|
|
103
109
|
if (previous &&
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/config",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Layered configuration management with multiple sources, validation, and environment-specific overrides.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"node": ">=24.0.0"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@zudojs/constants": "1.1.
|
|
32
|
-
"@zudojs/errors": "1.
|
|
31
|
+
"@zudojs/constants": "1.1.1",
|
|
32
|
+
"@zudojs/errors": "1.2.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"typescript": "7.0.2",
|