envpkt 0.14.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 = {
@@ -622,13 +680,14 @@ declare const diffConfigs: (a: EnvpktConfig, b: EnvpktConfig) => ConfigDiff;
622
680
  //#region src/core/copy.d.ts
623
681
  /**
624
682
  * The SecretMeta to write into the destination on copy.
625
- * - `created` is reset to today: the entry is new *here*, regardless of the source's age.
683
+ * - `created` comes from the caller, never the source: today for an entry new to the
684
+ * destination, or the destination's own `created` when the copy overwrites it.
626
685
  * - `last_rotated_at` is dropped — it's the source's rotation history, not the copy's.
627
686
  * - `encryptedValue` re-derives the ciphertext: `Some(cipher)` sets the resealed value,
628
687
  * `None` strips it entirely (a metadata-only copy of a secret with no sealed value).
629
688
  */
630
689
  declare const copyableSecretMeta: (meta: SecretMeta, opts: {
631
- readonly today: string;
690
+ readonly created: string;
632
691
  readonly encryptedValue: Option<string>;
633
692
  }) => SecretMeta;
634
693
  /** Serialize a `[secret.<name>]` block from its metadata, round-trippable by the TOML parser. */
@@ -638,10 +697,15 @@ declare const serializeEnvBlock: (name: string, meta: EnvMeta) => string;
638
697
  //#endregion
639
698
  //#region src/core/toml-edit.d.ts
640
699
  /**
641
- * Remove a TOML section (e.g. `[secret.X]`) and all its fields through the next section or EOF.
642
- * Strips trailing blank lines left behind.
700
+ * Remove a TOML section (e.g. `[secret.X]`) and all its fields. A comment above the next
701
+ * section stays. The sections on either side end up one blank line apart.
643
702
  */
644
703
  declare const removeSection: (raw: string, sectionHeader: string) => Either<TomlEditError, string>;
704
+ /**
705
+ * Replace a TOML section with `block`, in place. Lines outside the section, including the
706
+ * blank and comment lines between it and the next header, are left as they were.
707
+ */
708
+ declare const replaceSection: (raw: string, sectionHeader: string, block: string) => Either<TomlEditError, string>;
645
709
  /**
646
710
  * Rename a TOML section header (e.g. `[secret.OLD]` → `[secret.NEW]`).
647
711
  * Errors if old doesn't exist or new already exists.
@@ -721,4 +785,4 @@ type ToolDef = {
721
785
  declare const toolDefinitions: readonly ToolDef[];
722
786
  declare const callTool: (name: string, args: Record<string, unknown>) => CallToolResult;
723
787
  //#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 };
788
+ 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, replaceSection, 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
+ });
2141
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);
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}`);
@@ -2381,7 +2507,8 @@ const tomlInlineTable = (rec) => {
2381
2507
  };
2382
2508
  /**
2383
2509
  * The SecretMeta to write into the destination on copy.
2384
- * - `created` is reset to today: the entry is new *here*, regardless of the source's age.
2510
+ * - `created` comes from the caller, never the source: today for an entry new to the
2511
+ * destination, or the destination's own `created` when the copy overwrites it.
2385
2512
  * - `last_rotated_at` is dropped — it's the source's rotation history, not the copy's.
2386
2513
  * - `encryptedValue` re-derives the ciphertext: `Some(cipher)` sets the resealed value,
2387
2514
  * `None` strips it entirely (a metadata-only copy of a secret with no sealed value).
@@ -2390,10 +2517,10 @@ const copyableSecretMeta = (meta, opts) => {
2390
2517
  const { last_rotated_at: _lra, encrypted_value: _ev, ...rest } = meta;
2391
2518
  return opts.encryptedValue.fold(() => ({
2392
2519
  ...rest,
2393
- created: opts.today
2520
+ created: opts.created
2394
2521
  }), (cipher) => ({
2395
2522
  ...rest,
2396
- created: opts.today,
2523
+ created: opts.created,
2397
2524
  encrypted_value: cipher
2398
2525
  }));
2399
2526
  };
@@ -2468,11 +2595,28 @@ const findSectionRange = (lines, sectionHeader) => {
2468
2595
  end: List(lines.slice(start + 1)).zipWithIndex().foldLeft(initial)((state, entry) => scanSectionBoundary(state, entry[0], start + 1 + entry[1])).end
2469
2596
  };
2470
2597
  };
2598
+ /**
2599
+ * End (exclusive) of a section's own lines: through its last field, before the blank and
2600
+ * comment lines that run up to the next header. Those trailing lines separate it from its
2601
+ * neighbour (or are the neighbour's doc comment), so edits to this section leave them alone.
2602
+ */
2603
+ const sectionBodyEnd = (lines, range) => {
2604
+ const lastContent = lines.slice(range.start, range.end).findLastIndex((l) => {
2605
+ const t = l.trim();
2606
+ return t !== "" && !t.startsWith("#");
2607
+ });
2608
+ return range.start + lastContent + 1;
2609
+ };
2610
+ const dropTrailingBlanks = (xs) => xs.slice(0, xs.findLastIndex((l) => l.trim() !== "") + 1);
2611
+ const dropLeadingBlanks = (xs) => {
2612
+ const first = xs.findIndex((l) => l.trim() !== "");
2613
+ return first === -1 ? [] : xs.slice(first);
2614
+ };
2471
2615
  /** Check whether a section header exists in the raw TOML */
2472
2616
  const sectionExists = (lines, sectionHeader) => lines.some((l) => l.trim() === sectionHeader);
2473
2617
  /**
2474
- * Remove a TOML section (e.g. `[secret.X]`) and all its fields through the next section or EOF.
2475
- * Strips trailing blank lines left behind.
2618
+ * Remove a TOML section (e.g. `[secret.X]`) and all its fields. A comment above the next
2619
+ * section stays. The sections on either side end up one blank line apart.
2476
2620
  */
2477
2621
  const removeSection = (raw, sectionHeader) => {
2478
2622
  const lines = raw.split("\n");
@@ -2481,11 +2625,33 @@ const removeSection = (raw, sectionHeader) => {
2481
2625
  _tag: "SectionNotFound",
2482
2626
  section: sectionHeader
2483
2627
  });
2484
- const after = lines.slice(range.end);
2485
- const beforeAll = lines.slice(0, range.start);
2486
- const lastNonBlank = beforeAll.findLastIndex((l) => l.trim() !== "");
2487
- const result = [...lastNonBlank === -1 ? [] : beforeAll.slice(0, lastNonBlank + 1), ...after].join("\n");
2488
- return Either.right(result);
2628
+ const before = dropTrailingBlanks(lines.slice(0, range.start));
2629
+ const after = dropLeadingBlanks(lines.slice(sectionBodyEnd(lines, range)));
2630
+ if (after.length === 0) return Either.right([...before, ...raw.endsWith("\n") ? [""] : []].join("\n"));
2631
+ if (before.length === 0) return Either.right(after.join("\n"));
2632
+ return Either.right([
2633
+ ...before,
2634
+ "",
2635
+ ...after
2636
+ ].join("\n"));
2637
+ };
2638
+ /**
2639
+ * Replace a TOML section with `block`, in place. Lines outside the section, including the
2640
+ * blank and comment lines between it and the next header, are left as they were.
2641
+ */
2642
+ const replaceSection = (raw, sectionHeader, block) => {
2643
+ const lines = raw.split("\n");
2644
+ const range = findSectionRange(lines, sectionHeader);
2645
+ if (!range) return Either.left({
2646
+ _tag: "SectionNotFound",
2647
+ section: sectionHeader
2648
+ });
2649
+ const replaced = [
2650
+ ...lines.slice(0, range.start),
2651
+ ...block.trimEnd().split("\n"),
2652
+ ...lines.slice(sectionBodyEnd(lines, range))
2653
+ ];
2654
+ return Either.right(replaced.join("\n"));
2489
2655
  };
