Skip to content

Library API

envpkt is available as a TypeScript library with a functional programming API built on functype. All functions return Either<Error, Result> or Option<T> — no thrown exceptions.

import { boot, loadConfig, computeAudit, scanFleet } from "envpkt"

High-level API that loads config, resolves catalog, audits, and optionally injects secrets. Both functions are async: await them. Without await, boot() returns before it injects anything, so the next line reads process.env without the secrets. From CommonJS, use boot().then(…).

const result = await boot({ configPath: "envpkt.toml", inject: true })
console.log(result.audit.status) // "healthy" | "degraded" | "critical"

Rejects with EnvpktBootError on failure.

const safe = await bootSafe({ configPath: "envpkt.toml" })
safe.fold(
(err) => console.error("Boot failed:", err._tag),
(result) => console.log(`${result.injected.length} secrets injected`),
)

Resolves to Either<BootError, BootResult>. An unexpected crash inside boot (a bug, or a logger that throws) rejects with the original error instead of becoming a Left.

type BootOptions = {
readonly configPath?: string
readonly profile?: string
readonly inject?: boolean
readonly failOnExpired?: boolean
readonly warnOnly?: boolean
readonly logger?: DirectLogger // from functype-log; see "Diagnostic logging" below
}

bootSafe emits structured trace events at each resolution decision point. Pass a DirectLogger via BootOptions.logger to observe them; omit for silent default (zero overhead).

import { bootSafe, createDirectConsoleLogger } from "envpkt"
const logger = createDirectConsoleLogger({ level: "debug" })
await bootSafe({ logger })

Emitted events (all scoped with { component: "envpkt.boot" }):

| Level | Message | Metadata | | ------- | ------------------------------- | ---------------------------------------- | | debug | alias.validate.success | { aliases: number } | | warn | alias.validate.failed | { tag: AliasError._tag, key?: string } | | warn | phase.sealed.decrypt_failed | { message: string } | | debug | phase.sealed.resolved | { key: string } | | warn | phase.fnox.export_failed | { message: string, skipped: number } | | debug | phase.fnox.resolved | { key: string, profile?: string } | | debug | phase.fnox.not_in_export | { key: string, profile?: string } | | debug | phase.fnox.unavailable | { skipped: number } | | debug | phase.alias.copied | { alias: string, target: string } | | debug | phase.alias.target_unresolved | { alias: string, target: string } |

For tests, use createDirectTestLogger which captures entries in memory:

import { bootSafe, createDirectTestLogger } from "envpkt"
const { logger, entries, hasEntry } = createDirectTestLogger()
await bootSafe({ logger })
expect(hasEntry("debug", "phase.alias.copied")).toBe(true)
type BootResult = {
readonly audit: AuditResult
readonly injected: ReadonlyArray<string>
readonly skipped: ReadonlyArray<string>
readonly secrets: Readonly<Record<string, string>>
readonly warnings: ReadonlyArray<string>
readonly envDefaults: Readonly<Record<string, string>>
readonly overridden: ReadonlyArray<string>
// Logical key → injected wire name (e.g. API_KEY → CIV__API_KEY).
// Identity map when no [namespace] is configured. secrets/envDefaults
// stay keyed by the logical name; this recovers the actual process.env name.
readonly envNames: Readonly<Record<string, string>>
readonly configPath: string
readonly configSource: ConfigSource
}
type ConfigSource = "flag" | "env" | "cwd" | "search"

Indicates how the config was resolved: explicit --config flag, ENVPKT_CONFIG env var, current working directory, or the discovery search chain.

Load and validate an envpkt.toml file. Returns Either<ConfigError, EnvpktConfig>.

const config = loadConfig("envpkt.toml")
config.fold(
(err) => console.error("Failed:", err._tag),
(config) => console.log(config),
)

resolveConfigPath(flagPath?, envVar?, cwd?)

Section titled “resolveConfigPath(flagPath?, envVar?, cwd?)”

Resolve config path via priority chain: explicit flag → ENVPKT_CONFIG env var → CWD + discovery chain. Returns Either<ConfigError, ResolvedPath>.

type ResolvedPath = { readonly path: string; readonly source: ConfigSource }

Discover config by checking CWD, then ENVPKT_SEARCH_PATH, then built-in candidate paths (~/.envpkt/, cloud storage, etc.). Returns Option<{ path: string; source: "cwd" | "search" }>.

