@karmaniverous/get-dotenv 5.0.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 +393 -21
- package/dist/cliHost.d.cts +132 -3
- package/dist/cliHost.d.mts +132 -3
- package/dist/cliHost.d.ts +132 -3
- package/dist/cliHost.mjs +393 -22
- 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 +678 -39
- package/dist/index.cjs +604 -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 +604 -246
- package/dist/plugins-aws.cjs +43 -1
- package/dist/plugins-aws.d.cts +115 -0
- package/dist/plugins-aws.d.mts +115 -0
- package/dist/plugins-aws.d.ts +115 -0
- package/dist/plugins-aws.mjs +43 -1
- package/dist/plugins-batch.cjs +46 -6
- package/dist/plugins-batch.d.cts +115 -0
- package/dist/plugins-batch.d.mts +115 -0
- package/dist/plugins-batch.d.ts +115 -0
- package/dist/plugins-batch.mjs +46 -6
- package/dist/plugins-init.d.cts +115 -0
- package/dist/plugins-init.d.mts +115 -0
- package/dist/plugins-init.d.ts +115 -0
- package/package.json +25 -24
package/dist/index.cjs
CHANGED
|
@@ -7,11 +7,11 @@ var path = require('path');
|
|
|
7
7
|
var execa = require('execa');
|
|
8
8
|
var fs = require('fs-extra');
|
|
9
9
|
var url = require('url');
|
|
10
|
+
var YAML = require('yaml');
|
|
11
|
+
var zod = require('zod');
|
|
10
12
|
var nanoid = require('nanoid');
|
|
11
13
|
var dotenv = require('dotenv');
|
|
12
14
|
var crypto = require('crypto');
|
|
13
|
-
var YAML = require('yaml');
|
|
14
|
-
var zod = require('zod');
|
|
15
15
|
|
|
16
16
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
17
17
|
/**
|
|
@@ -155,6 +155,28 @@ const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
|
|
|
155
155
|
* Uses provided defaults to render help labels without coupling to generators.
|
|
156
156
|
*/
|
|
157
157
|
const attachRootOptions = (program, defaults, opts) => {
|
|
158
|
+
// Install temporary wrappers to tag all options added here as "base".
|
|
159
|
+
const GROUP = 'base';
|
|
160
|
+
const tagLatest = (cmd, group) => {
|
|
161
|
+
const optsArr = cmd.options;
|
|
162
|
+
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
163
|
+
const last = optsArr[optsArr.length - 1];
|
|
164
|
+
last.__group = group;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const originalAddOption = program.addOption.bind(program);
|
|
168
|
+
const originalOption = program.option.bind(program);
|
|
169
|
+
program.addOption = function patchedAdd(opt) {
|
|
170
|
+
// Tag before adding, in case consumers inspect the Option directly.
|
|
171
|
+
opt.__group = GROUP;
|
|
172
|
+
const ret = originalAddOption(opt);
|
|
173
|
+
return ret;
|
|
174
|
+
};
|
|
175
|
+
program.option = function patchedOption(...args) {
|
|
176
|
+
const ret = originalOption(...args);
|
|
177
|
+
tagLatest(this, GROUP);
|
|
178
|
+
return ret;
|
|
179
|
+
};
|
|
158
180
|
const { defaultEnv, dotenvToken, dynamicPath, env, excludeDynamic, excludeEnv, excludeGlobal, excludePrivate, excludePublic, loadProcess, log, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, shell, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
|
|
159
181
|
const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
|
|
160
182
|
const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
|
|
@@ -210,6 +232,7 @@ const attachRootOptions = (program, defaults, opts) => {
|
|
|
210
232
|
.addOption(new commander.Option('-l, --log', `console log loaded variables ON${log ? ' (default)' : ''}`).conflicts('logOff'))
|
|
211
233
|
.addOption(new commander.Option('-L, --log-off', `console log loaded variables OFF${!log ? ' (default)' : ''}`).conflicts('log'))
|
|
212
234
|
.option('--capture', 'capture child process stdio for commands (tests/CI)')
|
|
235
|
+
.option('--redact', 'mask secret-like values in logs/trace (presentation-only)')
|
|
213
236
|
.option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
|
|
214
237
|
.option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
|
|
215
238
|
.option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
|
|
@@ -227,6 +250,19 @@ const attachRootOptions = (program, defaults, opts) => {
|
|
|
227
250
|
.hideHelp());
|
|
228
251
|
// Diagnostics: opt-in tracing; optional variadic keys after the flag.
|
|
229
252
|
p = p.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)');
|
|
253
|
+
// Validation: strict mode fails on env validation issues (warn by default).
|
|
254
|
+
p = p.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
|
|
255
|
+
// Entropy diagnostics (presentation-only)
|
|
256
|
+
p = p
|
|
257
|
+
.addOption(new commander.Option('--entropy-warn', 'enable entropy warnings (default on)').conflicts('entropyWarnOff'))
|
|
258
|
+
.addOption(new commander.Option('--entropy-warn-off', 'disable entropy warnings').conflicts('entropyWarn'))
|
|
259
|
+
.option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
|
|
260
|
+
.option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
|
|
261
|
+
.option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
|
|
262
|
+
.option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
|
|
263
|
+
// Restore original methods to avoid tagging future additions outside base.
|
|
264
|
+
program.addOption = originalAddOption;
|
|
265
|
+
program.option = originalOption;
|
|
230
266
|
return p;
|
|
231
267
|
};
|
|
232
268
|
|
|
@@ -395,6 +431,48 @@ const runCommand = async (command, shell, opts) => {
|
|
|
395
431
|
}
|
|
396
432
|
};
|
|
397
433
|
|
|
434
|
+
const dropUndefined = (bag) => Object.fromEntries(Object.entries(bag).filter((e) => typeof e[1] === 'string'));
|
|
435
|
+
/** Build a sanitized env for child processes from base + overlay. */
|
|
436
|
+
const buildSpawnEnv = (base, overlay) => {
|
|
437
|
+
const raw = {
|
|
438
|
+
...(base ?? {}),
|
|
439
|
+
...(overlay ?? {}),
|
|
440
|
+
};
|
|
441
|
+
// Drop undefined first
|
|
442
|
+
const entries = Object.entries(dropUndefined(raw));
|
|
443
|
+
if (process.platform === 'win32') {
|
|
444
|
+
// Windows: keys are case-insensitive; collapse duplicates
|
|
445
|
+
const byLower = new Map();
|
|
446
|
+
for (const [k, v] of entries) {
|
|
447
|
+
byLower.set(k.toLowerCase(), [k, v]); // last wins; preserve latest casing
|
|
448
|
+
}
|
|
449
|
+
const out = {};
|
|
450
|
+
for (const [, [k, v]] of byLower)
|
|
451
|
+
out[k] = v;
|
|
452
|
+
// HOME fallback from USERPROFILE (common expectation)
|
|
453
|
+
if (!Object.prototype.hasOwnProperty.call(out, 'HOME')) {
|
|
454
|
+
const up = out['USERPROFILE'];
|
|
455
|
+
if (typeof up === 'string' && up.length > 0)
|
|
456
|
+
out['HOME'] = up;
|
|
457
|
+
}
|
|
458
|
+
// Normalize TMP/TEMP coherence (pick any present; reflect to both)
|
|
459
|
+
const tmp = out['TMP'] ?? out['TEMP'];
|
|
460
|
+
if (typeof tmp === 'string' && tmp.length > 0) {
|
|
461
|
+
out['TMP'] = tmp;
|
|
462
|
+
out['TEMP'] = tmp;
|
|
463
|
+
}
|
|
464
|
+
return out;
|
|
465
|
+
}
|
|
466
|
+
// POSIX: keep keys as-is
|
|
467
|
+
const out = Object.fromEntries(entries);
|
|
468
|
+
// Ensure TMPDIR exists when any temp key is present (best-effort)
|
|
469
|
+
const tmpdir = out['TMPDIR'] ?? out['TMP'] ?? out['TEMP'];
|
|
470
|
+
if (typeof tmpdir === 'string' && tmpdir.length > 0) {
|
|
471
|
+
out['TMPDIR'] = tmpdir;
|
|
472
|
+
}
|
|
473
|
+
return out;
|
|
474
|
+
};
|
|
475
|
+
|
|
398
476
|
const globPaths = async ({ globs, logger, pkgCwd, rootPath, }) => {
|
|
399
477
|
let cwd = process.cwd();
|
|
400
478
|
if (pkgCwd) {
|
|
@@ -467,14 +545,12 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, igno
|
|
|
467
545
|
const hasCmd = (typeof command === 'string' && command.length > 0) ||
|
|
468
546
|
(Array.isArray(command) && command.length > 0);
|
|
469
547
|
if (hasCmd) {
|
|
548
|
+
const envBag = getDotenvCliOptions !== undefined
|
|
549
|
+
? { getDotenvCliOptions: JSON.stringify(getDotenvCliOptions) }
|
|
550
|
+
: undefined;
|
|
470
551
|
await runCommand(command, shell, {
|
|
471
552
|
cwd: path,
|
|
472
|
-
env:
|
|
473
|
-
...process.env,
|
|
474
|
-
getDotenvCliOptions: getDotenvCliOptions
|
|
475
|
-
? JSON.stringify(getDotenvCliOptions)
|
|
476
|
-
: undefined,
|
|
477
|
-
},
|
|
553
|
+
env: buildSpawnEnv(process.env, envBag),
|
|
478
554
|
stdio: capture ? 'pipe' : 'inherit',
|
|
479
555
|
});
|
|
480
556
|
}
|
|
@@ -627,6 +703,11 @@ const baseRootOptionDefaults = {
|
|
|
627
703
|
dotenvToken: '.env',
|
|
628
704
|
loadProcess: true,
|
|
629
705
|
logger: console,
|
|
706
|
+
// Diagnostics defaults
|
|
707
|
+
warnEntropy: true,
|
|
708
|
+
entropyThreshold: 3.8,
|
|
709
|
+
entropyMinLength: 16,
|
|
710
|
+
entropyWhitelist: ['^GIT_', '^npm_', '^CI$', 'SHLVL'],
|
|
630
711
|
paths: './',
|
|
631
712
|
pathsDelimiter: ' ',
|
|
632
713
|
privateToken: 'local',
|
|
@@ -647,7 +728,7 @@ const baseRootOptionDefaults = {
|
|
|
647
728
|
const baseGetDotenvCliOptions = baseRootOptionDefaults;
|
|
648
729
|
|
|
649
730
|
/** @internal */
|
|
650
|
-
const isPlainObject = (value) => value !== null &&
|
|
731
|
+
const isPlainObject$1 = (value) => value !== null &&
|
|
651
732
|
typeof value === 'object' &&
|
|
652
733
|
Object.getPrototypeOf(value) === Object.prototype;
|
|
653
734
|
const mergeInto = (target, source) => {
|
|
@@ -655,10 +736,10 @@ const mergeInto = (target, source) => {
|
|
|
655
736
|
if (sVal === undefined)
|
|
656
737
|
continue; // do not overwrite with undefined
|
|
657
738
|
const tVal = target[key];
|
|
658
|
-
if (isPlainObject(tVal) && isPlainObject(sVal)) {
|
|
739
|
+
if (isPlainObject$1(tVal) && isPlainObject$1(sVal)) {
|
|
659
740
|
target[key] = mergeInto({ ...tVal }, sVal);
|
|
660
741
|
}
|
|
661
|
-
else if (isPlainObject(sVal)) {
|
|
742
|
+
else if (isPlainObject$1(sVal)) {
|
|
662
743
|
target[key] = mergeInto({}, sVal);
|
|
663
744
|
}
|
|
664
745
|
else {
|
|
@@ -908,7 +989,7 @@ const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
|
|
|
908
989
|
const parent = typeof parentJson === 'string' && parentJson.length > 0
|
|
909
990
|
? JSON.parse(parentJson)
|
|
910
991
|
: undefined;
|
|
911
|
-
const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, scripts, shellOff, ...rest } = rawCliOptions;
|
|
992
|
+
const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, entropyWarn, entropyWarnOff, scripts, shellOff, ...rest } = rawCliOptions;
|
|
912
993
|
const current = { ...rest };
|
|
913
994
|
if (typeof scripts === 'string') {
|
|
914
995
|
try {
|
|
@@ -928,6 +1009,8 @@ const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
|
|
|
928
1009
|
setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
|
|
929
1010
|
setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
|
|
930
1011
|
setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
|
|
1012
|
+
// warnEntropy (tri-state)
|
|
1013
|
+
setOptionalFlag(merged, 'warnEntropy', resolveExclusion(merged.warnEntropy, entropyWarnOff, d.warnEntropy));
|
|
931
1014
|
// Normalize shell for predictability: explicit default shell per OS.
|
|
932
1015
|
const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
|
|
933
1016
|
let resolvedShell = merged.shell;
|
|
@@ -945,6 +1028,315 @@ const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
|
|
|
945
1028
|
return cmd !== undefined ? { merged, command: cmd } : { merged };
|
|
946
1029
|
};
|
|
947
1030
|
|
|
1031
|
+
/**
|
|
1032
|
+
* Zod schemas for configuration files discovered by the new loader.
|
|
1033
|
+
*
|
|
1034
|
+
* Notes:
|
|
1035
|
+
* - RAW: all fields optional; shapes are stringly-friendly (paths may be string[] or string).
|
|
1036
|
+
* - RESOLVED: normalized shapes (paths always string[]).
|
|
1037
|
+
* - For JSON/YAML configs, the loader rejects "dynamic" and "schema" (JS/TS-only).
|
|
1038
|
+
*/
|
|
1039
|
+
// String-only env value map
|
|
1040
|
+
const stringMap = zod.z.record(zod.z.string(), zod.z.string());
|
|
1041
|
+
const envStringMap = zod.z.record(zod.z.string(), stringMap);
|
|
1042
|
+
// Allow string[] or single string for "paths" in RAW; normalize later.
|
|
1043
|
+
const rawPathsSchema = zod.z.union([zod.z.array(zod.z.string()), zod.z.string()]).optional();
|
|
1044
|
+
const getDotenvConfigSchemaRaw = zod.z.object({
|
|
1045
|
+
dotenvToken: zod.z.string().optional(),
|
|
1046
|
+
privateToken: zod.z.string().optional(),
|
|
1047
|
+
paths: rawPathsSchema,
|
|
1048
|
+
loadProcess: zod.z.boolean().optional(),
|
|
1049
|
+
log: zod.z.boolean().optional(),
|
|
1050
|
+
shell: zod.z.union([zod.z.string(), zod.z.boolean()]).optional(),
|
|
1051
|
+
scripts: zod.z.record(zod.z.string(), zod.z.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
|
|
1052
|
+
requiredKeys: zod.z.array(zod.z.string()).optional(),
|
|
1053
|
+
schema: zod.z.unknown().optional(), // JS/TS-only; loader rejects in JSON/YAML
|
|
1054
|
+
vars: stringMap.optional(), // public, global
|
|
1055
|
+
envVars: envStringMap.optional(), // public, per-env
|
|
1056
|
+
// Dynamic in config (JS/TS only). JSON/YAML loader will reject if set.
|
|
1057
|
+
dynamic: zod.z.unknown().optional(),
|
|
1058
|
+
// Per-plugin config bag; validated by plugins/host when used.
|
|
1059
|
+
plugins: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
|
|
1060
|
+
});
|
|
1061
|
+
// Normalize paths to string[]
|
|
1062
|
+
const normalizePaths = (p) => p === undefined ? undefined : Array.isArray(p) ? p : [p];
|
|
1063
|
+
const getDotenvConfigSchemaResolved = getDotenvConfigSchemaRaw.transform((raw) => ({
|
|
1064
|
+
...raw,
|
|
1065
|
+
paths: normalizePaths(raw.paths),
|
|
1066
|
+
}));
|
|
1067
|
+
|
|
1068
|
+
// Discovery candidates (first match wins per scope/privacy).
|
|
1069
|
+
// Order preserves historical JSON/YAML precedence; JS/TS added afterwards.
|
|
1070
|
+
const PUBLIC_FILENAMES = [
|
|
1071
|
+
'getdotenv.config.json',
|
|
1072
|
+
'getdotenv.config.yaml',
|
|
1073
|
+
'getdotenv.config.yml',
|
|
1074
|
+
'getdotenv.config.js',
|
|
1075
|
+
'getdotenv.config.mjs',
|
|
1076
|
+
'getdotenv.config.cjs',
|
|
1077
|
+
'getdotenv.config.ts',
|
|
1078
|
+
'getdotenv.config.mts',
|
|
1079
|
+
'getdotenv.config.cts',
|
|
1080
|
+
];
|
|
1081
|
+
const LOCAL_FILENAMES = [
|
|
1082
|
+
'getdotenv.config.local.json',
|
|
1083
|
+
'getdotenv.config.local.yaml',
|
|
1084
|
+
'getdotenv.config.local.yml',
|
|
1085
|
+
'getdotenv.config.local.js',
|
|
1086
|
+
'getdotenv.config.local.mjs',
|
|
1087
|
+
'getdotenv.config.local.cjs',
|
|
1088
|
+
'getdotenv.config.local.ts',
|
|
1089
|
+
'getdotenv.config.local.mts',
|
|
1090
|
+
'getdotenv.config.local.cts',
|
|
1091
|
+
];
|
|
1092
|
+
const isYaml = (p) => ['.yaml', '.yml'].includes(path.extname(p).toLowerCase());
|
|
1093
|
+
const isJson = (p) => path.extname(p).toLowerCase() === '.json';
|
|
1094
|
+
const isJsOrTs = (p) => ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'].includes(path.extname(p).toLowerCase());
|
|
1095
|
+
// --- Internal JS/TS module loader helpers (default export) ---
|
|
1096
|
+
const importDefault$1 = async (fileUrl) => {
|
|
1097
|
+
const mod = (await import(fileUrl));
|
|
1098
|
+
return mod.default;
|
|
1099
|
+
};
|
|
1100
|
+
const cacheName = (absPath, suffix) => {
|
|
1101
|
+
// sanitized filename with suffix; recompile on mtime changes not tracked here (simplified)
|
|
1102
|
+
const base = path.basename(absPath).replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
1103
|
+
return `${base}.${suffix}.mjs`;
|
|
1104
|
+
};
|
|
1105
|
+
const ensureDir = async (dir) => {
|
|
1106
|
+
await fs.ensureDir(dir);
|
|
1107
|
+
return dir;
|
|
1108
|
+
};
|
|
1109
|
+
const loadJsTsDefault = async (absPath) => {
|
|
1110
|
+
const fileUrl = url.pathToFileURL(absPath).toString();
|
|
1111
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
1112
|
+
if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
|
|
1113
|
+
return importDefault$1(fileUrl);
|
|
1114
|
+
}
|
|
1115
|
+
// Try direct import first in case a TS loader is active.
|
|
1116
|
+
try {
|
|
1117
|
+
const val = await importDefault$1(fileUrl);
|
|
1118
|
+
if (val)
|
|
1119
|
+
return val;
|
|
1120
|
+
}
|
|
1121
|
+
catch {
|
|
1122
|
+
/* fallthrough */
|
|
1123
|
+
}
|
|
1124
|
+
// esbuild bundle to a temp ESM file
|
|
1125
|
+
try {
|
|
1126
|
+
const esbuild = (await import('esbuild'));
|
|
1127
|
+
const outDir = await ensureDir(path.resolve('.tsbuild', 'getdotenv-config'));
|
|
1128
|
+
const outfile = path.join(outDir, cacheName(absPath, 'bundle'));
|
|
1129
|
+
await esbuild.build({
|
|
1130
|
+
entryPoints: [absPath],
|
|
1131
|
+
bundle: true,
|
|
1132
|
+
platform: 'node',
|
|
1133
|
+
format: 'esm',
|
|
1134
|
+
target: 'node20',
|
|
1135
|
+
outfile,
|
|
1136
|
+
sourcemap: false,
|
|
1137
|
+
logLevel: 'silent',
|
|
1138
|
+
});
|
|
1139
|
+
return await importDefault$1(url.pathToFileURL(outfile).toString());
|
|
1140
|
+
}
|
|
1141
|
+
catch {
|
|
1142
|
+
/* fallthrough to TS transpile */
|
|
1143
|
+
}
|
|
1144
|
+
// typescript.transpileModule simple transpile (single-file)
|
|
1145
|
+
try {
|
|
1146
|
+
const ts = (await import('typescript'));
|
|
1147
|
+
const src = await fs.readFile(absPath, 'utf-8');
|
|
1148
|
+
const out = ts.transpileModule(src, {
|
|
1149
|
+
compilerOptions: {
|
|
1150
|
+
module: 'ESNext',
|
|
1151
|
+
target: 'ES2022',
|
|
1152
|
+
moduleResolution: 'NodeNext',
|
|
1153
|
+
},
|
|
1154
|
+
}).outputText;
|
|
1155
|
+
const outDir = await ensureDir(path.resolve('.tsbuild', 'getdotenv-config'));
|
|
1156
|
+
const outfile = path.join(outDir, cacheName(absPath, 'ts'));
|
|
1157
|
+
await fs.writeFile(outfile, out, 'utf-8');
|
|
1158
|
+
return await importDefault$1(url.pathToFileURL(outfile).toString());
|
|
1159
|
+
}
|
|
1160
|
+
catch {
|
|
1161
|
+
throw new Error(`Unable to load JS/TS config: ${absPath}. Install 'esbuild' for robust bundling or ensure a TS loader.`);
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
/**
|
|
1165
|
+
* Discover JSON/YAML config files in the packaged root and project root.
|
|
1166
|
+
* Order: packaged public → project public → project local. */
|
|
1167
|
+
const discoverConfigFiles = async (importMetaUrl) => {
|
|
1168
|
+
const files = [];
|
|
1169
|
+
// Packaged root via importMetaUrl (optional)
|
|
1170
|
+
if (importMetaUrl) {
|
|
1171
|
+
const fromUrl = url.fileURLToPath(importMetaUrl);
|
|
1172
|
+
const packagedRoot = await packageDirectory.packageDirectory({ cwd: fromUrl });
|
|
1173
|
+
if (packagedRoot) {
|
|
1174
|
+
for (const name of PUBLIC_FILENAMES) {
|
|
1175
|
+
const p = path.join(packagedRoot, name);
|
|
1176
|
+
if (await fs.pathExists(p)) {
|
|
1177
|
+
files.push({ path: p, privacy: 'public', scope: 'packaged' });
|
|
1178
|
+
break; // only one public file expected per scope
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
// By policy, packaged .local is not expected; skip even if present.
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
// Project root (from current working directory)
|
|
1185
|
+
const projectRoot = await packageDirectory.packageDirectory();
|
|
1186
|
+
if (projectRoot) {
|
|
1187
|
+
for (const name of PUBLIC_FILENAMES) {
|
|
1188
|
+
const p = path.join(projectRoot, name);
|
|
1189
|
+
if (await fs.pathExists(p)) {
|
|
1190
|
+
files.push({ path: p, privacy: 'public', scope: 'project' });
|
|
1191
|
+
break;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
for (const name of LOCAL_FILENAMES) {
|
|
1195
|
+
const p = path.join(projectRoot, name);
|
|
1196
|
+
if (await fs.pathExists(p)) {
|
|
1197
|
+
files.push({ path: p, privacy: 'local', scope: 'project' });
|
|
1198
|
+
break;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return files;
|
|
1203
|
+
};
|
|
1204
|
+
/**
|
|
1205
|
+
* Load a single config file (JSON/YAML). JS/TS is not supported in this step.
|
|
1206
|
+
* Validates with Zod RAW schema, then normalizes to RESOLVED.
|
|
1207
|
+
*
|
|
1208
|
+
* For JSON/YAML: if a "dynamic" property is present, throws with guidance.
|
|
1209
|
+
* For JS/TS: default export is loaded; "dynamic" is allowed.
|
|
1210
|
+
*/
|
|
1211
|
+
const loadConfigFile = async (filePath) => {
|
|
1212
|
+
let raw = {};
|
|
1213
|
+
try {
|
|
1214
|
+
const abs = path.resolve(filePath);
|
|
1215
|
+
if (isJsOrTs(abs)) {
|
|
1216
|
+
// JS/TS support: load default export via robust pipeline.
|
|
1217
|
+
const mod = await loadJsTsDefault(abs);
|
|
1218
|
+
raw = mod ?? {};
|
|
1219
|
+
}
|
|
1220
|
+
else {
|
|
1221
|
+
const txt = await fs.readFile(abs, 'utf-8');
|
|
1222
|
+
raw = isJson(abs) ? JSON.parse(txt) : isYaml(abs) ? YAML.parse(txt) : {};
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
catch (err) {
|
|
1226
|
+
throw new Error(`Failed to read/parse config: ${filePath}. ${String(err)}`);
|
|
1227
|
+
}
|
|
1228
|
+
// Validate RAW
|
|
1229
|
+
const parsed = getDotenvConfigSchemaRaw.safeParse(raw);
|
|
1230
|
+
if (!parsed.success) {
|
|
1231
|
+
const msgs = parsed.error.issues
|
|
1232
|
+
.map((i) => `${i.path.join('.')}: ${i.message}`)
|
|
1233
|
+
.join('\n');
|
|
1234
|
+
throw new Error(`Invalid config ${filePath}:\n${msgs}`);
|
|
1235
|
+
}
|
|
1236
|
+
// Disallow dynamic and schema in JSON/YAML; allow both in JS/TS.
|
|
1237
|
+
if (!isJsOrTs(filePath) &&
|
|
1238
|
+
(parsed.data.dynamic !== undefined || parsed.data.schema !== undefined)) {
|
|
1239
|
+
throw new Error(`Config ${filePath} specifies unsupported keys for JSON/YAML. ` +
|
|
1240
|
+
`Use JS/TS config for "dynamic" or "schema".`);
|
|
1241
|
+
}
|
|
1242
|
+
return getDotenvConfigSchemaResolved.parse(parsed.data);
|
|
1243
|
+
};
|
|
1244
|
+
/**
|
|
1245
|
+
* Discover and load configs into resolved shapes, ordered by scope/privacy.
|
|
1246
|
+
* JSON/YAML/JS/TS supported; first match per scope/privacy applies.
|
|
1247
|
+
*/
|
|
1248
|
+
const resolveGetDotenvConfigSources = async (importMetaUrl) => {
|
|
1249
|
+
const discovered = await discoverConfigFiles(importMetaUrl);
|
|
1250
|
+
const result = {};
|
|
1251
|
+
for (const f of discovered) {
|
|
1252
|
+
const cfg = await loadConfigFile(f.path);
|
|
1253
|
+
if (f.scope === 'packaged') {
|
|
1254
|
+
// packaged public only
|
|
1255
|
+
result.packaged = cfg;
|
|
1256
|
+
}
|
|
1257
|
+
else {
|
|
1258
|
+
result.project ??= {};
|
|
1259
|
+
if (f.privacy === 'public')
|
|
1260
|
+
result.project.public = cfg;
|
|
1261
|
+
else
|
|
1262
|
+
result.project.local = cfg;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return result;
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1268
|
+
/** src/diagnostics/entropy.ts
|
|
1269
|
+
* Entropy diagnostics (presentation-only).
|
|
1270
|
+
* - Gated by min length and printable ASCII.
|
|
1271
|
+
* - Warn once per key per run when bits/char \>= threshold.
|
|
1272
|
+
* - Supports whitelist patterns to suppress known-noise keys.
|
|
1273
|
+
*/
|
|
1274
|
+
const warned = new Set();
|
|
1275
|
+
const isPrintableAscii = (s) => /^[\x20-\x7E]+$/.test(s);
|
|
1276
|
+
const compile$1 = (patterns) => (patterns ?? []).map((p) => new RegExp(p, 'i'));
|
|
1277
|
+
const whitelisted = (key, regs) => regs.some((re) => re.test(key));
|
|
1278
|
+
const shannonBitsPerChar = (s) => {
|
|
1279
|
+
const freq = new Map();
|
|
1280
|
+
for (const ch of s)
|
|
1281
|
+
freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
1282
|
+
const n = s.length;
|
|
1283
|
+
let h = 0;
|
|
1284
|
+
for (const c of freq.values()) {
|
|
1285
|
+
const p = c / n;
|
|
1286
|
+
h -= p * Math.log2(p);
|
|
1287
|
+
}
|
|
1288
|
+
return h;
|
|
1289
|
+
};
|
|
1290
|
+
/**
|
|
1291
|
+
* Maybe emit a one-line entropy warning for a key.
|
|
1292
|
+
* Caller supplies an `emit(line)` function; the helper ensures once-per-key.
|
|
1293
|
+
*/
|
|
1294
|
+
const maybeWarnEntropy = (key, value, origin, opts, emit) => {
|
|
1295
|
+
if (!opts || opts.warnEntropy === false)
|
|
1296
|
+
return;
|
|
1297
|
+
if (warned.has(key))
|
|
1298
|
+
return;
|
|
1299
|
+
const v = value ?? '';
|
|
1300
|
+
const minLen = Math.max(0, opts.entropyMinLength ?? 16);
|
|
1301
|
+
const threshold = opts.entropyThreshold ?? 3.8;
|
|
1302
|
+
if (v.length < minLen)
|
|
1303
|
+
return;
|
|
1304
|
+
if (!isPrintableAscii(v))
|
|
1305
|
+
return;
|
|
1306
|
+
const wl = compile$1(opts.entropyWhitelist);
|
|
1307
|
+
if (whitelisted(key, wl))
|
|
1308
|
+
return;
|
|
1309
|
+
const bpc = shannonBitsPerChar(v);
|
|
1310
|
+
if (bpc >= threshold) {
|
|
1311
|
+
warned.add(key);
|
|
1312
|
+
emit(`[entropy] key=${key} score=${bpc.toFixed(2)} len=${String(v.length)} origin=${origin}`);
|
|
1313
|
+
}
|
|
1314
|
+
};
|
|
1315
|
+
|
|
1316
|
+
const DEFAULT_PATTERNS = [
|
|
1317
|
+
'\\bsecret\\b',
|
|
1318
|
+
'\\btoken\\b',
|
|
1319
|
+
'\\bpass(word)?\\b',
|
|
1320
|
+
'\\bapi[_-]?key\\b',
|
|
1321
|
+
'\\bkey\\b',
|
|
1322
|
+
];
|
|
1323
|
+
const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => new RegExp(p, 'i'));
|
|
1324
|
+
const shouldRedactKey = (key, regs) => regs.some((re) => re.test(key));
|
|
1325
|
+
const MASK = '[redacted]';
|
|
1326
|
+
/**
|
|
1327
|
+
* Produce a shallow redacted copy of an env-like object for display.
|
|
1328
|
+
*/
|
|
1329
|
+
const redactObject = (obj, opts) => {
|
|
1330
|
+
if (!opts?.redact)
|
|
1331
|
+
return { ...obj };
|
|
1332
|
+
const regs = compile(opts.redactPatterns);
|
|
1333
|
+
const out = {};
|
|
1334
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1335
|
+
out[k] = v && shouldRedactKey(k, regs) ? MASK : v;
|
|
1336
|
+
}
|
|
1337
|
+
return out;
|
|
1338
|
+
};
|
|
1339
|
+
|
|
948
1340
|
const applyKv = (current, kv) => {
|
|
949
1341
|
if (!kv || Object.keys(kv).length === 0)
|
|
950
1342
|
return current;
|
|
@@ -1000,7 +1392,7 @@ const readDotenv = async (path) => {
|
|
|
1000
1392
|
}
|
|
1001
1393
|
};
|
|
1002
1394
|
|
|
1003
|
-
const importDefault
|
|
1395
|
+
const importDefault = async (fileUrl) => {
|
|
1004
1396
|
const mod = (await import(fileUrl));
|
|
1005
1397
|
return mod.default;
|
|
1006
1398
|
};
|
|
@@ -1051,11 +1443,11 @@ const loadModuleDefault = async (absPath, cacheDirName) => {
|
|
|
1051
1443
|
const ext = path.extname(absPath).toLowerCase();
|
|
1052
1444
|
const fileUrl = url.pathToFileURL(absPath).toString();
|
|
1053
1445
|
if (!['.ts', '.mts', '.cts', '.tsx'].includes(ext)) {
|
|
1054
|
-
return importDefault
|
|
1446
|
+
return importDefault(fileUrl);
|
|
1055
1447
|
}
|
|
1056
1448
|
// Try direct import first (TS loader active)
|
|
1057
1449
|
try {
|
|
1058
|
-
const dyn = await importDefault
|
|
1450
|
+
const dyn = await importDefault(fileUrl);
|
|
1059
1451
|
if (dyn)
|
|
1060
1452
|
return dyn;
|
|
1061
1453
|
}
|
|
@@ -1080,7 +1472,7 @@ const loadModuleDefault = async (absPath, cacheDirName) => {
|
|
|
1080
1472
|
sourcemap: false,
|
|
1081
1473
|
logLevel: 'silent',
|
|
1082
1474
|
});
|
|
1083
|
-
const result = await importDefault
|
|
1475
|
+
const result = await importDefault(url.pathToFileURL(cacheFile).toString());
|
|
1084
1476
|
// Best-effort: trim older cache files for this source.
|
|
1085
1477
|
await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
|
|
1086
1478
|
return result;
|
|
@@ -1100,7 +1492,7 @@ const loadModuleDefault = async (absPath, cacheDirName) => {
|
|
|
1100
1492
|
},
|
|
1101
1493
|
}).outputText;
|
|
1102
1494
|
await fs.writeFile(cacheFile, out, 'utf-8');
|
|
1103
|
-
const result = await importDefault
|
|
1495
|
+
const result = await importDefault(url.pathToFileURL(cacheFile).toString());
|
|
1104
1496
|
// Best-effort: trim older cache files for this source.
|
|
1105
1497
|
await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
|
|
1106
1498
|
return result;
|
|
@@ -1234,8 +1626,41 @@ const getDotenv = async (options = {}) => {
|
|
|
1234
1626
|
resultDotenv = dotenvForOutput;
|
|
1235
1627
|
}
|
|
1236
1628
|
// Log result.
|
|
1237
|
-
if (log)
|
|
1238
|
-
|
|
1629
|
+
if (log) {
|
|
1630
|
+
const redactFlag = options.redact ?? false;
|
|
1631
|
+
const redactPatterns = options.redactPatterns ?? undefined;
|
|
1632
|
+
const redOpts = {};
|
|
1633
|
+
if (redactFlag)
|
|
1634
|
+
redOpts.redact = true;
|
|
1635
|
+
if (redactFlag && Array.isArray(redactPatterns))
|
|
1636
|
+
redOpts.redactPatterns = redactPatterns;
|
|
1637
|
+
const bag = redactFlag
|
|
1638
|
+
? redactObject(resultDotenv, redOpts)
|
|
1639
|
+
: { ...resultDotenv };
|
|
1640
|
+
logger.log(bag);
|
|
1641
|
+
// Entropy warnings: once-per-key-per-run (presentation only)
|
|
1642
|
+
const warnEntropyVal = options.warnEntropy ?? true;
|
|
1643
|
+
const entropyThresholdVal = options
|
|
1644
|
+
.entropyThreshold;
|
|
1645
|
+
const entropyMinLengthVal = options
|
|
1646
|
+
.entropyMinLength;
|
|
1647
|
+
const entropyWhitelistVal = options
|
|
1648
|
+
.entropyWhitelist;
|
|
1649
|
+
const entOpts = {};
|
|
1650
|
+
if (typeof warnEntropyVal === 'boolean')
|
|
1651
|
+
entOpts.warnEntropy = warnEntropyVal;
|
|
1652
|
+
if (typeof entropyThresholdVal === 'number')
|
|
1653
|
+
entOpts.entropyThreshold = entropyThresholdVal;
|
|
1654
|
+
if (typeof entropyMinLengthVal === 'number')
|
|
1655
|
+
entOpts.entropyMinLength = entropyMinLengthVal;
|
|
1656
|
+
if (Array.isArray(entropyWhitelistVal))
|
|
1657
|
+
entOpts.entropyWhitelist = entropyWhitelistVal;
|
|
1658
|
+
for (const [k, v] of Object.entries(resultDotenv)) {
|
|
1659
|
+
maybeWarnEntropy(k, v, v !== undefined ? 'dotenv' : 'unset', entOpts, (line) => {
|
|
1660
|
+
logger.log(line);
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1239
1664
|
// Load process.env.
|
|
1240
1665
|
if (loadProcess)
|
|
1241
1666
|
Object.assign(process.env, resultDotenv);
|
|
@@ -1243,235 +1668,59 @@ const getDotenv = async (options = {}) => {
|
|
|
1243
1668
|
};
|
|
1244
1669
|
|
|
1245
1670
|
/**
|
|
1246
|
-
*
|
|
1247
|
-
*
|
|
1248
|
-
* -
|
|
1249
|
-
* -
|
|
1250
|
-
*
|
|
1671
|
+
* Deep interpolation utility for string leaves.
|
|
1672
|
+
* - Expands string values using dotenv-style expansion against the provided envRef.
|
|
1673
|
+
* - Preserves non-strings as-is.
|
|
1674
|
+
* - Does not recurse into arrays (arrays are returned unchanged).
|
|
1675
|
+
*
|
|
1676
|
+
* Intended for:
|
|
1677
|
+
* - Phase C option/config interpolation after composing ctx.dotenv.
|
|
1678
|
+
* - Per-plugin config slice interpolation before afterResolve.
|
|
1251
1679
|
*/
|
|
1252
|
-
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
const getDotenvConfigSchemaRaw = zod.z.object({
|
|
1258
|
-
dotenvToken: zod.z.string().optional(),
|
|
1259
|
-
privateToken: zod.z.string().optional(),
|
|
1260
|
-
paths: rawPathsSchema,
|
|
1261
|
-
loadProcess: zod.z.boolean().optional(),
|
|
1262
|
-
log: zod.z.boolean().optional(),
|
|
1263
|
-
shell: zod.z.union([zod.z.string(), zod.z.boolean()]).optional(),
|
|
1264
|
-
scripts: zod.z.record(zod.z.string(), zod.z.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
|
|
1265
|
-
vars: stringMap.optional(), // public, global
|
|
1266
|
-
envVars: envStringMap.optional(), // public, per-env
|
|
1267
|
-
// Dynamic in config (JS/TS only). JSON/YAML loader will reject if set.
|
|
1268
|
-
dynamic: zod.z.unknown().optional(),
|
|
1269
|
-
// Per-plugin config bag; validated by plugins/host when used.
|
|
1270
|
-
plugins: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
|
|
1271
|
-
});
|
|
1272
|
-
// Normalize paths to string[]
|
|
1273
|
-
const normalizePaths = (p) => p === undefined ? undefined : Array.isArray(p) ? p : [p];
|
|
1274
|
-
const getDotenvConfigSchemaResolved = getDotenvConfigSchemaRaw.transform((raw) => ({
|
|
1275
|
-
...raw,
|
|
1276
|
-
paths: normalizePaths(raw.paths),
|
|
1277
|
-
}));
|
|
1278
|
-
|
|
1279
|
-
// Discovery candidates (first match wins per scope/privacy).
|
|
1280
|
-
// Order preserves historical JSON/YAML precedence; JS/TS added afterwards.
|
|
1281
|
-
const PUBLIC_FILENAMES = [
|
|
1282
|
-
'getdotenv.config.json',
|
|
1283
|
-
'getdotenv.config.yaml',
|
|
1284
|
-
'getdotenv.config.yml',
|
|
1285
|
-
'getdotenv.config.js',
|
|
1286
|
-
'getdotenv.config.mjs',
|
|
1287
|
-
'getdotenv.config.cjs',
|
|
1288
|
-
'getdotenv.config.ts',
|
|
1289
|
-
'getdotenv.config.mts',
|
|
1290
|
-
'getdotenv.config.cts',
|
|
1291
|
-
];
|
|
1292
|
-
const LOCAL_FILENAMES = [
|
|
1293
|
-
'getdotenv.config.local.json',
|
|
1294
|
-
'getdotenv.config.local.yaml',
|
|
1295
|
-
'getdotenv.config.local.yml',
|
|
1296
|
-
'getdotenv.config.local.js',
|
|
1297
|
-
'getdotenv.config.local.mjs',
|
|
1298
|
-
'getdotenv.config.local.cjs',
|
|
1299
|
-
'getdotenv.config.local.ts',
|
|
1300
|
-
'getdotenv.config.local.mts',
|
|
1301
|
-
'getdotenv.config.local.cts',
|
|
1302
|
-
];
|
|
1303
|
-
const isYaml = (p) => ['.yaml', '.yml'].includes(path.extname(p).toLowerCase());
|
|
1304
|
-
const isJson = (p) => path.extname(p).toLowerCase() === '.json';
|
|
1305
|
-
const isJsOrTs = (p) => ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'].includes(path.extname(p).toLowerCase());
|
|
1306
|
-
// --- Internal JS/TS module loader helpers (default export) ---
|
|
1307
|
-
const importDefault = async (fileUrl) => {
|
|
1308
|
-
const mod = (await import(fileUrl));
|
|
1309
|
-
return mod.default;
|
|
1310
|
-
};
|
|
1311
|
-
const cacheName = (absPath, suffix) => {
|
|
1312
|
-
// sanitized filename with suffix; recompile on mtime changes not tracked here (simplified)
|
|
1313
|
-
const base = path.basename(absPath).replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
1314
|
-
return `${base}.${suffix}.mjs`;
|
|
1315
|
-
};
|
|
1316
|
-
const ensureDir = async (dir) => {
|
|
1317
|
-
await fs.ensureDir(dir);
|
|
1318
|
-
return dir;
|
|
1319
|
-
};
|
|
1320
|
-
const loadJsTsDefault = async (absPath) => {
|
|
1321
|
-
const fileUrl = url.pathToFileURL(absPath).toString();
|
|
1322
|
-
const ext = path.extname(absPath).toLowerCase();
|
|
1323
|
-
if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
|
|
1324
|
-
return importDefault(fileUrl);
|
|
1325
|
-
}
|
|
1326
|
-
// Try direct import first in case a TS loader is active.
|
|
1327
|
-
try {
|
|
1328
|
-
const val = await importDefault(fileUrl);
|
|
1329
|
-
if (val)
|
|
1330
|
-
return val;
|
|
1331
|
-
}
|
|
1332
|
-
catch {
|
|
1333
|
-
/* fallthrough */
|
|
1334
|
-
}
|
|
1335
|
-
// esbuild bundle to a temp ESM file
|
|
1336
|
-
try {
|
|
1337
|
-
const esbuild = (await import('esbuild'));
|
|
1338
|
-
const outDir = await ensureDir(path.resolve('.tsbuild', 'getdotenv-config'));
|
|
1339
|
-
const outfile = path.join(outDir, cacheName(absPath, 'bundle'));
|
|
1340
|
-
await esbuild.build({
|
|
1341
|
-
entryPoints: [absPath],
|
|
1342
|
-
bundle: true,
|
|
1343
|
-
platform: 'node',
|
|
1344
|
-
format: 'esm',
|
|
1345
|
-
target: 'node20',
|
|
1346
|
-
outfile,
|
|
1347
|
-
sourcemap: false,
|
|
1348
|
-
logLevel: 'silent',
|
|
1349
|
-
});
|
|
1350
|
-
return await importDefault(url.pathToFileURL(outfile).toString());
|
|
1351
|
-
}
|
|
1352
|
-
catch {
|
|
1353
|
-
/* fallthrough to TS transpile */
|
|
1354
|
-
}
|
|
1355
|
-
// typescript.transpileModule simple transpile (single-file)
|
|
1356
|
-
try {
|
|
1357
|
-
const ts = (await import('typescript'));
|
|
1358
|
-
const src = await fs.readFile(absPath, 'utf-8');
|
|
1359
|
-
const out = ts.transpileModule(src, {
|
|
1360
|
-
compilerOptions: {
|
|
1361
|
-
module: 'ESNext',
|
|
1362
|
-
target: 'ES2022',
|
|
1363
|
-
moduleResolution: 'NodeNext',
|
|
1364
|
-
},
|
|
1365
|
-
}).outputText;
|
|
1366
|
-
const outDir = await ensureDir(path.resolve('.tsbuild', 'getdotenv-config'));
|
|
1367
|
-
const outfile = path.join(outDir, cacheName(absPath, 'ts'));
|
|
1368
|
-
await fs.writeFile(outfile, out, 'utf-8');
|
|
1369
|
-
return await importDefault(url.pathToFileURL(outfile).toString());
|
|
1370
|
-
}
|
|
1371
|
-
catch {
|
|
1372
|
-
throw new Error(`Unable to load JS/TS config: ${absPath}. Install 'esbuild' for robust bundling or ensure a TS loader.`);
|
|
1373
|
-
}
|
|
1374
|
-
};
|
|
1375
|
-
/**
|
|
1376
|
-
* Discover JSON/YAML config files in the packaged root and project root.
|
|
1377
|
-
* Order: packaged public → project public → project local. */
|
|
1378
|
-
const discoverConfigFiles = async (importMetaUrl) => {
|
|
1379
|
-
const files = [];
|
|
1380
|
-
// Packaged root via importMetaUrl (optional)
|
|
1381
|
-
if (importMetaUrl) {
|
|
1382
|
-
const fromUrl = url.fileURLToPath(importMetaUrl);
|
|
1383
|
-
const packagedRoot = await packageDirectory.packageDirectory({ cwd: fromUrl });
|
|
1384
|
-
if (packagedRoot) {
|
|
1385
|
-
for (const name of PUBLIC_FILENAMES) {
|
|
1386
|
-
const p = path.join(packagedRoot, name);
|
|
1387
|
-
if (await fs.pathExists(p)) {
|
|
1388
|
-
files.push({ path: p, privacy: 'public', scope: 'packaged' });
|
|
1389
|
-
break; // only one public file expected per scope
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
// By policy, packaged .local is not expected; skip even if present.
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
// Project root (from current working directory)
|
|
1396
|
-
const projectRoot = await packageDirectory.packageDirectory();
|
|
1397
|
-
if (projectRoot) {
|
|
1398
|
-
for (const name of PUBLIC_FILENAMES) {
|
|
1399
|
-
const p = path.join(projectRoot, name);
|
|
1400
|
-
if (await fs.pathExists(p)) {
|
|
1401
|
-
files.push({ path: p, privacy: 'public', scope: 'project' });
|
|
1402
|
-
break;
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
for (const name of LOCAL_FILENAMES) {
|
|
1406
|
-
const p = path.join(projectRoot, name);
|
|
1407
|
-
if (await fs.pathExists(p)) {
|
|
1408
|
-
files.push({ path: p, privacy: 'local', scope: 'project' });
|
|
1409
|
-
break;
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
return files;
|
|
1414
|
-
};
|
|
1680
|
+
/** @internal */
|
|
1681
|
+
const isPlainObject = (v) => v !== null &&
|
|
1682
|
+
typeof v === 'object' &&
|
|
1683
|
+
!Array.isArray(v) &&
|
|
1684
|
+
Object.getPrototypeOf(v) === Object.prototype;
|
|
1415
1685
|
/**
|
|
1416
|
-
*
|
|
1417
|
-
*
|
|
1686
|
+
* Deeply interpolate string leaves against envRef.
|
|
1687
|
+
* Arrays are not recursed into; they are returned unchanged.
|
|
1418
1688
|
*
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
1689
|
+
* @typeParam T - Shape of the input value.
|
|
1690
|
+
* @param value - Input value (object/array/primitive).
|
|
1691
|
+
* @param envRef - Reference environment for interpolation.
|
|
1692
|
+
* @returns A new value with string leaves interpolated.
|
|
1421
1693
|
*/
|
|
1422
|
-
const
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
const
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
const mod = await loadJsTsDefault(abs);
|
|
1429
|
-
raw = mod ?? {};
|
|
1430
|
-
}
|
|
1431
|
-
else {
|
|
1432
|
-
const txt = await fs.readFile(abs, 'utf-8');
|
|
1433
|
-
raw = isJson(abs) ? JSON.parse(txt) : isYaml(abs) ? YAML.parse(txt) : {};
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
catch (err) {
|
|
1437
|
-
throw new Error(`Failed to read/parse config: ${filePath}. ${String(err)}`);
|
|
1438
|
-
}
|
|
1439
|
-
// Validate RAW
|
|
1440
|
-
const parsed = getDotenvConfigSchemaRaw.safeParse(raw);
|
|
1441
|
-
if (!parsed.success) {
|
|
1442
|
-
const msgs = parsed.error.issues
|
|
1443
|
-
.map((i) => `${i.path.join('.')}: ${i.message}`)
|
|
1444
|
-
.join('\n');
|
|
1445
|
-
throw new Error(`Invalid config ${filePath}:\n${msgs}`);
|
|
1694
|
+
const interpolateDeep = (value, envRef) => {
|
|
1695
|
+
// Strings: expand and return
|
|
1696
|
+
if (typeof value === 'string') {
|
|
1697
|
+
const out = dotenvExpand(value, envRef);
|
|
1698
|
+
// dotenvExpand returns string | undefined; preserve original on undefined
|
|
1699
|
+
return (out ?? value);
|
|
1446
1700
|
}
|
|
1447
|
-
//
|
|
1448
|
-
if (
|
|
1449
|
-
|
|
1701
|
+
// Arrays: return as-is (no recursion)
|
|
1702
|
+
if (Array.isArray(value)) {
|
|
1703
|
+
return value;
|
|
1450
1704
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
// packaged public only
|
|
1464
|
-
result.packaged = cfg;
|
|
1465
|
-
}
|
|
1466
|
-
else {
|
|
1467
|
-
result.project ??= {};
|
|
1468
|
-
if (f.privacy === 'public')
|
|
1469
|
-
result.project.public = cfg;
|
|
1705
|
+
// Plain objects: shallow clone and recurse into values
|
|
1706
|
+
if (isPlainObject(value)) {
|
|
1707
|
+
const src = value;
|
|
1708
|
+
const out = {};
|
|
1709
|
+
for (const [k, v] of Object.entries(src)) {
|
|
1710
|
+
// Recurse for strings/objects; keep arrays as-is; preserve other scalars
|
|
1711
|
+
if (typeof v === 'string')
|
|
1712
|
+
out[k] = dotenvExpand(v, envRef) ?? v;
|
|
1713
|
+
else if (Array.isArray(v))
|
|
1714
|
+
out[k] = v;
|
|
1715
|
+
else if (isPlainObject(v))
|
|
1716
|
+
out[k] = interpolateDeep(v, envRef);
|
|
1470
1717
|
else
|
|
1471
|
-
|
|
1718
|
+
out[k] = v;
|
|
1472
1719
|
}
|
|
1720
|
+
return out;
|
|
1473
1721
|
}
|
|
1474
|
-
return
|
|
1722
|
+
// Other primitives/types: return as-is
|
|
1723
|
+
return value;
|
|
1475
1724
|
};
|
|
1476
1725
|
|
|
1477
1726
|
/**
|
|
@@ -1483,7 +1732,9 @@ const resolveGetDotenvConfigSources = async (importMetaUrl) => {
|
|
|
1483
1732
|
* 2) Discover packaged + project config sources and overlay onto base.
|
|
1484
1733
|
* 3) Apply dynamics in order:
|
|
1485
1734
|
* programmatic dynamic \> config dynamic (packaged → project public → project local)
|
|
1486
|
-
* \> file dynamicPath.
|
|
1735
|
+
* \> file dynamicPath.
|
|
1736
|
+
* 4) Phase C interpolation of remaining string options (e.g., outputPath).
|
|
1737
|
+
* 5) Optionally write outputPath, log, and merge into process.env.
|
|
1487
1738
|
*/
|
|
1488
1739
|
const resolveDotenvWithConfigLoader = async (validated) => {
|
|
1489
1740
|
// 1) Base from files, no dynamic, no programmatic vars
|
|
@@ -1531,21 +1782,112 @@ const resolveDotenvWithConfigLoader = async (validated) => {
|
|
|
1531
1782
|
throw new Error(`Unable to load dynamic from ${validated.dynamicPath}`);
|
|
1532
1783
|
}
|
|
1533
1784
|
}
|
|
1534
|
-
// 4)
|
|
1535
|
-
|
|
1536
|
-
|
|
1785
|
+
// 4) Phase C: interpolate remaining string options (exclude bootstrap set).
|
|
1786
|
+
// For now, interpolate outputPath only; bootstrap keys are excluded by design.
|
|
1787
|
+
const envRef = { ...process.env, ...dotenv };
|
|
1788
|
+
const outputPathInterpolated = typeof validated.outputPath === 'string'
|
|
1789
|
+
? interpolateDeep(validated.outputPath, envRef)
|
|
1790
|
+
: undefined;
|
|
1791
|
+
// 5) Output/log/process merge (use interpolated outputPath if present)
|
|
1792
|
+
if (outputPathInterpolated) {
|
|
1793
|
+
await fs.writeFile(outputPathInterpolated, Object.keys(dotenv).reduce((contents, key) => {
|
|
1537
1794
|
const value = dotenv[key] ?? '';
|
|
1538
1795
|
return `${contents}${key}=${value.includes('\n') ? `"${value}"` : value}\n`;
|
|
1539
1796
|
}, ''), { encoding: 'utf-8' });
|
|
1540
1797
|
}
|
|
1541
1798
|
const logger = validated.logger ?? console;
|
|
1542
|
-
if (validated.log)
|
|
1543
|
-
|
|
1799
|
+
if (validated.log) {
|
|
1800
|
+
const redactFlag = validated.redact ?? false;
|
|
1801
|
+
const redactPatterns = validated.redactPatterns;
|
|
1802
|
+
const redOpts = {};
|
|
1803
|
+
if (redactFlag)
|
|
1804
|
+
redOpts.redact = true;
|
|
1805
|
+
if (redactFlag && Array.isArray(redactPatterns))
|
|
1806
|
+
redOpts.redactPatterns = redactPatterns;
|
|
1807
|
+
const bag = redactFlag ? redactObject(dotenv, redOpts) : { ...dotenv };
|
|
1808
|
+
logger.log(bag);
|
|
1809
|
+
// Entropy warnings: once per key per run (presentation only)
|
|
1810
|
+
const warnEntropyVal = validated.warnEntropy ?? true;
|
|
1811
|
+
const entropyThresholdVal = validated.entropyThreshold;
|
|
1812
|
+
const entropyMinLengthVal = validated.entropyMinLength;
|
|
1813
|
+
const entropyWhitelistVal = validated.entropyWhitelist;
|
|
1814
|
+
const entOpts = {};
|
|
1815
|
+
// include keys only when defined to satisfy exactOptionalPropertyTypes
|
|
1816
|
+
if (typeof warnEntropyVal === 'boolean')
|
|
1817
|
+
entOpts.warnEntropy = warnEntropyVal;
|
|
1818
|
+
if (typeof entropyThresholdVal === 'number')
|
|
1819
|
+
entOpts.entropyThreshold = entropyThresholdVal;
|
|
1820
|
+
if (typeof entropyMinLengthVal === 'number')
|
|
1821
|
+
entOpts.entropyMinLength = entropyMinLengthVal;
|
|
1822
|
+
if (Array.isArray(entropyWhitelistVal))
|
|
1823
|
+
entOpts.entropyWhitelist = entropyWhitelistVal;
|
|
1824
|
+
for (const [k, v] of Object.entries(dotenv)) {
|
|
1825
|
+
maybeWarnEntropy(k, v, v !== undefined ? 'dotenv' : 'unset', entOpts, (line) => {
|
|
1826
|
+
logger.log(line);
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1544
1830
|
if (validated.loadProcess)
|
|
1545
1831
|
Object.assign(process.env, dotenv);
|
|
1546
1832
|
return dotenv;
|
|
1547
1833
|
};
|
|
1548
1834
|
|
|
1835
|
+
/**
|
|
1836
|
+
* Validate a composed env against config-provided validation surfaces.
|
|
1837
|
+
* Precedence for validation definitions:
|
|
1838
|
+
* project.local -\> project.public -\> packaged
|
|
1839
|
+
*
|
|
1840
|
+
* Behavior:
|
|
1841
|
+
* - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
|
|
1842
|
+
* - Else if `requiredKeys` is present, check presence (value !== undefined).
|
|
1843
|
+
* - Returns a flat list of issue strings; caller decides warn vs fail.
|
|
1844
|
+
*/
|
|
1845
|
+
const validateEnvAgainstSources = (finalEnv, sources) => {
|
|
1846
|
+
const pick = (getter) => {
|
|
1847
|
+
const pl = sources.project?.local;
|
|
1848
|
+
const pp = sources.project?.public;
|
|
1849
|
+
const pk = sources.packaged;
|
|
1850
|
+
return ((pl && getter(pl)) ||
|
|
1851
|
+
(pp && getter(pp)) ||
|
|
1852
|
+
(pk && getter(pk)) ||
|
|
1853
|
+
undefined);
|
|
1854
|
+
};
|
|
1855
|
+
const schema = pick((cfg) => cfg['schema']);
|
|
1856
|
+
if (schema &&
|
|
1857
|
+
typeof schema.safeParse === 'function') {
|
|
1858
|
+
try {
|
|
1859
|
+
const parsed = schema.safeParse(finalEnv);
|
|
1860
|
+
if (!parsed.success) {
|
|
1861
|
+
// Try to render zod-style issues when available.
|
|
1862
|
+
const err = parsed.error;
|
|
1863
|
+
const issues = Array.isArray(err.issues) && err.issues.length > 0
|
|
1864
|
+
? err.issues.map((i) => {
|
|
1865
|
+
const path = Array.isArray(i.path) ? i.path.join('.') : '';
|
|
1866
|
+
const msg = i.message ?? 'Invalid value';
|
|
1867
|
+
return path ? `[schema] ${path}: ${msg}` : `[schema] ${msg}`;
|
|
1868
|
+
})
|
|
1869
|
+
: ['[schema] validation failed'];
|
|
1870
|
+
return issues;
|
|
1871
|
+
}
|
|
1872
|
+
return [];
|
|
1873
|
+
}
|
|
1874
|
+
catch {
|
|
1875
|
+
// If schema invocation fails, surface a single diagnostic.
|
|
1876
|
+
return [
|
|
1877
|
+
'[schema] validation failed (unable to execute schema.safeParse)',
|
|
1878
|
+
];
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
const requiredKeys = pick((cfg) => cfg['requiredKeys']);
|
|
1882
|
+
if (Array.isArray(requiredKeys) && requiredKeys.length > 0) {
|
|
1883
|
+
const missing = requiredKeys.filter((k) => finalEnv[k] === undefined);
|
|
1884
|
+
if (missing.length > 0) {
|
|
1885
|
+
return missing.map((k) => `[requiredKeys] missing: ${k}`);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
return [];
|
|
1889
|
+
};
|
|
1890
|
+
|
|
1549
1891
|
/**
|
|
1550
1892
|
* Omit a "logger" key from an options object in a typed manner.
|
|
1551
1893
|
*/
|
|
@@ -1584,6 +1926,22 @@ const makePreSubcommandHook = ({ logger, preHook, postHook, defaults, }) => {
|
|
|
1584
1926
|
// Execute getdotenv via always-on config loader/overlay path.
|
|
1585
1927
|
const serviceOptions = getDotenvCliOptions2Options(mergedGetDotenvCliOptions);
|
|
1586
1928
|
const dotenv = await resolveDotenvWithConfigLoader(serviceOptions);
|
|
1929
|
+
// Global validation against config (warn by default; --strict fails).
|
|
1930
|
+
try {
|
|
1931
|
+
const sources = await resolveGetDotenvConfigSources((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
1932
|
+
const issues = validateEnvAgainstSources(dotenv, sources);
|
|
1933
|
+
if (Array.isArray(issues) && issues.length > 0) {
|
|
1934
|
+
issues.forEach((m) => {
|
|
1935
|
+
logger.error(m);
|
|
1936
|
+
});
|
|
1937
|
+
if (mergedGetDotenvCliOptions.strict) {
|
|
1938
|
+
process.exit(1);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
catch {
|
|
1943
|
+
// Tolerate validator failures in non-strict mode
|
|
1944
|
+
}
|
|
1587
1945
|
// Execute post-hook.
|
|
1588
1946
|
if (postHook)
|
|
1589
1947
|
await postHook(dotenv); // Execute command.
|
|
@@ -1654,3 +2012,4 @@ exports.dotenvExpandFromProcessEnv = dotenvExpandFromProcessEnv;
|
|
|
1654
2012
|
exports.generateGetDotenvCli = generateGetDotenvCli;
|
|
1655
2013
|
exports.getDotenv = getDotenv;
|
|
1656
2014
|
exports.getDotenvCliOptions2Options = getDotenvCliOptions2Options;
|
|
2015
|
+
exports.interpolateDeep = interpolateDeep;
|