@zudojs/config 0.1.0 → 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.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -15
  3. package/dist/configEntry/configEntry.type.d.ts +5 -1
  4. package/dist/configEntry/configEntry.type.js +5 -10
  5. package/dist/configFactory/configFactory.core.d.ts +4 -6
  6. package/dist/configLoader/configLoader.core.d.ts +48 -4
  7. package/dist/configLoader/configLoader.core.js +56 -37
  8. package/dist/configManager/configManager.core.d.ts +35 -3
  9. package/dist/configManager/configManager.core.js +109 -16
  10. package/dist/configManager/configManager.type.d.ts +12 -0
  11. package/dist/configResolver/accessors/configResolver.scoped.d.ts +1 -1
  12. package/dist/configResolver/accessors/index.d.ts +1 -2
  13. package/dist/configResolver/accessors/index.js +1 -2
  14. package/dist/configResolver/core/configResolver.core.d.ts +1 -1
  15. package/dist/configResolver/core/configResolver.core.js +24 -7
  16. package/dist/configSchema/configSchema.type.d.ts +44 -11
  17. package/dist/configSchema/configSchema.validator.d.ts +3 -3
  18. package/dist/configSchema/configSchema.validator.js +58 -16
  19. package/dist/configSchema/index.d.ts +1 -1
  20. package/dist/configSchema/index.js +1 -1
  21. package/dist/configSource/configSource.core.d.ts +28 -11
  22. package/dist/configSource/configSource.core.js +43 -17
  23. package/dist/configSource/configSource.environment.d.ts +66 -0
  24. package/dist/configSource/configSource.environment.js +75 -0
  25. package/dist/configSource/index.d.ts +1 -0
  26. package/dist/configSource/index.js +1 -0
  27. package/dist/configStore/configStore.core.d.ts +15 -0
  28. package/dist/configStore/configStore.core.js +35 -5
  29. package/dist/configStore/configStore.type.d.ts +11 -0
  30. package/dist/configValue/configValue.core.d.ts +38 -0
  31. package/dist/configValue/configValue.core.js +78 -7
  32. package/package.json +19 -12
  33. package/dist/.tsbuildinfo +0 -1
  34. package/dist/configEntry/configEntry.type.d.ts.map +0 -1
  35. package/dist/configEntry/configEntry.type.js.map +0 -1
  36. package/dist/configEntry/index.d.ts.map +0 -1
  37. package/dist/configEntry/index.js.map +0 -1
  38. package/dist/configFactory/configFactory.core.d.ts.map +0 -1
  39. package/dist/configFactory/configFactory.core.js.map +0 -1
  40. package/dist/configFactory/index.d.ts.map +0 -1
  41. package/dist/configFactory/index.js.map +0 -1
  42. package/dist/configLoader/configLoader.core.d.ts.map +0 -1
  43. package/dist/configLoader/configLoader.core.js.map +0 -1
  44. package/dist/configLoader/index.d.ts.map +0 -1
  45. package/dist/configLoader/index.js.map +0 -1
  46. package/dist/configManager/configManager.core.d.ts.map +0 -1
  47. package/dist/configManager/configManager.core.js.map +0 -1
  48. package/dist/configManager/configManager.error.d.ts.map +0 -1
  49. package/dist/configManager/configManager.error.js.map +0 -1
  50. package/dist/configManager/configManager.factory.d.ts.map +0 -1
  51. package/dist/configManager/configManager.factory.js.map +0 -1
  52. package/dist/configManager/configManager.type.d.ts.map +0 -1
  53. package/dist/configManager/configManager.type.js.map +0 -1
  54. package/dist/configManager/index.d.ts.map +0 -1
  55. package/dist/configManager/index.js.map +0 -1
  56. package/dist/configResolver/accessors/configResolver.accessors.d.ts +0 -19
  57. package/dist/configResolver/accessors/configResolver.accessors.d.ts.map +0 -1
  58. package/dist/configResolver/accessors/configResolver.accessors.js +0 -237
  59. package/dist/configResolver/accessors/configResolver.accessors.js.map +0 -1
  60. package/dist/configResolver/accessors/configResolver.scoped.d.ts.map +0 -1
  61. package/dist/configResolver/accessors/configResolver.scoped.js.map +0 -1
  62. package/dist/configResolver/accessors/index.d.ts.map +0 -1
  63. package/dist/configResolver/accessors/index.js.map +0 -1
  64. package/dist/configResolver/core/configResolver.core.d.ts.map +0 -1
  65. package/dist/configResolver/core/configResolver.core.js.map +0 -1
  66. package/dist/configResolver/core/configResolver.error.d.ts.map +0 -1
  67. package/dist/configResolver/core/configResolver.error.js.map +0 -1
  68. package/dist/configResolver/core/configResolver.factory.d.ts.map +0 -1
  69. package/dist/configResolver/core/configResolver.factory.js.map +0 -1
  70. package/dist/configResolver/core/configResolver.type.d.ts.map +0 -1
  71. package/dist/configResolver/core/configResolver.type.js.map +0 -1
  72. package/dist/configResolver/core/index.d.ts.map +0 -1
  73. package/dist/configResolver/core/index.js.map +0 -1
  74. package/dist/configResolver/index.d.ts.map +0 -1
  75. package/dist/configResolver/index.js.map +0 -1
  76. package/dist/configSchema/configSchema.core.d.ts +0 -8
  77. package/dist/configSchema/configSchema.core.d.ts.map +0 -1
  78. package/dist/configSchema/configSchema.core.js +0 -8
  79. package/dist/configSchema/configSchema.core.js.map +0 -1
  80. package/dist/configSchema/configSchema.rules.d.ts +0 -18
  81. package/dist/configSchema/configSchema.rules.d.ts.map +0 -1
  82. package/dist/configSchema/configSchema.rules.js +0 -108
  83. package/dist/configSchema/configSchema.rules.js.map +0 -1
  84. package/dist/configSchema/configSchema.type.d.ts.map +0 -1
  85. package/dist/configSchema/configSchema.type.js.map +0 -1
  86. package/dist/configSchema/configSchema.validator.d.ts.map +0 -1
  87. package/dist/configSchema/configSchema.validator.js.map +0 -1
  88. package/dist/configSchema/index.d.ts.map +0 -1
  89. package/dist/configSchema/index.js.map +0 -1
  90. package/dist/configSource/configSource.core.d.ts.map +0 -1
  91. package/dist/configSource/configSource.core.js.map +0 -1
  92. package/dist/configSource/index.d.ts.map +0 -1
  93. package/dist/configSource/index.js.map +0 -1
  94. package/dist/configStore/configStore.core.d.ts.map +0 -1
  95. package/dist/configStore/configStore.core.js.map +0 -1
  96. package/dist/configStore/configStore.factory.d.ts.map +0 -1
  97. package/dist/configStore/configStore.factory.js.map +0 -1
  98. package/dist/configStore/configStore.type.d.ts.map +0 -1
  99. package/dist/configStore/configStore.type.js.map +0 -1
  100. package/dist/configStore/index.d.ts.map +0 -1
  101. package/dist/configStore/index.js.map +0 -1
  102. package/dist/configValue/configValue.core.d.ts.map +0 -1
  103. package/dist/configValue/configValue.core.js.map +0 -1
  104. package/dist/configValue/index.d.ts.map +0 -1
  105. package/dist/configValue/index.js.map +0 -1
  106. package/dist/index.d.ts.map +0 -1
  107. package/dist/index.js.map +0 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zudojs Contributors
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 CHANGED
@@ -1,6 +1,7 @@
1
1
  # @zudojs/config
