# `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 injected `KeysLogger`. - `checkSignal(signal, command)` — cooperative abort check that throws `ServerKeysError(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 the `cause` chain. - `loadRootKeysResult(rootVaultPath, password)` — reconstructs the `RootKeysResult` shape the crypto-vault orchestration expects, by re-reading the sibling `.pub` file. - `extractSignPublicKey(bytes, sourcePath)` — pulls the `sign` slot's public key out of a public-keychain blob; CHAIN_VERIFY_FAILED on missing slot. ## Usage: Basic ```ts 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: ```ts 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) => KeysLogger` - `checkSignal(signal, command) => void` (throws on abort) - `writeVaultPair(dir, vaultName, publicName, vaultBytes, publicBytes) => Promise<{ vaultPath, publicPath }>` - `loadFile(path) => Promise` - `decryptVault(bytes, password, path) => Promise` - `loadRootKeysResult(path, password) => Promise` - `extractSignPublicKey(bytes, sourcePath) => Promise` ## 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.