@zudojs/config 0.1.0 → 1.0.1

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 +89 -20
  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 +23 -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
@@ -17,16 +17,6 @@ export declare enum ConfigSourceType {
17
17
  REMOTE = "remote",
18
18
  CUSTOM = "custom"
19
19
  }
20
- /**
21
- * A single configuration value supplied by a source.
22
- */
23
- export interface ConfigSourceEntry {
24
- readonly key: string;
25
- readonly value: ConfigValue;
26
- readonly source: string;
27
- readonly priority?: number;
28
- readonly sensitive?: boolean;
29
- }
30
20
  /**
31
21
  * Context supplied to a configuration source while loading values.
32
22
  */
@@ -42,6 +32,13 @@ export interface ConfigSourceResult {
42
32
  readonly values: Readonly<Record<string, ConfigValue>>;
43
33
  readonly source: string;
44
34
  readonly type: ConfigSourceType;
35
+ /**
36
+ * Keys within `values` whose entries must be marked sensitive.
37
+ *
38
+ * Sensitive entries are redacted by safe serialization paths such
39
+ * as ConfigStore.toSafeObject() and toSafeConfigEntry().
40
+ */
41
+ readonly sensitiveKeys?: readonly string[];
45
42
  }
46
43
  /**
47
44
  * Configuration source contract.
@@ -100,12 +97,21 @@ export declare function createDefaultsConfigSource(values: Readonly<Record<strin
100
97
  export declare function createCustomConfigSource(name: string, loader: ConfigSourceLoader, options?: Omit<ConfigSourceOptions, "name" | "type">): FunctionConfigSource;
101
98
  /**
102
99
  * Normalizes a source result.
100
+ *
101
+ * Note on reference semantics: the returned `values` record is a
102
+ * frozen SHALLOW copy — nested objects and arrays are still shared
103
+ * with the object the source returned. Consumers that need isolation
104
+ * (e.g. the loader/store with freeze enabled) clone values before
105
+ * storing them.
103
106
  */
104
107
  export declare function normalizeConfigSourceResult(result: ConfigSourceResult, fallbackName: string, fallbackType: ConfigSourceType): ConfigSourceResult;
105
108
  /**
106
109
  * Sorts configuration sources by priority.
107
110
  *
108
- * Higher-priority sources are returned first.
111
+ * Higher-priority sources are returned first. Sources with equal
112
+ * priority keep their REGISTRATION ORDER (the sort is stable), so
113
+ * among equal priorities the last-registered source is applied last
114
+ * and wins.
109
115
  */
110
116
  export declare function sortConfigSources(sources: readonly ConfigSource[]): readonly ConfigSource[];
111
117
  /**
@@ -124,6 +130,17 @@ export declare function deduplicateConfigSources(sources: readonly ConfigSource[
124
130
  * Optional sources are allowed to fail without preventing startup.
125
131
  */
126
132
  export declare function loadConfigSource(source: ConfigSource, context?: ConfigSourceContext): Promise<ConfigSourceResult | undefined>;
133
+ /**
134
+ * Loads a single configuration source WITHOUT swallowing failures.
135
+ *
136
+ * An unavailable optional source still resolves to `undefined`, but
137
+ * every other failure propagates so callers can route it to their own
138
+ * error handling (ConfigLoader forwards it to `onSourceError`).
139
+ *
140
+ * This is the single implementation of the source loading contract;
141
+ * `loadConfigSource` is the error-swallowing wrapper around it.
142
+ */
143
+ export declare function loadConfigSourceStrict(source: ConfigSource, context?: ConfigSourceContext): Promise<ConfigSourceResult | undefined>;
127
144
  /**
128
145
  * Loads multiple configuration sources in priority order.
129
146
  */
@@ -99,6 +99,12 @@ export function createCustomConfigSource(name, loader, options = {}) {
99
99
  }
100
100
  /**
101
101
  * Normalizes a source result.
102
+ *
103
+ * Note on reference semantics: the returned `values` record is a
104
+ * frozen SHALLOW copy — nested objects and arrays are still shared
105
+ * with the object the source returned. Consumers that need isolation
106
+ * (e.g. the loader/store with freeze enabled) clone values before
107
+ * storing them.
102
108
  */
