@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 +55 -0
- package/dist/index.d.ts +24 -2
- package/dist/index.js +79 -2
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|