2
2
 
3
- Layered configuration management with multiple sources, validation, and environment-specific overrides.
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 { createConfig } from "@zudojs/config";
15
+ import {
16
+ createConfigManager,
17
+ createDefaultsConfigSource,
18
+ createMemoryConfigSource,
19
+ } from "@zudojs/config";
15
20
 
16
- const config = await createConfig({
21
+ const manager = createConfigManager({
17
22
  sources: [
18
- env({ prefix: "APP_" }),
19
- file("./config.json"),
20
- defaults({ port: 3000 }),
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
- const port = config.get("port");
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
- ## Features
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
- - Layered source pattern with priority ordering
30
- - Environment variable support with prefix matching
31
- - JSON, YAML, and TOML file sources
32
- - Schema validation with Zod
33
- - Sensitive value auto-redaction
34
- - Hot reload support
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 management integration
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(value: ConfigValue, replacement?: string): ConfigValue;
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(value, replacement = "[REDACTED]") {
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 { ConfigSchema } from "../configSchema/configSchema.type.js";
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 interface ConfigFactoryOptions extends ConfigManagerOptions {
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, ConfigSchema>>;
38
- readonly additionalProperties?: boolean | ConfigSchema;
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(): Promise<void>;
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 therefore retained when a lower-priority source
104
- * attempts to replace them.
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 { loadConfigSources, sortConfigSources, } from "../configSource/configSource.core.js";
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 loadSingleSource(source, this.context);
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: this.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
- for (const source of this.sources) {
180
- if (!source.close) {
181
- continue;
182
- }
183
- try {
184
- await source.close();
185
- }
186
- catch {
187
- // Continue closing remaining sources.
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 therefore retained when a lower-priority source
208
- * attempts to replace them.
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/configSchema.core.js";
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, ConfigSchema>>;
68
- readonly additionalProperties?: boolean | ConfigSchema;
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
  */