103
109
  export function normalizeConfigSourceResult(result, fallbackName, fallbackType) {
104
110
  if (!result || typeof result !== "object") {
@@ -115,20 +121,23 @@ export function normalizeConfigSourceResult(result, fallbackName, fallbackType)
115
121
  values: Object.freeze({
116
122
  ...result.values,
117
123
  }),
124
+ ...(result.sensitiveKeys
125
+ ? { sensitiveKeys: Object.freeze([...result.sensitiveKeys]) }
126
+ : {}),
118
127
  });
119
128
  }
120
129
  /**
121
130
  * Sorts configuration sources by priority.
122
131
  *
123
- * Higher-priority sources are returned first.
132
+ * Higher-priority sources are returned first. Sources with equal
133
+ * priority keep their REGISTRATION ORDER (the sort is stable), so
134
+ * among equal priorities the last-registered source is applied last
135
+ * and wins.
124
136
  */
125
137
  export function sortConfigSources(sources) {
126
- return [...sources].sort((left, right) => {
127
- if (right.priority !== left.priority) {
128
- return right.priority - left.priority;
129
- }
130
- return left.name.localeCompare(right.name);
131
- });
138
+ // Array.prototype.sort is stable, so equal priorities preserve the
139
+ // original registration order.
140
+ return [...sources].sort((left, right) => right.priority - left.priority);
132
141
  }
133
142
  /**
134
143
  * Finds a source by name.
@@ -160,16 +169,7 @@ export function deduplicateConfigSources(sources) {
160
169
  */
