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

4.2 KiB

commands/keys/keys

(no submodules)

Purpose

Non-chain key-lifecycle commands for @suaveplan/server-keys. These functions are the library surface every key/view bin in the package calls into: root keychain creation, server keychain derivation, server keychain rotation, and the four read-only inspectors (view-keys, view-vault, view-public-key-file, view-chain-public). The sibling keys-chain.ts covers the hierarchical chain variants and verify-key-chain; keys-io.ts holds the shared filesystem and decryption helpers.

Every command writes through @suaveplan/storage-file's NodeFsStorage, so vault and public-key persistence flows through the same gated I/O as the rest of the storage stack — no direct fs.writeFile calls live in this module. The crypto-vault orchestration (createRootKeys, createServerKeys, rollServerKeys, openVault, viewPublicKeys) is the format owner.

Features

  • Filename convention mirroring upstream: root.vault/root.pub, server.vault/server.pub, server.v<N>.vault/server.v<N>.pub.
  • Structured ServerKeysError(SERVER_KEYS_*) failures everywhere — callers always see a typed code on error.code.
  • Injectable KeysLogger (defaults to silent) — bins wire a console shim; tests can capture every printed line as a string array.
  • Cooperative abort via options.signal; each command checks before expensive crypto or filesystem work.

Usage: Basic

import {
    createRootKeysCommand,
    createServerKeysCommand,
} from "@suaveplan/server-keys/commands/keys/keys";

await createRootKeysCommand("hunter2hunter2", { output: "/tmp/keys" });
await createServerKeysCommand(
    "/tmp/keys/root.vault",
    "hunter2hunter2",
    "newserverpw",
    { output: "/tmp/keys" },
);

Usage: Advanced

Drive the viewers from a higher-level orchestrator with a capturing logger:

import {
    viewKeysCommand,
    viewVaultCommand,
    viewPublicKeyFileCommand,
} from "@suaveplan/server-keys/commands/keys/keys";

const lines: string[] = [];
const logger = {
    info: (m: string) => lines.push(`I:${m}`),
    error: (m: string) => lines.push(`E:${m}`),
};

const stat = await viewVaultCommand("/tmp/keys/root.vault", { logger });
const keychain = await viewKeysCommand(
    "/tmp/keys/root.vault",
    "hunter2hunter2",
    { logger },
);
const pubChain = await viewPublicKeyFileCommand("/tmp/keys/root.pub", {
    logger,
});
expect(stat.bytes).toBeGreaterThan(0);
expect(Object.keys(pubChain).length).toBeGreaterThan(0);

// Rotate with an abort guard:
const ac = new AbortController();
setTimeout(() => ac.abort(), 100);
await rollServerKeysCommand(
    "/tmp/keys/root.vault",
    "hunter2hunter2",
    "newserverpw",
    1,
    { output: "/tmp/keys", signal: ac.signal },
);

API Reference

  • createRootKeysCommand(password, options?) => Promise<void>
  • createServerKeysCommand(rootVaultPath, rootPassword, serverPassword, options?) => Promise<void>
  • rollServerKeysCommand(rootVaultPath, rootPassword, serverPassword, currentVersion, options?) => Promise<void>
  • viewKeysCommand(vaultPath, password, options?) => Promise<Keychain>
  • viewVaultCommand(vaultPath, options?) => Promise<{ path, bytes }>
  • viewPublicKeyFileCommand(publicKeyPath, options?) => Promise<PublicKeychain>
  • viewChainPublicCommand(publicKeyPath, signKeyPath?, options?) => Promise<PublicKeychain>

All options are KeyCommandOptions = { output?, logger?, signal? }.

Implementation Notes

viewKeysCommand returns the decoded keychain instead of void so test harnesses can assert on the result without re-decrypting; the information is already in memory by the time the function logs, and exposing it avoids a redundant decrypt in callers. Slot iteration goes through Object.entries(keychain as Record<string, unknown>) because Keychain is Readonly<Record<PurposeTag, KeySlotPrivate>> where PurposeTag is a branded string & { __brand } — TS cannot iterate branded keys directly, so a typed-string view is the correct narrowing. The roll-server output uses server.v<N> (dot-separated) instead of upstream's server-v<N> to keep version segments visually distinct in operator listings.