Load config from CWD or discovery chain. Returns Either<ConfigError, { path: string; source: string; config: EnvpktConfig }>.

Check for envpkt.toml in a specific directory. Returns Option<string>.

Parse raw TOML string. Returns Either<ConfigError, unknown>.

Validate parsed data against the schema. Returns Either<ConfigError, EnvpktConfig>.

Validate all from_key references in a resolved config. Returns Either<AliasError, AliasTable>. Called automatically inside bootSafe before the secret resolution passes.

import { validateAliases } from "envpkt"
validateAliases(config).fold(
(err) => console.error("Alias error:", err._tag),
(table) => console.log(`${table.entries.size} alias(es) resolved`),
)
type AliasTable = {
readonly entries: ReadonlyMap<
string,
{ readonly kind: "secret" | "env"; readonly targetKind: "secret" | "env"; readonly targetKey: string }
>
}

Keys are type-prefixed ("secret.<KEY>" or "env.<KEY>") so the two namespaces don’t collide.

  • Alias entries appear in BootResult.injected alongside their target when the target resolves; in BootResult.skipped when the target doesn’t.
  • BootResult.secrets[aliasKey] holds the same value as secrets[targetKey].
  • AuditResult.secrets includes alias rows with alias_of: Option<string> and status inherited from the target; AuditResult.aliases counts them separately.
  • Cross-type and chained aliases fail at load time with AliasError variants — see Error Types → AliasError.

Build the HTTP headers that envpkt headers prints for Claude Code’s headersHelper.

Collect every secret whose mcp list names server and merge them into one header object. values is keyed by logical secret name, as in BootResult.secrets, so aliases and namespaces are already resolved. Returns Either<McpHeadersError, Record<string, string>>. Only this server’s bindings are checked.

import { bootSafe, buildMcpHeaders, formatMcpHeadersError, loadConfig } from "envpkt"
const config = loadConfig("./envpkt.toml").orThrow()
const booted = await bootSafe({ inject: false })
booted.fold(
(err) => console.error(err._tag),
(boot) =>
buildMcpHeaders(config, boot.secrets, "civala-github").fold(
(err) => console.error(formatMcpHeadersError(err)),
(headers) => console.log(headers), // { Authorization: "Bearer ..." }
),
)

Check every mcp binding: header names and schemes must be HTTP tokens, and no (server, header) pair may be claimed twice. Returns Either<McpConfigError, void>. envpkt validate and the CLI write gate call it.

One-line message for any McpHeadersError. It names the server and secret keys and never includes a secret value.

TypeBox schema for one mcp entry: { server, header?, scheme? }.

Resolve catalog references. Returns Either<CatalogError, ResolveResult>.

import { loadConfig, resolveConfig } from "envpkt"
import { dirname } from "node:path"
const configPath = "agents/pipeline/envpkt.toml"
loadConfig(configPath).fold(
(err) => console.error(err),
(config) => {
resolveConfig(config, dirname(configPath)).fold(
(err) => console.error("Catalog error:", err._tag),
(result) => {
console.log("Resolved keys:", result.merged)
console.log("Overridden:", result.overridden)
},
)
},
)
type ResolveResult = {
readonly config: EnvpktConfig
readonly catalogPath?: string
readonly merged: ReadonlyArray<string>
readonly overridden: ReadonlyArray<string>
readonly warnings: ReadonlyArray<string>
}

Compute audit results for a config. Returns AuditResult.

const audit = computeAudit(config)
console.log(audit.status) // "healthy" | "degraded" | "critical"
audit.secrets.forEach((s) => {
console.log(`${s.key}: ${s.status}`)
})

Compute drift results for [env.*] entries. Returns EnvAuditResult.

const envAudit = computeEnvAudit(config)
console.log(`${envAudit.total} env defaults, ${envAudit.defaults_applied} using default`)
envAudit.entries.forEach((e) => {
console.log(`${e.key}: ${e.status}`) // "default" | "overridden" | "not_set"
})
type EnvAuditResult = {
readonly entries: ReadonlyArray<EnvDriftEntry>
readonly total: number
readonly defaults_applied: number
readonly overridden: number
readonly not_set: number
}
type EnvMeta = {
readonly value: string
readonly purpose?: string
readonly comment?: string
readonly tags?: Record<string, string>
}

