4.2 KiB
commands/keys/keys-io
Purpose
Private I/O and orchestration-layer helpers shared by every public keys
command. Extracted from keys.ts to keep each file under the 300-line
hard limit while still concentrating filesystem access, vault decryption,
signal checking, and logger defaulting in a single auditable module.
Every public failure surfaces as a ServerKeysError with the matching
SERVER_KEYS_* code: FILE_NOT_FOUND, VAULT_DECRYPT_FAILED,
CHAIN_VERIFY_FAILED, or ABORTED.
All filesystem I/O goes through node:fs/promises directly, NOT
through @suaveplan/storage-file. The storage-file driver appends
.data / .meta extensions to every key on write, which would force
operator scripts and downstream consumers to address artefacts by
root.vault.data instead of the conventional root.vault. The CLI
contract is byte-name-stable across upstream and suaveplan, so the
helpers stay on plain Node fs.
Features
pickLogger(options)— silent default; honours an injectedKeysLogger.checkSignal(signal, command)— cooperative abort check that throwsServerKeysError(SERVER_KEYS_ABORTED).writeVaultPair(dir, vaultName, publicName, vaultBytes, publicBytes)— single transaction that persists both vault and public-key file via storage-file.loadFile(path)— bytes loader with FILE_NOT_FOUND translation.decryptVault(bytes, password, path)— VAULT_DECRYPT_FAILED on any failure, preserving thecausechain.loadRootKeysResult(rootVaultPath, password)— reconstructs theRootKeysResultshape the crypto-vault orchestration expects, by re-reading the sibling.pubfile.extractSignPublicKey(bytes, sourcePath)— pulls thesignslot's public key out of a public-keychain blob; CHAIN_VERIFY_FAILED on missing slot.
Usage: Basic
import {
loadFile,
decryptVault,
writeVaultPair,
pickLogger,
} from "@suaveplan/server-keys/commands/keys/keys-io";
const bytes = await loadFile("./keys/root.vault");
const keychain = await decryptVault(bytes, password, "./keys/root.vault");
await writeVaultPair(
"./keys",
"server.vault",
"server.pub",
serverVaultBytes,
serverPubBytes,
);
pickLogger(undefined).info("done");
Usage: Advanced
Compose with checkSignal for an abort-aware command body, and use
loadRootKeysResult to feed the crypto-vault createServerKeys
orchestration:
import { createServerKeys } from "@suaveplan/crypto-vault";
import {
checkSignal,
loadRootKeysResult,
writeVaultPair,
} from "@suaveplan/server-keys/commands/keys/keys-io";
export async function deriveServer(
rootVaultPath: string,
rootPassword: string,
serverPassword: string,
signal: AbortSignal | undefined,
) {
checkSignal(signal, "derive-server");
const rootResult = await loadRootKeysResult(rootVaultPath, rootPassword);
checkSignal(signal, "derive-server");
const server = await createServerKeys(rootResult, serverPassword);
return writeVaultPair(
"./keys",
"server.vault",
"server.pub",
server.vaultBytes,
server.publicKeyBytes,
);
}
API Reference
pickLogger(options) => KeysLoggercheckSignal(signal, command) => void(throws on abort)writeVaultPair(dir, vaultName, publicName, vaultBytes, publicBytes) => Promise<{ vaultPath, publicPath }>loadFile(path) => Promise<Uint8Array>decryptVault(bytes, password, path) => Promise<Keychain>loadRootKeysResult(path, password) => Promise<RootKeysResult>extractSignPublicKey(bytes, sourcePath) => Promise<Uint8Array>
Implementation Notes
storageFor constructs a fresh NodeFsStorage per call instead of
caching: each call's rootDir is independent (the keys CLI talks to
arbitrary user paths), so a cache would conflate unrelated paths and
introduce subtle leakage between commands. loadRootKeysResult derives
the sibling .pub path with a .vault → .pub substitution because the
upstream layout convention is mirrored verbatim — any deviation here
breaks create-server-keys against an existing create-root-keys
output. The error translations preserve cause so the SuaveplanError
chain stays walkable in operator log forensics.