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 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 */
@@ -856,7 +879,8 @@ const runAudit = (options) => {
856
879
  console.error(formatError(err));
857
880
  process.exit(2);
858
881
  }, (rawConfig) => {
859
- resolveConfig(rawConfig, dirname(path)).fold((err) => {
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) if (options.format === "json") console.log(formatEnvAuditJson(config));
924
- else formatEnvAuditTable(config);
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 audit = computeAudit(config, detectFnoxKeys(configDir), void 0, aliasTable);
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) if (fnoxAvailable()) fnoxExport(opts.profile, identityKey.orUndefined()).fold((err) => {
1543
- log.warn("phase.fnox.export_failed", {
1544
- message: err.message,
1545
- skipped: remainingKeys.length
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
- notFound.forEach((key) => {
1560
- log.debug("phase.fnox.not_in_export", {
1561
- key,
1562
- 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
+ });
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
- injected.push(...found);
1566
- skipped.push(...notFound);
1567
- });
1568
- else {
1569
- log.debug("phase.fnox.unavailable", { skipped: remainingKeys.length });
1570
- if (!hasSealedValues) warnings.push("fnox not available — no secrets injected");
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(orExit(ageEncrypt(plaintext, recipient), (err) => `${RED}Error:${RESET} reseal failed for "${destName}": ${err.message}`, 2))
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 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);
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 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}`;
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
- 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) => ({
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
- 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) => ({
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
- resolveConfig(rawConfig, dirname(path)).fold((err) => {
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
- 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}`);
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
- 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}`);
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 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) => {
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 fleet = scanFleet(resolve(options.dir ?? "."), { maxDepth: options.depth });
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
- 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) => {
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
- resolveConfig(config, dirname(configPath)).fold((err) => {
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
- return unwrapAgentKey(resolve(configDir, expandPath(keyFile))).fold((err) => {
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 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) => {
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 updated = appendSection(readFileSync(configPath, "utf-8"), block);
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
- 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}`);
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
- 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) => {
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 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}`;
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) 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)),
@@ -2075,7 +2198,8 @@ const bootSafe = (options) => {
2075
2198
  _tag: "ReadError",
2076
2199
  message: "unexpected"
2077
2200
  }));
2078
- const audit = computeAudit(config, detectFnoxKeys(configDir), void 0, aliasTable);
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) if (fnoxAvailable()) fnoxExport(opts.profile, identityKey.orUndefined()).fold((err) => {
2121
- log.warn("phase.fnox.export_failed", {
2122
- message: err.message,
2123
- skipped: remainingKeys.length
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
- notFound.forEach((key) => {
2138
- log.debug("phase.fnox.not_in_export", {
2139
- key,
2140
- 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
+ });
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
- injected.push(...found);
2144
- skipped.push(...notFound);
2145
- });
2146
- else {
2147
- log.debug("phase.fnox.unavailable", { skipped: remainingKeys.length });
2148
- if (!hasSealedValues) warnings.push("fnox not available — no secrets injected");
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 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) => {
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.14.1",
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.30.0",
42
+ "@modelcontextprotocol/sdk": "^1.32.0",
43
43
  "@sinclair/typebox": "^0.34.52",
44
44
  "commander": "^15.0.0",
45
- "functype": "^1.8.0",
46
- "functype-log": "^1.8.0",
47
- "functype-os": "^1.8.0",
48
- "smol-toml": "^1.7.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.13.3",
52
- "ts-builds": "^3.2.3",
51
+ "@types/node": "^24.19.1",
52
+ "ts-builds": "^3.4.3",
53
53
  "tsdown": "^0.22.14",
54
- "tsx": "^4.23.1"
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.18.0+sha512.33d83c77da82f49fba836925c6f1b841181ec3132b670639bd012f7075f5c7cf634c5f870147c19aae7478fac01df09d8892e880454896edd23ee9b33757563c"
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
  }