envpkt 0.16.0 → 0.18.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.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { FormatRegistry, Type } from "@sinclair/typebox";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { TypeCompiler } from "@sinclair/typebox/compiler";
4
- import { $, Cond, Do, Either, Left, List, Map as Map$1, None, Option, Right, Set as Set$1, Some, Try } from "functype";
4
+ import { $, Cond, Do, Either, Left, List, Map as Map$1, None, Option, Right, Set as Set$1, Some, Try, tryCatch, tryCatchAsync } from "functype";
5
5
  import { Env, Fs, Path, Platform } from "functype-os";
6
6
  import { TomlDate, parse } from "smol-toml";
7
- import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
8
- import { homedir, tmpdir } from "node:os";
7
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
9
  import { createDirectConsoleLogger, createDirectTestLogger, directSilentLogger, directSilentLogger as directSilentLogger$1 } from "functype-log/direct";
10
10
  import { execFileSync } from "node:child_process";
11
+ import { Decrypter, Encrypter, armor, generateX25519Identity, identityToRecipient } from "age-encryption";
11
12
  import { createInterface } from "node:readline";
12
13
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
13
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -1766,16 +1767,104 @@ const fnoxAvailable = () => Try(() => {
1766
1767
  return true;
1767
1768
  }).fold(() => false, (v) => v);
1768
1769
  //#endregion
1770
+ //#region src/core/age.ts
1771
+ /**
1772
+ * In-process age encryption via typage (`age-encryption`). This is the only module that imports
1773
+ * it. Ciphertext is age's ASCII armor, interoperable with the `age` CLI in both directions.
1774
+ */
1775
+ /** Encrypt `plaintext` to an age recipient (`age1…`), returning armored text without a trailing newline. */
1776
+ const encryptArmored = (plaintext, recipient) => tryCatchAsync(async () => {
1777
+ const encrypter = new Encrypter();
1778
+ encrypter.addRecipient(recipient);
1779
+ return armor.encode(await encrypter.encrypt(plaintext)).trim();
1780
+ }, (err) => ({
1781
+ _tag: "EncryptFailed",
1782
+ key: "",
1783
+ message: `age encrypt failed: ${String(err)}`
1784
+ }));
1785
+ const decryptFailed = (message) => ({
1786
+ _tag: "DecryptFailed",
1787
+ key: "",
1788
+ message: `age decrypt failed: ${message}`
1789
+ });
1790
+ /** Mask any age secret key in an error message. typage's bech32 errors quote the key they rejected. */
1791
+ const redactKeys = (message) => message.replace(/AGE-SECRET-KEY-[0-9A-Z-]+/gi, "AGE-SECRET-KEY-…");
1792
+ /** Add each identity to a fresh Decrypter. A malformed key fails with its position, never its text. */
1793
+ const toDecrypter = (identities) => identities.reduce((acc, identity, i) => acc.flatMap((decrypter) => tryCatch(() => {
1794
+ decrypter.addIdentity(identity);
1795
+ return decrypter;
1796
+ }, () => decryptFailed(`identity ${i + 1} is not a valid age secret key`))), Right(new Decrypter()));
1797
+ const utf8 = new TextDecoder("utf-8", { ignoreBOM: true });
1798
+ /**
1799
+ * Decrypt armored age ciphertext with any of `identities` (`AGE-SECRET-KEY-…`). The plaintext is
1800
+ * returned as-is: byte for byte when it is valid UTF-8.
1801
+ */
1802
+ const decryptArmored = (armored, identities) => {
1803
+ if (identities.length === 0) return Promise.resolve(Left(decryptFailed("no identity provided")));
1804
+ return toDecrypter(identities).fold((err) => Promise.resolve(Left(err)), (decrypter) => tryCatchAsync(async () => utf8.decode(await decrypter.decrypt(armor.decode(armored))), (err) => decryptFailed(redactKeys(String(err)))));
1805
+ };
1806
+ /** Generate an X25519 age keypair: the secret identity and its public recipient. */
1807
+ const generateAgeKeypair = () => tryCatchAsync(async () => {
1808
+ const identity = await generateX25519Identity();
1809
+ return {
1810
+ identity,
1811
+ recipient: await identityToRecipient(identity)
1812
+ };
1813
+ }, (err) => ({
1814
+ _tag: "KeygenFailed",
1815
+ message: `age keygen failed: ${String(err)}`
1816
+ }));
1817
+ const SECRET_KEY_PREFIX = "AGE-SECRET-KEY-";
1818
+ /** Classify a non-key line, so the error can name what it is without echoing its content. */
1819
+ const unsupportedLine = (line, lineNumber) => {
1820
+ if (line.startsWith("-----BEGIN AGE ENCRYPTED FILE-----") || line.startsWith("age-encryption.org/")) return { _tag: "PassphraseProtectedIdentity" };
1821
+ if (line.startsWith("AGE-PLUGIN-")) return {
1822
+ _tag: "UnsupportedIdentity",
1823
+ kind: "plugin",
1824
+ line: lineNumber
1825
+ };
1826
+ if (line.startsWith("-----BEGIN") || line.startsWith("ssh-")) return {
1827
+ _tag: "UnsupportedIdentity",
1828
+ kind: "ssh",
1829
+ line: lineNumber
1830
+ };
1831
+ return {
1832
+ _tag: "UnsupportedIdentity",
1833
+ kind: "unknown",
1834
+ line: lineNumber
1835
+ };
1836
+ };
1837
+ /**
1838
+ * Read every `AGE-SECRET-KEY-…` line from an age identity file, as the `age` CLI does. Blank lines
1839
+ * and `#` comments are skipped. Any other line — a plugin or SSH key, or a passphrase-protected
1840
+ * (age-encrypted) key file — is an error, since in-process decryption can't use it.
1841
+ */
1842
+ const parseIdentityFile = (content) => {
1843
+ const significant = content.split("\n").map((line, i) => ({
1844
+ text: line.trim(),
1845
+ number: i + 1
1846
+ })).filter(({ text }) => text !== "" && !text.startsWith("#"));
1847
+ const firstUnsupported = significant.find(({ text }) => !text.startsWith(SECRET_KEY_PREFIX));
1848
+ if (firstUnsupported) return Left(unsupportedLine(firstUnsupported.text, firstUnsupported.number));
1849
+ if (significant.length === 0) return Left({ _tag: "NoIdentityKey" });
1850
+ return Right(significant.map(({ text }) => text));
1851
+ };
1852
+ //#endregion
1769
1853
  //#region src/fnox/identity.ts
