@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.
Files changed (106) hide show
  1. package/README.md +127 -15
  2. package/dist/configEntry/configEntry.type.d.ts +5 -1
  3. package/dist/configEntry/configEntry.type.js +5 -10
  4. package/dist/configFactory/configFactory.core.d.ts +4 -6
  5. package/dist/configLoader/configLoader.core.d.ts +48 -4
  6. package/dist/configLoader/configLoader.core.js +56 -37
  7. package/dist/configManager/configManager.core.d.ts +35 -3
  8. package/dist/configManager/configManager.core.js +109 -16
  9. package/dist/configManager/configManager.type.d.ts +12 -0
  10. package/dist/configResolver/accessors/configResolver.scoped.d.ts +1 -1
  11. package/dist/configResolver/accessors/index.d.ts +1 -2
  12. package/dist/configResolver/accessors/index.js +1 -2
  13. package/dist/configResolver/core/configResolver.core.d.ts +1 -1
  14. package/dist/configResolver/core/configResolver.core.js +24 -7
  15. package/dist/configSchema/configSchema.type.d.ts +44 -11
  16. package/dist/configSchema/configSchema.validator.d.ts +3 -3
  17. package/dist/configSchema/configSchema.validator.js +58 -16
  18. package/dist/configSchema/index.d.ts +1 -1
  19. package/dist/configSchema/index.js +1 -1
  20. package/dist/configSource/configSource.core.d.ts +28 -11
  21. package/dist/configSource/configSource.core.js +43 -17
  22. package/dist/configSource/configSource.environment.d.ts +66 -0
  23. package/dist/configSource/configSource.environment.js +75 -0
  24. package/dist/configSource/index.d.ts +1 -0
  25. package/dist/configSource/index.js +1 -0
  26. package/dist/configStore/configStore.core.d.ts +15 -0
  27. package/dist/configStore/configStore.core.js +35 -5
  28. package/dist/configStore/configStore.type.d.ts +11 -0
  29. package/dist/configValue/configValue.core.d.ts +38 -0
  30. package/dist/configValue/configValue.core.js +78 -7
  31. package/package.json +12 -5
  32. package/dist/.tsbuildinfo +0 -1
  33. package/dist/configEntry/configEntry.type.d.ts.map +0 -1
  34. package/dist/configEntry/configEntry.type.js.map +0 -1
  35. package/dist/configEntry/index.d.ts.map +0 -1
  36. package/dist/configEntry/index.js.map +0 -1
  37. package/dist/configFactory/configFactory.core.d.ts.map +0 -1
  38. package/dist/configFactory/configFactory.core.js.map +0 -1
  39. package/dist/configFactory/index.d.ts.map +0 -1
  40. package/dist/configFactory/index.js.map +0 -1
  41. package/dist/configLoader/configLoader.core.d.ts.map +0 -1
  42. package/dist/configLoader/configLoader.core.js.map +0 -1
  43. package/dist/configLoader/index.d.ts.map +0 -1
  44. package/dist/configLoader/index.js.map +0 -1
  45. package/dist/configManager/configManager.core.d.ts.map +0 -1
  46. package/dist/configManager/configManager.core.js.map +0 -1
  47. package/dist/configManager/configManager.error.d.ts.map +0 -1
  48. package/dist/configManager/configManager.error.js.map +0 -1
  49. package/dist/configManager/configManager.factory.d.ts.map +0 -1
  50. package/dist/configManager/configManager.factory.js.map +0 -1
  51. package/dist/configManager/configManager.type.d.ts.map +0 -1
  52. package/dist/configManager/configManager.type.js.map +0 -1
  53. package/dist/configManager/index.d.ts.map +0 -1
  54. package/dist/configManager/index.js.map +0 -1
  55. package/dist/configResolver/accessors/configResolver.accessors.d.ts +0 -19
  56. package/dist/configResolver/accessors/configResolver.accessors.d.ts.map +0 -1
  57. package/dist/configResolver/accessors/configResolver.accessors.js +0 -237
  58. package/dist/configResolver/accessors/configResolver.accessors.js.map +0 -1
  59. package/dist/configResolver/accessors/configResolver.scoped.d.ts.map +0 -1
  60. package/dist/configResolver/accessors/configResolver.scoped.js.map +0 -1
  61. package/dist/configResolver/accessors/index.d.ts.map +0 -1
  62. package/dist/configResolver/accessors/index.js.map +0 -1
  63. package/dist/configResolver/core/configResolver.core.d.ts.map +0 -1
  64. package/dist/configResolver/core/configResolver.core.js.map +0 -1
  65. package/dist/configResolver/core/configResolver.error.d.ts.map +0 -1
  66. package/dist/configResolver/core/configResolver.error.js.map +0 -1
  67. package/dist/configResolver/core/configResolver.factory.d.ts.map +0 -1
  68. package/dist/configResolver/core/configResolver.factory.js.map +0 -1
  69. package/dist/configResolver/core/configResolver.type.d.ts.map +0 -1
  70. package/dist/configResolver/core/configResolver.type.js.map +0 -1
  71. package/dist/configResolver/core/index.d.ts.map +0 -1
  72. package/dist/configResolver/core/index.js.map +0 -1
  73. package/dist/configResolver/index.d.ts.map +0 -1
  74. package/dist/configResolver/index.js.map +0 -1
  75. package/dist/configSchema/configSchema.core.d.ts +0 -8
  76. package/dist/configSchema/configSchema.core.d.ts.map +0 -1
  77. package/dist/configSchema/configSchema.core.js +0 -8
  78. package/dist/configSchema/configSchema.core.js.map +0 -1
  79. package/dist/configSchema/configSchema.rules.d.ts +0 -18
  80. package/dist/configSchema/configSchema.rules.d.ts.map +0 -1
  81. package/dist/configSchema/configSchema.rules.js +0 -108
  82. package/dist/configSchema/configSchema.rules.js.map +0 -1
  83. package/dist/configSchema/configSchema.type.d.ts.map +0 -1
  84. package/dist/configSchema/configSchema.type.js.map +0 -1
  85. package/dist/configSchema/configSchema.validator.d.ts.map +0 -1
  86. package/dist/configSchema/configSchema.validator.js.map +0 -1
  87. package/dist/configSchema/index.d.ts.map +0 -1
  88. package/dist/configSchema/index.js.map +0 -1
  89. package/dist/configSource/configSource.core.d.ts.map +0 -1
  90. package/dist/configSource/configSource.core.js.map +0 -1
  91. package/dist/configSource/index.d.ts.map +0 -1
  92. package/dist/configSource/index.js.map +0 -1
  93. package/dist/configStore/configStore.core.d.ts.map +0 -1
  94. package/dist/configStore/configStore.core.js.map +0 -1
  95. package/dist/configStore/configStore.factory.d.ts.map +0 -1
  96. package/dist/configStore/configStore.factory.js.map +0 -1
  97. package/dist/configStore/configStore.type.d.ts.map +0 -1
  98. package/dist/configStore/configStore.type.js.map +0 -1
  99. package/dist/configStore/index.d.ts.map +0 -1
  100. package/dist/configStore/index.js.map +0 -1
  101. package/dist/configValue/configValue.core.d.ts.map +0 -1
  102. package/dist/configValue/configValue.core.js.map +0 -1
  103. package/dist/configValue/index.d.ts.map +0 -1
  104. package/dist/configValue/index.js.map +0 -1
  105. package/dist/index.d.ts.map +0 -1
  106. package/dist/index.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { cloneConfigValue } from "../configValue/configValue.core.js";
