Files
skills/docs/security-stack/security/server-keys/src/commands/keys/keys-io.md
2026-05-29 08:20:56 +01:00

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 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

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) => KeysLogger
  • checkSignal(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.