envpkt 0.14.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/cli.js +273 -64
- package/dist/index.d.ts +59 -1
- package/dist/index.js +171 -44
- package/package.json +10 -10
- package/schemas/envpkt.schema.json +29 -0
package/README.md
CHANGED
|
@@ -584,6 +584,21 @@ Inject resolved secrets into a GitHub Actions job. Emits `::add-mask::` for each
|
|
|
584
584
|
npx envpkt env github --strict
|
|
585
585
|
```
|
|
586
586
|
|
|
587
|
+
### `envpkt headers`
|
|
588
|
+
|
|
589
|
+
Print the HTTP headers for one MCP server as JSON, for Claude Code's `headersHelper`. Bind a secret to a server with `mcp = [{ server = "civala-github" }]` (optional `header`, default `"Authorization"`; `scheme`, default `"Bearer"`, `""` for the raw value).
|
|
590
|
+
|
|
591
|
+
```bash
|
|
592
|
+
envpkt headers civala-github # {"Authorization":"Bearer ..."}
|
|
593
|
+
envpkt headers # server from $CLAUDE_CODE_MCP_SERVER_NAME
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
```json
|
|
597
|
+
{ "type": "http", "url": "https://github.civala.ai/mcp", "headersHelper": "envpkt headers" }
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
stdout holds only the JSON; warnings and errors go to stderr. It decrypts sealed values through `identity.key_file`, so it works when Claude was launched with a stripped environment and `${VAR}` refs in `.mcp.json` would expand empty. Exits non-zero, naming the server, when nothing matches or a value does not resolve. Without `-c`, it finds `envpkt.toml` like every other command, falling back to the global package when the project has none.
|
|
601
|
+
|
|
587
602
|
### `envpkt mcp`
|
|
588
603
|
|
|
589
604
|
Start the envpkt MCP server (stdio transport) for AI agent integration.
|
package/dist/cli.js
CHANGED
|
@@ -123,14 +123,15 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
|
|
|
123
123
|
const staleWarningDays = lifecycle.stale_warning_days ?? 90;
|
|
124
124
|
const requireExpiration = lifecycle.require_expiration ?? false;
|
|
125
125
|
const requireService = lifecycle.require_service ?? false;
|
|
126
|
-
const keys = fnoxKeys ??
|
|
126
|
+
const keys = fnoxKeys ?? Set$1.empty();
|
|
127
127
|
const secretEntries = config.secret ?? {};
|
|
128
128
|
const nonAliasEntries = Object.entries(secretEntries).filter(([, meta]) => meta.from_key === void 0);
|
|
129
129
|
const aliasEntries = Object.entries(secretEntries).filter(([, meta]) => meta.from_key !== void 0);
|
|
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) => {
|
|
133
|
-
|
|
133
|
+
const match = from_key.match(/^secret\.(.+)$/);
|
|
134
|
+
return Option(match?.[1]);
|
|
134
135
|
};
|
|
135
136
|
const aliasHealth = aliasEntries.map(([key, meta]) => {
|
|
136
137
|
const tableEntry = aliasTable?.entries.get(`secret.${key}`);
|
|
@@ -184,8 +185,10 @@ const computeEnvAudit = (config, env = process.env) => {
|
|
|
184
185
|
const envWire = (key) => namer(key, envEntries[key]?.namespace);
|
|
185
186
|
const resolveEffectiveDefault = (entry) => {
|
|
186
187
|
return Do(function* () {
|
|
187
|
-
const
|
|
188
|
-
|
|
188
|
+
const fk = yield* $(Option(entry.from_key));
|
|
189
|
+
const targetKey = yield* $(Option(fk.match(/^env\.(.+)$/)?.[1]));
|
|
190
|
+
const targetEntry = yield* $(Option(envEntries[targetKey]));
|
|
191
|
+
return yield* $(Option(targetEntry.value));
|
|
189
192
|
}).orElse(entry.value ?? "");
|
|
190
193
|
};
|
|
191
194
|
const entries = Object.entries(envEntries).map(([key, entry]) => {
|
|
@@ -241,6 +244,21 @@ const NamespaceSchema = Type.Object({
|
|
|
241
244
|
description: "Separator between prefix and key. Default '__' (shell-safe). Note: '.' and ':' are not valid in shell identifiers."
|
|
242
245
|
}))
|
|
243
246
|
}, { description: "Optional namespace/package prefix for injected environment variable names" });
|
|
247
|
+
const McpHeaderSchema = Type.Object({
|
|
248
|
+
server: Type.String({
|
|
249
|
+
minLength: 1,
|
|
250
|
+
description: "MCP server name, as keyed in .mcp.json"
|
|
251
|
+
}),
|
|
252
|
+
header: Type.Optional(Type.String({
|
|
253
|
+
default: "Authorization",
|
|
254
|
+
minLength: 1,
|
|
255
|
+
description: "HTTP header name to send the secret in"
|
|
256
|
+
})),
|
|
257
|
+
scheme: Type.Optional(Type.String({
|
|
258
|
+
default: "Bearer",
|
|
259
|
+
description: "Prefix placed before the value (e.g. 'Bearer'). Empty string sends the raw value."
|
|
260
|
+
}))
|
|
261
|
+
}, { description: "Send this secret as an HTTP header to an MCP server" });
|
|
244
262
|
const SecretMetaSchema = Type.Object({
|
|
245
263
|
service: Type.Optional(Type.String({ description: "Service or system this secret authenticates to" })),
|
|
246
264
|
expires: Type.Optional(Type.String({
|
|
@@ -271,7 +289,8 @@ const SecretMetaSchema = Type.Object({
|
|
|
271
289
|
required: Type.Optional(Type.Boolean({ description: "Whether this secret is required for operation" })),
|
|
272
290
|
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'." })),
|
|
273
291
|
tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" })),
|
|
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." }))
|
|
292
|
+
namespace: Type.Optional(Type.String({ description: "Override the file-level namespace for this entry's injected name. Empty string opts out of any prefix." })),
|
|
293
|
+
mcp: Type.Optional(Type.Array(McpHeaderSchema, { description: "MCP servers this secret authenticates to. Read by `envpkt headers` (Claude Code headersHelper)." }))
|
|
275
294
|
}, { description: "Metadata about a single secret" });
|
|
276
295
|
const LifecycleConfigSchema = Type.Object({
|
|
277
296
|
stale_warning_days: Type.Optional(Type.Number({
|
|
@@ -399,10 +418,13 @@ const discoverConfig = (cwd) => {
|
|
|
399
418
|
template,
|
|
400
419
|
expanded: expandPath(template)
|
|
401
420
|
})).filter(({ expanded }) => expanded !== "" && !expanded.startsWith("/.envpkt")).map(({ expanded }) => expandGlobPath(expanded)).find((matches) => matches.length > 0);
|
|
402
|
-
if (customMatch)
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
421
|
+
if (customMatch) {
|
|
422
|
+
const found = {
|
|
423
|
+
path: customMatch[0],
|
|
424
|
+
source: "search"
|
|
425
|
+
};
|
|
426
|
+
return Option(found);
|
|
427
|
+
}
|
|
406
428
|
const platformMatch = buildSearchPaths().find((p) => Fs.existsSync(p));
|
|
407
429
|
if (platformMatch) return Option({
|
|
408
430
|
path: platformMatch,
|
|
@@ -444,9 +466,10 @@ const parseToml = (raw) => Try(() => parse(raw)).fold((err) => Left({
|
|
|
444
466
|
/** Validate parsed data against the TypeBox schema */
|
|
445
467
|
const validateConfig = (data) => {
|
|
446
468
|
if (compiledSchema.Check(data)) return Right(data);
|
|
469
|
+
const errors = List([...compiledSchema.Errors(data)].map((e) => `${e.path}: ${e.message}`));
|
|
447
470
|
return Left({
|
|
448
471
|
_tag: "ValidationError",
|
|
449
|
-
errors
|
|
472
|
+
errors
|
|
450
473
|
});
|
|
451
474
|
};
|
|
452
475
|
/** Load and validate an envpkt.toml from a file path */
|
|
@@ -856,7 +879,8 @@ const runAudit = (options) => {
|
|
|
856
879
|
console.error(formatError(err));
|
|
857
880
|
process.exit(2);
|
|
858
881
|
}, (rawConfig) => {
|
|
859
|
-
|
|
882
|
+
const configDir = dirname(path);
|
|
883
|
+
resolveConfig(rawConfig, configDir).fold((err) => {
|
|
860
884
|
console.error(formatError(err));
|
|
861
885
|
process.exit(2);
|
|
862
886
|
}, (resolveResult) => {
|
|
@@ -920,8 +944,10 @@ const runAuditOnConfig = (config, options) => {
|
|
|
920
944
|
if (options.format === "json") console.log(formatAuditJson(sorted));
|
|
921
945
|
else if (options.format === "minimal") console.log(formatAuditMinimal(sorted));
|
|
922
946
|
else console.log(formatAudit(sorted));
|
|
923
|
-
if (options.all)
|
|
924
|
-
|
|
947
|
+
if (options.all) {
|
|
948
|
+
if (options.format === "json") console.log(formatEnvAuditJson(config));
|
|
949
|
+
else formatEnvAuditTable(config);
|
|
950
|
+
}
|
|
925
951
|
const code = options.strict ? exitCodeForAudit(audit) : audit.status === "critical" ? 2 : 0;
|
|
926
952
|
process.exit(code);
|
|
927
953
|
};
|
|
@@ -1497,7 +1523,8 @@ const bootSafe = (options) => {
|
|
|
1497
1523
|
_tag: "ReadError",
|
|
1498
1524
|
message: "unexpected"
|
|
1499
1525
|
}));
|
|
1500
|
-
const
|
|
1526
|
+
const fnoxKeys = detectFnoxKeys(configDir);
|
|
1527
|
+
const audit = computeAudit(config, fnoxKeys, void 0, aliasTable);
|
|
1501
1528
|
const sealIdentity = hasSealedValues ? resolveSealIdentity(config, configDir) : Option(void 0);
|
|
1502
1529
|
if (hasSealedValues && sealIdentity.isEmpty) return Left({
|
|
1503
1530
|
_tag: "SealKeyUnavailable",
|
|
@@ -1539,37 +1566,39 @@ const bootSafe = (options) => {
|
|
|
1539
1566
|
}
|
|
1540
1567
|
});
|
|
1541
1568
|
const remainingKeys = nonAliasMetaKeys.filter((k) => !sealedKeys.has(k));
|
|
1542
|
-
if (remainingKeys.length > 0)
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
warnings.push(`fnox export failed: ${err.message}`);
|
|
1548
|
-
skipped.push(...remainingKeys);
|
|
1549
|
-
}, (exported) => {
|
|
1550
|
-
const found = remainingKeys.filter((key) => key in exported);
|
|
1551
|
-
const notFound = remainingKeys.filter((key) => !(key in exported));
|
|
1552
|
-
found.forEach((key) => {
|
|
1553
|
-
secrets[key] = exported[key];
|
|
1554
|
-
log.debug("phase.fnox.resolved", {
|
|
1555
|
-
key,
|
|
1556
|
-
profile: opts.profile
|
|
1569
|
+
if (remainingKeys.length > 0) {
|
|
1570
|
+
if (fnoxAvailable()) fnoxExport(opts.profile, identityKey.orUndefined()).fold((err) => {
|
|
1571
|
+
log.warn("phase.fnox.export_failed", {
|
|
1572
|
+
message: err.message,
|
|
1573
|
+
skipped: remainingKeys.length
|
|
1557
1574
|
});
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1575
|
+
warnings.push(`fnox export failed: ${err.message}`);
|
|
1576
|
+
skipped.push(...remainingKeys);
|
|
1577
|
+
}, (exported) => {
|
|
1578
|
+
const found = remainingKeys.filter((key) => key in exported);
|
|
1579
|
+
const notFound = remainingKeys.filter((key) => !(key in exported));
|
|
1580
|
+
found.forEach((key) => {
|
|
1581
|
+
secrets[key] = exported[key];
|
|
1582
|
+
log.debug("phase.fnox.resolved", {
|
|
1583
|
+
key,
|
|
1584
|
+
profile: opts.profile
|
|
1585
|
+
});
|
|
1586
|
+
});
|
|
1587
|
+
notFound.forEach((key) => {
|
|
1588
|
+
log.debug("phase.fnox.not_in_export", {
|
|
1589
|
+
key,
|
|
1590
|
+
profile: opts.profile
|
|
1591
|
+
});
|
|
1563
1592
|
});
|
|
1593
|
+
injected.push(...found);
|
|
1594
|
+
skipped.push(...notFound);
|
|
1564
1595
|
});
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
else warnings.push("fnox not available — unsealed secrets could not be resolved");
|
|
1572
|
-
skipped.push(...remainingKeys);
|
|
1596
|
+
else {
|
|
1597
|
+
log.debug("phase.fnox.unavailable", { skipped: remainingKeys.length });
|
|
1598
|
+
if (!hasSealedValues) warnings.push("fnox not available — no secrets injected");
|
|
1599
|
+
else warnings.push("fnox not available — unsealed secrets could not be resolved");
|
|
1600
|
+
skipped.push(...remainingKeys);
|
|
1601
|
+
}
|
|
1573
1602
|
}
|
|
1574
1603
|
aliasSecretKeys.forEach((aliasKey) => {
|
|
1575
1604
|
const entry = aliasTable.entries.get(`secret.${aliasKey}`);
|
|
@@ -1964,16 +1993,114 @@ const sortConfigToml = (raw) => {
|
|
|
1964
1993
|
return `${parts.join("\n\n")}\n`;
|
|
1965
1994
|
};
|
|
1966
1995
|
//#endregion
|
|
1996
|
+
//#region src/core/mcp-headers.ts
|
|
1997
|
+
const DEFAULT_HEADER = "Authorization";
|
|
1998
|
+
const DEFAULT_SCHEME = "Bearer";
|
|
1999
|
+
/** RFC 9110 `token`: the legal characters of a header name (and of an auth scheme). */
|
|
2000
|
+
const TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
2001
|
+
/** Characters that would split or corrupt an HTTP header value. */
|
|
2002
|
+
const UNSAFE_VALUE_RE = /[\r\n\0]/;
|
|
2003
|
+
/** Flatten every `[secret.*].mcp` entry into one binding per (secret, server), with defaults applied. */
|
|
2004
|
+
const collectBindings = (config) => Object.entries(config.secret ?? {}).flatMap(([key, meta]) => (meta.mcp ?? []).map((m) => ({
|
|
2005
|
+
key,
|
|
2006
|
+
server: m.server,
|
|
2007
|
+
header: m.header ?? DEFAULT_HEADER,
|
|
2008
|
+
scheme: m.scheme ?? DEFAULT_SCHEME
|
|
2009
|
+
})));
|
|
2010
|
+
/**
|
|
2011
|
+
* Check bindings for structural problems: header names and schemes that are not HTTP
|
|
2012
|
+
* tokens, and two bindings claiming the same (server, header) pair. Header names compare
|
|
2013
|
+
* case-insensitively, as HTTP does.
|
|
2014
|
+
*/
|
|
2015
|
+
const validateBindings = (bindings) => {
|
|
2016
|
+
const badHeader = bindings.find((b) => !TOKEN_RE.test(b.header));
|
|
2017
|
+
if (badHeader) return Left({
|
|
2018
|
+
_tag: "McpHeaderInvalid",
|
|
2019
|
+
key: badHeader.key,
|
|
2020
|
+
server: badHeader.server,
|
|
2021
|
+
header: badHeader.header
|
|
2022
|
+
});
|
|
2023
|
+
const badScheme = bindings.find((b) => b.scheme !== "" && !TOKEN_RE.test(b.scheme));
|
|
2024
|
+
if (badScheme) return Left({
|
|
2025
|
+
_tag: "McpSchemeInvalid",
|
|
2026
|
+
key: badScheme.key,
|
|
2027
|
+
server: badScheme.server,
|
|
2028
|
+
scheme: badScheme.scheme
|
|
2029
|
+
});
|
|
2030
|
+
const claims = bindings.reduce((acc, b) => {
|
|
2031
|
+
const id = `${b.server}\u0000${b.header.toLowerCase()}`;
|
|
2032
|
+
return {
|
|
2033
|
+
...acc,
|
|
2034
|
+
[id]: [...acc[id] ?? [], b]
|
|
2035
|
+
};
|
|
2036
|
+
}, {});
|
|
2037
|
+
const clash = Object.values(claims).find((group) => group.length > 1);
|
|
2038
|
+
if (clash) {
|
|
2039
|
+
const first = clash[0];
|
|
2040
|
+
return Left({
|
|
2041
|
+
_tag: "McpHeaderDuplicate",
|
|
2042
|
+
server: first.server,
|
|
2043
|
+
header: first.header,
|
|
2044
|
+
keys: Set$1(clash.map((b) => b.key)).toArray()
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
return Right(void 0);
|
|
2048
|
+
};
|
|
2049
|
+
/** Check every `mcp` binding in a config. Used by `envpkt validate` and the write gate. */
|
|
2050
|
+
const validateMcpHeaders = (config) => validateBindings(collectBindings(config));
|
|
2051
|
+
/**
|
|
2052
|
+
* Build the HTTP header object for one MCP server: every secret whose `mcp` list names
|
|
2053
|
+
* `server`, merged into one object. `values` is keyed by logical secret name (as in
|
|
2054
|
+
* `BootResult.secrets`), so namespaces and aliases are already resolved by the caller.
|
|
2055
|
+
* Only this server's bindings are checked, so a problem on another server never blocks it.
|
|
2056
|
+
*/
|
|
2057
|
+
const buildMcpHeaders = (config, values, server) => {
|
|
2058
|
+
const matches = collectBindings(config).filter((b) => b.server === server);
|
|
2059
|
+
if (matches.length === 0) return Left({
|
|
2060
|
+
_tag: "McpServerUnknown",
|
|
2061
|
+
server
|
|
2062
|
+
});
|
|
2063
|
+
return validateBindings(matches).flatMap(() => {
|
|
2064
|
+
const unresolved = matches.filter((b) => !values[b.key]).map((b) => b.key);
|
|
2065
|
+
if (unresolved.length > 0) return Left({
|
|
2066
|
+
_tag: "McpValueUnresolved",
|
|
2067
|
+
server,
|
|
2068
|
+
keys: unresolved
|
|
2069
|
+
});
|
|
2070
|
+
const unsafe = matches.filter((b) => UNSAFE_VALUE_RE.test(values[b.key])).map((b) => b.key);
|
|
2071
|
+
if (unsafe.length > 0) return Left({
|
|
2072
|
+
_tag: "McpValueInvalid",
|
|
2073
|
+
server,
|
|
2074
|
+
keys: unsafe
|
|
2075
|
+
});
|
|
2076
|
+
return Right(Object.fromEntries(matches.map((b) => {
|
|
2077
|
+
const value = values[b.key];
|
|
2078
|
+
return [b.header, b.scheme === "" ? value : `${b.scheme} ${value}`];
|
|
2079
|
+
})));
|
|
2080
|
+
});
|
|
2081
|
+
};
|
|
2082
|
+
/** Human-readable one-liner for any McpHeadersError tag. Never includes a secret value. */
|
|
2083
|
+
const formatMcpHeadersError = (error) => {
|
|
2084
|
+
switch (error._tag) {
|
|
2085
|
+
case "McpHeaderInvalid": return `[secret.${error.key}] mcp server "${error.server}": header "${error.header}" is not a valid HTTP header name`;
|
|
2086
|
+
case "McpSchemeInvalid": return `[secret.${error.key}] mcp server "${error.server}": scheme "${error.scheme}" must be a single word or ""`;
|
|
2087
|
+
case "McpHeaderDuplicate": return `MCP server "${error.server}": header "${error.header}" is claimed by more than one binding: ${error.keys.join(", ")}`;
|
|
2088
|
+
case "McpServerUnknown": return `No secret declares mcp server "${error.server}"`;
|
|
2089
|
+
case "McpValueUnresolved": return `MCP server "${error.server}": no value resolved for ${error.keys.join(", ")}`;
|
|
2090
|
+
case "McpValueInvalid": return `MCP server "${error.server}": value of ${error.keys.join(", ")} contains a line break or NUL and cannot be sent as a header`;
|
|
2091
|
+
}
|
|
2092
|
+
};
|
|
2093
|
+
//#endregion
|
|
1967
2094
|
//#region src/core/validate.ts
|
|
1968
2095
|
/**
|
|
1969
|
-
* Validate a raw TOML string as a complete envpkt config: parse → schema → aliases.
|
|
2096
|
+
* Validate a raw TOML string as a complete envpkt config: parse → schema → aliases → mcp.
|
|
1970
2097
|
*
|
|
1971
2098
|
* Used by write-path CLI commands to verify the post-edit file would still be
|
|
1972
2099
|
* structurally valid before persisting. Catalog resolution is intentionally
|
|
1973
2100
|
* excluded — catalog issues depend on external files, not on the local edit,
|
|
1974
2101
|
* and `envpkt validate` covers them as a separate explicit check.
|
|
1975
2102
|
*/
|
|
1976
|
-
const validateRawConfig = (raw) => parseToml(raw).flatMap(validateConfig).flatMap((config) => validateAliases(config).map(() => config));
|
|
2103
|
+
const validateRawConfig = (raw) => parseToml(raw).flatMap(validateConfig).flatMap((config) => validateAliases(config).map(() => config)).flatMap((config) => validateMcpHeaders(config).map(() => config));
|
|
1977
2104
|
/** Human-readable one-liner for any ValidationError tag. */
|
|
1978
2105
|
const formatValidationError = (err) => {
|
|
1979
2106
|
switch (err._tag) {
|
|
@@ -1987,6 +2114,9 @@ const formatValidationError = (err) => {
|
|
|
1987
2114
|
case "AliasChained":
|
|
1988
2115
|
case "AliasCrossType":
|
|
1989
2116
|
case "AliasValueConflict": return formatAliasError(err);
|
|
2117
|
+
case "McpHeaderInvalid":
|
|
2118
|
+
case "McpSchemeInvalid":
|
|
2119
|
+
case "McpHeaderDuplicate": return formatMcpHeadersError(err);
|
|
1990
2120
|
}
|
|
1991
2121
|
};
|
|
1992
2122
|
//#endregion
|
|
@@ -2073,14 +2203,16 @@ const sealedSecretBlock = (key, destName, srcConfig, srcPath, destConfig) => {
|
|
|
2073
2203
|
console.error(`${RED}Error:${RESET} "${key}" produced no plaintext on unseal.`);
|
|
2074
2204
|
return process.exit(2);
|
|
2075
2205
|
}
|
|
2206
|
+
const cipher = orExit(ageEncrypt(plaintext, recipient), (err) => `${RED}Error:${RESET} reseal failed for "${destName}": ${err.message}`, 2);
|
|
2076
2207
|
return serializeSecretBlock(destName, copyableSecretMeta(meta, {
|
|
2077
2208
|
today,
|
|
2078
|
-
encryptedValue: Option(
|
|
2209
|
+
encryptedValue: Option(cipher)
|
|
2079
2210
|
}));
|
|
2080
2211
|
};
|
|
2081
2212
|
const writeBlock = (destPath, header, block, overwrite, dryRun, successMsg) => {
|
|
2082
2213
|
const raw = readFileSync(destPath, "utf-8");
|
|
2083
|
-
const
|
|
2214
|
+
const baseRaw = overwrite ? orExit(removeSection(raw, header), (err) => `${RED}Error:${RESET} ${err._tag}: ${err.section}`, 2) : raw;
|
|
2215
|
+
const updated = appendSection(baseRaw, block);
|
|
2084
2216
|
if (dryRun) {
|
|
2085
2217
|
previewIfValid(updated, block);
|
|
2086
2218
|
return;
|
|
@@ -2121,7 +2253,8 @@ const runCopy = (key, options) => {
|
|
|
2121
2253
|
process.exit(1);
|
|
2122
2254
|
}
|
|
2123
2255
|
const block = inSecret ? sealedSecretBlock(key, destName, srcConfig, src.path, destConfig) : serializeEnvBlock(destName, srcConfig.env[key]);
|
|
2124
|
-
const
|
|
2256
|
+
const renamed = destName !== key ? ` → ${BOLD}${destName}${RESET}` : "";
|
|
2257
|
+
const successMsg = `${GREEN}✓${RESET} Copied ${kindWord} ${BOLD}${key}${RESET}${renamed} to ${CYAN}${dest.path}${RESET}`;
|
|
2125
2258
|
writeBlock(dest.path, `[${kindWord}.${destName}]`, block, existsInDest, options.dryRun === true, successMsg);
|
|
2126
2259
|
};
|
|
2127
2260
|
//#endregion
|
|
@@ -2906,7 +3039,8 @@ const VALUE_SHAPE_PATTERNS = [
|
|
|
2906
3039
|
];
|
|
2907
3040
|
/** Detect service from value prefix/shape */
|
|
2908
3041
|
const matchValueShape = (value) => {
|
|
2909
|
-
|
|
3042
|
+
const match = VALUE_SHAPE_PATTERNS.find((vp) => value.startsWith(vp.prefix));
|
|
3043
|
+
return Option(match).map((vp) => ({
|
|
2910
3044
|
service: vp.service,
|
|
2911
3045
|
description: vp.description
|
|
2912
3046
|
}));
|
|
@@ -2946,7 +3080,8 @@ const matchEnvVar = (name, value) => {
|
|
|
2946
3080
|
matchedBy: `exact:${exactMatch.pattern}`
|
|
2947
3081
|
});
|
|
2948
3082
|
return matchValueShape(value).fold(() => {
|
|
2949
|
-
|
|
3083
|
+
const suffixMatch = SUFFIX_PATTERNS.find((sp) => name.endsWith(sp.suffix));
|
|
3084
|
+
return Option(suffixMatch).map((sp) => ({
|
|
2950
3085
|
envVar: name,
|
|
2951
3086
|
value,
|
|
2952
3087
|
service: Option(deriveServiceFromName(name)),
|
|
@@ -3148,7 +3283,8 @@ const runEnvCheck = (options) => {
|
|
|
3148
3283
|
console.error(formatError(err));
|
|
3149
3284
|
process.exit(2);
|
|
3150
3285
|
}, (rawConfig) => {
|
|
3151
|
-
|
|
3286
|
+
const configDir = dirname(path);
|
|
3287
|
+
resolveConfig(rawConfig, configDir).fold((err) => {
|
|
3152
3288
|
console.error(formatError(err));
|
|
3153
3289
|
process.exit(2);
|
|
3154
3290
|
}, (resolveResult) => {
|
|
@@ -3319,7 +3455,9 @@ const runEnvAdd = (name, value, options) => {
|
|
|
3319
3455
|
console.log(block);
|
|
3320
3456
|
return;
|
|
3321
3457
|
}
|
|
3322
|
-
|
|
3458
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
3459
|
+
const updated = appendSection(raw, block);
|
|
3460
|
+
writeIfValid(configPath, updated, `${GREEN}✓${RESET} Added ${BOLD}${name}${RESET} to ${CYAN}${configPath}${RESET}`);
|
|
3323
3461
|
});
|
|
3324
3462
|
});
|
|
3325
3463
|
};
|
|
@@ -3440,7 +3578,9 @@ const runEnvAlias = (name, options) => {
|
|
|
3440
3578
|
return;
|
|
3441
3579
|
}
|
|
3442
3580
|
const raw = readFileSync(configPath, "utf-8");
|
|
3443
|
-
|
|
3581
|
+
const base = existing ? removeSection(raw, `[env.${name}]`).fold(() => raw, (r) => r) : raw;
|
|
3582
|
+
const updated = appendSection(base, block);
|
|
3583
|
+
writeIfValid(configPath, updated, `${GREEN}✓${RESET} Aliased ${BOLD}${name}${RESET} → ${BOLD}${options.from}${RESET} in ${CYAN}${configPath}${RESET}`);
|
|
3444
3584
|
});
|
|
3445
3585
|
});
|
|
3446
3586
|
};
|
|
@@ -3565,7 +3705,7 @@ function* findEnvpktFiles(dir, maxDepth, currentDepth = 0) {
|
|
|
3565
3705
|
}
|
|
3566
3706
|
const scanFleet = (rootDir, options) => {
|
|
3567
3707
|
const maxDepth = options?.maxDepth ?? 3;
|
|
3568
|
-
const
|
|
3708
|
+
const agents = Array.from(findEnvpktFiles(rootDir, maxDepth)).flatMap((configPath) => loadConfig(configPath).fold(() => [], (config) => {
|
|
3569
3709
|
const audit = computeAudit(config);
|
|
3570
3710
|
return [{
|
|
3571
3711
|
path: configPath,
|
|
@@ -3573,7 +3713,8 @@ const scanFleet = (rootDir, options) => {
|
|
|
3573
3713
|
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(),
|
|
3574
3714
|
audit
|
|
3575
3715
|
}];
|
|
3576
|
-
}))
|
|
3716
|
+
}));
|
|
3717
|
+
const agentList = List(agents);
|
|
3577
3718
|
const total_agents = agentList.size;
|
|
3578
3719
|
const total_secrets = agentList.toArray().reduce((acc, a) => acc + a.audit.total, 0);
|
|
3579
3720
|
const expired = agentList.toArray().reduce((acc, a) => acc + a.audit.expired, 0);
|
|
@@ -3599,7 +3740,8 @@ const statusIcon = (status) => {
|
|
|
3599
3740
|
}
|
|
3600
3741
|
};
|
|
3601
3742
|
const runFleet = (options) => {
|
|
3602
|
-
const
|
|
3743
|
+
const rootDir = resolve(options.dir ?? ".");
|
|
3744
|
+
const fleet = scanFleet(rootDir, { maxDepth: options.depth });
|
|
3603
3745
|
if (options.format === "json") {
|
|
3604
3746
|
console.log(formatFleetJson(fleet));
|
|
3605
3747
|
process.exit(fleet.status === "critical" ? 2 : 0);
|
|
@@ -3618,6 +3760,36 @@ const runFleet = (options) => {
|
|
|
3618
3760
|
process.exit(fleet.status === "critical" ? 2 : 0);
|
|
3619
3761
|
};
|
|
3620
3762
|
//#endregion
|
|
3763
|
+
//#region src/cli/commands/headers.ts
|
|
3764
|
+
const SERVER_ENV = "CLAUDE_CODE_MCP_SERVER_NAME";
|
|
3765
|
+
/**
|
|
3766
|
+
* Claude Code `headersHelper` entry point: print the HTTP headers for one MCP server as a
|
|
3767
|
+
* JSON object. stdout carries the JSON and nothing else; every diagnostic goes to stderr,
|
|
3768
|
+
* because Claude parses stdout verbatim.
|
|
3769
|
+
*/
|
|
3770
|
+
const runHeaders = (serverArg, options) => {
|
|
3771
|
+
const server = serverArg ?? process.env[SERVER_ENV];
|
|
3772
|
+
if (!server) {
|
|
3773
|
+
console.error(`${RED}Error:${RESET} No MCP server named. Pass [server] or set ${SERVER_ENV}.`);
|
|
3774
|
+
process.exit(2);
|
|
3775
|
+
}
|
|
3776
|
+
resolveForEmit(options).fold((err) => {
|
|
3777
|
+
console.error(`MCP server "${server}":\n${formatError(err)}`);
|
|
3778
|
+
process.exit(2);
|
|
3779
|
+
}, (boot) => {
|
|
3780
|
+
emitWarnings(boot);
|
|
3781
|
+
loadConfig(boot.configPath).flatMap((config) => resolveConfig(config, dirname(boot.configPath)).map((r) => r.config)).fold((err) => {
|
|
3782
|
+
console.error(`MCP server "${server}":\n${formatError(err)}`);
|
|
3783
|
+
return process.exit(2);
|
|
3784
|
+
}, (config) => buildMcpHeaders(config, boot.secrets, server)).fold((err) => {
|
|
3785
|
+
console.error(`${RED}Error:${RESET} ${formatMcpHeadersError(err)}`);
|
|
3786
|
+
process.exit(1);
|
|
3787
|
+
}, (map) => {
|
|
3788
|
+
process.stdout.write(`${JSON.stringify(map)}\n`);
|
|
3789
|
+
});
|
|
3790
|
+
});
|
|
3791
|
+
};
|
|
3792
|
+
//#endregion
|
|
3621
3793
|
//#region src/cli/commands/init.ts
|
|
3622
3794
|
const CONFIG_FILENAME = "envpkt.toml";
|
|
3623
3795
|
/** Resolve the top-level `scope` to scaffold: explicit --scope wins; --global implies "shell". */
|
|
@@ -3893,7 +4065,8 @@ const deriveKeyPath = (configPath) => {
|
|
|
3893
4065
|
};
|
|
3894
4066
|
const runKeygen = (options) => {
|
|
3895
4067
|
const configPath = resolve(options.config ?? join(process.cwd(), "envpkt.toml"));
|
|
3896
|
-
|
|
4068
|
+
const outputPath = options.output ?? (options.global ? resolveKeyPath() : deriveKeyPath(configPath));
|
|
4069
|
+
generateKeypair({ outputPath }).fold((err) => {
|
|
3897
4070
|
if (err._tag === "KeyExists") {
|
|
3898
4071
|
console.error(`${YELLOW}Warning:${RESET} Identity file already exists: ${CYAN}${err.path}${RESET}`);
|
|
3899
4072
|
console.error(`${DIM}To replace it: remove the file first, then re-run keygen.${RESET}`);
|
|
@@ -4263,7 +4436,8 @@ const runResolve = (options) => {
|
|
|
4263
4436
|
console.error(formatError(err));
|
|
4264
4437
|
process.exit(2);
|
|
4265
4438
|
}, (config) => {
|
|
4266
|
-
|
|
4439
|
+
const configDir = dirname(configPath);
|
|
4440
|
+
resolveConfig(config, configDir).fold((err) => {
|
|
4267
4441
|
console.error(formatError(err));
|
|
4268
4442
|
process.exit(2);
|
|
4269
4443
|
}, (result) => {
|
|
@@ -4490,7 +4664,8 @@ const runSeal = async (options) => {
|
|
|
4490
4664
|
process.exit(2);
|
|
4491
4665
|
}
|
|
4492
4666
|
const identityKey = Option(config.identity.key_file).flatMap((keyFile) => {
|
|
4493
|
-
|
|
4667
|
+
const identityPath = resolve(configDir, expandPath(keyFile));
|
|
4668
|
+
return unwrapAgentKey(identityPath).fold((err) => {
|
|
4494
4669
|
const msg = err._tag === "IdentityNotFound" ? `not found: ${err.path}` : err.message;
|
|
4495
4670
|
console.error(`${YELLOW}Warning:${RESET} Could not unwrap agent key: ${msg}`);
|
|
4496
4671
|
return Option.none();
|
|
@@ -4586,7 +4761,8 @@ const runSeal = async (options) => {
|
|
|
4586
4761
|
console.error(`${DIM} 3. ~/.envpkt/age-key.txt${RESET}`);
|
|
4587
4762
|
process.exit(2);
|
|
4588
4763
|
}
|
|
4589
|
-
const
|
|
4764
|
+
const sealedEntries = Object.fromEntries(alreadySealed.map((k) => [k, allSecretEntries[k]]));
|
|
4765
|
+
const decrypted = unsealSecrets(sealedEntries, identityPath).fold((err) => {
|
|
4590
4766
|
console.error(`${RED}Error:${RESET} Failed to decrypt existing secrets: ${err.message}`);
|
|
4591
4767
|
process.exit(2);
|
|
4592
4768
|
return {};
|
|
@@ -4723,7 +4899,8 @@ const runSecretAdd = (name, options) => {
|
|
|
4723
4899
|
process.exit(1);
|
|
4724
4900
|
}
|
|
4725
4901
|
const block = buildSecretBlock(name, options);
|
|
4726
|
-
const
|
|
4902
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
4903
|
+
const updated = appendSection(raw, block);
|
|
4727
4904
|
if (options.dryRun) {
|
|
4728
4905
|
previewIfValid(updated, block);
|
|
4729
4906
|
return;
|
|
@@ -4864,7 +5041,9 @@ const runSecretAlias = (name, options) => {
|
|
|
4864
5041
|
return;
|
|
4865
5042
|
}
|
|
4866
5043
|
const raw = readFileSync(configPath, "utf-8");
|
|
4867
|
-
|
|
5044
|
+
const base = existing ? removeSection(raw, `[secret.${name}]`).fold(() => raw, (r) => r) : raw;
|
|
5045
|
+
const updated = appendSection(base, block);
|
|
5046
|
+
writeIfValid(configPath, updated, `${GREEN}✓${RESET} Aliased ${BOLD}${name}${RESET} → ${BOLD}${options.from}${RESET} in ${CYAN}${configPath}${RESET}`);
|
|
4868
5047
|
});
|
|
4869
5048
|
});
|
|
4870
5049
|
};
|
|
@@ -4947,7 +5126,8 @@ const runSecretRotate = async (name, options) => {
|
|
|
4947
5126
|
process.exit(2);
|
|
4948
5127
|
return "";
|
|
4949
5128
|
}, (ct) => ct);
|
|
4950
|
-
|
|
5129
|
+
const withSealed = applySealedToml(raw, { [name]: { encrypted_value: ciphertext } });
|
|
5130
|
+
updateSectionFields(withSealed, `[secret.${name}]`, { last_rotated_at: `"${today}"` }).fold((err) => {
|
|
4951
5131
|
console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
|
|
4952
5132
|
process.exit(2);
|
|
4953
5133
|
}, (result) => {
|
|
@@ -5182,6 +5362,7 @@ const CHECK_NAMES = {
|
|
|
5182
5362
|
schema: "Schema",
|
|
5183
5363
|
catalog: "Catalog",
|
|
5184
5364
|
aliases: "Aliases",
|
|
5365
|
+
mcp: "MCP headers",
|
|
5185
5366
|
sealed: "Sealed blocks"
|
|
5186
5367
|
};
|
|
5187
5368
|
/** Structural sanity scan over each [secret.*].encrypted_value PEM block. No decryption. */
|
|
@@ -5210,6 +5391,24 @@ const checkSealedBlocks = (config) => {
|
|
|
5210
5391
|
error: broken.join("; ")
|
|
5211
5392
|
};
|
|
5212
5393
|
};
|
|
5394
|
+
/** `mcp` bindings: valid header names and schemes, no (server, header) pair claimed twice. */
|
|
5395
|
+
const checkMcpHeaders = (config) => {
|
|
5396
|
+
const count = Object.values(config.secret ?? {}).reduce((n, meta) => n + (meta.mcp?.length ?? 0), 0);
|
|
5397
|
+
if (count === 0) return {
|
|
5398
|
+
name: CHECK_NAMES.mcp,
|
|
5399
|
+
status: "na",
|
|
5400
|
+
detail: "no mcp bindings"
|
|
5401
|
+
};
|
|
5402
|
+
return validateMcpHeaders(config).fold((err) => ({
|
|
5403
|
+
name: CHECK_NAMES.mcp,
|
|
5404
|
+
status: "failed",
|
|
5405
|
+
error: formatValidationError(err)
|
|
5406
|
+
}), () => ({
|
|
5407
|
+
name: CHECK_NAMES.mcp,
|
|
5408
|
+
status: "ok",
|
|
5409
|
+
detail: `${count} binding(s)`
|
|
5410
|
+
}));
|
|
5411
|
+
};
|
|
5213
5412
|
const skipped = (name) => ({
|
|
5214
5413
|
name,
|
|
5215
5414
|
status: "skipped"
|
|
@@ -5227,6 +5426,7 @@ const buildReport = (path, raw) => {
|
|
|
5227
5426
|
skipped(CHECK_NAMES.schema),
|
|
5228
5427
|
skipped(CHECK_NAMES.catalog),
|
|
5229
5428
|
skipped(CHECK_NAMES.aliases),
|
|
5429
|
+
skipped(CHECK_NAMES.mcp),
|
|
5230
5430
|
skipped(CHECK_NAMES.sealed)
|
|
5231
5431
|
]
|
|
5232
5432
|
}), (data) => {
|
|
@@ -5246,6 +5446,7 @@ const buildReport = (path, raw) => {
|
|
|
5246
5446
|
},
|
|
5247
5447
|
skipped(CHECK_NAMES.catalog),
|
|
5248
5448
|
skipped(CHECK_NAMES.aliases),
|
|
5449
|
+
skipped(CHECK_NAMES.mcp),
|
|
5249
5450
|
skipped(CHECK_NAMES.sealed)
|
|
5250
5451
|
]
|
|
5251
5452
|
}), (config) => {
|
|
@@ -5275,12 +5476,15 @@ const buildReport = (path, raw) => {
|
|
|
5275
5476
|
status: "ok",
|
|
5276
5477
|
detail: `${table.entries.size} alias(es)`
|
|
5277
5478
|
}));
|
|
5479
|
+
const merged = config.catalog ? resolveConfig(config, dirname(path)).fold(() => config, (r) => r.config) : config;
|
|
5480
|
+
const mcpCheck = checkMcpHeaders(merged);
|
|
5278
5481
|
const sealedCheck = checkSealedBlocks(config);
|
|
5279
5482
|
const checks = [
|
|
5280
5483
|
tomlOk,
|
|
5281
5484
|
schemaOk,
|
|
5282
5485
|
catalogCheck,
|
|
5283
5486
|
aliasCheck,
|
|
5487
|
+
mcpCheck,
|
|
5284
5488
|
sealedCheck
|
|
5285
5489
|
];
|
|
5286
5490
|
return {
|
|
@@ -5303,7 +5507,9 @@ const renderCheckLines = (check) => {
|
|
|
5303
5507
|
}
|
|
5304
5508
|
};
|
|
5305
5509
|
const formatTextReport = (report, sourceMsg) => {
|
|
5306
|
-
const
|
|
5510
|
+
const headerIcon = report.ok ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`;
|
|
5511
|
+
const headerLabel = report.ok ? `${GREEN}VALID${RESET}` : `${RED}INVALID${RESET}`;
|
|
5512
|
+
const header = `${headerIcon} ${BOLD}${headerLabel}${RESET} — ${CYAN}${report.configPath}${RESET}`;
|
|
5307
5513
|
const checkLines = report.checks.flatMap((c) => [...renderCheckLines(c)]);
|
|
5308
5514
|
return [
|
|
5309
5515
|
...sourceMsg ? [sourceMsg] : [],
|
|
@@ -5388,7 +5594,7 @@ program.command("audit").description("Audit credential health from envpkt.toml (
|
|
|
5388
5594
|
program.command("fleet").description("Scan directory tree for envpkt.toml files and aggregate health (use in CI for fleet-wide monitoring)").option("-d, --dir <path>", "Root directory to scan", ".").option("--depth <n>", "Max directory depth", parseInt).option("--format <format>", "Output format: table | json", "table").option("--status <status>", "Filter agents by health status").action((options) => {
|
|
5389
5595
|
runFleet(options);
|
|
5390
5596
|
});
|
|
5391
|
-
program.command("validate").description("Verify envpkt.toml integrity — runs TOML syntax, schema, catalog, alias, and sealed-block structural checks").option("-c, --config <path>", "Path to envpkt.toml").option("--json", "Output structured JSON instead of human-readable text").action((options) => {
|
|
5597
|
+
program.command("validate").description("Verify envpkt.toml integrity — runs TOML syntax, schema, catalog, alias, MCP header, and sealed-block structural checks").option("-c, --config <path>", "Path to envpkt.toml").option("--json", "Output structured JSON instead of human-readable text").action((options) => {
|
|
5392
5598
|
runValidate(options);
|
|
5393
5599
|
});
|
|
5394
5600
|
program.command("inspect").description("Display structured view of envpkt.toml").option("-c, --config <path>", "Path to envpkt.toml").option("--format <format>", "Output format: table | json", "table").option("--resolved", "Show resolved view (catalog merged)").option("--secrets", "Show secret values from environment (masked by default)").option("--plaintext", "Show secret values in plaintext (requires --secrets)").option("--sort", "Display secrets and env entries in alphabetical order (no file mutation)").action((options) => {
|
|
@@ -5397,6 +5603,9 @@ program.command("inspect").description("Display structured view of envpkt.toml")
|
|
|
5397
5603
|
program.command("exec").description("Run pre-flight audit then execute a command with injected secrets (sealed → fnox → env cascade)").argument("<command...>", "Command to execute").option("-c, --config <path>", "Path to envpkt.toml").option("--profile <profile>", "fnox profile to use").option("--skip-audit", "Skip the pre-flight audit (alias: --no-check)").option("--no-check", "Skip the pre-flight audit").option("--warn-only", "Warn on critical audit but do not abort").option("--strict", "Abort on any non-healthy secret").action((args, options) => {
|
|
5398
5604
|
runExec(args, options);
|
|
5399
5605
|
});
|
|
5606
|
+
program.command("headers").description("Print HTTP headers for an MCP server as JSON — for Claude Code's headersHelper in .mcp.json").argument("[server]", "MCP server name (default: $CLAUDE_CODE_MCP_SERVER_NAME)").option("-c, --config <path>", "Path to envpkt.toml").option("--profile <profile>", "fnox profile to use").action((server, options) => {
|
|
5607
|
+
runHeaders(server, options);
|
|
5608
|
+
});
|
|
5400
5609
|
program.command("resolve").description("Resolve catalog references and output a flat, self-contained config").option("-c, --config <path>", "Path to envpkt.toml").option("-o, --output <path>", "Write resolved config to file (default: stdout)").option("--format <format>", "Output format: toml | json", "toml").option("--dry-run", "Show what would be resolved without writing").action((options) => {
|
|
5401
5610
|
runResolve(options);
|
|
5402
5611
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,12 @@ declare const AgentIdentitySchema: import("@sinclair/typebox").TObject<{
|
|
|
31
31
|
recipient: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
32
32
|
secrets: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
|
|
33
33
|
}>;
|
|
34
|
+
declare const McpHeaderSchema: import("@sinclair/typebox").TObject<{
|
|
35
|
+
server: import("@sinclair/typebox").TString;
|
|
36
|
+
header: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
37
|
+
scheme: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
38
|
+
}>;
|
|
39
|
+
type McpHeader = Static<typeof McpHeaderSchema>;
|
|
34
40
|
declare const SecretMetaSchema: import("@sinclair/typebox").TObject<{
|
|
35
41
|
service: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
36
42
|
expires: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
@@ -50,6 +56,11 @@ declare const SecretMetaSchema: import("@sinclair/typebox").TObject<{
|
|
|
50
56
|
external: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
51
57
|
tags: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>>;
|
|
52
58
|
namespace: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
59
|
+
mcp: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
60
|
+
server: import("@sinclair/typebox").TString;
|
|
61
|
+
header: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
62
|
+
scheme: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
63
|
+
}>>>;
|
|
53
64
|
}>;
|
|
54
65
|
type SecretMeta = Static<typeof SecretMetaSchema>;
|
|
55
66
|
declare const LifecycleConfigSchema: import("@sinclair/typebox").TObject<{
|
|
@@ -113,6 +124,11 @@ declare const EnvpktConfigSchema: import("@sinclair/typebox").TObject<{
|
|
|
113
124
|
external: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
114
125
|
tags: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>>;
|
|
115
126
|
namespace: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
127
|
+
mcp: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
128
|
+
server: import("@sinclair/typebox").TString;
|
|
129
|
+
header: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
130
|
+
scheme: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
131
|
+
}>>>;
|
|
116
132
|
}>>>;
|
|
117
133
|
env: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TObject<{
|
|
118
134
|
value: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
@@ -299,6 +315,35 @@ type AliasError = {
|
|
|
299
315
|
readonly kind: "secret" | "env";
|
|
300
316
|
readonly field: string;
|
|
301
317
|
};
|
|
318
|
+
/** Structural problems in `mcp` bindings — caught by validation, independent of secret values. */
|
|
319
|
+
type McpConfigError = {
|
|
320
|
+
readonly _tag: "McpHeaderInvalid";
|
|
321
|
+
readonly key: string;
|
|
322
|
+
readonly server: string;
|
|
323
|
+
readonly header: string;
|
|
324
|
+
} | {
|
|
325
|
+
readonly _tag: "McpSchemeInvalid";
|
|
326
|
+
readonly key: string;
|
|
327
|
+
readonly server: string;
|
|
328
|
+
readonly scheme: string;
|
|
329
|
+
} | {
|
|
330
|
+
readonly _tag: "McpHeaderDuplicate";
|
|
331
|
+
readonly server: string;
|
|
332
|
+
readonly header: string;
|
|
333
|
+
readonly keys: ReadonlyArray<string>;
|
|
334
|
+
};
|
|
335
|
+
type McpHeadersError = McpConfigError | {
|
|
336
|
+
readonly _tag: "McpServerUnknown";
|
|
337
|
+
readonly server: string;
|
|
338
|
+
} | {
|
|
339
|
+
readonly _tag: "McpValueUnresolved";
|
|
340
|
+
readonly server: string;
|
|
341
|
+
readonly keys: ReadonlyArray<string>;
|
|
342
|
+
} | {
|
|
343
|
+
readonly _tag: "McpValueInvalid";
|
|
344
|
+
readonly server: string;
|
|
345
|
+
readonly keys: ReadonlyArray<string>;
|
|
346
|
+
};
|
|
302
347
|
type BootOptions = {
|
|
303
348
|
readonly configPath?: string;
|
|
304
349
|
readonly profile?: string;
|
|
@@ -447,6 +492,19 @@ declare const isEnvAlias: (meta: {
|
|
|
447
492
|
/** Format an alias error into a human-readable message */
|
|
448
493
|
declare const formatAliasError: (error: AliasError) => string;
|
|
449
494
|
//#endregion
|
|
495
|
+
//#region src/core/mcp-headers.d.ts
|
|
496
|
+
/** Check every `mcp` binding in a config. Used by `envpkt validate` and the write gate. */
|
|
497
|
+
declare const validateMcpHeaders: (config: EnvpktConfig) => Either<McpConfigError, void>;
|
|
498
|
+
/**
|
|
499
|
+
* Build the HTTP header object for one MCP server: every secret whose `mcp` list names
|
|
500
|
+
* `server`, merged into one object. `values` is keyed by logical secret name (as in
|
|
501
|
+
* `BootResult.secrets`), so namespaces and aliases are already resolved by the caller.
|
|
502
|
+
* Only this server's bindings are checked, so a problem on another server never blocks it.
|
|
503
|
+
*/
|
|
504
|
+
declare const buildMcpHeaders: (config: EnvpktConfig, values: Readonly<Record<string, string>>, server: string) => Either<McpHeadersError, Record<string, string>>;
|
|
505
|
+
/** Human-readable one-liner for any McpHeadersError tag. Never includes a secret value. */
|
|
506
|
+
declare const formatMcpHeadersError: (error: McpHeadersError) => string;
|
|
507
|
+
//#endregion
|
|
450
508
|
//#region src/core/format.d.ts
|
|
451
509
|
type SecretDisplay = "encrypted" | "plaintext";
|
|
452
510
|
type FormatPacketOptions = {
|
|
@@ -721,4 +779,4 @@ type ToolDef = {
|
|
|
721
779
|
declare const toolDefinitions: readonly ToolDef[];
|
|
722
780
|
declare const callTool: (name: string, args: Record<string, unknown>) => CallToolResult;
|
|
723
781
|
//#endregion
|
|
724
|
-
export { type AgentIdentity, AgentIdentitySchema, type AliasError, type AliasTable, type AuditResult, type BootError, type BootOptions, type BootResult, type CallbackConfig, CallbackConfigSchema, type CatalogError, type ChangedEntry, type CheckResult, type ConfidenceLevel, type ConfigDiff, type ConfigError, type ConfigSource, ConsumerType, type CredentialPattern, type DirectLogger, type DirectTestLoggerHandle, type DotenvEntry, type DriftEntry, type DriftStatus, type EnvAuditResult, type EnvDriftEntry, type EnvDriftStatus, type EnvMeta, EnvMetaSchema, EnvpktBootError, type EnvpktConfig, EnvpktConfigSchema, type FieldChange, type FleetAgent, type FleetHealth, type FnoxConfig, type FnoxError, type FnoxSecret, type FormatDotenvOptions, type FormatPacketOptions, type HealthStatus, type Identity, type IdentityError, IdentitySchema, type KeygenError, type KeygenResult, type LifecycleConfig, LifecycleConfigSchema, type LogEntry, type LogLevel, type LogMetadata, type MatchResult, type ResolveOptions, type ResolveResult, type ResolvedPath, type ScanOptions, type ScanResult, type SealError, type SecretDisplay, type SecretHealth, type SecretMeta, SecretMetaSchema, type SecretStatus, type SectionDiff, type TomlEditError, type ToolsConfig, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, appendSection, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, copyableSecretMeta, createDirectConsoleLogger, createDirectTestLogger, createServer, deriveServiceFromName, detectFnox, diffConfigs, directSilentLogger, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatAliasError, formatDotenv, formatPacket, generateKeypair, generateTomlFromScan, isEnvAlias, isSecretAlias, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, quoteDotenvValue, readConfigFile, readFnoxConfig, readResource, removeSection, renameSection, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, serializeEnvBlock, serializeSecretBlock, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigIdentity, updateSectionFields, validateAliases, validateConfig };
|
|
782
|
+
export { type AgentIdentity, AgentIdentitySchema, type AliasError, type AliasTable, type AuditResult, type BootError, type BootOptions, type BootResult, type CallbackConfig, CallbackConfigSchema, type CatalogError, type ChangedEntry, type CheckResult, type ConfidenceLevel, type ConfigDiff, type ConfigError, type ConfigSource, ConsumerType, type CredentialPattern, type DirectLogger, type DirectTestLoggerHandle, type DotenvEntry, type DriftEntry, type DriftStatus, type EnvAuditResult, type EnvDriftEntry, type EnvDriftStatus, type EnvMeta, EnvMetaSchema, EnvpktBootError, type EnvpktConfig, EnvpktConfigSchema, type FieldChange, type FleetAgent, type FleetHealth, type FnoxConfig, type FnoxError, type FnoxSecret, type FormatDotenvOptions, type FormatPacketOptions, type HealthStatus, type Identity, type IdentityError, IdentitySchema, type KeygenError, type KeygenResult, type LifecycleConfig, LifecycleConfigSchema, type LogEntry, type LogLevel, type LogMetadata, type MatchResult, type McpConfigError, type McpHeader, McpHeaderSchema, type McpHeadersError, type ResolveOptions, type ResolveResult, type ResolvedPath, type ScanOptions, type ScanResult, type SealError, type SecretDisplay, type SecretHealth, type SecretMeta, SecretMetaSchema, type SecretStatus, type SectionDiff, type TomlEditError, type ToolsConfig, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, appendSection, boot, bootSafe, buildMcpHeaders, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, copyableSecretMeta, createDirectConsoleLogger, createDirectTestLogger, createServer, deriveServiceFromName, detectFnox, diffConfigs, directSilentLogger, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatAliasError, formatDotenv, formatMcpHeadersError, formatPacket, generateKeypair, generateTomlFromScan, isEnvAlias, isSecretAlias, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, quoteDotenvValue, readConfigFile, readFnoxConfig, readResource, removeSection, renameSection, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, serializeEnvBlock, serializeSecretBlock, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigIdentity, updateSectionFields, validateAliases, validateConfig, validateMcpHeaders };
|
package/dist/index.js
CHANGED
|
@@ -46,6 +46,21 @@ const NamespaceSchema = Type.Object({
|
|
|
46
46
|
description: "Separator between prefix and key. Default '__' (shell-safe). Note: '.' and ':' are not valid in shell identifiers."
|
|
47
47
|
}))
|
|
48
48
|
}, { description: "Optional namespace/package prefix for injected environment variable names" });
|
|
49
|
+
const McpHeaderSchema = Type.Object({
|
|
50
|
+
server: Type.String({
|
|
51
|
+
minLength: 1,
|
|
52
|
+
description: "MCP server name, as keyed in .mcp.json"
|
|
53
|
+
}),
|
|
54
|
+
header: Type.Optional(Type.String({
|
|
55
|
+
default: "Authorization",
|
|
56
|
+
minLength: 1,
|
|
57
|
+
description: "HTTP header name to send the secret in"
|
|
58
|
+
})),
|
|
59
|
+
scheme: Type.Optional(Type.String({
|
|
60
|
+
default: "Bearer",
|
|
61
|
+
description: "Prefix placed before the value (e.g. 'Bearer'). Empty string sends the raw value."
|
|
62
|
+
}))
|
|
63
|
+
}, { description: "Send this secret as an HTTP header to an MCP server" });
|
|
49
64
|
const SecretMetaSchema = Type.Object({
|
|
50
65
|
service: Type.Optional(Type.String({ description: "Service or system this secret authenticates to" })),
|
|
51
66
|
expires: Type.Optional(Type.String({
|
|
@@ -76,7 +91,8 @@ const SecretMetaSchema = Type.Object({
|
|
|
76
91
|
required: Type.Optional(Type.Boolean({ description: "Whether this secret is required for operation" })),
|
|
77
92
|
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'." })),
|
|
78
93
|
tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" })),
|
|
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." }))
|
|
94
|
+
namespace: Type.Optional(Type.String({ description: "Override the file-level namespace for this entry's injected name. Empty string opts out of any prefix." })),
|
|
95
|
+
mcp: Type.Optional(Type.Array(McpHeaderSchema, { description: "MCP servers this secret authenticates to. Read by `envpkt headers` (Claude Code headersHelper)." }))
|
|
80
96
|
}, { description: "Metadata about a single secret" });
|
|
81
97
|
const LifecycleConfigSchema = Type.Object({
|
|
82
98
|
stale_warning_days: Type.Optional(Type.Number({
|
|
@@ -209,10 +225,13 @@ const discoverConfig = (cwd) => {
|
|
|
209
225
|
template,
|
|
210
226
|
expanded: expandPath(template)
|
|
211
227
|
})).filter(({ expanded }) => expanded !== "" && !expanded.startsWith("/.envpkt")).map(({ expanded }) => expandGlobPath(expanded)).find((matches) => matches.length > 0);
|
|
212
|
-
if (customMatch)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
228
|
+
if (customMatch) {
|
|
229
|
+
const found = {
|
|
230
|
+
path: customMatch[0],
|
|
231
|
+
source: "search"
|
|
232
|
+
};
|
|
233
|
+
return Option(found);
|
|
234
|
+
}
|
|
216
235
|
const platformMatch = buildSearchPaths().find((p) => Fs.existsSync(p));
|
|
217
236
|
if (platformMatch) return Option({
|
|
218
237
|
path: platformMatch,
|
|
@@ -254,9 +273,10 @@ const parseToml = (raw) => Try(() => parse(raw)).fold((err) => Left({
|
|
|
254
273
|
/** Validate parsed data against the TypeBox schema */
|
|
255
274
|
const validateConfig = (data) => {
|
|
256
275
|
if (compiledSchema.Check(data)) return Right(data);
|
|
276
|
+
const errors = List([...compiledSchema.Errors(data)].map((e) => `${e.path}: ${e.message}`));
|
|
257
277
|
return Left({
|
|
258
278
|
_tag: "ValidationError",
|
|
259
|
-
errors
|
|
279
|
+
errors
|
|
260
280
|
});
|
|
261
281
|
};
|
|
262
282
|
/** Load and validate an envpkt.toml from a file path */
|
|
@@ -505,6 +525,104 @@ const formatAliasError = (error) => {
|
|
|
505
525
|
}
|
|
506
526
|
};
|
|
507
527
|
//#endregion
|
|
528
|
+
//#region src/core/mcp-headers.ts
|
|
529
|
+
const DEFAULT_HEADER = "Authorization";
|
|
530
|
+
const DEFAULT_SCHEME = "Bearer";
|
|
531
|
+
/** RFC 9110 `token`: the legal characters of a header name (and of an auth scheme). */
|
|
532
|
+
const TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
533
|
+
/** Characters that would split or corrupt an HTTP header value. */
|
|
534
|
+
const UNSAFE_VALUE_RE = /[\r\n\0]/;
|
|
535
|
+
/** Flatten every `[secret.*].mcp` entry into one binding per (secret, server), with defaults applied. */
|
|
536
|
+
const collectBindings = (config) => Object.entries(config.secret ?? {}).flatMap(([key, meta]) => (meta.mcp ?? []).map((m) => ({
|
|
537
|
+
key,
|
|
538
|
+
server: m.server,
|
|
539
|
+
header: m.header ?? DEFAULT_HEADER,
|
|
540
|
+
scheme: m.scheme ?? DEFAULT_SCHEME
|
|
541
|
+
})));
|
|
542
|
+
/**
|
|
543
|
+
* Check bindings for structural problems: header names and schemes that are not HTTP
|
|
544
|
+
* tokens, and two bindings claiming the same (server, header) pair. Header names compare
|
|
545
|
+
* case-insensitively, as HTTP does.
|
|
546
|
+
*/
|
|
547
|
+
const validateBindings = (bindings) => {
|
|
548
|
+
const badHeader = bindings.find((b) => !TOKEN_RE.test(b.header));
|
|
549
|
+
if (badHeader) return Left({
|
|
550
|
+
_tag: "McpHeaderInvalid",
|
|
551
|
+
key: badHeader.key,
|
|
552
|
+
server: badHeader.server,
|
|
553
|
+
header: badHeader.header
|
|
554
|
+
});
|
|
555
|
+
const badScheme = bindings.find((b) => b.scheme !== "" && !TOKEN_RE.test(b.scheme));
|
|
556
|
+
if (badScheme) return Left({
|
|
557
|
+
_tag: "McpSchemeInvalid",
|
|
558
|
+
key: badScheme.key,
|
|
559
|
+
server: badScheme.server,
|
|
560
|
+
scheme: badScheme.scheme
|
|
561
|
+
});
|
|
562
|
+
const claims = bindings.reduce((acc, b) => {
|
|
563
|
+
const id = `${b.server}\u0000${b.header.toLowerCase()}`;
|
|
564
|
+
return {
|
|
565
|
+
...acc,
|
|
566
|
+
[id]: [...acc[id] ?? [], b]
|
|
567
|
+
};
|
|
568
|
+
}, {});
|
|
569
|
+
const clash = Object.values(claims).find((group) => group.length > 1);
|
|
570
|
+
if (clash) {
|
|
571
|
+
const first = clash[0];
|
|
572
|
+
return Left({
|
|
573
|
+
_tag: "McpHeaderDuplicate",
|
|
574
|
+
server: first.server,
|
|
575
|
+
header: first.header,
|
|
576
|
+
keys: Set$1(clash.map((b) => b.key)).toArray()
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
return Right(void 0);
|
|
580
|
+
};
|
|
581
|
+
/** Check every `mcp` binding in a config. Used by `envpkt validate` and the write gate. */
|
|
582
|
+
const validateMcpHeaders = (config) => validateBindings(collectBindings(config));
|
|
583
|
+
/**
|
|
584
|
+
* Build the HTTP header object for one MCP server: every secret whose `mcp` list names
|
|
585
|
+
* `server`, merged into one object. `values` is keyed by logical secret name (as in
|
|
586
|
+
* `BootResult.secrets`), so namespaces and aliases are already resolved by the caller.
|
|
587
|
+
* Only this server's bindings are checked, so a problem on another server never blocks it.
|
|
588
|
+
*/
|
|
589
|
+
const buildMcpHeaders = (config, values, server) => {
|
|
590
|
+
const matches = collectBindings(config).filter((b) => b.server === server);
|
|
591
|
+
if (matches.length === 0) return Left({
|
|
592
|
+
_tag: "McpServerUnknown",
|
|
593
|
+
server
|
|
594
|
+
});
|
|
595
|
+
return validateBindings(matches).flatMap(() => {
|
|
596
|
+
const unresolved = matches.filter((b) => !values[b.key]).map((b) => b.key);
|
|
597
|
+
if (unresolved.length > 0) return Left({
|
|
598
|
+
_tag: "McpValueUnresolved",
|
|
599
|
+
server,
|
|
600
|
+
keys: unresolved
|
|
601
|
+
});
|
|
602
|
+
const unsafe = matches.filter((b) => UNSAFE_VALUE_RE.test(values[b.key])).map((b) => b.key);
|
|
603
|
+
if (unsafe.length > 0) return Left({
|
|
604
|
+
_tag: "McpValueInvalid",
|
|
605
|
+
server,
|
|
606
|
+
keys: unsafe
|
|
607
|
+
});
|
|
608
|
+
return Right(Object.fromEntries(matches.map((b) => {
|
|
609
|
+
const value = values[b.key];
|
|
610
|
+
return [b.header, b.scheme === "" ? value : `${b.scheme} ${value}`];
|
|
611
|
+
})));
|
|
612
|
+
});
|
|
613
|
+
};
|
|
614
|
+
/** Human-readable one-liner for any McpHeadersError tag. Never includes a secret value. */
|
|
615
|
+
const formatMcpHeadersError = (error) => {
|
|
616
|
+
switch (error._tag) {
|
|
617
|
+
case "McpHeaderInvalid": return `[secret.${error.key}] mcp server "${error.server}": header "${error.header}" is not a valid HTTP header name`;
|
|
618
|
+
case "McpSchemeInvalid": return `[secret.${error.key}] mcp server "${error.server}": scheme "${error.scheme}" must be a single word or ""`;
|
|
619
|
+
case "McpHeaderDuplicate": return `MCP server "${error.server}": header "${error.header}" is claimed by more than one binding: ${error.keys.join(", ")}`;
|
|
620
|
+
case "McpServerUnknown": return `No secret declares mcp server "${error.server}"`;
|
|
621
|
+
case "McpValueUnresolved": return `MCP server "${error.server}": no value resolved for ${error.keys.join(", ")}`;
|
|
622
|
+
case "McpValueInvalid": return `MCP server "${error.server}": value of ${error.keys.join(", ")} contains a line break or NUL and cannot be sent as a header`;
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
//#endregion
|
|
508
626
|
//#region src/core/format.ts
|
|
509
627
|
const maskValue = (value) => {
|
|
510
628
|
if (value.length > 8) return `${value.slice(0, 3)}${"•".repeat(5)}${value.slice(-4)}`;
|
|
@@ -690,14 +808,15 @@ const computeAudit = (config, fnoxKeys, today, aliasTable) => {
|
|
|
690
808
|
const staleWarningDays = lifecycle.stale_warning_days ?? 90;
|
|
691
809
|
const requireExpiration = lifecycle.require_expiration ?? false;
|
|
692
810
|
const requireService = lifecycle.require_service ?? false;
|
|
693
|
-
const keys = fnoxKeys ??
|
|
811
|
+
const keys = fnoxKeys ?? Set$1.empty();
|
|
694
812
|
const secretEntries = config.secret ?? {};
|
|
695
813
|
const nonAliasEntries = Object.entries(secretEntries).filter(([, meta]) => meta.from_key === void 0);
|
|
696
814
|
const aliasEntries = Object.entries(secretEntries).filter(([, meta]) => meta.from_key !== void 0);
|
|
697
815
|
const nonAliasHealth = nonAliasEntries.map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now));
|
|
698
816
|
const healthByKey = Map$1(nonAliasHealth.map((h) => [h.key, h]));
|
|
699
817
|
const parseTargetKey = (from_key) => {
|
|
700
|
-
|
|
818
|
+
const match = from_key.match(/^secret\.(.+)$/);
|
|
819
|
+
return Option(match?.[1]);
|
|
701
820
|
};
|
|
702
821
|
const aliasHealth = aliasEntries.map(([key, meta]) => {
|
|
703
822
|
const tableEntry = aliasTable?.entries.get(`secret.${key}`);
|
|
@@ -751,8 +870,10 @@ const computeEnvAudit = (config, env = process.env) => {
|
|
|
751
870
|
const envWire = (key) => namer(key, envEntries[key]?.namespace);
|
|
752
871
|
const resolveEffectiveDefault = (entry) => {
|
|
753
872
|
return Do(function* () {
|
|
754
|
-
const
|
|
755
|
-
|
|
873
|
+
const fk = yield* $(Option(entry.from_key));
|
|
874
|
+
const targetKey = yield* $(Option(fk.match(/^env\.(.+)$/)?.[1]));
|
|
875
|
+
const targetEntry = yield* $(Option(envEntries[targetKey]));
|
|
876
|
+
return yield* $(Option(targetEntry.value));
|
|
756
877
|
}).orElse(entry.value ?? "");
|
|
757
878
|
};
|
|
758
879
|
const entries = Object.entries(envEntries).map(([key, entry]) => {
|
|
@@ -1398,7 +1519,8 @@ const VALUE_SHAPE_PATTERNS = [
|
|
|
1398
1519
|
];
|
|
1399
1520
|
/** Detect service from value prefix/shape */
|
|
1400
1521
|
const matchValueShape = (value) => {
|
|
1401
|
-
|
|
1522
|
+
const match = VALUE_SHAPE_PATTERNS.find((vp) => value.startsWith(vp.prefix));
|
|
1523
|
+
return Option(match).map((vp) => ({
|
|
1402
1524
|
service: vp.service,
|
|
1403
1525
|
description: vp.description
|
|
1404
1526
|
}));
|
|
@@ -1438,7 +1560,8 @@ const matchEnvVar = (name, value) => {
|
|
|
1438
1560
|
matchedBy: `exact:${exactMatch.pattern}`
|
|
1439
1561
|
});
|
|
1440
1562
|
return matchValueShape(value).fold(() => {
|
|
1441
|
-
|
|
1563
|
+
const suffixMatch = SUFFIX_PATTERNS.find((sp) => name.endsWith(sp.suffix));
|
|
1564
|
+
return Option(suffixMatch).map((sp) => ({
|
|
1442
1565
|
envVar: name,
|
|
1443
1566
|
value,
|
|
1444
1567
|
service: Option(deriveServiceFromName(name)),
|
|
@@ -2075,7 +2198,8 @@ const bootSafe = (options) => {
|
|
|
2075
2198
|
_tag: "ReadError",
|
|
2076
2199
|
message: "unexpected"
|
|
2077
2200
|
}));
|
|
2078
|
-
const
|
|
2201
|
+
const fnoxKeys = detectFnoxKeys(configDir);
|
|
2202
|
+
const audit = computeAudit(config, fnoxKeys, void 0, aliasTable);
|
|
2079
2203
|
const sealIdentity = hasSealedValues ? resolveSealIdentity(config, configDir) : Option(void 0);
|
|
2080
2204
|
if (hasSealedValues && sealIdentity.isEmpty) return Left({
|
|
2081
2205
|
_tag: "SealKeyUnavailable",
|
|
@@ -2117,37 +2241,39 @@ const bootSafe = (options) => {
|
|
|
2117
2241
|
}
|
|
2118
2242
|
});
|
|
2119
2243
|
const remainingKeys = nonAliasMetaKeys.filter((k) => !sealedKeys.has(k));
|
|
2120
|
-
if (remainingKeys.length > 0)
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
warnings.push(`fnox export failed: ${err.message}`);
|
|
2126
|
-
skipped.push(...remainingKeys);
|
|
2127
|
-
}, (exported) => {
|
|
2128
|
-
const found = remainingKeys.filter((key) => key in exported);
|
|
2129
|
-
const notFound = remainingKeys.filter((key) => !(key in exported));
|
|
2130
|
-
found.forEach((key) => {
|
|
2131
|
-
secrets[key] = exported[key];
|
|
2132
|
-
log.debug("phase.fnox.resolved", {
|
|
2133
|
-
key,
|
|
2134
|
-
profile: opts.profile
|
|
2244
|
+
if (remainingKeys.length > 0) {
|
|
2245
|
+
if (fnoxAvailable()) fnoxExport(opts.profile, identityKey.orUndefined()).fold((err) => {
|
|
2246
|
+
log.warn("phase.fnox.export_failed", {
|
|
2247
|
+
message: err.message,
|
|
2248
|
+
skipped: remainingKeys.length
|
|
2135
2249
|
});
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2250
|
+
warnings.push(`fnox export failed: ${err.message}`);
|
|
2251
|
+
skipped.push(...remainingKeys);
|
|
2252
|
+
}, (exported) => {
|
|
2253
|
+
const found = remainingKeys.filter((key) => key in exported);
|
|
2254
|
+
const notFound = remainingKeys.filter((key) => !(key in exported));
|
|
2255
|
+
found.forEach((key) => {
|
|
2256
|
+
secrets[key] = exported[key];
|
|
2257
|
+
log.debug("phase.fnox.resolved", {
|
|
2258
|
+
key,
|
|
2259
|
+
profile: opts.profile
|
|
2260
|
+
});
|
|
2261
|
+
});
|
|
2262
|
+
notFound.forEach((key) => {
|
|
2263
|
+
log.debug("phase.fnox.not_in_export", {
|
|
2264
|
+
key,
|
|
2265
|
+
profile: opts.profile
|
|
2266
|
+
});
|
|
2141
2267
|
});
|
|
2268
|
+
injected.push(...found);
|
|
2269
|
+
skipped.push(...notFound);
|
|
2142
2270
|
});
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
else warnings.push("fnox not available — unsealed secrets could not be resolved");
|
|
2150
|
-
skipped.push(...remainingKeys);
|
|
2271
|
+
else {
|
|
2272
|
+
log.debug("phase.fnox.unavailable", { skipped: remainingKeys.length });
|
|
2273
|
+
if (!hasSealedValues) warnings.push("fnox not available — no secrets injected");
|
|
2274
|
+
else warnings.push("fnox not available — unsealed secrets could not be resolved");
|
|
2275
|
+
skipped.push(...remainingKeys);
|
|
2276
|
+
}
|
|
2151
2277
|
}
|
|
2152
2278
|
aliasSecretKeys.forEach((aliasKey) => {
|
|
2153
2279
|
const entry = aliasTable.entries.get(`secret.${aliasKey}`);
|
|
@@ -2584,7 +2710,7 @@ function* findEnvpktFiles(dir, maxDepth, currentDepth = 0) {
|
|
|
2584
2710
|
}
|
|
2585
2711
|
const scanFleet = (rootDir, options) => {
|
|
2586
2712
|
const maxDepth = options?.maxDepth ?? 3;
|
|
2587
|
-
const
|
|
2713
|
+
const agents = Array.from(findEnvpktFiles(rootDir, maxDepth)).flatMap((configPath) => loadConfig(configPath).fold(() => [], (config) => {
|
|
2588
2714
|
const audit = computeAudit(config);
|
|
2589
2715
|
return [{
|
|
2590
2716
|
path: configPath,
|
|
@@ -2592,7 +2718,8 @@ const scanFleet = (rootDir, options) => {
|
|
|
2592
2718
|
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(),
|
|
2593
2719
|
audit
|
|
2594
2720
|
}];
|
|
2595
|
-
}))
|
|
2721
|
+
}));
|
|
2722
|
+
const agentList = List(agents);
|
|
2596
2723
|
const total_agents = agentList.size;
|
|
2597
2724
|
const total_secrets = agentList.toArray().reduce((acc, a) => acc + a.audit.total, 0);
|
|
2598
2725
|
const expired = agentList.toArray().reduce((acc, a) => acc + a.audit.expired, 0);
|
|
@@ -2929,4 +3056,4 @@ const startServer = async () => {
|
|
|
2929
3056
|
await server.connect(transport);
|
|
2930
3057
|
};
|
|
2931
3058
|
//#endregion
|
|
2932
|
-
export { AgentIdentitySchema, CallbackConfigSchema, ConsumerType, EnvMetaSchema, EnvpktBootError, EnvpktConfigSchema, IdentitySchema, LifecycleConfigSchema, SecretMetaSchema, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, appendSection, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, copyableSecretMeta, createDirectConsoleLogger, createDirectTestLogger, createServer, deriveServiceFromName, detectFnox, diffConfigs, directSilentLogger, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatAliasError, formatDotenv, formatPacket, generateKeypair, generateTomlFromScan, isEnvAlias, isSecretAlias, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, quoteDotenvValue, readConfigFile, readFnoxConfig, readResource, removeSection, renameSection, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, serializeEnvBlock, serializeSecretBlock, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigIdentity, updateSectionFields, validateAliases, validateConfig };
|
|
3059
|
+
export { AgentIdentitySchema, CallbackConfigSchema, ConsumerType, EnvMetaSchema, EnvpktBootError, EnvpktConfigSchema, IdentitySchema, LifecycleConfigSchema, McpHeaderSchema, SecretMetaSchema, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, appendSection, boot, bootSafe, buildMcpHeaders, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, copyableSecretMeta, createDirectConsoleLogger, createDirectTestLogger, createServer, deriveServiceFromName, detectFnox, diffConfigs, directSilentLogger, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatAliasError, formatDotenv, formatMcpHeadersError, formatPacket, generateKeypair, generateTomlFromScan, isEnvAlias, isSecretAlias, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, quoteDotenvValue, readConfigFile, readFnoxConfig, readResource, removeSection, renameSection, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, serializeEnvBlock, serializeSecretBlock, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigIdentity, updateSectionFields, validateAliases, validateConfig, validateMcpHeaders };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "envpkt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
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.
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.32.0",
|
|
43
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.
|
|
45
|
+
"functype": "^1.13.0",
|
|
46
|
+
"functype-log": "^1.13.0",
|
|
47
|
+
"functype-os": "^1.13.0",
|
|
48
|
+
"smol-toml": "^1.9.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@types/node": "^24.
|
|
52
|
-
"ts-builds": "^3.
|
|
51
|
+
"@types/node": "^24.19.1",
|
|
52
|
+
"ts-builds": "^3.4.3",
|
|
53
53
|
"tsdown": "^0.22.14",
|
|
54
|
-
"tsx": "^4.23.
|
|
54
|
+
"tsx": "^4.23.15"
|
|
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.27.0+sha512.ad12821463bb143295f1fa3ecde77a6b9b97a7990386da800ff57bffc3964dc64b7b1c7559d5dbe5c337aea659ed9c8937d4370bd198ca2009bc6fa0249808e7"
|
|
75
75
|
}
|
|
@@ -211,6 +211,35 @@
|
|
|
211
211
|
"namespace": {
|
|
212
212
|
"description": "Override the file-level namespace for this entry's injected name. Empty string opts out of any prefix.",
|
|
213
213
|
"type": "string"
|
|
214
|
+
},
|
|
215
|
+
"mcp": {
|
|
216
|
+
"description": "MCP servers this secret authenticates to. Read by `envpkt headers` (Claude Code headersHelper).",
|
|
217
|
+
"type": "array",
|
|
218
|
+
"items": {
|
|
219
|
+
"description": "Send this secret as an HTTP header to an MCP server",
|
|
220
|
+
"type": "object",
|
|
221
|
+
"required": [
|
|
222
|
+
"server"
|
|
223
|
+
],
|
|
224
|
+
"properties": {
|
|
225
|
+
"server": {
|
|
226
|
+
"minLength": 1,
|
|
227
|
+
"description": "MCP server name, as keyed in .mcp.json",
|
|
228
|
+
"type": "string"
|
|
229
|
+
},
|
|
230
|
+
"header": {
|
|
231
|
+
"default": "Authorization",
|
|
232
|
+
"minLength": 1,
|
|
233
|
+
"description": "HTTP header name to send the secret in",
|
|
234
|
+
"type": "string"
|
|
235
|
+
},
|
|
236
|
+
"scheme": {
|
|
237
|
+
"default": "Bearer",
|
|
238
|
+
"description": "Prefix placed before the value (e.g. 'Bearer'). Empty string sends the raw value.",
|
|
239
|
+
"type": "string"
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
214
243
|
}
|
|
215
244
|
}
|
|
216
245
|
}
|