2
- import { ConfigValueType, validateConfigObject, } from "../configSchema/configSchema.core.js";
2
+ import { ConfigValueType, validateConfigObject, } from "../configSchema/index.js";
3
3
  import { createConfigSource } from "../configSource/configSource.core.js";
4
4
  import { createConfigLoader } from "../configLoader/configLoader.core.js";
5
5
  import { createConfigStore } from "../configStore/configStore.factory.js";
@@ -13,6 +13,12 @@ import { ConfigManagerValidationError } from "./configManager.error.js";
13
13
  * keeping their responsibilities separate.
14
14
  */
15
15
  export class ConfigManager {
16
+ /**
17
+ * Human-readable name for this manager, surfaced in status
18
+ * snapshots so applications running several managers can tell
19
+ * their diagnostics apart.
20
+ */
21
+ name;
16
22
  store;
17
23
  loader;
18
24
  resolver;
@@ -21,26 +27,52 @@ export class ConfigManager {
21
27
  lastLoadedAt;
22
28
  lastError;
23
29
  disposed = false;
30
+ loadPromise;
24
31
  constructor(options = {}) {
32
+ // When a caller supplies a loader (and no explicit store), the
33
+ // manager must resolve values from the SAME store that loader
34
+ // writes to.
35
+ const providedLoader = options.loader;
36
+ const suppliedStore = options.store ?? providedLoader?.getStore();
25
37
  this.store =
26
- options.store ??
38
+ suppliedStore ??
27
39
  createConfigStore({
28
40
  initialValues: options.initialValues,
29
41
  freeze: options.freeze ?? true,
30
42
  });
43
+ // initialValues used to be wired ONLY into a store the manager
44
+ // created itself: passing `store` or `loader` silently discarded
45
+ // them. Seed a supplied store explicitly instead.
46
+ if (suppliedStore && options.initialValues) {
47
+ for (const [key, value] of Object.entries(options.initialValues)) {
48
+ suppliedStore.set(key, value, { source: "initialValues" });
49
+ }
50
+ }
51
+ this.name = options.name ?? "config";
31
52
  this.loader =
32
- options.loader ??
53
+ providedLoader ??
33
54
  createConfigLoader({
34
55
  sources: options.sources,
35
56
  context: options.context,
36
57
  store: this.store,
37
58
  freeze: options.freeze ?? true,
59
+ // The manager seeds its store with initialValues and accepts
60
+ // runtime set() writes; loading must layer sources on top of
61
+ // those (by priority) instead of wiping them.
62
+ clearStore: false,
38
63
  });
39
64
  this.resolver = createConfigResolver(this.store, {
40
65
  strict: options.strict ?? true,
41
66
  allowUndefined: options.allowUndefined ?? true,
42
67
  clone: options.clone ?? false,
43
68
  });
69
+ if (options.autoLoad) {
70
+ // Kick off the initial load immediately; ready() (or load())
71
+ // exposes the in-flight promise. The catch handler prevents an
72
+ // unhandled rejection when nobody awaits ready(): the failure
73
+ // is still recorded in the FAILED state and lastError.
74
+ this.load().catch(() => { });
75
+ }
44
76
  }
45
77
  /**
46
78
  * Returns the current lifecycle state.
@@ -91,25 +123,40 @@ export class ConfigManager {
91
123
  }
92
124
  /**
93
125
  * Loads configuration.
126
+ *
127
+ * Calling load() while a load is already in flight returns the
128
+ * in-flight promise instead of starting a second load.
94
129
  */
95
130
  async load() {
96
131
  this.assertActive();
97
132
  if (this.isLoading) {
133
+ if (this.loadPromise) {
134
+ return this.loadPromise;
135
+ }
98
136
  throw new Error("Configuration manager is already loading.");
99
137
  }
100
138
  this.setState(ConfigManagerState.LOADING);
101
- try {
102
- const result = await this.loader.load();
103
- this.lastLoadedAt = result.loadedAt;
104
- this.lastError = undefined;
105
- this.setState(ConfigManagerState.READY);
106
- return result;
139
+ const promise = this.performLoad(false);
140
+ this.loadPromise = promise;
141
+ return promise;
142
+ }
143
+ /**
144
+ * Waits until the initial configuration load has completed.
145
+ *
146
+ * With `autoLoad: true` this awaits the load started by the
147
+ * constructor (rethrowing its failure). Otherwise it starts a load
148
+ * when none has happened yet.
149
+ */
150
+ async ready() {
151
+ this.assertActive();
152
+ if (this.loadPromise) {
153
+ await this.loadPromise;
154
+ return;
107
155
  }
108
- catch (error) {
109
- this.lastError = error;
110
- this.setState(ConfigManagerState.FAILED);
111
- throw error;
156
+ if (this.isReady) {
157
+ return;
112
158
  }
159
+ await this.load();
113
160
  }
114
161
  /**
115
162
  * Reloads configuration.
@@ -120,16 +167,34 @@ export class ConfigManager {
120
167
  throw new Error("Configuration manager is already loading.");
121
168
  }
122
169
  this.setState(ConfigManagerState.RELOADING);
170
+ const promise = this.performLoad(true);
171
+ this.loadPromise = promise;
172
+ return promise;
173
+ }
174
+ /**
175
+ * Executes a load or reload while keeping lifecycle state safe.
176
+ *
177
+ * If the manager is disposed while the load is in flight, no state
178
+ * transition happens afterwards: a disposed manager stays DISPOSED.
179
+ */
180
+ async performLoad(reload) {
123
181
  try {
124
- const result = await this.loader.reload();
182
+ const result = reload
183
+ ? await this.loader.reload()
184
+ : await this.loader.load();
185
+ if (this.disposed) {
186
+ return result;
187
+ }
125
188
  this.lastLoadedAt = result.loadedAt;
126
189
  this.lastError = undefined;
127
190
  this.setState(ConfigManagerState.READY);
128
191
  return result;
129
192
  }
130
193
  catch (error) {
131
- this.lastError = error;
132
- this.setState(ConfigManagerState.FAILED);
194
+ if (!this.disposed) {
195
+ this.lastError = error;
196
+ this.setState(ConfigManagerState.FAILED);
197
+ }
133
198
  throw error;
134
199
  }
135
200
  }
@@ -146,6 +211,23 @@ export class ConfigManager {
146
211
  if (!result.valid) {
147
212
  throw new ConfigManagerValidationError(result.issues);
148
213
  }
214
+ // Property schemas flagged `secret` mark the corresponding store
215
+ // entries as sensitive so safe serialization redacts them.
216
+ for (const [key, propertySchema] of Object.entries(schema.properties)) {
217
+ if (!propertySchema.secret) {
218
+ continue;
219
+ }
220
+ const entry = this.store.getEntry(key);
221
+ if (entry && !entry.sensitive) {
222
+ this.store.set(key, entry.value, {
223
+ source: entry.source,
224
+ sourceType: entry.sourceType,
225
+ priority: entry.priority,
226
+ sensitive: true,
227
+ resolved: entry.resolved,
228
+ });
229
+ }
230
+ }
149
231
  return cloneConfigValue(result.value);
150
232
  }
151
233
  /**
@@ -238,16 +320,27 @@ export class ConfigManager {
238
320
  }
239
321
  /**
240
322
  * Returns the complete configuration object.
323
+ *
324
+ * WARNING: values are returned RAW — sensitive entries are NOT
325
+ * redacted. Use toSafeObject() for logging or diagnostics.
241
326
  */
242
327
  toObject() {
243
328
  this.assertActive();
244
329
  return this.store.toObject();
245
330
  }
331
+ /**
332
+ * Returns the configuration object with sensitive values redacted.
333
+ */
334
+ toSafeObject() {
335
+ this.assertActive();
336
+ return this.store.toSafeObject();
337
+ }
246
338
  /**
247
339
  * Returns a manager status snapshot.
248
340
  */
249
341
  getStatus() {
250
342
  return {
343
+ name: this.name,
251
344
  state: this.state,
252
345
  loaded: this.isReady,
253
346
  loading: this.isLoading,
@@ -18,11 +18,22 @@ export declare enum ConfigManagerState {
18
18
  * Configuration manager options.
19
19
  */
20
20
  export interface ConfigManagerOptions extends ConfigResolverOptions {
21
+ /**
22
+ * Human-readable name for the manager. Surfaced by
23
+ * `manager.name` and in `getStatus()`.
24
+ */
25
+ readonly name?: string;
21
26
  readonly sources?: readonly ConfigSource[];
22
27
  readonly initialValues?: Readonly<Record<string, ConfigValue>>;
23
28
  readonly store?: ConfigStore;
24
29
  readonly loader?: ConfigLoader;
25
30
  readonly freeze?: boolean;
31
+ /**
32
+ * When true, the manager starts loading configuration immediately
33
+ * on construction. Await `manager.ready()` to wait for (and surface
34
+ * errors from) that initial load; `load()` called while the initial
35
+ * load is in flight reuses the same promise.
36
+ */
26
37
  readonly autoLoad?: boolean;
27
38
  readonly context?: {
28
39
  readonly environment?: string;
@@ -34,6 +45,7 @@ export interface ConfigManagerOptions extends ConfigResolverOptions {
34
45
  * Configuration manager status.
35
46
  */
36
47
  export interface ConfigManagerStatus {
48
+ readonly name: string;
37
49
  readonly state: ConfigManagerState;
38
50
  readonly loaded: boolean;
39
51
  readonly loading: boolean;
@@ -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 } from "../../configSchema/index.js";
3
3
  import type { ConfigResolver } from "../core/configResolver.core.js";
4
4
  /**
5
5
  * Resolver scoped to a configuration key prefix.
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * @zudojs/config/configResolver/accessors
3
3
  *
4
- * Configuration accessors and scoped resolver.
4
+ * Scoped configuration resolver.
5
5
  */
6
- export * from "./configResolver.accessors.js";
7
6
  export * from "./configResolver.scoped.js";
8
7
  //# sourceMappingURL=index.d.ts.map
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * @zudojs/config/configResolver/accessors
3
3
  *
4
- * Configuration accessors and scoped resolver.
4
+ * Scoped configuration resolver.
5
5
  */
6
- export * from "./configResolver.accessors.js";
7
6
  export * from "./configResolver.scoped.js";
8
7
  //# sourceMappingURL=index.js.map
@@ -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 } from "../../configSchema/index.js";
3
3
  import type { ConfigStore } from "../../configStore/configStore.core.js";
4
4
  import type { ConfigResolverOptions, ConfigResolutionResult } from "./configResolver.type.js";
5
5
  import { ScopedConfigResolver } from "../accessors/configResolver.scoped.js";
@@ -1,5 +1,5 @@
1
- import { parseConfigBigInt, parseConfigBoolean, parseConfigDate, parseConfigNumber, } from "../../configValue/configValue.core.js";
2
- import { validateConfigValue } from "../../configSchema/configSchema.core.js";
1
+ import { defineConfigProperty, parseConfigBigInt, parseConfigBoolean, parseConfigDate, parseConfigNumber, } from "../../configValue/configValue.core.js";
2
+ import { validateConfigValue } from "../../configSchema/index.js";
3
3
  import { ScopedConfigResolver } from "../accessors/configResolver.scoped.js";
4
4
  /**
5
5
  * Resolves typed configuration values from a ConfigStore.
@@ -34,8 +34,19 @@ export class ConfigResolver {
34
34
  path: key,
35
35
  root: value,
36
36
  });
37
- if (!result.valid && this.options.strict) {
38
- throw new ConfigResolutionError(key, result.issues);
37
+ if (!result.valid) {
38
+ if (this.options.strict) {
39
+ throw new ConfigResolutionError(key, result.issues);
40
+ }
41
+ // Non-strict mode: never return an invalid value. Fall back to
42
+ // the schema default when present, otherwise undefined.
43
+ const fallback = typeof schema.default === "function"
44
+ ? schema.default()
45
+ : schema.default;
46
+ if (fallback === undefined && !this.options.allowUndefined) {
47
+ throw new ConfigResolutionError(key, result.issues);
48
+ }
49
+ return this.prepareValue(fallback);
39
50
  }
40
51
  if (result.value === undefined && !this.options.allowUndefined) {
41
52
  throw new ConfigResolutionError(key, [
@@ -211,7 +222,13 @@ export class ConfigResolver {
211
222
  return fallback;
212
223
  }
213
224
  if (value instanceof Date) {
214
- return parseConfigDate(value);
225
+ const parsed = parseConfigDate(value);
226
+ if (parsed !== undefined) {
227
+ return parsed;
228
+ }
229
+ // Invalid Date instances route through the same strict/fallback
230
+ // handling as any other invalid type.
231
+ return this.invalidType(key, "date", value, fallback);
215
232
  }
216
233
  if (typeof value === "string") {
217
234
  const parsed = parseConfigDate(value);
@@ -274,7 +291,7 @@ export class ConfigResolver {
274
291
  for (const key of keys) {
275
292
  const value = this.get(key);
276
293
  if (value !== undefined) {
277
- result[key] = value;
294
+ defineConfigProperty(result, key, value);
278
295
  }
279
296
  }
280
297
  return Object.freeze(result);
@@ -309,7 +326,7 @@ export class ConfigResolver {
309
326
  }
310
327
  const result = {};
311
328
  for (const [key, child] of Object.entries(value)) {
312
- result[key] = this.prepareValue(child);
329
+ defineConfigProperty(result, key, this.prepareValue(child));
313
330
  }
314
331
  return result;
315
332
  }
@@ -57,24 +57,65 @@ export interface ConfigSchema<T extends ConfigValue = ConfigValue> {
57
57
  readonly nullable?: boolean;
58
58
  readonly default?: T | (() => T);
59
59
  readonly description?: string;
60
+ /**
61
+ * Marks the value as secret. When a ConfigManager validates its
62
+ * configuration, store entries whose property schema is flagged
63
+ * `secret` are marked sensitive, so toSafeObject() and safe entry
64
+ * serialization redact them.
65
+ */
60
66
  readonly secret?: boolean;
61
67
  readonly validate?: (value: T, context: ConfigValidationContext) => boolean | string | ConfigValidationIssue | readonly ConfigValidationIssue[];
62
68
  readonly transform?: (value: ConfigValue, context: ConfigValidationContext) => T;
63
69
  }
70
+ /**
71
+ * A schema for a nested value whose value type the container does not
72
+ * know.
73
+ *
74
+ * `ConfigSchema<T>` is contravariant in `T` through `validate(value: T,
75
+ * ...)` and `transform`, so a `ConfigStringSchema` (which extends
76
+ * `ConfigSchema<string>`) is NOT assignable to `ConfigSchema<ConfigValue>`:
77
+ * a validator that accepts only `string` cannot stand in for one that must
78
+ * accept any `ConfigValue`.
79
+ *
80
+ * That made typed object schemas inexpressible. Declaring `properties` as
81
+ * `Record<string, ConfigSchema>` rejected every specific variant, including
82
+ * through a typed intermediate constant, so there was no user-side
83
+ * workaround short of a cast:
84
+ *
85
+ * ```ts
86
+ * const schema: ConfigObjectSchema = {
87
+ * type: ConfigValueType.OBJECT,
88
+ * // previously: not assignable to ConfigSchema<ConfigValue>
89
+ * properties: { port: { type: ConfigValueType.NUMBER, min: 1 } },
90
+ * };
91
+ * ```
92
+ *
93
+ * A container of schemas over differing value types is an existential
94
+ * type, which TypeScript cannot express. `ConfigSchema<any>` is the
95
+ * deliberate encoding of that: `any` is assignable in both directions, so
96
+ * every variant is accepted and the validator can still invoke `validate`
97
+ * on what it reads back out.
98
+ *
99
+ * The named variants are unioned in as well so an inline literal may carry
100
+ * variant-specific fields (`minLength`, `min`, `max`, `enum`). Excess
101
+ * property checking against a union admits any property declared by some
102
+ * member, which a bare `ConfigSchema<any>` would reject.
103
+ */
104
+ export type AnyConfigSchema = ConfigSchema<any> | ConfigStringSchema | ConfigNumberSchema | ConfigBooleanSchema | ConfigObjectSchema | ConfigArraySchema;
64
105
  /**
65
106
  * Object configuration schema.
66
107
  */
67
108
  export interface ConfigObjectSchema<T extends ConfigValue = ConfigValue> extends ConfigSchema<T> {
68
109
  readonly type: ConfigValueType.OBJECT;
69
- readonly properties: Readonly<Record<string, ConfigSchema>>;
70
- readonly additionalProperties?: boolean | ConfigSchema;
110
+ readonly properties: Readonly<Record<string, AnyConfigSchema>>;
111
+ readonly additionalProperties?: boolean | AnyConfigSchema;
71
112
  }
72
113
  /**
73
114
  * Array configuration schema.
74
115
  */
75
116
  export interface ConfigArraySchema<T extends ConfigValue = ConfigValue> extends ConfigSchema<T> {
76
117
  readonly type: ConfigValueType.ARRAY;
77
- readonly items?: ConfigSchema;
118
+ readonly items?: AnyConfigSchema;
78
119
  readonly minItems?: number;
79
120
  readonly maxItems?: number;
80
121
  }
@@ -104,12 +145,4 @@ export interface ConfigNumberSchema extends ConfigSchema<number> {
104
145
  export interface ConfigBooleanSchema extends ConfigSchema<boolean> {
105
146
  readonly type: ConfigValueType.BOOLEAN;
106
147
  }
107
- /**
108
- * Schema definition shorthand.
109
- */
110
- export type ConfigSchemaDefinition = ConfigSchema | ConfigSchemaDefinition[] | ConfigSchemaBuilder;
111
- /**
112
- * Schema builder function.
113
- */
114
- export type ConfigSchemaBuilder = () => ConfigSchema;
115
148
  //# sourceMappingURL=configSchema.type.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import type { ConfigValue } from "../configValue/configValue.core.js";
2
- import type { ConfigSchema, ConfigValidationContext, ConfigValidationIssue, ConfigValidationResult } from "./configSchema.type.js";
2
+ import type { AnyConfigSchema, ConfigValidationContext, ConfigValidationIssue, ConfigValidationResult } from "./configSchema.type.js";
3
3
  import { ConfigValueType } from "./configSchema.type.js";
4
4
  import type { ConfigObjectSchema } from "./configSchema.type.js";
5
5
  /**
@@ -17,7 +17,7 @@ export declare function createConfigValidationIssue(path: string, message: strin
17
17
  /**
18
18
  * Validates a value against a schema.
19
19
  */
20
- export declare function validateConfigValue(value: unknown, schema: ConfigSchema, context?: Partial<ConfigValidationContext>): ConfigValidationResult;
20
+ export declare function validateConfigValue(value: unknown, schema: AnyConfigSchema, context?: Partial<ConfigValidationContext>): ConfigValidationResult;
21
21
  /**
22
22
  * Validates an entire configuration object.
23
23
  */
@@ -25,7 +25,7 @@ export declare function validateConfigObject(value: Readonly<Record<string, unkn
25
25
  /**
26
26
  * Validates an object against a schema and throws when invalid.
27
27
  */
28
- export declare function assertValidConfig(value: unknown, schema: ConfigSchema): ConfigValue;
28
+ export declare function assertValidConfig(value: unknown, schema: AnyConfigSchema): ConfigValue;
29
29
  import { ConfigurationError } from "@zudojs/errors";
30
30
  /**
31
31
  * Error thrown when schema validation fails.
@@ -1,3 +1,4 @@
1
+ import { defineConfigProperty, readOwnConfigProperty, } from "../configValue/configValue.core.js";
1
2
  import { ConfigValidationSeverity, ConfigValueType, } from "./configSchema.type.js";
2
3
  /**
3
4
  * Returns the runtime configuration value type.
@@ -121,7 +122,7 @@ function appendCustomValidationResult(result, path, issues) {
121
122
  issues.push(...result);
122
123
  return;
123
124
  }
124
- issues.push(...(Array.isArray(result) ? result : [result]));
125
+ issues.push(result);
125
126
  }
126
127
  /**
127
128
  * Built-in schema validation rules.
@@ -209,11 +210,17 @@ export function validateConfigValue(value, schema, context) {
209
210
  issues,
210
211
  };
211
212
  }
212
- return {
213
- valid: true,
214
- value: resolveDefaultValue(schema),
215
- issues,
216
- };
213
+ const defaultValue = resolveDefaultValue(schema);
214
+ if (defaultValue === undefined) {
215
+ return {
216
+ valid: true,
217
+ issues,
218
+ };
219
+ }
220
+ // Applied defaults run through the same validation and transform
221
+ // pipeline as supplied values (defaultValue is defined here, so
222
+ // this recursion terminates immediately).
223
+ return validateConfigValue(defaultValue, schema, context);
217
224
  }
218
225
  if (value === null && schema.nullable) {
219
226
  return {
@@ -233,22 +240,49 @@ export function validateConfigValue(value, schema, context) {
233
240
  };
234
241
  }
235
242
  validateBuiltInRules(value, schema, validationContext, issues);
243
+ // An object schema carries `properties` / `additionalProperties`.
244
+ // Those were declared on ConfigObjectSchema but read nowhere in this
245
+ // function, so every nested object schema — including the ones used
246
+ // by ConfigResolver.resolve() and ConfigManager.resolve() — passed
247
+ // validation unconditionally. Delegate to validateConfigObject so
248
+ // nested constraints are actually enforced.
249
+ const objectSchema = schema;
250
+ let base = value;
251
+ if (matchesConfigType(value, ConfigValueType.OBJECT) &&
252
+ (objectSchema.properties !== undefined ||
253
+ objectSchema.additionalProperties !== undefined)) {
254
+ const nested = validateConfigObject(value, {
255
+ type: ConfigValueType.OBJECT,
256
+ properties: objectSchema.properties ?? {},
257
+ additionalProperties: objectSchema.additionalProperties,
258
+ }, path);
259
+ issues.push(...nested.issues);
260
+ if (nested.value !== undefined) {
261
+ base = nested.value;
262
+ }
263
+ }
236
264
  if (schema.validate) {
237
- const result = schema.validate(value, validationContext);
265
+ const result = schema.validate(base, validationContext);
238
266
  appendCustomValidationResult(result, path, issues);
239
267
  }
240
- let transformed = value;
241
- if (schema.transform) {
268
+ let transformed = base;
269
+ const hasErrors = () => issues.some((issue) => issue.severity === ConfigValidationSeverity.ERROR);
270
+ // Transforms only run on values that passed validation; running
271
+ // them on invalid input would surface invalid values to callers.
272
+ if (schema.transform && !hasErrors()) {
242
273
  try {
243
- transformed = schema.transform(value, validationContext);
274
+ transformed = schema.transform(base, validationContext);
244
275
  }
245
276
  catch (error) {
246
277
  issues.push(createConfigValidationIssue(path, error instanceof Error ? error.message : String(error), "TRANSFORM_FAILED"));
247
278
  }
248
279
  }
280
+ const valid = !hasErrors();
249
281
  return {
250
- valid: !issues.some((issue) => issue.severity === ConfigValidationSeverity.ERROR),
251
- value: transformed,
282
+ valid,
283
+ // Invalid values are never returned; callers fall back to the
284
+ // schema default or undefined instead.
285
+ value: valid ? transformed : undefined,
252
286
  issues,
253
287
  };
254
288
  }
@@ -271,7 +305,11 @@ export function validateConfigObject(value, schema, path = "$") {
271
305
  const result = {};
272
306
  for (const [key, propertySchema] of Object.entries(schema.properties)) {
273
307
  const propertyPath = `${path}.${key}`;
274
- const propertyResult = validateConfigValue(value[key], propertySchema, {
308
+ // Own-property read only: bracket access would walk the prototype
309
+ // chain, so a schema property named "constructor" or "toString"
310
+ // would validate an inherited function instead of reporting a
311
+ // missing value.
312
+ const propertyResult = validateConfigValue(readOwnConfigProperty(value, key), propertySchema, {
275
313
  path: propertyPath,
276
314
  root: value,
277
315
  parent: value,
@@ -279,7 +317,7 @@ export function validateConfigObject(value, schema, path = "$") {
279
317
  });
280
318
  issues.push(...propertyResult.issues);
281
319
  if (propertyResult.value !== undefined) {
282
- result[key] = propertyResult.value;
320
+ defineConfigProperty(result, key, propertyResult.value);
283
321
  }
284
322
  }
285
323
  for (const [key, child] of Object.entries(value)) {
@@ -300,11 +338,15 @@ export function validateConfigObject(value, schema, path = "$") {
300
338
  });
301
339
  issues.push(...propertyResult.issues);
302
340
  if (propertyResult.value !== undefined) {
303
- result[key] = propertyResult.value;
341
+ defineConfigProperty(result, key, propertyResult.value);
304
342
  }
305
343
  }
306
344
  else {
307
- result[key] = child;
345
+ // Own "__proto__"/"constructor"/"prototype" keys (e.g. from
346
+ // JSON.parse of untrusted input) are kept as own data
347
+ // properties: plain assignment would reach the inherited
348
+ // "__proto__" setter and mutate the result's prototype.
349
+ defineConfigProperty(result, key, child);
308
350
  }
309
351
  }
310
352
  return {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @zudojs/config/configSchema
3
3
  *
4
- * Configuration schema validation with Zod.
4
+ * Dependency-free configuration schema validation.
5
5
  */
6
6
  export * from "./configSchema.type.js";
7
7
  export * from "./configSchema.validator.js";
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @zudojs/config/configSchema
3
3
  *
4
- * Configuration schema validation with Zod.
4
+ * Dependency-free configuration schema validation.
5
5
  */
6
6
  export * from "./configSchema.type.js";
7
7
  export * from "./configSchema.validator.js";