@zudojs/config 0.0.1 → 1.0.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 +127 -15
- package/dist/configEntry/configEntry.type.d.ts +5 -1
- package/dist/configEntry/configEntry.type.js +5 -10
- package/dist/configFactory/configFactory.core.d.ts +4 -6
- package/dist/configLoader/configLoader.core.d.ts +48 -4
- package/dist/configLoader/configLoader.core.js +56 -37
- package/dist/configManager/configManager.core.d.ts +35 -3
- package/dist/configManager/configManager.core.js +109 -16
- package/dist/configManager/configManager.type.d.ts +12 -0
- package/dist/configResolver/accessors/configResolver.scoped.d.ts +1 -1
- package/dist/configResolver/accessors/index.d.ts +1 -2
- package/dist/configResolver/accessors/index.js +1 -2
- package/dist/configResolver/core/configResolver.core.d.ts +1 -1
- package/dist/configResolver/core/configResolver.core.js +24 -7
- package/dist/configSchema/configSchema.type.d.ts +44 -11
- package/dist/configSchema/configSchema.validator.d.ts +3 -3
- package/dist/configSchema/configSchema.validator.js +58 -16
- package/dist/configSchema/index.d.ts +1 -1
- package/dist/configSchema/index.js +1 -1
- package/dist/configSource/configSource.core.d.ts +28 -11
- package/dist/configSource/configSource.core.js +43 -17
- package/dist/configSource/configSource.environment.d.ts +66 -0
- package/dist/configSource/configSource.environment.js +75 -0
- package/dist/configSource/index.d.ts +1 -0
- package/dist/configSource/index.js +1 -0
- package/dist/configStore/configStore.core.d.ts +15 -0
- package/dist/configStore/configStore.core.js +35 -5
- package/dist/configStore/configStore.type.d.ts +11 -0
- package/dist/configValue/configValue.core.d.ts +38 -0
- package/dist/configValue/configValue.core.js +78 -7
- package/package.json +12 -5
- package/dist/.tsbuildinfo +0 -1
- package/dist/configEntry/configEntry.type.d.ts.map +0 -1
- package/dist/configEntry/configEntry.type.js.map +0 -1
- package/dist/configEntry/index.d.ts.map +0 -1
- package/dist/configEntry/index.js.map +0 -1
- package/dist/configFactory/configFactory.core.d.ts.map +0 -1
- package/dist/configFactory/configFactory.core.js.map +0 -1
- package/dist/configFactory/index.d.ts.map +0 -1
- package/dist/configFactory/index.js.map +0 -1
- package/dist/configLoader/configLoader.core.d.ts.map +0 -1
- package/dist/configLoader/configLoader.core.js.map +0 -1
- package/dist/configLoader/index.d.ts.map +0 -1
- package/dist/configLoader/index.js.map +0 -1
- package/dist/configManager/configManager.core.d.ts.map +0 -1
- package/dist/configManager/configManager.core.js.map +0 -1
- package/dist/configManager/configManager.error.d.ts.map +0 -1
- package/dist/configManager/configManager.error.js.map +0 -1
- package/dist/configManager/configManager.factory.d.ts.map +0 -1
- package/dist/configManager/configManager.factory.js.map +0 -1
- package/dist/configManager/configManager.type.d.ts.map +0 -1
- package/dist/configManager/configManager.type.js.map +0 -1
- package/dist/configManager/index.d.ts.map +0 -1
- package/dist/configManager/index.js.map +0 -1
- package/dist/configResolver/accessors/configResolver.accessors.d.ts +0 -19
- package/dist/configResolver/accessors/configResolver.accessors.d.ts.map +0 -1
- package/dist/configResolver/accessors/configResolver.accessors.js +0 -237
- package/dist/configResolver/accessors/configResolver.accessors.js.map +0 -1
- package/dist/configResolver/accessors/configResolver.scoped.d.ts.map +0 -1
- package/dist/configResolver/accessors/configResolver.scoped.js.map +0 -1
- package/dist/configResolver/accessors/index.d.ts.map +0 -1
- package/dist/configResolver/accessors/index.js.map +0 -1
- package/dist/configResolver/core/configResolver.core.d.ts.map +0 -1
- package/dist/configResolver/core/configResolver.core.js.map +0 -1
- package/dist/configResolver/core/configResolver.error.d.ts.map +0 -1
- package/dist/configResolver/core/configResolver.error.js.map +0 -1
- package/dist/configResolver/core/configResolver.factory.d.ts.map +0 -1
- package/dist/configResolver/core/configResolver.factory.js.map +0 -1
- package/dist/configResolver/core/configResolver.type.d.ts.map +0 -1
- package/dist/configResolver/core/configResolver.type.js.map +0 -1
- package/dist/configResolver/core/index.d.ts.map +0 -1
- package/dist/configResolver/core/index.js.map +0 -1
- package/dist/configResolver/index.d.ts.map +0 -1
- package/dist/configResolver/index.js.map +0 -1
- package/dist/configSchema/configSchema.core.d.ts +0 -8
- package/dist/configSchema/configSchema.core.d.ts.map +0 -1
- package/dist/configSchema/configSchema.core.js +0 -8
- package/dist/configSchema/configSchema.core.js.map +0 -1
- package/dist/configSchema/configSchema.rules.d.ts +0 -18
- package/dist/configSchema/configSchema.rules.d.ts.map +0 -1
- package/dist/configSchema/configSchema.rules.js +0 -108
- package/dist/configSchema/configSchema.rules.js.map +0 -1
- package/dist/configSchema/configSchema.type.d.ts.map +0 -1
- package/dist/configSchema/configSchema.type.js.map +0 -1
- package/dist/configSchema/configSchema.validator.d.ts.map +0 -1
- package/dist/configSchema/configSchema.validator.js.map +0 -1
- package/dist/configSchema/index.d.ts.map +0 -1
- package/dist/configSchema/index.js.map +0 -1
- package/dist/configSource/configSource.core.d.ts.map +0 -1
- package/dist/configSource/configSource.core.js.map +0 -1
- package/dist/configSource/index.d.ts.map +0 -1
- package/dist/configSource/index.js.map +0 -1
- package/dist/configStore/configStore.core.d.ts.map +0 -1
- package/dist/configStore/configStore.core.js.map +0 -1
- package/dist/configStore/configStore.factory.d.ts.map +0 -1
- package/dist/configStore/configStore.factory.js.map +0 -1
- package/dist/configStore/configStore.type.d.ts.map +0 -1
- package/dist/configStore/configStore.type.js.map +0 -1
- package/dist/configStore/index.d.ts.map +0 -1
- package/dist/configStore/index.js.map +0 -1
- package/dist/configValue/configValue.core.d.ts.map +0 -1
- package/dist/configValue/configValue.core.js.map +0 -1
- package/dist/configValue/index.d.ts.map +0 -1
- package/dist/configValue/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# @zudojs/config
|
|
2
2
|
|
|
3
|
-
Layered configuration management with multiple sources,
|
|
3
|
+
Layered configuration management with multiple sources, priority-based
|
|
4
|
+
merging, schema validation, and sensitive-value redaction.
|
|
4
5
|
|
|
5
6
|
## Installation
|
|
6
7
|
|
|
@@ -11,31 +12,142 @@ npm install @zudojs/config
|
|
|
11
12
|
## Quick Start
|
|
12
13
|
|
|
13
14
|
```typescript
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
createConfigManager,
|
|
17
|
+
createDefaultsConfigSource,
|
|
18
|
+
createMemoryConfigSource,
|
|
19
|
+
} from "@zudojs/config";
|
|
15
20
|
|
|
16
|
-
const
|
|
21
|
+
const manager = createConfigManager({
|
|
17
22
|
sources: [
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
createDefaultsConfigSource({ "app.port": 3000, "app.name": "demo" }),
|
|
24
|
+
createMemoryConfigSource(
|
|
25
|
+
{ "app.port": 8080 },
|
|
26
|
+
{ name: "overrides", priority: 100 },
|
|
27
|
+
),
|
|
21
28
|
],
|
|
22
29
|
});
|
|
23
30
|
|
|
24
|
-
|
|
31
|
+
await manager.load();
|
|
32
|
+
|
|
33
|
+
manager.number("app.port"); // 8080 (higher priority wins)
|
|
34
|
+
manager.string("app.name"); // "demo"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or create and load in one step:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { initializeConfigManager } from "@zudojs/config";
|
|
41
|
+
|
|
42
|
+
const manager = await initializeConfigManager({
|
|
43
|
+
initialValues: { "app.env": "development" },
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
With `autoLoad: true` the manager starts loading on construction;
|
|
48
|
+
await `manager.ready()` before reading values.
|
|
49
|
+
|
|
50
|
+
## Sources
|
|
51
|
+
|
|
52
|
+
A source provides values plus a priority. Higher priority wins; equal
|
|
53
|
+
priorities are applied in registration order (last registered wins).
|
|
54
|
+
|
|
55
|
+
- `createDefaultsConfigSource(values, name?)` — low-priority defaults.
|
|
56
|
+
- `createMemoryConfigSource(values, { name, priority?, optional? })` —
|
|
57
|
+
in-memory values.
|
|
58
|
+
- `createEnvironmentConfigSource({ prefix, env?, priority?, keyMapper?, isSensitive? })`
|
|
59
|
+
— reads environment variables. Values stay RAW STRINGS (use the typed
|
|
60
|
+
accessors to coerce, so `"false"` becomes `false` instead of a truthy
|
|
61
|
+
string). Names matching a password/token/secret/API-key pattern are
|
|
62
|
+
reported as sensitive and therefore redacted by `toSafeObject()`.
|
|
63
|
+
- `createCustomConfigSource(name, loader, options?)` — async loader
|
|
64
|
+
function returning `{ values, source, type }`. Include
|
|
65
|
+
`sensitiveKeys: ["some.key"]` in the result to mark values as
|
|
66
|
+
sensitive so they are redacted by safe serialization.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import {
|
|
70
|
+
createEnvironmentConfigSource,
|
|
71
|
+
createCustomConfigSource,
|
|
72
|
+
ConfigSourceType,
|
|
73
|
+
} from "@zudojs/config";
|
|
74
|
+
|
|
75
|
+
// APP__DB__HOST=localhost -> "db.host"
|
|
76
|
+
const env = createEnvironmentConfigSource({ prefix: "APP__" });
|
|
77
|
+
|
|
78
|
+
const remote = createCustomConfigSource("vault", async () => ({
|
|
79
|
+
values: { "db.password": "s3cret" },
|
|
80
|
+
sensitiveKeys: ["db.password"],
|
|
81
|
+
source: "vault",
|
|
82
|
+
type: ConfigSourceType.CUSTOM,
|
|
83
|
+
}));
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Typed access
|
|
87
|
+
|
|
88
|
+
`ConfigManager` (and the underlying `ConfigResolver`) offer typed
|
|
89
|
+
getters: `string()`, `number()`, `boolean()`, `bigint()`, `date()`,
|
|
90
|
+
`object()`, `array()`, `get()`, `required()`, and `scoped(prefix)` for
|
|
91
|
+
prefix-scoped access.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
const db = manager.scoped("db");
|
|
95
|
+
db.string("host", "localhost");
|
|
25
96
|
```
|
|
26
97
|
|
|
27
|
-
##
|
|
98
|
+
## Schema validation
|
|
99
|
+
|
|
100
|
+
Schemas are plain objects validated by this package (no external
|
|
101
|
+
validation library is used):
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { ConfigValueType } from "@zudojs/config";
|
|
105
|
+
|
|
106
|
+
const config = manager.validate({
|
|
107
|
+
properties: {
|
|
108
|
+
"app.port": { type: ConfigValueType.NUMBER, required: true },
|
|
109
|
+
"db.password": { type: ConfigValueType.STRING, secret: true },
|
|
110
|
+
},
|
|
111
|
+
additionalProperties: true,
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Object schemas nest: a property schema of type `OBJECT` that declares
|
|
116
|
+
its own `properties` / `additionalProperties` is validated recursively,
|
|
117
|
+
so nested constraints are enforced by `validate()`, `resolve()` and the
|
|
118
|
+
standalone `validateConfigValue`.
|
|
119
|
+
|
|
120
|
+
`validate()` throws on failure and marks entries whose schema has
|
|
121
|
+
`secret: true` as sensitive. Individual values can be validated with
|
|
122
|
+
`manager.resolve(key, schema)` or the standalone
|
|
123
|
+
`validateConfigValue` / `validateConfigObject` functions.
|
|
124
|
+
|
|
125
|
+
## Redaction
|
|
126
|
+
|
|
127
|
+
- `manager.toObject()` / `store.toObject()` return RAW values,
|
|
128
|
+
including secrets.
|
|
129
|
+
- `manager.toSafeObject()` / `store.toSafeObject()` replace sensitive
|
|
130
|
+
values with `"[REDACTED]"` — use these for logging and diagnostics.
|
|
131
|
+
|
|
132
|
+
## Reloading
|
|
133
|
+
|
|
134
|
+
`manager.reload()` re-runs all sources. Values set at runtime via
|
|
135
|
+
`manager.set()` and `initialValues` survive loads; sources only
|
|
136
|
+
overwrite entries at equal or higher priority.
|
|
137
|
+
|
|
138
|
+
## Untrusted input
|
|
28
139
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
140
|
+
Configuration frequently originates from files, environment variables
|
|
141
|
+
or remote services. Keys such as `__proto__`, `constructor` and
|
|
142
|
+
`prototype` are copied with `Object.defineProperty`, never plain
|
|
143
|
+
assignment, so a hostile key becomes an ordinary own property and can
|
|
144
|
+
never replace an object's prototype. Schema properties are read as OWN
|
|
145
|
+
properties, so a schema property named `constructor` is reported as
|
|
146
|
+
missing rather than matching an inherited function.
|
|
35
147
|
|
|
36
148
|
## Use Cases
|
|
37
149
|
|
|
38
150
|
- Application configuration
|
|
39
151
|
- Environment-specific settings (dev, staging, prod)
|
|
40
152
|
- Feature flags and toggles
|
|
41
|
-
- Secrets
|
|
153
|
+
- Secrets handling with redacted diagnostics
|
|
@@ -45,8 +45,12 @@ export declare function updateConfigEntry<T extends ConfigValue>(entry: ConfigEn
|
|
|
45
45
|
export declare function isConfigEntry(value: unknown): value is ConfigEntry;
|
|
46
46
|
/**
|
|
47
47
|
* Masks a sensitive configuration value.
|
|
48
|
+
*
|
|
49
|
+
* The entire value — including arrays and objects — collapses to a
|
|
50
|
+
* single replacement token so that no structural information (such
|
|
51
|
+
* as an array's length) leaks about the sensitive value.
|
|
48
52
|
*/
|
|
49
|
-
export declare function redactConfigValue(
|
|
53
|
+
export declare function redactConfigValue(_value: ConfigValue, replacement?: string): ConfigValue;
|
|
50
54
|
/**
|
|
51
55
|
* Creates a safe representation of an entry for logging or
|
|
52
56
|
* diagnostics.
|
|
@@ -52,17 +52,12 @@ export function isConfigEntry(value) {
|
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
54
|
* Masks a sensitive configuration value.
|
|
55
|
+
*
|
|
56
|
+
* The entire value — including arrays and objects — collapses to a
|
|
57
|
+
* single replacement token so that no structural information (such
|
|
58
|
+
* as an array's length) leaks about the sensitive value.
|
|
55
59
|
*/
|
|
56
|
-
export function redactConfigValue(
|
|
57
|
-
if (value === undefined || value === null || typeof value !== "object") {
|
|
58
|
-
return replacement;
|
|
59
|
-
}
|
|
60
|
-
if (value instanceof Date) {
|
|
61
|
-
return replacement;
|
|
62
|
-
}
|
|
63
|
-
if (Array.isArray(value)) {
|
|
64
|
-
return value.map(() => replacement);
|
|
65
|
-
}
|
|
60
|
+
export function redactConfigValue(_value, replacement = "[REDACTED]") {
|
|
66
61
|
return replacement;
|
|
67
62
|
}
|
|
68
63
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConfigValue } from "../configValue/configValue.core.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { AnyConfigSchema } from "../configSchema/configSchema.type.js";
|
|
3
3
|
import type { ConfigSource } from "../configSource/configSource.core.js";
|
|
4
4
|
import type { ConfigStore } from "../configStore/configStore.core.js";
|
|
5
5
|
import type { ConfigManagerOptions } from "../configManager/configManager.type.js";
|
|
@@ -7,9 +7,7 @@ import type { ConfigManager } from "../configManager/configManager.core.js";
|
|
|
7
7
|
/**
|
|
8
8
|
* Factory options for creating a configuration manager.
|
|
9
9
|
*/
|
|
10
|
-
export
|
|
11
|
-
readonly name?: string;
|
|
12
|
-
}
|
|
10
|
+
export type ConfigFactoryOptions = ConfigManagerOptions;
|
|
13
11
|
/**
|
|
14
12
|
* Creates a configuration manager without loading it.
|
|
15
13
|
*/
|
|
@@ -34,8 +32,8 @@ export declare function createConfigurationFromStore(store: ConfigStore, options
|
|
|
34
32
|
* Creates a configuration manager and validates it against a schema.
|
|
35
33
|
*/
|
|
36
34
|
export declare function createValidatedConfiguration<T extends ConfigValue = ConfigValue>(schema: {
|
|
37
|
-
readonly properties: Readonly<Record<string,
|
|
38
|
-
readonly additionalProperties?: boolean |
|
|
35
|
+
readonly properties: Readonly<Record<string, AnyConfigSchema>>;
|
|
36
|
+
readonly additionalProperties?: boolean | AnyConfigSchema;
|
|
39
37
|
}, options?: ConfigFactoryOptions): Promise<{
|
|
40
38
|
readonly manager: ConfigManager;
|
|
41
39
|
readonly config: T;
|
|
@@ -8,9 +8,30 @@ export interface ConfigLoaderOptions {
|
|
|
8
8
|
readonly sources?: readonly ConfigSource[];
|
|
9
9
|
readonly context?: ConfigSourceContext;
|
|
10
10
|
readonly store?: ConfigStore;
|
|
11
|
+
/**
|
|
12
|
+
* Whether a store created by the loader deep-clones and freezes
|
|
13
|
+
* stored values. When false, the store shares REFERENCES with the
|
|
14
|
+
* values that sources returned — mutating them later mutates the
|
|
15
|
+
* store's view as well. Ignored when an existing `store` is given.
|
|
16
|
+
*/
|
|
11
17
|
readonly freeze?: boolean;
|
|
12
18
|
readonly clearStore?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Called after each source loads successfully.
|
|
21
|
+
*
|
|
22
|
+
* Receives the source itself and that source's result, in that order,
|
|
23
|
+
* as two separate arguments. Awaited, so a slow hook delays the rest of
|
|
24
|
+
* the load.
|
|
25
|
+
*/
|
|
13
26
|
readonly onSourceLoaded?: (source: ConfigSource, result: ConfigSourceResult) => void | Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Called when a source fails to load.
|
|
29
|
+
*
|
|
30
|
+
* Receives the source first and the thrown value second. The thrown
|
|
31
|
+
* value is `unknown` because a source may reject with anything, so
|
|
32
|
+
* narrow it before use. Awaited, and returning normally does not
|
|
33
|
+
* suppress the failure.
|
|
34
|
+
*/
|
|
14
35
|
readonly onSourceError?: (source: ConfigSource, error: unknown) => void | Promise<void>;
|
|
15
36
|
}
|
|
16
37
|
/**
|
|
@@ -35,6 +56,7 @@ export declare class ConfigLoader {
|
|
|
35
56
|
private readonly onSourceLoaded?;
|
|
36
57
|
private readonly onSourceError?;
|
|
37
58
|
private readonly clearStore;
|
|
59
|
+
private readonly freeze;
|
|
38
60
|
private loading;
|
|
39
61
|
private loaded;
|
|
40
62
|
private disposed;
|
|
@@ -62,6 +84,11 @@ export declare class ConfigLoader {
|
|
|
62
84
|
get lastLoadResult(): ConfigLoadResult | undefined;
|
|
63
85
|
/**
|
|
64
86
|
* Loads all configured sources.
|
|
87
|
+
*
|
|
88
|
+
* When the loader context carries an AbortSignal, the signal is
|
|
89
|
+
* checked between sources: once aborted, loading stops early and
|
|
90
|
+
* this method throws the signal's abort reason. Values applied by
|
|
91
|
+
* sources that completed before the abort remain in the store.
|
|
65
92
|
*/
|
|
66
93
|
load(): Promise<ConfigLoadResult>;
|
|
67
94
|
/**
|
|
@@ -72,6 +99,10 @@ export declare class ConfigLoader {
|
|
|
72
99
|
reload(): Promise<ConfigLoadResult>;
|
|
73
100
|
/**
|
|
74
101
|
* Loads only selected sources.
|
|
102
|
+
*
|
|
103
|
+
* The shared store is never cleared: values loaded previously are
|
|
104
|
+
* retained and only the given sources are (re)applied. Source
|
|
105
|
+
* callbacks configured on this loader are invoked as usual.
|
|
75
106
|
*/
|
|
76
107
|
loadSources(sources: readonly ConfigSource[]): Promise<ConfigLoadResult>;
|
|
77
108
|
/**
|
|
@@ -87,9 +118,14 @@ export declare class ConfigLoader {
|
|
|
87
118
|
*/
|
|
88
119
|
getSource(name: string): ConfigSource | undefined;
|
|
89
120
|
/**
|
|
90
|
-
* Disposes the loader and its source resources.
|
|
121
|
+
* Disposes the loader and, by default, its source resources.
|
|
122
|
+
*
|
|
123
|
+
* Pass `closeSources: false` when the sources are owned by the
|
|
124
|
+
* caller and must outlive this loader.
|
|
91
125
|
*/
|
|
92
|
-
dispose(
|
|
126
|
+
dispose(options?: {
|
|
127
|
+
readonly closeSources?: boolean;
|
|
128
|
+
}): Promise<void>;
|
|
93
129
|
/**
|
|
94
130
|
* Detaches this loader from its source lifecycle.
|
|
95
131
|
*
|
|
@@ -100,8 +136,13 @@ export declare class ConfigLoader {
|
|
|
100
136
|
* Applies one source to the configuration store.
|
|
101
137
|
*
|
|
102
138
|
* Sources are processed from highest to lowest priority. Existing
|
|
103
|
-
* values are
|
|
104
|
-
*
|
|
139
|
+
* strictly-higher-priority values are retained; an EQUAL-priority
|
|
140
|
+
* source overwrites, so among equal priorities the last-applied
|
|
141
|
+
* (last-registered) source wins.
|
|
142
|
+
*
|
|
143
|
+
* Keys listed in the result's `sensitiveKeys` are marked sensitive.
|
|
144
|
+
* Once an entry is sensitive it stays sensitive even when a later
|
|
145
|
+
* source overwrites its value.
|
|
105
146
|
*/
|
|
106
147
|
private applySource;
|
|
107
148
|
private assertActive;
|
|
@@ -112,6 +153,9 @@ export declare class ConfigLoader {
|
|
|
112
153
|
export declare function createConfigLoader(options?: ConfigLoaderOptions): ConfigLoader;
|
|
113
154
|
/**
|
|
114
155
|
* Loads configuration directly from a collection of sources.
|
|
156
|
+
*
|
|
157
|
+
* The temporary loader is disposed afterwards WITHOUT closing the
|
|
158
|
+
* given sources — they are owned by the caller and remain usable.
|
|
115
159
|
*/
|
|
116
160
|
export declare function loadConfiguration(sources: readonly ConfigSource[], options?: Omit<ConfigLoaderOptions, "sources">): Promise<ConfigLoadResult>;
|
|
117
161
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cloneConfigValue } from "../configValue/configValue.core.js";
|
|
2
2
|
import { createConfigEntry } from "../configEntry/configEntry.type.js";
|
|
3
|
-
import {
|
|
3
|
+
import { loadConfigSourceStrict, sortConfigSources, } from "../configSource/configSource.core.js";
|
|
4
4
|
import { createConfigStore } from "../configStore/configStore.factory.js";
|
|
5
5
|
/**
|
|
6
6
|
* Configuration loader.
|
|
@@ -15,6 +15,7 @@ export class ConfigLoader {
|
|
|
15
15
|
onSourceLoaded;
|
|
16
16
|
onSourceError;
|
|
17
17
|
clearStore;
|
|
18
|
+
freeze;
|
|
18
19
|
loading = false;
|
|
19
20
|
loaded = false;
|
|
20
21
|
disposed = false;
|
|
@@ -30,6 +31,7 @@ export class ConfigLoader {
|
|
|
30
31
|
this.onSourceLoaded = options.onSourceLoaded;
|
|
31
32
|
this.onSourceError = options.onSourceError;
|
|
32
33
|
this.clearStore = options.clearStore ?? true;
|
|
34
|
+
this.freeze = options.freeze ?? true;
|
|
33
35
|
}
|
|
34
36
|
/**
|
|
35
37
|
* Returns the underlying configuration store.
|
|
@@ -65,6 +67,11 @@ export class ConfigLoader {
|
|
|
65
67
|
}
|
|
66
68
|
/**
|
|
67
69
|
* Loads all configured sources.
|
|
70
|
+
*
|
|
71
|
+
* When the loader context carries an AbortSignal, the signal is
|
|
72
|
+
* checked between sources: once aborted, loading stops early and
|
|
73
|
+
* this method throws the signal's abort reason. Values applied by
|
|
74
|
+
* sources that completed before the abort remain in the store.
|
|
68
75
|
*/
|
|
69
76
|
async load() {
|
|
70
77
|
this.assertActive();
|
|
@@ -78,9 +85,14 @@ export class ConfigLoader {
|
|
|
78
85
|
}
|
|
79
86
|
const sortedSources = sortConfigSources(this.sources);
|
|
80
87
|
const results = [];
|
|
88
|
+
const signal = this.context.signal;
|
|
81
89
|
for (const source of sortedSources) {
|
|
90
|
+
if (signal?.aborted) {
|
|
91
|
+
throw (signal.reason ??
|
|
92
|
+
new Error("Configuration loading was aborted."));
|
|
93
|
+
}
|
|
82
94
|
try {
|
|
83
|
-
const result = await
|
|
95
|
+
const result = await loadConfigSourceStrict(source, this.context);
|
|
84
96
|
if (!result) {
|
|
85
97
|
continue;
|
|
86
98
|
}
|
|
@@ -125,6 +137,10 @@ export class ConfigLoader {
|
|
|
125
137
|
}
|
|
126
138
|
/**
|
|
127
139
|
* Loads only selected sources.
|
|
140
|
+
*
|
|
141
|
+
* The shared store is never cleared: values loaded previously are
|
|
142
|
+
* retained and only the given sources are (re)applied. Source
|
|
143
|
+
* callbacks configured on this loader are invoked as usual.
|
|
128
144
|
*/
|
|
129
145
|
async loadSources(sources) {
|
|
130
146
|
this.assertActive();
|
|
@@ -132,7 +148,10 @@ export class ConfigLoader {
|
|
|
132
148
|
sources,
|
|
133
149
|
context: this.context,
|
|
134
150
|
store: this.store,
|
|
135
|
-
clearStore:
|
|
151
|
+
clearStore: false,
|
|
152
|
+
freeze: this.freeze,
|
|
153
|
+
onSourceLoaded: this.onSourceLoaded,
|
|
154
|
+
onSourceError: this.onSourceError,
|
|
136
155
|
});
|
|
137
156
|
const result = await temporaryLoader.load();
|
|
138
157
|
temporaryLoader.detach();
|
|
@@ -168,23 +187,28 @@ export class ConfigLoader {
|
|
|
168
187
|
return this.sources.find((source) => source.name === name);
|
|
169
188
|
}
|
|
170
189
|
/**
|
|
171
|
-
* Disposes the loader and its source resources.
|
|
190
|
+
* Disposes the loader and, by default, its source resources.
|
|
191
|
+
*
|
|
192
|
+
* Pass `closeSources: false` when the sources are owned by the
|
|
193
|
+
* caller and must outlive this loader.
|
|
172
194
|
*/
|
|
173
|
-
async dispose() {
|
|
195
|
+
async dispose(options = {}) {
|
|
174
196
|
if (this.disposed) {
|
|
175
197
|
return;
|
|
176
198
|
}
|
|
177
199
|
this.disposed = true;
|
|
178
200
|
this.loaded = false;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
201
|
+
if (options.closeSources ?? true) {
|
|
202
|
+
for (const source of this.sources) {
|
|
203
|
+
if (!source.close) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
await source.close();
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Continue closing remaining sources.
|
|
211
|
+
}
|
|
188
212
|
}
|
|
189
213
|
}
|
|
190
214
|
this.sources.length = 0;
|
|
@@ -204,11 +228,17 @@ export class ConfigLoader {
|
|
|
204
228
|
* Applies one source to the configuration store.
|
|
205
229
|
*
|
|
206
230
|
* Sources are processed from highest to lowest priority. Existing
|
|
207
|
-
* values are
|
|
208
|
-
*
|
|
231
|
+
* strictly-higher-priority values are retained; an EQUAL-priority
|
|
232
|
+
* source overwrites, so among equal priorities the last-applied
|
|
233
|
+
* (last-registered) source wins.
|
|
234
|
+
*
|
|
235
|
+
* Keys listed in the result's `sensitiveKeys` are marked sensitive.
|
|
236
|
+
* Once an entry is sensitive it stays sensitive even when a later
|
|
237
|
+
* source overwrites its value.
|
|
209
238
|
*/
|
|
210
239
|
applySource(source, result) {
|
|
211
240
|
const priority = source.priority;
|
|
241
|
+
const sensitiveKeys = new Set(result.sensitiveKeys ?? []);
|
|
212
242
|
for (const [key, value] of Object.entries(result.values)) {
|
|
213
243
|
const existing = this.store.getEntry(key);
|
|
214
244
|
if (existing && existing.priority > priority) {
|
|
@@ -218,7 +248,7 @@ export class ConfigLoader {
|
|
|
218
248
|
source: source.name,
|
|
219
249
|
sourceType: source.type,
|
|
220
250
|
priority,
|
|
221
|
-
sensitive: existing?.sensitive ?? false,
|
|
251
|
+
sensitive: sensitiveKeys.has(key) || (existing?.sensitive ?? false),
|
|
222
252
|
resolved: true,
|
|
223
253
|
});
|
|
224
254
|
}
|
|
@@ -229,25 +259,6 @@ export class ConfigLoader {
|
|
|
229
259
|
}
|
|
230
260
|
}
|
|
231
261
|
}
|
|
232
|
-
/**
|
|
233
|
-
* Loads one source while preserving the source lifecycle contract.
|
|
234
|
-
*/
|
|
235
|
-
async function loadSingleSource(source, context) {
|
|
236
|
-
if (source.isAvailable) {
|
|
237
|
-
const available = await source.isAvailable(context);
|
|
238
|
-
if (!available) {
|
|
239
|
-
if (source.optional) {
|
|
240
|
-
return undefined;
|
|
241
|
-
}
|
|
242
|
-
throw new Error(`Configuration source "${source.name}" is unavailable.`);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
const result = await source.load(context);
|
|
246
|
-
if (!result || typeof result !== "object") {
|
|
247
|
-
throw new Error(`Configuration source "${source.name}" returned an invalid result.`);
|
|
248
|
-
}
|
|
249
|
-
return result;
|
|
250
|
-
}
|
|
251
262
|
/**
|
|
252
263
|
* Creates a configuration loader.
|
|
253
264
|
*/
|
|
@@ -256,6 +267,9 @@ export function createConfigLoader(options = {}) {
|
|
|
256
267
|
}
|
|
257
268
|
/**
|
|
258
269
|
* Loads configuration directly from a collection of sources.
|
|
270
|
+
*
|
|
271
|
+
* The temporary loader is disposed afterwards WITHOUT closing the
|
|
272
|
+
* given sources — they are owned by the caller and remain usable.
|
|
259
273
|
*/
|
|
260
274
|
export async function loadConfiguration(sources, options = {}) {
|
|
261
275
|
const loader = createConfigLoader({
|
|
@@ -266,7 +280,7 @@ export async function loadConfiguration(sources, options = {}) {
|
|
|
266
280
|
return await loader.load();
|
|
267
281
|
}
|
|
268
282
|
finally {
|
|
269
|
-
await loader.dispose();
|
|
283
|
+
await loader.dispose({ closeSources: false });
|
|
270
284
|
}
|
|
271
285
|
}
|
|
272
286
|
/**
|
|
@@ -277,6 +291,10 @@ export function sourceResultsToEntries(results, sources) {
|
|
|
277
291
|
const entries = [];
|
|
278
292
|
for (const result of results) {
|
|
279
293
|
const source = sourceMap.get(result.source);
|
|
294
|
+
// A source result's `sensitiveKeys` must survive the conversion:
|
|
295
|
+
// dropping it here produced entries with `sensitive: false` for
|
|
296
|
+
// credentials, which toSafeConfigEntry() then printed in clear.
|
|
297
|
+
const sensitiveKeys = new Set(result.sensitiveKeys ?? []);
|
|
280
298
|
for (const [key, value] of Object.entries(result.values)) {
|
|
281
299
|
entries.push(createConfigEntry({
|
|
282
300
|
key,
|
|
@@ -284,6 +302,7 @@ export function sourceResultsToEntries(results, sources) {
|
|
|
284
302
|
source: result.source,
|
|
285
303
|
sourceType: result.type,
|
|
286
304
|
priority: source?.priority ?? 0,
|
|
305
|
+
sensitive: sensitiveKeys.has(key),
|
|
287
306
|
}));
|
|
288
307
|
}
|
|
289
308
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConfigValue } from "../configValue/configValue.core.js";
|
|
2
|
-
import type { ConfigSchema } from "../configSchema/
|
|
2
|
+
import type { ConfigSchema, AnyConfigSchema } from "../configSchema/index.js";
|
|
3
3
|
import type { ConfigSource } from "../configSource/configSource.core.js";
|
|
4
4
|
import type { ConfigEntry } from "../configEntry/configEntry.type.js";
|
|
5
5
|
import type { ConfigLoader, ConfigLoadResult } from "../configLoader/configLoader.core.js";
|
|
@@ -15,6 +15,12 @@ import { ConfigManagerState } from "./configManager.type.js";
|
|
|
15
15
|
* keeping their responsibilities separate.
|
|
16
16
|
*/
|
|
17
17
|
export declare class ConfigManager {
|
|
18
|
+
/**
|
|
19
|
+
* Human-readable name for this manager, surfaced in status
|
|
20
|
+
* snapshots so applications running several managers can tell
|
|
21
|
+
* their diagnostics apart.
|
|
22
|
+
*/
|
|
23
|
+
readonly name: string;
|
|
18
24
|
private readonly store;
|
|
19
25
|
private readonly loader;
|
|
20
26
|
private readonly resolver;
|
|
@@ -23,6 +29,7 @@ export declare class ConfigManager {
|
|
|
23
29
|
private lastLoadedAt?;
|
|
24
30
|
private lastError?;
|
|
25
31
|
private disposed;
|
|
32
|
+
private loadPromise?;
|
|
26
33
|
constructor(options?: ConfigManagerOptions);
|
|
27
34
|
/**
|
|
28
35
|
* Returns the current lifecycle state.
|
|
@@ -54,18 +61,36 @@ export declare class ConfigManager {
|
|
|
54
61
|
scoped(prefix: string): ScopedConfigResolver;
|
|
55
62
|
/**
|
|
56
63
|
* Loads configuration.
|
|
64
|
+
*
|
|
65
|
+
* Calling load() while a load is already in flight returns the
|
|
66
|
+
* in-flight promise instead of starting a second load.
|
|
57
67
|
*/
|
|
58
68
|
load(): Promise<ConfigLoadResult>;
|
|
69
|
+
/**
|
|
70
|
+
* Waits until the initial configuration load has completed.
|
|
71
|
+
*
|
|
72
|
+
* With `autoLoad: true` this awaits the load started by the
|
|
73
|
+
* constructor (rethrowing its failure). Otherwise it starts a load
|
|
74
|
+
* when none has happened yet.
|
|
75
|
+
*/
|
|
76
|
+
ready(): Promise<void>;
|
|
59
77
|
/**
|
|
60
78
|
* Reloads configuration.
|
|
61
79
|
*/
|
|
62
80
|
reload(): Promise<ConfigLoadResult>;
|
|
81
|
+
/**
|
|
82
|
+
* Executes a load or reload while keeping lifecycle state safe.
|
|
83
|
+
*
|
|
84
|
+
* If the manager is disposed while the load is in flight, no state
|
|
85
|
+
* transition happens afterwards: a disposed manager stays DISPOSED.
|
|
86
|
+
*/
|
|
87
|
+
private performLoad;
|
|
63
88
|
/**
|
|
64
89
|
* Loads a schema and validates the complete configuration.
|
|
65
90
|
*/
|
|
66
91
|
validate<T extends ConfigValue>(schema: {
|
|
67
|
-
readonly properties: Readonly<Record<string,
|
|
68
|
-
readonly additionalProperties?: boolean |
|
|
92
|
+
readonly properties: Readonly<Record<string, AnyConfigSchema>>;
|
|
93
|
+
readonly additionalProperties?: boolean | AnyConfigSchema;
|
|
69
94
|
}): T;
|
|
70
95
|
/**
|
|
71
96
|
* Gets a raw configuration value.
|
|
@@ -121,8 +146,15 @@ export declare class ConfigManager {
|
|
|
121
146
|
delete(key: string): boolean;
|
|
122
147
|
/**
|
|
123
148
|
* Returns the complete configuration object.
|
|
149
|
+
*
|
|
150
|
+
* WARNING: values are returned RAW — sensitive entries are NOT
|
|
151
|
+
* redacted. Use toSafeObject() for logging or diagnostics.
|
|
124
152
|
*/
|
|
125
153
|
toObject(): Readonly<Record<string, ConfigValue>>;
|
|
154
|
+
/**
|
|
155
|
+
* Returns the configuration object with sensitive values redacted.
|
|
156
|
+
*/
|
|
157
|
+
toSafeObject(): Readonly<Record<string, ConfigValue>>;
|
|
126
158
|
/**
|
|
127
159
|
* Returns a manager status snapshot.
|
|
128
160
|
*/
|