envpkt 0.7.3 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +494 -443
- package/dist/index.d.ts +16 -8
- package/dist/index.js +294 -283
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { Cond, Either, Left, List, Option, Right, Try } from "functype";
|
|
@@ -24,17 +24,17 @@ const parseDate = (dateStr) => {
|
|
|
24
24
|
};
|
|
25
25
|
const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration, requireService, today) => {
|
|
26
26
|
const issues = [];
|
|
27
|
-
const created = Option(meta
|
|
28
|
-
const expires = Option(meta
|
|
29
|
-
const rotationUrl = Option(meta
|
|
30
|
-
const purpose = Option(meta
|
|
31
|
-
const service = Option(meta
|
|
27
|
+
const created = Option(meta.created).flatMap(parseDate);
|
|
28
|
+
const expires = Option(meta.expires).flatMap(parseDate);
|
|
29
|
+
const rotationUrl = Option(meta.rotation_url);
|
|
30
|
+
const purpose = Option(meta.purpose);
|
|
31
|
+
const service = Option(meta.service);
|
|
32
32
|
const daysRemaining = expires.map((exp) => daysBetween(today, exp));
|
|
33
33
|
const daysSinceCreated = created.map((c) => daysBetween(c, today));
|
|
34
34
|
const isExpired = daysRemaining.fold(() => false, (d) => d < 0);
|
|
35
35
|
const isExpiringSoon = daysRemaining.fold(() => false, (d) => d >= 0 && d <= WARN_BEFORE_DAYS);
|
|
36
36
|
const isStale = daysSinceCreated.fold(() => false, (d) => d > staleWarningDays);
|
|
37
|
-
const hasSealed = !!meta
|
|
37
|
+
const hasSealed = !!meta.encrypted_value;
|
|
38
38
|
const isMissing = fnoxKeys.size > 0 && !fnoxKeys.has(key) && !hasSealed;
|
|
39
39
|
const isMissingMetadata = requireExpiration && expires.isNone() || requireService && service.isNone();
|
|
40
40
|
if (isExpired) issues.push("Secret has expired");
|
|
@@ -52,8 +52,8 @@ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration
|
|
|
52
52
|
days_remaining: daysRemaining,
|
|
53
53
|
rotation_url: rotationUrl,
|
|
54
54
|
purpose,
|
|
55
|
-
created: Option(meta
|
|
56
|
-
expires: Option(meta
|
|
55
|
+
created: Option(meta.created),
|
|
56
|
+
expires: Option(meta.expires),
|
|
57
57
|
issues: List(issues)
|
|
58
58
|
};
|
|
59
59
|
};
|
|
@@ -91,18 +91,17 @@ const computeAudit = (config, fnoxKeys, today) => {
|
|
|
91
91
|
};
|
|
92
92
|
const computeEnvAudit = (config, env = process.env) => {
|
|
93
93
|
const envEntries = config.env ?? {};
|
|
94
|
-
const entries = []
|
|
95
|
-
for (const [key, entry] of Object.entries(envEntries)) {
|
|
94
|
+
const entries = Object.entries(envEntries).map(([key, entry]) => {
|
|
96
95
|
const currentValue = env[key];
|
|
97
96
|
const status = Cond.of().when(currentValue === void 0, "missing").elseWhen(currentValue !== entry.value, "overridden").else("default");
|
|
98
|
-
|
|
97
|
+
return {
|
|
99
98
|
key,
|
|
100
99
|
defaultValue: entry.value,
|
|
101
100
|
currentValue,
|
|
102
101
|
status,
|
|
103
102
|
purpose: entry.purpose
|
|
104
|
-
}
|
|
105
|
-
}
|
|
103
|
+
};
|
|
104
|
+
});
|
|
106
105
|
return {
|
|
107
106
|
entries,
|
|
108
107
|
total: entries.length,
|
|
@@ -220,7 +219,7 @@ const normalizeDates = (obj) => {
|
|
|
220
219
|
/** Expand ~ and $ENV_VAR / ${ENV_VAR} in a path string (silent — unresolved vars become "") */
|
|
221
220
|
const expandPath = (p) => {
|
|
222
221
|
return Path.expandTilde(p).replace(/\$\{(\w+)\}|\$(\w+)/g, (_, braced, bare) => {
|
|
223
|
-
const name = braced
|
|
222
|
+
const name = Option(braced).fold(() => Option(bare).fold(() => "", (b) => b), (b) => b);
|
|
224
223
|
return Env.getOrDefault(name, "");
|
|
225
224
|
});
|
|
226
225
|
};
|
|
@@ -255,10 +254,9 @@ const ENV_FALLBACK_PATHS = [
|
|
|
255
254
|
];
|
|
256
255
|
/** Build discovery paths dynamically from Platform home and cloud storage detection */
|
|
257
256
|
const buildSearchPaths = () => {
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
return paths;
|
|
257
|
+
const homePaths = Platform.homeDirs().toArray().map((home) => join(home, ".envpkt", CONFIG_FILENAME$2));
|
|
258
|
+
const cloudPaths = Platform.cloudStorageDirs().toArray().map((cloud) => join(cloud.path, ".envpkt", CONFIG_FILENAME$2));
|
|
259
|
+
return [...homePaths, ...cloudPaths];
|
|
262
260
|
};
|
|
263
261
|
/** Discover config by checking CWD, then ENVPKT_SEARCH_PATH, then dynamic Platform paths */
|
|
264
262
|
const discoverConfig = (cwd) => {
|
|
@@ -267,28 +265,24 @@ const discoverConfig = (cwd) => {
|
|
|
267
265
|
path: cwdCandidate,
|
|
268
266
|
source: "cwd"
|
|
269
267
|
});
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
268
|
+
const customMatch = Env.get("ENVPKT_SEARCH_PATH").fold(() => [], (v) => v.split(":").filter(Boolean)).map((template) => ({
|
|
269
|
+
template,
|
|
270
|
+
expanded: expandPath(template)
|
|
271
|
+
})).filter(({ expanded }) => expanded !== "" && !expanded.startsWith("/.envpkt")).map(({ expanded }) => expandGlobPath(expanded)).find((matches) => matches.length > 0);
|
|
272
|
+
if (customMatch) return Option({
|
|
273
|
+
path: customMatch[0],
|
|
274
|
+
source: "search"
|
|
275
|
+
});
|
|
276
|
+
const platformMatch = buildSearchPaths().find((p) => Fs.existsSync(p));
|
|
277
|
+
if (platformMatch) return Option({
|
|
278
|
+
path: platformMatch,
|
|
279
|
+
source: "search"
|
|
280
|
+
});
|
|
281
|
+
const fallbackMatch = ENV_FALLBACK_PATHS.map((template) => expandPath(template)).filter((expanded) => expanded !== "" && !expanded.startsWith("/.envpkt")).find((expanded) => Fs.existsSync(expanded));
|
|
282
|
+
if (fallbackMatch) return Option({
|
|
283
|
+
path: fallbackMatch,
|
|
282
284
|
source: "search"
|
|
283
285
|
});
|
|
284
|
-
for (const template of ENV_FALLBACK_PATHS) {
|
|
285
|
-
const expanded = expandPath(template);
|
|
286
|
-
if (!expanded || expanded.startsWith("/.envpkt")) continue;
|
|
287
|
-
if (Fs.existsSync(expanded)) return Option({
|
|
288
|
-
path: expanded,
|
|
289
|
-
source: "search"
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
286
|
return Option(void 0);
|
|
293
287
|
};
|
|
294
288
|
/** Read a config file, returning Either<ConfigError, string> */
|
|
@@ -380,22 +374,21 @@ const loadCatalog = (catalogPath) => loadConfig(catalogPath).fold((err) => {
|
|
|
380
374
|
}, (config) => Right(config));
|
|
381
375
|
/** Resolve secrets by merging catalog meta with agent overrides (shallow merge) */
|
|
382
376
|
const resolveSecrets = (agentMeta, catalogMeta, agentSecrets, catalogPath) => {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const catalogEntry = catalogMeta[key];
|
|
386
|
-
if (!catalogEntry) return Left({
|
|
377
|
+
return agentSecrets.reduce((acc, key) => acc.flatMap((resolved) => {
|
|
378
|
+
if (!(key in catalogMeta)) return Left({
|
|
387
379
|
_tag: "SecretNotInCatalog",
|
|
388
380
|
key,
|
|
389
381
|
catalogPath
|
|
390
382
|
});
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
...
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
383
|
+
const catalogEntry = catalogMeta[key];
|
|
384
|
+
return Right({
|
|
385
|
+
...resolved,
|
|
386
|
+
[key]: key in agentMeta ? {
|
|
387
|
+
...catalogEntry,
|
|
388
|
+
...agentMeta[key]
|
|
389
|
+
} : catalogEntry
|
|
390
|
+
});
|
|
391
|
+
}), Right({}));
|
|
399
392
|
};
|
|
400
393
|
/** Resolve an agent config against its catalog (if any), producing a flat self-contained config */
|
|
401
394
|
const resolveConfig = (agentConfig, agentConfigDir) => {
|
|
@@ -413,13 +406,9 @@ const resolveConfig = (agentConfig, agentConfigDir) => {
|
|
|
413
406
|
const agentSecrets = agentConfig.identity.secrets;
|
|
414
407
|
const agentSecretEntries = agentConfig.secret ?? {};
|
|
415
408
|
return loadCatalog(catalogPath).flatMap((catalogConfig) => resolveSecrets(agentSecretEntries, catalogConfig.secret ?? {}, agentSecrets, catalogPath).map((resolvedMeta) => {
|
|
416
|
-
const merged = [];
|
|
417
|
-
const overridden =
|
|
409
|
+
const merged = [...agentSecrets];
|
|
410
|
+
const overridden = agentSecrets.filter((key) => key in agentSecretEntries);
|
|
418
411
|
const warnings = [];
|
|
419
|
-
for (const key of agentSecrets) {
|
|
420
|
-
merged.push(key);
|
|
421
|
-
if (agentSecretEntries[key]) overridden.push(key);
|
|
422
|
-
}
|
|
423
412
|
const { catalog: _catalog, ...agentWithoutCatalog } = agentConfig;
|
|
424
413
|
const identityData = agentConfig.identity ? (() => {
|
|
425
414
|
const { secrets: _secrets, ...rest } = agentConfig.identity;
|
|
@@ -449,6 +438,8 @@ const DIM = "\x1B[2m";
|
|
|
449
438
|
const RED = "\x1B[31m";
|
|
450
439
|
const GREEN = "\x1B[32m";
|
|
451
440
|
const YELLOW = "\x1B[33m";
|
|
441
|
+
const BLUE = "\x1B[34m";
|
|
442
|
+
const MAGENTA = "\x1B[35m";
|
|
452
443
|
const CYAN = "\x1B[36m";
|
|
453
444
|
const statusColor = (status) => {
|
|
454
445
|
switch (status) {
|
|
@@ -677,11 +668,11 @@ const formatEnvAuditTable = (config) => {
|
|
|
677
668
|
return;
|
|
678
669
|
}
|
|
679
670
|
console.log(`\n${BOLD}Environment Defaults${RESET} (${envAudit.total} entries)`);
|
|
680
|
-
|
|
671
|
+
envAudit.entries.forEach((entry) => {
|
|
681
672
|
const statusIcon = entry.status === "default" ? `${GREEN}=${RESET}` : entry.status === "overridden" ? `${YELLOW}~${RESET}` : `${RED}!${RESET}`;
|
|
682
673
|
const statusLabel = entry.status === "default" ? `${DIM}using default${RESET}` : entry.status === "overridden" ? `${YELLOW}overridden${RESET} (${entry.currentValue})` : `${RED}not set${RESET}`;
|
|
683
674
|
console.log(` ${statusIcon} ${BOLD}${entry.key}${RESET} = "${entry.defaultValue}" ${statusLabel}`);
|
|
684
|
-
}
|
|
675
|
+
});
|
|
685
676
|
};
|
|
686
677
|
const formatEnvAuditJson = (config) => {
|
|
687
678
|
const envAudit = computeEnvAudit(config);
|
|
@@ -699,24 +690,24 @@ const runAuditOnConfig = (config, options) => {
|
|
|
699
690
|
const secretEntries = config.secret ?? {};
|
|
700
691
|
return {
|
|
701
692
|
...audit,
|
|
702
|
-
secrets: audit.secrets.filter((s) => !!secretEntries[s.key]
|
|
693
|
+
secrets: audit.secrets.filter((s) => !!secretEntries[s.key].encrypted_value)
|
|
703
694
|
};
|
|
704
695
|
})() : audit;
|
|
705
696
|
const afterExternal = options.external ? (() => {
|
|
706
697
|
const secretEntries = config.secret ?? {};
|
|
707
698
|
return {
|
|
708
699
|
...afterSealed,
|
|
709
|
-
secrets: afterSealed.secrets.filter((s) => !secretEntries[s.key]
|
|
700
|
+
secrets: afterSealed.secrets.filter((s) => !secretEntries[s.key].encrypted_value)
|
|
710
701
|
};
|
|
711
702
|
})() : afterSealed;
|
|
712
703
|
const afterStatus = options.status ? {
|
|
713
704
|
...afterExternal,
|
|
714
705
|
secrets: afterExternal.secrets.filter((s) => s.status === options.status)
|
|
715
706
|
} : afterExternal;
|
|
716
|
-
const filtered = options.expiring
|
|
707
|
+
const filtered = Option(options.expiring).fold(() => afterStatus, (expiring) => ({
|
|
717
708
|
...afterStatus,
|
|
718
|
-
secrets: afterStatus.secrets.filter((s) => s.days_remaining.fold(() => false, (d) => d >= 0 && d <=
|
|
719
|
-
}
|
|
709
|
+
secrets: afterStatus.secrets.filter((s) => s.days_remaining.fold(() => false, (d) => d >= 0 && d <= expiring))
|
|
710
|
+
}));
|
|
720
711
|
if (options.format === "json") console.log(formatAuditJson(filtered));
|
|
721
712
|
else if (options.format === "minimal") console.log(formatAuditMinimal(filtered));
|
|
722
713
|
else console.log(formatAudit(filtered));
|
|
@@ -747,13 +738,13 @@ const fnoxExport = (profile, agentKey) => {
|
|
|
747
738
|
message: `fnox export failed: ${err}`
|
|
748
739
|
}), (output) => {
|
|
749
740
|
const entries = {};
|
|
750
|
-
|
|
741
|
+
output.split("\n").forEach((line) => {
|
|
751
742
|
const eq = line.indexOf("=");
|
|
752
743
|
if (eq > 0) {
|
|
753
744
|
const key = line.slice(0, eq).trim();
|
|
754
745
|
entries[key] = line.slice(eq + 1).trim();
|
|
755
746
|
}
|
|
756
|
-
}
|
|
747
|
+
});
|
|
757
748
|
return Right(entries);
|
|
758
749
|
});
|
|
759
750
|
};
|
|
@@ -777,27 +768,38 @@ const ageAvailable = () => Try(() => {
|
|
|
777
768
|
execFileSync("age", ["--version"], { stdio: "pipe" });
|
|
778
769
|
return true;
|
|
779
770
|
}).fold(() => false, (v) => v);
|
|
780
|
-
/**
|
|
771
|
+
/**
|
|
772
|
+
* Extract the secret key from an age identity file (plain or encrypted).
|
|
773
|
+
* - Plain identity files (from `age-keygen`) contain `AGE-SECRET-KEY-*` lines directly
|
|
774
|
+
* - Encrypted identity files need `age --decrypt` to unwrap
|
|
775
|
+
*/
|
|
781
776
|
const unwrapAgentKey = (identityPath) => {
|
|
782
777
|
if (!existsSync(identityPath)) return Left({
|
|
783
778
|
_tag: "IdentityNotFound",
|
|
784
779
|
path: identityPath
|
|
785
780
|
});
|
|
786
|
-
|
|
787
|
-
_tag: "AgeNotFound",
|
|
788
|
-
message: "age CLI not found on PATH"
|
|
789
|
-
});
|
|
790
|
-
return Try(() => execFileSync("age", ["--decrypt", identityPath], {
|
|
791
|
-
stdio: [
|
|
792
|
-
"pipe",
|
|
793
|
-
"pipe",
|
|
794
|
-
"pipe"
|
|
795
|
-
],
|
|
796
|
-
encoding: "utf-8"
|
|
797
|
-
})).fold((err) => Left({
|
|
781
|
+
return Try(() => readFileSync(identityPath, "utf-8")).fold((err) => Left({
|
|
798
782
|
_tag: "DecryptFailed",
|
|
799
|
-
message: `
|
|
800
|
-
}), (
|
|
783
|
+
message: `Failed to read identity file: ${err}`
|
|
784
|
+
}), (content) => {
|
|
785
|
+
const secretKeyLine = content.split("\n").find((l) => l.startsWith("AGE-SECRET-KEY-"));
|
|
786
|
+
if (secretKeyLine) return Right(secretKeyLine.trim());
|
|
787
|
+
if (!ageAvailable()) return Left({
|
|
788
|
+
_tag: "AgeNotFound",
|
|
789
|
+
message: "age CLI not found on PATH"
|
|
790
|
+
});
|
|
791
|
+
return Try(() => execFileSync("age", ["--decrypt", identityPath], {
|
|
792
|
+
stdio: [
|
|
793
|
+
"pipe",
|
|
794
|
+
"pipe",
|
|
795
|
+
"pipe"
|
|
796
|
+
],
|
|
797
|
+
encoding: "utf-8"
|
|
798
|
+
})).fold((err) => Left({
|
|
799
|
+
_tag: "DecryptFailed",
|
|
800
|
+
message: `age decrypt failed: ${err}`
|
|
801
|
+
}), (output) => Right(output.trim()));
|
|
802
|
+
});
|
|
801
803
|
};
|
|
802
804
|
//#endregion
|
|
803
805
|
//#region src/fnox/parse.ts
|
|
@@ -823,14 +825,14 @@ const extractFnoxKeys = (config) => new Set(Object.keys(config.secrets));
|
|
|
823
825
|
//#region src/core/keygen.ts
|
|
824
826
|
/** Resolve the age identity file path: ENVPKT_AGE_KEY_FILE env var > ~/.envpkt/age-key.txt */
|
|
825
827
|
const resolveKeyPath = () => process.env["ENVPKT_AGE_KEY_FILE"] ?? join(homedir(), ".envpkt", "age-key.txt");
|
|
826
|
-
/** Generate an age keypair and write to disk */
|
|
828
|
+
/** Generate an age keypair and write to disk. Refuses to overwrite if the file already exists. */
|
|
827
829
|
const generateKeypair = (options) => {
|
|
828
830
|
if (!ageAvailable()) return Left({
|
|
829
831
|
_tag: "AgeNotFound",
|
|
830
832
|
message: "age-keygen CLI not found on PATH. Install age: https://github.com/FiloSottile/age"
|
|
831
833
|
});
|
|
832
834
|
const outputPath = options?.outputPath ?? resolveKeyPath();
|
|
833
|
-
if (existsSync(outputPath)
|
|
835
|
+
if (existsSync(outputPath)) return Left({
|
|
834
836
|
_tag: "KeyExists",
|
|
835
837
|
path: outputPath
|
|
836
838
|
});
|
|
@@ -872,46 +874,77 @@ const generateKeypair = (options) => {
|
|
|
872
874
|
}));
|
|
873
875
|
});
|
|
874
876
|
};
|
|
875
|
-
/** Update identity
|
|
876
|
-
const
|
|
877
|
-
|
|
877
|
+
/** Update identity fields (recipient, key_file, name) in an envpkt.toml file, preserving structure */
|
|
878
|
+
const updateConfigIdentity = (configPath, options) => {
|
|
879
|
+
const readResult = Try(() => readFileSync(configPath, "utf-8"));
|
|
880
|
+
const fieldUpdaters = [
|
|
881
|
+
{
|
|
882
|
+
re: /^recipient\s*=/,
|
|
883
|
+
line: `recipient = "${options.recipient}"`
|
|
884
|
+
},
|
|
885
|
+
...options.name ? [{
|
|
886
|
+
re: /^name\s*=/,
|
|
887
|
+
line: `name = "${options.name}"`
|
|
888
|
+
}] : [],
|
|
889
|
+
...options.keyFile ? [{
|
|
890
|
+
re: /^key_file\s*=/,
|
|
891
|
+
line: `key_file = "${options.keyFile}"`
|
|
892
|
+
}] : []
|
|
893
|
+
];
|
|
894
|
+
return readResult.fold((err) => Left({
|
|
878
895
|
_tag: "ConfigUpdateError",
|
|
879
896
|
message: `Failed to read config: ${err}`
|
|
880
897
|
}), (raw) => {
|
|
881
898
|
const lines = raw.split("\n");
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
output.push(line);
|
|
891
|
-
continue;
|
|
892
|
-
}
|
|
899
|
+
const updatedFields = /* @__PURE__ */ new Set();
|
|
900
|
+
const acc = lines.reduce((state, line) => {
|
|
901
|
+
if (/^\[identity\]\s*$/.test(line)) return {
|
|
902
|
+
...state,
|
|
903
|
+
output: [...state.output, line],
|
|
904
|
+
inIdentitySection: true,
|
|
905
|
+
hasIdentitySection: true
|
|
906
|
+
};
|
|
893
907
|
if (/^\[/.test(line) && !/^\[identity\]\s*$/.test(line)) {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
908
|
+
const missing = state.inIdentitySection ? fieldUpdaters.filter((f) => !updatedFields.has(f.re.source)).map((f) => f.line) : [];
|
|
909
|
+
missing.forEach((l) => updatedFields.add(l));
|
|
910
|
+
return {
|
|
911
|
+
...state,
|
|
912
|
+
output: [
|
|
913
|
+
...state.output,
|
|
914
|
+
...missing,
|
|
915
|
+
line
|
|
916
|
+
],
|
|
917
|
+
inIdentitySection: false
|
|
918
|
+
};
|
|
901
919
|
}
|
|
902
|
-
if (inIdentitySection
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
920
|
+
if (state.inIdentitySection) {
|
|
921
|
+
const match = fieldUpdaters.find((f) => f.re.test(line));
|
|
922
|
+
if (match) {
|
|
923
|
+
updatedFields.add(match.re.source);
|
|
924
|
+
return {
|
|
925
|
+
...state,
|
|
926
|
+
output: [...state.output, match.line]
|
|
927
|
+
};
|
|
928
|
+
}
|
|
906
929
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
output
|
|
913
|
-
|
|
914
|
-
|
|
930
|
+
return {
|
|
931
|
+
...state,
|
|
932
|
+
output: [...state.output, line]
|
|
933
|
+
};
|
|
934
|
+
}, {
|
|
935
|
+
output: [],
|
|
936
|
+
inIdentitySection: false,
|
|
937
|
+
hasIdentitySection: false
|
|
938
|
+
});
|
|
939
|
+
const missingAtEof = acc.inIdentitySection ? fieldUpdaters.filter((f) => !updatedFields.has(f.re.source)).map((f) => f.line) : [];
|
|
940
|
+
const afterEof = [...acc.output, ...missingAtEof];
|
|
941
|
+
const identityLines = fieldUpdaters.map((f) => f.line);
|
|
942
|
+
const output = !acc.hasIdentitySection ? [
|
|
943
|
+
...afterEof,
|
|
944
|
+
"",
|
|
945
|
+
"[identity]",
|
|
946
|
+
...identityLines
|
|
947
|
+
] : afterEof;
|
|
915
948
|
return Try(() => writeFileSync(configPath, output.join("\n"))).fold((err) => Left({
|
|
916
949
|
_tag: "ConfigUpdateError",
|
|
917
950
|
message: `Failed to write config: ${err}`
|
|
@@ -975,27 +1008,23 @@ const sealSecrets = (meta, values, recipient) => {
|
|
|
975
1008
|
_tag: "AgeNotFound",
|
|
976
1009
|
message: "age CLI not found on PATH"
|
|
977
1010
|
});
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
}
|
|
985
|
-
const outcome = ageEncrypt(plaintext, recipient).fold((err) => Left({
|
|
1011
|
+
return Object.entries(meta).reduce((acc, [key, secretMeta]) => acc.flatMap((result) => {
|
|
1012
|
+
if (!(key in values)) return Right({
|
|
1013
|
+
...result,
|
|
1014
|
+
[key]: secretMeta
|
|
1015
|
+
});
|
|
1016
|
+
return ageEncrypt(values[key], recipient).mapLeft((err) => ({
|
|
986
1017
|
_tag: "EncryptFailed",
|
|
987
1018
|
key,
|
|
988
1019
|
message: err.message
|
|
989
|
-
})
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
}
|
|
998
|
-
return Right(result);
|
|
1020
|
+
})).map((ciphertext) => ({
|
|
1021
|
+
...result,
|
|
1022
|
+
[key]: {
|
|
1023
|
+
...secretMeta,
|
|
1024
|
+
encrypted_value: ciphertext
|
|
1025
|
+
}
|
|
1026
|
+
}));
|
|
1027
|
+
}), Right({}));
|
|
999
1028
|
};
|
|
1000
1029
|
/** Unseal secrets: decrypt encrypted_value for each meta entry that has one */
|
|
1001
1030
|
const unsealSecrets = (meta, identityPath) => {
|
|
@@ -1003,19 +1032,14 @@ const unsealSecrets = (meta, identityPath) => {
|
|
|
1003
1032
|
_tag: "AgeNotFound",
|
|
1004
1033
|
message: "age CLI not found on PATH"
|
|
1005
1034
|
});
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
const failed = outcome.fold((err) => err, () => void 0);
|
|
1015
|
-
if (failed) return Left(failed);
|
|
1016
|
-
result[key] = outcome.fold(() => "", (v) => v);
|
|
1017
|
-
}
|
|
1018
|
-
return Right(result);
|
|
1035
|
+
return Object.entries(meta).filter(([, secretMeta]) => secretMeta.encrypted_value !== void 0 && secretMeta.encrypted_value !== "").reduce((acc, [key, secretMeta]) => acc.flatMap((result) => ageDecrypt(secretMeta.encrypted_value, identityPath).mapLeft((err) => ({
|
|
1036
|
+
_tag: "DecryptFailed",
|
|
1037
|
+
key,
|
|
1038
|
+
message: err.message
|
|
1039
|
+
})).map((plaintext) => ({
|
|
1040
|
+
...result,
|
|
1041
|
+
[key]: plaintext
|
|
1042
|
+
}))), Right({}));
|
|
1019
1043
|
};
|
|
1020
1044
|
//#endregion
|
|
1021
1045
|
//#region src/core/boot.ts
|
|
@@ -1030,15 +1054,13 @@ const resolveAndLoad = (opts) => resolveConfigPath(opts.configPath).fold((err) =
|
|
|
1030
1054
|
}));
|
|
1031
1055
|
/** Resolve identity file path with explicit fallback control */
|
|
1032
1056
|
const resolveIdentityFilePath = (config, configDir, useDefaultFallback) => {
|
|
1033
|
-
if (config.identity?.key_file) return resolve(configDir, expandPath(config.identity.key_file));
|
|
1034
|
-
if (!useDefaultFallback) return void 0;
|
|
1057
|
+
if (config.identity?.key_file) return Option(resolve(configDir, expandPath(config.identity.key_file)));
|
|
1058
|
+
if (!useDefaultFallback) return Option(void 0);
|
|
1035
1059
|
const defaultPath = resolveKeyPath();
|
|
1036
|
-
return existsSync(defaultPath) ? defaultPath : void 0;
|
|
1060
|
+
return existsSync(defaultPath) ? Option(defaultPath) : Option(void 0);
|
|
1037
1061
|
};
|
|
1038
1062
|
const resolveIdentityKey = (config, configDir) => {
|
|
1039
|
-
|
|
1040
|
-
if (!identityPath) return Right(void 0);
|
|
1041
|
-
return unwrapAgentKey(identityPath).fold((err) => Left(err), (key) => Right(key));
|
|
1063
|
+
return resolveIdentityFilePath(config, configDir, false).fold(() => Right(Option(void 0)), (path) => unwrapAgentKey(path).fold((err) => Left(err), (key) => Right(Option(key))));
|
|
1042
1064
|
};
|
|
1043
1065
|
const detectFnoxKeys = (configDir) => detectFnox(configDir).fold(() => /* @__PURE__ */ new Set(), (fnoxPath) => readFnoxConfig(fnoxPath).fold(() => /* @__PURE__ */ new Set(), (fnoxConfig) => extractFnoxKeys(fnoxConfig)));
|
|
1044
1066
|
const checkExpiration = (audit, failOnExpired, warnOnly) => {
|
|
@@ -1066,10 +1088,8 @@ const looksLikeSecret = (value) => {
|
|
|
1066
1088
|
return false;
|
|
1067
1089
|
};
|
|
1068
1090
|
const checkEnvMisclassification = (config) => {
|
|
1069
|
-
const warnings = [];
|
|
1070
1091
|
const envEntries = config.env ?? {};
|
|
1071
|
-
|
|
1072
|
-
return warnings;
|
|
1092
|
+
return Object.entries(envEntries).filter(([, entry]) => looksLikeSecret(entry.value)).map(([key]) => `[env.${key}] value looks like a secret — consider moving to [secret.${key}]`);
|
|
1073
1093
|
};
|
|
1074
1094
|
/** Programmatic boot — returns Either<BootError, BootResult> */
|
|
1075
1095
|
const bootSafe = (options) => {
|
|
@@ -1080,11 +1100,13 @@ const bootSafe = (options) => {
|
|
|
1080
1100
|
return resolveAndLoad(opts).flatMap(({ config, configPath, configDir, configSource }) => {
|
|
1081
1101
|
const secretEntries = config.secret ?? {};
|
|
1082
1102
|
const metaKeys = Object.keys(secretEntries);
|
|
1083
|
-
const hasSealedValues = metaKeys.some((k) => !!secretEntries[k]
|
|
1103
|
+
const hasSealedValues = metaKeys.some((k) => !!secretEntries[k].encrypted_value);
|
|
1084
1104
|
const identityKeyResult = resolveIdentityKey(config, configDir);
|
|
1085
|
-
const identityKey = identityKeyResult.fold(() => void 0, (k) => k);
|
|
1086
|
-
|
|
1087
|
-
|
|
1105
|
+
const identityKey = identityKeyResult.fold(() => Option(void 0), (k) => k);
|
|
1106
|
+
if (identityKeyResult.isLeft() && !hasSealedValues) return identityKeyResult.fold((err) => Left(err), () => Left({
|
|
1107
|
+
_tag: "ReadError",
|
|
1108
|
+
message: "unexpected"
|
|
1109
|
+
}));
|
|
1088
1110
|
const audit = computeAudit(config, detectFnoxKeys(configDir));
|
|
1089
1111
|
return checkExpiration(audit, failOnExpired, warnOnly).map((warnings) => {
|
|
1090
1112
|
const secrets = {};
|
|
@@ -1092,40 +1114,47 @@ const bootSafe = (options) => {
|
|
|
1092
1114
|
const skipped = [];
|
|
1093
1115
|
warnings.push(...checkEnvMisclassification(config));
|
|
1094
1116
|
const envEntries = config.env ?? {};
|
|
1095
|
-
const
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
}
|
|
1117
|
+
const envEntriesArr = Object.entries(envEntries);
|
|
1118
|
+
const envDefaults = Object.fromEntries(envEntriesArr.flatMap(([key, entry]) => Option(process.env[key]).fold(() => [[key, entry.value]], () => [])));
|
|
1119
|
+
const overridden = envEntriesArr.flatMap(([key]) => Option(process.env[key]).fold(() => [], () => [key]));
|
|
1120
|
+
if (inject) Object.entries(envDefaults).forEach(([key, value]) => {
|
|
1121
|
+
process.env[key] = value;
|
|
1122
|
+
});
|
|
1101
1123
|
const sealedKeys = /* @__PURE__ */ new Set();
|
|
1102
1124
|
const identityFilePath = resolveIdentityFilePath(config, configDir, true);
|
|
1103
|
-
if (hasSealedValues
|
|
1104
|
-
warnings.push(
|
|
1105
|
-
}, (
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1125
|
+
if (hasSealedValues) identityFilePath.fold(() => {
|
|
1126
|
+
warnings.push("Sealed values found but no identity file available for decryption");
|
|
1127
|
+
}, (idPath) => {
|
|
1128
|
+
unsealSecrets(secretEntries, idPath).fold((err) => {
|
|
1129
|
+
warnings.push(`Sealed value decryption failed: ${err.message}`);
|
|
1130
|
+
}, (unsealed) => {
|
|
1131
|
+
const unsealedEntries = Object.entries(unsealed);
|
|
1132
|
+
Object.assign(secrets, unsealed);
|
|
1133
|
+
injected.push(...unsealedEntries.map(([key]) => key));
|
|
1134
|
+
unsealedEntries.map(([key]) => key).forEach((key) => sealedKeys.add(key));
|
|
1135
|
+
});
|
|
1111
1136
|
});
|
|
1112
|
-
else if (hasSealedValues && !identityFilePath) warnings.push("Sealed values found but no identity file available for decryption");
|
|
1113
1137
|
const remainingKeys = metaKeys.filter((k) => !sealedKeys.has(k));
|
|
1114
|
-
if (remainingKeys.length > 0) if (fnoxAvailable()) fnoxExport(opts.profile, identityKey).fold((err) => {
|
|
1138
|
+
if (remainingKeys.length > 0) if (fnoxAvailable()) fnoxExport(opts.profile, identityKey.orUndefined()).fold((err) => {
|
|
1115
1139
|
warnings.push(`fnox export failed: ${err.message}`);
|
|
1116
|
-
|
|
1140
|
+
skipped.push(...remainingKeys);
|
|
1117
1141
|
}, (exported) => {
|
|
1118
|
-
|
|
1142
|
+
const found = remainingKeys.filter((key) => key in exported);
|
|
1143
|
+
const notFound = remainingKeys.filter((key) => !(key in exported));
|
|
1144
|
+
found.forEach((key) => {
|
|
1119
1145
|
secrets[key] = exported[key];
|
|
1120
|
-
|
|
1121
|
-
|
|
1146
|
+
});
|
|
1147
|
+
injected.push(...found);
|
|
1148
|
+
skipped.push(...notFound);
|
|
1122
1149
|
});
|
|
1123
1150
|
else {
|
|
1124
1151
|
if (!hasSealedValues) warnings.push("fnox not available — no secrets injected");
|
|
1125
1152
|
else warnings.push("fnox not available — unsealed secrets could not be resolved");
|
|
1126
|
-
|
|
1153
|
+
skipped.push(...remainingKeys);
|
|
1127
1154
|
}
|
|
1128
|
-
if (inject)
|
|
1155
|
+
if (inject) Object.entries(secrets).forEach(([key, value]) => {
|
|
1156
|
+
process.env[key] = value;
|
|
1157
|
+
});
|
|
1129
1158
|
return {
|
|
1130
1159
|
audit,
|
|
1131
1160
|
injected,
|
|
@@ -1763,11 +1792,10 @@ const VALUE_SHAPE_PATTERNS = [
|
|
|
1763
1792
|
];
|
|
1764
1793
|
/** Detect service from value prefix/shape */
|
|
1765
1794
|
const matchValueShape = (value) => {
|
|
1766
|
-
|
|
1795
|
+
return Option(VALUE_SHAPE_PATTERNS.find((vp) => value.startsWith(vp.prefix))).map((vp) => ({
|
|
1767
1796
|
service: vp.service,
|
|
1768
1797
|
description: vp.description
|
|
1769
|
-
});
|
|
1770
|
-
return Option(void 0);
|
|
1798
|
+
}));
|
|
1771
1799
|
};
|
|
1772
1800
|
/** Strip common suffixes and derive a service name from an env var name */
|
|
1773
1801
|
const deriveServiceFromName = (name) => {
|
|
@@ -1795,22 +1823,22 @@ const deriveServiceFromName = (name) => {
|
|
|
1795
1823
|
/** Match a single env var against all patterns */
|
|
1796
1824
|
const matchEnvVar = (name, value) => {
|
|
1797
1825
|
if (EXCLUDED_VARS.has(name)) return Option(void 0);
|
|
1798
|
-
|
|
1826
|
+
const exactMatch = EXACT_NAME_PATTERNS.find((p) => name === p.pattern);
|
|
1827
|
+
if (exactMatch) return Option({
|
|
1799
1828
|
envVar: name,
|
|
1800
1829
|
value,
|
|
1801
|
-
service: Option(
|
|
1802
|
-
confidence:
|
|
1803
|
-
matchedBy: `exact:${
|
|
1830
|
+
service: Option(exactMatch.service),
|
|
1831
|
+
confidence: exactMatch.confidence,
|
|
1832
|
+
matchedBy: `exact:${exactMatch.pattern}`
|
|
1804
1833
|
});
|
|
1805
1834
|
return matchValueShape(value).fold(() => {
|
|
1806
|
-
|
|
1835
|
+
return Option(SUFFIX_PATTERNS.find((sp) => name.endsWith(sp.suffix))).map((sp) => ({
|
|
1807
1836
|
envVar: name,
|
|
1808
1837
|
value,
|
|
1809
1838
|
service: Option(deriveServiceFromName(name)),
|
|
1810
1839
|
confidence: "medium",
|
|
1811
1840
|
matchedBy: `suffix:${sp.suffix}`
|
|
1812
|
-
});
|
|
1813
|
-
return Option(void 0);
|
|
1841
|
+
}));
|
|
1814
1842
|
}, (vm) => Option({
|
|
1815
1843
|
envVar: name,
|
|
1816
1844
|
value,
|
|
@@ -1821,11 +1849,10 @@ const matchEnvVar = (name, value) => {
|
|
|
1821
1849
|
};
|
|
1822
1850
|
/** Scan full env, sorted by confidence (high first) then alphabetically */
|
|
1823
1851
|
const scanEnv = (env) => {
|
|
1824
|
-
const results = []
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
}
|
|
1852
|
+
const results = Object.entries(env).flatMap(([name, value]) => {
|
|
1853
|
+
if (value === void 0 || value === "") return [];
|
|
1854
|
+
return matchEnvVar(name, value).fold(() => [], (m) => [m]);
|
|
1855
|
+
});
|
|
1829
1856
|
const confidenceOrder = {
|
|
1830
1857
|
high: 0,
|
|
1831
1858
|
medium: 1,
|
|
@@ -1858,38 +1885,44 @@ const envScan = (env, options) => {
|
|
|
1858
1885
|
};
|
|
1859
1886
|
/** Bidirectional drift detection between config and live environment */
|
|
1860
1887
|
const envCheck = (config, env) => {
|
|
1861
|
-
const entries = [];
|
|
1862
1888
|
const secretEntries = config.secret ?? {};
|
|
1863
1889
|
const metaKeys = Object.keys(secretEntries);
|
|
1864
1890
|
const trackedSet = new Set(metaKeys);
|
|
1865
|
-
|
|
1891
|
+
const secretDriftEntries = metaKeys.map((key) => {
|
|
1866
1892
|
const meta = secretEntries[key];
|
|
1867
1893
|
const present = env[key] !== void 0 && env[key] !== "";
|
|
1868
|
-
|
|
1894
|
+
return {
|
|
1869
1895
|
envVar: key,
|
|
1870
|
-
service: Option(meta
|
|
1896
|
+
service: Option(meta.service),
|
|
1871
1897
|
status: present ? "tracked" : "missing_from_env",
|
|
1872
1898
|
confidence: Option(void 0)
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1899
|
+
};
|
|
1900
|
+
});
|
|
1875
1901
|
const envDefaults = config.env ?? {};
|
|
1876
|
-
|
|
1902
|
+
const envDefaultEntries = Object.keys(envDefaults).filter((key) => {
|
|
1903
|
+
if (trackedSet.has(key)) return false;
|
|
1877
1904
|
trackedSet.add(key);
|
|
1905
|
+
return true;
|
|
1906
|
+
}).map((key) => {
|
|
1878
1907
|
const present = env[key] !== void 0 && env[key] !== "";
|
|
1879
|
-
|
|
1908
|
+
return {
|
|
1880
1909
|
envVar: key,
|
|
1881
1910
|
service: Option(void 0),
|
|
1882
1911
|
status: present ? "tracked" : "missing_from_env",
|
|
1883
1912
|
confidence: Option(void 0)
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
const
|
|
1887
|
-
for (const match of envMatches) if (!trackedSet.has(match.envVar)) entries.push({
|
|
1913
|
+
};
|
|
1914
|
+
});
|
|
1915
|
+
const untrackedEntries = scanEnv(env).filter((match) => !trackedSet.has(match.envVar)).map((match) => ({
|
|
1888
1916
|
envVar: match.envVar,
|
|
1889
1917
|
service: match.service,
|
|
1890
1918
|
status: "untracked",
|
|
1891
1919
|
confidence: Option(match.confidence)
|
|
1892
|
-
});
|
|
1920
|
+
}));
|
|
1921
|
+
const entries = [
|
|
1922
|
+
...secretDriftEntries,
|
|
1923
|
+
...envDefaultEntries,
|
|
1924
|
+
...untrackedEntries
|
|
1925
|
+
];
|
|
1893
1926
|
const tracked_and_present = entries.filter((e) => e.status === "tracked").length;
|
|
1894
1927
|
const missing_from_env = entries.filter((e) => e.status === "missing_from_env").length;
|
|
1895
1928
|
const untracked_credentials = entries.filter((e) => e.status === "untracked").length;
|
|
@@ -1904,10 +1937,9 @@ const envCheck = (config, env) => {
|
|
|
1904
1937
|
const todayIso$1 = () => (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1905
1938
|
/** Generate TOML [secret.*] blocks from scan results, mirroring init.ts pattern */
|
|
1906
1939
|
const generateTomlFromScan = (matches) => {
|
|
1907
|
-
|
|
1908
|
-
for (const match of matches) {
|
|
1940
|
+
return matches.map((match) => {
|
|
1909
1941
|
const svc = match.service.fold(() => match.envVar.toLowerCase().replace(/_/g, "-"), (s) => s);
|
|
1910
|
-
|
|
1942
|
+
return `[secret.${match.envVar}]
|
|
1911
1943
|
service = "${svc}"
|
|
1912
1944
|
# purpose = "" # Why: what this secret enables
|
|
1913
1945
|
# capabilities = [] # What operations this grants
|
|
@@ -1916,9 +1948,8 @@ created = "${todayIso$1()}"
|
|
|
1916
1948
|
# rotation_url = "" # URL for rotation procedure
|
|
1917
1949
|
# source = "" # Where the value originates (e.g. vault, ci)
|
|
1918
1950
|
# tags = {}
|
|
1919
|
-
|
|
1920
|
-
}
|
|
1921
|
-
return blocks.join("\n");
|
|
1951
|
+
`;
|
|
1952
|
+
}).join("\n");
|
|
1922
1953
|
};
|
|
1923
1954
|
//#endregion
|
|
1924
1955
|
//#region src/core/toml-edit.ts
|
|
@@ -1930,11 +1961,7 @@ const MULTILINE_OPEN = "\"\"\"";
|
|
|
1930
1961
|
* Handles multiline `"""..."""` values when scanning for section boundaries.
|
|
1931
1962
|
*/
|
|
1932
1963
|
const findSectionRange = (lines, sectionHeader) => {
|
|
1933
|
-
|
|
1934
|
-
for (let i = 0; i < lines.length; i++) if (lines[i].trim() === sectionHeader) {
|
|
1935
|
-
start = i;
|
|
1936
|
-
break;
|
|
1937
|
-
}
|
|
1964
|
+
const start = lines.findIndex((l) => l.trim() === sectionHeader);
|
|
1938
1965
|
if (start === -1) return void 0;
|
|
1939
1966
|
let end = lines.length;
|
|
1940
1967
|
let inMultiline = false;
|
|
@@ -2056,7 +2083,8 @@ const updateSectionFields = (raw, sectionHeader, updates) => {
|
|
|
2056
2083
|
}
|
|
2057
2084
|
remaining.push(line);
|
|
2058
2085
|
}
|
|
2059
|
-
|
|
2086
|
+
const newFields = Object.entries(updates).filter(([key, value]) => value !== null && !updatedKeys.has(key)).map(([key, value]) => `${key} = ${value}`);
|
|
2087
|
+
remaining.push(...newFields);
|
|
2060
2088
|
const result = [
|
|
2061
2089
|
...before,
|
|
2062
2090
|
...remaining,
|
|
@@ -2156,13 +2184,21 @@ const runEnvExport = (options) => {
|
|
|
2156
2184
|
}, (boot) => {
|
|
2157
2185
|
const sourceMsg = formatConfigSource(boot.configPath, boot.configSource);
|
|
2158
2186
|
if (sourceMsg) console.error(sourceMsg);
|
|
2159
|
-
|
|
2160
|
-
|
|
2187
|
+
boot.warnings.forEach((warning) => {
|
|
2188
|
+
console.error(`${YELLOW}Warning:${RESET} ${warning}`);
|
|
2189
|
+
});
|
|
2190
|
+
Object.entries(boot.envDefaults).forEach(([key, value]) => {
|
|
2191
|
+
console.log(`export ${key}='${shellEscape(value)}'`);
|
|
2192
|
+
});
|
|
2161
2193
|
if (boot.overridden.length > 0) loadConfig(boot.configPath).fold(() => {}, (config) => {
|
|
2162
2194
|
const envEntries = config.env ?? {};
|
|
2163
|
-
|
|
2195
|
+
boot.overridden.forEach((key) => {
|
|
2196
|
+
if (key in envEntries) console.log(`export ${key}='${shellEscape(envEntries[key].value)}'`);
|
|
2197
|
+
});
|
|
2198
|
+
});
|
|
2199
|
+
Object.entries(boot.secrets).forEach(([key, value]) => {
|
|
2200
|
+
console.log(`export ${key}='${shellEscape(value)}'`);
|
|
2164
2201
|
});
|
|
2165
|
-
for (const [key, value] of Object.entries(boot.secrets)) console.log(`export ${key}='${shellEscape(value)}'`);
|
|
2166
2202
|
});
|
|
2167
2203
|
};
|
|
2168
2204
|
const buildEnvBlock = (name, value, options) => {
|
|
@@ -2179,7 +2215,7 @@ const buildEnvBlock = (name, value, options) => {
|
|
|
2179
2215
|
return `${lines.join("\n")}\n`;
|
|
2180
2216
|
};
|
|
2181
2217
|
const withConfig$1 = (configFlag, fn) => {
|
|
2182
|
-
resolveConfigPath(configFlag).fold((err) => {
|
|
2218
|
+
resolveConfigPath(configFlag.orUndefined()).fold((err) => {
|
|
2183
2219
|
console.error(formatError(err));
|
|
2184
2220
|
process.exit(2);
|
|
2185
2221
|
}, ({ path: configPath, source }) => {
|
|
@@ -2215,7 +2251,7 @@ const runEnvAdd = (name, value, options) => {
|
|
|
2215
2251
|
});
|
|
2216
2252
|
};
|
|
2217
2253
|
const runEnvEdit = (name, options) => {
|
|
2218
|
-
withConfig$1(options.config, (configPath, raw) => {
|
|
2254
|
+
withConfig$1(Option(options.config), (configPath, raw) => {
|
|
2219
2255
|
loadConfig(configPath).fold((err) => {
|
|
2220
2256
|
console.error(formatError(err));
|
|
2221
2257
|
process.exit(2);
|
|
@@ -2252,7 +2288,7 @@ const runEnvEdit = (name, options) => {
|
|
|
2252
2288
|
});
|
|
2253
2289
|
};
|
|
2254
2290
|
const runEnvRm = (name, options) => {
|
|
2255
|
-
withConfig$1(options.config, (configPath, raw) => {
|
|
2291
|
+
withConfig$1(Option(options.config), (configPath, raw) => {
|
|
2256
2292
|
removeSection(raw, `[env.${name}]`).fold((err) => {
|
|
2257
2293
|
console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
|
|
2258
2294
|
process.exit(1);
|
|
@@ -2268,7 +2304,7 @@ const runEnvRm = (name, options) => {
|
|
|
2268
2304
|
});
|
|
2269
2305
|
};
|
|
2270
2306
|
const runEnvRename = (oldName, newName, options) => {
|
|
2271
|
-
withConfig$1(options.config, (configPath, raw) => {
|
|
2307
|
+
withConfig$1(Option(options.config), (configPath, raw) => {
|
|
2272
2308
|
renameSection(raw, `[env.${oldName}]`, `[env.${newName}]`).fold((err) => {
|
|
2273
2309
|
console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
|
|
2274
2310
|
process.exit(1);
|
|
@@ -2345,20 +2381,24 @@ const runExec = (args, options) => {
|
|
|
2345
2381
|
}
|
|
2346
2382
|
if (boot.audit.status === "critical" && options.warnOnly) console.error(`${YELLOW}Warning:${RESET} Proceeding despite critical audit status (--warn-only)`);
|
|
2347
2383
|
}
|
|
2348
|
-
|
|
2384
|
+
boot.warnings.forEach((warning) => {
|
|
2385
|
+
console.error(`${YELLOW}Warning:${RESET} ${warning}`);
|
|
2386
|
+
});
|
|
2349
2387
|
const env = { ...process.env };
|
|
2350
|
-
|
|
2351
|
-
|
|
2388
|
+
Object.entries(boot.envDefaults).forEach(([key, value]) => {
|
|
2389
|
+
if (!(key in env)) env[key] = value;
|
|
2390
|
+
});
|
|
2391
|
+
Object.entries(boot.secrets).forEach(([key, value]) => {
|
|
2392
|
+
env[key] = value;
|
|
2393
|
+
});
|
|
2352
2394
|
const [cmd, ...cmdArgs] = args;
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
});
|
|
2358
|
-
} catch (err) {
|
|
2395
|
+
Try(() => execFileSync(cmd, cmdArgs, {
|
|
2396
|
+
env,
|
|
2397
|
+
stdio: "inherit"
|
|
2398
|
+
})).fold((err) => {
|
|
2359
2399
|
const exitCode = err.status ?? 1;
|
|
2360
2400
|
process.exit(exitCode);
|
|
2361
|
-
}
|
|
2401
|
+
}, () => {});
|
|
2362
2402
|
};
|
|
2363
2403
|
//#endregion
|
|
2364
2404
|
//#region src/core/fleet.ts
|
|
@@ -2398,17 +2438,15 @@ function* findEnvpktFiles(dir, maxDepth, currentDepth = 0) {
|
|
|
2398
2438
|
}
|
|
2399
2439
|
const scanFleet = (rootDir, options) => {
|
|
2400
2440
|
const maxDepth = options?.maxDepth ?? 3;
|
|
2401
|
-
const
|
|
2402
|
-
for (const configPath of findEnvpktFiles(rootDir, maxDepth)) loadConfig(configPath).fold(() => {}, (config) => {
|
|
2441
|
+
const agentList = List(Array.from(findEnvpktFiles(rootDir, maxDepth)).flatMap((configPath) => loadConfig(configPath).fold(() => [], (config) => {
|
|
2403
2442
|
const audit = computeAudit(config);
|
|
2404
|
-
|
|
2443
|
+
return [{
|
|
2405
2444
|
path: configPath,
|
|
2406
2445
|
identity: config.identity,
|
|
2407
|
-
min_expiry_days: audit.secrets.toArray().reduce((min, s) => s.days_remaining.fold(() => min, (d) => min
|
|
2446
|
+
min_expiry_days: audit.secrets.toArray().reduce((min, s) => s.days_remaining.fold(() => min, (d) => min.fold(() => Option(d), (m) => Option(Math.min(m, d)))), Option(void 0)).orUndefined(),
|
|
2408
2447
|
audit
|
|
2409
|
-
}
|
|
2410
|
-
});
|
|
2411
|
-
const agentList = List(agents);
|
|
2448
|
+
}];
|
|
2449
|
+
})));
|
|
2412
2450
|
const total_agents = agentList.size;
|
|
2413
2451
|
const total_secrets = agentList.toArray().reduce((acc, a) => acc + a.audit.total, 0);
|
|
2414
2452
|
const expired = agentList.toArray().reduce((acc, a) => acc + a.audit.expired, 0);
|
|
@@ -2440,17 +2478,16 @@ const runFleet = (options) => {
|
|
|
2440
2478
|
process.exit(fleet.status === "critical" ? 2 : 0);
|
|
2441
2479
|
return;
|
|
2442
2480
|
}
|
|
2443
|
-
const
|
|
2444
|
-
const agents = statusFilter ? fleet.agents.filter((a) => a.audit.status === statusFilter) : fleet.agents;
|
|
2481
|
+
const agents = Option(options.status).fold(() => fleet.agents, (s) => fleet.agents.filter((a) => a.audit.status === s));
|
|
2445
2482
|
console.log(`${statusIcon(fleet.status)} ${BOLD}Fleet: ${fleet.status.toUpperCase()}${RESET} — ${fleet.total_agents} agents, ${fleet.total_secrets} secrets`);
|
|
2446
2483
|
if (fleet.expired > 0) console.log(` ${RED}${fleet.expired}${RESET} expired`);
|
|
2447
2484
|
if (fleet.expiring_soon > 0) console.log(` ${YELLOW}${fleet.expiring_soon}${RESET} expiring soon`);
|
|
2448
2485
|
console.log("");
|
|
2449
|
-
|
|
2486
|
+
agents.forEach((agent) => {
|
|
2450
2487
|
const name = agent.identity?.name ? BOLD + agent.identity.name + RESET : DIM + agent.path + RESET;
|
|
2451
2488
|
const icon = statusIcon(agent.audit.status);
|
|
2452
2489
|
console.log(` ${icon} ${name} ${DIM}(${agent.audit.total} secrets)${RESET}`);
|
|
2453
|
-
}
|
|
2490
|
+
});
|
|
2454
2491
|
process.exit(fleet.status === "critical" ? 2 : 0);
|
|
2455
2492
|
};
|
|
2456
2493
|
//#endregion
|
|
@@ -2507,7 +2544,7 @@ const generateTemplate = (options, fnoxKeys) => {
|
|
|
2507
2544
|
lines.push(``);
|
|
2508
2545
|
if (fnoxKeys && fnoxKeys.length > 0) {
|
|
2509
2546
|
lines.push(`# Secrets detected from fnox.toml`);
|
|
2510
|
-
|
|
2547
|
+
lines.push(...fnoxKeys.map((key) => generateSecretBlock(key)));
|
|
2511
2548
|
} else {
|
|
2512
2549
|
lines.push(`# Add your secret metadata below.`);
|
|
2513
2550
|
lines.push(`# Each [secret.<key>] describes a secret your agent needs.`);
|
|
@@ -2542,8 +2579,8 @@ const runInit = (dir, options) => {
|
|
|
2542
2579
|
console.error(`${RED}Error:${RESET} ${CONFIG_FILENAME} already exists. Use --force to overwrite.`);
|
|
2543
2580
|
process.exit(1);
|
|
2544
2581
|
}
|
|
2545
|
-
const fnoxKeys = options.fromFnox
|
|
2546
|
-
const fnoxPath =
|
|
2582
|
+
const fnoxKeys = Option(options.fromFnox).map((fromFnox) => {
|
|
2583
|
+
const fnoxPath = fromFnox === "true" || fromFnox === "" ? join(dir, "fnox.toml") : fromFnox;
|
|
2547
2584
|
if (!existsSync(fnoxPath)) {
|
|
2548
2585
|
console.error(`${RED}Error:${RESET} fnox.toml not found at ${fnoxPath}`);
|
|
2549
2586
|
process.exit(1);
|
|
@@ -2551,15 +2588,18 @@ const runInit = (dir, options) => {
|
|
|
2551
2588
|
return readFnoxKeys(fnoxPath).fold((err) => {
|
|
2552
2589
|
console.error(`${RED}Error:${RESET} Failed to read fnox.toml: ${formatConfigError(err)}`);
|
|
2553
2590
|
process.exit(1);
|
|
2591
|
+
return [];
|
|
2554
2592
|
}, (keys) => keys);
|
|
2555
|
-
})
|
|
2556
|
-
const content = generateTemplate(options, fnoxKeys);
|
|
2593
|
+
});
|
|
2594
|
+
const content = generateTemplate(options, fnoxKeys.orUndefined());
|
|
2557
2595
|
Try(() => writeFileSync(outPath, content, "utf-8")).fold((err) => {
|
|
2558
2596
|
console.error(`${RED}Error:${RESET} Failed to write ${CONFIG_FILENAME}: ${err}`);
|
|
2559
2597
|
process.exit(1);
|
|
2560
2598
|
}, () => {
|
|
2561
2599
|
console.log(`${GREEN}✓${RESET} Created ${BOLD}${CONFIG_FILENAME}${RESET} in ${CYAN}${dir}${RESET}`);
|
|
2562
|
-
|
|
2600
|
+
fnoxKeys.forEach((keys) => {
|
|
2601
|
+
console.log(` Scaffolded ${keys.length} secret(s) from fnox.toml`);
|
|
2602
|
+
});
|
|
2563
2603
|
console.log(` ${BOLD}Next:${RESET} Fill in metadata for each secret`);
|
|
2564
2604
|
});
|
|
2565
2605
|
};
|
|
@@ -2572,75 +2612,76 @@ const maskValue = (value) => {
|
|
|
2572
2612
|
//#endregion
|
|
2573
2613
|
//#region src/cli/commands/inspect.ts
|
|
2574
2614
|
const printSecretMeta = (meta, indent) => {
|
|
2575
|
-
if (meta.purpose) console.log(`${indent}purpose
|
|
2576
|
-
if (meta.comment) console.log(`${indent}comment
|
|
2577
|
-
if (meta.capabilities) console.log(`${indent}
|
|
2615
|
+
if (meta.purpose) console.log(`${indent}${DIM}purpose:${RESET} ${meta.purpose}`);
|
|
2616
|
+
if (meta.comment) console.log(`${indent}${DIM}comment:${RESET} ${DIM}${meta.comment}${RESET}`);
|
|
2617
|
+
if (meta.capabilities) console.log(`${indent}${DIM}capabilities:${RESET} ${meta.capabilities.map((c) => `${MAGENTA}${c}${RESET}`).join(", ")}`);
|
|
2578
2618
|
const dateParts = [];
|
|
2579
|
-
if (meta.created) dateParts.push(
|
|
2580
|
-
if (meta.expires) dateParts.push(
|
|
2619
|
+
if (meta.created) dateParts.push(`${DIM}created:${RESET} ${BLUE}${meta.created}${RESET}`);
|
|
2620
|
+
if (meta.expires) dateParts.push(`${DIM}expires:${RESET} ${YELLOW}${meta.expires}${RESET}`);
|
|
2581
2621
|
if (dateParts.length > 0) console.log(`${indent}${dateParts.join(" ")}`);
|
|
2582
2622
|
const opsParts = [];
|
|
2583
|
-
if (meta.rotates) opsParts.push(
|
|
2584
|
-
if (meta.rate_limit) opsParts.push(
|
|
2623
|
+
if (meta.rotates) opsParts.push(`${DIM}rotates:${RESET} ${CYAN}${meta.rotates}${RESET}`);
|
|
2624
|
+
if (meta.rate_limit) opsParts.push(`${DIM}rate_limit:${RESET} ${CYAN}${meta.rate_limit}${RESET}`);
|
|
2585
2625
|
if (opsParts.length > 0) console.log(`${indent}${opsParts.join(" ")}`);
|
|
2586
|
-
if (meta.source) console.log(`${indent}source
|
|
2587
|
-
if (meta.model_hint) console.log(`${indent}model_hint
|
|
2588
|
-
if (meta.rotation_url) console.log(`${indent}rotation_url
|
|
2589
|
-
if (meta.required !== void 0) console.log(`${indent}required
|
|
2626
|
+
if (meta.source) console.log(`${indent}${DIM}source:${RESET} ${BLUE}${meta.source}${RESET}`);
|
|
2627
|
+
if (meta.model_hint) console.log(`${indent}${DIM}model_hint:${RESET} ${MAGENTA}${meta.model_hint}${RESET}`);
|
|
2628
|
+
if (meta.rotation_url) console.log(`${indent}${DIM}rotation_url:${RESET} ${DIM}${meta.rotation_url}${RESET}`);
|
|
2629
|
+
if (meta.required !== void 0) console.log(`${indent}${DIM}required:${RESET} ${meta.required ? `${GREEN}true${RESET}` : `${DIM}false${RESET}`}`);
|
|
2590
2630
|
if (meta.tags) {
|
|
2591
|
-
const tagStr = Object.entries(meta.tags).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
2592
|
-
console.log(`${indent}tags
|
|
2631
|
+
const tagStr = Object.entries(meta.tags).map(([k, v]) => `${CYAN}${k}${RESET}=${DIM}${v}${RESET}`).join(", ");
|
|
2632
|
+
console.log(`${indent}${DIM}tags:${RESET} ${tagStr}`);
|
|
2593
2633
|
}
|
|
2594
2634
|
};
|
|
2595
2635
|
const printConfig = (config, path, resolveResult, opts) => {
|
|
2596
2636
|
console.log(`${BOLD}envpkt.toml${RESET} ${DIM}(${path})${RESET}`);
|
|
2597
2637
|
if (resolveResult?.catalogPath) console.log(`${DIM}Catalog: ${CYAN}${resolveResult.catalogPath}${RESET}`);
|
|
2598
|
-
console.log(
|
|
2638
|
+
console.log(`${DIM}version:${RESET} ${config.version}`);
|
|
2599
2639
|
console.log("");
|
|
2600
2640
|
if (config.identity) {
|
|
2601
|
-
console.log(`${BOLD}Identity:${RESET} ${config.identity.name}`);
|
|
2602
|
-
if (config.identity.consumer) console.log(` consumer
|
|
2603
|
-
if (config.identity.description) console.log(` description
|
|
2604
|
-
if (config.identity.capabilities) console.log(` capabilities
|
|
2605
|
-
if (config.identity.expires) console.log(` expires
|
|
2606
|
-
if (config.identity.services) console.log(` services
|
|
2607
|
-
if (config.identity.secrets) console.log(` secrets
|
|
2641
|
+
console.log(`${BOLD}Identity:${RESET} ${GREEN}${config.identity.name}${RESET}`);
|
|
2642
|
+
if (config.identity.consumer) console.log(` ${DIM}consumer:${RESET} ${MAGENTA}${config.identity.consumer}${RESET}`);
|
|
2643
|
+
if (config.identity.description) console.log(` ${DIM}description:${RESET} ${config.identity.description}`);
|
|
2644
|
+
if (config.identity.capabilities) console.log(` ${DIM}capabilities:${RESET} ${config.identity.capabilities.map((c) => `${MAGENTA}${c}${RESET}`).join(", ")}`);
|
|
2645
|
+
if (config.identity.expires) console.log(` ${DIM}expires:${RESET} ${YELLOW}${config.identity.expires}${RESET}`);
|
|
2646
|
+
if (config.identity.services) console.log(` ${DIM}services:${RESET} ${config.identity.services.map((s) => `${CYAN}${s}${RESET}`).join(", ")}`);
|
|
2647
|
+
if (config.identity.secrets) console.log(` ${DIM}secrets:${RESET} ${config.identity.secrets.map((s) => `${BOLD}${s}${RESET}`).join(", ")}`);
|
|
2608
2648
|
console.log("");
|
|
2609
2649
|
}
|
|
2610
2650
|
const secretEntries = config.secret ?? {};
|
|
2611
2651
|
console.log(`${BOLD}Secrets:${RESET} ${Object.keys(secretEntries).length}`);
|
|
2612
|
-
|
|
2613
|
-
const
|
|
2614
|
-
const valueSuffix = secretValue !== void 0 ? ` = ${YELLOW}${(opts?.secretDisplay ?? "encrypted") === "plaintext" ? secretValue : maskValue(secretValue)}${RESET}` : "";
|
|
2652
|
+
Object.entries(secretEntries).forEach(([key, meta]) => {
|
|
2653
|
+
const valueSuffix = Option(opts?.secrets?.[key]).fold(() => "", (secretValue) => ` = ${YELLOW}${(opts?.secretDisplay ?? "encrypted") === "plaintext" ? secretValue : maskValue(secretValue)}${RESET}`);
|
|
2615
2654
|
const sealedTag = meta.encrypted_value ? ` ${CYAN}[sealed]${RESET}` : "";
|
|
2616
2655
|
console.log(` ${BOLD}${key}${RESET} → ${meta.service ?? key}${sealedTag}${valueSuffix}`);
|
|
2617
2656
|
printSecretMeta(meta, " ");
|
|
2618
|
-
}
|
|
2657
|
+
});
|
|
2619
2658
|
const envEntries = config.env ?? {};
|
|
2620
2659
|
const envKeys = Object.keys(envEntries);
|
|
2621
2660
|
if (envKeys.length > 0) {
|
|
2622
2661
|
console.log("");
|
|
2623
2662
|
console.log(`${BOLD}Environment Defaults:${RESET} ${envKeys.length}`);
|
|
2624
|
-
|
|
2625
|
-
console.log(` ${BOLD}${key}${RESET} = "${entry.value}"`);
|
|
2626
|
-
if (entry.purpose) console.log(` purpose
|
|
2627
|
-
if (entry.comment) console.log(` comment
|
|
2628
|
-
}
|
|
2663
|
+
Object.entries(envEntries).forEach(([key, entry]) => {
|
|
2664
|
+
console.log(` ${BOLD}${key}${RESET} = ${GREEN}"${entry.value}"${RESET}`);
|
|
2665
|
+
if (entry.purpose) console.log(` ${DIM}purpose:${RESET} ${entry.purpose}`);
|
|
2666
|
+
if (entry.comment) console.log(` ${DIM}comment:${RESET} ${DIM}${entry.comment}${RESET}`);
|
|
2667
|
+
});
|
|
2629
2668
|
}
|
|
2630
2669
|
if (config.lifecycle) {
|
|
2631
2670
|
console.log("");
|
|
2632
2671
|
console.log(`${BOLD}Lifecycle:${RESET}`);
|
|
2633
|
-
if (config.lifecycle.stale_warning_days !== void 0) console.log(` stale_warning_days
|
|
2634
|
-
if (config.lifecycle.require_expiration !== void 0) console.log(` require_expiration
|
|
2635
|
-
if (config.lifecycle.require_service !== void 0) console.log(` require_service
|
|
2672
|
+
if (config.lifecycle.stale_warning_days !== void 0) console.log(` ${DIM}stale_warning_days:${RESET} ${YELLOW}${config.lifecycle.stale_warning_days}${RESET}`);
|
|
2673
|
+
if (config.lifecycle.require_expiration !== void 0) console.log(` ${DIM}require_expiration:${RESET} ${config.lifecycle.require_expiration ? `${GREEN}true${RESET}` : `${DIM}false${RESET}`}`);
|
|
2674
|
+
if (config.lifecycle.require_service !== void 0) console.log(` ${DIM}require_service:${RESET} ${config.lifecycle.require_service ? `${GREEN}true${RESET}` : `${DIM}false${RESET}`}`);
|
|
2636
2675
|
}
|
|
2637
2676
|
if (resolveResult?.catalogPath) {
|
|
2638
2677
|
console.log("");
|
|
2639
2678
|
console.log(`${BOLD}Catalog Resolution:${RESET}`);
|
|
2640
|
-
console.log(` merged
|
|
2641
|
-
if (resolveResult.overridden.length > 0) console.log(` overridden
|
|
2642
|
-
else console.log(`
|
|
2643
|
-
|
|
2679
|
+
console.log(` ${DIM}merged:${RESET} ${GREEN}${resolveResult.merged.length}${RESET} keys`);
|
|
2680
|
+
if (resolveResult.overridden.length > 0) console.log(` ${DIM}overridden:${RESET} ${resolveResult.overridden.map((k) => `${YELLOW}${k}${RESET}`).join(", ")}`);
|
|
2681
|
+
else console.log(` ${DIM}overridden: (none)${RESET}`);
|
|
2682
|
+
resolveResult.warnings.forEach((w) => {
|
|
2683
|
+
console.log(` ${YELLOW}warning:${RESET} ${w}`);
|
|
2684
|
+
});
|
|
2644
2685
|
}
|
|
2645
2686
|
};
|
|
2646
2687
|
const runInspect = (options) => {
|
|
@@ -2690,15 +2731,33 @@ const runInspect = (options) => {
|
|
|
2690
2731
|
};
|
|
2691
2732
|
//#endregion
|
|
2692
2733
|
//#region src/cli/commands/keygen.ts
|
|
2734
|
+
/** Shorten a path under $HOME to use ~ prefix */
|
|
2735
|
+
const tildeShorten = (p) => {
|
|
2736
|
+
const home = homedir();
|
|
2737
|
+
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
2738
|
+
};
|
|
2739
|
+
/** Derive a default identity name from the config path's parent directory */
|
|
2740
|
+
const deriveIdentityName = (configPath) => basename(dirname(resolve(configPath)));
|
|
2741
|
+
/**
|
|
2742
|
+
* Derive a project-specific key path from the config path.
|
|
2743
|
+
* - `envpkt.toml` → `~/.envpkt/<dir>-key.txt`
|
|
2744
|
+
* - `prod.envpkt.toml` → `~/.envpkt/<dir>-prod-key.txt`
|
|
2745
|
+
* - `foo.envpkt.toml` → `~/.envpkt/<dir>-foo-key.txt`
|
|
2746
|
+
*/
|
|
2747
|
+
const deriveKeyPath = (configPath) => {
|
|
2748
|
+
const abs = resolve(configPath);
|
|
2749
|
+
const dir = basename(dirname(abs));
|
|
2750
|
+
const stem = basename(abs).replace(/\.envpkt\.toml$/, "").replace(/\.toml$/, "");
|
|
2751
|
+
const name = stem === "envpkt" || stem === "" ? dir : `${dir}-${stem}`;
|
|
2752
|
+
return join(homedir(), ".envpkt", `${name}-key.txt`);
|
|
2753
|
+
};
|
|
2693
2754
|
const runKeygen = (options) => {
|
|
2694
|
-
const
|
|
2695
|
-
generateKeypair({
|
|
2696
|
-
force: options.force,
|
|
2697
|
-
outputPath
|
|
2698
|
-
}).fold((err) => {
|
|
2755
|
+
const configPath = resolve(options.config ?? join(process.cwd(), "envpkt.toml"));
|
|
2756
|
+
generateKeypair({ outputPath: options.output ?? (options.global ? resolveKeyPath() : deriveKeyPath(configPath)) }).fold((err) => {
|
|
2699
2757
|
if (err._tag === "KeyExists") {
|
|
2700
2758
|
console.error(`${YELLOW}Warning:${RESET} Identity file already exists: ${CYAN}${err.path}${RESET}`);
|
|
2701
|
-
console.error(`${DIM}
|
|
2759
|
+
console.error(`${DIM}To replace it: remove the file first, then re-run keygen.${RESET}`);
|
|
2760
|
+
console.error(`${DIM}To use a different path: pass -o <path>.${RESET}`);
|
|
2702
2761
|
process.exit(1);
|
|
2703
2762
|
}
|
|
2704
2763
|
console.error(formatError(err));
|
|
@@ -2707,16 +2766,24 @@ const runKeygen = (options) => {
|
|
|
2707
2766
|
console.log(`${GREEN}Generated${RESET} age identity: ${CYAN}${identityPath}${RESET}`);
|
|
2708
2767
|
console.log(`${BOLD}Recipient:${RESET} ${recipient}`);
|
|
2709
2768
|
console.log("");
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2769
|
+
if (existsSync(configPath)) {
|
|
2770
|
+
const name = deriveIdentityName(configPath);
|
|
2771
|
+
const keyFile = tildeShorten(identityPath);
|
|
2772
|
+
updateConfigIdentity(configPath, {
|
|
2773
|
+
recipient,
|
|
2774
|
+
name,
|
|
2775
|
+
keyFile
|
|
2776
|
+
}).fold((err) => {
|
|
2777
|
+
console.error(`${YELLOW}Warning:${RESET} Could not update config: ${"message" in err ? err.message : err._tag}`);
|
|
2778
|
+
console.log(`${DIM}Manually add to your envpkt.toml:${RESET}`);
|
|
2779
|
+
console.log(` [identity]`);
|
|
2780
|
+
console.log(` name = "${name}"`);
|
|
2781
|
+
console.log(` recipient = "${recipient}"`);
|
|
2782
|
+
console.log(` key_file = "${keyFile}"`);
|
|
2783
|
+
}, () => {
|
|
2784
|
+
console.log(`${GREEN}Updated${RESET} ${CYAN}${configPath}${RESET} with identity (name, recipient, key_file)`);
|
|
2785
|
+
});
|
|
2786
|
+
} else {
|
|
2720
2787
|
console.log(`${BOLD}Next steps:${RESET}`);
|
|
2721
2788
|
console.log(` ${DIM}1.${RESET} envpkt init ${DIM}# create envpkt.toml${RESET}`);
|
|
2722
2789
|
console.log(` ${DIM}2.${RESET} envpkt env scan --write ${DIM}# discover credentials${RESET}`);
|
|
@@ -2727,7 +2794,7 @@ const runKeygen = (options) => {
|
|
|
2727
2794
|
//#endregion
|
|
2728
2795
|
//#region src/mcp/resources.ts
|
|
2729
2796
|
const loadConfigSafe = () => {
|
|
2730
|
-
return resolveConfigPath().fold(() =>
|
|
2797
|
+
return resolveConfigPath().fold(() => Option.none(), ({ path }) => loadConfig(path).fold(() => Option.none(), (config) => Option({
|
|
2731
2798
|
config,
|
|
2732
2799
|
path
|
|
2733
2800
|
})));
|
|
@@ -2743,14 +2810,11 @@ const resourceDefinitions = [{
|
|
|
2743
2810
|
description: "Capabilities declared by the agent and per-secret capability grants",
|
|
2744
2811
|
mimeType: "application/json"
|
|
2745
2812
|
}];
|
|
2746
|
-
const readHealth = () => {
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
text: JSON.stringify({ error: "No envpkt.toml found" })
|
|
2752
|
-
}] };
|
|
2753
|
-
const { config, path } = loaded;
|
|
2813
|
+
const readHealth = () => loadConfigSafe().fold(() => ({ contents: [{
|
|
2814
|
+
uri: "envpkt://health",
|
|
2815
|
+
mimeType: "application/json",
|
|
2816
|
+
text: JSON.stringify({ error: "No envpkt.toml found" })
|
|
2817
|
+
}] }), ({ config, path }) => {
|
|
2754
2818
|
const audit = computeAudit(config);
|
|
2755
2819
|
return { contents: [{
|
|
2756
2820
|
uri: "envpkt://health",
|
|
@@ -2766,19 +2830,17 @@ const readHealth = () => {
|
|
|
2766
2830
|
missing: audit.missing
|
|
2767
2831
|
}, null, 2)
|
|
2768
2832
|
}] };
|
|
2769
|
-
};
|
|
2770
|
-
const readCapabilities = () => {
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
text: JSON.stringify({ error: "No envpkt.toml found" })
|
|
2776
|
-
}] };
|
|
2777
|
-
const { config } = loaded;
|
|
2833
|
+
});
|
|
2834
|
+
const readCapabilities = () => loadConfigSafe().fold(() => ({ contents: [{
|
|
2835
|
+
uri: "envpkt://capabilities",
|
|
2836
|
+
mimeType: "application/json",
|
|
2837
|
+
text: JSON.stringify({ error: "No envpkt.toml found" })
|
|
2838
|
+
}] }), ({ config }) => {
|
|
2778
2839
|
const agentCapabilities = config.identity?.capabilities ?? [];
|
|
2779
2840
|
const secretCapabilities = {};
|
|
2780
|
-
|
|
2781
|
-
|
|
2841
|
+
Object.entries(config.secret ?? {}).forEach(([key, meta]) => {
|
|
2842
|
+
if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
|
|
2843
|
+
});
|
|
2782
2844
|
return { contents: [{
|
|
2783
2845
|
uri: "envpkt://capabilities",
|
|
2784
2846
|
mimeType: "application/json",
|
|
@@ -2792,15 +2854,12 @@ const readCapabilities = () => {
|
|
|
2792
2854
|
secrets: secretCapabilities
|
|
2793
2855
|
}, null, 2)
|
|
2794
2856
|
}] };
|
|
2795
|
-
};
|
|
2857
|
+
});
|
|
2796
2858
|
const resourceHandlers = {
|
|
2797
2859
|
"envpkt://health": readHealth,
|
|
2798
2860
|
"envpkt://capabilities": readCapabilities
|
|
2799
2861
|
};
|
|
2800
|
-
const readResource = (uri) =>
|
|
2801
|
-
const handler = resourceHandlers[uri];
|
|
2802
|
-
return handler?.();
|
|
2803
|
-
};
|
|
2862
|
+
const readResource = (uri) => Option(resourceHandlers[uri]).map((handler) => handler());
|
|
2804
2863
|
//#endregion
|
|
2805
2864
|
//#region src/mcp/tools.ts
|
|
2806
2865
|
const textResult = (text) => ({ content: [{
|
|
@@ -2815,7 +2874,7 @@ const errorResult = (message) => ({
|
|
|
2815
2874
|
isError: true
|
|
2816
2875
|
});
|
|
2817
2876
|
const loadConfigForTool = (configPath) => {
|
|
2818
|
-
return resolveConfigPath(configPath).fold((err) => ({
|
|
2877
|
+
return resolveConfigPath(configPath.fold(() => void 0, (v) => v)).fold((err) => ({
|
|
2819
2878
|
ok: false,
|
|
2820
2879
|
result: errorResult(`Config error: ${err._tag} — ${err._tag === "FileNotFound" ? err.path : ""}`)
|
|
2821
2880
|
}), ({ path }) => loadConfig(path).fold((err) => ({
|
|
@@ -2898,8 +2957,9 @@ const toolDefinitions = [
|
|
|
2898
2957
|
}
|
|
2899
2958
|
}
|
|
2900
2959
|
];
|
|
2960
|
+
const configPathArg = (args) => Option(typeof args.configPath === "string" ? args.configPath : null);
|
|
2901
2961
|
const handleGetPacketHealth = (args) => {
|
|
2902
|
-
const loaded = loadConfigForTool(args
|
|
2962
|
+
const loaded = loadConfigForTool(configPathArg(args));
|
|
2903
2963
|
if (!loaded.ok) return loaded.result;
|
|
2904
2964
|
const { config, path } = loaded;
|
|
2905
2965
|
const audit = computeAudit(config);
|
|
@@ -2924,13 +2984,14 @@ const handleGetPacketHealth = (args) => {
|
|
|
2924
2984
|
}, null, 2));
|
|
2925
2985
|
};
|
|
2926
2986
|
const handleListCapabilities = (args) => {
|
|
2927
|
-
const loaded = loadConfigForTool(args
|
|
2987
|
+
const loaded = loadConfigForTool(configPathArg(args));
|
|
2928
2988
|
if (!loaded.ok) return loaded.result;
|
|
2929
2989
|
const { config } = loaded;
|
|
2930
2990
|
const agentCapabilities = config.identity?.capabilities ?? [];
|
|
2931
2991
|
const secretCapabilities = {};
|
|
2932
|
-
|
|
2933
|
-
|
|
2992
|
+
Object.entries(config.secret ?? {}).forEach(([key, meta]) => {
|
|
2993
|
+
if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
|
|
2994
|
+
});
|
|
2934
2995
|
return textResult(JSON.stringify({
|
|
2935
2996
|
identity: config.identity ? {
|
|
2936
2997
|
name: config.identity.name,
|
|
@@ -2945,21 +3006,21 @@ const handleListCapabilities = (args) => {
|
|
|
2945
3006
|
const handleGetSecretMeta = (args) => {
|
|
2946
3007
|
const key = args.key;
|
|
2947
3008
|
if (!key) return errorResult("Missing required argument: key");
|
|
2948
|
-
const loaded = loadConfigForTool(args
|
|
3009
|
+
const loaded = loadConfigForTool(configPathArg(args));
|
|
2949
3010
|
if (!loaded.ok) return loaded.result;
|
|
2950
3011
|
const { config } = loaded;
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
}
|
|
3012
|
+
return Option((config.secret ?? {})[key]).fold(() => errorResult(`Secret not found: ${key}`), (meta) => {
|
|
3013
|
+
const { encrypted_value: _, ...safeMeta } = meta;
|
|
3014
|
+
return textResult(JSON.stringify({
|
|
3015
|
+
key,
|
|
3016
|
+
...safeMeta
|
|
3017
|
+
}, null, 2));
|
|
3018
|
+
});
|
|
2958
3019
|
};
|
|
2959
3020
|
const handleCheckExpiration = (args) => {
|
|
2960
3021
|
const key = args.key;
|
|
2961
3022
|
if (!key) return errorResult("Missing required argument: key");
|
|
2962
|
-
const loaded = loadConfigForTool(args
|
|
3023
|
+
const loaded = loadConfigForTool(configPathArg(args));
|
|
2963
3024
|
if (!loaded.ok) return loaded.result;
|
|
2964
3025
|
const { config } = loaded;
|
|
2965
3026
|
return computeAudit(config).secrets.find((s) => s.key === key).fold(() => errorResult(`Secret not found: ${key}`), (s) => textResult(JSON.stringify({
|
|
@@ -2973,7 +3034,7 @@ const handleCheckExpiration = (args) => {
|
|
|
2973
3034
|
}, null, 2)));
|
|
2974
3035
|
};
|
|
2975
3036
|
const handleGetEnvMeta = (args) => {
|
|
2976
|
-
const loaded = loadConfigForTool(args
|
|
3037
|
+
const loaded = loadConfigForTool(configPathArg(args));
|
|
2977
3038
|
if (!loaded.ok) return loaded.result;
|
|
2978
3039
|
const { config } = loaded;
|
|
2979
3040
|
const envAudit = computeEnvAudit(config);
|
|
@@ -2986,11 +3047,7 @@ const handlers = {
|
|
|
2986
3047
|
checkExpiration: handleCheckExpiration,
|
|
2987
3048
|
getEnvMeta: handleGetEnvMeta
|
|
2988
3049
|
};
|
|
2989
|
-
const callTool = (name, args) => {
|
|
2990
|
-
const handler = handlers[name];
|
|
2991
|
-
if (!handler) return errorResult(`Unknown tool: ${name}`);
|
|
2992
|
-
return handler(args);
|
|
2993
|
-
};
|
|
3050
|
+
const callTool = (name, args) => Option(handlers[name]).fold(() => errorResult(`Unknown tool: ${name}`), (handler) => handler(args));
|
|
2994
3051
|
//#endregion
|
|
2995
3052
|
//#region src/mcp/server.ts
|
|
2996
3053
|
const createServer = () => {
|
|
@@ -3016,13 +3073,11 @@ const createServer = () => {
|
|
|
3016
3073
|
server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [...resourceDefinitions] }));
|
|
3017
3074
|
server.setRequestHandler(ReadResourceRequestSchema, (request) => {
|
|
3018
3075
|
const { uri } = request.params;
|
|
3019
|
-
|
|
3020
|
-
if (!result) return { contents: [{
|
|
3076
|
+
return readResource(uri).fold(() => ({ contents: [{
|
|
3021
3077
|
uri,
|
|
3022
3078
|
mimeType: "text/plain",
|
|
3023
3079
|
text: `Resource not found: ${uri}`
|
|
3024
|
-
}] };
|
|
3025
|
-
return result;
|
|
3080
|
+
}] }), (result) => result);
|
|
3026
3081
|
});
|
|
3027
3082
|
return server;
|
|
3028
3083
|
};
|
|
@@ -3066,8 +3121,10 @@ const runResolve = (options) => {
|
|
|
3066
3121
|
} else process.stdout.write(content);
|
|
3067
3122
|
if (result.catalogPath) {
|
|
3068
3123
|
const summaryTarget = options.output ? process.stdout : process.stderr;
|
|
3069
|
-
summaryTarget.write(`\n${CYAN}Catalog:${RESET} ${result.catalogPath}\n${GREEN}Merged:${RESET} ${result.merged.length} key(s)${result.overridden.length > 0 ? ` ${YELLOW}(${result.overridden.length} overridden: ${result.overridden.join(
|
|
3070
|
-
|
|
3124
|
+
summaryTarget.write(`\n${BOLD}${CYAN}Catalog:${RESET} ${BLUE}${result.catalogPath}${RESET}\n${GREEN}Merged:${RESET} ${BOLD}${result.merged.length}${RESET} key(s)${result.overridden.length > 0 ? ` ${YELLOW}(${result.overridden.length} overridden: ${BOLD}${result.overridden.join(`${RESET}${YELLOW}, ${BOLD}`)}${RESET}${YELLOW})${RESET}` : ""}\n`);
|
|
3125
|
+
result.warnings.forEach((w) => {
|
|
3126
|
+
summaryTarget.write(`${RED}Warning:${RESET} ${w}\n`);
|
|
3127
|
+
});
|
|
3071
3128
|
}
|
|
3072
3129
|
});
|
|
3073
3130
|
});
|
|
@@ -3080,18 +3137,20 @@ const resolveValues = async (keys, profile, agentKey) => {
|
|
|
3080
3137
|
const result = {};
|
|
3081
3138
|
const remaining = new Set(keys);
|
|
3082
3139
|
if (fnoxAvailable()) fnoxExport(profile, agentKey).fold(() => {}, (exported) => {
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3140
|
+
[...remaining].forEach((key) => {
|
|
3141
|
+
if (key in exported) {
|
|
3142
|
+
result[key] = exported[key];
|
|
3143
|
+
remaining.delete(key);
|
|
3144
|
+
}
|
|
3145
|
+
});
|
|
3087
3146
|
});
|
|
3088
|
-
|
|
3147
|
+
[...remaining].forEach((key) => {
|
|
3089
3148
|
const envValue = process.env[key];
|
|
3090
3149
|
if (envValue !== void 0 && envValue !== "") {
|
|
3091
3150
|
result[key] = envValue;
|
|
3092
3151
|
remaining.delete(key);
|
|
3093
3152
|
}
|
|
3094
|
-
}
|
|
3153
|
+
});
|
|
3095
3154
|
if (remaining.size > 0 && process.stdin.isTTY) {
|
|
3096
3155
|
const rl = createInterface({
|
|
3097
3156
|
input: process.stdin,
|
|
@@ -3114,53 +3173,55 @@ const resolveValues = async (keys, profile, agentKey) => {
|
|
|
3114
3173
|
const writeSealedToml = (configPath, sealedMeta) => {
|
|
3115
3174
|
const lines = readFileSync(configPath, "utf-8").split("\n");
|
|
3116
3175
|
const output = [];
|
|
3117
|
-
let currentMetaKey;
|
|
3176
|
+
let currentMetaKey = Option.none();
|
|
3118
3177
|
let insideMetaBlock = false;
|
|
3119
3178
|
let hasEncryptedValue = false;
|
|
3120
3179
|
const pendingSeals = /* @__PURE__ */ new Map();
|
|
3121
|
-
|
|
3180
|
+
Object.entries(sealedMeta).forEach(([key, meta]) => {
|
|
3181
|
+
if (meta.encrypted_value) pendingSeals.set(key, meta.encrypted_value);
|
|
3182
|
+
});
|
|
3122
3183
|
const metaSectionRe = /^\[secret\.(.+)\]\s*$/;
|
|
3123
3184
|
const encryptedValueRe = /^encrypted_value\s*=/;
|
|
3124
3185
|
const newSectionRe = /^\[/;
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
if (metaMatch) {
|
|
3129
|
-
if (currentMetaKey && !hasEncryptedValue && pendingSeals.has(currentMetaKey)) {
|
|
3186
|
+
const flushPending = () => {
|
|
3187
|
+
currentMetaKey.forEach((key) => {
|
|
3188
|
+
if (!hasEncryptedValue && pendingSeals.has(key)) {
|
|
3130
3189
|
output.push(`encrypted_value = """`);
|
|
3131
|
-
output.push(pendingSeals.get(
|
|
3190
|
+
output.push(pendingSeals.get(key));
|
|
3132
3191
|
output.push(`"""`);
|
|
3133
3192
|
output.push("");
|
|
3134
|
-
pendingSeals.delete(
|
|
3193
|
+
pendingSeals.delete(key);
|
|
3135
3194
|
}
|
|
3136
|
-
|
|
3195
|
+
});
|
|
3196
|
+
};
|
|
3197
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3198
|
+
const line = lines[i];
|
|
3199
|
+
const metaMatch = metaSectionRe.exec(line);
|
|
3200
|
+
if (metaMatch) {
|
|
3201
|
+
flushPending();
|
|
3202
|
+
currentMetaKey = Option(metaMatch[1]);
|
|
3137
3203
|
insideMetaBlock = true;
|
|
3138
3204
|
hasEncryptedValue = false;
|
|
3139
3205
|
output.push(line);
|
|
3140
3206
|
continue;
|
|
3141
3207
|
}
|
|
3142
3208
|
if (insideMetaBlock && newSectionRe.test(line) && !metaSectionRe.test(line)) {
|
|
3143
|
-
|
|
3144
|
-
output.push(`encrypted_value = """`);
|
|
3145
|
-
output.push(pendingSeals.get(currentMetaKey));
|
|
3146
|
-
output.push(`"""`);
|
|
3147
|
-
output.push("");
|
|
3148
|
-
pendingSeals.delete(currentMetaKey);
|
|
3149
|
-
}
|
|
3209
|
+
flushPending();
|
|
3150
3210
|
insideMetaBlock = false;
|
|
3151
|
-
currentMetaKey =
|
|
3211
|
+
currentMetaKey = Option.none();
|
|
3152
3212
|
output.push(line);
|
|
3153
3213
|
continue;
|
|
3154
3214
|
}
|
|
3155
3215
|
if (insideMetaBlock && encryptedValueRe.test(line)) {
|
|
3156
3216
|
hasEncryptedValue = true;
|
|
3157
|
-
const replacing =
|
|
3158
|
-
if (replacing) {
|
|
3217
|
+
const replacing = currentMetaKey.fold(() => false, (key) => pendingSeals.has(key));
|
|
3218
|
+
if (replacing) currentMetaKey.forEach((key) => {
|
|
3159
3219
|
output.push(`encrypted_value = """`);
|
|
3160
|
-
output.push(pendingSeals.get(
|
|
3220
|
+
output.push(pendingSeals.get(key));
|
|
3161
3221
|
output.push(`"""`);
|
|
3162
|
-
pendingSeals.delete(
|
|
3163
|
-
}
|
|
3222
|
+
pendingSeals.delete(key);
|
|
3223
|
+
});
|
|
3224
|
+
else output.push(line);
|
|
3164
3225
|
if (line.slice(line.indexOf("=") + 1).trim().includes("\"\"\"")) {
|
|
3165
3226
|
while (i + 1 < lines.length && !lines[i + 1].includes("\"\"\"")) {
|
|
3166
3227
|
if (!replacing) output.push(lines[i + 1]);
|
|
@@ -3175,12 +3236,7 @@ const writeSealedToml = (configPath, sealedMeta) => {
|
|
|
3175
3236
|
}
|
|
3176
3237
|
output.push(line);
|
|
3177
3238
|
}
|
|
3178
|
-
|
|
3179
|
-
output.push(`encrypted_value = """`);
|
|
3180
|
-
output.push(pendingSeals.get(currentMetaKey));
|
|
3181
|
-
output.push(`"""`);
|
|
3182
|
-
pendingSeals.delete(currentMetaKey);
|
|
3183
|
-
}
|
|
3239
|
+
flushPending();
|
|
3184
3240
|
writeFileSync(configPath, output.join("\n"));
|
|
3185
3241
|
};
|
|
3186
3242
|
const runSeal = async (options) => {
|
|
@@ -3217,10 +3273,13 @@ const runSeal = async (options) => {
|
|
|
3217
3273
|
console.error(`${DIM}Move these to [secret.*] only, or remove from [env.*] before sealing.${RESET}`);
|
|
3218
3274
|
process.exit(2);
|
|
3219
3275
|
}
|
|
3220
|
-
const identityKey =
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3276
|
+
const identityKey = Option(config.identity.key_file).flatMap((keyFile) => {
|
|
3277
|
+
return unwrapAgentKey(resolve(configDir, expandPath(keyFile))).fold((err) => {
|
|
3278
|
+
const msg = err._tag === "IdentityNotFound" ? `not found: ${err.path}` : err.message;
|
|
3279
|
+
console.error(`${YELLOW}Warning:${RESET} Could not unwrap agent key: ${msg}`);
|
|
3280
|
+
return Option.none();
|
|
3281
|
+
}, (k) => Option(k));
|
|
3282
|
+
});
|
|
3224
3283
|
const editKeys = options.edit ? options.edit.split(",").map((k) => k.trim()).filter((k) => k.length > 0) : [];
|
|
3225
3284
|
if (editKeys.length > 0) {
|
|
3226
3285
|
const allSecretEntries = config.secret ?? {};
|
|
@@ -3269,8 +3328,8 @@ const runSeal = async (options) => {
|
|
|
3269
3328
|
}
|
|
3270
3329
|
const allSecretEntries = config.secret ?? {};
|
|
3271
3330
|
const allKeys = Object.keys(allSecretEntries);
|
|
3272
|
-
const alreadySealed = allKeys.filter((k) => allSecretEntries[k]
|
|
3273
|
-
const unsealed = allKeys.filter((k) => !allSecretEntries[k]
|
|
3331
|
+
const alreadySealed = allKeys.filter((k) => allSecretEntries[k].encrypted_value);
|
|
3332
|
+
const unsealed = allKeys.filter((k) => !allSecretEntries[k].encrypted_value);
|
|
3274
3333
|
if (!options.reseal && alreadySealed.length > 0) {
|
|
3275
3334
|
if (unsealed.length === 0) {
|
|
3276
3335
|
console.log(`${GREEN}✓${RESET} All ${BOLD}${alreadySealed.length}${RESET} secret(s) already sealed. Use ${CYAN}--reseal${RESET} to re-encrypt.`);
|
|
@@ -3303,13 +3362,13 @@ const runSeal = async (options) => {
|
|
|
3303
3362
|
process.exit(2);
|
|
3304
3363
|
return {};
|
|
3305
3364
|
}, (d) => d);
|
|
3306
|
-
const newValues = unsealed.length > 0 ? await resolveValues(unsealed, options.profile, identityKey) : {};
|
|
3365
|
+
const newValues = unsealed.length > 0 ? await resolveValues(unsealed, options.profile, identityKey.orUndefined()) : {};
|
|
3307
3366
|
return {
|
|
3308
3367
|
...decrypted,
|
|
3309
3368
|
...newValues
|
|
3310
3369
|
};
|
|
3311
3370
|
}
|
|
3312
|
-
return resolveValues(metaKeys, options.profile, identityKey);
|
|
3371
|
+
return resolveValues(metaKeys, options.profile, identityKey.orUndefined());
|
|
3313
3372
|
})();
|
|
3314
3373
|
const resolved = Object.keys(values).length;
|
|
3315
3374
|
const skipped = metaKeys.length - resolved;
|
|
@@ -3382,7 +3441,7 @@ const buildFieldUpdates = (options) => {
|
|
|
3382
3441
|
return updates;
|
|
3383
3442
|
};
|
|
3384
3443
|
const withConfig = (configFlag, fn) => {
|
|
3385
|
-
resolveConfigPath(configFlag).fold((err) => {
|
|
3444
|
+
resolveConfigPath(configFlag.orUndefined()).fold((err) => {
|
|
3386
3445
|
console.error(formatError(err));
|
|
3387
3446
|
process.exit(2);
|
|
3388
3447
|
}, ({ path: configPath, source }) => {
|
|
@@ -3426,7 +3485,7 @@ const runSecretEdit = (name, options) => {
|
|
|
3426
3485
|
console.error(`${RED}Error:${RESET} Invalid date format for --expires: "${options.expires}" (expected YYYY-MM-DD)`);
|
|
3427
3486
|
process.exit(1);
|
|
3428
3487
|
}
|
|
3429
|
-
withConfig(options.config, (configPath, raw) => {
|
|
3488
|
+
withConfig(Option(options.config), (configPath, raw) => {
|
|
3430
3489
|
loadConfig(configPath).fold((err) => {
|
|
3431
3490
|
console.error(formatError(err));
|
|
3432
3491
|
process.exit(2);
|
|
@@ -3456,7 +3515,7 @@ const runSecretEdit = (name, options) => {
|
|
|
3456
3515
|
});
|
|
3457
3516
|
};
|
|
3458
3517
|
const runSecretRm = (name, options) => {
|
|
3459
|
-
withConfig(options.config, (configPath, raw) => {
|
|
3518
|
+
withConfig(Option(options.config), (configPath, raw) => {
|
|
3460
3519
|
removeSection(raw, `[secret.${name}]`).fold((err) => {
|
|
3461
3520
|
console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
|
|
3462
3521
|
process.exit(1);
|
|
@@ -3472,7 +3531,7 @@ const runSecretRm = (name, options) => {
|
|
|
3472
3531
|
});
|
|
3473
3532
|
};
|
|
3474
3533
|
const runSecretRename = (oldName, newName, options) => {
|
|
3475
|
-
withConfig(options.config, (configPath, raw) => {
|
|
3534
|
+
withConfig(Option(options.config), (configPath, raw) => {
|
|
3476
3535
|
renameSection(raw, `[secret.${oldName}]`, `[secret.${newName}]`).fold((err) => {
|
|
3477
3536
|
console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
|
|
3478
3537
|
process.exit(1);
|
|
@@ -3546,42 +3605,34 @@ const runShellHook = (shell) => {
|
|
|
3546
3605
|
};
|
|
3547
3606
|
//#endregion
|
|
3548
3607
|
//#region src/cli/commands/upgrade.ts
|
|
3549
|
-
const getCurrentVersion = () =>
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
]
|
|
3563
|
-
}).match(/"envpkt":\s*\{\s*"version":\s*"([^"]+)"/)?.[1] ?? "unknown";
|
|
3564
|
-
} catch {
|
|
3565
|
-
return "unknown";
|
|
3566
|
-
}
|
|
3567
|
-
};
|
|
3608
|
+
const getCurrentVersion = () => Try(() => execFileSync("npm", [
|
|
3609
|
+
"list",
|
|
3610
|
+
"-g",
|
|
3611
|
+
"envpkt",
|
|
3612
|
+
"--json"
|
|
3613
|
+
], {
|
|
3614
|
+
encoding: "utf-8",
|
|
3615
|
+
stdio: [
|
|
3616
|
+
"pipe",
|
|
3617
|
+
"pipe",
|
|
3618
|
+
"pipe"
|
|
3619
|
+
]
|
|
3620
|
+
})).fold(() => "unknown", (output) => output.match(/"envpkt":\s*\{\s*"version":\s*"([^"]+)"/)?.[1] ?? "unknown");
|
|
3568
3621
|
const runUpgrade = () => {
|
|
3569
3622
|
const before = getCurrentVersion();
|
|
3570
3623
|
console.log(`${DIM}Current version: ${before}${RESET}`);
|
|
3571
3624
|
console.log(`${CYAN}Upgrading envpkt...${RESET}\n`);
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
});
|
|
3582
|
-
} catch {
|
|
3625
|
+
Try(() => execFileSync("npm", [
|
|
3626
|
+
"install",
|
|
3627
|
+
"-g",
|
|
3628
|
+
"envpkt@latest",
|
|
3629
|
+
"--prefer-online"
|
|
3630
|
+
], {
|
|
3631
|
+
stdio: "inherit",
|
|
3632
|
+
encoding: "utf-8"
|
|
3633
|
+
})).fold(() => {
|
|
3583
3634
|
console.error(`\n${RED}Error:${RESET} npm install failed. Trying with cache clean...`);
|
|
3584
|
-
|
|
3635
|
+
Try(() => {
|
|
3585
3636
|
execFileSync("npm", [
|
|
3586
3637
|
"cache",
|
|
3587
3638
|
"clean",
|
|
@@ -3595,12 +3646,12 @@ const runUpgrade = () => {
|
|
|
3595
3646
|
stdio: "inherit",
|
|
3596
3647
|
encoding: "utf-8"
|
|
3597
3648
|
});
|
|
3598
|
-
}
|
|
3649
|
+
}).fold(() => {
|
|
3599
3650
|
console.error(`${RED}Error:${RESET} Upgrade failed. Try manually:`);
|
|
3600
3651
|
console.error(` ${BOLD}sudo npm install -g envpkt@latest --prefer-online${RESET}`);
|
|
3601
3652
|
process.exit(1);
|
|
3602
|
-
}
|
|
3603
|
-
}
|
|
3653
|
+
}, () => {});
|
|
3654
|
+
}, () => {});
|
|
3604
3655
|
const after = getCurrentVersion();
|
|
3605
3656
|
if (before === after && before !== "unknown") console.log(`\n${GREEN}✓${RESET} Already on latest version ${BOLD}${after}${RESET}`);
|
|
3606
3657
|
else console.log(`\n${GREEN}✓${RESET} Upgraded ${YELLOW}${before}${RESET} → ${BOLD}${after}${RESET}`);
|
|
@@ -3620,7 +3671,7 @@ program.name("envpkt").description("Credential lifecycle and fleet management fo
|
|
|
3620
3671
|
program.command("init").description("Initialize a new envpkt.toml in the current directory").option("--from-fnox [path]", "Scaffold from fnox.toml (optionally specify path)").option("--catalog <path>", "Path to shared secret catalog").option("--identity", "Include [identity] section").option("--name <name>", "Identity name (requires --identity)").option("--capabilities <caps>", "Comma-separated capabilities (requires --identity)").option("--expires <date>", "Credential expiration YYYY-MM-DD (requires --identity)").option("--force", "Overwrite existing envpkt.toml").action((options) => {
|
|
3621
3672
|
runInit(process.cwd(), options);
|
|
3622
3673
|
});
|
|
3623
|
-
program.command("keygen").description("Generate an age keypair for sealing secrets — run this before `seal` if you don't have a key yet").option("-c, --config <path>", "Path to envpkt.toml (updates identity.recipient if found)").option("
|
|
3674
|
+
program.command("keygen").description("Generate an age keypair for sealing secrets — run this before `seal` if you don't have a key yet").option("-c, --config <path>", "Path to envpkt.toml (updates identity.recipient if found)").option("-o, --output <path>", "Output path for identity file (default: ~/.envpkt/<project>-key.txt)").option("--global", "Write key to the shared default path (~/.envpkt/age-key.txt) instead of a project-specific one").action((options) => {
|
|
3624
3675
|
runKeygen(options);
|
|
3625
3676
|
});
|
|
3626
3677
|
program.command("audit").description("Audit credential health from envpkt.toml (use --strict in CI pipelines to gate deploys)").option("-c, --config <path>", "Path to envpkt.toml").option("--format <format>", "Output format: table | json | minimal", "table").option("--expiring <days>", "Show secrets expiring within N days", parseInt).option("--status <status>", "Filter by status: healthy | expiring_soon | expired | stale | missing").option("--strict", "Exit non-zero on any non-healthy secret").option("--all", "Show both secrets and env defaults").option("--env-only", "Show only env defaults (drift detection)").option("--sealed", "Show only secrets with encrypted_value").option("--external", "Show only secrets without encrypted_value").action((options) => {
|