@meltstudio/config-loader 3.4.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,6 +62,7 @@ No separate interface to maintain. No `as` casts. The types flow from the schema
62
62
  - **Nested objects and arrays** — deeply nested configs with full type safety
63
63
  - **Structured errors** — typed `ConfigLoadError` with per-field error details and warnings
64
64
  - **Enum constraints** — restrict values to a fixed set with `oneOf`, with full type narrowing
65
+ - **Sensitive fields** — mark fields with `sensitive: true` to auto-mask in `printConfig()` and `maskSecrets()`
65
66
  - **Schema validation** — optional per-field validation via [Standard Schema](https://github.com/standard-schema/standard-schema) (Zod, Valibot, ArkType, or custom)
66
67
  - **Strict mode** — promote warnings to errors for production safety
67
68
  - **Default values** — static or computed (via functions)
@@ -293,6 +294,58 @@ const config = c
293
294
 
294
295
  When used with `cli: true`, the `--help` output automatically lists the allowed values.
295
296
 
297
+ ## Sensitive Fields
298
+
299
+ Mark fields as `sensitive: true` to prevent their values from being exposed in logs or debug output:
300
+
301
+ ```typescript
302
+ const schema = {
303
+ host: c.string({ defaultValue: "localhost" }),
304
+ apiKey: c.string({ env: "API_KEY", sensitive: true }),
305
+ db: c.object({
306
+ item: {
307
+ host: c.string({ defaultValue: "db.local" }),
308
+ password: c.string({ env: "DB_PASS", sensitive: true }),
309
+ },
310
+ }),
311
+ };
312
+
313
+ const config = c.schema(schema).load({ env: true, args: false });
314
+ ```
315
+
316
+ Sensitive values load normally — `config.apiKey` returns the real value. The flag only affects masking utilities.
317
+
318
+ ### `printConfig()` auto-masking
319
+
320
+ `printConfig()` automatically masks sensitive fields:
321
+
322
+ ```typescript
323
+ const result = c.schema(schema).loadExtended({ env: true, args: false });
324
+ printConfig(result);
325
+ // apiKey shows "***" instead of the real value
326
+ // db.password shows "***" instead of the real value
327
+ ```
328
+
329
+ ### `maskSecrets()`
330
+
331
+ Use `maskSecrets()` to create a safe-to-log copy of your config:
332
+
333
+ ```typescript
334
+ import c, { maskSecrets } from "@meltstudio/config-loader";
335
+
336
+ // With a plain config from load()
337
+ const config = c.schema(schema).load({ env: true, args: false });
338
+ console.log(maskSecrets(config, schema));
339
+ // { host: "localhost", apiKey: "***", db: { host: "db.local", password: "***" } }
340
+
341
+ // With an extended result from loadExtended()
342
+ const result = c.schema(schema).loadExtended({ env: true, args: false });
343
+ const masked = maskSecrets(result);
344
+ // masked.data contains ConfigNodes with "***" for sensitive values
345
+ ```
346
+
347
+ The original config object is never mutated — `maskSecrets()` always returns a new copy.
348
+
296
349
  ## Validation
297
350
 
298
351
  Add per-field validation using the `validate` option. config-loader accepts any [Standard Schema v1](https://github.com/standard-schema/standard-schema) implementation — including **Zod**, **Valibot**, and **ArkType** — or a custom validator.
@@ -511,6 +564,8 @@ import c, {
511
564
  ConfigNodeArray, // Class representing an array of ConfigNode values
512
565
  type RecursivePartial, // Deep partial utility used by the defaults option
513
566
  type StandardSchemaV1, // Standard Schema v1 interface for validators
567
+ maskSecrets, // Create a safe-to-log copy with sensitive values masked
568
+ printConfig, // Format loadExtended() result as a readable table
514
569
  } from "@meltstudio/config-loader";
515
570
  ```
516
571
 
package/dist/index.d.ts CHANGED
@@ -60,6 +60,7 @@ interface OptionClassParams<T extends OptionKind> {
60
60
  env: string | null;
61
61
  cli: boolean;
62
62
  help: string;
63
+ sensitive?: boolean;
63
64
  defaultValue?: TypedDefaultValue<T>;
64
65
  oneOf?: ReadonlyArray<string | number | boolean>;
65
66
  validate?: StandardSchemaV1;
@@ -100,7 +101,8 @@ declare class ConfigNode {
100
101
  argName: string | null;
101
102
  line: number | null;
102
103
  column: number | null;
103
- constructor(value: Value | ArrayValue, path: string, sourceType: SourceTypes, file: string | null, variableName: string | null, argName: string | null, line?: number | null, column?: number | null);
104
+ sensitive: boolean;
105
+ constructor(value: Value | ArrayValue, path: string, sourceType: SourceTypes, file: string | null, variableName: string | null, argName: string | null, line?: number | null, column?: number | null, sensitive?: boolean);
104
106
  }
105
107
 
106
108
  declare class PrimitiveOption<T extends PrimitiveKind = PrimitiveKind, Narrowed = TypeOfPrimitiveKind<T>> extends OptionBase<T> {
@@ -225,6 +227,24 @@ declare class SettingsBuilder<T extends Node> {
225
227
  loadExtended(sources: SettingsSources<SchemaValue<T>>): ExtendedResult;
226
228
  }
227
229
 
230
+ /**
231
+ * Masks sensitive values in an `ExtendedResult` from `loadExtended()`.
232
+ * Fields marked `sensitive: true` have their values replaced with `"***"`.
233
+ *
234
+ * @param result - The `ExtendedResult` returned by `loadExtended()`.
235
+ * @returns A new `ExtendedResult` with sensitive values masked.
236
+ */
237
+ declare function maskSecrets(result: ExtendedResult): ExtendedResult;
238
+ /**
239
+ * Masks sensitive values in a plain config object from `load()`.
240
+ * Fields marked `sensitive: true` in the schema have their values replaced with `"***"`.
241
+ *
242
+ * @param config - The plain config object returned by `load()`.
243
+ * @param schema - The schema definition used to identify sensitive fields.
244
+ * @returns A new object with sensitive values masked.
245
+ */
246
+ declare function maskSecrets<T extends Record<string, unknown>>(config: T, schema: Node): T;
247
+
228
248
  declare class ConfigNodeArray {
229
249
  arrayValues: ConfigNode[];
230
250
  constructor(arrayValues: ConfigNode[]);
@@ -262,6 +282,8 @@ interface OptionPropsArgs<T> {
262
282
  defaultValue?: T | (() => T);
263
283
  /** Help text shown in CLI `--help` output. */
264
284
  help?: string;
285
+ /** Mark this field as sensitive. Sensitive values are masked by `printConfig()` and `maskSecrets()`. */
286
+ sensitive?: boolean;
265
287
  /** Restrict the value to a fixed set of allowed values. Checked after type coercion, before `validate`. */
266
288
  oneOf?: readonly T[];
267
289
  /** Standard Schema validator run after type coercion. Accepts Zod, Valibot, ArkType, or any Standard Schema v1 implementation. */
@@ -341,4 +363,4 @@ declare const option: {
341
363
  schema: <T extends Node>(theSchema: T) => SettingsBuilder<T>;
342
364
  };
343
365
 
344
- export { type ConfigErrorEntry, ConfigFileError, ConfigLoadError, ConfigNode, ConfigNodeArray, type ExtendedResult, type NodeTree, type RecursivePartial, type SchemaValue, type SettingsSources, type StandardSchemaV1, option as default, printConfig };
366
+ export { type ConfigErrorEntry, ConfigFileError, ConfigLoadError, ConfigNode, ConfigNodeArray, type ExtendedResult, type NodeTree, type RecursivePartial, type SchemaValue, type SettingsSources, type StandardSchemaV1, option as default, maskSecrets, printConfig };
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  ConfigNode: () => configNode_default,
36
36
  ConfigNodeArray: () => configNodeArray_default,
37
37
  default: () => index_default,
38
+ maskSecrets: () => maskSecrets,
38
39
  printConfig: () => printConfig
39
40
  });
40
41
  module.exports = __toCommonJS(index_exports);
@@ -81,7 +82,8 @@ var ConfigNode = class {
81
82
  argName;
82
83
  line;
83
84
  column;
84
- constructor(value, path2, sourceType, file, variableName, argName, line = null, column = null) {
85
+ sensitive;
86
+ constructor(value, path2, sourceType, file, variableName, argName, line = null, column = null, sensitive = false) {
85
87
  this.value = value;
86
88
  this.path = path2;
87
89
  this.sourceType = sourceType;
@@ -90,6 +92,7 @@ var ConfigNode = class {
90
92
  this.argName = argName;
91
93
  this.line = line;
92
94
  this.column = column;
95
+ this.sensitive = sensitive;
93
96
  }
94
97
  };
95
98
  var configNode_default = ConfigNode;
@@ -292,6 +295,9 @@ var OptionBase = class {
292
295
  envFileResults,
293
296
  errors
294
297
  );
298
+ if (resolved && this.params.sensitive) {
299
+ resolved.sensitive = true;
300
+ }
295
301
  if (resolved && this.params.oneOf) {
296
302
  const passed = this.runOneOfCheck(resolved, path2, errors);
297
303
  if (!passed) return resolved;
@@ -1127,6 +1133,76 @@ var SettingsBuilder = class {
1127
1133
  }
1128
1134
  };
1129
1135
 
1136
+ // src/maskSecrets.ts
1137
+ var MASK = "***";
1138
+ function maskNodeTree(tree) {
1139
+ const result = {};
1140
+ for (const [key, entry] of Object.entries(tree)) {
1141
+ if (entry instanceof configNode_default) {
1142
+ if (entry.sensitive) {
1143
+ const masked = new configNode_default(
1144
+ MASK,
1145
+ entry.path,
1146
+ entry.sourceType,
1147
+ entry.file,
1148
+ entry.variableName,
1149
+ entry.argName,
1150
+ entry.line,
1151
+ entry.column,
1152
+ entry.sensitive
1153
+ );
1154
+ if (entry.value instanceof configNodeArray_default) {
1155
+ masked.value = entry.value;
1156
+ }
1157
+ result[key] = masked;
1158
+ } else {
1159
+ result[key] = entry;
1160
+ }
1161
+ } else {
1162
+ result[key] = maskNodeTree(entry);
1163
+ }
1164
+ }
1165
+ return result;
1166
+ }
1167
+ function maskPlainObject(obj, schema2) {
1168
+ const result = {};
1169
+ for (const [key, value] of Object.entries(obj)) {
1170
+ const schemaNode = schema2[key];
1171
+ if (!schemaNode) {
1172
+ result[key] = value;
1173
+ continue;
1174
+ }
1175
+ if (schemaNode instanceof ObjectOption) {
1176
+ if (value && typeof value === "object" && !Array.isArray(value)) {
1177
+ result[key] = maskPlainObject(
1178
+ value,
1179
+ schemaNode.item
1180
+ );
1181
+ } else {
1182
+ result[key] = value;
1183
+ }
1184
+ } else if (schemaNode instanceof OptionBase) {
1185
+ result[key] = schemaNode.params.sensitive ? MASK : value;
1186
+ } else {
1187
+ result[key] = value;
1188
+ }
1189
+ }
1190
+ return result;
1191
+ }
1192
+ function maskSecrets(resultOrConfig, schema2) {
1193
+ if ("data" in resultOrConfig && "warnings" in resultOrConfig && !schema2) {
1194
+ const extended = resultOrConfig;
1195
+ return {
1196
+ data: maskNodeTree(extended.data),
1197
+ warnings: [...extended.warnings]
1198
+ };
1199
+ }
1200
+ if (schema2) {
1201
+ return maskPlainObject(resultOrConfig, schema2);
1202
+ }
1203
+ return resultOrConfig;
1204
+ }
1205
+
1130
1206
  // src/printConfig.ts
1131
1207
  function truncate(str, max) {
1132
1208
  if (str.length <= max) return str;
@@ -1183,7 +1259,7 @@ function flattenTree(tree, prefix = "") {
1183
1259
  } else {
1184
1260
  rows.push({
1185
1261
  path: path2,
1186
- value: formatValue(entry.value),
1262
+ value: entry.sensitive ? "***" : formatValue(entry.value),
1187
1263
  source: entry.sourceType,
1188
1264
  detail: formatDetail(entry)
1189
1265
  });
@@ -1306,5 +1382,6 @@ var index_default = option;
1306
1382
  ConfigLoadError,
1307
1383
  ConfigNode,
1308
1384
  ConfigNodeArray,
1385
+ maskSecrets,
1309
1386
  printConfig
1310
1387
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meltstudio/config-loader",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "Type-safe configuration loader with full TypeScript inference. Load from YAML, JSON, .env, environment variables, and CLI args.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",