@karmaniverous/get-dotenv 5.1.0 → 5.2.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 +14 -0
- package/dist/cliHost.cjs +222 -19
- package/dist/cliHost.d.cts +17 -0
- package/dist/cliHost.d.mts +17 -0
- package/dist/cliHost.d.ts +17 -0
- package/dist/cliHost.mjs +222 -19
- package/dist/config.cjs +10 -5
- package/dist/config.d.cts +2 -0
- package/dist/config.d.mts +2 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.mjs +10 -5
- package/dist/env-overlay.d.cts +2 -0
- package/dist/env-overlay.d.mts +2 -0
- package/dist/env-overlay.d.ts +2 -0
- package/dist/getdotenv.cli.mjs +479 -36
- package/dist/index.cjs +579 -245
- package/dist/index.d.cts +36 -1
- package/dist/index.d.mts +36 -1
- package/dist/index.d.ts +36 -1
- package/dist/index.mjs +579 -246
- package/dist/plugins-aws.cjs +43 -1
- package/dist/plugins-aws.d.cts +17 -0
- package/dist/plugins-aws.d.mts +17 -0
- package/dist/plugins-aws.d.ts +17 -0
- package/dist/plugins-aws.mjs +43 -1
- package/dist/plugins-batch.cjs +46 -6
- package/dist/plugins-batch.d.cts +17 -0
- package/dist/plugins-batch.d.mts +17 -0
- package/dist/plugins-batch.d.ts +17 -0
- package/dist/plugins-batch.mjs +46 -6
- package/dist/plugins-init.d.cts +17 -0
- package/dist/plugins-init.d.mts +17 -0
- package/dist/plugins-init.d.ts +17 -0
- package/package.json +25 -24
package/dist/getdotenv.cli.mjs
CHANGED
|
@@ -19,6 +19,11 @@ const baseRootOptionDefaults = {
|
|
|
19
19
|
dotenvToken: '.env',
|
|
20
20
|
loadProcess: true,
|
|
21
21
|
logger: console,
|
|
22
|
+
// Diagnostics defaults
|
|
23
|
+
warnEntropy: true,
|
|
24
|
+
entropyThreshold: 3.8,
|
|
25
|
+
entropyMinLength: 16,
|
|
26
|
+
entropyWhitelist: ['^GIT_', '^npm_', '^CI$', 'SHLVL'],
|
|
22
27
|
paths: './',
|
|
23
28
|
pathsDelimiter: ' ',
|
|
24
29
|
privateToken: 'local',
|
|
@@ -39,7 +44,7 @@ const baseRootOptionDefaults = {
|
|
|
39
44
|
const baseGetDotenvCliOptions = baseRootOptionDefaults;
|
|
40
45
|
|
|
41
46
|
/** @internal */
|
|
42
|
-
const isPlainObject = (value) => value !== null &&
|
|
47
|
+
const isPlainObject$1 = (value) => value !== null &&
|
|
43
48
|
typeof value === 'object' &&
|
|
44
49
|
Object.getPrototypeOf(value) === Object.prototype;
|
|
45
50
|
const mergeInto = (target, source) => {
|
|
@@ -47,10 +52,10 @@ const mergeInto = (target, source) => {
|
|
|
47
52
|
if (sVal === undefined)
|
|
48
53
|
continue; // do not overwrite with undefined
|
|
49
54
|
const tVal = target[key];
|
|
50
|
-
if (isPlainObject(tVal) && isPlainObject(sVal)) {
|
|
55
|
+
if (isPlainObject$1(tVal) && isPlainObject$1(sVal)) {
|
|
51
56
|
target[key] = mergeInto({ ...tVal }, sVal);
|
|
52
57
|
}
|
|
53
|
-
else if (isPlainObject(sVal)) {
|
|
58
|
+
else if (isPlainObject$1(sVal)) {
|
|
54
59
|
target[key] = mergeInto({}, sVal);
|
|
55
60
|
}
|
|
56
61
|
else {
|
|
@@ -210,11 +215,12 @@ const getDotenvOptionsSchemaRaw = z.object({
|
|
|
210
215
|
const getDotenvOptionsSchemaResolved = getDotenvOptionsSchemaRaw;
|
|
211
216
|
|
|
212
217
|
/**
|
|
213
|
-
* Zod schemas for configuration files discovered by the new loader.
|
|
218
|
+
* Zod schemas for configuration files discovered by the new loader.
|
|
219
|
+
*
|
|
214
220
|
* Notes:
|
|
215
221
|
* - RAW: all fields optional; shapes are stringly-friendly (paths may be string[] or string).
|
|
216
222
|
* - RESOLVED: normalized shapes (paths always string[]).
|
|
217
|
-
* - For
|
|
223
|
+
* - For JSON/YAML configs, the loader rejects "dynamic" and "schema" (JS/TS-only).
|
|
218
224
|
*/
|
|
219
225
|
// String-only env value map
|
|
220
226
|
const stringMap = z.record(z.string(), z.string());
|
|
@@ -229,6 +235,8 @@ const getDotenvConfigSchemaRaw = z.object({
|
|
|
229
235
|
log: z.boolean().optional(),
|
|
230
236
|
shell: z.union([z.string(), z.boolean()]).optional(),
|
|
231
237
|
scripts: z.record(z.string(), z.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
|
|
238
|
+
requiredKeys: z.array(z.string()).optional(),
|
|
239
|
+
schema: z.unknown().optional(), // JS/TS-only; loader rejects in JSON/YAML
|
|
232
240
|
vars: stringMap.optional(), // public, global
|
|
233
241
|
envVars: envStringMap.optional(), // public, per-env
|
|
234
242
|
// Dynamic in config (JS/TS only). JSON/YAML loader will reject if set.
|
|
@@ -411,9 +419,11 @@ const loadConfigFile = async (filePath) => {
|
|
|
411
419
|
.join('\n');
|
|
412
420
|
throw new Error(`Invalid config ${filePath}:\n${msgs}`);
|
|
413
421
|
}
|
|
414
|
-
// Disallow dynamic in JSON/YAML; allow in JS/TS
|
|
415
|
-
if (!isJsOrTs(filePath) &&
|
|
416
|
-
|
|
422
|
+
// Disallow dynamic and schema in JSON/YAML; allow both in JS/TS.
|
|
423
|
+
if (!isJsOrTs(filePath) &&
|
|
424
|
+
(parsed.data.dynamic !== undefined || parsed.data.schema !== undefined)) {
|
|
425
|
+
throw new Error(`Config ${filePath} specifies unsupported keys for JSON/YAML. ` +
|
|
426
|
+
`Use JS/TS config for "dynamic" or "schema".`);
|
|
417
427
|
}
|
|
418
428
|
return getDotenvConfigSchemaResolved.parse(parsed.data);
|
|
419
429
|
};
|
|
@@ -617,6 +627,99 @@ const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
|
|
|
617
627
|
return current;
|
|
618
628
|
};
|
|
619
629
|
|
|
630
|
+
/** src/diagnostics/entropy.ts
|
|
631
|
+
* Entropy diagnostics (presentation-only).
|
|
632
|
+
* - Gated by min length and printable ASCII.
|
|
633
|
+
* - Warn once per key per run when bits/char \>= threshold.
|
|
634
|
+
* - Supports whitelist patterns to suppress known-noise keys.
|
|
635
|
+
*/
|
|
636
|
+
const warned = new Set();
|
|
637
|
+
const isPrintableAscii = (s) => /^[\x20-\x7E]+$/.test(s);
|
|
638
|
+
const compile$1 = (patterns) => (patterns ?? []).map((p) => new RegExp(p, 'i'));
|
|
639
|
+
const whitelisted = (key, regs) => regs.some((re) => re.test(key));
|
|
640
|
+
const shannonBitsPerChar = (s) => {
|
|
641
|
+
const freq = new Map();
|
|
642
|
+
for (const ch of s)
|
|
643
|
+
freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
644
|
+
const n = s.length;
|
|
645
|
+
let h = 0;
|
|
646
|
+
for (const c of freq.values()) {
|
|
647
|
+
const p = c / n;
|
|
648
|
+
h -= p * Math.log2(p);
|
|
649
|
+
}
|
|
650
|
+
return h;
|
|
651
|
+
};
|
|
652
|
+
/**
|
|
653
|
+
* Maybe emit a one-line entropy warning for a key.
|
|
654
|
+
* Caller supplies an `emit(line)` function; the helper ensures once-per-key.
|
|
655
|
+
*/
|
|
656
|
+
const maybeWarnEntropy = (key, value, origin, opts, emit) => {
|
|
657
|
+
if (!opts || opts.warnEntropy === false)
|
|
658
|
+
return;
|
|
659
|
+
if (warned.has(key))
|
|
660
|
+
return;
|
|
661
|
+
const v = value ?? '';
|
|
662
|
+
const minLen = Math.max(0, opts.entropyMinLength ?? 16);
|
|
663
|
+
const threshold = opts.entropyThreshold ?? 3.8;
|
|
664
|
+
if (v.length < minLen)
|
|
665
|
+
return;
|
|
666
|
+
if (!isPrintableAscii(v))
|
|
667
|
+
return;
|
|
668
|
+
const wl = compile$1(opts.entropyWhitelist);
|
|
669
|
+
if (whitelisted(key, wl))
|
|
670
|
+
return;
|
|
671
|
+
const bpc = shannonBitsPerChar(v);
|
|
672
|
+
if (bpc >= threshold) {
|
|
673
|
+
warned.add(key);
|
|
674
|
+
emit(`[entropy] key=${key} score=${bpc.toFixed(2)} len=${String(v.length)} origin=${origin}`);
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
const DEFAULT_PATTERNS = [
|
|
679
|
+
'\\bsecret\\b',
|
|
680
|
+
'\\btoken\\b',
|
|
681
|
+
'\\bpass(word)?\\b',
|
|
682
|
+
'\\bapi[_-]?key\\b',
|
|
683
|
+
'\\bkey\\b',
|
|
684
|
+
];
|
|
685
|
+
const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => new RegExp(p, 'i'));
|
|
686
|
+
const shouldRedactKey = (key, regs) => regs.some((re) => re.test(key));
|
|
687
|
+
const MASK = '[redacted]';
|
|
688
|
+
/**
|
|
689
|
+
* Produce a shallow redacted copy of an env-like object for display.
|
|
690
|
+
*/
|
|
691
|
+
const redactObject = (obj, opts) => {
|
|
692
|
+
if (!opts?.redact)
|
|
693
|
+
return { ...obj };
|
|
694
|
+
const regs = compile(opts.redactPatterns);
|
|
695
|
+
const out = {};
|
|
696
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
697
|
+
out[k] = v && shouldRedactKey(k, regs) ? MASK : v;
|
|
698
|
+
}
|
|
699
|
+
return out;
|
|
700
|
+
};
|
|
701
|
+
/**
|
|
702
|
+
* Utility to redact three related displayed values (parent/dotenv/final)
|
|
703
|
+
* consistently for trace lines.
|
|
704
|
+
*/
|
|
705
|
+
const redactTriple = (key, triple, opts) => {
|
|
706
|
+
if (!opts?.redact)
|
|
707
|
+
return triple;
|
|
708
|
+
const regs = compile(opts.redactPatterns);
|
|
709
|
+
const maskIf = (v) => (v && shouldRedactKey(key, regs) ? MASK : v);
|
|
710
|
+
const out = {};
|
|
711
|
+
const p = maskIf(triple.parent);
|
|
712
|
+
const d = maskIf(triple.dotenv);
|
|
713
|
+
const f = maskIf(triple.final);
|
|
714
|
+
if (p !== undefined)
|
|
715
|
+
out.parent = p;
|
|
716
|
+
if (d !== undefined)
|
|
717
|
+
out.dotenv = d;
|
|
718
|
+
if (f !== undefined)
|
|
719
|
+
out.final = f;
|
|
720
|
+
return out;
|
|
721
|
+
};
|
|
722
|
+
|
|
620
723
|
/**
|
|
621
724
|
* Asynchronously read a dotenv file & parse it into an object.
|
|
622
725
|
*
|
|
@@ -866,14 +969,103 @@ const getDotenv = async (options = {}) => {
|
|
|
866
969
|
resultDotenv = dotenvForOutput;
|
|
867
970
|
}
|
|
868
971
|
// Log result.
|
|
869
|
-
if (log)
|
|
870
|
-
|
|
972
|
+
if (log) {
|
|
973
|
+
const redactFlag = options.redact ?? false;
|
|
974
|
+
const redactPatterns = options.redactPatterns ?? undefined;
|
|
975
|
+
const redOpts = {};
|
|
976
|
+
if (redactFlag)
|
|
977
|
+
redOpts.redact = true;
|
|
978
|
+
if (redactFlag && Array.isArray(redactPatterns))
|
|
979
|
+
redOpts.redactPatterns = redactPatterns;
|
|
980
|
+
const bag = redactFlag
|
|
981
|
+
? redactObject(resultDotenv, redOpts)
|
|
982
|
+
: { ...resultDotenv };
|
|
983
|
+
logger.log(bag);
|
|
984
|
+
// Entropy warnings: once-per-key-per-run (presentation only)
|
|
985
|
+
const warnEntropyVal = options.warnEntropy ?? true;
|
|
986
|
+
const entropyThresholdVal = options
|
|
987
|
+
.entropyThreshold;
|
|
988
|
+
const entropyMinLengthVal = options
|
|
989
|
+
.entropyMinLength;
|
|
990
|
+
const entropyWhitelistVal = options
|
|
991
|
+
.entropyWhitelist;
|
|
992
|
+
const entOpts = {};
|
|
993
|
+
if (typeof warnEntropyVal === 'boolean')
|
|
994
|
+
entOpts.warnEntropy = warnEntropyVal;
|
|
995
|
+
if (typeof entropyThresholdVal === 'number')
|
|
996
|
+
entOpts.entropyThreshold = entropyThresholdVal;
|
|
997
|
+
if (typeof entropyMinLengthVal === 'number')
|
|
998
|
+
entOpts.entropyMinLength = entropyMinLengthVal;
|
|
999
|
+
if (Array.isArray(entropyWhitelistVal))
|
|
1000
|
+
entOpts.entropyWhitelist = entropyWhitelistVal;
|
|
1001
|
+
for (const [k, v] of Object.entries(resultDotenv)) {
|
|
1002
|
+
maybeWarnEntropy(k, v, v !== undefined ? 'dotenv' : 'unset', entOpts, (line) => {
|
|
1003
|
+
logger.log(line);
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
871
1007
|
// Load process.env.
|
|
872
1008
|
if (loadProcess)
|
|
873
1009
|
Object.assign(process.env, resultDotenv);
|
|
874
1010
|
return resultDotenv;
|
|
875
1011
|
};
|
|
876
1012
|
|
|
1013
|
+
/**
|
|
1014
|
+
* Deep interpolation utility for string leaves.
|
|
1015
|
+
* - Expands string values using dotenv-style expansion against the provided envRef.
|
|
1016
|
+
* - Preserves non-strings as-is.
|
|
1017
|
+
* - Does not recurse into arrays (arrays are returned unchanged).
|
|
1018
|
+
*
|
|
1019
|
+
* Intended for:
|
|
1020
|
+
* - Phase C option/config interpolation after composing ctx.dotenv.
|
|
1021
|
+
* - Per-plugin config slice interpolation before afterResolve.
|
|
1022
|
+
*/
|
|
1023
|
+
/** @internal */
|
|
1024
|
+
const isPlainObject = (v) => v !== null &&
|
|
1025
|
+
typeof v === 'object' &&
|
|
1026
|
+
!Array.isArray(v) &&
|
|
1027
|
+
Object.getPrototypeOf(v) === Object.prototype;
|
|
1028
|
+
/**
|
|
1029
|
+
* Deeply interpolate string leaves against envRef.
|
|
1030
|
+
* Arrays are not recursed into; they are returned unchanged.
|
|
1031
|
+
*
|
|
1032
|
+
* @typeParam T - Shape of the input value.
|
|
1033
|
+
* @param value - Input value (object/array/primitive).
|
|
1034
|
+
* @param envRef - Reference environment for interpolation.
|
|
1035
|
+
* @returns A new value with string leaves interpolated.
|
|
1036
|
+
*/
|
|
1037
|
+
const interpolateDeep = (value, envRef) => {
|
|
1038
|
+
// Strings: expand and return
|
|
1039
|
+
if (typeof value === 'string') {
|
|
1040
|
+
const out = dotenvExpand(value, envRef);
|
|
1041
|
+
// dotenvExpand returns string | undefined; preserve original on undefined
|
|
1042
|
+
return (out ?? value);
|
|
1043
|
+
}
|
|
1044
|
+
// Arrays: return as-is (no recursion)
|
|
1045
|
+
if (Array.isArray(value)) {
|
|
1046
|
+
return value;
|
|
1047
|
+
}
|
|
1048
|
+
// Plain objects: shallow clone and recurse into values
|
|
1049
|
+
if (isPlainObject(value)) {
|
|
1050
|
+
const src = value;
|
|
1051
|
+
const out = {};
|
|
1052
|
+
for (const [k, v] of Object.entries(src)) {
|
|
1053
|
+
// Recurse for strings/objects; keep arrays as-is; preserve other scalars
|
|
1054
|
+
if (typeof v === 'string')
|
|
1055
|
+
out[k] = dotenvExpand(v, envRef) ?? v;
|
|
1056
|
+
else if (Array.isArray(v))
|
|
1057
|
+
out[k] = v;
|
|
1058
|
+
else if (isPlainObject(v))
|
|
1059
|
+
out[k] = interpolateDeep(v, envRef);
|
|
1060
|
+
else
|
|
1061
|
+
out[k] = v;
|
|
1062
|
+
}
|
|
1063
|
+
return out;
|
|
1064
|
+
}
|
|
1065
|
+
// Other primitives/types: return as-is
|
|
1066
|
+
return value;
|
|
1067
|
+
};
|
|
1068
|
+
|
|
877
1069
|
/**
|
|
878
1070
|
* Compute the dotenv context for the host (uses the config loader/overlay path).
|
|
879
1071
|
* - Resolves and validates options strictly (host-only).
|
|
@@ -957,19 +1149,32 @@ const computeContext = async (customOptions, plugins, hostMetaUrl) => {
|
|
|
957
1149
|
{};
|
|
958
1150
|
const mergedPluginConfigs = defaultsDeep({}, packagedPlugins, publicPlugins, localPlugins);
|
|
959
1151
|
for (const p of plugins) {
|
|
960
|
-
if (!p.id
|
|
1152
|
+
if (!p.id)
|
|
961
1153
|
continue;
|
|
962
1154
|
const slice = mergedPluginConfigs[p.id];
|
|
963
1155
|
if (slice === undefined)
|
|
964
1156
|
continue;
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1157
|
+
// Per-plugin interpolation just before validation/afterResolve:
|
|
1158
|
+
// precedence: process.env wins over ctx.dotenv for slice defaults.
|
|
1159
|
+
const envRef = {
|
|
1160
|
+
...dotenv,
|
|
1161
|
+
...process.env,
|
|
1162
|
+
};
|
|
1163
|
+
const interpolated = interpolateDeep(slice, envRef);
|
|
1164
|
+
// Validate if a schema is provided; otherwise accept interpolated slice as-is.
|
|
1165
|
+
if (p.configSchema) {
|
|
1166
|
+
const parsed = p.configSchema.safeParse(interpolated);
|
|
1167
|
+
if (!parsed.success) {
|
|
1168
|
+
const msgs = parsed.error.issues
|
|
1169
|
+
.map((i) => `${i.path.join('.')}: ${i.message}`)
|
|
1170
|
+
.join('\n');
|
|
1171
|
+
throw new Error(`Invalid config for plugin '${p.id}':\n${msgs}`);
|
|
1172
|
+
}
|
|
1173
|
+
mergedPluginConfigs[p.id] = parsed.data;
|
|
1174
|
+
}
|
|
1175
|
+
else {
|
|
1176
|
+
mergedPluginConfigs[p.id] = interpolated;
|
|
971
1177
|
}
|
|
972
|
-
mergedPluginConfigs[p.id] = parsed.data;
|
|
973
1178
|
}
|
|
974
1179
|
return {
|
|
975
1180
|
optionsResolved: validated,
|
|
@@ -1013,10 +1218,23 @@ class GetDotenvCli extends Command {
|
|
|
1013
1218
|
visibleOptions: (cmd) => {
|
|
1014
1219
|
const all = cmd.options ??
|
|
1015
1220
|
[];
|
|
1016
|
-
|
|
1221
|
+
const base = all.filter((opt) => {
|
|
1017
1222
|
const group = opt.__group;
|
|
1018
1223
|
return group === 'base';
|
|
1019
1224
|
});
|
|
1225
|
+
// Sort: short-aliased options first, then long-only; stable by flags.
|
|
1226
|
+
const hasShort = (opt) => {
|
|
1227
|
+
const flags = opt.flags ?? '';
|
|
1228
|
+
// Matches "-x," or starting "-x " before any long
|
|
1229
|
+
return /(^|\s|,)-[A-Za-z]/.test(flags);
|
|
1230
|
+
};
|
|
1231
|
+
const byFlags = (opt) => opt.flags ?? '';
|
|
1232
|
+
base.sort((a, b) => {
|
|
1233
|
+
const aS = hasShort(a) ? 1 : 0;
|
|
1234
|
+
const bS = hasShort(b) ? 1 : 0;
|
|
1235
|
+
return bS - aS || byFlags(a).localeCompare(byFlags(b));
|
|
1236
|
+
});
|
|
1237
|
+
return base;
|
|
1020
1238
|
},
|
|
1021
1239
|
});
|
|
1022
1240
|
this.addHelpText('beforeAll', () => {
|
|
@@ -1205,6 +1423,12 @@ class GetDotenvCli extends Command {
|
|
|
1205
1423
|
return '';
|
|
1206
1424
|
const renderRows = (title, rows) => {
|
|
1207
1425
|
const width = Math.min(40, rows.reduce((m, r) => Math.max(m, r.flags.length), 0));
|
|
1426
|
+
// Sort within group: short-aliased flags first
|
|
1427
|
+
rows.sort((a, b) => {
|
|
1428
|
+
const aS = /(^|\s|,)-[A-Za-z]/.test(a.flags) ? 1 : 0;
|
|
1429
|
+
const bS = /(^|\s|,)-[A-Za-z]/.test(b.flags) ? 1 : 0;
|
|
1430
|
+
return bS - aS || a.flags.localeCompare(b.flags);
|
|
1431
|
+
});
|
|
1208
1432
|
const lines = rows
|
|
1209
1433
|
.map((r) => {
|
|
1210
1434
|
const pad = ' '.repeat(Math.max(2, width - r.flags.length + 2));
|
|
@@ -1233,6 +1457,62 @@ class GetDotenvCli extends Command {
|
|
|
1233
1457
|
}
|
|
1234
1458
|
}
|
|
1235
1459
|
|
|
1460
|
+
/**
|
|
1461
|
+
* Validate a composed env against config-provided validation surfaces.
|
|
1462
|
+
* Precedence for validation definitions:
|
|
1463
|
+
* project.local -\> project.public -\> packaged
|
|
1464
|
+
*
|
|
1465
|
+
* Behavior:
|
|
1466
|
+
* - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
|
|
1467
|
+
* - Else if `requiredKeys` is present, check presence (value !== undefined).
|
|
1468
|
+
* - Returns a flat list of issue strings; caller decides warn vs fail.
|
|
1469
|
+
*/
|
|
1470
|
+
const validateEnvAgainstSources = (finalEnv, sources) => {
|
|
1471
|
+
const pick = (getter) => {
|
|
1472
|
+
const pl = sources.project?.local;
|
|
1473
|
+
const pp = sources.project?.public;
|
|
1474
|
+
const pk = sources.packaged;
|
|
1475
|
+
return ((pl && getter(pl)) ||
|
|
1476
|
+
(pp && getter(pp)) ||
|
|
1477
|
+
(pk && getter(pk)) ||
|
|
1478
|
+
undefined);
|
|
1479
|
+
};
|
|
1480
|
+
const schema = pick((cfg) => cfg['schema']);
|
|
1481
|
+
if (schema &&
|
|
1482
|
+
typeof schema.safeParse === 'function') {
|
|
1483
|
+
try {
|
|
1484
|
+
const parsed = schema.safeParse(finalEnv);
|
|
1485
|
+
if (!parsed.success) {
|
|
1486
|
+
// Try to render zod-style issues when available.
|
|
1487
|
+
const err = parsed.error;
|
|
1488
|
+
const issues = Array.isArray(err.issues) && err.issues.length > 0
|
|
1489
|
+
? err.issues.map((i) => {
|
|
1490
|
+
const path = Array.isArray(i.path) ? i.path.join('.') : '';
|
|
1491
|
+
const msg = i.message ?? 'Invalid value';
|
|
1492
|
+
return path ? `[schema] ${path}: ${msg}` : `[schema] ${msg}`;
|
|
1493
|
+
})
|
|
1494
|
+
: ['[schema] validation failed'];
|
|
1495
|
+
return issues;
|
|
1496
|
+
}
|
|
1497
|
+
return [];
|
|
1498
|
+
}
|
|
1499
|
+
catch {
|
|
1500
|
+
// If schema invocation fails, surface a single diagnostic.
|
|
1501
|
+
return [
|
|
1502
|
+
'[schema] validation failed (unable to execute schema.safeParse)',
|
|
1503
|
+
];
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
const requiredKeys = pick((cfg) => cfg['requiredKeys']);
|
|
1507
|
+
if (Array.isArray(requiredKeys) && requiredKeys.length > 0) {
|
|
1508
|
+
const missing = requiredKeys.filter((k) => finalEnv[k] === undefined);
|
|
1509
|
+
if (missing.length > 0) {
|
|
1510
|
+
return missing.map((k) => `[requiredKeys] missing: ${k}`);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
return [];
|
|
1514
|
+
};
|
|
1515
|
+
|
|
1236
1516
|
/**
|
|
1237
1517
|
* Attach legacy root flags to a Commander program.
|
|
1238
1518
|
* Uses provided defaults to render help labels without coupling to generators.
|
|
@@ -1315,6 +1595,7 @@ const attachRootOptions = (program, defaults, opts) => {
|
|
|
1315
1595
|
.addOption(new Option('-l, --log', `console log loaded variables ON${log ? ' (default)' : ''}`).conflicts('logOff'))
|
|
1316
1596
|
.addOption(new Option('-L, --log-off', `console log loaded variables OFF${!log ? ' (default)' : ''}`).conflicts('log'))
|
|
1317
1597
|
.option('--capture', 'capture child process stdio for commands (tests/CI)')
|
|
1598
|
+
.option('--redact', 'mask secret-like values in logs/trace (presentation-only)')
|
|
1318
1599
|
.option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
|
|
1319
1600
|
.option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
|
|
1320
1601
|
.option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
|
|
@@ -1332,6 +1613,16 @@ const attachRootOptions = (program, defaults, opts) => {
|
|
|
1332
1613
|
.hideHelp());
|
|
1333
1614
|
// Diagnostics: opt-in tracing; optional variadic keys after the flag.
|
|
1334
1615
|
p = p.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)');
|
|
1616
|
+
// Validation: strict mode fails on env validation issues (warn by default).
|
|
1617
|
+
p = p.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
|
|
1618
|
+
// Entropy diagnostics (presentation-only)
|
|
1619
|
+
p = p
|
|
1620
|
+
.addOption(new Option('--entropy-warn', 'enable entropy warnings (default on)').conflicts('entropyWarnOff'))
|
|
1621
|
+
.addOption(new Option('--entropy-warn-off', 'disable entropy warnings').conflicts('entropyWarn'))
|
|
1622
|
+
.option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
|
|
1623
|
+
.option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
|
|
1624
|
+
.option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
|
|
1625
|
+
.option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
|
|
1335
1626
|
// Restore original methods to avoid tagging future additions outside base.
|
|
1336
1627
|
program.addOption = originalAddOption;
|
|
1337
1628
|
program.option = originalOption;
|
|
@@ -1424,7 +1715,7 @@ const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
|
|
|
1424
1715
|
const parent = typeof parentJson === 'string' && parentJson.length > 0
|
|
1425
1716
|
? JSON.parse(parentJson)
|
|
1426
1717
|
: undefined;
|
|
1427
|
-
const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, scripts, shellOff, ...rest } = rawCliOptions;
|
|
1718
|
+
const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, entropyWarn, entropyWarnOff, scripts, shellOff, ...rest } = rawCliOptions;
|
|
1428
1719
|
const current = { ...rest };
|
|
1429
1720
|
if (typeof scripts === 'string') {
|
|
1430
1721
|
try {
|
|
@@ -1444,6 +1735,8 @@ const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
|
|
|
1444
1735
|
setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
|
|
1445
1736
|
setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
|
|
1446
1737
|
setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
|
|
1738
|
+
// warnEntropy (tri-state)
|
|
1739
|
+
setOptionalFlag(merged, 'warnEntropy', resolveExclusion(merged.warnEntropy, entropyWarnOff, d.warnEntropy));
|
|
1447
1740
|
// Normalize shell for predictability: explicit default shell per OS.
|
|
1448
1741
|
const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
|
|
1449
1742
|
let resolvedShell = merged.shell;
|
|
@@ -1479,6 +1772,29 @@ GetDotenvCli.prototype.passOptions = function (defaults) {
|
|
|
1479
1772
|
// Build service options and compute context (always-on config loader path).
|
|
1480
1773
|
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
1481
1774
|
await this.resolveAndLoad(serviceOptions);
|
|
1775
|
+
// Global validation: once after Phase C using config sources.
|
|
1776
|
+
try {
|
|
1777
|
+
const ctx = this.getCtx();
|
|
1778
|
+
const dotenv = (ctx?.dotenv ?? {});
|
|
1779
|
+
const sources = await resolveGetDotenvConfigSources(import.meta.url);
|
|
1780
|
+
const issues = validateEnvAgainstSources(dotenv, sources);
|
|
1781
|
+
if (Array.isArray(issues) && issues.length > 0) {
|
|
1782
|
+
const logger = (merged.logger ??
|
|
1783
|
+
console);
|
|
1784
|
+
const emit = logger.error ?? logger.log;
|
|
1785
|
+
issues.forEach((m) => {
|
|
1786
|
+
emit(m);
|
|
1787
|
+
});
|
|
1788
|
+
if (merged.strict) {
|
|
1789
|
+
// Deterministic failure under strict mode
|
|
1790
|
+
process.exit(1);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
catch {
|
|
1795
|
+
// Be tolerant: validation errors reported above; unexpected failures here
|
|
1796
|
+
// should not crash non-strict flows.
|
|
1797
|
+
}
|
|
1482
1798
|
});
|
|
1483
1799
|
// Also handle root-level flows (no subcommand) so option-aliases can run
|
|
1484
1800
|
// with the same merged options and context without duplicating logic.
|
|
@@ -1492,6 +1808,26 @@ GetDotenvCli.prototype.passOptions = function (defaults) {
|
|
|
1492
1808
|
if (!this.getCtx()) {
|
|
1493
1809
|
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
1494
1810
|
await this.resolveAndLoad(serviceOptions);
|
|
1811
|
+
try {
|
|
1812
|
+
const ctx = this.getCtx();
|
|
1813
|
+
const dotenv = (ctx?.dotenv ?? {});
|
|
1814
|
+
const sources = await resolveGetDotenvConfigSources(import.meta.url);
|
|
1815
|
+
const issues = validateEnvAgainstSources(dotenv, sources);
|
|
1816
|
+
if (Array.isArray(issues) && issues.length > 0) {
|
|
1817
|
+
const logger = (merged
|
|
1818
|
+
.logger ?? console);
|
|
1819
|
+
const emit = logger.error ?? logger.log;
|
|
1820
|
+
issues.forEach((m) => {
|
|
1821
|
+
emit(m);
|
|
1822
|
+
});
|
|
1823
|
+
if (merged.strict) {
|
|
1824
|
+
process.exit(1);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
catch {
|
|
1829
|
+
// Tolerate validation side-effects in non-strict mode
|
|
1830
|
+
}
|
|
1495
1831
|
}
|
|
1496
1832
|
});
|
|
1497
1833
|
return this;
|
|
@@ -1688,6 +2024,48 @@ const runCommand = async (command, shell, opts) => {
|
|
|
1688
2024
|
}
|
|
1689
2025
|
};
|
|
1690
2026
|
|
|
2027
|
+
const dropUndefined = (bag) => Object.fromEntries(Object.entries(bag).filter((e) => typeof e[1] === 'string'));
|
|
2028
|
+
/** Build a sanitized env for child processes from base + overlay. */
|
|
2029
|
+
const buildSpawnEnv = (base, overlay) => {
|
|
2030
|
+
const raw = {
|
|
2031
|
+
...(base ?? {}),
|
|
2032
|
+
...(overlay ?? {}),
|
|
2033
|
+
};
|
|
2034
|
+
// Drop undefined first
|
|
2035
|
+
const entries = Object.entries(dropUndefined(raw));
|
|
2036
|
+
if (process.platform === 'win32') {
|
|
2037
|
+
// Windows: keys are case-insensitive; collapse duplicates
|
|
2038
|
+
const byLower = new Map();
|
|
2039
|
+
for (const [k, v] of entries) {
|
|
2040
|
+
byLower.set(k.toLowerCase(), [k, v]); // last wins; preserve latest casing
|
|
2041
|
+
}
|
|
2042
|
+
const out = {};
|
|
2043
|
+
for (const [, [k, v]] of byLower)
|
|
2044
|
+
out[k] = v;
|
|
2045
|
+
// HOME fallback from USERPROFILE (common expectation)
|
|
2046
|
+
if (!Object.prototype.hasOwnProperty.call(out, 'HOME')) {
|
|
2047
|
+
const up = out['USERPROFILE'];
|
|
2048
|
+
if (typeof up === 'string' && up.length > 0)
|
|
2049
|
+
out['HOME'] = up;
|
|
2050
|
+
}
|
|
2051
|
+
// Normalize TMP/TEMP coherence (pick any present; reflect to both)
|
|
2052
|
+
const tmp = out['TMP'] ?? out['TEMP'];
|
|
2053
|
+
if (typeof tmp === 'string' && tmp.length > 0) {
|
|
2054
|
+
out['TMP'] = tmp;
|
|
2055
|
+
out['TEMP'] = tmp;
|
|
2056
|
+
}
|
|
2057
|
+
return out;
|
|
2058
|
+
}
|
|
2059
|
+
// POSIX: keep keys as-is
|
|
2060
|
+
const out = Object.fromEntries(entries);
|
|
2061
|
+
// Ensure TMPDIR exists when any temp key is present (best-effort)
|
|
2062
|
+
const tmpdir = out['TMPDIR'] ?? out['TMP'] ?? out['TEMP'];
|
|
2063
|
+
if (typeof tmpdir === 'string' && tmpdir.length > 0) {
|
|
2064
|
+
out['TMPDIR'] = tmpdir;
|
|
2065
|
+
}
|
|
2066
|
+
return out;
|
|
2067
|
+
};
|
|
2068
|
+
|
|
1691
2069
|
/**
|
|
1692
2070
|
* Define a GetDotenv CLI plugin with compositional helpers.
|
|
1693
2071
|
*
|
|
@@ -2045,7 +2423,7 @@ const awsPlugin = () => definePlugin({
|
|
|
2045
2423
|
const shellSetting = resolveShell(rootOpts?.scripts, 'aws', rootOpts?.shell);
|
|
2046
2424
|
const ctxDotenv = (ctx?.dotenv ?? {});
|
|
2047
2425
|
const exit = await runCommand(argv, shellSetting, {
|
|
2048
|
-
env:
|
|
2426
|
+
env: buildSpawnEnv(process.env, ctxDotenv),
|
|
2049
2427
|
stdio: capture ? 'pipe' : 'inherit',
|
|
2050
2428
|
});
|
|
2051
2429
|
// Deterministic termination (suppressed under tests)
|
|
@@ -2184,14 +2562,12 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, igno
|
|
|
2184
2562
|
const hasCmd = (typeof command === 'string' && command.length > 0) ||
|
|
2185
2563
|
(Array.isArray(command) && command.length > 0);
|
|
2186
2564
|
if (hasCmd) {
|
|
2565
|
+
const envBag = getDotenvCliOptions !== undefined
|
|
2566
|
+
? { getDotenvCliOptions: JSON.stringify(getDotenvCliOptions) }
|
|
2567
|
+
: undefined;
|
|
2187
2568
|
await runCommand(command, shell, {
|
|
2188
2569
|
cwd: path,
|
|
2189
|
-
env:
|
|
2190
|
-
...process.env,
|
|
2191
|
-
getDotenvCliOptions: getDotenvCliOptions
|
|
2192
|
-
? JSON.stringify(getDotenvCliOptions)
|
|
2193
|
-
: undefined,
|
|
2194
|
-
},
|
|
2570
|
+
env: buildSpawnEnv(process.env, envBag),
|
|
2195
2571
|
stdio: capture ? 'pipe' : 'inherit',
|
|
2196
2572
|
});
|
|
2197
2573
|
}
|
|
@@ -2549,7 +2925,9 @@ const attachParentAlias = (cli, options, _cmd) => {
|
|
|
2549
2925
|
aliasHandled = true;
|
|
2550
2926
|
dbg('alias-only invocation detected');
|
|
2551
2927
|
// Merge CLI options and resolve dotenv context.
|
|
2552
|
-
const { merged } = resolveCliOptions(o,
|
|
2928
|
+
const { merged } = resolveCliOptions(o,
|
|
2929
|
+
// cast through unknown to avoid readonly -> mutable incompatibilities
|
|
2930
|
+
baseRootOptionDefaults, process.env.getDotenvCliOptions);
|
|
2553
2931
|
const logger = merged.logger ?? console;
|
|
2554
2932
|
const serviceOptions = getDotenvCliOptions2Options(merged);
|
|
2555
2933
|
await cli.resolveAndLoad(serviceOptions);
|
|
@@ -2602,7 +2980,41 @@ const attachParentAlias = (cli, options, _cmd) => {
|
|
|
2602
2980
|
: parent !== undefined
|
|
2603
2981
|
? 'parent'
|
|
2604
2982
|
: 'unset';
|
|
2605
|
-
|
|
2983
|
+
// Build redact options and triple bag without undefined-valued fields
|
|
2984
|
+
const redOpts = {};
|
|
2985
|
+
const redFlag = merged.redact;
|
|
2986
|
+
const redPatterns = merged
|
|
2987
|
+
.redactPatterns;
|
|
2988
|
+
if (redFlag)
|
|
2989
|
+
redOpts.redact = true;
|
|
2990
|
+
if (redFlag && Array.isArray(redPatterns))
|
|
2991
|
+
redOpts.redactPatterns = redPatterns;
|
|
2992
|
+
const tripleBag = {};
|
|
2993
|
+
if (parent !== undefined)
|
|
2994
|
+
tripleBag.parent = parent;
|
|
2995
|
+
if (dot !== undefined)
|
|
2996
|
+
tripleBag.dotenv = dot;
|
|
2997
|
+
if (final !== undefined)
|
|
2998
|
+
tripleBag.final = final;
|
|
2999
|
+
const triple = redactTriple(k, tripleBag, redOpts);
|
|
3000
|
+
process.stderr.write(`[trace] key=${k} origin=${origin} parent=${triple.parent ?? ''} dotenv=${triple.dotenv ?? ''} final=${triple.final ?? ''}\n`);
|
|
3001
|
+
const entOpts = {};
|
|
3002
|
+
const warnEntropy = merged.warnEntropy;
|
|
3003
|
+
const entropyThreshold = merged
|
|
3004
|
+
.entropyThreshold;
|
|
3005
|
+
const entropyMinLength = merged
|
|
3006
|
+
.entropyMinLength;
|
|
3007
|
+
const entropyWhitelist = merged
|
|
3008
|
+
.entropyWhitelist;
|
|
3009
|
+
if (typeof warnEntropy === 'boolean')
|
|
3010
|
+
entOpts.warnEntropy = warnEntropy;
|
|
3011
|
+
if (typeof entropyThreshold === 'number')
|
|
3012
|
+
entOpts.entropyThreshold = entropyThreshold;
|
|
3013
|
+
if (typeof entropyMinLength === 'number')
|
|
3014
|
+
entOpts.entropyMinLength = entropyMinLength;
|
|
3015
|
+
if (Array.isArray(entropyWhitelist))
|
|
3016
|
+
entOpts.entropyWhitelist = entropyWhitelist;
|
|
3017
|
+
maybeWarnEntropy(k, final, origin, entOpts, (line) => process.stderr.write(line + '\n'));
|
|
2606
3018
|
}
|
|
2607
3019
|
}
|
|
2608
3020
|
let exitCode = Number.NaN;
|
|
@@ -2657,11 +3069,10 @@ const attachParentAlias = (cli, options, _cmd) => {
|
|
|
2657
3069
|
}
|
|
2658
3070
|
}
|
|
2659
3071
|
exitCode = await runCommand(commandArg, shellSetting, {
|
|
2660
|
-
env: {
|
|
2661
|
-
...process.env,
|
|
3072
|
+
env: buildSpawnEnv(process.env, {
|
|
2662
3073
|
...dotenv,
|
|
2663
3074
|
getDotenvCliOptions: JSON.stringify(envBag),
|
|
2664
|
-
},
|
|
3075
|
+
}),
|
|
2665
3076
|
stdio: capture ? 'pipe' : 'inherit',
|
|
2666
3077
|
});
|
|
2667
3078
|
dbg('run:done', { exitCode });
|
|
@@ -2819,8 +3230,41 @@ const cmdPlugin = (options = {}) => definePlugin({
|
|
|
2819
3230
|
: parent !== undefined
|
|
2820
3231
|
? 'parent'
|
|
2821
3232
|
: 'unset';
|
|
3233
|
+
// Apply presentation-time redaction (if enabled)
|
|
3234
|
+
const redFlag = merged.redact;
|
|
3235
|
+
const redPatterns = merged
|
|
3236
|
+
.redactPatterns;
|
|
3237
|
+
const redOpts = {};
|
|
3238
|
+
if (redFlag)
|
|
3239
|
+
redOpts.redact = true;
|
|
3240
|
+
if (redFlag && Array.isArray(redPatterns))
|
|
3241
|
+
redOpts.redactPatterns = redPatterns;
|
|
3242
|
+
const tripleBag = {};
|
|
3243
|
+
if (parent !== undefined)
|
|
3244
|
+
tripleBag.parent = parent;
|
|
3245
|
+
if (dot !== undefined)
|
|
3246
|
+
tripleBag.dotenv = dot;
|
|
3247
|
+
if (final !== undefined)
|
|
3248
|
+
tripleBag.final = final;
|
|
3249
|
+
const triple = redactTriple(k, tripleBag, redOpts);
|
|
2822
3250
|
// Emit concise diagnostic line to stderr.
|
|
2823
|
-
process.stderr.write(`[trace] key=${k} origin=${origin} parent=${parent ?? ''} dotenv=${
|
|
3251
|
+
process.stderr.write(`[trace] key=${k} origin=${origin} parent=${triple.parent ?? ''} dotenv=${triple.dotenv ?? ''} final=${triple.final ?? ''}\n`);
|
|
3252
|
+
// Optional entropy warning (once-per-key)
|
|
3253
|
+
const entOpts = {};
|
|
3254
|
+
const warnEntropy = merged
|
|
3255
|
+
.warnEntropy;
|
|
3256
|
+
const entropyThreshold = merged.entropyThreshold;
|
|
3257
|
+
const entropyMinLength = merged.entropyMinLength;
|
|
3258
|
+
const entropyWhitelist = merged.entropyWhitelist;
|
|
3259
|
+
if (typeof warnEntropy === 'boolean')
|
|
3260
|
+
entOpts.warnEntropy = warnEntropy;
|
|
3261
|
+
if (typeof entropyThreshold === 'number')
|
|
3262
|
+
entOpts.entropyThreshold = entropyThreshold;
|
|
3263
|
+
if (typeof entropyMinLength === 'number')
|
|
3264
|
+
entOpts.entropyMinLength = entropyMinLength;
|
|
3265
|
+
if (Array.isArray(entropyWhitelist))
|
|
3266
|
+
entOpts.entropyWhitelist = entropyWhitelist;
|
|
3267
|
+
maybeWarnEntropy(k, final, origin, entOpts, (line) => process.stderr.write(line + '\n'));
|
|
2824
3268
|
}
|
|
2825
3269
|
}
|
|
2826
3270
|
const shellSetting = resolveShell(scripts, input, shell);
|
|
@@ -2838,11 +3282,10 @@ const cmdPlugin = (options = {}) => definePlugin({
|
|
|
2838
3282
|
? args.map(String)
|
|
2839
3283
|
: resolved;
|
|
2840
3284
|
await runCommand(commandArg, shellSetting, {
|
|
2841
|
-
env: {
|
|
2842
|
-
...process.env,
|
|
3285
|
+
env: buildSpawnEnv(process.env, {
|
|
2843
3286
|
...dotenv,
|
|
2844
3287
|
getDotenvCliOptions: JSON.stringify(envBag),
|
|
2845
|
-
},
|
|
3288
|
+
}),
|
|
2846
3289
|
stdio: capture ? 'pipe' : 'inherit',
|
|
2847
3290
|
});
|
|
2848
3291
|
});
|