1770
- /** Check if the age CLI is available on PATH */
1771
- const ageAvailable = () => Try(() => {
1772
- execFileSync("age", ["--version"], { stdio: "pipe" });
1773
- return true;
1774
- }).fold(() => false, (v) => v);
1854
+ /** The `source` name for the inline key in identity errors. */
1855
+ const INLINE_KEY_SOURCE = "ENVPKT_AGE_KEY";
1775
1856
  /**
1776
- * Extract the secret key from an age identity file (plain or encrypted).
1777
- * - Plain identity files (from `age-keygen`) contain `AGE-SECRET-KEY-*` lines directly
1778
- * - Encrypted identity files need `age --decrypt` to unwrap
1857
+ * Read the age secret keys (`AGE-SECRET-KEY-…`) from identity text, such as a key file's content
1858
+ * or the inline `ENVPKT_AGE_KEY`. `source` names it in errors.
1859
+ */
1860
+ const parseIdentityKeys = (content, source) => parseIdentityFile(content).mapLeft((err) => ({
1861
+ ...err,
1862
+ source
1863
+ }));
1864
+ /**
1865
+ * Read every secret key from a plain age identity file (as written by `envpkt keygen` or
1866
+ * `age-keygen`). A missing or unreadable file, and passphrase-protected, plugin and SSH key files,
1867
+ * are rejected with a typed error.
1779
1868
  */
1780
1869
  const unwrapAgentKey = (identityPath) => {
1781
1870
  if (!existsSync(identityPath)) return Left({
@@ -1783,27 +1872,32 @@ const unwrapAgentKey = (identityPath) => {
1783
1872
  path: identityPath
1784
1873
  });
1785
1874
  return Try(() => readFileSync(identityPath, "utf-8")).fold((err) => Left({
1786
- _tag: "DecryptFailed",
1787
- message: `Failed to read identity file: ${err}`
1788
- }), (content) => {
1789
- const secretKeyLine = content.split("\n").find((l) => l.startsWith("AGE-SECRET-KEY-"));
1790
- if (secretKeyLine) return Right(secretKeyLine.trim());
1791
- if (!ageAvailable()) return Left({
1792
- _tag: "AgeNotFound",
1793
- message: "age CLI not found on PATH"
1794
- });
1795
- return Try(() => execFileSync("age", ["--decrypt", identityPath], {
1796
- stdio: [
1797
- "pipe",
1798
- "pipe",
1799
- "pipe"
1800
- ],
1801
- encoding: "utf-8"
1802
- })).fold((err) => Left({
1803
- _tag: "DecryptFailed",
1804
- message: `age decrypt failed: ${err}`
1805
- }), (output) => Right(output.trim()));
1806
- });
1875
+ _tag: "IdentityUnreadable",
1876
+ path: identityPath,
1877
+ message: err.message
1878
+ }), (content) => parseIdentityKeys(content, identityPath));
1879
+ };
1880
+ new Set(Object.keys({
1881
+ IdentityNotFound: true,
1882
+ IdentityUnreadable: true,
1883
+ NoIdentityKey: true,
1884
+ PassphraseProtectedIdentity: true,
1885
+ UnsupportedIdentity: true
1886
+ }));
1887
+ const UNSUPPORTED_KIND = {
1888
+ plugin: "an age plugin key",
1889
+ ssh: "an SSH key",
1890
+ unknown: "not an age secret key"
1891
+ };
1892
+ /** Explain an identity error. Never includes key material. */
1893
+ const describeIdentityError = (err) => {
1894
+ switch (err._tag) {
1895
+ case "IdentityNotFound": return `Identity file not found: ${err.path}`;
1896
+ case "IdentityUnreadable": return `Can't read key file ${err.path}: ${err.message}`;
1897
+ case "NoIdentityKey": return `${err.source} has no age secret key (AGE-SECRET-KEY-…)`;
1898
+ case "PassphraseProtectedIdentity": return err.source === "ENVPKT_AGE_KEY" ? `${INLINE_KEY_SOURCE} holds a passphrase-protected key, which envpkt can't read. Set it to the plain key file's content (its AGE-SECRET-KEY-… line).` : `${err.source} is a passphrase-protected key file, which envpkt can't read. Unwrap it once with \`age -d -o <plain-key-file> ${err.source}\` and point envpkt at the plain file.`;
1899
+ case "UnsupportedIdentity": return `${err.source} line ${err.line} is ${UNSUPPORTED_KIND[err.kind]}; envpkt reads plain age secret keys (AGE-SECRET-KEY-…) only`;
1900
+ }
1807
1901
  };
1808
1902
  //#endregion
1809
1903
  //#region src/fnox/parse.ts
@@ -1834,54 +1928,32 @@ const resolveInlineKey = () => {
1834
1928
  const key = process.env["ENVPKT_AGE_KEY"];
1835
1929
  return key ? Some(key) : None();
1836
1930
  };
