envpkt 0.14.0 → 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 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 ?? /* @__PURE__ */ new Set();
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
- return Option(from_key.match(/^secret\.(.+)$/)?.[1]);
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 targetKey = yield* $(Option((yield* $(Option(entry.from_key))).match(/^env\.(.+)$/)?.[1]));
188
- return yield* $(Option((yield* $(Option(envEntries[targetKey]))).value));
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) return Option({
403
- path: customMatch[0],
404
- source: "search"
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: List([...compiledSchema.Errors(data)].map((e) => `${e.path}: ${e.message}`))
472
+ errors
450
473
  });
451
474
  };
452
475
  /** Load and validate an envpkt.toml from a file path */
@@ -728,12 +751,13 @@ const formatError = (error) => {
728
751
  case "IdentityNotFound": return `${RED}Error:${RESET} Identity file not found: ${error.path}`;
729
752
  case "SealKeyUnavailable": {
730
753
  const e = error;
754
+ 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)`;
731
755
  return [
732
756
  `${RED}Error:${RESET} ${e.sealedKeys.length} sealed secret(s) can't be decrypted — no age key found.`,
733
757
  `${DIM}Searched (in order):${RESET}`,
734
758
  e.searched.map((l) => ` • ${l}`).join("\n"),
735
759
  `${DIM}Fix one:${RESET}`,
736
- ` • Restore your key to ~/.envpkt/age-key.txt (or set ENVPKT_AGE_KEY_FILE / ENVPKT_AGE_KEY)`,
760
+ restoreLine,
737
761
  ` • Re-provision from source: envpkt seal --edit <KEY>`
738
762
  ].join("\n");
739
763
  }
