envpkt 0.13.6 → 0.14.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/README.md +5 -1
- package/dist/cli.js +83 -52
- package/dist/index.d.ts +17 -7
- package/dist/index.js +27 -16
- package/package.json +12 -12
- package/schemas/envpkt.schema.json +4 -0
package/README.md
CHANGED
|
@@ -141,6 +141,10 @@ See [`examples/`](./examples/) for more configurations.
|
|
|
141
141
|
|
|
142
142
|
Sealed packets embed age-encrypted secret values directly in `envpkt.toml`. This makes your config fully self-contained — no external secrets backend needed at runtime.
|
|
143
143
|
|
|
144
|
+
> **Requires the [`age`](https://github.com/FiloSottile/age) CLI.** envpkt shells out to `age` to seal and unseal `encrypted_value` — it's a runtime dependency, not bundled, and there is **no built-in / JS fallback**. Every path that decrypts a sealed value (`boot()`, `exec`, `env export`, `env github`, the GitHub Action) needs `age` on `PATH` plus the private key.
|
|
145
|
+
>
|
|
146
|
+
> **Gotcha — partial resolution hides a missing `age`.** Plaintext `[env.*]` defaults and fnox-backed values resolve _without_ `age`, so a config can look like it's working while every sealed secret silently fails to decrypt. If sealed values aren't showing up, confirm `age` is installed — `envpkt doctor` reports its presence.
|
|
147
|
+
|
|
144
148
|
### Setup
|
|
145
149
|
|
|
146
150
|
```bash
|
|
@@ -214,7 +218,7 @@ A composite action resolves the credentials in `envpkt.toml` into the CI job —
|
|
|
214
218
|
|
|
215
219
|
**Inputs:** `config`, `version` (npm version to run, default `latest`), `strict`, `profile`.
|
|
216
220
|
|
|
217
|
-
>
|
|
221
|
+
> **The [`age`](https://github.com/FiloSottile/age) CLI is required on the runner to unseal `encrypted_value` packets** (install it first, as above) — envpkt shells out to `age`, with no bundled/JS fallback. Without it, sealed secrets won't decrypt or be injected, even though plaintext `[env.*]` defaults and fnox-backed values still resolve — which can mask the missing dependency until a sealed value turns up empty downstream. Reference `@v0` for the moving major tag (re-pointed to each `0.x` release), or pin an exact release like `@v0.13.6` for immutability. `@v1` ships when envpkt reaches 1.0. Node is assumed present; add `actions/setup-node` first to pin a version.
|
|
218
222
|
|
|
219
223
|
### Anti-rot CI gate
|
|
220
224
|
|
package/dist/cli.js
CHANGED
|
@@ -70,7 +70,8 @@ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration
|
|
|
70
70
|
const isExpiringSoon = daysRemaining.fold(() => false, (d) => d >= 0 && d <= WARN_BEFORE_DAYS);
|
|
71
71
|
const isStale = daysSinceRotation.fold(() => false, (d) => d > staleWarningDays);
|
|
72
72
|
const hasSealed = !!meta.encrypted_value;
|
|
73
|
-
const
|
|
73
|
+
const isExternal = meta.external === true;
|
|
74
|
+
const isMissing = !isExternal && fnoxKeys.size > 0 && !fnoxKeys.has(key) && !hasSealed;
|
|
74
75
|
const isMissingMetadata = requireExpiration && expires.isNone() || requireService && service.isNone();
|
|
75
76
|
if (isExpired) issues.push("Secret has expired");
|
|
76
77
|
if (isExpiringSoon) issues.push(`Expires in ${daysRemaining.fold(() => "?", (d) => String(d))} days`);
|
|
@@ -87,7 +88,7 @@ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration
|
|
|
87
88
|
return {
|
|
88
89
|
key,
|
|
89
90
|
service,
|
|
90
|
-
status: Cond.of().when(isExpired, "expired").elseWhen(isMissing, "missing").elseWhen(isMissingMetadata, "missing_metadata").elseWhen(isExpiringSoon, "expiring_soon").elseWhen(isStale, "stale").else("healthy"),
|
|
91
|
+
status: Cond.of().when(isExpired, "expired").elseWhen(isMissing, "missing").elseWhen(isMissingMetadata, "missing_metadata").elseWhen(isExpiringSoon, "expiring_soon").elseWhen(isStale, "stale").elseWhen(isExternal, "external").else("healthy"),
|
|
91
92
|
days_remaining: daysRemaining,
|
|
92
93
|
rotation_url: rotationUrl,
|
|
93
94
|
purpose,
|
|
@@ -126,7 +127,6 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
|
|
|
126
127
|
const secretEntries = config.secret ?? {};
|
|
127
128
|
const nonAliasEntries = Object.entries(secretEntries).filter(([, meta]) => meta.from_key === void 0);
|
|
128
129
|
const aliasEntries = Object.entries(secretEntries).filter(([, meta]) => meta.from_key !== void 0);
|
|
129
|
-
const nonAliasMetaKeys = Set$1(nonAliasEntries.map(([k]) => k));
|
|
130
130
|
const nonAliasHealth = nonAliasEntries.map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now));
|
|
131
131
|
const healthByKey = Map$1(nonAliasHealth.map((h) => [h.key, h]));
|
|
132
132
|
const parseTargetKey = (from_key) => {
|
|
@@ -152,13 +152,14 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
|
|
|
152
152
|
}), (health) => classifyAlias(key, meta, health, targetRef));
|
|
153
153
|
});
|
|
154
154
|
const secrets = List([...nonAliasHealth, ...aliasHealth]);
|
|
155
|
-
const orphaned = keys.size > 0 ?
|
|
155
|
+
const orphaned = keys.size > 0 ? nonAliasEntries.filter(([k, meta]) => !keys.has(k) && meta.external !== true).length : 0;
|
|
156
156
|
const total = secrets.size;
|
|
157
157
|
const expired = secrets.count((s) => s.status === "expired");
|
|
158
158
|
const missing = secrets.count((s) => s.status === "missing");
|
|
159
159
|
const missing_metadata = secrets.count((s) => s.status === "missing_metadata");
|
|
160
160
|
const expiring_soon = secrets.count((s) => s.status === "expiring_soon");
|
|
161
161
|
const stale = secrets.count((s) => s.status === "stale");
|
|
162
|
+
const external = secrets.count((s) => s.status === "external");
|
|
162
163
|
const healthy = secrets.count((s) => s.status === "healthy");
|
|
163
164
|
const aliases = aliasHealth.length;
|
|
164
165
|
return {
|
|
@@ -171,6 +172,7 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
|
|
|
171
172
|
stale,
|
|
172
173
|
missing,
|
|
173
174
|
missing_metadata,
|
|
175
|
+
external,
|
|
174
176
|
orphaned,
|
|
175
177
|
aliases,
|
|
176
178
|
identity: config.identity
|
|
@@ -182,7 +184,8 @@ const computeEnvAudit = (config, env = process.env) => {
|
|
|
182
184
|
const envWire = (key) => namer(key, envEntries[key]?.namespace);
|
|
183
185
|
const resolveEffectiveDefault = (entry) => {
|
|
184
186
|
return Do(function* () {
|
|
185
|
-
|
|
187
|
+
const targetKey = yield* $(Option((yield* $(Option(entry.from_key))).match(/^env\.(.+)$/)?.[1]));
|
|
188
|
+
return yield* $(Option((yield* $(Option(envEntries[targetKey]))).value));
|
|
186
189
|
}).orElse(entry.value ?? "");
|
|
187
190
|
};
|
|
188
191
|
const entries = Object.entries(envEntries).map(([key, entry]) => {
|
|
@@ -266,6 +269,7 @@ const SecretMetaSchema = Type.Object({
|
|
|
266
269
|
encrypted_value: Type.Optional(Type.String({ description: "Age-encrypted secret value (armored ciphertext, safe to commit)" })),
|
|
267
270
|
from_key: Type.Optional(Type.String({ description: "Reference another entry (format: 'secret.<KEY>') whose resolved value this alias reuses. Mutually exclusive with encrypted_value." })),
|
|
268
271
|
required: Type.Optional(Type.Boolean({ description: "Whether this secret is required for operation" })),
|
|
272
|
+
external: Type.Optional(Type.Boolean({ description: "Value is managed outside envpkt (e.g. a Cloudflare/Vault secret). The entry registers the name for inventory and dev/prod parity only: seal never touches it, and audit reports it as 'external' rather than 'missing'." })),
|
|
269
273
|
tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" })),
|
|
270
274
|
namespace: Type.Optional(Type.String({ description: "Override the file-level namespace for this entry's injected name. Empty string opts out of any prefix." }))
|
|
271
275
|
}, { description: "Metadata about a single secret" });
|
|
@@ -385,7 +389,8 @@ const walkUpForConfig = (dir) => {
|
|
|
385
389
|
};
|
|
386
390
|
/** Discover config by walking up from CWD, then ENVPKT_SEARCH_PATH, then dynamic Platform paths */
|
|
387
391
|
const discoverConfig = (cwd) => {
|
|
388
|
-
const
|
|
392
|
+
const dir = cwd ?? process.cwd();
|
|
393
|
+
const walked = walkUpForConfig(dir).orUndefined();
|
|
389
394
|
if (walked) return Option({
|
|
390
395
|
path: walked,
|
|
391
396
|
source: "cwd"
|
|
@@ -646,6 +651,7 @@ const secretStatusIcon = (status) => {
|
|
|
646
651
|
case "stale": return `${YELLOW}○${RESET}`;
|
|
647
652
|
case "missing": return `${RED}?${RESET}`;
|
|
648
653
|
case "missing_metadata": return `${YELLOW}!${RESET}`;
|
|
654
|
+
case "external": return `${CYAN}↗${RESET}`;
|
|
649
655
|
default: return " ";
|
|
650
656
|
}
|
|
651
657
|
};
|
|
@@ -667,9 +673,10 @@ const formatAudit = (audit) => {
|
|
|
667
673
|
audit.stale > 0 ? ` ${YELLOW}${audit.stale}${RESET} stale` : null,
|
|
668
674
|
audit.missing > 0 ? ` ${RED}${audit.missing}${RESET} missing` : null,
|
|
669
675
|
audit.missing_metadata > 0 ? ` ${YELLOW}${audit.missing_metadata}${RESET} missing metadata` : null,
|
|
676
|
+
audit.external > 0 ? ` ${CYAN}${audit.external}${RESET} external` : null,
|
|
670
677
|
audit.orphaned > 0 ? ` ${YELLOW}${audit.orphaned}${RESET} orphaned` : null
|
|
671
678
|
].filter(Boolean).join("\n"),
|
|
672
|
-
audit.secrets.filter((s) => s.status !== "healthy").map(formatSecretRow).toArray().join("\n")
|
|
679
|
+
audit.secrets.filter((s) => s.status !== "healthy" && s.status !== "external").map(formatSecretRow).toArray().join("\n")
|
|
673
680
|
].filter((s) => s.length > 0).join("\n\n");
|
|
674
681
|
};
|
|
675
682
|
const formatAuditJson = (audit) => JSON.stringify({
|
|
@@ -721,12 +728,13 @@ const formatError = (error) => {
|
|
|
721
728
|
case "IdentityNotFound": return `${RED}Error:${RESET} Identity file not found: ${error.path}`;
|
|
722
729
|
case "SealKeyUnavailable": {
|
|
723
730
|
const e = error;
|
|
731
|
+
const restoreLine = e.configuredKeyFile ? ` • Restore the configured key to ${e.configuredKeyFile} (or set ENVPKT_AGE_KEY_FILE / ENVPKT_AGE_KEY)` : ` • Restore your key to ~/.envpkt/age-key.txt (or set ENVPKT_AGE_KEY_FILE / ENVPKT_AGE_KEY)`;
|
|
724
732
|
return [
|
|
725
733
|
`${RED}Error:${RESET} ${e.sealedKeys.length} sealed secret(s) can't be decrypted — no age key found.`,
|
|
726
734
|
`${DIM}Searched (in order):${RESET}`,
|
|
727
735
|
e.searched.map((l) => ` • ${l}`).join("\n"),
|
|
728
736
|
`${DIM}Fix one:${RESET}`,
|
|
729
|
-
|
|
737
|
+
restoreLine,
|
|
730
738
|
` • Re-provision from source: envpkt seal --edit <KEY>`
|
|
731
739
|
].join("\n");
|
|
732
740
|
}
|
|
@@ -757,8 +765,9 @@ const confidenceIcon = (confidence) => {
|
|
|
757
765
|
}
|
|
758
766
|
};
|
|
759
767
|
const formatScanTable = (scan) => {
|
|
768
|
+
const total = scan.discovered.size;
|
|
760
769
|
return [
|
|
761
|
-
`${BOLD}Scan Results${RESET} — ${
|
|
770
|
+
`${BOLD}Scan Results${RESET} — ${total} credential(s) found in ${scan.total_scanned} env vars`,
|
|
762
771
|
[
|
|
763
772
|
scan.high_confidence > 0 ? ` ${GREEN}${scan.high_confidence}${RESET} high confidence` : null,
|
|
764
773
|
scan.medium_confidence > 0 ? ` ${YELLOW}${scan.medium_confidence}${RESET} medium confidence` : null,
|
|
@@ -955,7 +964,8 @@ const fnoxExport = (profile, agentKey) => {
|
|
|
955
964
|
const eq = line.indexOf("=");
|
|
956
965
|
if (eq > 0) {
|
|
957
966
|
const key = line.slice(0, eq).trim();
|
|
958
|
-
|
|
967
|
+
const value = line.slice(eq + 1).trim();
|
|
968
|
+
entries[key] = value;
|
|
959
969
|
}
|
|
960
970
|
});
|
|
961
971
|
return Right(entries);
|
|
@@ -1395,11 +1405,13 @@ const resolveSealIdentity = (config, configDir) => {
|
|
|
1395
1405
|
});
|
|
1396
1406
|
const inlineKey = resolveInlineKey().orUndefined();
|
|
1397
1407
|
if (inlineKey) return Option(materializeInlineKey(inlineKey));
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1408
|
+
if (!config.identity?.key_file) {
|
|
1409
|
+
const defaultPath = join(homedir(), ".envpkt", "age-key.txt");
|
|
1410
|
+
if (existsSync(defaultPath)) return Option({
|
|
1411
|
+
path: defaultPath,
|
|
1412
|
+
dispose: noop
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1403
1415
|
return Option(void 0);
|
|
1404
1416
|
};
|
|
1405
1417
|
/** Describe the seal-identity precedence chain with per-entry status, for a clear no-key error. */
|
|
@@ -1414,7 +1426,7 @@ const describeSealKeySearch = (config, configDir) => {
|
|
|
1414
1426
|
keyFileLine,
|
|
1415
1427
|
envFileLine,
|
|
1416
1428
|
inlineLine,
|
|
1417
|
-
`${defaultPath} (${existsSync(defaultPath) ? "found" : "missing"})`
|
|
1429
|
+
keyFile ? `${defaultPath} (skipped — identity.key_file is set)` : `${defaultPath} (${existsSync(defaultPath) ? "found" : "missing"})`
|
|
1418
1430
|
];
|
|
1419
1431
|
};
|
|
1420
1432
|
const resolveIdentityKey = (config, configDir) => {
|
|
@@ -1490,7 +1502,8 @@ const bootSafe = (options) => {
|
|
|
1490
1502
|
if (hasSealedValues && sealIdentity.isEmpty) return Left({
|
|
1491
1503
|
_tag: "SealKeyUnavailable",
|
|
1492
1504
|
sealedKeys: nonAliasMetaKeys.filter((k) => !!nonAliasSecretEntries[k]?.encrypted_value),
|
|
1493
|
-
searched: describeSealKeySearch(config, configDir)
|
|
1505
|
+
searched: describeSealKeySearch(config, configDir),
|
|
1506
|
+
configuredKeyFile: config.identity?.key_file ? resolve(configDir, expandPath(config.identity.key_file)) : void 0
|
|
1494
1507
|
});
|
|
1495
1508
|
return checkExpiration(audit, failOnExpired, warnOnly).map((warnings) => {
|
|
1496
1509
|
const secrets = {};
|
|
@@ -1585,7 +1598,8 @@ const bootSafe = (options) => {
|
|
|
1585
1598
|
}
|
|
1586
1599
|
const targetEntry = envEntries[entry.targetKey];
|
|
1587
1600
|
if (targetEntry?.value === void 0) return;
|
|
1588
|
-
|
|
1601
|
+
const resolvedTarget = process.env[envEnv(entry.targetKey)] ?? targetEntry.value;
|
|
1602
|
+
envDefaults[aliasKey] = resolvedTarget;
|
|
1589
1603
|
});
|
|
1590
1604
|
if (inject) {
|
|
1591
1605
|
Object.entries(envDefaults).forEach(([key, value]) => {
|
|
@@ -4419,7 +4433,8 @@ const collectEditedValues = async (editKeys, entries, prompt, onSkip) => {
|
|
|
4419
4433
|
const values = {};
|
|
4420
4434
|
for (const key of editKeys) {
|
|
4421
4435
|
if (entries[key]?.encrypted_value) {
|
|
4422
|
-
|
|
4436
|
+
const confirm = await prompt(`Replace the sealed value for ${key}? The previous value can't be recovered without the original key. [y/N] `);
|
|
4437
|
+
if (!isAffirmative(confirm)) {
|
|
4423
4438
|
onSkip?.(key, "declined");
|
|
4424
4439
|
continue;
|
|
4425
4440
|
}
|
|
@@ -4435,7 +4450,8 @@ const collectEditedValues = async (editKeys, entries, prompt, onSkip) => {
|
|
|
4435
4450
|
};
|
|
4436
4451
|
/** Write sealed values back into the TOML file, preserving structure. */
|
|
4437
4452
|
const writeSealedToml = (configPath, sealedMeta) => {
|
|
4438
|
-
const
|
|
4453
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
4454
|
+
const finalContent = applySealedToml(raw, sealedMeta);
|
|
4439
4455
|
validateOrExit(finalContent);
|
|
4440
4456
|
writeFileSync(configPath, finalContent);
|
|
4441
4457
|
};
|
|
@@ -4495,6 +4511,12 @@ const runSeal = async (options) => {
|
|
|
4495
4511
|
console.error(`${DIM}Aliases reference another secret's value via from_key — seal the target instead.${RESET}`);
|
|
4496
4512
|
process.exit(2);
|
|
4497
4513
|
}
|
|
4514
|
+
const externalKeys = editKeys.filter((k) => allSecretEntries[k].external === true);
|
|
4515
|
+
if (externalKeys.length > 0) {
|
|
4516
|
+
console.error(`${RED}Error:${RESET} Cannot seal external entries: ${externalKeys.join(", ")}`);
|
|
4517
|
+
console.error(`${DIM}external = true means the value is managed outside envpkt — set it in that store instead.${RESET}`);
|
|
4518
|
+
process.exit(2);
|
|
4519
|
+
}
|
|
4498
4520
|
if (!process.stdin.isTTY) {
|
|
4499
4521
|
console.error(`${RED}Error:${RESET} --edit requires an interactive terminal`);
|
|
4500
4522
|
process.exit(2);
|
|
@@ -4527,14 +4549,16 @@ const runSeal = async (options) => {
|
|
|
4527
4549
|
return;
|
|
4528
4550
|
}
|
|
4529
4551
|
const allSecretEntries = config.secret ?? {};
|
|
4530
|
-
const allKeys = Object.keys(allSecretEntries).filter((k) => allSecretEntries[k].from_key === void 0);
|
|
4552
|
+
const allKeys = Object.keys(allSecretEntries).filter((k) => allSecretEntries[k].from_key === void 0 && allSecretEntries[k].external !== true);
|
|
4531
4553
|
const skippedAliases = Object.keys(allSecretEntries).filter((k) => allSecretEntries[k].from_key !== void 0);
|
|
4554
|
+
const skippedExternal = Object.keys(allSecretEntries).filter((k) => allSecretEntries[k].from_key === void 0 && allSecretEntries[k].external === true);
|
|
4532
4555
|
const alreadySealed = allKeys.filter((k) => allSecretEntries[k].encrypted_value);
|
|
4533
4556
|
const unsealed = allKeys.filter((k) => !allSecretEntries[k].encrypted_value);
|
|
4534
4557
|
if (skippedAliases.length > 0) {
|
|
4535
4558
|
const noun = skippedAliases.length === 1 ? "alias" : "aliases";
|
|
4536
4559
|
console.error(`${DIM}Skipping ${skippedAliases.length} ${noun}: ${skippedAliases.join(", ")}${RESET}`);
|
|
4537
4560
|
}
|
|
4561
|
+
if (skippedExternal.length > 0) console.error(`${DIM}Skipping ${skippedExternal.length} external (value managed outside envpkt): ${skippedExternal.join(", ")}${RESET}`);
|
|
4538
4562
|
if (!options.reseal && alreadySealed.length > 0) {
|
|
4539
4563
|
if (unsealed.length === 0) {
|
|
4540
4564
|
console.log(`${GREEN}✓${RESET} All ${BOLD}${alreadySealed.length}${RESET} secret(s) already sealed. Use ${CYAN}--reseal${RESET} to re-encrypt.`);
|
|
@@ -4937,11 +4961,13 @@ const runSecretRotate = async (name, options) => {
|
|
|
4937
4961
|
const addSecretFlags = (cmd) => cmd.option("--service <service>", "Service this secret authenticates to").option("--purpose <purpose>", "Why this secret exists").option("--comment <comment>", "Free-form annotation").option("--expires <date>", "Expiration date (YYYY-MM-DD)").option("--capabilities <caps>", "Comma-separated capabilities (e.g. read,write)").option("--rotates <schedule>", "Rotation schedule (e.g. 90d, quarterly)").option("--rate-limit <limit>", "Rate limit info (e.g. 1000/min)").option("--model-hint <hint>", "Suggested model or tier").option("--source <source>", "Where the value originates (e.g. vault, ci)").option("--rotation-url <url>", "URL for secret rotation procedure").option("--tags <tags>", "Comma-separated key=value tags (e.g. env=prod,team=payments)");
|
|
4938
4962
|
const registerSecretCommands = (program) => {
|
|
4939
4963
|
const secret = program.command("secret").description("Manage secret entries in envpkt.toml");
|
|
4940
|
-
|
|
4964
|
+
const addCmd = secret.command("add").description("Add a new secret entry to envpkt.toml").argument("<name>", "Secret name (becomes the env var key)").option("-c, --config <path>", "Path to envpkt.toml").option("--required", "Mark this secret as required").option("--dry-run", "Preview the TOML block without writing");
|
|
4965
|
+
addSecretFlags(addCmd).action((name, options) => {
|
|
4941
4966
|
runSecretAdd(name, options);
|
|
4942
4967
|
});
|
|
4943
4968
|
const collect = (value, previous) => [...previous, value];
|
|
4944
|
-
|
|
4969
|
+
const editCmd = secret.command("edit").description("Update metadata fields on an existing secret").argument("<name>", "Secret name to edit").option("-c, --config <path>", "Path to envpkt.toml").option("--required", "Mark this secret as required").option("--no-required", "Mark this secret as not required").option("--unset <field>", "Remove an optional field (repeatable). Field names match TOML keys, e.g. expires, rate_limit", collect, []).option("--dry-run", "Preview the changes without writing");
|
|
4970
|
+
addSecretFlags(editCmd).action((name, options) => {
|
|
4945
4971
|
runSecretEdit(name, options);
|
|
4946
4972
|
});
|
|
4947
4973
|
secret.command("rm").description("Remove a secret entry from envpkt.toml").argument("<name>", "Secret name to remove").option("-c, --config <path>", "Path to envpkt.toml").option("--dry-run", "Preview the result without writing").action((name, options) => {
|
|
@@ -5223,35 +5249,39 @@ const buildReport = (path, raw) => {
|
|
|
5223
5249
|
skipped(CHECK_NAMES.sealed)
|
|
5224
5250
|
]
|
|
5225
5251
|
}), (config) => {
|
|
5252
|
+
const schemaOk = {
|
|
5253
|
+
name: CHECK_NAMES.schema,
|
|
5254
|
+
status: "ok"
|
|
5255
|
+
};
|
|
5256
|
+
const catalogCheck = config.catalog ? resolveConfig(config, dirname(path)).fold((err) => ({
|
|
5257
|
+
name: CHECK_NAMES.catalog,
|
|
5258
|
+
status: "failed",
|
|
5259
|
+
error: formatError(err)
|
|
5260
|
+
}), () => ({
|
|
5261
|
+
name: CHECK_NAMES.catalog,
|
|
5262
|
+
status: "ok",
|
|
5263
|
+
detail: config.catalog
|
|
5264
|
+
})) : {
|
|
5265
|
+
name: CHECK_NAMES.catalog,
|
|
5266
|
+
status: "na",
|
|
5267
|
+
detail: "no catalog declared"
|
|
5268
|
+
};
|
|
5269
|
+
const aliasCheck = validateAliases(config).fold((err) => ({
|
|
5270
|
+
name: CHECK_NAMES.aliases,
|
|
5271
|
+
status: "failed",
|
|
5272
|
+
error: formatValidationError(err)
|
|
5273
|
+
}), (table) => ({
|
|
5274
|
+
name: CHECK_NAMES.aliases,
|
|
5275
|
+
status: "ok",
|
|
5276
|
+
detail: `${table.entries.size} alias(es)`
|
|
5277
|
+
}));
|
|
5278
|
+
const sealedCheck = checkSealedBlocks(config);
|
|
5226
5279
|
const checks = [
|
|
5227
5280
|
tomlOk,
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
config.catalog ? resolveConfig(config, dirname(path)).fold((err) => ({
|
|
5233
|
-
name: CHECK_NAMES.catalog,
|
|
5234
|
-
status: "failed",
|
|
5235
|
-
error: formatError(err)
|
|
5236
|
-
}), () => ({
|
|
5237
|
-
name: CHECK_NAMES.catalog,
|
|
5238
|
-
status: "ok",
|
|
5239
|
-
detail: config.catalog
|
|
5240
|
-
})) : {
|
|
5241
|
-
name: CHECK_NAMES.catalog,
|
|
5242
|
-
status: "na",
|
|
5243
|
-
detail: "no catalog declared"
|
|
5244
|
-
},
|
|
5245
|
-
validateAliases(config).fold((err) => ({
|
|
5246
|
-
name: CHECK_NAMES.aliases,
|
|
5247
|
-
status: "failed",
|
|
5248
|
-
error: formatValidationError(err)
|
|
5249
|
-
}), (table) => ({
|
|
5250
|
-
name: CHECK_NAMES.aliases,
|
|
5251
|
-
status: "ok",
|
|
5252
|
-
detail: `${table.entries.size} alias(es)`
|
|
5253
|
-
})),
|
|
5254
|
-
checkSealedBlocks(config)
|
|
5281
|
+
schemaOk,
|
|
5282
|
+
catalogCheck,
|
|
5283
|
+
aliasCheck,
|
|
5284
|
+
sealedCheck
|
|
5255
5285
|
];
|
|
5256
5286
|
return {
|
|
5257
5287
|
ok: checks.every((c) => c.status === "ok" || c.status === "na"),
|
|
@@ -5316,7 +5346,7 @@ const runValidate = (options) => {
|
|
|
5316
5346
|
}, ({ path, source }) => {
|
|
5317
5347
|
const sourceMsg = formatConfigSource(path, source);
|
|
5318
5348
|
Try(() => readFileSync(path, "utf-8")).fold((e) => {
|
|
5319
|
-
|
|
5349
|
+
const report = {
|
|
5320
5350
|
ok: false,
|
|
5321
5351
|
configPath: path,
|
|
5322
5352
|
checks: [{
|
|
@@ -5324,7 +5354,8 @@ const runValidate = (options) => {
|
|
|
5324
5354
|
status: "failed",
|
|
5325
5355
|
error: e.message
|
|
5326
5356
|
}]
|
|
5327
|
-
}
|
|
5357
|
+
};
|
|
5358
|
+
emit(report, sourceMsg, options.json === true);
|
|
5328
5359
|
process.exit(2);
|
|
5329
5360
|
}, (raw) => {
|
|
5330
5361
|
const report = buildReport(path, raw);
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { DirectLogger, DirectTestLoggerHandle, createDirectConsoleLogger, create
|
|
|
4
4
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
5
|
import { CallToolResult, ReadResourceResult, Resource } from "@modelcontextprotocol/sdk/types.js";
|
|
6
6
|
import { DirectLogger as DirectLogger$1, LogEntry, LogLevel, LogMetadata } from "functype-log";
|
|
7
|
-
|
|
8
7
|
//#region src/core/schema.d.ts
|
|
9
8
|
declare const ConsumerType: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"agent">, import("@sinclair/typebox").TLiteral<"service">, import("@sinclair/typebox").TLiteral<"developer">, import("@sinclair/typebox").TLiteral<"ci">]>;
|
|
10
9
|
type ConsumerType = Static<typeof ConsumerType>;
|
|
@@ -48,6 +47,7 @@ declare const SecretMetaSchema: import("@sinclair/typebox").TObject<{
|
|
|
48
47
|
encrypted_value: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
49
48
|
from_key: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
50
49
|
required: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
50
|
+
external: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
51
51
|
tags: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>>;
|
|
52
52
|
namespace: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
53
53
|
}>;
|
|
@@ -110,6 +110,7 @@ declare const EnvpktConfigSchema: import("@sinclair/typebox").TObject<{
|
|
|
110
110
|
encrypted_value: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
111
111
|
from_key: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
112
112
|
required: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
113
|
+
external: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
113
114
|
tags: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>>;
|
|
114
115
|
namespace: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
115
116
|
}>>>;
|
|
@@ -139,7 +140,7 @@ type EnvpktConfig = Static<typeof EnvpktConfigSchema>;
|
|
|
139
140
|
/** @deprecated Use `Identity` instead */
|
|
140
141
|
type AgentIdentity = Identity;
|
|
141
142
|
type HealthStatus = "healthy" | "degraded" | "critical";
|
|
142
|
-
type SecretStatus = "healthy" | "expiring_soon" | "expired" | "stale" | "missing" | "missing_metadata";
|
|
143
|
+
type SecretStatus = "healthy" | "expiring_soon" | "expired" | "stale" | "missing" | "missing_metadata" | "external";
|
|
143
144
|
type SecretHealth = {
|
|
144
145
|
readonly key: string;
|
|
145
146
|
readonly service: Option<string>;
|
|
@@ -150,7 +151,8 @@ type SecretHealth = {
|
|
|
150
151
|
readonly created: Option<string>;
|
|
151
152
|
readonly expires: Option<string>;
|
|
152
153
|
readonly last_rotated_at: Option<string>;
|
|
153
|
-
readonly issues: List<string>;
|
|
154
|
+
readonly issues: List<string>;
|
|
155
|
+
/** If this entry is an alias (from_key), the reference it points at (e.g. "secret.X") */
|
|
154
156
|
readonly alias_of: Option<string>;
|
|
155
157
|
};
|
|
156
158
|
type AuditResult = {
|
|
@@ -163,7 +165,10 @@ type AuditResult = {
|
|
|
163
165
|
readonly stale: number;
|
|
164
166
|
readonly missing: number;
|
|
165
167
|
readonly missing_metadata: number;
|
|
166
|
-
|
|
168
|
+
/** Count of entries whose value is managed outside envpkt (external = true). Reported, not a health problem. */
|
|
169
|
+
readonly external: number;
|
|
170
|
+
readonly orphaned: number;
|
|
171
|
+
/** Count of entries that are aliases (from_key). Included in `secrets` but reported separately for visibility. */
|
|
167
172
|
readonly aliases: number;
|
|
168
173
|
readonly identity?: Identity;
|
|
169
174
|
};
|
|
@@ -173,7 +178,8 @@ type EnvDriftEntry = {
|
|
|
173
178
|
readonly defaultValue: string;
|
|
174
179
|
readonly currentValue: string | undefined;
|
|
175
180
|
readonly status: EnvDriftStatus;
|
|
176
|
-
readonly purpose: string | undefined;
|
|
181
|
+
readonly purpose: string | undefined;
|
|
182
|
+
/** If this entry is an alias (from_key), the reference it points at (e.g. "env.X") */
|
|
177
183
|
readonly alias_of: Option<string>;
|
|
178
184
|
};
|
|
179
185
|
type EnvAuditResult = {
|
|
@@ -259,7 +265,8 @@ type CatalogError = {
|
|
|
259
265
|
readonly message: string;
|
|
260
266
|
};
|
|
261
267
|
type AliasTable = {
|
|
262
|
-
/** key → { type: "secret"|"env", targetType, targetKey } for every alias entry */
|
|
268
|
+
/** key → { type: "secret"|"env", targetType, targetKey } for every alias entry */
|
|
269
|
+
readonly entries: ReadonlyMap<string, {
|
|
263
270
|
readonly kind: "secret" | "env";
|
|
264
271
|
readonly targetKind: "secret" | "env";
|
|
265
272
|
readonly targetKey: string;
|
|
@@ -332,6 +339,7 @@ type BootError = ConfigError | FnoxError | CatalogError | AliasError | {
|
|
|
332
339
|
readonly _tag: "SealKeyUnavailable";
|
|
333
340
|
readonly sealedKeys: ReadonlyArray<string>;
|
|
334
341
|
readonly searched: ReadonlyArray<string>;
|
|
342
|
+
readonly configuredKeyFile?: string;
|
|
335
343
|
} | IdentityError;
|
|
336
344
|
type IdentityError = {
|
|
337
345
|
readonly _tag: "AgeNotFound";
|
|
@@ -571,7 +579,9 @@ type DotenvEntry = {
|
|
|
571
579
|
readonly secret: boolean;
|
|
572
580
|
};
|
|
573
581
|
type FormatDotenvOptions = {
|
|
574
|
-
/** Write secret values into the output. Default true (matches `env export`/`env github`). */
|
|
582
|
+
/** Write secret values into the output. Default true (matches `env export`/`env github`). */
|
|
583
|
+
readonly includeSecrets?: boolean;
|
|
584
|
+
/** A pre-formatted comment block (each line `#`-prefixed) placed at the top. */
|
|
575
585
|
readonly header?: string;
|
|
576
586
|
};
|
|
577
587
|
/**
|
package/dist/index.js
CHANGED
|
@@ -74,6 +74,7 @@ const SecretMetaSchema = Type.Object({
|
|
|
74
74
|
encrypted_value: Type.Optional(Type.String({ description: "Age-encrypted secret value (armored ciphertext, safe to commit)" })),
|
|
75
75
|
from_key: Type.Optional(Type.String({ description: "Reference another entry (format: 'secret.<KEY>') whose resolved value this alias reuses. Mutually exclusive with encrypted_value." })),
|
|
76
76
|
required: Type.Optional(Type.Boolean({ description: "Whether this secret is required for operation" })),
|
|
77
|
+
external: Type.Optional(Type.Boolean({ description: "Value is managed outside envpkt (e.g. a Cloudflare/Vault secret). The entry registers the name for inventory and dev/prod parity only: seal never touches it, and audit reports it as 'external' rather than 'missing'." })),
|
|
77
78
|
tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" })),
|
|
78
79
|
namespace: Type.Optional(Type.String({ description: "Override the file-level namespace for this entry's injected name. Empty string opts out of any prefix." }))
|
|
79
80
|
}, { description: "Metadata about a single secret" });
|
|
@@ -198,7 +199,8 @@ const walkUpForConfig = (dir) => {
|
|
|
198
199
|
};
|
|
199
200
|
/** Discover config by walking up from CWD, then ENVPKT_SEARCH_PATH, then dynamic Platform paths */
|
|
200
201
|
const discoverConfig = (cwd) => {
|
|
201
|
-
const
|
|
202
|
+
const dir = cwd ?? process.cwd();
|
|
203
|
+
const walked = walkUpForConfig(dir).orUndefined();
|
|
202
204
|
if (walked) return Option({
|
|
203
205
|
path: walked,
|
|
204
206
|
source: "cwd"
|
|
@@ -635,7 +637,8 @@ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration
|
|
|
635
637
|
const isExpiringSoon = daysRemaining.fold(() => false, (d) => d >= 0 && d <= WARN_BEFORE_DAYS);
|
|
636
638
|
const isStale = daysSinceRotation.fold(() => false, (d) => d > staleWarningDays);
|
|
637
639
|
const hasSealed = !!meta.encrypted_value;
|
|
638
|
-
const
|
|
640
|
+
const isExternal = meta.external === true;
|
|
641
|
+
const isMissing = !isExternal && fnoxKeys.size > 0 && !fnoxKeys.has(key) && !hasSealed;
|
|
639
642
|
const isMissingMetadata = requireExpiration && expires.isNone() || requireService && service.isNone();
|
|
640
643
|
if (isExpired) issues.push("Secret has expired");
|
|
641
644
|
if (isExpiringSoon) issues.push(`Expires in ${daysRemaining.fold(() => "?", (d) => String(d))} days`);
|
|
@@ -652,7 +655,7 @@ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration
|
|
|
652
655
|
return {
|
|
653
656
|
key,
|
|
654
657
|
service,
|
|
655
|
-
status: Cond.of().when(isExpired, "expired").elseWhen(isMissing, "missing").elseWhen(isMissingMetadata, "missing_metadata").elseWhen(isExpiringSoon, "expiring_soon").elseWhen(isStale, "stale").else("healthy"),
|
|
658
|
+
status: Cond.of().when(isExpired, "expired").elseWhen(isMissing, "missing").elseWhen(isMissingMetadata, "missing_metadata").elseWhen(isExpiringSoon, "expiring_soon").elseWhen(isStale, "stale").elseWhen(isExternal, "external").else("healthy"),
|
|
656
659
|
days_remaining: daysRemaining,
|
|
657
660
|
rotation_url: rotationUrl,
|
|
658
661
|
purpose,
|
|
@@ -691,7 +694,6 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
|
|
|
691
694
|
const secretEntries = config.secret ?? {};
|
|
692
695
|
const nonAliasEntries = Object.entries(secretEntries).filter(([, meta]) => meta.from_key === void 0);
|
|
693
696
|
const aliasEntries = Object.entries(secretEntries).filter(([, meta]) => meta.from_key !== void 0);
|
|
694
|
-
const nonAliasMetaKeys = Set$1(nonAliasEntries.map(([k]) => k));
|
|
695
697
|
const nonAliasHealth = nonAliasEntries.map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now));
|
|
696
698
|
const healthByKey = Map$1(nonAliasHealth.map((h) => [h.key, h]));
|
|
697
699
|
const parseTargetKey = (from_key) => {
|
|
@@ -717,13 +719,14 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
|
|
|
717
719
|
}), (health) => classifyAlias(key, meta, health, targetRef));
|
|
718
720
|
});
|
|
719
721
|
const secrets = List([...nonAliasHealth, ...aliasHealth]);
|
|
720
|
-
const orphaned = keys.size > 0 ?
|
|
722
|
+
const orphaned = keys.size > 0 ? nonAliasEntries.filter(([k, meta]) => !keys.has(k) && meta.external !== true).length : 0;
|
|
721
723
|
const total = secrets.size;
|
|
722
724
|
const expired = secrets.count((s) => s.status === "expired");
|
|
723
725
|
const missing = secrets.count((s) => s.status === "missing");
|
|
724
726
|
const missing_metadata = secrets.count((s) => s.status === "missing_metadata");
|
|
725
727
|
const expiring_soon = secrets.count((s) => s.status === "expiring_soon");
|
|
726
728
|
const stale = secrets.count((s) => s.status === "stale");
|
|
729
|
+
const external = secrets.count((s) => s.status === "external");
|
|
727
730
|
const healthy = secrets.count((s) => s.status === "healthy");
|
|
728
731
|
const aliases = aliasHealth.length;
|
|
729
732
|
return {
|
|
@@ -736,6 +739,7 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
|
|
|
736
739
|
stale,
|
|
737
740
|
missing,
|
|
738
741
|
missing_metadata,
|
|
742
|
+
external,
|
|
739
743
|
orphaned,
|
|
740
744
|
aliases,
|
|
741
745
|
identity: config.identity
|
|
@@ -747,7 +751,8 @@ const computeEnvAudit = (config, env = process.env) => {
|
|
|
747
751
|
const envWire = (key) => namer(key, envEntries[key]?.namespace);
|
|
748
752
|
const resolveEffectiveDefault = (entry) => {
|
|
749
753
|
return Do(function* () {
|
|
750
|
-
|
|
754
|
+
const targetKey = yield* $(Option((yield* $(Option(entry.from_key))).match(/^env\.(.+)$/)?.[1]));
|
|
755
|
+
return yield* $(Option((yield* $(Option(envEntries[targetKey]))).value));
|
|
751
756
|
}).orElse(entry.value ?? "");
|
|
752
757
|
};
|
|
753
758
|
const entries = Object.entries(envEntries).map(([key, entry]) => {
|
|
@@ -1596,7 +1601,8 @@ const fnoxExport = (profile, agentKey) => {
|
|
|
1596
1601
|
const eq = line.indexOf("=");
|
|
1597
1602
|
if (eq > 0) {
|
|
1598
1603
|
const key = line.slice(0, eq).trim();
|
|
1599
|
-
|
|
1604
|
+
const value = line.slice(eq + 1).trim();
|
|
1605
|
+
entries[key] = value;
|
|
1600
1606
|
}
|
|
1601
1607
|
});
|
|
1602
1608
|
return Right(entries);
|
|
@@ -1977,11 +1983,13 @@ const resolveSealIdentity = (config, configDir) => {
|
|
|
1977
1983
|
});
|
|
1978
1984
|
const inlineKey = resolveInlineKey().orUndefined();
|
|
1979
1985
|
if (inlineKey) return Option(materializeInlineKey(inlineKey));
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1986
|
+
if (!config.identity?.key_file) {
|
|
1987
|
+
const defaultPath = join(homedir(), ".envpkt", "age-key.txt");
|
|
1988
|
+
if (existsSync(defaultPath)) return Option({
|
|
1989
|
+
path: defaultPath,
|
|
1990
|
+
dispose: noop
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1985
1993
|
return Option(void 0);
|
|
1986
1994
|
};
|
|
1987
1995
|
/** Describe the seal-identity precedence chain with per-entry status, for a clear no-key error. */
|
|
@@ -1996,7 +2004,7 @@ const describeSealKeySearch = (config, configDir) => {
|
|
|
1996
2004
|
keyFileLine,
|
|
1997
2005
|
envFileLine,
|
|
1998
2006
|
inlineLine,
|
|
1999
|
-
`${defaultPath} (${existsSync(defaultPath) ? "found" : "missing"})`
|
|
2007
|
+
keyFile ? `${defaultPath} (skipped — identity.key_file is set)` : `${defaultPath} (${existsSync(defaultPath) ? "found" : "missing"})`
|
|
2000
2008
|
];
|
|
2001
2009
|
};
|
|
2002
2010
|
const resolveIdentityKey = (config, configDir) => {
|
|
@@ -2072,7 +2080,8 @@ const bootSafe = (options) => {
|
|
|
2072
2080
|
if (hasSealedValues && sealIdentity.isEmpty) return Left({
|
|
2073
2081
|
_tag: "SealKeyUnavailable",
|
|
2074
2082
|
sealedKeys: nonAliasMetaKeys.filter((k) => !!nonAliasSecretEntries[k]?.encrypted_value),
|
|
2075
|
-
searched: describeSealKeySearch(config, configDir)
|
|
2083
|
+
searched: describeSealKeySearch(config, configDir),
|
|
2084
|
+
configuredKeyFile: config.identity?.key_file ? resolve(configDir, expandPath(config.identity.key_file)) : void 0
|
|
2076
2085
|
});
|
|
2077
2086
|
return checkExpiration(audit, failOnExpired, warnOnly).map((warnings) => {
|
|
2078
2087
|
const secrets = {};
|
|
@@ -2167,7 +2176,8 @@ const bootSafe = (options) => {
|
|
|
2167
2176
|
}
|
|
2168
2177
|
const targetEntry = envEntries[entry.targetKey];
|
|
2169
2178
|
if (targetEntry?.value === void 0) return;
|
|
2170
|
-
|
|
2179
|
+
const resolvedTarget = process.env[envEnv(entry.targetKey)] ?? targetEntry.value;
|
|
2180
|
+
envDefaults[aliasKey] = resolvedTarget;
|
|
2171
2181
|
});
|
|
2172
2182
|
if (inject) {
|
|
2173
2183
|
Object.entries(envDefaults).forEach(([key, value]) => {
|
|
@@ -2225,12 +2235,13 @@ const formatBootError = (error) => {
|
|
|
2225
2235
|
case "SealKeyUnavailable": {
|
|
2226
2236
|
const keys = error.sealedKeys.length;
|
|
2227
2237
|
const searched = error.searched.map((line) => ` • ${line}`).join("\n");
|
|
2238
|
+
const restoreLine = error.configuredKeyFile ? ` • Restore the configured key to ${error.configuredKeyFile} (or set ENVPKT_AGE_KEY_FILE / ENVPKT_AGE_KEY)` : ` • Restore your key to ~/.envpkt/age-key.txt (or set ENVPKT_AGE_KEY_FILE / ENVPKT_AGE_KEY)`;
|
|
2228
2239
|
return [
|
|
2229
2240
|
`${keys} sealed secret(s) can't be decrypted — no age key found.`,
|
|
2230
2241
|
`Searched (in order):`,
|
|
2231
2242
|
searched,
|
|
2232
2243
|
`Fix one:`,
|
|
2233
|
-
|
|
2244
|
+
restoreLine,
|
|
2234
2245
|
` • Re-provision from source: envpkt seal --edit <KEY>`,
|
|
2235
2246
|
`Refusing to inject empty values for sealed secrets.`
|
|
2236
2247
|
].join("\n");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "envpkt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Credential lifecycle and fleet management for AI agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"credentials",
|
|
@@ -39,19 +39,19 @@
|
|
|
39
39
|
"prepublishOnly": "pnpm validate"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
43
|
-
"@sinclair/typebox": "^0.34.
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
43
|
+
"@sinclair/typebox": "^0.34.52",
|
|
44
44
|
"commander": "^15.0.0",
|
|
45
|
-
"functype": "^1.
|
|
46
|
-
"functype-log": "^1.
|
|
47
|
-
"functype-os": "^1.
|
|
48
|
-
"smol-toml": "^1.7.
|
|
45
|
+
"functype": "^1.8.0",
|
|
46
|
+
"functype-log": "^1.8.0",
|
|
47
|
+
"functype-os": "^1.8.0",
|
|
48
|
+
"smol-toml": "^1.7.1"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@types/node": "^24.13.
|
|
52
|
-
"ts-builds": "^3.2.
|
|
53
|
-
"tsdown": "^0.22.
|
|
54
|
-
"tsx": "^4.
|
|
51
|
+
"@types/node": "^24.13.3",
|
|
52
|
+
"ts-builds": "^3.2.3",
|
|
53
|
+
"tsdown": "^0.22.14",
|
|
54
|
+
"tsx": "^4.23.1"
|
|
55
55
|
},
|
|
56
56
|
"type": "module",
|
|
57
57
|
"main": "./dist/index.js",
|
|
@@ -71,5 +71,5 @@
|
|
|
71
71
|
"schemas"
|
|
72
72
|
],
|
|
73
73
|
"prettier": "ts-builds/prettier",
|
|
74
|
-
"packageManager": "pnpm@11.
|
|
74
|
+
"packageManager": "pnpm@11.18.0+sha512.33d83c77da82f49fba836925c6f1b841181ec3132b670639bd012f7075f5c7cf634c5f870147c19aae7478fac01df09d8892e880454896edd23ee9b33757563c"
|
|
75
75
|
}
|
|
@@ -195,6 +195,10 @@
|
|
|
195
195
|
"description": "Whether this secret is required for operation",
|
|
196
196
|
"type": "boolean"
|
|
197
197
|
},
|
|
198
|
+
"external": {
|
|
199
|
+
"description": "Value is managed outside envpkt (e.g. a Cloudflare/Vault secret). The entry registers the name for inventory and dev/prod parity only: seal never touches it, and audit reports it as 'external' rather than 'missing'.",
|
|
200
|
+
"type": "boolean"
|
|
201
|
+
},
|
|
198
202
|
"tags": {
|
|
199
203
|
"description": "Key-value tags for grouping and filtering",
|
|
200
204
|
"type": "object",
|