1837
- /** Generate an age keypair and write to disk. Refuses to overwrite if the file already exists. */
1838
- const generateKeypair = (options) => {
1839
- if (!ageAvailable()) return Left({
1840
- _tag: "AgeNotFound",
1841
- message: "age-keygen CLI not found on PATH. Install age: https://github.com/FiloSottile/age"
1931
+ /** The identity file `age-keygen` writes: creation time, public key, secret key. */
1932
+ const identityFileContent = (identity, recipient) => `# created: ${(/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z")}\n# public key: ${recipient}\n${identity}\n`;
1933
+ /** Write the identity file (0600), creating its directory if needed. Never overwrites a file. */
1934
+ const writeIdentityFile = (outputPath, content) => Try(() => {
1935
+ mkdirSync(dirname(outputPath), { recursive: true });
1936
+ writeFileSync(outputPath, content, {
1937
+ mode: 384,
1938
+ flag: "wx"
1842
1939
  });
1940
+ chmodSync(outputPath, 384);
1941
+ }).fold((err) => Left({
1942
+ _tag: "WriteError",
1943
+ message: `Failed to write identity file: ${err}`
1944
+ }), () => Right(void 0));
1945
+ /** Generate an age keypair and write to disk. Refuses to overwrite if the file already exists. */
1946
+ const generateKeypair = async (options) => {
1843
1947
  const outputPath = options?.outputPath ?? resolveKeyPath();
1844
1948
  if (existsSync(outputPath)) return Left({
1845
1949
  _tag: "KeyExists",
1846
1950
  path: outputPath
1847
1951
  });
1848
- return Try(() => execFileSync("age-keygen", [], {
1849
- stdio: [
1850
- "pipe",
1851
- "pipe",
1852
- "pipe"
1853
- ],
1854
- encoding: "utf-8"
1855
- })).fold((err) => Left({
1856
- _tag: "KeygenFailed",
1857
- message: `age-keygen failed: ${err}`
1858
- }), (output) => {
1859
- const recipientLine = output.split("\n").find((l) => l.startsWith("# public key:"));
1860
- if (!recipientLine) return Left({
1861
- _tag: "KeygenFailed",
1862
- message: "Could not parse public key from age-keygen output"
1863
- });
1864
- const recipient = recipientLine.replace("# public key: ", "").trim();
1865
- const dir = dirname(outputPath);
1866
- const mkdirFailed = Try(() => {
1867
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1868
- }).fold((err) => ({
1869
- _tag: "WriteError",
1870
- message: `Failed to create directory ${dir}: ${err}`
1871
- }), () => void 0);
1872
- if (mkdirFailed) return Left(mkdirFailed);
1873
- return Try(() => {
1874
- writeFileSync(outputPath, output, { mode: 384 });
1875
- chmodSync(outputPath, 384);
1876
- }).fold((err) => Left({
1877
- _tag: "WriteError",
1878
- message: `Failed to write identity file: ${err}`
1879
- }), () => Right({
1880
- recipient,
1881
- identityPath: outputPath,
1882
- configUpdated: false
1883
- }));
1884
- });
1952
+ return (await generateAgeKeypair()).flatMap(({ identity, recipient }) => writeIdentityFile(outputPath, identityFileContent(identity, recipient)).map(() => ({
1953
+ recipient,
1954
+ identityPath: outputPath,
1955
+ configUpdated: false
1956
+ })));
1885
1957
  };
1886
1958
  /** Update identity fields (recipient, key_file, name) in an envpkt.toml file, preserving structure */
1887
1959
  const updateConfigIdentity = (configPath, options) => {
@@ -1962,94 +2034,36 @@ const updateConfigIdentity = (configPath, options) => {
1962
2034
  };
1963
2035
  //#endregion
1964
2036
  //#region src/core/seal.ts
1965
- /** Encrypt a plaintext string using age with the given recipient public key (armored output) */
1966
- const ageEncrypt = (plaintext, recipient) => {
1967
- if (!ageAvailable()) return Left({
1968
- _tag: "AgeNotFound",
1969
- message: "age CLI not found on PATH"
1970
- });
1971
- return Try(() => execFileSync("age", [
1972
- "--encrypt",
1973
- "--recipient",
1974
- recipient,
1975
- "--armor"
1976
- ], {
1977
- input: plaintext,
1978
- stdio: [
1979
- "pipe",
1980
- "pipe",
1981
- "pipe"
1982
- ],
1983
- encoding: "utf-8"
1984
- })).fold((err) => Left({
1985
- _tag: "EncryptFailed",
1986
- key: "",
1987
- message: `age encrypt failed: ${err}`
1988
- }), (output) => Right(output.trim()));
1989
- };
1990
- /** Decrypt an age-armored ciphertext using the given identity file */
1991
- const ageDecrypt = (ciphertext, identityPath) => {
1992
- if (!ageAvailable()) return Left({
1993
- _tag: "AgeNotFound",
1994
- message: "age CLI not found on PATH"
1995
- });
1996
- return Try(() => execFileSync("age", [
1997
- "--decrypt",
1998
- "--identity",
1999
- identityPath
2000
- ], {
2001
- input: ciphertext,
2002
- stdio: [
2003
- "pipe",
2004
- "pipe",
2005
- "pipe"
2006
- ],
2007
- encoding: "utf-8"
2008
- })).fold((err) => Left({
2009
- _tag: "DecryptFailed",
2010
- key: "",
2011
- message: `age decrypt failed: ${err}`
2012
- }), (output) => Right(output.trim()));
2013
- };
2037
+ /** Encrypt a plaintext string to an age recipient public key (armored output) */
2038
+ const ageEncrypt = (plaintext, recipient) => encryptArmored(plaintext, recipient);
2039
+ /**
2040
+ * Decrypt an age-armored ciphertext with any of the given secret keys (`AGE-SECRET-KEY-…`, as
2041
+ * returned by `unwrapAgentKey`). The plaintext is returned as-is, untrimmed.
2042
+ */
2043
+ const ageDecrypt = (ciphertext, identities) => decryptArmored(ciphertext, identities);
2014
2044
  /** Seal multiple secrets: encrypt each value with the recipient key and set encrypted_value on meta */
2015
- const sealSecrets = (meta, values, recipient) => {
2016
- if (!ageAvailable()) return Left({
2017
- _tag: "AgeNotFound",
2018
- message: "age CLI not found on PATH"
2019
- });
2020
- return Object.entries(meta).reduce((acc, [key, secretMeta]) => acc.flatMap((result) => {
2045
+ const sealSecrets = async (meta, values, recipient) => {
2046
+ return (await Either.traverseAsync(Object.entries(meta), async ([key, secretMeta]) => {
2021
2047
  const value = values[key];
2022
- if (value === void 0) return Right({
2023
- ...result,
2024
- [key]: secretMeta
2025
- });
2026
- return ageEncrypt(value, recipient).mapLeft((err) => ({
2048
+ if (value === void 0) return Right([key, secretMeta]);
2049
+ return (await ageEncrypt(value, recipient)).mapLeft((err) => ({
2027
2050
  _tag: "EncryptFailed",
2028
2051
  key,
2029
2052
  message: err.message
2030
- })).map((ciphertext) => ({
2031
- ...result,
2032
- [key]: {
2033
- ...secretMeta,
2034
- encrypted_value: ciphertext
2035
- }
2036
- }));
2037
- }), Right({}));
2038
- };
2039
- /** Unseal secrets: decrypt encrypted_value for each meta entry that has one */
2040
- const unsealSecrets = (meta, identityPath) => {
2041
- if (!ageAvailable()) return Left({
2042
- _tag: "AgeNotFound",
2043
- message: "age CLI not found on PATH"
2044
- });
2045
- return Object.entries(meta).filter(([, secretMeta]) => secretMeta.encrypted_value !== void 0 && secretMeta.encrypted_value !== "").reduce((acc, [key, secretMeta]) => acc.flatMap((result) => ageDecrypt(secretMeta.encrypted_value, identityPath).mapLeft((err) => ({
2053
+ })).map((ciphertext) => [key, {
2054
+ ...secretMeta,
2055
+ encrypted_value: ciphertext
2056
+ }]);
2057
+ })).map((entries) => Object.fromEntries(entries));
2058
+ };
2059
+ /** Unseal secrets: decrypt encrypted_value for each meta entry that has one, with any of `identities` */
2060
+ const unsealSecrets = async (meta, identities) => {
2061
+ const sealedEntries = Object.entries(meta).filter(([, secretMeta]) => secretMeta.encrypted_value !== void 0 && secretMeta.encrypted_value !== "");
2062
+ return (await Either.traverseAsync(sealedEntries, async ([key, secretMeta]) => (await ageDecrypt(secretMeta.encrypted_value, identities)).mapLeft((err) => ({
2046
2063
  _tag: "DecryptFailed",
2047
2064
  key,
2048
2065
  message: err.message
2049
- })).map((plaintext) => ({
2050
- ...result,
2051
- [key]: plaintext
2052
- }))), Right({}));
2066
+ })).map((plaintext) => [key, plaintext]))).map((entries) => Object.fromEntries(entries));
2053
2067
  };
2054
2068
  //#endregion
2055
2069
  //#region src/core/boot.ts
@@ -2069,51 +2083,27 @@ const resolveIdentityFilePath = (config, configDir, useDefaultFallback) => {
2069
2083
  const defaultPath = resolveKeyPath();
2070
2084
  return existsSync(defaultPath) ? Option(defaultPath) : Option(void 0);
2071
2085
  };
2072
- const noop = () => {};
2073
- /** Write an inline age key to a private temp file so the `age` CLI (file-based) can use it. */
2074
- const materializeInlineKey = (key) => {
2075
- const dir = mkdtempSync(join(tmpdir(), "envpkt-age-"));
2076
- const keyPath = join(dir, "age-key.txt");
2077
- writeFileSync(keyPath, key.endsWith("\n") ? key : `${key}\n`);
2078
- chmodSync(keyPath, 384);
2079
- return {
2080
- path: keyPath,
2081
- dispose: () => rmSync(dir, {
2082
- recursive: true,
2083
- force: true
2084
- })
2085
- };
2086
- };
2086
+ const keysFrom = (keys) => keys.map((k) => Option(k));
2087
2087
  /**
2088
- * Resolve an age identity for unsealing sealed packets. Precedence:
2088
+ * Resolve the age secret keys for unsealing sealed packets. Precedence:
2089
2089
  * config.identity.key_file > ENVPKT_AGE_KEY_FILE > ENVPKT_AGE_KEY (inline) > ~/.envpkt/age-key.txt
2090
2090
  * The inline key (CI secret) ranks above the homedir default so an explicit
2091
- * env var beats a stray local key. Inline keys are written to a 0600 temp file
2092
- * the caller must dispose().
2091
+ * env var beats a stray local key. It is parsed in memory and never written to disk.
2093
2092
  */
2094
2093
  const resolveSealIdentity = (config, configDir) => {
2095
2094
  if (config.identity?.key_file) {
2096
2095
  const keyFilePath = resolve(configDir, expandPath(config.identity.key_file));
2097
- if (existsSync(keyFilePath)) return Option({
2098
- path: keyFilePath,
2099
- dispose: noop
2100
- });
2096
+ if (existsSync(keyFilePath)) return keysFrom(unwrapAgentKey(keyFilePath));
2101
2097
  }
2102
2098
  const envFile = process.env["ENVPKT_AGE_KEY_FILE"];
2103
- if (envFile && existsSync(envFile)) return Option({
2104
- path: envFile,
2105
- dispose: noop
2106
- });
2099
+ if (envFile && existsSync(envFile)) return keysFrom(unwrapAgentKey(envFile));
2107
2100
  const inlineKey = resolveInlineKey().orUndefined();
2108
- if (inlineKey) return Option(materializeInlineKey(inlineKey));
2101
+ if (inlineKey) return keysFrom(parseIdentityKeys(inlineKey, INLINE_KEY_SOURCE));
2109
2102
  if (!config.identity?.key_file) {
2110
2103
  const defaultPath = join(homedir(), ".envpkt", "age-key.txt");
2111
- if (existsSync(defaultPath)) return Option({
2112
- path: defaultPath,
2113
- dispose: noop
2114
- });
2104
+ if (existsSync(defaultPath)) return keysFrom(unwrapAgentKey(defaultPath));
2115
2105
  }
2116
- return Option(void 0);
2106
+ return Right(Option(void 0));
2117
2107
  };
2118
2108
  /** Describe the seal-identity precedence chain with per-entry status, for a clear no-key error. */
2119
2109
  const describeSealKeySearch = (config, configDir) => {
@@ -2131,7 +2121,7 @@ const describeSealKeySearch = (config, configDir) => {
2131
2121
  ];
2132
2122
  };
2133
2123
  const resolveIdentityKey = (config, configDir) => {
2134
- return resolveIdentityFilePath(config, configDir, false).fold(() => Right(Option(void 0)), (path) => unwrapAgentKey(path).fold((err) => Left(err), (key) => Right(Option(key))));
2124
+ return resolveIdentityFilePath(config, configDir, false).fold(() => Right(Option(void 0)), (path) => unwrapAgentKey(path).fold((err) => Left(err), (keys) => Right(Option(keys[0]))));
2135
2125
  };
2136
2126
  const detectFnoxKeys = (configDir) => detectFnox(configDir).fold(() => /* @__PURE__ */ new Set(), (fnoxPath) => readFnoxConfig(fnoxPath).fold(() => /* @__PURE__ */ new Set(), (fnoxConfig) => extractFnoxKeys(fnoxConfig)));
2137
2127
  const checkExpiration = (audit, failOnExpired, warnOnly) => {
@@ -2144,6 +2134,19 @@ const checkExpiration = (audit, failOnExpired, warnOnly) => {
2144
2134
  if (audit.expired > 0 && warnOnly) warnings.push(`${audit.expired} secret(s) have expired (warn-only mode)`);
2145
2135
  return Right(warnings);
2146
2136
  };
2137
+ /**
2138
+ * Sealed values are declared, so a usable key is required. No key anywhere → SealKeyUnavailable;
2139
+ * a key that can't be used (an unreadable file, or a passphrase-protected, plugin or SSH key) → its
2140
+ * IdentityError. Either way
2141
+ * boot refuses rather than inject empty values — even under warnOnly, which governs soft issues
2142
+ * like expiry, not an absent key.
2143
+ */
2144
+ const requireSealKeys = (config, configDir, entries) => resolveSealIdentity(config, configDir).flatMap((keys) => keys.fold(() => Left({
2145
+ _tag: "SealKeyUnavailable",
2146
+ sealedKeys: Object.keys(entries).filter((k) => !!entries[k]?.encrypted_value),
2147
+ searched: describeSealKeySearch(config, configDir),
2148
+ configuredKeyFile: config.identity?.key_file ? resolve(configDir, expandPath(config.identity.key_file)) : void 0
2149
+ }), (k) => Right(k)));
2147
2150
  const SECRET_PATTERNS = [
2148
2151
  /^sk-/,
2149
2152
  /^ghp_/,
@@ -2162,174 +2165,165 @@ const checkEnvMisclassification = (config) => {
2162
2165
  const envEntries = config.env ?? {};
2163
2166
  return Object.entries(envEntries).filter(([, entry]) => entry.value !== void 0 && looksLikeSecret(entry.value)).map(([key]) => `[env.${key}] value looks like a secret — consider moving to [secret.${key}]`);
2164
2167
  };
2165
- /** Programmatic boot — returns Either<BootError, BootResult> */
2166
- const bootSafe = (options) => {
2168
+ /** Programmatic boot — resolves to Either<BootError, BootResult> */
2169
+ const bootSafe = async (options) => {
2167
2170
  const opts = options ?? {};
2168
2171
  const inject = opts.inject !== false;
2169
2172
  const failOnExpired = opts.failOnExpired !== false;
2170
2173
  const warnOnly = opts.warnOnly ?? false;
2171
2174
  const log = (opts.logger ?? directSilentLogger$1).withContext({ component: "envpkt.boot" });
2172
- return resolveAndLoad(opts).flatMap(({ config, configPath, configDir, configSource }) => validateAliases(config).fold((err) => {
2175
+ const loaded = resolveAndLoad(opts);
2176
+ if (loaded.isLeft()) return Left(loaded.value);
2177
+ const { config, configPath, configDir, configSource } = loaded.value;
2178
+ const aliases = validateAliases(config);
2179
+ if (aliases.isLeft()) {
2180
+ const err = aliases.value;
2173
2181
  log.warn("alias.validate.failed", {
2174
2182
  tag: err._tag,
2175
2183
  key: "key" in err ? err.key : void 0
2176
2184
  });
2177
2185
  return Left(err);
2178
- }, (aliasTable) => {
2179
- log.debug("alias.validate.success", { aliases: aliasTable.entries.size });
2180
- const secretEntries = config.secret ?? {};
2181
- const envEntries = config.env ?? {};
2182
- const namer = makeEnvNamer(config);
2183
- const secretEnv = (key) => namer(key, secretEntries[key]?.namespace);
2184
- const envEnv = (key) => namer(key, envEntries[key]?.namespace);
2185
- const envNames = {
2186
- ...Object.fromEntries(Object.keys(envEntries).map((k) => [k, envEnv(k)])),
2187
- ...Object.fromEntries(Object.keys(secretEntries).map((k) => [k, secretEnv(k)]))
2188
- };
2189
- const nonAliasSecretEntries = Object.fromEntries(Object.entries(secretEntries).filter(([, meta]) => meta.from_key === void 0));
2190
- const aliasSecretKeys = Object.entries(secretEntries).filter(([, meta]) => meta.from_key !== void 0).map(([k]) => k);
2191
- const nonAliasEnvEntries = Object.entries(envEntries).filter(([, meta]) => meta.from_key === void 0);
2192
- const aliasEnvKeys = Object.entries(envEntries).filter(([, meta]) => meta.from_key !== void 0).map(([k]) => k);
2193
- const nonAliasMetaKeys = Object.keys(nonAliasSecretEntries);
2194
- const hasSealedValues = Object.values(nonAliasSecretEntries).some((meta) => !!meta.encrypted_value);
2195
- const identityKeyResult = resolveIdentityKey(config, configDir);
2196
- const identityKey = identityKeyResult.fold(() => Option(void 0), (k) => k);
2197
- if (identityKeyResult.isLeft() && !hasSealedValues) return identityKeyResult.fold((err) => Left(err), () => Left({
2198
- _tag: "ReadError",
2199
- message: "unexpected"
2200
- }));
2201
- const fnoxKeys = detectFnoxKeys(configDir);
2202
- const audit = computeAudit(config, fnoxKeys, void 0, aliasTable);
2203
- const sealIdentity = hasSealedValues ? resolveSealIdentity(config, configDir) : Option(void 0);
2204
- if (hasSealedValues && sealIdentity.isEmpty) return Left({
2205
- _tag: "SealKeyUnavailable",
2206
- sealedKeys: nonAliasMetaKeys.filter((k) => !!nonAliasSecretEntries[k]?.encrypted_value),
2207
- searched: describeSealKeySearch(config, configDir),
2208
- configuredKeyFile: config.identity?.key_file ? resolve(configDir, expandPath(config.identity.key_file)) : void 0
2186
+ }
2187
+ const aliasTable = aliases.value;
2188
+ log.debug("alias.validate.success", { aliases: aliasTable.entries.size });
2189
+ const secretEntries = config.secret ?? {};
2190
+ const envEntries = config.env ?? {};
2191
+ const namer = makeEnvNamer(config);
2192
+ const secretEnv = (key) => namer(key, secretEntries[key]?.namespace);
2193
+ const envEnv = (key) => namer(key, envEntries[key]?.namespace);
2194
+ const envNames = {
2195
+ ...Object.fromEntries(Object.keys(envEntries).map((k) => [k, envEnv(k)])),
2196
+ ...Object.fromEntries(Object.keys(secretEntries).map((k) => [k, secretEnv(k)]))
2197
+ };
2198
+ const nonAliasSecretEntries = Object.fromEntries(Object.entries(secretEntries).filter(([, meta]) => meta.from_key === void 0));
2199
+ const aliasSecretKeys = Object.entries(secretEntries).filter(([, meta]) => meta.from_key !== void 0).map(([k]) => k);
2200
+ const nonAliasEnvEntries = Object.entries(envEntries).filter(([, meta]) => meta.from_key === void 0);
2201
+ const aliasEnvKeys = Object.entries(envEntries).filter(([, meta]) => meta.from_key !== void 0).map(([k]) => k);
2202
+ const nonAliasMetaKeys = Object.keys(nonAliasSecretEntries);
2203
+ const hasSealedValues = Object.values(nonAliasSecretEntries).some((meta) => !!meta.encrypted_value);
2204
+ const identityKeyResult = resolveIdentityKey(config, configDir);
2205
+ const identityKey = identityKeyResult.fold(() => Option(void 0), (k) => k);
2206
+ if (identityKeyResult.isLeft() && !hasSealedValues) return Left(identityKeyResult.value);
2207
+ const fnoxKeys = detectFnoxKeys(configDir);
2208
+ const audit = computeAudit(config, fnoxKeys, void 0, aliasTable);
2209
+ const sealKeys = hasSealedValues ? requireSealKeys(config, configDir, nonAliasSecretEntries) : Right([]);
2210
+ if (sealKeys.isLeft()) return Left(sealKeys.value);
2211
+ const unsealKeys = sealKeys.value;
2212
+ const expiration = checkExpiration(audit, failOnExpired, warnOnly);
2213
+ if (expiration.isLeft()) return Left(expiration.value);
2214
+ const warnings = expiration.value;
2215
+ const secrets = {};
2216
+ const injected = [];
2217
+ const skipped = [];
2218
+ warnings.push(...checkEnvMisclassification(config));
2219
+ const nsSeparator = config.namespace?.separator;
2220
+ if (nsSeparator !== void 0 && !isShellSafeSeparator(nsSeparator)) warnings.push(`[namespace] separator "${nsSeparator}" is not shell-safe — injected names won't be usable via shell $VAR/export. Use '_' or '__'.`);
2221
+ const envDefaults = Object.fromEntries(nonAliasEnvEntries.flatMap(([key, entry]) => Option(process.env[envEnv(key)]).fold(() => Option(entry.value).fold(() => [], (v) => [[key, v]]), () => [])));
2222
+ const overridden = nonAliasEnvEntries.flatMap(([key]) => Option(process.env[envEnv(key)]).fold(() => [], () => [key]));
2223
+ if (inject) Object.entries(envDefaults).forEach(([key, value]) => {
2224
+ process.env[envEnv(key)] = value;
2225
+ });
2226
+ const sealedKeys = /* @__PURE__ */ new Set();
2227
+ if (hasSealedValues) (await unsealSecrets(nonAliasSecretEntries, unsealKeys)).fold((err) => {
2228
+ log.warn("phase.sealed.decrypt_failed", { message: err.message });
2229
+ warnings.push(`Sealed value decryption failed: ${err.message}`);
2230
+ }, (unsealed) => {
2231
+ const unsealedEntries = Object.entries(unsealed);
2232
+ Object.assign(secrets, unsealed);
2233
+ injected.push(...unsealedEntries.map(([key]) => key));
2234
+ unsealedEntries.forEach(([key]) => {
2235
+ sealedKeys.add(key);
2236
+ log.debug("phase.sealed.resolved", { key });
2209
2237
  });
2210
- return checkExpiration(audit, failOnExpired, warnOnly).map((warnings) => {
2211
- const secrets = {};
2212
- const injected = [];
2213
- const skipped = [];
2214
- warnings.push(...checkEnvMisclassification(config));
2215
- const nsSeparator = config.namespace?.separator;
2216
- if (nsSeparator !== void 0 && !isShellSafeSeparator(nsSeparator)) warnings.push(`[namespace] separator "${nsSeparator}" is not shell-safe — injected names won't be usable via shell $VAR/export. Use '_' or '__'.`);
2217
- const envDefaults = Object.fromEntries(nonAliasEnvEntries.flatMap(([key, entry]) => Option(process.env[envEnv(key)]).fold(() => Option(entry.value).fold(() => [], (v) => [[key, v]]), () => [])));
2218
- const overridden = nonAliasEnvEntries.flatMap(([key]) => Option(process.env[envEnv(key)]).fold(() => [], () => [key]));
2219
- if (inject) Object.entries(envDefaults).forEach(([key, value]) => {
2220
- process.env[envEnv(key)] = value;
2221
- });
2222
- const sealedKeys = /* @__PURE__ */ new Set();
2223
- if (hasSealedValues) sealIdentity.fold(() => {
2224
- log.warn("phase.sealed.no_identity_file", { sealed_keys: nonAliasMetaKeys.filter((k) => !!nonAliasSecretEntries[k]?.encrypted_value).length });
2225
- }, ({ path: idPath, dispose }) => {
2226
- try {
2227
- unsealSecrets(nonAliasSecretEntries, idPath).fold((err) => {
2228
- log.warn("phase.sealed.decrypt_failed", { message: err.message });
2229
- warnings.push(`Sealed value decryption failed: ${err.message}`);
2230
- }, (unsealed) => {
2231
- const unsealedEntries = Object.entries(unsealed);
2232
- Object.assign(secrets, unsealed);
2233
- injected.push(...unsealedEntries.map(([key]) => key));
2234
- unsealedEntries.forEach(([key]) => {
2235
- sealedKeys.add(key);
2236
- log.debug("phase.sealed.resolved", { key });
2237
- });
2238
- });
2239
- } finally {
2240
- dispose();
2241
- }
2238
+ });
2239
+ const remainingKeys = nonAliasMetaKeys.filter((k) => !sealedKeys.has(k));
2240
+ if (remainingKeys.length > 0) {
2241
+ if (fnoxAvailable()) fnoxExport(opts.profile, identityKey.orUndefined()).fold((err) => {
2242
+ log.warn("phase.fnox.export_failed", {
2243
+ message: err.message,
2244
+ skipped: remainingKeys.length
2242
2245
  });
2243
- const remainingKeys = nonAliasMetaKeys.filter((k) => !sealedKeys.has(k));
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
2249
- });
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
- });
2267
- });
2268
- injected.push(...found);
2269
- skipped.push(...notFound);
2246
+ warnings.push(`fnox export failed: ${err.message}`);
2247
+ skipped.push(...remainingKeys);
2248
+ }, (exported) => {
2249
+ const found = remainingKeys.filter((key) => key in exported);
2250
+ const notFound = remainingKeys.filter((key) => !(key in exported));
2251
+ found.forEach((key) => {
2252
+ secrets[key] = exported[key];
2253
+ log.debug("phase.fnox.resolved", {
2254
+ key,
2255
+ profile: opts.profile
2270
2256
  });
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
- }
2277
- }
2278
- aliasSecretKeys.forEach((aliasKey) => {
2279
- const entry = aliasTable.entries.get(`secret.${aliasKey}`);
2280
- if (!entry) return;
2281
- Option(secrets[entry.targetKey]).fold(() => {
2282
- skipped.push(aliasKey);
2283
- log.debug("phase.alias.target_unresolved", {
2284
- alias: aliasKey,
2285
- target: entry.targetKey
2286
- });
2287
- }, (targetValue) => {
2288
- secrets[aliasKey] = targetValue;
2289
- injected.push(aliasKey);
2290
- log.debug("phase.alias.copied", {
2291
- alias: aliasKey,
2292
- target: entry.targetKey
2293
- });
2257
+ });
2258
+ notFound.forEach((key) => {
2259
+ log.debug("phase.fnox.not_in_export", {
2260
+ key,
2261
+ profile: opts.profile
2294
2262
  });
2295
2263
  });
2296
- aliasEnvKeys.forEach((aliasKey) => {
2297
- const entry = aliasTable.entries.get(`env.${aliasKey}`);
2298
- if (!entry) return;
2299
- if (process.env[envEnv(aliasKey)] !== void 0) {
2300
- overridden.push(aliasKey);
2301
- return;
2302
- }
2303
- const targetEntry = envEntries[entry.targetKey];
2304
- if (targetEntry?.value === void 0) return;
2305
- const resolvedTarget = process.env[envEnv(entry.targetKey)] ?? targetEntry.value;
2306
- envDefaults[aliasKey] = resolvedTarget;
2264
+ injected.push(...found);
2265
+ skipped.push(...notFound);
2266
+ });
2267
+ else {
2268
+ log.debug("phase.fnox.unavailable", { skipped: remainingKeys.length });
2269
+ if (!hasSealedValues) warnings.push("fnox not available — no secrets injected");
2270
+ else warnings.push("fnox not available — unsealed secrets could not be resolved");
2271
+ skipped.push(...remainingKeys);
2272
+ }
2273
+ }
2274
+ aliasSecretKeys.forEach((aliasKey) => {
2275
+ const entry = aliasTable.entries.get(`secret.${aliasKey}`);
2276
+ if (!entry) return;
2277
+ Option(secrets[entry.targetKey]).fold(() => {
2278
+ skipped.push(aliasKey);
2279
+ log.debug("phase.alias.target_unresolved", {
2280
+ alias: aliasKey,
2281
+ target: entry.targetKey
2282
+ });
2283
+ }, (targetValue) => {
2284
+ secrets[aliasKey] = targetValue;
2285
+ injected.push(aliasKey);
2286
+ log.debug("phase.alias.copied", {
2287
+ alias: aliasKey,
2288
+ target: entry.targetKey
2307
2289
  });
2308
- if (inject) {
2309
- Object.entries(envDefaults).forEach(([key, value]) => {
2310
- process.env[envEnv(key)] ??= value;
2311
- });
2312
- Object.entries(secrets).forEach(([key, value]) => {
2313
- process.env[secretEnv(key)] = value;
2314
- });
2315
- }
2316
- return {
2317
- audit,
2318
- injected,
2319
- skipped,
2320
- secrets,
2321
- warnings,
2322
- envDefaults,
2323
- overridden,
2324
- envNames,
2325
- configPath,
2326
- configSource
2327
- };
2328
2290
  });
2329
- }));
2291
+ });
2292
+ aliasEnvKeys.forEach((aliasKey) => {
2293
+ const entry = aliasTable.entries.get(`env.${aliasKey}`);
2294
+ if (!entry) return;
2295
+ if (process.env[envEnv(aliasKey)] !== void 0) {
2296
+ overridden.push(aliasKey);
2297
+ return;
2298
+ }
2299
+ const targetEntry = envEntries[entry.targetKey];
2300
+ if (targetEntry?.value === void 0) return;
2301
+ const resolvedTarget = process.env[envEnv(entry.targetKey)] ?? targetEntry.value;
2302
+ envDefaults[aliasKey] = resolvedTarget;
2303
+ });
2304
+ if (inject) {
2305
+ Object.entries(envDefaults).forEach(([key, value]) => {
2306
+ process.env[envEnv(key)] ??= value;
2307
+ });
2308
+ Object.entries(secrets).forEach(([key, value]) => {
2309
+ process.env[secretEnv(key)] = value;
2310
+ });
2311
+ }
2312
+ return Right({
2313
+ audit,
2314
+ injected,
2315
+ skipped,
2316
+ secrets,
2317
+ warnings,
2318
+ envDefaults,
2319
+ overridden,
2320
+ envNames,
2321
+ configPath,
2322
+ configSource
2323
+ });
2330
2324
  };
2331
- /** Programmatic boot — throws EnvpktBootError on failure (intentional throwing wrapper over bootSafe) */
2332
- const boot = (options) => bootSafe(options).fold((err) => {
2325
+ /** Programmatic boot — rejects with EnvpktBootError on failure (intentional throwing wrapper over bootSafe) */
2326
+ const boot = async (options) => (await bootSafe(options)).fold((err) => {
2333
2327
  throw new EnvpktBootError(err);
2334
2328
  }, (r) => r);
2335
2329
  /** Error class for boot() failures */
@@ -2355,9 +2349,11 @@ const formatBootError = (error) => {
2355
2349
  case "CatalogLoadError": return `Catalog load error: ${error.message}`;
2356
2350
  case "SecretNotInCatalog": return `Secret "${error.key}" not found in catalog: ${error.catalogPath}`;
2357
2351
  case "MissingSecretsList": return `Missing secrets list: ${error.message}`;
2358
- case "AgeNotFound": return `age not found: ${error.message}`;
2359
- case "DecryptFailed": return `Decrypt failed: ${error.message}`;
2360
- case "IdentityNotFound": return `Identity file not found: ${error.path}`;
2352
+ case "IdentityNotFound":
2353
+ case "IdentityUnreadable":
2354
+ case "NoIdentityKey":
2355
+ case "PassphraseProtectedIdentity":
2356
+ case "UnsupportedIdentity": return describeIdentityError(error);
2361
2357
  case "SealKeyUnavailable": {
2362
2358
  const keys = error.sealedKeys.length;
2363
2359
  const searched = error.searched.map((line) => ` • ${line}`).join("\n");
@@ -2383,6 +2379,8 @@ const formatBootError = (error) => {
2383
2379
  };
2384
2380
  //#endregion
2385
2381
  //#region src/core/resolve-values.ts
2382
+ /** Strip one trailing newline (`\n` or `\r\n`): a shell-set env var often carries one by mistake. */
2383
+ const stripTrailingNewline = (value) => value.replace(/\r?\n$/, "");
2386
2384
  /** Resolve plaintext values for the given keys via cascade: fnox → env → interactive prompt */
2387
2385
  const resolveValues = async (keys, profile, agentKey) => {
2388
2386
  const result = {};
@@ -2396,8 +2394,8 @@ const resolveValues = async (keys, profile, agentKey) => {
2396
2394
  });
2397
2395
  });
2398
2396
  [...remaining].forEach((key) => {
2399
- const envValue = process.env[key];
2400
- if (envValue !== void 0 && envValue !== "") {
2397
+ const envValue = stripTrailingNewline(process.env[key] ?? "");
2398
+ if (envValue.trim() !== "") {
2401
2399
  result[key] = envValue;
2402
2400
  remaining.delete(key);
2403
2401
  }
@@ -2412,7 +2410,7 @@ const resolveValues = async (keys, profile, agentKey) => {
2412
2410
  });
2413
2411
  for (const key of remaining) {
2414
2412
  const value = await prompt(`Enter value for ${key}: `);
2415
- if (value !== "") result[key] = value;
2413
+ if (value.trim() !== "") result[key] = value;
2416
2414
  }
2417
2415
  rl.close();
2418
2416
  }
@@ -3096,4 +3094,4 @@ const startServer = async () => {
3096
3094
  await server.connect(transport);
3097
3095
  };
3098
3096
  //#endregion
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 };
3097
+ export { AgentIdentitySchema, CallbackConfigSchema, ConsumerType, EnvMetaSchema, EnvpktBootError, EnvpktConfigSchema, IdentitySchema, LifecycleConfigSchema, McpHeaderSchema, SecretMetaSchema, ToolsConfigSchema, ageDecrypt, ageEncrypt, appendSection, boot, bootSafe, buildMcpHeaders, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, copyableSecretMeta, createDirectConsoleLogger, createDirectTestLogger, createServer, deriveServiceFromName, describeIdentityError, 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 };