@@ -855,7 +879,8 @@ const runAudit = (options) => {
855
879
  console.error(formatError(err));
856
880
  process.exit(2);
857
881
  }, (rawConfig) => {
858
- resolveConfig(rawConfig, dirname(path)).fold((err) => {
882
+ const configDir = dirname(path);
883
+ resolveConfig(rawConfig, configDir).fold((err) => {
859
884
  console.error(formatError(err));
860
885
  process.exit(2);
861
886
  }, (resolveResult) => {
@@ -919,8 +944,10 @@ const runAuditOnConfig = (config, options) => {
919
944
  if (options.format === "json") console.log(formatAuditJson(sorted));
920
945
  else if (options.format === "minimal") console.log(formatAuditMinimal(sorted));
921
946
  else console.log(formatAudit(sorted));
922
- if (options.all) if (options.format === "json") console.log(formatEnvAuditJson(config));
923
- else formatEnvAuditTable(config);
947
+ if (options.all) {
948
+ if (options.format === "json") console.log(formatEnvAuditJson(config));
949
+ else formatEnvAuditTable(config);
950
+ }
924
951
  const code = options.strict ? exitCodeForAudit(audit) : audit.status === "critical" ? 2 : 0;
925
952
  process.exit(code);
926
953
  };
@@ -1404,11 +1431,13 @@ const resolveSealIdentity = (config, configDir) => {
1404
1431
  });
1405
1432
  const inlineKey = resolveInlineKey().orUndefined();
1406
1433
  if (inlineKey) return Option(materializeInlineKey(inlineKey));
1407
- const defaultPath = join(homedir(), ".envpkt", "age-key.txt");
1408
- if (existsSync(defaultPath)) return Option({
1409
- path: defaultPath,
1410
- dispose: noop
1411
- });
1434
+ if (!config.identity?.key_file) {
1435
+ const defaultPath = join(homedir(), ".envpkt", "age-key.txt");
1436
+ if (existsSync(defaultPath)) return Option({
1437
+ path: defaultPath,
1438
+ dispose: noop
1439
+ });
1440
+ }
1412
1441
  return Option(void 0);
1413
1442
  };
1414
1443
  /** Describe the seal-identity precedence chain with per-entry status, for a clear no-key error. */
@@ -1423,7 +1452,7 @@ const describeSealKeySearch = (config, configDir) => {
1423
1452
  keyFileLine,
1424
1453
  envFileLine,
1425
1454
  inlineLine,
1426
- `${defaultPath} (${existsSync(defaultPath) ? "found" : "missing"})`
1455
+ keyFile ? `${defaultPath} (skipped — identity.key_file is set)` : `${defaultPath} (${existsSync(defaultPath) ? "found" : "missing"})`
1427
1456
  ];
1428
1457
  };
1429
1458
  const resolveIdentityKey = (config, configDir) => {
@@ -1494,12 +1523,14 @@ const bootSafe = (options) => {
1494
1523
  _tag: "ReadError",
1495
1524
  message: "unexpected"
1496
1525
  }));
1497
- const audit = computeAudit(config, detectFnoxKeys(configDir), void 0, aliasTable);
1526
+ const fnoxKeys = detectFnoxKeys(configDir);
1527
+ const audit = computeAudit(config, fnoxKeys, void 0, aliasTable);
1498
1528
  const sealIdentity = hasSealedValues ? resolveSealIdentity(config, configDir) : Option(void 0);
1499
1529
  if (hasSealedValues && sealIdentity.isEmpty) return Left({
1500
1530
  _tag: "SealKeyUnavailable",
1501
1531
  sealedKeys: nonAliasMetaKeys.filter((k) => !!nonAliasSecretEntries[k]?.encrypted_value),
1502
- searched: describeSealKeySearch(config, configDir)
1532
+ searched: describeSealKeySearch(config, configDir),
1533
+ configuredKeyFile: config.identity?.key_file ? resolve(configDir, expandPath(config.identity.key_file)) : void 0
1503
1534
  });
1504
1535
  return checkExpiration(audit, failOnExpired, warnOnly).map((warnings) => {
1505
1536
  const secrets = {};
@@ -1535,37 +1566,39 @@ const bootSafe = (options) => {
1535
1566
  }
1536
1567
  });
1537
1568
  const remainingKeys = nonAliasMetaKeys.filter((k) => !sealedKeys.has(k));
1538
- if (remainingKeys.length > 0) if (fnoxAvailable()) fnoxExport(opts.profile, identityKey.orUndefined()).fold((err) => {
1539
- log.warn("phase.fnox.export_failed", {
1540
- message: err.message,
1541
- skipped: remainingKeys.length
1542
- });
1543
- warnings.push(`fnox export failed: ${err.message}`);
1544
- skipped.push(...remainingKeys);
1545
- }, (exported) => {
1546
- const found = remainingKeys.filter((key) => key in exported);
1547
- const notFound = remainingKeys.filter((key) => !(key in exported));
1548
- found.forEach((key) => {
1549
- secrets[key] = exported[key];
1550
- log.debug("phase.fnox.resolved", {
1551
- key,
1552
- 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
1553
1574
  });
1554
- });
1555
- notFound.forEach((key) => {
1556
- log.debug("phase.fnox.not_in_export", {
1557
- key,
1558
- profile: opts.profile
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
+ });
1559
1586
  });
1587
+ notFound.forEach((key) => {
1588
+ log.debug("phase.fnox.not_in_export", {
1589
+ key,
1590
+ profile: opts.profile
1591
+ });
1592
+ });
1593
+ injected.push(...found);
1594
+ skipped.push(...notFound);
1560
1595
  });
1561
- injected.push(...found);
1562
- skipped.push(...notFound);
1563
- });
1564
- else {
1565
- log.debug("phase.fnox.unavailable", { skipped: remainingKeys.length });
1566
- if (!hasSealedValues) warnings.push("fnox not available — no secrets injected");
1567
- else warnings.push("fnox not available — unsealed secrets could not be resolved");
1568
- 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
+ }
1569
1602
  }
1570
1603
  aliasSecretKeys.forEach((aliasKey) => {
1571
1604
  const entry = aliasTable.entries.get(`secret.${aliasKey}`);
@@ -1960,16 +1993,114 @@ const sortConfigToml = (raw) => {
1960
1993
  return `${parts.join("\n\n")}\n`;
1961
1994
  };
1962
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
1963
2094
  //#region src/core/validate.ts
1964
2095
  /**
1965
- * 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.
1966
2097
  *
1967
2098
  * Used by write-path CLI commands to verify the post-edit file would still be
1968
2099
  * structurally valid before persisting. Catalog resolution is intentionally
1969
2100
  * excluded — catalog issues depend on external files, not on the local edit,
1970
2101
  * and `envpkt validate` covers them as a separate explicit check.
1971
2102
  */
1972
- 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));
1973
2104
  /** Human-readable one-liner for any ValidationError tag. */
