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