161
170
  export async function loadConfigSource(source, context = {}) {
162
171
  try {
163
- if (source.isAvailable) {
164
- const available = await source.isAvailable(context);
165
- if (!available) {
166
- if (source.optional) {
167
- return undefined;
168
- }
169
- throw new Error(`Configuration source "${source.name}" is unavailable.`);
170
- }
171
- }
172
- return await source.load(context);
172
+ return await loadConfigSourceStrict(source, context);
173
173
  }
174
174
  catch (error) {
175
175
  if (source.optional) {
@@ -178,6 +178,32 @@ export async function loadConfigSource(source, context = {}) {
178
178
  throw error;
179
179
  }
180
180
  }
181
+ /**
182
+ * Loads a single configuration source WITHOUT swallowing failures.
183
+ *
184
+ * An unavailable optional source still resolves to `undefined`, but
185
+ * every other failure propagates so callers can route it to their own
186
+ * error handling (ConfigLoader forwards it to `onSourceError`).
187
+ *
188
+ * This is the single implementation of the source loading contract;
189
+ * `loadConfigSource` is the error-swallowing wrapper around it.
190
+ */
191
+ export async function loadConfigSourceStrict(source, context = {}) {
192
+ if (source.isAvailable) {
193
+ const available = await source.isAvailable(context);
194
+ if (!available) {
195
+ if (source.optional) {
196
+ return undefined;
197
+ }
198
+ throw new Error(`Configuration source "${source.name}" is unavailable.`);
199
+ }
200
+ }
201
+ const result = await source.load(context);
202
+ if (!result || typeof result !== "object") {
203
+ throw new Error(`Configuration source "${source.name}" returned an invalid result.`);
204
+ }
205
+ return result;
206
+ }
181
207
  /**
182
208
  * Loads multiple configuration sources in priority order.
183
209
  */
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Environment-variable configuration source.
3
+ *
4
+ * ConfigSourceType.ENVIRONMENT existed from the first release but no
5
+ * implementation ever shipped, so the documented "environment-specific
6
+ * settings" use case had to be hand-rolled by every consumer.
7
+ */
8
+ import type { ConfigSource } from "./configSource.core.js";
9
+ /**
10
+ * Options for the environment configuration source.
11
+ */
12
+ export interface EnvironmentConfigSourceOptions {
13
+ /**
14
+ * Only variables starting with this prefix are read. The prefix is
15
+ * removed from the resulting configuration key. When omitted, every
16
+ * variable is read — rarely what an application wants.
17
+ */
18
+ readonly prefix?: string;
19
+ /**
20
+ * Source name (default "environment").
21
+ */
22
+ readonly name?: string;
23
+ /**
24
+ * Source priority (default 100, above in-memory defaults).
25
+ */
26
+ readonly priority?: number;
27
+ /**
28
+ * Whether load failures are tolerated (default false).
29
+ */
30
+ readonly optional?: boolean;
31
+ /**
32
+ * Variable map to read. Defaults to `process.env`.
33
+ */
34
+ readonly env?: Readonly<Record<string, string | undefined>>;
35
+ /**
36
+ * Overrides the default variable-name to configuration-key mapping.
37
+ * Receives the name with `prefix` already removed.
38
+ *
39
+ * The default lowercases the name and maps "__" to "." so
40
+ * `APP__DB__HOST` becomes `db.host` under the prefix `APP__`.
41
+ */
42
+ readonly keyMapper?: (name: string) => string;
43
+ /**
44
+ * Decides whether a variable holds a secret. Receives the ORIGINAL
45
+ * variable name and the derived configuration key. Defaults to a
46
+ * name pattern covering passwords, tokens, API keys and credentials.
47
+ *
48
+ * Pass `() => false` to opt out — values are then NOT redacted by
49
+ * safe serialization.
50
+ */
51
+ readonly isSensitive?: (name: string, key: string) => boolean;
52
+ }
53
+ /**
54
+ * Creates a configuration source backed by environment variables.
55
+ *
56
+ * Values are returned as RAW STRINGS: no boolean or numeric inference
57
+ * happens here, because guessing types from strings is how `"false"`
58
+ * ends up truthy. Use the typed accessors (`manager.number(key)`,
59
+ * `manager.boolean(key)`) which coerce explicitly and reject values
60
+ * that do not parse.
61
+ *
62
+ * Variables whose value is `undefined` are skipped; a variable set to
63
+ * the empty string is kept, since "set but empty" is meaningful.
64
+ */
65
+ export declare function createEnvironmentConfigSource(options?: EnvironmentConfigSourceOptions): ConfigSource;
66
+ //# sourceMappingURL=configSource.environment.d.ts.map
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Environment-variable configuration source.
3
+ *
4
+ * ConfigSourceType.ENVIRONMENT existed from the first release but no
5
+ * implementation ever shipped, so the documented "environment-specific
6
+ * settings" use case had to be hand-rolled by every consumer.
7
+ */
8
+ import { defineConfigProperty } from "../configValue/configValue.core.js";
9
+ import { ConfigSourceType, createConfigSource } from "./configSource.core.js";
10
+ /**
11
+ * Environment variable names whose values are treated as secrets.
12
+ *
13
+ * Matching entries are reported through `sensitiveKeys`, so
14
+ * `toSafeObject()` and safe entry serialization redact them instead of
15
+ * printing credentials into logs.
16
+ */
17
+ const DEFAULT_SECRET_PATTERN = /(pass(word)?|secret|token|api[_.-]?key|private[_.-]?key|credential|auth)/i;
18
+ /**
19
+ * Maps an environment variable name to a configuration key.
20
+ */
21
+ function defaultKeyMapper(name) {
22
+ return name.toLowerCase().split("__").join(".");
23
+ }
24
+ /**
25
+ * Creates a configuration source backed by environment variables.
26
+ *
27
+ * Values are returned as RAW STRINGS: no boolean or numeric inference
28
+ * happens here, because guessing types from strings is how `"false"`
29
+ * ends up truthy. Use the typed accessors (`manager.number(key)`,
30
+ * `manager.boolean(key)`) which coerce explicitly and reject values
31
+ * that do not parse.
32
+ *
33
+ * Variables whose value is `undefined` are skipped; a variable set to
34
+ * the empty string is kept, since "set but empty" is meaningful.
35
+ */
36
+ export function createEnvironmentConfigSource(options = {}) {
37
+ const prefix = options.prefix ?? "";
38
+ const keyMapper = options.keyMapper ?? defaultKeyMapper;
39
+ const isSensitive = options.isSensitive ??
40
+ ((name) => DEFAULT_SECRET_PATTERN.test(name));
41
+ const name = options.name ?? "environment";
42
+ return createConfigSource({
43
+ name,
44
+ type: ConfigSourceType.ENVIRONMENT,
45
+ priority: options.priority ?? 100,
46
+ optional: options.optional ?? false,
47
+ }, () => {
48
+ const env = options.env ?? process.env;
49
+ const values = {};
50
+ const sensitiveKeys = [];
51
+ for (const [variable, raw] of Object.entries(env)) {
52
+ if (raw === undefined) {
53
+ continue;
54
+ }
55
+ if (prefix.length > 0 && !variable.startsWith(prefix)) {
56
+ continue;
57
+ }
58
+ const key = keyMapper(variable.slice(prefix.length));
59
+ if (key.length === 0) {
60
+ continue;
61
+ }
62
+ defineConfigProperty(values, key, raw);
63
+ if (isSensitive(variable, key)) {
64
+ sensitiveKeys.push(key);
65
+ }
66
+ }
67
+ return {
68
+ source: name,
69
+ type: ConfigSourceType.ENVIRONMENT,
70
+ values,
71
+ sensitiveKeys,
72
+ };
73
+ });
74
+ }
75
+ //# sourceMappingURL=configSource.environment.js.map
@@ -4,4 +4,5 @@
4
4
  * Configuration source abstractions and implementations.
5
5
  */
6
6
  export * from "./configSource.core.js";
7
+ export * from "./configSource.environment.js";
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -4,4 +4,5 @@
4
4
  * Configuration source abstractions and implementations.
5
5
  */
6
6
  export * from "./configSource.core.js";
7
+ export * from "./configSource.environment.js";
7
8
  //# sourceMappingURL=index.js.map
@@ -87,8 +87,23 @@ export declare class ConfigStore {
87
87
  values(): readonly ConfigValue[];
88
88
  /**
89
89
  * Returns a plain object containing all configuration values.
90
+ *
91
+ * WARNING: values are returned RAW — sensitive entries are NOT
92
+ * redacted. Use toSafeObject() for logging or diagnostics.
93
+ *
94
+ * The returned object is a defensive snapshot: when the store does
95
+ * not freeze values, each value is deep-cloned so later mutations
96
+ * of the snapshot never affect the store (and vice versa). Frozen
97
+ * stores share their (immutable) values directly.
90
98
  */
91
99
  toObject(): Readonly<Record<string, ConfigValue>>;
100
+ /**
101
+ * Returns a plain object with sensitive values redacted.
102
+ *
103
+ * Entries marked sensitive are replaced with "[REDACTED]". This is
104
+ * the safe counterpart to toObject() for logging and diagnostics.
105
+ */
106
+ toSafeObject(): Readonly<Record<string, ConfigValue>>;
92
107
  /**
93
108
  * Returns entries matching a prefix.
94
109
  */
@@ -1,5 +1,5 @@
1
- import { cloneConfigValue, configValuesEqual, freezeConfigValue, } from "../configValue/configValue.core.js";
2
- import { createConfigEntry } from "../configEntry/configEntry.type.js";
1
+ import { cloneConfigValue, configValuesEqual, defineConfigProperty, freezeConfigValue, } from "../configValue/configValue.core.js";
2
+ import { createConfigEntry, toSafeConfigEntry, } from "../configEntry/configEntry.type.js";
3
3
  /**
4
4
  * Normalizes a configuration key.
5
5
  */
@@ -107,7 +107,9 @@ export class ConfigStore {
107
107
  previous.priority === entry.priority &&
108
108
  previous.sensitive === entry.sensitive &&
109
109
  previous.resolved === entry.resolved) {
110
- return entry;
110
+ // No-op write: return the entry that is actually stored so
111
+ // callers always hold a reference to live store state.
112
+ return previous;
111
113
  }
112
114
  this.entries.set(normalizedKey, entry);
113
115
  this.emitChange({
@@ -199,18 +201,46 @@ export class ConfigStore {
199
201
  }
200
202
  /**
201
203
  * Returns a plain object containing all configuration values.
204
+ *
205
+ * WARNING: values are returned RAW — sensitive entries are NOT
206
+ * redacted. Use toSafeObject() for logging or diagnostics.
207
+ *
208
+ * The returned object is a defensive snapshot: when the store does
209
+ * not freeze values, each value is deep-cloned so later mutations
210
+ * of the snapshot never affect the store (and vice versa). Frozen
211
+ * stores share their (immutable) values directly.
202
212
  */
203
213
  toObject() {
204
214
  this.assertActive();
205
215
  const result = {};
206
216
  for (const entry of this.entries.values()) {
207
- result[entry.key] = entry.value;
217
+ // Keys come from configuration sources and may be hostile
218
+ // ("__proto__"): define them instead of assigning so they can
219
+ // never reach an inherited setter.
220
+ defineConfigProperty(result, entry.key, this.shouldFreeze ? entry.value : cloneConfigValue(entry.value));
208
221
  }
209
222
  if (this.shouldFreeze) {
210
223
  freezeConfigValue(result);
211
224
  }
212
225
  return result;
213
226
  }
227
+ /**
228
+ * Returns a plain object with sensitive values redacted.
229
+ *
230
+ * Entries marked sensitive are replaced with "[REDACTED]". This is
231
+ * the safe counterpart to toObject() for logging and diagnostics.
232
+ */
233
+ toSafeObject() {
234
+ this.assertActive();
235
+ const result = {};
236
+ for (const entry of this.entries.values()) {
237
+ const safeEntry = toSafeConfigEntry(entry);
238
+ defineConfigProperty(result, safeEntry.key, safeEntry.sensitive
239
+ ? safeEntry.value
240
+ : cloneConfigValue(safeEntry.value));
241
+ }
242
+ return Object.freeze(result);
243
+ }
214
244
  /**
215
245
  * Returns entries matching a prefix.
216
246
  */
@@ -232,7 +262,7 @@ export class ConfigStore {
232
262
  ? ""
233
263
  : entry.key.slice(normalizedPrefix.length + 1);
234
264
  if (key.length > 0) {
235
- result[key] = entry.value;
265
+ defineConfigProperty(result, key, entry.value);
236
266
  }
237
267
  }
238
268
  if (this.shouldFreeze) {
@@ -19,6 +19,17 @@ export type ConfigChangeListener = (event: ConfigChangeEvent) => void;
19
19
  export interface ConfigStoreOptions {
20
20
  readonly initialValues?: Readonly<Record<string, ConfigValue>>;
21
21
  readonly entries?: readonly ConfigEntry[];
22
+ /**
23
+ * Whether stored values are deep-cloned and deep-frozen on write
24
+ * (default true).
25
+ *
26
+ * When `false`, the store keeps REFERENCES to the exact objects
27
+ * passed to `set()`: callers can still mutate them afterwards and
28
+ * those mutations are visible through the store. `get()` likewise
29
+ * returns the shared reference. Only `toObject()` defends against
30
+ * this by cloning. Use `freeze: false` only for trusted,
31
+ * performance-sensitive code that treats values as immutable.
32
+ */
22
33
  readonly freeze?: boolean;
23
34
  }
24
35
  //# sourceMappingURL=configStore.type.d.ts.map
@@ -28,6 +28,10 @@ export type ConfigValue = ConfigPrimitive | Date | ConfigValue[] | {
28
28
  export type ResolvedConfigValue = string | number | boolean | bigint | null | undefined | Date | ResolvedConfigValue[] | {
29
29
  readonly [key: string]: ResolvedConfigValue;
30
30
  };
31
+ /**
32
+ * Checks whether an object key is unsafe to copy onto plain objects.
33
+ */
34
+ export declare function isUnsafeConfigKey(key: string): boolean;
31
35
  /**
32
36
  * Checks whether a value is a configuration primitive.
33
37
  */
@@ -84,14 +88,48 @@ export declare function parseConfigDate(value: string | Date | undefined): Date
84
88
  *
85
89
  * Configuration objects should normally be immutable after
86
90
  * resolution to prevent accidental runtime mutation.
91
+ *
92
+ * A shallow-frozen container is still traversed so its nested
93
+ * children are frozen as well. Non-plain objects (class instances)
94
+ * are returned by reference WITHOUT being frozen — consistent with
95
+ * cloneConfigValue, which also returns such values by reference.
96
+ * Values are assumed to be acyclic, as required by ConfigValue.
87
97
  */
88
98
  export declare function freezeConfigValue<T extends ConfigValue>(value: T): T;
89
99
  /**
90
100
  * Deeply clones a configuration value.
101
+ *
102
+ * Plain objects, arrays, and Dates are copied deeply. Non-plain
103
+ * objects (class instances) are returned BY REFERENCE — the clone
104
+ * shares them with the original (freezeConfigValue treats them the
105
+ * same way and never freezes them). Unsafe keys ("__proto__",
106
+ * "constructor", "prototype") are copied as own data properties via
107
+ * Object.defineProperty, so untrusted payloads can never pollute a
108
+ * prototype through the clone.
91
109
  */
92
110
  export declare function cloneConfigValue<T extends ConfigValue>(value: T): T;
93
111
  /**
94
112
  * Compares two configuration values deeply.
95
113
  */
96
114
  export declare function configValuesEqual(left: ConfigValue, right: ConfigValue): boolean;
115
+ /**
116
+ * Assigns a key onto a plain object without prototype pollution.
117
+ *
118
+ * Plain assignment (`target[key] = value`) invokes inherited setters:
119
+ * for the key "__proto__" that REPLACES the target's prototype instead
120
+ * of creating an own property, which both loses the value and lets an
121
+ * untrusted configuration key mutate object shape. Defining the
122
+ * property explicitly always creates an own, enumerable data property,
123
+ * so keys such as "__proto__", "constructor" and "prototype" survive
124
+ * round-trips without ever reaching a setter.
125
+ */
126
+ export declare function defineConfigProperty(target: Record<string, ConfigValue>, key: string, value: ConfigValue): void;
127
+ /**
128
+ * Reads an OWN property from a configuration-shaped object.
129
+ *
130
+ * Bracket access would walk the prototype chain, so a schema property
131
+ * named "constructor" or "toString" would read an inherited function
132
+ * rather than reporting a missing value.
133
+ */
134
+ export declare function readOwnConfigProperty(source: Readonly<Record<string, unknown>>, key: string): unknown;
97
135
  //# sourceMappingURL=configValue.core.d.ts.map
@@ -6,6 +6,25 @@
6
6
  * possible, while allowing common runtime values such as undefined,
7
7
  * Date, and Buffer-like binary data to be handled safely.
8
8
  */
9
+ /**
10
+ * Object keys that must never be copied onto plain objects.
11
+ *
12
+ * Assigning these keys (most notably "__proto__") on a plain object
13
+ * mutates its prototype instead of creating an own property, which
14
+ * enables prototype-pollution attacks via untrusted configuration
15
+ * payloads such as JSON.parse output.
16
+ */
17
+ const UNSAFE_CONFIG_KEYS = new Set([
18
+ "__proto__",
19
+ "constructor",
20
+ "prototype",
21
+ ]);
22
+ /**
23
+ * Checks whether an object key is unsafe to copy onto plain objects.
24
+ */
25
+ export function isUnsafeConfigKey(key) {
26
+ return UNSAFE_CONFIG_KEYS.has(key);
27
+ }
9
28
  /**
10
29
  * Checks whether a value is a configuration primitive.
11
30
  */
@@ -223,26 +242,43 @@ export function parseConfigDate(value) {
223
242
  *
224
243
  * Configuration objects should normally be immutable after
225
244
  * resolution to prevent accidental runtime mutation.
245
+ *
246
+ * A shallow-frozen container is still traversed so its nested
247
+ * children are frozen as well. Non-plain objects (class instances)
248
+ * are returned by reference WITHOUT being frozen — consistent with
249
+ * cloneConfigValue, which also returns such values by reference.
250
+ * Values are assumed to be acyclic, as required by ConfigValue.
226
251
  */
227
252
  export function freezeConfigValue(value) {
228
253
  if (typeof value !== "object" || value === null) {
229
254
  return value;
230
255
  }
231
- if (Object.isFrozen(value)) {
232
- return value;
233
- }
234
256
  if (value instanceof Date) {
235
- return Object.freeze(value);
257
+ return Object.isFrozen(value) ? value : Object.freeze(value);
258
+ }
259
+ if (!Array.isArray(value) && !isConfigObject(value)) {
260
+ // Non-plain values (class instances) are intentionally left
261
+ // untouched; they are not valid ConfigValues and callers keep
262
+ // full ownership of them.
263
+ return value;
236
264
  }
237
265
  for (const child of Object.values(value)) {
238
- if (isConfigValue(child)) {
266
+ if (typeof child === "object" && child !== null) {
239
267
  freezeConfigValue(child);
240
268
  }
241
269
  }
242
- return Object.freeze(value);
270
+ return Object.isFrozen(value) ? value : Object.freeze(value);
243
271
  }
244
272
  /**
245
273
  * Deeply clones a configuration value.
274
+ *
275
+ * Plain objects, arrays, and Dates are copied deeply. Non-plain
276
+ * objects (class instances) are returned BY REFERENCE — the clone
277
+ * shares them with the original (freezeConfigValue treats them the
278
+ * same way and never freezes them). Unsafe keys ("__proto__",
279
+ * "constructor", "prototype") are copied as own data properties via
280
+ * Object.defineProperty, so untrusted payloads can never pollute a
281
+ * prototype through the clone.
246
282
  */
247
283
  export function cloneConfigValue(value) {
248
284
  if (value === null || value === undefined || typeof value !== "object") {
@@ -257,7 +293,11 @@ export function cloneConfigValue(value) {
257
293
  if (isConfigObject(value)) {
258
294
  const result = {};
259
295
  for (const [key, child] of Object.entries(value)) {
260
- result[key] = cloneConfigValue(child);
296
+ // Unsafe keys ("__proto__" and friends) are preserved as own
297
+ // data properties rather than assigned: assignment would reach
298
+ // the inherited "__proto__" setter and mutate the clone's
299
+ // prototype instead of copying the value.
300
+ defineConfigProperty(result, key, cloneConfigValue(child));
261
301
  }
262
302
  return result;
263
303
  }
@@ -290,4 +330,35 @@ export function configValuesEqual(left, right) {
290
330
  }
291
331
  return false;
292
332
  }
333
+ /**
334
+ * Assigns a key onto a plain object without prototype pollution.
335
+ *
336
+ * Plain assignment (`target[key] = value`) invokes inherited setters:
337
+ * for the key "__proto__" that REPLACES the target's prototype instead
338
+ * of creating an own property, which both loses the value and lets an
339
+ * untrusted configuration key mutate object shape. Defining the
340
+ * property explicitly always creates an own, enumerable data property,
341
+ * so keys such as "__proto__", "constructor" and "prototype" survive
342
+ * round-trips without ever reaching a setter.
343
+ */
344
+ export function defineConfigProperty(target, key, value) {
345
+ Object.defineProperty(target, key, {
346
+ value,
347
+ enumerable: true,
348
+ writable: true,
349
+ configurable: true,
350
+ });
351
+ }
352
+ /**
353
+ * Reads an OWN property from a configuration-shaped object.
354
+ *
355
+ * Bracket access would walk the prototype chain, so a schema property
356
+ * named "constructor" or "toString" would read an inherited function
357
+ * rather than reporting a missing value.
358
+ */
359
+ export function readOwnConfigProperty(source, key) {
360
+ return Object.prototype.hasOwnProperty.call(source, key)
361
+ ? source[key]
362
+ : undefined;
363
+ }
293
364
  //# sourceMappingURL=configValue.core.js.map