Scan a directory tree for envpkt.toml files. Returns FleetHealth.

const fleet = scanFleet("/opt/agents", { maxDepth: 3 })
console.log(`${fleet.total_agents} agents, ${fleet.total_secrets} secrets`)

Scan an environment object for credentials. Returns ScanResult.

const scan = envScan(process.env)
console.log(`Found ${scan.discovered.size} credentials`)

Bidirectional drift detection. Returns CheckResult.

const check = envCheck(config, process.env)
if (!check.is_clean) {
console.log(`${check.missing_from_env} missing, ${check.untracked_credentials} untracked`)
}

Generate TOML [secret.*] blocks from scan results.

Match a single env var against all patterns. Returns Option<MatchResult>.

matchEnvVar("OPENAI_API_KEY", "sk-test123").fold(
() => console.log("Not a credential"),
(m) => console.log(`Matched: ${m.confidence} confidence`),
)

Produce a human-readable text summary of a resolved config.

import { formatPacket } from "envpkt"
// Without secrets
const text = formatPacket(resolveResult)
// With masked secrets
const masked = formatPacket(resolveResult, {
secrets: { DATABASE_URL: "postgres://user:pass@host/db" },
})
// With plaintext secrets
const plain = formatPacket(resolveResult, {
secrets: { DATABASE_URL: "postgres://user:pass@host/db" },
secretDisplay: "plaintext",
})

Mask a secret value for display (e.g., postgres://user:pass@host/db → pos•••••t/db).

Resolve the age identity file path: -o flag > ENVPKT_AGE_KEY_FILE env var > ~/.envpkt/age-key.txt. Returns string.

Check for inline age key in ENVPKT_AGE_KEY env var (for CI). Returns Option<string>.

Generate an age keypair in-process and write it to disk with 0600 permissions. Never overwrites an existing file. Resolves to Either<KeygenError, KeygenResult>.

import { generateKeypair } from "envpkt"
const result = await generateKeypair() // writes to resolveKeyPath()
result.fold(
(err) => console.error(`Keygen failed: ${err._tag}`),
(res) => console.log(`Recipient: ${res.recipient}`),
)

Options: { outputPath?: string }. outputPath defaults to resolveKeyPath(). It is used as given, so ~ is not expanded: pass join(homedir(), ".envpkt", "age-key.txt"), not "~/.envpkt/age-key.txt".

updateConfigIdentity(configPath, { recipient, keyFile?, name? })

Section titled “updateConfigIdentity(configPath, { recipient, keyFile?, name? })”

Set identity.recipient, and identity.key_file and identity.name when given, in an envpkt.toml file, preserving TOML structure. Creates the [identity] section if missing. Returns Either<KeygenError, true>.

type KeygenResult = {
readonly recipient: string
readonly identityPath: string
readonly configUpdated: boolean
}

Check for fnox availability.

readFnoxConfig(path) / extractFnoxKeys(config)

Section titled “readFnoxConfig(path) / extractFnoxKeys(config)”

Read and parse fnox configuration.

compareFnoxAndEnvpkt(fnoxConfig, envpktConfig)

Section titled “compareFnoxAndEnvpkt(fnoxConfig, envpktConfig)”

Compare keys between fnox and envpkt for orphan detection.

fnoxGet(key, profile?) / fnoxExport(keys, profile?)

Section titled “fnoxGet(key, profile?) / fnoxExport(keys, profile?)”

Retrieve secret values from fnox.

unwrapAgentKey(identityPath) / describeIdentityError(err)

Section titled “unwrapAgentKey(identityPath) / describeIdentityError(err)”

unwrapAgentKey reads every secret key (AGE-SECRET-KEY-…) from a plain age key file, returning Either<IdentityError, ReadonlyArray<string>>. Pass the keys to ageDecrypt or unsealSecrets. A missing, unreadable, passphrase-protected, plugin or SSH key file gives a typed IdentityError (see Error Types). describeIdentityError explains one without printing key material.

Create and start the MCP server programmatically.

toolDefinitions / callTool(name, args, config, audit)

Section titled “toolDefinitions / callTool(name, args, config, audit)”

MCP tool definitions and invocation.

resourceDefinitions / readResource(uri, config, audit)

Section titled “resourceDefinitions / readResource(uri, config, audit)”

MCP resource definitions and reading.