envpkt 0.7.2 → 0.8.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/dist/cli.js +475 -470
- package/dist/index.d.ts +108 -99
- package/dist/index.js +292 -303
- package/package.json +27 -29
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";
|
|
@@ -14,7 +14,6 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
14
14
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
15
|
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
16
16
|
import { createInterface } from "node:readline";
|
|
17
|
-
|
|
18
17
|
//#region src/core/audit.ts
|
|
19
18
|
const MS_PER_DAY = 864e5;
|
|
20
19
|
const WARN_BEFORE_DAYS = 30;
|
|
@@ -25,17 +24,17 @@ const parseDate = (dateStr) => {
|
|
|
25
24
|
};
|
|
26
25
|
const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration, requireService, today) => {
|
|
27
26
|
const issues = [];
|
|
28
|
-
const created = Option(meta
|
|
29
|
-
const expires = Option(meta
|
|
30
|
-
const rotationUrl = Option(meta
|
|
31
|
-
const purpose = Option(meta
|
|
32
|
-
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);
|
|
33
32
|
const daysRemaining = expires.map((exp) => daysBetween(today, exp));
|
|
34
33
|
const daysSinceCreated = created.map((c) => daysBetween(c, today));
|
|
35
34
|
const isExpired = daysRemaining.fold(() => false, (d) => d < 0);
|
|
36
35
|
const isExpiringSoon = daysRemaining.fold(() => false, (d) => d >= 0 && d <= WARN_BEFORE_DAYS);
|
|
37
36
|
const isStale = daysSinceCreated.fold(() => false, (d) => d > staleWarningDays);
|
|
38
|
-
const hasSealed = !!meta
|
|
37
|
+
const hasSealed = !!meta.encrypted_value;
|
|
39
38
|
const isMissing = fnoxKeys.size > 0 && !fnoxKeys.has(key) && !hasSealed;
|
|
40
39
|
const isMissingMetadata = requireExpiration && expires.isNone() || requireService && service.isNone();
|
|
41
40
|
if (isExpired) issues.push("Secret has expired");
|
|
@@ -53,8 +52,8 @@ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration
|
|
|
53
52
|
days_remaining: daysRemaining,
|
|
54
53
|
rotation_url: rotationUrl,
|
|
55
54
|
purpose,
|
|
56
|
-
created: Option(meta
|
|
57
|
-
expires: Option(meta
|
|
55
|
+
created: Option(meta.created),
|
|
56
|
+
expires: Option(meta.expires),
|
|
58
57
|
issues: List(issues)
|
|
59
58
|
};
|
|
60
59
|
};
|
|
@@ -92,18 +91,17 @@ const computeAudit = (config, fnoxKeys, today) => {
|
|
|
92
91
|
};
|
|
93
92
|
const computeEnvAudit = (config, env = process.env) => {
|
|
94
93
|
const envEntries = config.env ?? {};
|
|
95
|
-
const entries = []
|
|
96
|
-
for (const [key, entry] of Object.entries(envEntries)) {
|
|
94
|
+
const entries = Object.entries(envEntries).map(([key, entry]) => {
|
|
97
95
|
const currentValue = env[key];
|
|
98
96
|
const status = Cond.of().when(currentValue === void 0, "missing").elseWhen(currentValue !== entry.value, "overridden").else("default");
|
|
99
|
-
|
|
97
|
+
return {
|
|
100
98
|
key,
|
|
101
99
|
defaultValue: entry.value,
|
|
102
100
|
currentValue,
|
|
103
101
|
status,
|
|
104
102
|
purpose: entry.purpose
|
|
105
|
-
}
|
|
106
|
-
}
|
|
103
|
+
};
|
|
104
|
+
});
|
|
107
105
|
return {
|
|
108
106
|
entries,
|
|
109
107
|
total: entries.length,
|
|
@@ -112,7 +110,6 @@ const computeEnvAudit = (config, env = process.env) => {
|
|
|
112
110
|
missing: entries.filter((e) => e.status === "missing").length
|
|
113
111
|
};
|
|
114
112
|
};
|
|
115
|
-
|
|
116
113
|
//#endregion
|
|
117
114
|
//#region src/core/schema.ts
|
|
118
115
|
const DATE_RE$1 = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -207,7 +204,6 @@ const EnvpktConfigSchema = Type.Object({
|
|
|
207
204
|
title: "envpkt configuration",
|
|
208
205
|
description: "Credential lifecycle and fleet management configuration for AI agents"
|
|
209
206
|
});
|
|
210
|
-
|
|
211
207
|
//#endregion
|
|
212
208
|
//#region src/core/config.ts
|
|
213
209
|
const CONFIG_FILENAME$2 = "envpkt.toml";
|
|
@@ -223,7 +219,7 @@ const normalizeDates = (obj) => {
|
|
|
223
219
|
/** Expand ~ and $ENV_VAR / ${ENV_VAR} in a path string (silent — unresolved vars become "") */
|
|
224
220
|
const expandPath = (p) => {
|
|
225
221
|
return Path.expandTilde(p).replace(/\$\{(\w+)\}|\$(\w+)/g, (_, braced, bare) => {
|
|
226
|
-
const name = braced
|
|
222
|
+
const name = Option(braced).fold(() => Option(bare).fold(() => "", (b) => b), (b) => b);
|
|
227
223
|
return Env.getOrDefault(name, "");
|
|
228
224
|
});
|
|
229
225
|
};
|
|
@@ -258,10 +254,9 @@ const ENV_FALLBACK_PATHS = [
|
|
|
258
254
|
];
|
|
259
255
|
/** Build discovery paths dynamically from Platform home and cloud storage detection */
|
|
260
256
|
const buildSearchPaths = () => {
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
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];
|
|
265
260
|
};
|
|
266
261
|
/** Discover config by checking CWD, then ENVPKT_SEARCH_PATH, then dynamic Platform paths */
|
|
267
262
|
const discoverConfig = (cwd) => {
|
|
@@ -270,28 +265,24 @@ const discoverConfig = (cwd) => {
|
|
|
270
265
|
path: cwdCandidate,
|
|
271
266
|
source: "cwd"
|
|
272
267
|
});
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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,
|
|
285
284
|
source: "search"
|
|
286
285
|
});
|
|
287
|
-
for (const template of ENV_FALLBACK_PATHS) {
|
|
288
|
-
const expanded = expandPath(template);
|
|
289
|
-
if (!expanded || expanded.startsWith("/.envpkt")) continue;
|
|
290
|
-
if (Fs.existsSync(expanded)) return Option({
|
|
291
|
-
path: expanded,
|
|
292
|
-
source: "search"
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
286
|
return Option(void 0);
|
|
296
287
|
};
|
|
297
288
|
/** Read a config file, returning Either<ConfigError, string> */
|
|
@@ -368,7 +359,6 @@ const resolveConfigPath = (flagPath, envVar, cwd) => {
|
|
|
368
359
|
source
|
|
369
360
|
}));
|
|
370
361
|
};
|
|
371
|
-
|
|
372
362
|
//#endregion
|
|
373
363
|
//#region src/core/catalog.ts
|
|
374
364
|
/** Load and validate a catalog file, mapping ConfigError → CatalogError */
|
|
@@ -384,22 +374,21 @@ const loadCatalog = (catalogPath) => loadConfig(catalogPath).fold((err) => {
|
|
|
384
374
|
}, (config) => Right(config));
|
|
385
375
|
/** Resolve secrets by merging catalog meta with agent overrides (shallow merge) */
|
|
386
376
|
const resolveSecrets = (agentMeta, catalogMeta, agentSecrets, catalogPath) => {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const catalogEntry = catalogMeta[key];
|
|
390
|
-
if (!catalogEntry) return Left({
|
|
377
|
+
return agentSecrets.reduce((acc, key) => acc.flatMap((resolved) => {
|
|
378
|
+
if (!(key in catalogMeta)) return Left({
|
|
391
379
|
_tag: "SecretNotInCatalog",
|
|
392
380
|
key,
|
|
393
381
|
catalogPath
|
|
394
382
|
});
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
...
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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({}));
|
|
403
392
|
};
|
|
404
393
|
/** Resolve an agent config against its catalog (if any), producing a flat self-contained config */
|
|
405
394
|
const resolveConfig = (agentConfig, agentConfigDir) => {
|
|
@@ -417,13 +406,9 @@ const resolveConfig = (agentConfig, agentConfigDir) => {
|
|
|
417
406
|
const agentSecrets = agentConfig.identity.secrets;
|
|
418
407
|
const agentSecretEntries = agentConfig.secret ?? {};
|
|
419
408
|
return loadCatalog(catalogPath).flatMap((catalogConfig) => resolveSecrets(agentSecretEntries, catalogConfig.secret ?? {}, agentSecrets, catalogPath).map((resolvedMeta) => {
|
|
420
|
-
const merged = [];
|
|
421
|
-
const overridden =
|
|
409
|
+
const merged = [...agentSecrets];
|
|
410
|
+
const overridden = agentSecrets.filter((key) => key in agentSecretEntries);
|
|
422
411
|
const warnings = [];
|
|
423
|
-
for (const key of agentSecrets) {
|
|
424
|
-
merged.push(key);
|
|
425
|
-
if (agentSecretEntries[key]) overridden.push(key);
|
|
426
|
-
}
|
|
427
412
|
const { catalog: _catalog, ...agentWithoutCatalog } = agentConfig;
|
|
428
413
|
const identityData = agentConfig.identity ? (() => {
|
|
429
414
|
const { secrets: _secrets, ...rest } = agentConfig.identity;
|
|
@@ -445,7 +430,6 @@ const resolveConfig = (agentConfig, agentConfigDir) => {
|
|
|
445
430
|
};
|
|
446
431
|
}));
|
|
447
432
|
};
|
|
448
|
-
|
|
449
433
|
//#endregion
|
|
450
434
|
//#region src/cli/output.ts
|
|
451
435
|
const RESET = "\x1B[0m";
|
|
@@ -454,6 +438,8 @@ const DIM = "\x1B[2m";
|
|
|
454
438
|
const RED = "\x1B[31m";
|
|
455
439
|
const GREEN = "\x1B[32m";
|
|
456
440
|
const YELLOW = "\x1B[33m";
|
|
441
|
+
const BLUE = "\x1B[34m";
|
|
442
|
+
const MAGENTA = "\x1B[35m";
|
|
457
443
|
const CYAN = "\x1B[36m";
|
|
458
444
|
const statusColor = (status) => {
|
|
459
445
|
switch (status) {
|
|
@@ -652,7 +638,6 @@ const formatConfigSource = (path, source) => {
|
|
|
652
638
|
if (source === "cwd") return "";
|
|
653
639
|
return `${DIM}envpkt: loaded ${path}${RESET}`;
|
|
654
640
|
};
|
|
655
|
-
|
|
656
641
|
//#endregion
|
|
657
642
|
//#region src/cli/commands/audit.ts
|
|
658
643
|
const runAudit = (options) => {
|
|
@@ -683,11 +668,11 @@ const formatEnvAuditTable = (config) => {
|
|
|
683
668
|
return;
|
|
684
669
|
}
|
|
685
670
|
console.log(`\n${BOLD}Environment Defaults${RESET} (${envAudit.total} entries)`);
|
|
686
|
-
|
|
671
|
+
envAudit.entries.forEach((entry) => {
|
|
687
672
|
const statusIcon = entry.status === "default" ? `${GREEN}=${RESET}` : entry.status === "overridden" ? `${YELLOW}~${RESET}` : `${RED}!${RESET}`;
|
|
688
673
|
const statusLabel = entry.status === "default" ? `${DIM}using default${RESET}` : entry.status === "overridden" ? `${YELLOW}overridden${RESET} (${entry.currentValue})` : `${RED}not set${RESET}`;
|
|
689
674
|
console.log(` ${statusIcon} ${BOLD}${entry.key}${RESET} = "${entry.defaultValue}" ${statusLabel}`);
|
|
690
|
-
}
|
|
675
|
+
});
|
|
691
676
|
};
|
|
692
677
|
const formatEnvAuditJson = (config) => {
|
|
693
678
|
const envAudit = computeEnvAudit(config);
|
|
@@ -705,24 +690,24 @@ const runAuditOnConfig = (config, options) => {
|
|
|
705
690
|
const secretEntries = config.secret ?? {};
|
|
706
691
|
return {
|
|
707
692
|
...audit,
|
|
708
|
-
secrets: audit.secrets.filter((s) => !!secretEntries[s.key]
|
|
693
|
+
secrets: audit.secrets.filter((s) => !!secretEntries[s.key].encrypted_value)
|
|
709
694
|
};
|
|
710
695
|
})() : audit;
|
|
711
696
|
const afterExternal = options.external ? (() => {
|
|
712
697
|
const secretEntries = config.secret ?? {};
|
|
713
698
|
return {
|
|
714
699
|
...afterSealed,
|
|
715
|
-
secrets: afterSealed.secrets.filter((s) => !secretEntries[s.key]
|
|
700
|
+
secrets: afterSealed.secrets.filter((s) => !secretEntries[s.key].encrypted_value)
|
|
716
701
|
};
|
|
717
702
|
})() : afterSealed;
|
|
718
703
|
const afterStatus = options.status ? {
|
|
719
704
|
...afterExternal,
|
|
720
705
|
secrets: afterExternal.secrets.filter((s) => s.status === options.status)
|
|
721
706
|
} : afterExternal;
|
|
722
|
-
const filtered = options.expiring
|
|
707
|
+
const filtered = Option(options.expiring).fold(() => afterStatus, (expiring) => ({
|
|
723
708
|
...afterStatus,
|
|
724
|
-
secrets: afterStatus.secrets.filter((s) => s.days_remaining.fold(() => false, (d) => d >= 0 && d <=
|
|
725
|
-
}
|
|
709
|
+
secrets: afterStatus.secrets.filter((s) => s.days_remaining.fold(() => false, (d) => d >= 0 && d <= expiring))
|
|
710
|
+
}));
|
|
726
711
|
if (options.format === "json") console.log(formatAuditJson(filtered));
|
|
727
712
|
else if (options.format === "minimal") console.log(formatAuditMinimal(filtered));
|
|
728
713
|
else console.log(formatAudit(filtered));
|
|
@@ -731,7 +716,6 @@ const runAuditOnConfig = (config, options) => {
|
|
|
731
716
|
const code = options.strict ? exitCodeForAudit(audit) : audit.status === "critical" ? 2 : 0;
|
|
732
717
|
process.exit(code);
|
|
733
718
|
};
|
|
734
|
-
|
|
735
719
|
//#endregion
|
|
736
720
|
//#region src/fnox/cli.ts
|
|
737
721
|
/** Export all secrets from fnox as key=value pairs for a given profile */
|
|
@@ -754,17 +738,16 @@ const fnoxExport = (profile, agentKey) => {
|
|
|
754
738
|
message: `fnox export failed: ${err}`
|
|
755
739
|
}), (output) => {
|
|
756
740
|
const entries = {};
|
|
757
|
-
|
|
741
|
+
output.split("\n").forEach((line) => {
|
|
758
742
|
const eq = line.indexOf("=");
|
|
759
743
|
if (eq > 0) {
|
|
760
744
|
const key = line.slice(0, eq).trim();
|
|
761
745
|
entries[key] = line.slice(eq + 1).trim();
|
|
762
746
|
}
|
|
763
|
-
}
|
|
747
|
+
});
|
|
764
748
|
return Right(entries);
|
|
765
749
|
});
|
|
766
750
|
};
|
|
767
|
-
|
|
768
751
|
//#endregion
|
|
769
752
|
//#region src/fnox/detect.ts
|
|
770
753
|
const FNOX_CONFIG = "fnox.toml";
|
|
@@ -778,7 +761,6 @@ const fnoxAvailable = () => Try(() => {
|
|
|
778
761
|
execFileSync("fnox", ["--version"], { stdio: "pipe" });
|
|
779
762
|
return true;
|
|
780
763
|
}).fold(() => false, (v) => v);
|
|
781
|
-
|
|
782
764
|
//#endregion
|
|
783
765
|
//#region src/fnox/identity.ts
|
|
784
766
|
/** Check if the age CLI is available on PATH */
|
|
@@ -786,29 +768,39 @@ const ageAvailable = () => Try(() => {
|
|
|
786
768
|
execFileSync("age", ["--version"], { stdio: "pipe" });
|
|
787
769
|
return true;
|
|
788
770
|
}).fold(() => false, (v) => v);
|
|
789
|
-
/**
|
|
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
|
+
*/
|
|
790
776
|
const unwrapAgentKey = (identityPath) => {
|
|
791
777
|
if (!existsSync(identityPath)) return Left({
|
|
792
778
|
_tag: "IdentityNotFound",
|
|
793
779
|
path: identityPath
|
|
794
780
|
});
|
|
795
|
-
|
|
796
|
-
_tag: "AgeNotFound",
|
|
797
|
-
message: "age CLI not found on PATH"
|
|
798
|
-
});
|
|
799
|
-
return Try(() => execFileSync("age", ["--decrypt", identityPath], {
|
|
800
|
-
stdio: [
|
|
801
|
-
"pipe",
|
|
802
|
-
"pipe",
|
|
803
|
-
"pipe"
|
|
804
|
-
],
|
|
805
|
-
encoding: "utf-8"
|
|
806
|
-
})).fold((err) => Left({
|
|
781
|
+
return Try(() => readFileSync(identityPath, "utf-8")).fold((err) => Left({
|
|
807
782
|
_tag: "DecryptFailed",
|
|
808
|
-
message: `
|
|
809
|
-
}), (
|
|
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
|
+
});
|
|
810
803
|
};
|
|
811
|
-
|
|
812
804
|
//#endregion
|
|
813
805
|
//#region src/fnox/parse.ts
|
|
814
806
|
/** Read and parse fnox.toml, extracting secret keys and profiles */
|
|
@@ -829,7 +821,6 @@ const readFnoxConfig = (path) => Try(() => readFileSync(path, "utf-8")).fold((er
|
|
|
829
821
|
}));
|
|
830
822
|
/** Extract the set of secret key names from a parsed fnox config */
|
|
831
823
|
const extractFnoxKeys = (config) => new Set(Object.keys(config.secrets));
|
|
832
|
-
|
|
833
824
|
//#endregion
|
|
834
825
|
//#region src/core/keygen.ts
|
|
835
826
|
/** Resolve the age identity file path: ENVPKT_AGE_KEY_FILE env var > ~/.envpkt/age-key.txt */
|
|
@@ -883,53 +874,83 @@ const generateKeypair = (options) => {
|
|
|
883
874
|
}));
|
|
884
875
|
});
|
|
885
876
|
};
|
|
886
|
-
/** Update identity
|
|
887
|
-
const
|
|
888
|
-
|
|
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({
|
|
889
895
|
_tag: "ConfigUpdateError",
|
|
890
896
|
message: `Failed to read config: ${err}`
|
|
891
897
|
}), (raw) => {
|
|
892
898
|
const lines = raw.split("\n");
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
output.push(line);
|
|
902
|
-
continue;
|
|
903
|
-
}
|
|
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
|
+
};
|
|
904
907
|
if (/^\[/.test(line) && !/^\[identity\]\s*$/.test(line)) {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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
|
+
};
|
|
912
919
|
}
|
|
913
|
-
if (inIdentitySection
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
+
}
|
|
917
929
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
output
|
|
924
|
-
|
|
925
|
-
|
|
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;
|
|
926
948
|
return Try(() => writeFileSync(configPath, output.join("\n"))).fold((err) => Left({
|
|
927
949
|
_tag: "ConfigUpdateError",
|
|
928
950
|
message: `Failed to write config: ${err}`
|
|
929
951
|
}), () => Right(true));
|
|
930
952
|
});
|
|
931
953
|
};
|
|
932
|
-
|
|
933
954
|
//#endregion
|
|
934
955
|
//#region src/core/seal.ts
|
|
935
956
|
/** Encrypt a plaintext string using age with the given recipient public key (armored output) */
|
|
@@ -987,27 +1008,23 @@ const sealSecrets = (meta, values, recipient) => {
|
|
|
987
1008
|
_tag: "AgeNotFound",
|
|
988
1009
|
message: "age CLI not found on PATH"
|
|
989
1010
|
});
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
}
|
|
997
|
-
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) => ({
|
|
998
1017
|
_tag: "EncryptFailed",
|
|
999
1018
|
key,
|
|
1000
1019
|
message: err.message
|
|
1001
|
-
})
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
}
|
|
1010
|
-
return Right(result);
|
|
1020
|
+
})).map((ciphertext) => ({
|
|
1021
|
+
...result,
|
|
1022
|
+
[key]: {
|
|
1023
|
+
...secretMeta,
|
|
1024
|
+
encrypted_value: ciphertext
|
|
1025
|
+
}
|
|
1026
|
+
}));
|
|
1027
|
+
}), Right({}));
|
|
1011
1028
|
};
|
|
1012
1029
|
/** Unseal secrets: decrypt encrypted_value for each meta entry that has one */
|
|
1013
1030
|
const unsealSecrets = (meta, identityPath) => {
|
|
@@ -1015,21 +1032,15 @@ const unsealSecrets = (meta, identityPath) => {
|
|
|
1015
1032
|
_tag: "AgeNotFound",
|
|
1016
1033
|
message: "age CLI not found on PATH"
|
|
1017
1034
|
});
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
const failed = outcome.fold((err) => err, () => void 0);
|
|
1027
|
-
if (failed) return Left(failed);
|
|
1028
|
-
result[key] = outcome.fold(() => "", (v) => v);
|
|
1029
|
-
}
|
|
1030
|
-
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({}));
|
|
1031
1043
|
};
|
|
1032
|
-
|
|
1033
1044
|
//#endregion
|
|
1034
1045
|
//#region src/core/boot.ts
|
|
1035
1046
|
const resolveAndLoad = (opts) => resolveConfigPath(opts.configPath).fold((err) => Left(err), ({ path: configPath, source: configSource }) => loadConfig(configPath).fold((err) => Left(err), (config) => {
|
|
@@ -1043,15 +1054,13 @@ const resolveAndLoad = (opts) => resolveConfigPath(opts.configPath).fold((err) =
|
|
|
1043
1054
|
}));
|
|
1044
1055
|
/** Resolve identity file path with explicit fallback control */
|
|
1045
1056
|
const resolveIdentityFilePath = (config, configDir, useDefaultFallback) => {
|
|
1046
|
-
if (config.identity?.key_file) return resolve(configDir, expandPath(config.identity.key_file));
|
|
1047
|
-
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);
|
|
1048
1059
|
const defaultPath = resolveKeyPath();
|
|
1049
|
-
return existsSync(defaultPath) ? defaultPath : void 0;
|
|
1060
|
+
return existsSync(defaultPath) ? Option(defaultPath) : Option(void 0);
|
|
1050
1061
|
};
|
|
1051
1062
|
const resolveIdentityKey = (config, configDir) => {
|
|
1052
|
-
|
|
1053
|
-
if (!identityPath) return Right(void 0);
|
|
1054
|
-
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))));
|
|
1055
1064
|
};
|
|
1056
1065
|
const detectFnoxKeys = (configDir) => detectFnox(configDir).fold(() => /* @__PURE__ */ new Set(), (fnoxPath) => readFnoxConfig(fnoxPath).fold(() => /* @__PURE__ */ new Set(), (fnoxConfig) => extractFnoxKeys(fnoxConfig)));
|
|
1057
1066
|
const checkExpiration = (audit, failOnExpired, warnOnly) => {
|
|
@@ -1079,10 +1088,8 @@ const looksLikeSecret = (value) => {
|
|
|
1079
1088
|
return false;
|
|
1080
1089
|
};
|
|
1081
1090
|
const checkEnvMisclassification = (config) => {
|
|
1082
|
-
const warnings = [];
|
|
1083
1091
|
const envEntries = config.env ?? {};
|
|
1084
|
-
|
|
1085
|
-
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}]`);
|
|
1086
1093
|
};
|
|
1087
1094
|
/** Programmatic boot — returns Either<BootError, BootResult> */
|
|
1088
1095
|
const bootSafe = (options) => {
|
|
@@ -1093,11 +1100,13 @@ const bootSafe = (options) => {
|
|
|
1093
1100
|
return resolveAndLoad(opts).flatMap(({ config, configPath, configDir, configSource }) => {
|
|
1094
1101
|
const secretEntries = config.secret ?? {};
|
|
1095
1102
|
const metaKeys = Object.keys(secretEntries);
|
|
1096
|
-
const hasSealedValues = metaKeys.some((k) => !!secretEntries[k]
|
|
1103
|
+
const hasSealedValues = metaKeys.some((k) => !!secretEntries[k].encrypted_value);
|
|
1097
1104
|
const identityKeyResult = resolveIdentityKey(config, configDir);
|
|
1098
|
-
const identityKey = identityKeyResult.fold(() => void 0, (k) => k);
|
|
1099
|
-
|
|
1100
|
-
|
|
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
|
+
}));
|
|
1101
1110
|
const audit = computeAudit(config, detectFnoxKeys(configDir));
|
|
1102
1111
|
return checkExpiration(audit, failOnExpired, warnOnly).map((warnings) => {
|
|
1103
1112
|
const secrets = {};
|
|
@@ -1105,40 +1114,47 @@ const bootSafe = (options) => {
|
|
|
1105
1114
|
const skipped = [];
|
|
1106
1115
|
warnings.push(...checkEnvMisclassification(config));
|
|
1107
1116
|
const envEntries = config.env ?? {};
|
|
1108
|
-
const
|
|
1109
|
-
const
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
}
|
|
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
|
+
});
|
|
1114
1123
|
const sealedKeys = /* @__PURE__ */ new Set();
|
|
1115
1124
|
const identityFilePath = resolveIdentityFilePath(config, configDir, true);
|
|
1116
|
-
if (hasSealedValues
|
|
1117
|
-
warnings.push(
|
|
1118
|
-
}, (
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
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
|
+
});
|
|
1124
1136
|
});
|
|
1125
|
-
else if (hasSealedValues && !identityFilePath) warnings.push("Sealed values found but no identity file available for decryption");
|
|
1126
1137
|
const remainingKeys = metaKeys.filter((k) => !sealedKeys.has(k));
|
|
1127
|
-
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) => {
|
|
1128
1139
|
warnings.push(`fnox export failed: ${err.message}`);
|
|
1129
|
-
|
|
1140
|
+
skipped.push(...remainingKeys);
|
|
1130
1141
|
}, (exported) => {
|
|
1131
|
-
|
|
1142
|
+
const found = remainingKeys.filter((key) => key in exported);
|
|
1143
|
+
const notFound = remainingKeys.filter((key) => !(key in exported));
|
|
1144
|
+
found.forEach((key) => {
|
|
1132
1145
|
secrets[key] = exported[key];
|
|
1133
|
-
|
|
1134
|
-
|
|
1146
|
+
});
|
|
1147
|
+
injected.push(...found);
|
|
1148
|
+
skipped.push(...notFound);
|
|
1135
1149
|
});
|
|
1136
1150
|
else {
|
|
1137
1151
|
if (!hasSealedValues) warnings.push("fnox not available — no secrets injected");
|
|
1138
1152
|
else warnings.push("fnox not available — unsealed secrets could not be resolved");
|
|
1139
|
-
|
|
1153
|
+
skipped.push(...remainingKeys);
|
|
1140
1154
|
}
|
|
1141
|
-
if (inject)
|
|
1155
|
+
if (inject) Object.entries(secrets).forEach(([key, value]) => {
|
|
1156
|
+
process.env[key] = value;
|
|
1157
|
+
});
|
|
1142
1158
|
return {
|
|
1143
1159
|
audit,
|
|
1144
1160
|
injected,
|
|
@@ -1153,7 +1169,6 @@ const bootSafe = (options) => {
|
|
|
1153
1169
|
});
|
|
1154
1170
|
});
|
|
1155
1171
|
};
|
|
1156
|
-
|
|
1157
1172
|
//#endregion
|
|
1158
1173
|
//#region src/core/patterns.ts
|
|
1159
1174
|
const EXCLUDED_VARS = new Set([
|
|
@@ -1777,11 +1792,10 @@ const VALUE_SHAPE_PATTERNS = [
|
|
|
1777
1792
|
];
|
|
1778
1793
|
/** Detect service from value prefix/shape */
|
|
1779
1794
|
const matchValueShape = (value) => {
|
|
1780
|
-
|
|
1795
|
+
return Option(VALUE_SHAPE_PATTERNS.find((vp) => value.startsWith(vp.prefix))).map((vp) => ({
|
|
1781
1796
|
service: vp.service,
|
|
1782
1797
|
description: vp.description
|
|
1783
|
-
});
|
|
1784
|
-
return Option(void 0);
|
|
1798
|
+
}));
|
|
1785
1799
|
};
|
|
1786
1800
|
/** Strip common suffixes and derive a service name from an env var name */
|
|
1787
1801
|
const deriveServiceFromName = (name) => {
|
|
@@ -1809,22 +1823,22 @@ const deriveServiceFromName = (name) => {
|
|
|
1809
1823
|
/** Match a single env var against all patterns */
|
|
1810
1824
|
const matchEnvVar = (name, value) => {
|
|
1811
1825
|
if (EXCLUDED_VARS.has(name)) return Option(void 0);
|
|
1812
|
-
|
|
1826
|
+
const exactMatch = EXACT_NAME_PATTERNS.find((p) => name === p.pattern);
|
|
1827
|
+
if (exactMatch) return Option({
|
|
1813
1828
|
envVar: name,
|
|
1814
1829
|
value,
|
|
1815
|
-
service: Option(
|
|
1816
|
-
confidence:
|
|
1817
|
-
matchedBy: `exact:${
|
|
1830
|
+
service: Option(exactMatch.service),
|
|
1831
|
+
confidence: exactMatch.confidence,
|
|
1832
|
+
matchedBy: `exact:${exactMatch.pattern}`
|
|
1818
1833
|
});
|
|
1819
1834
|
return matchValueShape(value).fold(() => {
|
|
1820
|
-
|
|
1835
|
+
return Option(SUFFIX_PATTERNS.find((sp) => name.endsWith(sp.suffix))).map((sp) => ({
|
|
1821
1836
|
envVar: name,
|
|
1822
1837
|
value,
|
|
1823
1838
|
service: Option(deriveServiceFromName(name)),
|
|
1824
1839
|
confidence: "medium",
|
|
1825
1840
|
matchedBy: `suffix:${sp.suffix}`
|
|
1826
|
-
});
|
|
1827
|
-
return Option(void 0);
|
|
1841
|
+
}));
|
|
1828
1842
|
}, (vm) => Option({
|
|
1829
1843
|
envVar: name,
|
|
1830
1844
|
value,
|
|
@@ -1835,11 +1849,10 @@ const matchEnvVar = (name, value) => {
|
|
|
1835
1849
|
};
|
|
1836
1850
|
/** Scan full env, sorted by confidence (high first) then alphabetically */
|
|
1837
1851
|
const scanEnv = (env) => {
|
|
1838
|
-
const results = []
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
}
|
|
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
|
+
});
|
|
1843
1856
|
const confidenceOrder = {
|
|
1844
1857
|
high: 0,
|
|
1845
1858
|
medium: 1,
|
|
@@ -1852,7 +1865,6 @@ const scanEnv = (env) => {
|
|
|
1852
1865
|
});
|
|
1853
1866
|
return results;
|
|
1854
1867
|
};
|
|
1855
|
-
|
|
1856
1868
|
//#endregion
|
|
1857
1869
|
//#region src/core/env.ts
|
|
1858
1870
|
/** Scan env for credentials, returning structured results */
|
|
@@ -1873,38 +1885,44 @@ const envScan = (env, options) => {
|
|
|
1873
1885
|
};
|
|
1874
1886
|
/** Bidirectional drift detection between config and live environment */
|
|
1875
1887
|
const envCheck = (config, env) => {
|
|
1876
|
-
const entries = [];
|
|
1877
1888
|
const secretEntries = config.secret ?? {};
|
|
1878
1889
|
const metaKeys = Object.keys(secretEntries);
|
|
1879
1890
|
const trackedSet = new Set(metaKeys);
|
|
1880
|
-
|
|
1891
|
+
const secretDriftEntries = metaKeys.map((key) => {
|
|
1881
1892
|
const meta = secretEntries[key];
|
|
1882
1893
|
const present = env[key] !== void 0 && env[key] !== "";
|
|
1883
|
-
|
|
1894
|
+
return {
|
|
1884
1895
|
envVar: key,
|
|
1885
|
-
service: Option(meta
|
|
1896
|
+
service: Option(meta.service),
|
|
1886
1897
|
status: present ? "tracked" : "missing_from_env",
|
|
1887
1898
|
confidence: Option(void 0)
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1899
|
+
};
|
|
1900
|
+
});
|
|
1890
1901
|
const envDefaults = config.env ?? {};
|
|
1891
|
-
|
|
1902
|
+
const envDefaultEntries = Object.keys(envDefaults).filter((key) => {
|
|
1903
|
+
if (trackedSet.has(key)) return false;
|
|
1892
1904
|
trackedSet.add(key);
|
|
1905
|
+
return true;
|
|
1906
|
+
}).map((key) => {
|
|
1893
1907
|
const present = env[key] !== void 0 && env[key] !== "";
|
|
1894
|
-
|
|
1908
|
+
return {
|
|
1895
1909
|
envVar: key,
|
|
1896
1910
|
service: Option(void 0),
|
|
1897
1911
|
status: present ? "tracked" : "missing_from_env",
|
|
1898
1912
|
confidence: Option(void 0)
|
|
1899
|
-
}
|
|
1900
|
-
}
|
|
1901
|
-
const
|
|
1902
|
-
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) => ({
|
|
1903
1916
|
envVar: match.envVar,
|
|
1904
1917
|
service: match.service,
|
|
1905
1918
|
status: "untracked",
|
|
1906
1919
|
confidence: Option(match.confidence)
|
|
1907
|
-
});
|
|
1920
|
+
}));
|
|
1921
|
+
const entries = [
|
|
1922
|
+
...secretDriftEntries,
|
|
1923
|
+
...envDefaultEntries,
|
|
1924
|
+
...untrackedEntries
|
|
1925
|
+
];
|
|
1908
1926
|
const tracked_and_present = entries.filter((e) => e.status === "tracked").length;
|
|
1909
1927
|
const missing_from_env = entries.filter((e) => e.status === "missing_from_env").length;
|
|
1910
1928
|
const untracked_credentials = entries.filter((e) => e.status === "untracked").length;
|
|
@@ -1919,10 +1937,9 @@ const envCheck = (config, env) => {
|
|
|
1919
1937
|
const todayIso$1 = () => (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1920
1938
|
/** Generate TOML [secret.*] blocks from scan results, mirroring init.ts pattern */
|
|
1921
1939
|
const generateTomlFromScan = (matches) => {
|
|
1922
|
-
|
|
1923
|
-
for (const match of matches) {
|
|
1940
|
+
return matches.map((match) => {
|
|
1924
1941
|
const svc = match.service.fold(() => match.envVar.toLowerCase().replace(/_/g, "-"), (s) => s);
|
|
1925
|
-
|
|
1942
|
+
return `[secret.${match.envVar}]
|
|
1926
1943
|
service = "${svc}"
|
|
1927
1944
|
# purpose = "" # Why: what this secret enables
|
|
1928
1945
|
# capabilities = [] # What operations this grants
|
|
@@ -1931,11 +1948,9 @@ created = "${todayIso$1()}"
|
|
|
1931
1948
|
# rotation_url = "" # URL for rotation procedure
|
|
1932
1949
|
# source = "" # Where the value originates (e.g. vault, ci)
|
|
1933
1950
|
# tags = {}
|
|
1934
|
-
|
|
1935
|
-
}
|
|
1936
|
-
return blocks.join("\n");
|
|
1951
|
+
`;
|
|
1952
|
+
}).join("\n");
|
|
1937
1953
|
};
|
|
1938
|
-
|
|
1939
1954
|
//#endregion
|
|
1940
1955
|
//#region src/core/toml-edit.ts
|
|
1941
1956
|
const SECTION_RE = /^\[.+\]\s*$/;
|
|
@@ -1946,11 +1961,7 @@ const MULTILINE_OPEN = "\"\"\"";
|
|
|
1946
1961
|
* Handles multiline `"""..."""` values when scanning for section boundaries.
|
|
1947
1962
|
*/
|
|
1948
1963
|
const findSectionRange = (lines, sectionHeader) => {
|
|
1949
|
-
|
|
1950
|
-
for (let i = 0; i < lines.length; i++) if (lines[i].trim() === sectionHeader) {
|
|
1951
|
-
start = i;
|
|
1952
|
-
break;
|
|
1953
|
-
}
|
|
1964
|
+
const start = lines.findIndex((l) => l.trim() === sectionHeader);
|
|
1954
1965
|
if (start === -1) return void 0;
|
|
1955
1966
|
let end = lines.length;
|
|
1956
1967
|
let inMultiline = false;
|
|
@@ -2072,7 +2083,8 @@ const updateSectionFields = (raw, sectionHeader, updates) => {
|
|
|
2072
2083
|
}
|
|
2073
2084
|
remaining.push(line);
|
|
2074
2085
|
}
|
|
2075
|
-
|
|
2086
|
+
const newFields = Object.entries(updates).filter(([key, value]) => value !== null && !updatedKeys.has(key)).map(([key, value]) => `${key} = ${value}`);
|
|
2087
|
+
remaining.push(...newFields);
|
|
2076
2088
|
const result = [
|
|
2077
2089
|
...before,
|
|
2078
2090
|
...remaining,
|
|
@@ -2085,7 +2097,6 @@ const updateSectionFields = (raw, sectionHeader, updates) => {
|
|
|
2085
2097
|
* Ensures proper spacing (double newline before the block).
|
|
2086
2098
|
*/
|
|
2087
2099
|
const appendSection = (raw, block) => `${raw.trimEnd()}\n\n${block}`;
|
|
2088
|
-
|
|
2089
2100
|
//#endregion
|
|
2090
2101
|
//#region src/cli/commands/env.ts
|
|
2091
2102
|
const printPostWriteGuidance = () => {
|
|
@@ -2173,13 +2184,21 @@ const runEnvExport = (options) => {
|
|
|
2173
2184
|
}, (boot) => {
|
|
2174
2185
|
const sourceMsg = formatConfigSource(boot.configPath, boot.configSource);
|
|
2175
2186
|
if (sourceMsg) console.error(sourceMsg);
|
|
2176
|
-
|
|
2177
|
-
|
|
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
|
+
});
|
|
2178
2193
|
if (boot.overridden.length > 0) loadConfig(boot.configPath).fold(() => {}, (config) => {
|
|
2179
2194
|
const envEntries = config.env ?? {};
|
|
2180
|
-
|
|
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)}'`);
|
|
2181
2201
|
});
|
|
2182
|
-
for (const [key, value] of Object.entries(boot.secrets)) console.log(`export ${key}='${shellEscape(value)}'`);
|
|
2183
2202
|
});
|
|
2184
2203
|
};
|
|
2185
2204
|
const buildEnvBlock = (name, value, options) => {
|
|
@@ -2196,7 +2215,7 @@ const buildEnvBlock = (name, value, options) => {
|
|
|
2196
2215
|
return `${lines.join("\n")}\n`;
|
|
2197
2216
|
};
|
|
2198
2217
|
const withConfig$1 = (configFlag, fn) => {
|
|
2199
|
-
resolveConfigPath(configFlag).fold((err) => {
|
|
2218
|
+
resolveConfigPath(configFlag.orUndefined()).fold((err) => {
|
|
2200
2219
|
console.error(formatError(err));
|
|
2201
2220
|
process.exit(2);
|
|
2202
2221
|
}, ({ path: configPath, source }) => {
|
|
@@ -2232,7 +2251,7 @@ const runEnvAdd = (name, value, options) => {
|
|
|
2232
2251
|
});
|
|
2233
2252
|
};
|
|
2234
2253
|
const runEnvEdit = (name, options) => {
|
|
2235
|
-
withConfig$1(options.config, (configPath, raw) => {
|
|
2254
|
+
withConfig$1(Option(options.config), (configPath, raw) => {
|
|
2236
2255
|
loadConfig(configPath).fold((err) => {
|
|
2237
2256
|
console.error(formatError(err));
|
|
2238
2257
|
process.exit(2);
|
|
@@ -2269,7 +2288,7 @@ const runEnvEdit = (name, options) => {
|
|
|
2269
2288
|
});
|
|
2270
2289
|
};
|
|
2271
2290
|
const runEnvRm = (name, options) => {
|
|
2272
|
-
withConfig$1(options.config, (configPath, raw) => {
|
|
2291
|
+
withConfig$1(Option(options.config), (configPath, raw) => {
|
|
2273
2292
|
removeSection(raw, `[env.${name}]`).fold((err) => {
|
|
2274
2293
|
console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
|
|
2275
2294
|
process.exit(1);
|
|
@@ -2285,7 +2304,7 @@ const runEnvRm = (name, options) => {
|
|
|
2285
2304
|
});
|
|
2286
2305
|
};
|
|
2287
2306
|
const runEnvRename = (oldName, newName, options) => {
|
|
2288
|
-
withConfig$1(options.config, (configPath, raw) => {
|
|
2307
|
+
withConfig$1(Option(options.config), (configPath, raw) => {
|
|
2289
2308
|
renameSection(raw, `[env.${oldName}]`, `[env.${newName}]`).fold((err) => {
|
|
2290
2309
|
console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
|
|
2291
2310
|
process.exit(1);
|
|
@@ -2324,7 +2343,6 @@ const registerEnvCommands = (program) => {
|
|
|
2324
2343
|
runEnvRename(oldName, newName, options);
|
|
2325
2344
|
});
|
|
2326
2345
|
};
|
|
2327
|
-
|
|
2328
2346
|
//#endregion
|
|
2329
2347
|
//#region src/cli/commands/exec.ts
|
|
2330
2348
|
const runExec = (args, options) => {
|
|
@@ -2363,22 +2381,25 @@ const runExec = (args, options) => {
|
|
|
2363
2381
|
}
|
|
2364
2382
|
if (boot.audit.status === "critical" && options.warnOnly) console.error(`${YELLOW}Warning:${RESET} Proceeding despite critical audit status (--warn-only)`);
|
|
2365
2383
|
}
|
|
2366
|
-
|
|
2384
|
+
boot.warnings.forEach((warning) => {
|
|
2385
|
+
console.error(`${YELLOW}Warning:${RESET} ${warning}`);
|
|
2386
|
+
});
|
|
2367
2387
|
const env = { ...process.env };
|
|
2368
|
-
|
|
2369
|
-
|
|
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
|
+
});
|
|
2370
2394
|
const [cmd, ...cmdArgs] = args;
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
});
|
|
2376
|
-
} catch (err) {
|
|
2395
|
+
Try(() => execFileSync(cmd, cmdArgs, {
|
|
2396
|
+
env,
|
|
2397
|
+
stdio: "inherit"
|
|
2398
|
+
})).fold((err) => {
|
|
2377
2399
|
const exitCode = err.status ?? 1;
|
|
2378
2400
|
process.exit(exitCode);
|
|
2379
|
-
}
|
|
2401
|
+
}, () => {});
|
|
2380
2402
|
};
|
|
2381
|
-
|
|
2382
2403
|
//#endregion
|
|
2383
2404
|
//#region src/core/fleet.ts
|
|
2384
2405
|
const CONFIG_FILENAME$1 = "envpkt.toml";
|
|
@@ -2417,17 +2438,15 @@ function* findEnvpktFiles(dir, maxDepth, currentDepth = 0) {
|
|
|
2417
2438
|
}
|
|
2418
2439
|
const scanFleet = (rootDir, options) => {
|
|
2419
2440
|
const maxDepth = options?.maxDepth ?? 3;
|
|
2420
|
-
const
|
|
2421
|
-
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) => {
|
|
2422
2442
|
const audit = computeAudit(config);
|
|
2423
|
-
|
|
2443
|
+
return [{
|
|
2424
2444
|
path: configPath,
|
|
2425
2445
|
identity: config.identity,
|
|
2426
|
-
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(),
|
|
2427
2447
|
audit
|
|
2428
|
-
}
|
|
2429
|
-
});
|
|
2430
|
-
const agentList = List(agents);
|
|
2448
|
+
}];
|
|
2449
|
+
})));
|
|
2431
2450
|
const total_agents = agentList.size;
|
|
2432
2451
|
const total_secrets = agentList.toArray().reduce((acc, a) => acc + a.audit.total, 0);
|
|
2433
2452
|
const expired = agentList.toArray().reduce((acc, a) => acc + a.audit.expired, 0);
|
|
@@ -2443,7 +2462,6 @@ const scanFleet = (rootDir, options) => {
|
|
|
2443
2462
|
expiring_soon
|
|
2444
2463
|
};
|
|
2445
2464
|
};
|
|
2446
|
-
|
|
2447
2465
|
//#endregion
|
|
2448
2466
|
//#region src/cli/commands/fleet.ts
|
|
2449
2467
|
const statusIcon = (status) => {
|
|
@@ -2460,20 +2478,18 @@ const runFleet = (options) => {
|
|
|
2460
2478
|
process.exit(fleet.status === "critical" ? 2 : 0);
|
|
2461
2479
|
return;
|
|
2462
2480
|
}
|
|
2463
|
-
const
|
|
2464
|
-
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));
|
|
2465
2482
|
console.log(`${statusIcon(fleet.status)} ${BOLD}Fleet: ${fleet.status.toUpperCase()}${RESET} — ${fleet.total_agents} agents, ${fleet.total_secrets} secrets`);
|
|
2466
2483
|
if (fleet.expired > 0) console.log(` ${RED}${fleet.expired}${RESET} expired`);
|
|
2467
2484
|
if (fleet.expiring_soon > 0) console.log(` ${YELLOW}${fleet.expiring_soon}${RESET} expiring soon`);
|
|
2468
2485
|
console.log("");
|
|
2469
|
-
|
|
2486
|
+
agents.forEach((agent) => {
|
|
2470
2487
|
const name = agent.identity?.name ? BOLD + agent.identity.name + RESET : DIM + agent.path + RESET;
|
|
2471
2488
|
const icon = statusIcon(agent.audit.status);
|
|
2472
2489
|
console.log(` ${icon} ${name} ${DIM}(${agent.audit.total} secrets)${RESET}`);
|
|
2473
|
-
}
|
|
2490
|
+
});
|
|
2474
2491
|
process.exit(fleet.status === "critical" ? 2 : 0);
|
|
2475
2492
|
};
|
|
2476
|
-
|
|
2477
2493
|
//#endregion
|
|
2478
2494
|
//#region src/cli/commands/init.ts
|
|
2479
2495
|
const CONFIG_FILENAME = "envpkt.toml";
|
|
@@ -2528,7 +2544,7 @@ const generateTemplate = (options, fnoxKeys) => {
|
|
|
2528
2544
|
lines.push(``);
|
|
2529
2545
|
if (fnoxKeys && fnoxKeys.length > 0) {
|
|
2530
2546
|
lines.push(`# Secrets detected from fnox.toml`);
|
|
2531
|
-
|
|
2547
|
+
lines.push(...fnoxKeys.map((key) => generateSecretBlock(key)));
|
|
2532
2548
|
} else {
|
|
2533
2549
|
lines.push(`# Add your secret metadata below.`);
|
|
2534
2550
|
lines.push(`# Each [secret.<key>] describes a secret your agent needs.`);
|
|
@@ -2563,8 +2579,8 @@ const runInit = (dir, options) => {
|
|
|
2563
2579
|
console.error(`${RED}Error:${RESET} ${CONFIG_FILENAME} already exists. Use --force to overwrite.`);
|
|
2564
2580
|
process.exit(1);
|
|
2565
2581
|
}
|
|
2566
|
-
const fnoxKeys = options.fromFnox
|
|
2567
|
-
const fnoxPath =
|
|
2582
|
+
const fnoxKeys = Option(options.fromFnox).map((fromFnox) => {
|
|
2583
|
+
const fnoxPath = fromFnox === "true" || fromFnox === "" ? join(dir, "fnox.toml") : fromFnox;
|
|
2568
2584
|
if (!existsSync(fnoxPath)) {
|
|
2569
2585
|
console.error(`${RED}Error:${RESET} fnox.toml not found at ${fnoxPath}`);
|
|
2570
2586
|
process.exit(1);
|
|
@@ -2572,98 +2588,100 @@ const runInit = (dir, options) => {
|
|
|
2572
2588
|
return readFnoxKeys(fnoxPath).fold((err) => {
|
|
2573
2589
|
console.error(`${RED}Error:${RESET} Failed to read fnox.toml: ${formatConfigError(err)}`);
|
|
2574
2590
|
process.exit(1);
|
|
2591
|
+
return [];
|
|
2575
2592
|
}, (keys) => keys);
|
|
2576
|
-
})
|
|
2577
|
-
const content = generateTemplate(options, fnoxKeys);
|
|
2593
|
+
});
|
|
2594
|
+
const content = generateTemplate(options, fnoxKeys.orUndefined());
|
|
2578
2595
|
Try(() => writeFileSync(outPath, content, "utf-8")).fold((err) => {
|
|
2579
2596
|
console.error(`${RED}Error:${RESET} Failed to write ${CONFIG_FILENAME}: ${err}`);
|
|
2580
2597
|
process.exit(1);
|
|
2581
2598
|
}, () => {
|
|
2582
2599
|
console.log(`${GREEN}✓${RESET} Created ${BOLD}${CONFIG_FILENAME}${RESET} in ${CYAN}${dir}${RESET}`);
|
|
2583
|
-
|
|
2600
|
+
fnoxKeys.forEach((keys) => {
|
|
2601
|
+
console.log(` Scaffolded ${keys.length} secret(s) from fnox.toml`);
|
|
2602
|
+
});
|
|
2584
2603
|
console.log(` ${BOLD}Next:${RESET} Fill in metadata for each secret`);
|
|
2585
2604
|
});
|
|
2586
2605
|
};
|
|
2587
|
-
|
|
2588
2606
|
//#endregion
|
|
2589
2607
|
//#region src/core/format.ts
|
|
2590
2608
|
const maskValue = (value) => {
|
|
2591
2609
|
if (value.length > 8) return `${value.slice(0, 3)}${"•".repeat(5)}${value.slice(-4)}`;
|
|
2592
2610
|
return "•".repeat(5);
|
|
2593
2611
|
};
|
|
2594
|
-
|
|
2595
2612
|
//#endregion
|
|
2596
2613
|
//#region src/cli/commands/inspect.ts
|
|
2597
2614
|
const printSecretMeta = (meta, indent) => {
|
|
2598
|
-
if (meta.purpose) console.log(`${indent}purpose
|
|
2599
|
-
if (meta.comment) console.log(`${indent}comment
|
|
2600
|
-
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(", ")}`);
|
|
2601
2618
|
const dateParts = [];
|
|
2602
|
-
if (meta.created) dateParts.push(
|
|
2603
|
-
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}`);
|
|
2604
2621
|
if (dateParts.length > 0) console.log(`${indent}${dateParts.join(" ")}`);
|
|
2605
2622
|
const opsParts = [];
|
|
2606
|
-
if (meta.rotates) opsParts.push(
|
|
2607
|
-
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}`);
|
|
2608
2625
|
if (opsParts.length > 0) console.log(`${indent}${opsParts.join(" ")}`);
|
|
2609
|
-
if (meta.source) console.log(`${indent}source
|
|
2610
|
-
if (meta.model_hint) console.log(`${indent}model_hint
|
|
2611
|
-
if (meta.rotation_url) console.log(`${indent}rotation_url
|
|
2612
|
-
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}`}`);
|
|
2613
2630
|
if (meta.tags) {
|
|
2614
|
-
const tagStr = Object.entries(meta.tags).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
2615
|
-
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}`);
|
|
2616
2633
|
}
|
|
2617
2634
|
};
|
|
2618
2635
|
const printConfig = (config, path, resolveResult, opts) => {
|
|
2619
2636
|
console.log(`${BOLD}envpkt.toml${RESET} ${DIM}(${path})${RESET}`);
|
|
2620
2637
|
if (resolveResult?.catalogPath) console.log(`${DIM}Catalog: ${CYAN}${resolveResult.catalogPath}${RESET}`);
|
|
2621
|
-
console.log(
|
|
2638
|
+
console.log(`${DIM}version:${RESET} ${config.version}`);
|
|
2622
2639
|
console.log("");
|
|
2623
2640
|
if (config.identity) {
|
|
2624
|
-
console.log(`${BOLD}Identity:${RESET} ${config.identity.name}`);
|
|
2625
|
-
if (config.identity.consumer) console.log(` consumer
|
|
2626
|
-
if (config.identity.description) console.log(` description
|
|
2627
|
-
if (config.identity.capabilities) console.log(` capabilities
|
|
2628
|
-
if (config.identity.expires) console.log(` expires
|
|
2629
|
-
if (config.identity.services) console.log(` services
|
|
2630
|
-
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(", ")}`);
|
|
2631
2648
|
console.log("");
|
|
2632
2649
|
}
|
|
2633
2650
|
const secretEntries = config.secret ?? {};
|
|
2634
2651
|
console.log(`${BOLD}Secrets:${RESET} ${Object.keys(secretEntries).length}`);
|
|
2635
|
-
|
|
2636
|
-
const
|
|
2637
|
-
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}`);
|
|
2638
2654
|
const sealedTag = meta.encrypted_value ? ` ${CYAN}[sealed]${RESET}` : "";
|
|
2639
2655
|
console.log(` ${BOLD}${key}${RESET} → ${meta.service ?? key}${sealedTag}${valueSuffix}`);
|
|
2640
2656
|
printSecretMeta(meta, " ");
|
|
2641
|
-
}
|
|
2657
|
+
});
|
|
2642
2658
|
const envEntries = config.env ?? {};
|
|
2643
2659
|
const envKeys = Object.keys(envEntries);
|
|
2644
2660
|
if (envKeys.length > 0) {
|
|
2645
2661
|
console.log("");
|
|
2646
2662
|
console.log(`${BOLD}Environment Defaults:${RESET} ${envKeys.length}`);
|
|
2647
|
-
|
|
2648
|
-
console.log(` ${BOLD}${key}${RESET} = "${entry.value}"`);
|
|
2649
|
-
if (entry.purpose) console.log(` purpose
|
|
2650
|
-
if (entry.comment) console.log(` comment
|
|
2651
|
-
}
|
|
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
|
+
});
|
|
2652
2668
|
}
|
|
2653
2669
|
if (config.lifecycle) {
|
|
2654
2670
|
console.log("");
|
|
2655
2671
|
console.log(`${BOLD}Lifecycle:${RESET}`);
|
|
2656
|
-
if (config.lifecycle.stale_warning_days !== void 0) console.log(` stale_warning_days
|
|
2657
|
-
if (config.lifecycle.require_expiration !== void 0) console.log(` require_expiration
|
|
2658
|
-
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}`}`);
|
|
2659
2675
|
}
|
|
2660
2676
|
if (resolveResult?.catalogPath) {
|
|
2661
2677
|
console.log("");
|
|
2662
2678
|
console.log(`${BOLD}Catalog Resolution:${RESET}`);
|
|
2663
|
-
console.log(` merged
|
|
2664
|
-
if (resolveResult.overridden.length > 0) console.log(` overridden
|
|
2665
|
-
else console.log(`
|
|
2666
|
-
|
|
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
|
+
});
|
|
2667
2685
|
}
|
|
2668
2686
|
};
|
|
2669
2687
|
const runInspect = (options) => {
|
|
@@ -2711,9 +2729,15 @@ const runInspect = (options) => {
|
|
|
2711
2729
|
});
|
|
2712
2730
|
});
|
|
2713
2731
|
};
|
|
2714
|
-
|
|
2715
2732
|
//#endregion
|
|
2716
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)));
|
|
2717
2741
|
const runKeygen = (options) => {
|
|
2718
2742
|
const outputPath = options.output ?? resolveKeyPath();
|
|
2719
2743
|
generateKeypair({
|
|
@@ -2732,15 +2756,24 @@ const runKeygen = (options) => {
|
|
|
2732
2756
|
console.log(`${BOLD}Recipient:${RESET} ${recipient}`);
|
|
2733
2757
|
console.log("");
|
|
2734
2758
|
const configPath = resolve(options.config ?? join(process.cwd(), "envpkt.toml"));
|
|
2735
|
-
if (existsSync(configPath))
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2759
|
+
if (existsSync(configPath)) {
|
|
2760
|
+
const name = deriveIdentityName(configPath);
|
|
2761
|
+
const keyFile = tildeShorten(identityPath);
|
|
2762
|
+
updateConfigIdentity(configPath, {
|
|
2763
|
+
recipient,
|
|
2764
|
+
name,
|
|
2765
|
+
keyFile
|
|
2766
|
+
}).fold((err) => {
|
|
2767
|
+
console.error(`${YELLOW}Warning:${RESET} Could not update config: ${"message" in err ? err.message : err._tag}`);
|
|
2768
|
+
console.log(`${DIM}Manually add to your envpkt.toml:${RESET}`);
|
|
2769
|
+
console.log(` [identity]`);
|
|
2770
|
+
console.log(` name = "${name}"`);
|
|
2771
|
+
console.log(` recipient = "${recipient}"`);
|
|
2772
|
+
console.log(` key_file = "${keyFile}"`);
|
|
2773
|
+
}, () => {
|
|
2774
|
+
console.log(`${GREEN}Updated${RESET} ${CYAN}${configPath}${RESET} with identity (name, recipient, key_file)`);
|
|
2775
|
+
});
|
|
2776
|
+
} else {
|
|
2744
2777
|
console.log(`${BOLD}Next steps:${RESET}`);
|
|
2745
2778
|
console.log(` ${DIM}1.${RESET} envpkt init ${DIM}# create envpkt.toml${RESET}`);
|
|
2746
2779
|
console.log(` ${DIM}2.${RESET} envpkt env scan --write ${DIM}# discover credentials${RESET}`);
|
|
@@ -2748,11 +2781,10 @@ const runKeygen = (options) => {
|
|
|
2748
2781
|
}
|
|
2749
2782
|
});
|
|
2750
2783
|
};
|
|
2751
|
-
|
|
2752
2784
|
//#endregion
|
|
2753
2785
|
//#region src/mcp/resources.ts
|
|
2754
2786
|
const loadConfigSafe = () => {
|
|
2755
|
-
return resolveConfigPath().fold(() =>
|
|
2787
|
+
return resolveConfigPath().fold(() => Option.none(), ({ path }) => loadConfig(path).fold(() => Option.none(), (config) => Option({
|
|
2756
2788
|
config,
|
|
2757
2789
|
path
|
|
2758
2790
|
})));
|
|
@@ -2768,14 +2800,11 @@ const resourceDefinitions = [{
|
|
|
2768
2800
|
description: "Capabilities declared by the agent and per-secret capability grants",
|
|
2769
2801
|
mimeType: "application/json"
|
|
2770
2802
|
}];
|
|
2771
|
-
const readHealth = () => {
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
text: JSON.stringify({ error: "No envpkt.toml found" })
|
|
2777
|
-
}] };
|
|
2778
|
-
const { config, path } = loaded;
|
|
2803
|
+
const readHealth = () => loadConfigSafe().fold(() => ({ contents: [{
|
|
2804
|
+
uri: "envpkt://health",
|
|
2805
|
+
mimeType: "application/json",
|
|
2806
|
+
text: JSON.stringify({ error: "No envpkt.toml found" })
|
|
2807
|
+
}] }), ({ config, path }) => {
|
|
2779
2808
|
const audit = computeAudit(config);
|
|
2780
2809
|
return { contents: [{
|
|
2781
2810
|
uri: "envpkt://health",
|
|
@@ -2791,19 +2820,17 @@ const readHealth = () => {
|
|
|
2791
2820
|
missing: audit.missing
|
|
2792
2821
|
}, null, 2)
|
|
2793
2822
|
}] };
|
|
2794
|
-
};
|
|
2795
|
-
const readCapabilities = () => {
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
text: JSON.stringify({ error: "No envpkt.toml found" })
|
|
2801
|
-
}] };
|
|
2802
|
-
const { config } = loaded;
|
|
2823
|
+
});
|
|
2824
|
+
const readCapabilities = () => loadConfigSafe().fold(() => ({ contents: [{
|
|
2825
|
+
uri: "envpkt://capabilities",
|
|
2826
|
+
mimeType: "application/json",
|
|
2827
|
+
text: JSON.stringify({ error: "No envpkt.toml found" })
|
|
2828
|
+
}] }), ({ config }) => {
|
|
2803
2829
|
const agentCapabilities = config.identity?.capabilities ?? [];
|
|
2804
2830
|
const secretCapabilities = {};
|
|
2805
|
-
|
|
2806
|
-
|
|
2831
|
+
Object.entries(config.secret ?? {}).forEach(([key, meta]) => {
|
|
2832
|
+
if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
|
|
2833
|
+
});
|
|
2807
2834
|
return { contents: [{
|
|
2808
2835
|
uri: "envpkt://capabilities",
|
|
2809
2836
|
mimeType: "application/json",
|
|
@@ -2817,16 +2844,12 @@ const readCapabilities = () => {
|
|
|
2817
2844
|
secrets: secretCapabilities
|
|
2818
2845
|
}, null, 2)
|
|
2819
2846
|
}] };
|
|
2820
|
-
};
|
|
2847
|
+
});
|
|
2821
2848
|
const resourceHandlers = {
|
|
2822
2849
|
"envpkt://health": readHealth,
|
|
2823
2850
|
"envpkt://capabilities": readCapabilities
|
|
2824
2851
|
};
|
|
2825
|
-
const readResource = (uri) =>
|
|
2826
|
-
const handler = resourceHandlers[uri];
|
|
2827
|
-
return handler?.();
|
|
2828
|
-
};
|
|
2829
|
-
|
|
2852
|
+
const readResource = (uri) => Option(resourceHandlers[uri]).map((handler) => handler());
|
|
2830
2853
|
//#endregion
|
|
2831
2854
|
//#region src/mcp/tools.ts
|
|
2832
2855
|
const textResult = (text) => ({ content: [{
|
|
@@ -2841,7 +2864,7 @@ const errorResult = (message) => ({
|
|
|
2841
2864
|
isError: true
|
|
2842
2865
|
});
|
|
2843
2866
|
const loadConfigForTool = (configPath) => {
|
|
2844
|
-
return resolveConfigPath(configPath).fold((err) => ({
|
|
2867
|
+
return resolveConfigPath(configPath.fold(() => void 0, (v) => v)).fold((err) => ({
|
|
2845
2868
|
ok: false,
|
|
2846
2869
|
result: errorResult(`Config error: ${err._tag} — ${err._tag === "FileNotFound" ? err.path : ""}`)
|
|
2847
2870
|
}), ({ path }) => loadConfig(path).fold((err) => ({
|
|
@@ -2924,8 +2947,9 @@ const toolDefinitions = [
|
|
|
2924
2947
|
}
|
|
2925
2948
|
}
|
|
2926
2949
|
];
|
|
2950
|
+
const configPathArg = (args) => Option(typeof args.configPath === "string" ? args.configPath : null);
|
|
2927
2951
|
const handleGetPacketHealth = (args) => {
|
|
2928
|
-
const loaded = loadConfigForTool(args
|
|
2952
|
+
const loaded = loadConfigForTool(configPathArg(args));
|
|
2929
2953
|
if (!loaded.ok) return loaded.result;
|
|
2930
2954
|
const { config, path } = loaded;
|
|
2931
2955
|
const audit = computeAudit(config);
|
|
@@ -2950,13 +2974,14 @@ const handleGetPacketHealth = (args) => {
|
|
|
2950
2974
|
}, null, 2));
|
|
2951
2975
|
};
|
|
2952
2976
|
const handleListCapabilities = (args) => {
|
|
2953
|
-
const loaded = loadConfigForTool(args
|
|
2977
|
+
const loaded = loadConfigForTool(configPathArg(args));
|
|
2954
2978
|
if (!loaded.ok) return loaded.result;
|
|
2955
2979
|
const { config } = loaded;
|
|
2956
2980
|
const agentCapabilities = config.identity?.capabilities ?? [];
|
|
2957
2981
|
const secretCapabilities = {};
|
|
2958
|
-
|
|
2959
|
-
|
|
2982
|
+
Object.entries(config.secret ?? {}).forEach(([key, meta]) => {
|
|
2983
|
+
if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
|
|
2984
|
+
});
|
|
2960
2985
|
return textResult(JSON.stringify({
|
|
2961
2986
|
identity: config.identity ? {
|
|
2962
2987
|
name: config.identity.name,
|
|
@@ -2971,21 +2996,21 @@ const handleListCapabilities = (args) => {
|
|
|
2971
2996
|
const handleGetSecretMeta = (args) => {
|
|
2972
2997
|
const key = args.key;
|
|
2973
2998
|
if (!key) return errorResult("Missing required argument: key");
|
|
2974
|
-
const loaded = loadConfigForTool(args
|
|
2999
|
+
const loaded = loadConfigForTool(configPathArg(args));
|
|
2975
3000
|
if (!loaded.ok) return loaded.result;
|
|
2976
3001
|
const { config } = loaded;
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
}
|
|
3002
|
+
return Option((config.secret ?? {})[key]).fold(() => errorResult(`Secret not found: ${key}`), (meta) => {
|
|
3003
|
+
const { encrypted_value: _, ...safeMeta } = meta;
|
|
3004
|
+
return textResult(JSON.stringify({
|
|
3005
|
+
key,
|
|
3006
|
+
...safeMeta
|
|
3007
|
+
}, null, 2));
|
|
3008
|
+
});
|
|
2984
3009
|
};
|
|
2985
3010
|
const handleCheckExpiration = (args) => {
|
|
2986
3011
|
const key = args.key;
|
|
2987
3012
|
if (!key) return errorResult("Missing required argument: key");
|
|
2988
|
-
const loaded = loadConfigForTool(args
|
|
3013
|
+
const loaded = loadConfigForTool(configPathArg(args));
|
|
2989
3014
|
if (!loaded.ok) return loaded.result;
|
|
2990
3015
|
const { config } = loaded;
|
|
2991
3016
|
return computeAudit(config).secrets.find((s) => s.key === key).fold(() => errorResult(`Secret not found: ${key}`), (s) => textResult(JSON.stringify({
|
|
@@ -2999,7 +3024,7 @@ const handleCheckExpiration = (args) => {
|
|
|
2999
3024
|
}, null, 2)));
|
|
3000
3025
|
};
|
|
3001
3026
|
const handleGetEnvMeta = (args) => {
|
|
3002
|
-
const loaded = loadConfigForTool(args
|
|
3027
|
+
const loaded = loadConfigForTool(configPathArg(args));
|
|
3003
3028
|
if (!loaded.ok) return loaded.result;
|
|
3004
3029
|
const { config } = loaded;
|
|
3005
3030
|
const envAudit = computeEnvAudit(config);
|
|
@@ -3012,12 +3037,7 @@ const handlers = {
|
|
|
3012
3037
|
checkExpiration: handleCheckExpiration,
|
|
3013
3038
|
getEnvMeta: handleGetEnvMeta
|
|
3014
3039
|
};
|
|
3015
|
-
const callTool = (name, args) => {
|
|
3016
|
-
const handler = handlers[name];
|
|
3017
|
-
if (!handler) return errorResult(`Unknown tool: ${name}`);
|
|
3018
|
-
return handler(args);
|
|
3019
|
-
};
|
|
3020
|
-
|
|
3040
|
+
const callTool = (name, args) => Option(handlers[name]).fold(() => errorResult(`Unknown tool: ${name}`), (handler) => handler(args));
|
|
3021
3041
|
//#endregion
|
|
3022
3042
|
//#region src/mcp/server.ts
|
|
3023
3043
|
const createServer = () => {
|
|
@@ -3043,13 +3063,11 @@ const createServer = () => {
|
|
|
3043
3063
|
server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [...resourceDefinitions] }));
|
|
3044
3064
|
server.setRequestHandler(ReadResourceRequestSchema, (request) => {
|
|
3045
3065
|
const { uri } = request.params;
|
|
3046
|
-
|
|
3047
|
-
if (!result) return { contents: [{
|
|
3066
|
+
return readResource(uri).fold(() => ({ contents: [{
|
|
3048
3067
|
uri,
|
|
3049
3068
|
mimeType: "text/plain",
|
|
3050
3069
|
text: `Resource not found: ${uri}`
|
|
3051
|
-
}] };
|
|
3052
|
-
return result;
|
|
3070
|
+
}] }), (result) => result);
|
|
3053
3071
|
});
|
|
3054
3072
|
return server;
|
|
3055
3073
|
};
|
|
@@ -3058,7 +3076,6 @@ const startServer = async () => {
|
|
|
3058
3076
|
const transport = new StdioServerTransport();
|
|
3059
3077
|
await server.connect(transport);
|
|
3060
3078
|
};
|
|
3061
|
-
|
|
3062
3079
|
//#endregion
|
|
3063
3080
|
//#region src/cli/commands/mcp.ts
|
|
3064
3081
|
const runMcp = (_options) => {
|
|
@@ -3067,7 +3084,6 @@ const runMcp = (_options) => {
|
|
|
3067
3084
|
process.exit(1);
|
|
3068
3085
|
});
|
|
3069
3086
|
};
|
|
3070
|
-
|
|
3071
3087
|
//#endregion
|
|
3072
3088
|
//#region src/cli/commands/resolve.ts
|
|
3073
3089
|
const runResolve = (options) => {
|
|
@@ -3095,14 +3111,15 @@ const runResolve = (options) => {
|
|
|
3095
3111
|
} else process.stdout.write(content);
|
|
3096
3112
|
if (result.catalogPath) {
|
|
3097
3113
|
const summaryTarget = options.output ? process.stdout : process.stderr;
|
|
3098
|
-
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(
|
|
3099
|
-
|
|
3114
|
+
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`);
|
|
3115
|
+
result.warnings.forEach((w) => {
|
|
3116
|
+
summaryTarget.write(`${RED}Warning:${RESET} ${w}\n`);
|
|
3117
|
+
});
|
|
3100
3118
|
}
|
|
3101
3119
|
});
|
|
3102
3120
|
});
|
|
3103
3121
|
});
|
|
3104
3122
|
};
|
|
3105
|
-
|
|
3106
3123
|
//#endregion
|
|
3107
3124
|
//#region src/core/resolve-values.ts
|
|
3108
3125
|
/** Resolve plaintext values for the given keys via cascade: fnox → env → interactive prompt */
|
|
@@ -3110,18 +3127,20 @@ const resolveValues = async (keys, profile, agentKey) => {
|
|
|
3110
3127
|
const result = {};
|
|
3111
3128
|
const remaining = new Set(keys);
|
|
3112
3129
|
if (fnoxAvailable()) fnoxExport(profile, agentKey).fold(() => {}, (exported) => {
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3130
|
+
[...remaining].forEach((key) => {
|
|
3131
|
+
if (key in exported) {
|
|
3132
|
+
result[key] = exported[key];
|
|
3133
|
+
remaining.delete(key);
|
|
3134
|
+
}
|
|
3135
|
+
});
|
|
3117
3136
|
});
|
|
3118
|
-
|
|
3137
|
+
[...remaining].forEach((key) => {
|
|
3119
3138
|
const envValue = process.env[key];
|
|
3120
3139
|
if (envValue !== void 0 && envValue !== "") {
|
|
3121
3140
|
result[key] = envValue;
|
|
3122
3141
|
remaining.delete(key);
|
|
3123
3142
|
}
|
|
3124
|
-
}
|
|
3143
|
+
});
|
|
3125
3144
|
if (remaining.size > 0 && process.stdin.isTTY) {
|
|
3126
3145
|
const rl = createInterface({
|
|
3127
3146
|
input: process.stdin,
|
|
@@ -3138,60 +3157,61 @@ const resolveValues = async (keys, profile, agentKey) => {
|
|
|
3138
3157
|
}
|
|
3139
3158
|
return result;
|
|
3140
3159
|
};
|
|
3141
|
-
|
|
3142
3160
|
//#endregion
|
|
3143
3161
|
//#region src/cli/commands/seal.ts
|
|
3144
3162
|
/** Write sealed values back into the TOML file, preserving structure */
|
|
3145
3163
|
const writeSealedToml = (configPath, sealedMeta) => {
|
|
3146
3164
|
const lines = readFileSync(configPath, "utf-8").split("\n");
|
|
3147
3165
|
const output = [];
|
|
3148
|
-
let currentMetaKey;
|
|
3166
|
+
let currentMetaKey = Option.none();
|
|
3149
3167
|
let insideMetaBlock = false;
|
|
3150
3168
|
let hasEncryptedValue = false;
|
|
3151
3169
|
const pendingSeals = /* @__PURE__ */ new Map();
|
|
3152
|
-
|
|
3170
|
+
Object.entries(sealedMeta).forEach(([key, meta]) => {
|
|
3171
|
+
if (meta.encrypted_value) pendingSeals.set(key, meta.encrypted_value);
|
|
3172
|
+
});
|
|
3153
3173
|
const metaSectionRe = /^\[secret\.(.+)\]\s*$/;
|
|
3154
3174
|
const encryptedValueRe = /^encrypted_value\s*=/;
|
|
3155
3175
|
const newSectionRe = /^\[/;
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
if (metaMatch) {
|
|
3160
|
-
if (currentMetaKey && !hasEncryptedValue && pendingSeals.has(currentMetaKey)) {
|
|
3176
|
+
const flushPending = () => {
|
|
3177
|
+
currentMetaKey.forEach((key) => {
|
|
3178
|
+
if (!hasEncryptedValue && pendingSeals.has(key)) {
|
|
3161
3179
|
output.push(`encrypted_value = """`);
|
|
3162
|
-
output.push(pendingSeals.get(
|
|
3180
|
+
output.push(pendingSeals.get(key));
|
|
3163
3181
|
output.push(`"""`);
|
|
3164
3182
|
output.push("");
|
|
3165
|
-
pendingSeals.delete(
|
|
3183
|
+
pendingSeals.delete(key);
|
|
3166
3184
|
}
|
|
3167
|
-
|
|
3185
|
+
});
|
|
3186
|
+
};
|
|
3187
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3188
|
+
const line = lines[i];
|
|
3189
|
+
const metaMatch = metaSectionRe.exec(line);
|
|
3190
|
+
if (metaMatch) {
|
|
3191
|
+
flushPending();
|
|
3192
|
+
currentMetaKey = Option(metaMatch[1]);
|
|
3168
3193
|
insideMetaBlock = true;
|
|
3169
3194
|
hasEncryptedValue = false;
|
|
3170
3195
|
output.push(line);
|
|
3171
3196
|
continue;
|
|
3172
3197
|
}
|
|
3173
3198
|
if (insideMetaBlock && newSectionRe.test(line) && !metaSectionRe.test(line)) {
|
|
3174
|
-
|
|
3175
|
-
output.push(`encrypted_value = """`);
|
|
3176
|
-
output.push(pendingSeals.get(currentMetaKey));
|
|
3177
|
-
output.push(`"""`);
|
|
3178
|
-
output.push("");
|
|
3179
|
-
pendingSeals.delete(currentMetaKey);
|
|
3180
|
-
}
|
|
3199
|
+
flushPending();
|
|
3181
3200
|
insideMetaBlock = false;
|
|
3182
|
-
currentMetaKey =
|
|
3201
|
+
currentMetaKey = Option.none();
|
|
3183
3202
|
output.push(line);
|
|
3184
3203
|
continue;
|
|
3185
3204
|
}
|
|
3186
3205
|
if (insideMetaBlock && encryptedValueRe.test(line)) {
|
|
3187
3206
|
hasEncryptedValue = true;
|
|
3188
|
-
const replacing =
|
|
3189
|
-
if (replacing) {
|
|
3207
|
+
const replacing = currentMetaKey.fold(() => false, (key) => pendingSeals.has(key));
|
|
3208
|
+
if (replacing) currentMetaKey.forEach((key) => {
|
|
3190
3209
|
output.push(`encrypted_value = """`);
|
|
3191
|
-
output.push(pendingSeals.get(
|
|
3210
|
+
output.push(pendingSeals.get(key));
|
|
3192
3211
|
output.push(`"""`);
|
|
3193
|
-
pendingSeals.delete(
|
|
3194
|
-
}
|
|
3212
|
+
pendingSeals.delete(key);
|
|
3213
|
+
});
|
|
3214
|
+
else output.push(line);
|
|
3195
3215
|
if (line.slice(line.indexOf("=") + 1).trim().includes("\"\"\"")) {
|
|
3196
3216
|
while (i + 1 < lines.length && !lines[i + 1].includes("\"\"\"")) {
|
|
3197
3217
|
if (!replacing) output.push(lines[i + 1]);
|
|
@@ -3206,12 +3226,7 @@ const writeSealedToml = (configPath, sealedMeta) => {
|
|
|
3206
3226
|
}
|
|
3207
3227
|
output.push(line);
|
|
3208
3228
|
}
|
|
3209
|
-
|
|
3210
|
-
output.push(`encrypted_value = """`);
|
|
3211
|
-
output.push(pendingSeals.get(currentMetaKey));
|
|
3212
|
-
output.push(`"""`);
|
|
3213
|
-
pendingSeals.delete(currentMetaKey);
|
|
3214
|
-
}
|
|
3229
|
+
flushPending();
|
|
3215
3230
|
writeFileSync(configPath, output.join("\n"));
|
|
3216
3231
|
};
|
|
3217
3232
|
const runSeal = async (options) => {
|
|
@@ -3248,10 +3263,13 @@ const runSeal = async (options) => {
|
|
|
3248
3263
|
console.error(`${DIM}Move these to [secret.*] only, or remove from [env.*] before sealing.${RESET}`);
|
|
3249
3264
|
process.exit(2);
|
|
3250
3265
|
}
|
|
3251
|
-
const identityKey =
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3266
|
+
const identityKey = Option(config.identity.key_file).flatMap((keyFile) => {
|
|
3267
|
+
return unwrapAgentKey(resolve(configDir, expandPath(keyFile))).fold((err) => {
|
|
3268
|
+
const msg = err._tag === "IdentityNotFound" ? `not found: ${err.path}` : err.message;
|
|
3269
|
+
console.error(`${YELLOW}Warning:${RESET} Could not unwrap agent key: ${msg}`);
|
|
3270
|
+
return Option.none();
|
|
3271
|
+
}, (k) => Option(k));
|
|
3272
|
+
});
|
|
3255
3273
|
const editKeys = options.edit ? options.edit.split(",").map((k) => k.trim()).filter((k) => k.length > 0) : [];
|
|
3256
3274
|
if (editKeys.length > 0) {
|
|
3257
3275
|
const allSecretEntries = config.secret ?? {};
|
|
@@ -3300,8 +3318,8 @@ const runSeal = async (options) => {
|
|
|
3300
3318
|
}
|
|
3301
3319
|
const allSecretEntries = config.secret ?? {};
|
|
3302
3320
|
const allKeys = Object.keys(allSecretEntries);
|
|
3303
|
-
const alreadySealed = allKeys.filter((k) => allSecretEntries[k]
|
|
3304
|
-
const unsealed = allKeys.filter((k) => !allSecretEntries[k]
|
|
3321
|
+
const alreadySealed = allKeys.filter((k) => allSecretEntries[k].encrypted_value);
|
|
3322
|
+
const unsealed = allKeys.filter((k) => !allSecretEntries[k].encrypted_value);
|
|
3305
3323
|
if (!options.reseal && alreadySealed.length > 0) {
|
|
3306
3324
|
if (unsealed.length === 0) {
|
|
3307
3325
|
console.log(`${GREEN}✓${RESET} All ${BOLD}${alreadySealed.length}${RESET} secret(s) already sealed. Use ${CYAN}--reseal${RESET} to re-encrypt.`);
|
|
@@ -3334,13 +3352,13 @@ const runSeal = async (options) => {
|
|
|
3334
3352
|
process.exit(2);
|
|
3335
3353
|
return {};
|
|
3336
3354
|
}, (d) => d);
|
|
3337
|
-
const newValues = unsealed.length > 0 ? await resolveValues(unsealed, options.profile, identityKey) : {};
|
|
3355
|
+
const newValues = unsealed.length > 0 ? await resolveValues(unsealed, options.profile, identityKey.orUndefined()) : {};
|
|
3338
3356
|
return {
|
|
3339
3357
|
...decrypted,
|
|
3340
3358
|
...newValues
|
|
3341
3359
|
};
|
|
3342
3360
|
}
|
|
3343
|
-
return resolveValues(metaKeys, options.profile, identityKey);
|
|
3361
|
+
return resolveValues(metaKeys, options.profile, identityKey.orUndefined());
|
|
3344
3362
|
})();
|
|
3345
3363
|
const resolved = Object.keys(values).length;
|
|
3346
3364
|
const skipped = metaKeys.length - resolved;
|
|
@@ -3363,7 +3381,6 @@ const runSeal = async (options) => {
|
|
|
3363
3381
|
console.log(`${GREEN}Sealed${RESET} ${sealedCount} secret(s) into ${DIM}${configPath}${RESET}${summary}`);
|
|
3364
3382
|
});
|
|
3365
3383
|
};
|
|
3366
|
-
|
|
3367
3384
|
//#endregion
|
|
3368
3385
|
//#region src/cli/commands/secret.ts
|
|
3369
3386
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -3414,7 +3431,7 @@ const buildFieldUpdates = (options) => {
|
|
|
3414
3431
|
return updates;
|
|
3415
3432
|
};
|
|
3416
3433
|
const withConfig = (configFlag, fn) => {
|
|
3417
|
-
resolveConfigPath(configFlag).fold((err) => {
|
|
3434
|
+
resolveConfigPath(configFlag.orUndefined()).fold((err) => {
|
|
3418
3435
|
console.error(formatError(err));
|
|
3419
3436
|
process.exit(2);
|
|
3420
3437
|
}, ({ path: configPath, source }) => {
|
|
@@ -3458,7 +3475,7 @@ const runSecretEdit = (name, options) => {
|
|
|
3458
3475
|
console.error(`${RED}Error:${RESET} Invalid date format for --expires: "${options.expires}" (expected YYYY-MM-DD)`);
|
|
3459
3476
|
process.exit(1);
|
|
3460
3477
|
}
|
|
3461
|
-
withConfig(options.config, (configPath, raw) => {
|
|
3478
|
+
withConfig(Option(options.config), (configPath, raw) => {
|
|
3462
3479
|
loadConfig(configPath).fold((err) => {
|
|
3463
3480
|
console.error(formatError(err));
|
|
3464
3481
|
process.exit(2);
|
|
@@ -3488,7 +3505,7 @@ const runSecretEdit = (name, options) => {
|
|
|
3488
3505
|
});
|
|
3489
3506
|
};
|
|
3490
3507
|
const runSecretRm = (name, options) => {
|
|
3491
|
-
withConfig(options.config, (configPath, raw) => {
|
|
3508
|
+
withConfig(Option(options.config), (configPath, raw) => {
|
|
3492
3509
|
removeSection(raw, `[secret.${name}]`).fold((err) => {
|
|
3493
3510
|
console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
|
|
3494
3511
|
process.exit(1);
|
|
@@ -3504,7 +3521,7 @@ const runSecretRm = (name, options) => {
|
|
|
3504
3521
|
});
|
|
3505
3522
|
};
|
|
3506
3523
|
const runSecretRename = (oldName, newName, options) => {
|
|
3507
|
-
withConfig(options.config, (configPath, raw) => {
|
|
3524
|
+
withConfig(Option(options.config), (configPath, raw) => {
|
|
3508
3525
|
renameSection(raw, `[secret.${oldName}]`, `[secret.${newName}]`).fold((err) => {
|
|
3509
3526
|
console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
|
|
3510
3527
|
process.exit(1);
|
|
@@ -3535,7 +3552,6 @@ const registerSecretCommands = (program) => {
|
|
|
3535
3552
|
runSecretRename(oldName, newName, options);
|
|
3536
3553
|
});
|
|
3537
3554
|
};
|
|
3538
|
-
|
|
3539
3555
|
//#endregion
|
|
3540
3556
|
//#region src/cli/commands/shell-hook.ts
|
|
3541
3557
|
const ZSH_HOOK = `# envpkt shell hook — add to your .zshrc
|
|
@@ -3577,45 +3593,36 @@ const runShellHook = (shell) => {
|
|
|
3577
3593
|
process.exit(1);
|
|
3578
3594
|
}
|
|
3579
3595
|
};
|
|
3580
|
-
|
|
3581
3596
|
//#endregion
|
|
3582
3597
|
//#region src/cli/commands/upgrade.ts
|
|
3583
|
-
const getCurrentVersion = () =>
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
]
|
|
3597
|
-
}).match(/"envpkt":\s*\{\s*"version":\s*"([^"]+)"/)?.[1] ?? "unknown";
|
|
3598
|
-
} catch {
|
|
3599
|
-
return "unknown";
|
|
3600
|
-
}
|
|
3601
|
-
};
|
|
3598
|
+
const getCurrentVersion = () => Try(() => execFileSync("npm", [
|
|
3599
|
+
"list",
|
|
3600
|
+
"-g",
|
|
3601
|
+
"envpkt",
|
|
3602
|
+
"--json"
|
|
3603
|
+
], {
|
|
3604
|
+
encoding: "utf-8",
|
|
3605
|
+
stdio: [
|
|
3606
|
+
"pipe",
|
|
3607
|
+
"pipe",
|
|
3608
|
+
"pipe"
|
|
3609
|
+
]
|
|
3610
|
+
})).fold(() => "unknown", (output) => output.match(/"envpkt":\s*\{\s*"version":\s*"([^"]+)"/)?.[1] ?? "unknown");
|
|
3602
3611
|
const runUpgrade = () => {
|
|
3603
3612
|
const before = getCurrentVersion();
|
|
3604
3613
|
console.log(`${DIM}Current version: ${before}${RESET}`);
|
|
3605
3614
|
console.log(`${CYAN}Upgrading envpkt...${RESET}\n`);
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
});
|
|
3616
|
-
} catch {
|
|
3615
|
+
Try(() => execFileSync("npm", [
|
|
3616
|
+
"install",
|
|
3617
|
+
"-g",
|
|
3618
|
+
"envpkt@latest",
|
|
3619
|
+
"--prefer-online"
|
|
3620
|
+
], {
|
|
3621
|
+
stdio: "inherit",
|
|
3622
|
+
encoding: "utf-8"
|
|
3623
|
+
})).fold(() => {
|
|
3617
3624
|
console.error(`\n${RED}Error:${RESET} npm install failed. Trying with cache clean...`);
|
|
3618
|
-
|
|
3625
|
+
Try(() => {
|
|
3619
3626
|
execFileSync("npm", [
|
|
3620
3627
|
"cache",
|
|
3621
3628
|
"clean",
|
|
@@ -3629,17 +3636,16 @@ const runUpgrade = () => {
|
|
|
3629
3636
|
stdio: "inherit",
|
|
3630
3637
|
encoding: "utf-8"
|
|
3631
3638
|
});
|
|
3632
|
-
}
|
|
3639
|
+
}).fold(() => {
|
|
3633
3640
|
console.error(`${RED}Error:${RESET} Upgrade failed. Try manually:`);
|
|
3634
3641
|
console.error(` ${BOLD}sudo npm install -g envpkt@latest --prefer-online${RESET}`);
|
|
3635
3642
|
process.exit(1);
|
|
3636
|
-
}
|
|
3637
|
-
}
|
|
3643
|
+
}, () => {});
|
|
3644
|
+
}, () => {});
|
|
3638
3645
|
const after = getCurrentVersion();
|
|
3639
3646
|
if (before === after && before !== "unknown") console.log(`\n${GREEN}✓${RESET} Already on latest version ${BOLD}${after}${RESET}`);
|
|
3640
3647
|
else console.log(`\n${GREEN}✓${RESET} Upgraded ${YELLOW}${before}${RESET} → ${BOLD}${after}${RESET}`);
|
|
3641
3648
|
};
|
|
3642
|
-
|
|
3643
3649
|
//#endregion
|
|
3644
3650
|
//#region src/cli/index.ts
|
|
3645
3651
|
const program = new Command();
|
|
@@ -3688,6 +3694,5 @@ program.command("shell-hook").description("Output shell function for ambient cre
|
|
|
3688
3694
|
runShellHook(shell);
|
|
3689
3695
|
});
|
|
3690
3696
|
program.parse();
|
|
3691
|
-
|
|
3692
3697
|
//#endregion
|
|
3693
|
-
export {
|
|
3698
|
+
export {};
|