1974
2105
  const formatValidationError = (err) => {
1975
2106
  switch (err._tag) {
@@ -1983,6 +2114,9 @@ const formatValidationError = (err) => {
1983
2114
  case "AliasChained":
1984
2115
  case "AliasCrossType":
1985
2116
  case "AliasValueConflict": return formatAliasError(err);
2117
+ case "McpHeaderInvalid":
2118
+ case "McpSchemeInvalid":
2119
+ case "McpHeaderDuplicate": return formatMcpHeadersError(err);
1986
2120
  }
1987
2121
  };
1988
2122
  //#endregion
@@ -2069,14 +2203,16 @@ const sealedSecretBlock = (key, destName, srcConfig, srcPath, destConfig) => {
2069
2203
  console.error(`${RED}Error:${RESET} "${key}" produced no plaintext on unseal.`);
2070
2204
  return process.exit(2);
2071
2205
  }
2206
+ const cipher = orExit(ageEncrypt(plaintext, recipient), (err) => `${RED}Error:${RESET} reseal failed for "${destName}": ${err.message}`, 2);
2072
2207
  return serializeSecretBlock(destName, copyableSecretMeta(meta, {
2073
2208
  today,
2074
- encryptedValue: Option(orExit(ageEncrypt(plaintext, recipient), (err) => `${RED}Error:${RESET} reseal failed for "${destName}": ${err.message}`, 2))
2209
+ encryptedValue: Option(cipher)
2075
2210
  }));
2076
2211
  };