2490
2656
  /**
2491
2657
  * Rename a TOML section header (e.g. `[secret.OLD]` → `[secret.NEW]`).
@@ -2584,7 +2750,7 @@ function* findEnvpktFiles(dir, maxDepth, currentDepth = 0) {
2584
2750
  }
2585
2751
  const scanFleet = (rootDir, options) => {
2586
2752
  const maxDepth = options?.maxDepth ?? 3;
2587
- const agentList = List(Array.from(findEnvpktFiles(rootDir, maxDepth)).flatMap((configPath) => loadConfig(configPath).fold(() => [], (config) => {
2753
+ const agents = Array.from(findEnvpktFiles(rootDir, maxDepth)).flatMap((configPath) => loadConfig(configPath).fold(() => [], (config) => {
2588
2754
  const audit = computeAudit(config);
2589
2755
  return [{
2590
2756
  path: configPath,
@@ -2592,7 +2758,8 @@ const scanFleet = (rootDir, options) => {
2592
2758
  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
2759
  audit
2594
2760
  }];
2595
- })));
2761
+ }));
2762
+ const agentList = List(agents);
2596
2763
  const total_agents = agentList.size;
2597
2764
  const total_secrets = agentList.toArray().reduce((acc, a) => acc + a.audit.total, 0);
2598
2765
  const expired = agentList.toArray().reduce((acc, a) => acc + a.audit.expired, 0);
@@ -2929,4 +3096,4 @@ const startServer = async () => {
2929
3096
  await server.connect(transport);
2930
3097
  };
2931
3098
  //#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 };
3099
+ 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, replaceSection, 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.16.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.1",
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.15.0",
46
+ "functype-log": "^1.15.0",
47
+ "functype-os": "^1.15.0",
48
+ "smol-toml": "^1.9.1"
49
49
  },
50
50
  "devDependencies": {
51
- "@types/node": "^24.13.3",
52
- "ts-builds": "^3.2.3",
51
+ "@types/node": "^24.19.2",
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
  }