envpkt 0.17.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/README.md +32 -24
- package/dist/cli.js +460 -470
- package/dist/index.d.ts +37 -30
- package/dist/index.js +337 -341
- package/package.json +6 -2
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,
|
|
8
|
-
import { homedir
|
|
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
|
-
/**
|
|
1771
|
-
const
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1854
|
+
/** The `source` name for the inline key in identity errors. */
|
|
1855
|
+
const INLINE_KEY_SOURCE = "ENVPKT_AGE_KEY";
|
|
1856
|
+
/**
|
|
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
|
+
}));
|
|
1775
1864
|
/**
|
|
1776
|
-
*
|
|
1777
|
-
*
|
|
1778
|
-
*
|
|
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: "
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
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
|
-
/**
|
|
1838
|
-
const
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
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
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
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
|
|
1966
|
-
const ageEncrypt = (plaintext, recipient) =>
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
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. The plaintext is returned as-is, untrimmed. */
|
|
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));
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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), (
|
|
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 —
|
|
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
|
-
|
|
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
|
-
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
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
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
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
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
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
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
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
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
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 —
|
|
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 "
|
|
2359
|
-
case "
|
|
2360
|
-
case "
|
|
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");
|
|
@@ -3098,4 +3094,4 @@ const startServer = async () => {
|
|
|
3098
3094
|
await server.connect(transport);
|
|
3099
3095
|
};
|
|
3100
3096
|
//#endregion
|
|
3101
|
-
export { AgentIdentitySchema, CallbackConfigSchema, ConsumerType, EnvMetaSchema, EnvpktBootError, EnvpktConfigSchema, IdentitySchema, LifecycleConfigSchema, McpHeaderSchema, SecretMetaSchema, ToolsConfigSchema,
|
|
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 };
|