2077
2212
  const writeBlock = (destPath, header, block, overwrite, dryRun, successMsg) => {
2078
2213
  const raw = readFileSync(destPath, "utf-8");
2079
- const updated = appendSection(overwrite ? orExit(removeSection(raw, header), (err) => `${RED}Error:${RESET} ${err._tag}: ${err.section}`, 2) : raw, block);
2214
+ const baseRaw = overwrite ? orExit(removeSection(raw, header), (err) => `${RED}Error:${RESET} ${err._tag}: ${err.section}`, 2) : raw;
2215
+ const updated = appendSection(baseRaw, block);
2080
2216
  if (dryRun) {
2081
2217
  previewIfValid(updated, block);
2082
2218
  return;
@@ -2117,7 +2253,8 @@ const runCopy = (key, options) => {
2117
2253
  process.exit(1);
2118
2254
  }
2119
2255
  const block = inSecret ? sealedSecretBlock(key, destName, srcConfig, src.path, destConfig) : serializeEnvBlock(destName, srcConfig.env[key]);
2120
- const successMsg = `${GREEN}✓${RESET} Copied ${kindWord} ${BOLD}${key}${RESET}${destName !== key ? ` → ${BOLD}${destName}${RESET}` : ""} to ${CYAN}${dest.path}${RESET}`;
2256
+ const renamed = destName !== key ? ` → ${BOLD}${destName}${RESET}` : "";
2257
+ const successMsg = `${GREEN}✓${RESET} Copied ${kindWord} ${BOLD}${key}${RESET}${renamed} to ${CYAN}${dest.path}${RESET}`;
2121
2258
  writeBlock(dest.path, `[${kindWord}.${destName}]`, block, existsInDest, options.dryRun === true, successMsg);
2122
2259
  };
2123
2260
  //#endregion
@@ -2902,7 +3039,8 @@ const VALUE_SHAPE_PATTERNS = [
2902
3039
  ];
2903
3040
  /** Detect service from value prefix/shape */
2904
3041
  const matchValueShape = (value) => {
2905
- return Option(VALUE_SHAPE_PATTERNS.find((vp) => value.startsWith(vp.prefix))).map((vp) => ({
3042
+ const match = VALUE_SHAPE_PATTERNS.find((vp) => value.startsWith(vp.prefix));
3043
+ return Option(match).map((vp) => ({
2906
3044
  service: vp.service,
2907
3045
  description: vp.description
2908
3046
  }));
@@ -2942,7 +3080,8 @@ const matchEnvVar = (name, value) => {
2942
3080
  matchedBy: `exact:${exactMatch.pattern}`
2943
3081
  });
2944
3082
  return matchValueShape(value).fold(() => {
2945
- return Option(SUFFIX_PATTERNS.find((sp) => name.endsWith(sp.suffix))).map((sp) => ({
3083
+ const suffixMatch = SUFFIX_PATTERNS.find((sp) => name.endsWith(sp.suffix));
3084
+ return Option(suffixMatch).map((sp) => ({
2946
3085
  envVar: name,
2947
3086
  value,
2948
3087
  service: Option(deriveServiceFromName(name)),
@@ -3144,7 +3283,8 @@ const runEnvCheck = (options) => {
3144
3283
  console.error(formatError(err));
3145
3284
  process.exit(2);
3146
3285
  }, (rawConfig) => {
3147
- resolveConfig(rawConfig, dirname(path)).fold((err) => {
3286
+ const configDir = dirname(path);
3287
+ resolveConfig(rawConfig, configDir).fold((err) => {
3148
3288
  console.error(formatError(err));
3149
3289
  process.exit(2);
3150
3290
  }, (resolveResult) => {
@@ -3315,7 +3455,9 @@ const runEnvAdd = (name, value, options) => {
3315
3455
  console.log(block);
3316
3456
  return;
3317
3457
  }
3318
- writeIfValid(configPath, appendSection(readFileSync(configPath, "utf-8"), block), `${GREEN}✓${RESET} Added ${BOLD}${name}${RESET} to ${CYAN}${configPath}${RESET}`);
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}`);
3319
3461
  });
3320
3462
  });
3321
3463
  };
@@ -3436,7 +3578,9 @@ const runEnvAlias = (name, options) => {
3436
3578
  return;
3437
3579
  }
3438
3580
  const raw = readFileSync(configPath, "utf-8");
3439
- writeIfValid(configPath, appendSection(existing ? removeSection(raw, `[env.${name}]`).fold(() => raw, (r) => r) : raw, block), `${GREEN}✓${RESET} Aliased ${BOLD}${name}${RESET} → ${BOLD}${options.from}${RESET} in ${CYAN}${configPath}${RESET}`);
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}`);
3440
3584
  });
3441
3585
  });
3442
3586
  };
@@ -3561,7 +3705,7 @@ function* findEnvpktFiles(dir, maxDepth, currentDepth = 0) {
3561
3705
  }
3562
3706
  const scanFleet = (rootDir, options) => {
3563
3707
  const maxDepth = options?.maxDepth ?? 3;
3564
- const agentList = List(Array.from(findEnvpktFiles(rootDir, maxDepth)).flatMap((configPath) => loadConfig(configPath).fold(() => [], (config) => {
3708
+ const agents = Array.from(findEnvpktFiles(rootDir, maxDepth)).flatMap((configPath) => loadConfig(configPath).fold(() => [], (config) => {
3565
3709
  const audit = computeAudit(config);
3566
3710
  return [{
3567
3711
  path: configPath,
@@ -3569,7 +3713,8 @@ const scanFleet = (rootDir, options) => {
3569
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(),
3570
3714
  audit
3571
3715
  }];
3572
- })));
3716
+ }));
3717
+ const agentList = List(agents);
3573
3718
  const total_agents = agentList.size;
3574
3719
  const total_secrets = agentList.toArray().reduce((acc, a) => acc + a.audit.total, 0);
3575
3720
  const expired = agentList.toArray().reduce((acc, a) => acc + a.audit.expired, 0);
@@ -3595,7 +3740,8 @@ const statusIcon = (status) => {
3595
3740
  }
3596
3741
  };
3597
3742
  const runFleet = (options) => {
3598
- const fleet = scanFleet(resolve(options.dir ?? "."), { maxDepth: options.depth });
3743
+ const rootDir = resolve(options.dir ?? ".");
3744
+ const fleet = scanFleet(rootDir, { maxDepth: options.depth });
3599
3745
  if (options.format === "json") {
3600
3746
  console.log(formatFleetJson(fleet));
3601
3747
  process.exit(fleet.status === "critical" ? 2 : 0);
@@ -3614,6 +3760,36 @@ const runFleet = (options) => {
3614
3760
  process.exit(fleet.status === "critical" ? 2 : 0);
3615
3761
  };
3616
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
3617
3793
  //#region src/cli/commands/init.ts
3618
3794
  const CONFIG_FILENAME = "envpkt.toml";
3619
3795
  /** Resolve the top-level `scope` to scaffold: explicit --scope wins; --global implies "shell". */
@@ -3889,7 +4065,8 @@ const deriveKeyPath = (configPath) => {
3889
4065
  };
3890
4066
  const runKeygen = (options) => {
3891
4067
  const configPath = resolve(options.config ?? join(process.cwd(), "envpkt.toml"));
3892
- generateKeypair({ outputPath: options.output ?? (options.global ? resolveKeyPath() : deriveKeyPath(configPath)) }).fold((err) => {
4068
+ const outputPath = options.output ?? (options.global ? resolveKeyPath() : deriveKeyPath(configPath));
4069
+ generateKeypair({ outputPath }).fold((err) => {
3893
4070
  if (err._tag === "KeyExists") {
3894
4071
  console.error(`${YELLOW}Warning:${RESET} Identity file already exists: ${CYAN}${err.path}${RESET}`);
3895
4072
  console.error(`${DIM}To replace it: remove the file first, then re-run keygen.${RESET}`);
@@ -4259,7 +4436,8 @@ const runResolve = (options) => {
4259
4436
  console.error(formatError(err));
4260
4437
  process.exit(2);
4261
4438
  }, (config) => {
4262
- resolveConfig(config, dirname(configPath)).fold((err) => {
4439
+ const configDir = dirname(configPath);
4440
+ resolveConfig(config, configDir).fold((err) => {
4263
4441
  console.error(formatError(err));
4264
4442
  process.exit(2);
4265
4443
  }, (result) => {
@@ -4486,7 +4664,8 @@ const runSeal = async (options) => {
4486
4664
  process.exit(2);
4487
4665
  }
4488
4666
  const identityKey = Option(config.identity.key_file).flatMap((keyFile) => {
4489
- return unwrapAgentKey(resolve(configDir, expandPath(keyFile))).fold((err) => {
4667
+ const identityPath = resolve(configDir, expandPath(keyFile));
4668
+ return unwrapAgentKey(identityPath).fold((err) => {
4490
4669
  const msg = err._tag === "IdentityNotFound" ? `not found: ${err.path}` : err.message;
4491
4670
  console.error(`${YELLOW}Warning:${RESET} Could not unwrap agent key: ${msg}`);
4492
4671
  return Option.none();
@@ -4582,7 +4761,8 @@ const runSeal = async (options) => {
4582
4761
  console.error(`${DIM} 3. ~/.envpkt/age-key.txt${RESET}`);
4583
4762
  process.exit(2);
4584
4763
  }
4585
- const decrypted = unsealSecrets(Object.fromEntries(alreadySealed.map((k) => [k, allSecretEntries[k]])), identityPath).fold((err) => {
4764
+ const sealedEntries = Object.fromEntries(alreadySealed.map((k) => [k, allSecretEntries[k]]));
4765
+ const decrypted = unsealSecrets(sealedEntries, identityPath).fold((err) => {
4586
4766
  console.error(`${RED}Error:${RESET} Failed to decrypt existing secrets: ${err.message}`);
4587
4767
  process.exit(2);
4588
4768
  return {};
@@ -4719,7 +4899,8 @@ const runSecretAdd = (name, options) => {
4719
4899
  process.exit(1);
4720
4900
  }
4721
4901
  const block = buildSecretBlock(name, options);
4722
- const updated = appendSection(readFileSync(configPath, "utf-8"), block);
4902
+ const raw = readFileSync(configPath, "utf-8");
4903
+ const updated = appendSection(raw, block);
4723
4904
  if (options.dryRun) {
4724
4905
  previewIfValid(updated, block);
4725
4906
  return;
@@ -4860,7 +5041,9 @@ const runSecretAlias = (name, options) => {
4860
5041
  return;
4861
5042
  }
4862
5043
  const raw = readFileSync(configPath, "utf-8");
4863
- writeIfValid(configPath, appendSection(existing ? removeSection(raw, `[secret.${name}]`).fold(() => raw, (r) => r) : raw, block), `${GREEN}✓${RESET} Aliased ${BOLD}${name}${RESET} → ${BOLD}${options.from}${RESET} in ${CYAN}${configPath}${RESET}`);
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}`);
4864
5047
  });
4865
5048
  });
4866
5049
  };
@@ -4943,7 +5126,8 @@ const runSecretRotate = async (name, options) => {
4943
5126
  process.exit(2);
4944
5127
  return "";
4945
5128
  }, (ct) => ct);
4946
- updateSectionFields(applySealedToml(raw, { [name]: { encrypted_value: ciphertext } }), `[secret.${name}]`, { last_rotated_at: `"${today}"` }).fold((err) => {
5129
+ const withSealed = applySealedToml(raw, { [name]: { encrypted_value: ciphertext } });
5130
+ updateSectionFields(withSealed, `[secret.${name}]`, { last_rotated_at: `"${today}"` }).fold((err) => {
4947
5131
  console.error(`${RED}Error:${RESET} ${err._tag}: ${err.section}`);
4948
5132
  process.exit(2);
4949
5133
  }, (result) => {
@@ -5178,6 +5362,7 @@ const CHECK_NAMES = {
5178
5362
  schema: "Schema",
5179
5363
  catalog: "Catalog",
5180
5364
  aliases: "Aliases",
5365
+ mcp: "MCP headers",
5181
5366
  sealed: "Sealed blocks"
5182
5367
  };
5183
5368
  /** Structural sanity scan over each [secret.*].encrypted_value PEM block. No decryption. */
@@ -5206,6 +5391,24 @@ const checkSealedBlocks = (config) => {
5206
5391
  error: broken.join("; ")
5207
5392
  };
5208
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
+ };
5209
5412
  const skipped = (name) => ({
5210
5413
  name,
5211
5414
  status: "skipped"
@@ -5223,6 +5426,7 @@ const buildReport = (path, raw) => {
5223
5426
  skipped(CHECK_NAMES.schema),
5224
5427
  skipped(CHECK_NAMES.catalog),
5225
5428
  skipped(CHECK_NAMES.aliases),
5429
+ skipped(CHECK_NAMES.mcp),
5226
5430
  skipped(CHECK_NAMES.sealed)
5227
5431
  ]
5228
5432
  }), (data) => {
@@ -5242,6 +5446,7 @@ const buildReport = (path, raw) => {
5242
5446
  },
5243
5447
  skipped(CHECK_NAMES.catalog),
5244
5448
  skipped(CHECK_NAMES.aliases),
5449
+ skipped(CHECK_NAMES.mcp),
5245
5450
  skipped(CHECK_NAMES.sealed)
5246
5451
  ]
5247
5452
  }), (config) => {
@@ -5271,12 +5476,15 @@ const buildReport = (path, raw) => {
5271
5476
  status: "ok",
5272
5477
  detail: `${table.entries.size} alias(es)`
5273
5478
  }));
5479
+ const merged = config.catalog ? resolveConfig(config, dirname(path)).fold(() => config, (r) => r.config) : config;
5480
+ const mcpCheck = checkMcpHeaders(merged);
5274
5481
  const sealedCheck = checkSealedBlocks(config);
5275
5482
  const checks = [
5276
5483
  tomlOk,
5277
5484
  schemaOk,
5278
5485
  catalogCheck,
5279
5486
  aliasCheck,
5487
+ mcpCheck,
5280
5488
  sealedCheck
5281
5489
  ];
5282
5490
  return {
@@ -5299,7 +5507,9 @@ const renderCheckLines = (check) => {
5299
5507
  }
5300
5508
  };
5301
5509
  const formatTextReport = (report, sourceMsg) => {
5302
- const header = `${report.ok ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`} ${BOLD}${report.ok ? `${GREEN}VALID${RESET}` : `${RED}INVALID${RESET}`}${RESET} — ${CYAN}${report.configPath}${RESET}`;
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}`;
5303
5513
  const checkLines = report.checks.flatMap((c) => [...renderCheckLines(c)]);
5304
5514
  return [
5305
5515
  ...sourceMsg ? [sourceMsg] : [],
@@ -5384,7 +5594,7 @@ program.command("audit").description("Audit credential health from envpkt.toml (
5384
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) => {
5385
5595
  runFleet(options);
5386
5596
  });
5387
- 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) => {
5388
5598
  runValidate(options);
5389
5599
  });
5390
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) => {
@@ -5393,6 +5603,9 @@ program.command("inspect").description("Display structured view of envpkt.toml")
5393
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) => {
5394
5604
  runExec(args, options);
5395
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
+ });
5396
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) => {
5397
5610
  runResolve(options);
5398
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;
@@ -339,6 +384,7 @@ type BootError = ConfigError | FnoxError | CatalogError | AliasError | {
339
384
  readonly _tag: "SealKeyUnavailable";
340
385
  readonly sealedKeys: ReadonlyArray<string>;
341
386
  readonly searched: ReadonlyArray<string>;
387
+ readonly configuredKeyFile?: string;
342
388
  } | IdentityError;
343
389
  type IdentityError = {
344
390
  readonly _tag: "AgeNotFound";
@@ -446,6 +492,19 @@ declare const isEnvAlias: (meta: {
446
492
  /** Format an alias error into a human-readable message */
447
493
  declare const formatAliasError: (error: AliasError) => string;
448
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
449
508
  //#region src/core/format.d.ts
450
509
  type SecretDisplay = "encrypted" | "plaintext";
451
510
  type FormatPacketOptions = {
@@ -720,4 +779,4 @@ type ToolDef = {
720
779
  declare const toolDefinitions: readonly ToolDef[];
721
780
  declare const callTool: (name: string, args: Record<string, unknown>) => CallToolResult;
722
781
  //#endregion
723
- 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) return Option({
213
- path: customMatch[0],
214
- source: "search"
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: List([...compiledSchema.Errors(data)].map((e) => `${e.path}: ${e.message}`))
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 ?? /* @__PURE__ */ new Set();
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
- return Option(from_key.match(/^secret\.(.+)$/)?.[1]);
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 targetKey = yield* $(Option((yield* $(Option(entry.from_key))).match(/^env\.(.+)$/)?.[1]));
755
- return yield* $(Option((yield* $(Option(envEntries[targetKey]))).value));
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
- return Option(VALUE_SHAPE_PATTERNS.find((vp) => value.startsWith(vp.prefix))).map((vp) => ({
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
- return Option(SUFFIX_PATTERNS.find((sp) => name.endsWith(sp.suffix))).map((sp) => ({
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)),
@@ -1983,11 +2106,13 @@ const resolveSealIdentity = (config, configDir) => {
1983
2106
  });
1984
2107
  const inlineKey = resolveInlineKey().orUndefined();
1985
2108
  if (inlineKey) return Option(materializeInlineKey(inlineKey));
1986
- const defaultPath = join(homedir(), ".envpkt", "age-key.txt");
1987
- if (existsSync(defaultPath)) return Option({
1988
- path: defaultPath,
1989
- dispose: noop
1990
- });
2109
+ if (!config.identity?.key_file) {
2110
+ const defaultPath = join(homedir(), ".envpkt", "age-key.txt");
2111
+ if (existsSync(defaultPath)) return Option({
2112
+ path: defaultPath,
2113
+ dispose: noop
2114
+ });
2115
+ }
1991
2116
  return Option(void 0);
1992
2117
  };
1993
2118
  /** Describe the seal-identity precedence chain with per-entry status, for a clear no-key error. */
@@ -2002,7 +2127,7 @@ const describeSealKeySearch = (config, configDir) => {
2002
2127
  keyFileLine,
2003
2128
  envFileLine,
2004
2129
  inlineLine,
2005
- `${defaultPath} (${existsSync(defaultPath) ? "found" : "missing"})`
2130
+ keyFile ? `${defaultPath} (skipped — identity.key_file is set)` : `${defaultPath} (${existsSync(defaultPath) ? "found" : "missing"})`
2006
2131
  ];
2007
2132
  };
2008
2133
  const resolveIdentityKey = (config, configDir) => {
@@ -2073,12 +2198,14 @@ const bootSafe = (options) => {
2073
2198
  _tag: "ReadError",
2074
2199
  message: "unexpected"
2075
2200
  }));
2076
- const audit = computeAudit(config, detectFnoxKeys(configDir), void 0, aliasTable);
2201
+ const fnoxKeys = detectFnoxKeys(configDir);
2202
+ const audit = computeAudit(config, fnoxKeys, void 0, aliasTable);
2077
2203
  const sealIdentity = hasSealedValues ? resolveSealIdentity(config, configDir) : Option(void 0);
2078
2204
  if (hasSealedValues && sealIdentity.isEmpty) return Left({
2079
2205
  _tag: "SealKeyUnavailable",
2080
2206
  sealedKeys: nonAliasMetaKeys.filter((k) => !!nonAliasSecretEntries[k]?.encrypted_value),
2081
- searched: describeSealKeySearch(config, configDir)
2207
+ searched: describeSealKeySearch(config, configDir),
2208
+ configuredKeyFile: config.identity?.key_file ? resolve(configDir, expandPath(config.identity.key_file)) : void 0
2082
2209
  });
2083
2210
  return checkExpiration(audit, failOnExpired, warnOnly).map((warnings) => {
2084
2211
  const secrets = {};
@@ -2114,37 +2241,39 @@ const bootSafe = (options) => {
2114
2241
  }
2115
2242
  });
2116
2243
  const remainingKeys = nonAliasMetaKeys.filter((k) => !sealedKeys.has(k));
2117
- if (remainingKeys.length > 0) if (fnoxAvailable()) fnoxExport(opts.profile, identityKey.orUndefined()).fold((err) => {
2118
- log.warn("phase.fnox.export_failed", {
2119
- message: err.message,
2120
- skipped: remainingKeys.length
2121
- });
2122
- warnings.push(`fnox export failed: ${err.message}`);
2123
- skipped.push(...remainingKeys);
2124
- }, (exported) => {
2125
- const found = remainingKeys.filter((key) => key in exported);
2126
- const notFound = remainingKeys.filter((key) => !(key in exported));
2127
- found.forEach((key) => {
2128
- secrets[key] = exported[key];
2129
- log.debug("phase.fnox.resolved", {
2130
- key,
2131
- 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
2132
2249
  });
2133
- });
2134
- notFound.forEach((key) => {
2135
- log.debug("phase.fnox.not_in_export", {
2136
- key,
2137
- profile: opts.profile
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
+ });
2138
2261
  });
2262
+ notFound.forEach((key) => {
2263
+ log.debug("phase.fnox.not_in_export", {
2264
+ key,
2265
+ profile: opts.profile
2266
+ });
2267
+ });
2268
+ injected.push(...found);
2269
+ skipped.push(...notFound);
2139
2270
  });
2140
- injected.push(...found);
2141
- skipped.push(...notFound);
2142
- });
2143
- else {
2144
- log.debug("phase.fnox.unavailable", { skipped: remainingKeys.length });
2145
- if (!hasSealedValues) warnings.push("fnox not available — no secrets injected");
2146
- else warnings.push("fnox not available — unsealed secrets could not be resolved");
2147
- 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
+ }
2148
2277
  }
2149
2278
  aliasSecretKeys.forEach((aliasKey) => {
2150
2279
  const entry = aliasTable.entries.get(`secret.${aliasKey}`);
@@ -2232,12 +2361,13 @@ const formatBootError = (error) => {
2232
2361
  case "SealKeyUnavailable": {
2233
2362
  const keys = error.sealedKeys.length;
2234
2363
  const searched = error.searched.map((line) => ` • ${line}`).join("\n");
2364
+ 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)`;
2235
2365
  return [
2236
2366
  `${keys} sealed secret(s) can't be decrypted — no age key found.`,
2237
2367
  `Searched (in order):`,
2238
2368
  searched,
2239
2369
  `Fix one:`,
2240
- ` • Restore your key to ~/.envpkt/age-key.txt (or set ENVPKT_AGE_KEY_FILE / ENVPKT_AGE_KEY)`,
2370
+ restoreLine,
2241
2371
  ` • Re-provision from source: envpkt seal --edit <KEY>`,
2242
2372
  `Refusing to inject empty values for sealed secrets.`
2243
2373
  ].join("\n");
@@ -2580,7 +2710,7 @@ function* findEnvpktFiles(dir, maxDepth, currentDepth = 0) {
2580
2710
  }
2581
2711
  const scanFleet = (rootDir, options) => {
2582
2712
  const maxDepth = options?.maxDepth ?? 3;
2583
- const agentList = List(Array.from(findEnvpktFiles(rootDir, maxDepth)).flatMap((configPath) => loadConfig(configPath).fold(() => [], (config) => {
2713
+ const agents = Array.from(findEnvpktFiles(rootDir, maxDepth)).flatMap((configPath) => loadConfig(configPath).fold(() => [], (config) => {
2584
2714
  const audit = computeAudit(config);
2585
2715
  return [{
2586
2716
  path: configPath,
@@ -2588,7 +2718,8 @@ const scanFleet = (rootDir, options) => {
2588
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(),
2589
2719
  audit
2590
2720
  }];
2591
- })));
2721
+ }));
2722
+ const agentList = List(agents);
2592
2723
  const total_agents = agentList.size;
2593
2724
  const total_secrets = agentList.toArray().reduce((acc, a) => acc + a.audit.total, 0);
2594
2725
  const expired = agentList.toArray().reduce((acc, a) => acc + a.audit.expired, 0);
@@ -2925,4 +3056,4 @@ const startServer = async () => {
2925
3056
  await server.connect(transport);
2926
3057
  };
2927
3058
  //#endregion
2928
- 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.14.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.29.0",
42
+ "@modelcontextprotocol/sdk": "^1.32.0",
43
43
  "@sinclair/typebox": "^0.34.52",
44
44
  "commander": "^15.0.0",
45
- "functype": "^1.6.3",
46
- "functype-log": "^1.6.3",
47
- "functype-os": "^1.6.3",
48
- "smol-toml": "^1.7.0"
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.13.3",
52
- "ts-builds": "^3.2.2",
53
- "tsdown": "^0.22.9",
54
- "tsx": "^4.23.1"
51
+ "@types/node": "^24.19.1",
52
+ "ts-builds": "^3.4.3",
53
+ "tsdown": "^0.22.14",
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.15.0+sha512.266f8957a30d2be6e9468e5e66bcdedd35a794175f71b067ba8504d686cce1d0c0f429b33c323c3c569ad4891e667574a49ff71d1b89a22cc66f13c65818c578"
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
  }