first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,195 @@
# @suaveplan/server-keys - Operator-facing CLI bundle for the Suaveplan server-key lifecycle: 18 bins covering root + server keychain generation (including hierarchical ChainedKeySigner chains), encrypted vault persistence via @suaveplan/crypto-vault, key rotation, vault/key viewing, signed file packet preparation/serving/validation via @suaveplan/packer + @suaveplan/storage-file, and license issuance via @suaveplan/license. suaveplan-enterprise additions: SuaveplanError-typed SERVER_KEYS_* codes, OTel command counter + duration histogram, Zod boundary validation, injectable AbortSignal, LoggerLike injection, 100% coverage.
Operator-facing CLI bundle for the Suaveplan server-key lifecycle:
18 bins covering root + server keychain generation (including
hierarchical `ChainedKeySigner` chains via `@suaveplan/crypto-keys`),
encrypted vault persistence via `@suaveplan/crypto-vault`, key rotation,
vault / public-key / chain inspection, signed file packet
preparation / serving / validation via `@suaveplan/packer`, and license
issuance via `@suaveplan/license`. suaveplan-enterprise additions:
`SuaveplanError`-typed `SERVER_KEYS_*` codes, OTel command counter +
duration histogram, Zod-style input validation, `AbortSignal`
threading, injectable `LoggerLike`.
<!-- begin:auto-docs -->
## Documentation tree
- [create-root-keys-chain](./src/bin/create-root-keys-chain.md) — Commander entry point for the `create-root-keys-chain` operator command. Parses CLI flags, prompts for any required pas…
- [create-root-keys](./src/bin/create-root-keys.md) — Commander entry point for the `create-root-keys` operator command. Parses CLI flags, prompts for any required passwords…
- [create-server-keys-chain](./src/bin/create-server-keys-chain.md) — Commander entry point for the `create-server-keys-chain` operator command. Parses CLI flags, prompts for any required p…
- [create-server-keys](./src/bin/create-server-keys.md) — Commander entry point for the `create-server-keys` operator command. Parses CLI flags, prompts for any required passwor…
- [get-file-stats](./src/bin/get-file-stats.md) — Commander entry point for the `get-file-stats` operator command. Parses CLI flags, prompts for any required passwords v…
- [index](./src/bin/index.md) — Commander entry point for the `index` operator command. Parses CLI flags, prompts for any required passwords via `@suav…
- [license](./src/bin/license.md) — Commander entry point for the `license` operator command. Parses CLI flags, prompts for any required passwords via `@su…
- [prepare-file](./src/bin/prepare-file.md) — Commander entry point for the `prepare-file` operator command. Parses CLI flags, prompts for any required passwords via…
- [roll-server-keys-chain](./src/bin/roll-server-keys-chain.md) — Commander entry point for the `roll-server-keys-chain` operator command. Parses CLI flags, prompts for any required pas…
- [roll-server-keys](./src/bin/roll-server-keys.md) — Commander entry point for the `roll-server-keys` operator command. Parses CLI flags, prompts for any required passwords…
- [serve-file](./src/bin/serve-file.md) — Commander entry point for the `serve-file` operator command. Parses CLI flags, prompts for any required passwords via `…
- [validate-file](./src/bin/validate-file.md) — Commander entry point for the `validate-file` operator command. Parses CLI flags, prompts for any required passwords vi…
- [validate-packet](./src/bin/validate-packet.md) — Commander entry point for the `validate-packet` operator command. Parses CLI flags, prompts for any required passwords…
- [verify-key-chain](./src/bin/verify-key-chain.md) — Commander entry point for the `verify-key-chain` operator command. Parses CLI flags, prompts for any required passwords…
- [view-chain-public](./src/bin/view-chain-public.md) — Commander entry point for the `view-chain-public` operator command. Parses CLI flags, prompts for any required password…
- [view-keys](./src/bin/view-keys.md) — Commander entry point for the `view-keys` operator command. Parses CLI flags, prompts for any required passwords via `@…
- [view-public-key-file](./src/bin/view-public-key-file.md) — Commander entry point for the `view-public-key-file` operator command. Parses CLI flags, prompts for any required passw…
- [view-vault](./src/bin/view-vault.md) — Commander entry point for the `view-vault` operator command. Parses CLI flags, prompts for any required passwords via `…
- [files](./src/commands/files/files.md) — Local-packer file-transport commands: `prepareFileCommand`, `validateFileCommand`, `getFileStatsCommand`, `validatePack…
- [files](./src/commands/files/files.md) — Local-packer file-transport commands: `prepareFileCommand`, `validateFileCommand`, `getFileStatsCommand`, `validatePack…
- [packet-io](./src/commands/files/packet-io.md) — Shared on-disk format helpers for the file-transport commands. Holds the canonical packet envelope `{ data, signature,…
- [serve-file](./src/commands/files/serve-file.md) — Reconstructs the original file from a directory of prepared packets by verifying each packet's ed25519 signature + sha2…
- [keys](./src/commands/keys/keys.md) — Non-chain key-lifecycle commands for `@suaveplan/server-keys`. These functions are the library surface every key/view b…
- [keys-chain](./src/commands/keys/keys-chain.md) — Hierarchical `ChainedKeySigner`-backed key-lifecycle commands. Each chain command wraps the matching crypto-vault orche…
- [keys-io](./src/commands/keys/keys-io.md) — Private I/O and orchestration-layer helpers shared by every public keys command. Extracted from `keys.ts` to keep each…
- [keys](./src/commands/keys/keys.md) — Non-chain key-lifecycle commands for `@suaveplan/server-keys`. These functions are the library surface every key/view b…
- [license](./src/commands/license/license.md) — Wraps `@suaveplan/license` `LicenseManager` into the five sub-commands the operator-facing `license` bin exposes: `keyg…
- [hex](./src/commands/license/hex.md) — Tiny shared hex encode/decode helpers used by the license sub-commands to serialise and deserialise the secp256k1 key m…
- [license](./src/commands/license/license.md) — Wraps `@suaveplan/license` `LicenseManager` into the five sub-commands the operator-facing `license` bin exposes: `keyg…
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable `SERVER_KEYS_*` error-code registry used throughout `@suaveplan/server-keys`. Each code is a namespa…
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable `SERVER_KEYS_*` error-code registry used throughout `@suaveplan/server-keys`. Each code is a namespa…
- [otel](./src/otel/instruments.md) — Registers the OpenTelemetry counter and histogram that every CLI command in `@suaveplan/server-keys` emits exactly once…
- [instruments](./src/otel/instruments.md) — Registers the OpenTelemetry counter and histogram that every CLI command in `@suaveplan/server-keys` emits exactly once…
- [prompt](./src/prompt/prompt.md) — Thin, test-injectable wrappers around `@inquirer/prompts`' password prompt for the two interactive flows every server-k…
- [prompt](./src/prompt/prompt.md) — Thin, test-injectable wrappers around `@inquirer/prompts`' password prompt for the two interactive flows every server-k…
- [run-function](./src/run-function/run-function.md) — `runFunction` is the single wrapper every server-keys CLI bin uses for its commander `.action(...)` body. It standardis…
- [run-function](./src/run-function/run-function.md) — `runFunction` is the single wrapper every server-keys CLI bin uses for its commander `.action(...)` body. It standardis…
<!-- end:auto-docs -->
## Install
```bash
bun add @suaveplan/server-keys
# or, to install all 18 bins globally:
bun link
```
## Commands
| Bin | Purpose |
| --- | --- |
| `suaveplan` | Top-level dispatcher routing to the 17 sub-commands below |
| `create-root-keys` | Create root keychain (vault + signed public-key file) |
| `create-root-keys-chain` | Create root keychain with self-signed chain |
| `create-server-keys` | Derive a server keychain from a root vault |
| `create-server-keys-chain` | Derive a chained server keychain |
| `roll-server-keys` | Rotate a server keychain to `currentVersion + 1` |
| `roll-server-keys-chain` | Rotate a chained server keychain to `currentVersion + 1` |
| `verify-key-chain` | Verify a chain file against a trusted root file |
| `view-keys` | Decrypt a vault and print a keychain summary |
| `view-vault` | Print vault file metadata without decryption |
| `view-public-key-file` | Decode and print a signed public-key file |
| `view-chain-public` | Decode a public-key file with optional signature verification |
| `prepare-file` | Pack a file into signed packets under an output directory |
| `serve-file` | Reconstruct a file from prepared packets (byte-identical) |
| `validate-file` | Walk every packet in a prepared directory and verify integrity |
| `get-file-stats` | Print file packing statistics without packing |
| `validate-packet` | Validate a single packet against an explicit verify key |
| `license` | Generate / validate / parse / revoke software license keys |
## Programmatic-API quick-start
```ts
import {
createRootKeysCommand,
createServerKeysCommand,
} from "@suaveplan/server-keys/commands/keys/keys";
import {
prepareFileCommand,
} from "@suaveplan/server-keys/commands/files/files";
import { serveFileCommand } from "@suaveplan/server-keys/commands/files/serve-file";
import {
keygen,
generate,
validate,
} from "@suaveplan/server-keys/commands/license/license";
import { runFunction } from "@suaveplan/server-keys/run-function";
// Provision keys
await createRootKeysCommand("rootpw1234", { output: "./keys" });
await createServerKeysCommand(
"./keys/root.vault",
"rootpw1234",
"serverpw1234",
{ output: "./keys" },
);
// Prepare + serve a file with cooperative abort
const ac = new AbortController();
await prepareFileCommand(
"./data.bin",
"./keys/server.vault",
"serverpw1234",
{ output: "./prepared", packetSize: 256 * 1024, signal: ac.signal },
);
await serveFileCommand("./prepared", {
output: "./reconstructed.bin",
signal: ac.signal,
});
// License lifecycle
const kp = await keygen("./lic/keys.json", 1);
const lic = await generate({
signingKeyPath: "./lic/keys.json",
productId: 42,
tier: 2,
expirationDays: 365,
});
const checked = await validate(lic.key, {
publicKeysPath: "./lic/public-keys.json",
});
// Top-level wrapper for custom CLI integrations
await runFunction("custom-cmd", async (signal) => {
// ...your work; OTel + exit(1)-on-fail handled for you
});
```
## Modules
- [`error-codes`](src/error-codes/error-codes.md) — `SERVER_KEYS_*`
registry + `ServerKeysError`.
- [`otel/instruments`](src/otel/instruments.md) — counter + histogram +
`recordCommand`.
- [`run-function`](src/run-function.md) — top-level `.action(...)`
wrapper with OTel + structured error + abort plumbing.
- [`prompt/prompt`](src/prompt/prompt.md) — `promptForPassword`,
`promptForNewPassword` with injectable seam for tests.
- [`commands/keys/keys`](src/commands/keys/keys.md) — non-chain keys
commands.
- [`commands/keys/keys-chain`](src/commands/keys/keys-chain.md) —
chain variants + `verifyKeyChainCommand`.
- [`commands/keys/keys-io`](src/commands/keys/keys-io.md) — shared I/O
- decryption helpers.
- [`commands/files/files`](src/commands/files/files.md) — local-packer
`prepare` / `validate` / `stats` / `validate-packet`.
- [`commands/files/serve-file`](src/commands/files/serve-file.md) —
reconstruction.
- [`commands/files/packet-io`](src/commands/files/packet-io.md) —
packet wire format helpers.
- [`commands/license/license`](src/commands/license/license.md) —
`keygen` / `generate` / `validate` / `parse` / `revoke`.
- [`commands/license/hex`](src/commands/license/hex.md) — hex
encode / decode helpers.
## Architectural decisions
- **Local packer for file-transport bins.** Upstream's
`prepare-file` / `serve-file` / `validate-file` / `get-file-stats` /
`validate-packet` are HTTP clients pointed at a separate
`file-server`. Ships these as local packers backed by
`@suaveplan/packer`'s whole-file `Packer` / `Unpacker` + plain Node
`fs`. No network hop, no separate deployment dependency, byte-name
stable manifest + packet artefacts.
- **suaveplan-native vault wire format.** Vault and signed-public-key
files are NOT byte-compatible with the upstream `@suaveplan/keys`
BitPacker-based format — the crypto-vault orchestration is the
format owner. A future `@suaveplan/keys-migration` package will
ship the conversion path.
- **Plain Node `fs` instead of `@suaveplan/storage-file` for vault I/O.**
storage-file appends `.data` / `.meta` extensions to every key
on write, which would force operator scripts to address artefacts by
`root.vault.data` instead of the conventional `root.vault`. The CLI
contract is byte-name-stable across upstream and suaveplan.
- **Vault sign slot = ed25519, license sign slot = secp256k1.** The
license format requires secp256k1; the vault's sign slot is
ed25519. The license bin therefore uses its own `keygen` sub-command
rather than trying to reuse the vault's signing key.

View File

@@ -0,0 +1,57 @@
# `bin/create-root-keys-chain`
## Purpose
Commander entry point for the `create-root-keys-chain` operator command. Parses CLI
flags, prompts for any required passwords via `@suaveplan/server-keys/prompt`,
and dispatches to the matching function in
`@suaveplan/server-keys/commands/`. The body runs inside
`runFunction("create-root-keys-chain", ...)` so OTel emission, structured stderr
banners, and `exit(1)` on failure are all handled by the shared
wrapper instead of being re-implemented per bin.
This file is a thin wrapper — operator-facing behaviour lives in the
`commands/` module the bin imports. The bin's only responsibilities
are flag parsing, password prompting, and exit-code threading; the
spec compliance, error handling, and OTel emission all come from the
shared wrapper layer.
## Features
- `commander` flag parsing with `-h/--help` for every sub-command.
- Password prompts via `@inquirer/prompts` (echo-disabled).
- OTel command-total + duration histogram per invocation.
- Structured `SERVER_KEYS_*` error banners on failure.
- `process.exit(1)` on any thrown failure, after the banner.
## Usage: Basic
```bash
create-root-keys-chain --help
```
## Usage: Advanced
Run the bin from a shell with a non-TTY `stdin` (e.g. via process
substitution) to script multiple operations:
```bash
echo "mypassword" | create-root-keys-chain [flags...]
```
When invoked under a `SIGINT`-trapping shell, the bin honours the
shared `AbortController` plumbed through `runFunction`.
## API Reference
This bin exposes its surface via `commander` flags only. Run
`create-root-keys-chain --help` for the authoritative list. The library-level surface
the bin wraps is documented under the matching `commands/` module.
## Implementation Notes
The bin imports its target command lazily where possible to keep `bun
link` cold-start time short. Every prompt accepts `{ inputStream,
outputStream, signal }` via `PromptOptions` so tests can drive the
bin without a real TTY; in production the defaults resolve to
`process.stdin` / `process.stdout`.

View File

@@ -0,0 +1,57 @@
# `bin/create-root-keys`
## Purpose
Commander entry point for the `create-root-keys` operator command. Parses CLI
flags, prompts for any required passwords via `@suaveplan/server-keys/prompt`,
and dispatches to the matching function in
`@suaveplan/server-keys/commands/`. The body runs inside
`runFunction("create-root-keys", ...)` so OTel emission, structured stderr
banners, and `exit(1)` on failure are all handled by the shared
wrapper instead of being re-implemented per bin.
This file is a thin wrapper — operator-facing behaviour lives in the
`commands/` module the bin imports. The bin's only responsibilities
are flag parsing, password prompting, and exit-code threading; the
spec compliance, error handling, and OTel emission all come from the
shared wrapper layer.
## Features
- `commander` flag parsing with `-h/--help` for every sub-command.
- Password prompts via `@inquirer/prompts` (echo-disabled).
- OTel command-total + duration histogram per invocation.
- Structured `SERVER_KEYS_*` error banners on failure.
- `process.exit(1)` on any thrown failure, after the banner.
## Usage: Basic
```bash
create-root-keys --help
```
## Usage: Advanced
Run the bin from a shell with a non-TTY `stdin` (e.g. via process
substitution) to script multiple operations:
```bash
echo "mypassword" | create-root-keys [flags...]
```
When invoked under a `SIGINT`-trapping shell, the bin honours the
shared `AbortController` plumbed through `runFunction`.
## API Reference
This bin exposes its surface via `commander` flags only. Run
`create-root-keys --help` for the authoritative list. The library-level surface
the bin wraps is documented under the matching `commands/` module.
## Implementation Notes
The bin imports its target command lazily where possible to keep `bun
link` cold-start time short. Every prompt accepts `{ inputStream,
outputStream, signal }` via `PromptOptions` so tests can drive the
bin without a real TTY; in production the defaults resolve to
`process.stdin` / `process.stdout`.

View File

@@ -0,0 +1,57 @@
# `bin/create-server-keys-chain`
## Purpose
Commander entry point for the `create-server-keys-chain` operator command. Parses CLI
flags, prompts for any required passwords via `@suaveplan/server-keys/prompt`,
and dispatches to the matching function in
`@suaveplan/server-keys/commands/`. The body runs inside
`runFunction("create-server-keys-chain", ...)` so OTel emission, structured stderr
banners, and `exit(1)` on failure are all handled by the shared
wrapper instead of being re-implemented per bin.
This file is a thin wrapper — operator-facing behaviour lives in the
`commands/` module the bin imports. The bin's only responsibilities
are flag parsing, password prompting, and exit-code threading; the
spec compliance, error handling, and OTel emission all come from the
shared wrapper layer.
## Features
- `commander` flag parsing with `-h/--help` for every sub-command.
- Password prompts via `@inquirer/prompts` (echo-disabled).
- OTel command-total + duration histogram per invocation.
- Structured `SERVER_KEYS_*` error banners on failure.
- `process.exit(1)` on any thrown failure, after the banner.
## Usage: Basic
```bash
create-server-keys-chain --help
```
## Usage: Advanced
Run the bin from a shell with a non-TTY `stdin` (e.g. via process
substitution) to script multiple operations:
```bash
echo "mypassword" | create-server-keys-chain [flags...]
```
When invoked under a `SIGINT`-trapping shell, the bin honours the
shared `AbortController` plumbed through `runFunction`.
## API Reference
This bin exposes its surface via `commander` flags only. Run
`create-server-keys-chain --help` for the authoritative list. The library-level surface
the bin wraps is documented under the matching `commands/` module.
## Implementation Notes
The bin imports its target command lazily where possible to keep `bun
link` cold-start time short. Every prompt accepts `{ inputStream,
outputStream, signal }` via `PromptOptions` so tests can drive the
bin without a real TTY; in production the defaults resolve to
`process.stdin` / `process.stdout`.

View File

@@ -0,0 +1,57 @@
# `bin/create-server-keys`
## Purpose
Commander entry point for the `create-server-keys` operator command. Parses CLI
flags, prompts for any required passwords via `@suaveplan/server-keys/prompt`,
and dispatches to the matching function in
`@suaveplan/server-keys/commands/`. The body runs inside
`runFunction("create-server-keys", ...)` so OTel emission, structured stderr
banners, and `exit(1)` on failure are all handled by the shared
wrapper instead of being re-implemented per bin.
This file is a thin wrapper — operator-facing behaviour lives in the
`commands/` module the bin imports. The bin's only responsibilities
are flag parsing, password prompting, and exit-code threading; the
spec compliance, error handling, and OTel emission all come from the
shared wrapper layer.
## Features
- `commander` flag parsing with `-h/--help` for every sub-command.
- Password prompts via `@inquirer/prompts` (echo-disabled).
- OTel command-total + duration histogram per invocation.
- Structured `SERVER_KEYS_*` error banners on failure.
- `process.exit(1)` on any thrown failure, after the banner.
## Usage: Basic
```bash
create-server-keys --help
```
## Usage: Advanced
Run the bin from a shell with a non-TTY `stdin` (e.g. via process
substitution) to script multiple operations:
```bash
echo "mypassword" | create-server-keys [flags...]
```
When invoked under a `SIGINT`-trapping shell, the bin honours the
shared `AbortController` plumbed through `runFunction`.
## API Reference
This bin exposes its surface via `commander` flags only. Run
`create-server-keys --help` for the authoritative list. The library-level surface
the bin wraps is documented under the matching `commands/` module.
## Implementation Notes
The bin imports its target command lazily where possible to keep `bun
link` cold-start time short. Every prompt accepts `{ inputStream,
outputStream, signal }` via `PromptOptions` so tests can drive the
bin without a real TTY; in production the defaults resolve to
`process.stdin` / `process.stdout`.

View File

@@ -0,0 +1,57 @@
# `bin/get-file-stats`
## Purpose
Commander entry point for the `get-file-stats` operator command. Parses CLI
flags, prompts for any required passwords via `@suaveplan/server-keys/prompt`,
and dispatches to the matching function in
`@suaveplan/server-keys/commands/`. The body runs inside
`runFunction("get-file-stats", ...)` so OTel emission, structured stderr
banners, and `exit(1)` on failure are all handled by the shared
wrapper instead of being re-implemented per bin.
This file is a thin wrapper — operator-facing behaviour lives in the
`commands/` module the bin imports. The bin's only responsibilities
are flag parsing, password prompting, and exit-code threading; the
spec compliance, error handling, and OTel emission all come from the
shared wrapper layer.
## Features
- `commander` flag parsing with `-h/--help` for every sub-command.
- Password prompts via `@inquirer/prompts` (echo-disabled).
- OTel command-total + duration histogram per invocation.
- Structured `SERVER_KEYS_*` error banners on failure.
- `process.exit(1)` on any thrown failure, after the banner.
## Usage: Basic
```bash
get-file-stats --help
```
## Usage: Advanced
Run the bin from a shell with a non-TTY `stdin` (e.g. via process
substitution) to script multiple operations:
```bash
echo "mypassword" | get-file-stats [flags...]
```
When invoked under a `SIGINT`-trapping shell, the bin honours the
shared `AbortController` plumbed through `runFunction`.
## API Reference
This bin exposes its surface via `commander` flags only. Run
`get-file-stats --help` for the authoritative list. The library-level surface
the bin wraps is documented under the matching `commands/` module.
## Implementation Notes
The bin imports its target command lazily where possible to keep `bun
link` cold-start time short. Every prompt accepts `{ inputStream,
outputStream, signal }` via `PromptOptions` so tests can drive the
bin without a real TTY; in production the defaults resolve to
`process.stdin` / `process.stdout`.

View File

@@ -0,0 +1,57 @@
# `bin/index`
## Purpose
Commander entry point for the `index` operator command. Parses CLI
flags, prompts for any required passwords via `@suaveplan/server-keys/prompt`,
and dispatches to the matching function in
`@suaveplan/server-keys/commands/`. The body runs inside
`runFunction("index", ...)` so OTel emission, structured stderr
banners, and `exit(1)` on failure are all handled by the shared
wrapper instead of being re-implemented per bin.
This file is a thin wrapper — operator-facing behaviour lives in the
`commands/` module the bin imports. The bin's only responsibilities
are flag parsing, password prompting, and exit-code threading; the
spec compliance, error handling, and OTel emission all come from the
shared wrapper layer.
## Features
- `commander` flag parsing with `-h/--help` for every sub-command.
- Password prompts via `@inquirer/prompts` (echo-disabled).
- OTel command-total + duration histogram per invocation.
- Structured `SERVER_KEYS_*` error banners on failure.
- `process.exit(1)` on any thrown failure, after the banner.
## Usage: Basic
```bash
index --help
```
## Usage: Advanced
Run the bin from a shell with a non-TTY `stdin` (e.g. via process
substitution) to script multiple operations:
```bash
echo "mypassword" | index [flags...]
```
When invoked under a `SIGINT`-trapping shell, the bin honours the
shared `AbortController` plumbed through `runFunction`.
## API Reference
This bin exposes its surface via `commander` flags only. Run
`index --help` for the authoritative list. The library-level surface
the bin wraps is documented under the matching `commands/` module.
## Implementation Notes
The bin imports its target command lazily where possible to keep `bun
link` cold-start time short. Every prompt accepts `{ inputStream,
outputStream, signal }` via `PromptOptions` so tests can drive the
bin without a real TTY; in production the defaults resolve to
`process.stdin` / `process.stdout`.

View File

@@ -0,0 +1,57 @@
# `bin/license`
## Purpose
Commander entry point for the `license` operator command. Parses CLI
flags, prompts for any required passwords via `@suaveplan/server-keys/prompt`,
and dispatches to the matching function in
`@suaveplan/server-keys/commands/`. The body runs inside
`runFunction("license", ...)` so OTel emission, structured stderr
banners, and `exit(1)` on failure are all handled by the shared
wrapper instead of being re-implemented per bin.
This file is a thin wrapper — operator-facing behaviour lives in the
`commands/` module the bin imports. The bin's only responsibilities
are flag parsing, password prompting, and exit-code threading; the
spec compliance, error handling, and OTel emission all come from the
shared wrapper layer.
## Features
- `commander` flag parsing with `-h/--help` for every sub-command.
- Password prompts via `@inquirer/prompts` (echo-disabled).
- OTel command-total + duration histogram per invocation.
- Structured `SERVER_KEYS_*` error banners on failure.
- `process.exit(1)` on any thrown failure, after the banner.
## Usage: Basic
```bash
license --help
```
## Usage: Advanced
Run the bin from a shell with a non-TTY `stdin` (e.g. via process
substitution) to script multiple operations:
```bash
echo "mypassword" | license [flags...]
```
When invoked under a `SIGINT`-trapping shell, the bin honours the
shared `AbortController` plumbed through `runFunction`.
## API Reference
This bin exposes its surface via `commander` flags only. Run
`license --help` for the authoritative list. The library-level surface
the bin wraps is documented under the matching `commands/` module.
## Implementation Notes
The bin imports its target command lazily where possible to keep `bun
link` cold-start time short. Every prompt accepts `{ inputStream,
outputStream, signal }` via `PromptOptions` so tests can drive the
bin without a real TTY; in production the defaults resolve to
`process.stdin` / `process.stdout`.

View File

@@ -0,0 +1,57 @@
# `bin/prepare-file`
## Purpose
Commander entry point for the `prepare-file` operator command. Parses CLI
flags, prompts for any required passwords via `@suaveplan/server-keys/prompt`,
and dispatches to the matching function in
`@suaveplan/server-keys/commands/`. The body runs inside
`runFunction("prepare-file", ...)` so OTel emission, structured stderr
banners, and `exit(1)` on failure are all handled by the shared
wrapper instead of being re-implemented per bin.
This file is a thin wrapper — operator-facing behaviour lives in the
`commands/` module the bin imports. The bin's only responsibilities
are flag parsing, password prompting, and exit-code threading; the
spec compliance, error handling, and OTel emission all come from the
shared wrapper layer.
## Features
- `commander` flag parsing with `-h/--help` for every sub-command.
- Password prompts via `@inquirer/prompts` (echo-disabled).
- OTel command-total + duration histogram per invocation.
- Structured `SERVER_KEYS_*` error banners on failure.
- `process.exit(1)` on any thrown failure, after the banner.
## Usage: Basic
```bash
prepare-file --help
```
## Usage: Advanced
Run the bin from a shell with a non-TTY `stdin` (e.g. via process
substitution) to script multiple operations:
```bash
echo "mypassword" | prepare-file [flags...]
```
When invoked under a `SIGINT`-trapping shell, the bin honours the
shared `AbortController` plumbed through `runFunction`.
## API Reference
This bin exposes its surface via `commander` flags only. Run
`prepare-file --help` for the authoritative list. The library-level surface
the bin wraps is documented under the matching `commands/` module.
## Implementation Notes
The bin imports its target command lazily where possible to keep `bun
link` cold-start time short. Every prompt accepts `{ inputStream,
outputStream, signal }` via `PromptOptions` so tests can drive the
bin without a real TTY; in production the defaults resolve to
`process.stdin` / `process.stdout`.

View File

@@ -0,0 +1,57 @@
# `bin/roll-server-keys-chain`
## Purpose
Commander entry point for the `roll-server-keys-chain` operator command. Parses CLI
flags, prompts for any required passwords via `@suaveplan/server-keys/prompt`,
and dispatches to the matching function in
`@suaveplan/server-keys/commands/`. The body runs inside
`runFunction("roll-server-keys-chain", ...)` so OTel emission, structured stderr
banners, and `exit(1)` on failure are all handled by the shared
wrapper instead of being re-implemented per bin.
This file is a thin wrapper — operator-facing behaviour lives in the
`commands/` module the bin imports. The bin's only responsibilities
are flag parsing, password prompting, and exit-code threading; the
spec compliance, error handling, and OTel emission all come from the
shared wrapper layer.
## Features
- `commander` flag parsing with `-h/--help` for every sub-command.
- Password prompts via `@inquirer/prompts` (echo-disabled).
- OTel command-total + duration histogram per invocation.
- Structured `SERVER_KEYS_*` error banners on failure.
- `process.exit(1)` on any thrown failure, after the banner.
## Usage: Basic
```bash
roll-server-keys-chain --help
```
## Usage: Advanced
Run the bin from a shell with a non-TTY `stdin` (e.g. via process
substitution) to script multiple operations:
```bash
echo "mypassword" | roll-server-keys-chain [flags...]
```
When invoked under a `SIGINT`-trapping shell, the bin honours the
shared `AbortController` plumbed through `runFunction`.
## API Reference
This bin exposes its surface via `commander` flags only. Run
`roll-server-keys-chain --help` for the authoritative list. The library-level surface
the bin wraps is documented under the matching `commands/` module.
## Implementation Notes
The bin imports its target command lazily where possible to keep `bun
link` cold-start time short. Every prompt accepts `{ inputStream,
outputStream, signal }` via `PromptOptions` so tests can drive the
bin without a real TTY; in production the defaults resolve to
`process.stdin` / `process.stdout`.

View File

@@ -0,0 +1,57 @@
# `bin/roll-server-keys`
## Purpose
Commander entry point for the `roll-server-keys` operator command. Parses CLI
flags, prompts for any required passwords via `@suaveplan/server-keys/prompt`,
and dispatches to the matching function in
`@suaveplan/server-keys/commands/`. The body runs inside
`runFunction("roll-server-keys", ...)` so OTel emission, structured stderr
banners, and `exit(1)` on failure are all handled by the shared
wrapper instead of being re-implemented per bin.
This file is a thin wrapper — operator-facing behaviour lives in the
`commands/` module the bin imports. The bin's only responsibilities
are flag parsing, password prompting, and exit-code threading; the
spec compliance, error handling, and OTel emission all come from the
shared wrapper layer.
## Features
- `commander` flag parsing with `-h/--help` for every sub-command.
- Password prompts via `@inquirer/prompts` (echo-disabled).
- OTel command-total + duration histogram per invocation.
- Structured `SERVER_KEYS_*` error banners on failure.
- `process.exit(1)` on any thrown failure, after the banner.
## Usage: Basic
```bash
roll-server-keys --help
```
## Usage: Advanced
Run the bin from a shell with a non-TTY `stdin` (e.g. via process
substitution) to script multiple operations:
```bash
echo "mypassword" | roll-server-keys [flags...]
```
When invoked under a `SIGINT`-trapping shell, the bin honours the
shared `AbortController` plumbed through `runFunction`.
## API Reference
This bin exposes its surface via `commander` flags only. Run
`roll-server-keys --help` for the authoritative list. The library-level surface
the bin wraps is documented under the matching `commands/` module.
## Implementation Notes
The bin imports its target command lazily where possible to keep `bun
link` cold-start time short. Every prompt accepts `{ inputStream,
outputStream, signal }` via `PromptOptions` so tests can drive the
bin without a real TTY; in production the defaults resolve to
`process.stdin` / `process.stdout`.

View File

@@ -0,0 +1,57 @@
# `bin/serve-file`
## Purpose
Commander entry point for the `serve-file` operator command. Parses CLI
flags, prompts for any required passwords via `@suaveplan/server-keys/prompt`,
and dispatches to the matching function in
`@suaveplan/server-keys/commands/`. The body runs inside
`runFunction("serve-file", ...)` so OTel emission, structured stderr
banners, and `exit(1)` on failure are all handled by the shared
wrapper instead of being re-implemented per bin.
This file is a thin wrapper — operator-facing behaviour lives in the
`commands/` module the bin imports. The bin's only responsibilities
are flag parsing, password prompting, and exit-code threading; the
spec compliance, error handling, and OTel emission all come from the
shared wrapper layer.
## Features
- `commander` flag parsing with `-h/--help` for every sub-command.
- Password prompts via `@inquirer/prompts` (echo-disabled).
- OTel command-total + duration histogram per invocation.
- Structured `SERVER_KEYS_*` error banners on failure.
- `process.exit(1)` on any thrown failure, after the banner.
## Usage: Basic
```bash
serve-file --help
```
## Usage: Advanced
Run the bin from a shell with a non-TTY `stdin` (e.g. via process
substitution) to script multiple operations:
```bash
echo "mypassword" | serve-file [flags...]
```
When invoked under a `SIGINT`-trapping shell, the bin honours the
shared `AbortController` plumbed through `runFunction`.
## API Reference
This bin exposes its surface via `commander` flags only. Run
`serve-file --help` for the authoritative list. The library-level surface
the bin wraps is documented under the matching `commands/` module.
## Implementation Notes
The bin imports its target command lazily where possible to keep `bun
link` cold-start time short. Every prompt accepts `{ inputStream,
outputStream, signal }` via `PromptOptions` so tests can drive the
bin without a real TTY; in production the defaults resolve to
`process.stdin` / `process.stdout`.

View File

@@ -0,0 +1,57 @@
# `bin/validate-file`
## Purpose
Commander entry point for the `validate-file` operator command. Parses CLI
flags, prompts for any required passwords via `@suaveplan/server-keys/prompt`,
and dispatches to the matching function in
`@suaveplan/server-keys/commands/`. The body runs inside
`runFunction("validate-file", ...)` so OTel emission, structured stderr
banners, and `exit(1)` on failure are all handled by the shared
wrapper instead of being re-implemented per bin.
This file is a thin wrapper — operator-facing behaviour lives in the
`commands/` module the bin imports. The bin's only responsibilities
are flag parsing, password prompting, and exit-code threading; the
spec compliance, error handling, and OTel emission all come from the
shared wrapper layer.
## Features
- `commander` flag parsing with `-h/--help` for every sub-command.
- Password prompts via `@inquirer/prompts` (echo-disabled).
- OTel command-total + duration histogram per invocation.
- Structured `SERVER_KEYS_*` error banners on failure.
- `process.exit(1)` on any thrown failure, after the banner.
## Usage: Basic
```bash
validate-file --help
```
## Usage: Advanced
Run the bin from a shell with a non-TTY `stdin` (e.g. via process
substitution) to script multiple operations:
```bash
echo "mypassword" | validate-file [flags...]
```
When invoked under a `SIGINT`-trapping shell, the bin honours the
shared `AbortController` plumbed through `runFunction`.
## API Reference
This bin exposes its surface via `commander` flags only. Run
`validate-file --help` for the authoritative list. The library-level surface
the bin wraps is documented under the matching `commands/` module.
## Implementation Notes
The bin imports its target command lazily where possible to keep `bun
link` cold-start time short. Every prompt accepts `{ inputStream,
outputStream, signal }` via `PromptOptions` so tests can drive the
bin without a real TTY; in production the defaults resolve to
`process.stdin` / `process.stdout`.

View File

@@ -0,0 +1,57 @@
# `bin/validate-packet`
## Purpose
Commander entry point for the `validate-packet` operator command. Parses CLI
flags, prompts for any required passwords via `@suaveplan/server-keys/prompt`,
and dispatches to the matching function in
`@suaveplan/server-keys/commands/`. The body runs inside
`runFunction("validate-packet", ...)` so OTel emission, structured stderr
banners, and `exit(1)` on failure are all handled by the shared
wrapper instead of being re-implemented per bin.
This file is a thin wrapper — operator-facing behaviour lives in the
`commands/` module the bin imports. The bin's only responsibilities
are flag parsing, password prompting, and exit-code threading; the
spec compliance, error handling, and OTel emission all come from the
shared wrapper layer.
## Features
- `commander` flag parsing with `-h/--help` for every sub-command.
- Password prompts via `@inquirer/prompts` (echo-disabled).
- OTel command-total + duration histogram per invocation.
- Structured `SERVER_KEYS_*` error banners on failure.
- `process.exit(1)` on any thrown failure, after the banner.
## Usage: Basic
```bash
validate-packet --help
```
## Usage: Advanced
Run the bin from a shell with a non-TTY `stdin` (e.g. via process
substitution) to script multiple operations:
```bash
echo "mypassword" | validate-packet [flags...]
```
When invoked under a `SIGINT`-trapping shell, the bin honours the
shared `AbortController` plumbed through `runFunction`.
## API Reference
This bin exposes its surface via `commander` flags only. Run
`validate-packet --help` for the authoritative list. The library-level surface
the bin wraps is documented under the matching `commands/` module.
## Implementation Notes
The bin imports its target command lazily where possible to keep `bun
link` cold-start time short. Every prompt accepts `{ inputStream,
outputStream, signal }` via `PromptOptions` so tests can drive the
bin without a real TTY; in production the defaults resolve to
`process.stdin` / `process.stdout`.

View File

@@ -0,0 +1,57 @@
# `bin/verify-key-chain`
## Purpose
Commander entry point for the `verify-key-chain` operator command. Parses CLI
flags, prompts for any required passwords via `@suaveplan/server-keys/prompt`,
and dispatches to the matching function in
`@suaveplan/server-keys/commands/`. The body runs inside
`runFunction("verify-key-chain", ...)` so OTel emission, structured stderr
banners, and `exit(1)` on failure are all handled by the shared
wrapper instead of being re-implemented per bin.
This file is a thin wrapper — operator-facing behaviour lives in the
`commands/` module the bin imports. The bin's only responsibilities
are flag parsing, password prompting, and exit-code threading; the
spec compliance, error handling, and OTel emission all come from the
shared wrapper layer.
## Features
- `commander` flag parsing with `-h/--help` for every sub-command.
- Password prompts via `@inquirer/prompts` (echo-disabled).
- OTel command-total + duration histogram per invocation.
- Structured `SERVER_KEYS_*` error banners on failure.
- `process.exit(1)` on any thrown failure, after the banner.
## Usage: Basic
```bash
verify-key-chain --help
```
## Usage: Advanced
Run the bin from a shell with a non-TTY `stdin` (e.g. via process
substitution) to script multiple operations:
```bash
echo "mypassword" | verify-key-chain [flags...]
```
When invoked under a `SIGINT`-trapping shell, the bin honours the
shared `AbortController` plumbed through `runFunction`.
## API Reference
This bin exposes its surface via `commander` flags only. Run
`verify-key-chain --help` for the authoritative list. The library-level surface
the bin wraps is documented under the matching `commands/` module.
## Implementation Notes
The bin imports its target command lazily where possible to keep `bun
link` cold-start time short. Every prompt accepts `{ inputStream,
outputStream, signal }` via `PromptOptions` so tests can drive the
bin without a real TTY; in production the defaults resolve to
`process.stdin` / `process.stdout`.

View File

@@ -0,0 +1,57 @@
# `bin/view-chain-public`
## Purpose
Commander entry point for the `view-chain-public` operator command. Parses CLI
flags, prompts for any required passwords via `@suaveplan/server-keys/prompt`,
and dispatches to the matching function in
`@suaveplan/server-keys/commands/`. The body runs inside
`runFunction("view-chain-public", ...)` so OTel emission, structured stderr
banners, and `exit(1)` on failure are all handled by the shared
wrapper instead of being re-implemented per bin.
This file is a thin wrapper — operator-facing behaviour lives in the
`commands/` module the bin imports. The bin's only responsibilities
are flag parsing, password prompting, and exit-code threading; the
spec compliance, error handling, and OTel emission all come from the
shared wrapper layer.
## Features
- `commander` flag parsing with `-h/--help` for every sub-command.
- Password prompts via `@inquirer/prompts` (echo-disabled).
- OTel command-total + duration histogram per invocation.
- Structured `SERVER_KEYS_*` error banners on failure.
- `process.exit(1)` on any thrown failure, after the banner.
## Usage: Basic
```bash
view-chain-public --help
```
## Usage: Advanced
Run the bin from a shell with a non-TTY `stdin` (e.g. via process
substitution) to script multiple operations:
```bash
echo "mypassword" | view-chain-public [flags...]
```
When invoked under a `SIGINT`-trapping shell, the bin honours the
shared `AbortController` plumbed through `runFunction`.
## API Reference
This bin exposes its surface via `commander` flags only. Run
`view-chain-public --help` for the authoritative list. The library-level surface
the bin wraps is documented under the matching `commands/` module.
## Implementation Notes
The bin imports its target command lazily where possible to keep `bun
link` cold-start time short. Every prompt accepts `{ inputStream,
outputStream, signal }` via `PromptOptions` so tests can drive the
bin without a real TTY; in production the defaults resolve to
`process.stdin` / `process.stdout`.

View File

@@ -0,0 +1,57 @@
# `bin/view-keys`
## Purpose
Commander entry point for the `view-keys` operator command. Parses CLI
flags, prompts for any required passwords via `@suaveplan/server-keys/prompt`,
and dispatches to the matching function in
`@suaveplan/server-keys/commands/`. The body runs inside
`runFunction("view-keys", ...)` so OTel emission, structured stderr
banners, and `exit(1)` on failure are all handled by the shared
wrapper instead of being re-implemented per bin.
This file is a thin wrapper — operator-facing behaviour lives in the
`commands/` module the bin imports. The bin's only responsibilities
are flag parsing, password prompting, and exit-code threading; the
spec compliance, error handling, and OTel emission all come from the
shared wrapper layer.
## Features
- `commander` flag parsing with `-h/--help` for every sub-command.
- Password prompts via `@inquirer/prompts` (echo-disabled).
- OTel command-total + duration histogram per invocation.
- Structured `SERVER_KEYS_*` error banners on failure.
- `process.exit(1)` on any thrown failure, after the banner.
## Usage: Basic
```bash
view-keys --help
```
## Usage: Advanced
Run the bin from a shell with a non-TTY `stdin` (e.g. via process
substitution) to script multiple operations:
```bash
echo "mypassword" | view-keys [flags...]
```
When invoked under a `SIGINT`-trapping shell, the bin honours the
shared `AbortController` plumbed through `runFunction`.
## API Reference
This bin exposes its surface via `commander` flags only. Run
`view-keys --help` for the authoritative list. The library-level surface
the bin wraps is documented under the matching `commands/` module.
## Implementation Notes
The bin imports its target command lazily where possible to keep `bun
link` cold-start time short. Every prompt accepts `{ inputStream,
outputStream, signal }` via `PromptOptions` so tests can drive the
bin without a real TTY; in production the defaults resolve to
`process.stdin` / `process.stdout`.

View File

@@ -0,0 +1,57 @@
# `bin/view-public-key-file`
## Purpose
Commander entry point for the `view-public-key-file` operator command. Parses CLI
flags, prompts for any required passwords via `@suaveplan/server-keys/prompt`,
and dispatches to the matching function in
`@suaveplan/server-keys/commands/`. The body runs inside
`runFunction("view-public-key-file", ...)` so OTel emission, structured stderr
banners, and `exit(1)` on failure are all handled by the shared
wrapper instead of being re-implemented per bin.
This file is a thin wrapper — operator-facing behaviour lives in the
`commands/` module the bin imports. The bin's only responsibilities
are flag parsing, password prompting, and exit-code threading; the
spec compliance, error handling, and OTel emission all come from the
shared wrapper layer.
## Features
- `commander` flag parsing with `-h/--help` for every sub-command.
- Password prompts via `@inquirer/prompts` (echo-disabled).
- OTel command-total + duration histogram per invocation.
- Structured `SERVER_KEYS_*` error banners on failure.
- `process.exit(1)` on any thrown failure, after the banner.
## Usage: Basic
```bash
view-public-key-file --help
```
## Usage: Advanced
Run the bin from a shell with a non-TTY `stdin` (e.g. via process
substitution) to script multiple operations:
```bash
echo "mypassword" | view-public-key-file [flags...]
```
When invoked under a `SIGINT`-trapping shell, the bin honours the
shared `AbortController` plumbed through `runFunction`.
## API Reference
This bin exposes its surface via `commander` flags only. Run
`view-public-key-file --help` for the authoritative list. The library-level surface
the bin wraps is documented under the matching `commands/` module.
## Implementation Notes
The bin imports its target command lazily where possible to keep `bun
link` cold-start time short. Every prompt accepts `{ inputStream,
outputStream, signal }` via `PromptOptions` so tests can drive the
bin without a real TTY; in production the defaults resolve to
`process.stdin` / `process.stdout`.

View File

@@ -0,0 +1,57 @@
# `bin/view-vault`
## Purpose
Commander entry point for the `view-vault` operator command. Parses CLI
flags, prompts for any required passwords via `@suaveplan/server-keys/prompt`,
and dispatches to the matching function in
`@suaveplan/server-keys/commands/`. The body runs inside
`runFunction("view-vault", ...)` so OTel emission, structured stderr
banners, and `exit(1)` on failure are all handled by the shared
wrapper instead of being re-implemented per bin.
This file is a thin wrapper — operator-facing behaviour lives in the
`commands/` module the bin imports. The bin's only responsibilities
are flag parsing, password prompting, and exit-code threading; the
spec compliance, error handling, and OTel emission all come from the
shared wrapper layer.
## Features
- `commander` flag parsing with `-h/--help` for every sub-command.
- Password prompts via `@inquirer/prompts` (echo-disabled).
- OTel command-total + duration histogram per invocation.
- Structured `SERVER_KEYS_*` error banners on failure.
- `process.exit(1)` on any thrown failure, after the banner.
## Usage: Basic
```bash
view-vault --help
```
## Usage: Advanced
Run the bin from a shell with a non-TTY `stdin` (e.g. via process
substitution) to script multiple operations:
```bash
echo "mypassword" | view-vault [flags...]
```
When invoked under a `SIGINT`-trapping shell, the bin honours the
shared `AbortController` plumbed through `runFunction`.
## API Reference
This bin exposes its surface via `commander` flags only. Run
`view-vault --help` for the authoritative list. The library-level surface
the bin wraps is documented under the matching `commands/` module.
## Implementation Notes
The bin imports its target command lazily where possible to keep `bun
link` cold-start time short. Every prompt accepts `{ inputStream,
outputStream, signal }` via `PromptOptions` so tests can drive the
bin without a real TTY; in production the defaults resolve to
`process.stdin` / `process.stdout`.

View File

@@ -0,0 +1,122 @@
# `commands/files/files`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Local-packer file-transport commands: `prepareFileCommand`,
`validateFileCommand`, `getFileStatsCommand`, `validatePacketCommand`.
These four functions implement the operator-facing surface every file-
transport bin in `@suaveplan/server-keys` calls into. Reconstruction
lives in the sibling `serve-file.ts`. The implementation is fully local —
no HTTP, no remote file server — backed by `@suaveplan/packer`'s
whole-file `Packer` / `Unpacker` and plain Node `fs`.
The choice of a local packer (rather than an HTTP client to a remote
file-server, which the upstream package shipped) was made deliberately:
the upstream HTTP design forces operators to stand up a separate
file-server before they can prepare or serve a file, and the binary
contract is dominated by the packet wire format anyway. Owning the
format here keeps the suaveplan test surface auditable without a network
hop and removes a non-trivial deployment dependency.
## Features
- Fixed-size packetisation (`packetSize`, default 1 MiB) with a JSON
envelope per packet on disk.
- ed25519 signature per packet using the vault's `sign` slot secret;
recipient verifies against the manifest's `signerPublicKey`.
- Manifest is a single JSON document that lists every packet's
`index`, `checksum`, `filename`, and `originalSize`.
- Cooperative abort at every packet boundary via `options.signal`.
- Typed `ServerKeysError` for every failure (`INVALID_INPUT`,
`VAULT_DECRYPT_FAILED`, `PACKET_INTEGRITY_FAILED`, `FILE_NOT_FOUND`,
`ABORTED`).
## Usage: Basic
```ts
import {
prepareFileCommand,
validateFileCommand,
getFileStatsCommand,
} from "@suaveplan/server-keys/commands/files/files";
const stats = await getFileStatsCommand("/tmp/data.bin", {
packetSize: 256 * 1024,
});
console.log(stats.estimatedPackets);
await prepareFileCommand(
"/tmp/data.bin",
"/tmp/keys/server.vault",
"serverpw1234",
{ output: "/tmp/prepared", packetSize: 256 * 1024 },
);
const result = await validateFileCommand("/tmp/prepared");
expect(result.isComplete).toBe(true);
expect(result.invalidPackets).toEqual([]);
```
## Usage: Advanced
Single-packet validation against an explicit hex public key (useful for
spot-checking a remote-supplied packet without trusting the colocated
manifest):
```ts
import { validatePacketCommand } from "@suaveplan/server-keys/commands/files/files";
const r = await validatePacketCommand(
"/tmp/prepared/packet.000000.bin",
"abcd...ed25519-public-key-hex",
);
expect(r.isValid).toBe(true);
```
Detect a tampered packet:
```ts
import { promises as fs } from "node:fs";
const target = "/tmp/prepared/packet.000003.bin";
const original = await fs.readFile(target, "utf-8");
const obj = JSON.parse(original);
const bytes = Buffer.from(obj.data, "hex");
bytes[10] ^= 0xff;
obj.data = bytes.toString("hex");
await fs.writeFile(target, JSON.stringify(obj));
const r = await validateFileCommand("/tmp/prepared");
expect(r.isComplete).toBe(false);
expect(r.invalidPackets).toContain(3);
```
## API Reference
- `prepareFileCommand(filePath, vaultPath, password, options?) =>
Promise<FileManifest>`
- `validateFileCommand(packetDir, options?) =>
Promise<ValidateFileResult>`
- `getFileStatsCommand(filePath, options?) => Promise<FileStatsResult>`
- `validatePacketCommand(packetPath, verifyKeyHex, options?) =>
Promise<ValidatePacketResult>`
All take `FileCommandOptions = { output?, packetSize?, logger?,
signal? }`.
## Implementation Notes
`prepareFileCommand` constructs a new `Packer` per packet so the
underlying event bus stays scoped to a single pack call — the abstract
`Packer` is an `EventEmitter` and we don't want pack events leaking
across packets. Each packet stores its `signature`, `data`, `checksum`,
and `originalSize` in a small JSON envelope (`{ data, signature,
checksum, originalSize }`), all hex-encoded — operators inspecting
packets by hand can paste them into a hex viewer without further
tooling. The intentional `compressionLevel: 0` (no gzip) keeps each
packet's plaintext slice byte-identical to the input region; without
this, signing operates over the gzip stream and reconstruction
requires inverting it, which is an avoidable layer for the local
transport.

View File

@@ -0,0 +1,93 @@
# `commands/files/files.types`
## Purpose
Type definitions and the default packet-size constant shared by every
file-transport command in `@suaveplan/server-keys`. Centralising the
shapes here lets `files.ts`, `serve-file.ts`, and `packet-io.ts` all
agree on the wire format without circular imports, and gives downstream
programmatic consumers a single import for the typed return envelopes
(`FileManifest`, `ValidateFileResult`, `FileStatsResult`,
`ValidatePacketResult`, `PacketRecord`).
The `DEFAULT_PACKET_SIZE = 1_048_576` (1 MiB) constant mirrors the
upstream default; both the prepare and stats commands fall back to it
when the caller doesn't override `packetSize`.
## Features
- `FilesLogger` — `Pick<LoggerLike, "info" | "error">`; injected.
- `FileCommandOptions` — shared `{ output?, packetSize?, logger?,
signal? }`.
- `FileManifest` — top-level on-disk JSON shape, schema-versioned via
`schemaVersion: 1`.
- `PacketRecord` — per-packet metadata in the manifest.
- `ValidateFileResult`, `FileStatsResult`, `ValidatePacketResult` —
typed return envelopes.
- `DEFAULT_PACKET_SIZE = 1_048_576`.
## Usage: Basic
```ts
import type {
FileCommandOptions,
FileManifest,
} from "@suaveplan/server-keys/commands/files/files.types";
const options: FileCommandOptions = {
output: "/tmp/prepared",
packetSize: 512 * 1024,
};
function packetCount(m: FileManifest): number {
return m.totalPackets;
}
```
## Usage: Advanced
Validate a third-party-produced manifest at the boundary before
consuming it:
```ts
import type { FileManifest } from "@suaveplan/server-keys/commands/files/files.types";
function isFileManifest(value: unknown): value is FileManifest {
if (typeof value !== "object" || value === null) return false;
const v = value as Record<string, unknown>;
return (
v.schemaVersion === 1 &&
typeof v.fileId === "string" &&
typeof v.originalFilename === "string" &&
typeof v.originalSize === "number" &&
typeof v.packetSize === "number" &&
typeof v.totalPackets === "number" &&
typeof v.signerPublicKey === "string" &&
Array.isArray(v.packets)
);
}
```
## API Reference
- `interface FilesLogger`
- `interface FileCommandOptions`
- `interface PacketRecord { index, checksum, filename, originalSize }`
- `interface FileManifest { schemaVersion, fileId, originalFilename,
originalSize, packetSize, totalPackets, signerPublicKey, packets }`
- `interface ValidateFileResult { isComplete, totalPackets,
validPackets, invalidPackets, fileId }`
- `interface FileStatsResult { inputPath, originalSize, packetSize,
estimatedPackets, estimatedOverheadBytes, estimatedPackedBytes,
lastModified }`
- `interface ValidatePacketResult { isValid, index, checksum, error? }`
- `const DEFAULT_PACKET_SIZE: 1_048_576`
## Implementation Notes
Every field is `readonly` to keep accidental in-place mutation out of
the call sites; the manifest is also frozen-on-write by the JSON
round-trip in `writeManifest`. The `schemaVersion: 1` literal type pins
the on-disk format — a future v2 change would add `2` as a discriminator
and require an explicit migration path in the orchestration layer
instead of silently changing the shape.

View File

@@ -0,0 +1,87 @@
# `commands/files/packet-io`
## Purpose
Shared on-disk format helpers for the file-transport commands. Holds
the canonical packet envelope `{ data, signature, checksum,
originalSize }` (all hex-encoded scalars in JSON), the canonical
packet filename convention (`packet.<6-digit-index>.bin`), the manifest
filename (`manifest.json`), and the read/write helpers used by both
`prepareFileCommand` and `serveFileCommand`.
Lives in its own module so the format is the contract: any tool that
agrees on the envelope shape can interop with a server-keys-prepared
directory without depending on the full file-command surface. Also keeps
`files.ts` and `serve-file.ts` under the 300-line hard limit by lifting
out the duplicated serialisation code.
## Features
- `packetFilename(index)` — zero-padded 6-digit filename for
deterministic sort order.
- `bytesToHex` / `hexToBytes` — lowercase hex, strict input validation.
- `serializePacket(packed, originalSize)` — JSON-encode a `PackedData`
envelope.
- `deserializePacket(buffer)` — parse a JSON envelope back into the
shape `Unpacker.unpack` expects, with a typed `PACKET_INTEGRITY_FAILED`
failure on malformed JSON.
- `readManifest(dir)` / `writeManifest(dir, manifest)` — single-file
JSON manifest I/O with directory creation.
## Usage: Basic
```ts
import {
packetFilename,
serializePacket,
writeManifest,
} from "@suaveplan/server-keys/commands/files/packet-io";
const filename = packetFilename(7); // "packet.000007.bin"
const text = serializePacket(packed, 1024);
await fs.writeFile(`/tmp/prepared/${filename}`, text);
await writeManifest("/tmp/prepared", manifest);
```
## Usage: Advanced
Round-trip a packet envelope without touching disk:
```ts
import {
deserializePacket,
serializePacket,
} from "@suaveplan/server-keys/commands/files/packet-io";
const text = serializePacket(packed, slice.byteLength);
const buf = new TextEncoder().encode(text);
const { packed: rehydrated, envelope } = deserializePacket(buf);
expect(envelope.checksum).toBe(packed.metadata.checksum);
expect(Array.from(rehydrated.data)).toEqual(Array.from(packed.data));
```
## API Reference
- `MANIFEST_FILENAME = "manifest.json"`
- `packetFilename(index: number) => string`
- `bytesToHex(bytes: Uint8Array) => string`
- `hexToBytes(hex: string) => Uint8Array` (throws on malformed input)
- `interface SerializedPacket { data, signature, checksum, originalSize }`
- `serializePacket(packed, originalSize) => string`
- `deserializePacket(buffer) => { packed, envelope }`
- `writeManifest(dir, manifest) => Promise<void>`
- `readManifest(dir) => Promise<FileManifest>`
## Implementation Notes
`readManifest` translates a missing file into
`SERVER_KEYS_FILE_NOT_FOUND`; this is the closest match in the
registry for "the directory exists but the manifest entry is absent"
and aligns with how the keys commands report missing key files.
`deserializePacket` raises `PACKET_INTEGRITY_FAILED` on malformed JSON
rather than `INVALID_INPUT` because the input is a file the caller
already trusted enough to read — the failure is on the storage side,
not on a CLI option-bag boundary. The packet envelope deliberately
hex-encodes both `data` and `signature` (rather than base64) to match
the rest of the package's binary-on-disk convention and to keep the
file readable by hand.

View File

@@ -0,0 +1,87 @@
# `commands/files/serve-file`
## Purpose
Reconstructs the original file from a directory of prepared packets by
verifying each packet's ed25519 signature + sha256 checksum against the
manifest and concatenating the verified plaintext slices in index order.
The reconstruction is byte-identical to the input file passed to
`prepareFileCommand`. Failure to verify any packet aborts the operation
with `ServerKeysError(SERVER_KEYS_PACKET_INTEGRITY_FAILED)` BEFORE any
bytes are written to disk, so the output path is never half-written.
Lives in a sibling file to `files.ts` because the reconstruction path
has a distinctly different shape (in-memory contiguous buffer, strict
ordering, single output write) compared to the parallel packet-level
work the rest of the file commands do.
## Features
- Strict index ordering: refuses to serve if any packet's `index` does
not match its position in the sorted manifest.
- Signature + checksum verification per packet via `Unpacker.unpack`
with `verifySignature: true, verifyKey, verifyChecksum: true`.
- Single allocation for the reconstructed buffer (`new
Uint8Array(manifest.originalSize)`), populated in place — bounds the
serve-path memory at exactly the original file size.
- Cooperative abort via `options.signal`.
- Output defaults to `<packetDir>/<originalFilename>`; override with
`options.output`.
## Usage: Basic
```ts
import { serveFileCommand } from "@suaveplan/server-keys/commands/files/serve-file";
const result = await serveFileCommand("/tmp/prepared", {
output: "/tmp/reconstructed.bin",
});
expect(result.bytesWritten).toBe(originalSize);
expect(result.packetsRead).toBe(totalPackets);
```
## Usage: Advanced
Drive the reconstruction with a capturing logger and an external abort
signal, then compare the result byte-for-byte to the original:
```ts
import { promises as fs } from "node:fs";
import { serveFileCommand } from "@suaveplan/server-keys/commands/files/serve-file";
const lines: string[] = [];
const ac = new AbortController();
setTimeout(() => ac.abort(), 5000);
const result = await serveFileCommand("/tmp/prepared", {
output: "/tmp/out.bin",
logger: { info: (m) => lines.push(m), error: (m) => lines.push(m) },
signal: ac.signal,
});
const original = await fs.readFile("/tmp/data.bin");
const reconstructed = await fs.readFile(result.outputPath);
expect(Buffer.compare(original, reconstructed)).toBe(0);
```
## API Reference
- `serveFileCommand(packetDir, options?) => Promise<ServeFileResult>`
- `interface ServeFileResult { outputPath, bytesWritten, packetsRead,
fileId }`
`options: FileCommandOptions = { output?, logger?, signal? }`.
## Implementation Notes
The reconstruction buffer is sized to `manifest.originalSize` and we
assert `writeOffset === manifest.originalSize` at the end so a
manifest-vs-actual mismatch is caught as `PACKET_INTEGRITY_FAILED`
instead of silently truncating. We sort the manifest's packet records
before iterating because the JSON entries may have been written in any
order; sorting once and iterating in index order is O(n log n) for n
packets, which is dominated by the per-packet AEAD work anyway.
Verification is requested with all three checks enabled (`signature`,
`checksum`, and length) because the contract is "byte-identical
reconstruction" — any weakening would let a tampered packet through
with the same number of bytes.

View File

@@ -0,0 +1,115 @@
# `commands/keys/keys-chain`
## Purpose
Hierarchical `ChainedKeySigner`-backed key-lifecycle commands. Each
chain command wraps the matching crypto-vault orchestration in a
two-step pipeline: generate-or-derive a fresh keychain via
`@suaveplan/crypto-keys` (which produces a signed chain bytes blob), then
encrypt the keychain with `packVault` from `@suaveplan/crypto-vault`.
Persistence writes `<name>.vault` plus a `<name>.chain` file instead of
the non-chain `<name>.pub` so operators can tell at a glance which key
family a directory holds.
`verify-key-chain` rounds out the surface: it loads a chain file +
trusted root file, extracts the root's `sign` public key (whether the
root file is a signed-public-key file or another chain file), and
returns a structured `{ valid, depth, errors }` result without
throwing on a signature failure.
## Features
- Chain command parity with the non-chain commands: `create-root`,
`create-server`, `roll-server`, all producing matching vault + chain
artefacts.
- `verifyKeyChainCommand` accepts both signed-public-key files and
chain files as the trusted-root input — automatic fallback.
- Same `KeyCommandOptions` shape as `keys.ts`: output dir, logger,
abort signal.
- `parseChain` helper for non-verifying introspection (depth count).
## Usage: Basic
```ts
import {
createRootKeysChainCommand,
createServerKeysChainCommand,
verifyKeyChainCommand,
} from "@suaveplan/server-keys/commands/keys/keys-chain";
await createRootKeysChainCommand("rootpw1234", { output: "/tmp/keys" });
await createServerKeysChainCommand(
"/tmp/keys/root.vault",
"rootpw1234",
"serverpw1234",
{ output: "/tmp/keys" },
);
const result = await verifyKeyChainCommand(
"/tmp/keys/server.chain",
"/tmp/keys/root.chain",
);
expect(result.valid).toBe(true);
expect(result.depth).toBe(2);
```
## Usage: Advanced
Detect a tampered chain by flipping a byte in the chain file and
re-running the verifier; the result reports `valid: false` with a
populated `errors` array instead of throwing:
```ts
import { promises as fs } from "node:fs";
import {
rollServerKeysChainCommand,
verifyKeyChainCommand,
} from "@suaveplan/server-keys/commands/keys/keys-chain";
await rollServerKeysChainCommand(
"/tmp/keys/root.vault",
"rootpw1234",
"serverpw1234",
1,
{ output: "/tmp/keys" },
);
const chainPath = "/tmp/keys/server.v2.chain";
const original = await fs.readFile(chainPath);
const tampered = new Uint8Array(original);
tampered[tampered.length - 1] ^= 0xff;
await fs.writeFile(chainPath, tampered);
const r = await verifyKeyChainCommand(chainPath, "/tmp/keys/root.chain");
expect(r.valid).toBe(false);
expect(r.errors.length).toBeGreaterThan(0);
await fs.writeFile(chainPath, original); // restore
```
## API Reference
- `createRootKeysChainCommand(password, options?) => Promise<void>`
- `createServerKeysChainCommand(rootVaultPath, rootPassword,
serverPassword, options?) => Promise<void>`
- `rollServerKeysChainCommand(rootVaultPath, rootPassword,
serverPassword, currentVersion, options?) => Promise<void>`
- `verifyKeyChainCommand(chainPath, rootKeyPath, options?) =>
Promise<{ valid, depth, errors }>`
- `parseChain(chainBytes) => Promise<{ depth }>`
- `viewPublicKeys` re-export — for callers that need to decode a
signed-public-key file in the same import.
## Implementation Notes
The cast through
`as unknown as Parameters<typeof createServerKeysWithChain>[0]["rootKeys"]`
is the safe-narrowing shape `crypto-keys` expects — it requires a
`Keychain` typed against its own `LayoutV1Fields` import, which is
nominally distinct from crypto-vault's despite being structurally
identical. The double declaration of `LayoutV1Fields` across packages
mirrors the deliberate decoupling there; the cast keeps the boundary
explicit.
`verifyKeyChainCommand` does not re-throw on `valid: false` because the
spec scenario "tampered chain fails verification" requires a structured
result, not an exception — exceptions are reserved for input failures
(`INVALID_INPUT`, `FILE_NOT_FOUND`, missing `sign` slot).

View File

@@ -0,0 +1,116 @@
# `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<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.

View File

@@ -0,0 +1,124 @@
# `commands/keys/keys`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```ts
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:
```ts
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.

View File

@@ -0,0 +1,69 @@
# `commands/keys/keys.types`
## Purpose
Shared type definitions and filename constants for every keys command in
`@suaveplan/server-keys`. Centralising these here lets the public
commands, the chain commands, and the I/O helpers all agree on the
exact shape of `KeyCommandOptions`, the same set of filenames, and the
verifier return envelope without circular imports. Tests assert against
the same constants, so a typo in the filename convention surfaces as a
test failure rather than as silent drift between the bins and their
underlying commands.
## Features
- `KeysLogger` — `Pick<LoggerLike, "info" | "error">` from
`@suaveplan/types`; every command accepts an injected logger that
defaults to silent.
- `KeyCommandOptions` — `{ output?, logger?, signal? }`, the option
bag every public keys command takes.
- `KEY_FILE_NAMES` — frozen record of the conventional filenames
(`root.vault`, `root.pub`, `root.chain`, `server.vault`,
`server.pub`, `server.chain`).
- `VerifyKeyChainResult` — the structured envelope returned by
`verifyKeyChainCommand` (`{ valid, depth, errors }`).
## Usage: Basic
```ts
import {
KEY_FILE_NAMES,
type KeyCommandOptions,
} from "@suaveplan/server-keys/commands/keys/keys.types";
const options: KeyCommandOptions = { output: "/etc/suaveplan/keys" };
const path = `${options.output}/${KEY_FILE_NAMES.rootVault}`;
```
## Usage: Advanced
Match on the structured verifier envelope without re-implementing the
discrimination logic in every caller:
```ts
import type { VerifyKeyChainResult } from "@suaveplan/server-keys/commands/keys/keys.types";
function summariseChain(r: VerifyKeyChainResult): string {
if (r.valid) return `OK (depth=${r.depth})`;
return `INVALID (depth=${r.depth}): ${r.errors.join("; ")}`;
}
```
## API Reference
- `interface KeysLogger extends Pick<LoggerLike, "info" | "error">`
- `interface KeyCommandOptions { output?, logger?, signal? }`
- `const KEY_FILE_NAMES = Object.freeze({ rootVault, rootPublic,
rootChain, serverVault, serverPublic, serverChain })`
- `interface VerifyKeyChainResult { valid, depth, errors }`
## Implementation Notes
`KEY_FILE_NAMES` is `Object.freeze`-d so accidental mutation at runtime
throws in strict mode; this is the only mutable-looking constant in the
keys surface, and the freeze guards against bins or tests reaching in
to override a name (which would silently break the upstream-compatible
layout convention). The `KeysLogger` shape is narrower than the full
`LoggerLike` so the bins can supply a console shim with just two
methods without TypeScript complaining about missing trace/debug/warn.

View File

@@ -0,0 +1,73 @@
# `commands/license/hex`
## Purpose
Tiny shared hex encode/decode helpers used by the license sub-commands
to serialise and deserialise the secp256k1 key material carried in the
`LicenseKeypairFile` JSON envelope. Extracted from `license.ts` so the
license command module stays within the 300-line hard limit and so
tests can exercise the hex boundary failures in isolation. Every public
failure here surfaces as a `ServerKeysError(SERVER_KEYS_INVALID_INPUT)`
with a precise error message, so the top-level `runFunction` wrapper
prints actionable input diagnostics.
The choice of hex (rather than base32 or base64) mirrors upstream's JSON
key file layout — operators editing key files by hand find hex easier to
inspect, and the secp256k1 secret scalar (32 bytes) and public key (33
bytes compressed) fit comfortably in a single line each.
## Features
- `bytesToHex(bytes)` — lowercase hex, no separator, no `0x` prefix.
- `hexToBytes(hex)` — strict parser: rejects odd lengths and any
non-hex character with a positional error message.
## Usage: Basic
```ts
import { bytesToHex, hexToBytes } from "@suaveplan/server-keys/commands/license/hex";
const bytes = new Uint8Array([0xab, 0xcd, 0xef]);
const hex = bytesToHex(bytes); // "abcdef"
const round = hexToBytes(hex); // Uint8Array([0xab, 0xcd, 0xef])
```
## Usage: Advanced
Round-trip every byte with property-based testing:
```ts
import fc from "fast-check";
import { bytesToHex, hexToBytes } from "@suaveplan/server-keys/commands/license/hex";
fc.assert(
fc.property(fc.uint8Array({ minLength: 0, maxLength: 1024 }), (input) => {
const round = hexToBytes(bytesToHex(input));
expect(round).toEqual(input);
}),
);
```
Decode rejection on malformed input:
```ts
expect(() => hexToBytes("abc")).toThrow(/length must be even/);
expect(() => hexToBytes("zz")).toThrow(/Invalid hex byte "zz" at offset 0/);
```
## API Reference
- `bytesToHex(bytes: Uint8Array) => string`
- `hexToBytes(hex: string) => Uint8Array` — throws
`ServerKeysError(SERVER_KEYS_INVALID_INPUT)` on odd length or non-hex
characters.
## Implementation Notes
`bytesToHex` uses `padStart(2, "0")` instead of the `& 0xff` mask trick
because the input is already `Uint8Array` (each element is guaranteed
to be in `[0, 255]`), and the pad form keeps the per-byte intent
explicit at zero runtime cost. `hexToBytes` does not use a regex pre-
check because the per-byte `Number.parseInt(..., 16)` already enforces
the character set and provides a more useful positional error message
on failure.

View File

@@ -0,0 +1,133 @@
# `commands/license/license`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Wraps `@suaveplan/license` `LicenseManager` into the five sub-commands
the operator-facing `license` bin exposes: `keygen`, `generate`,
`validate`, `parse`, `revoke`, plus an `exportPublicKeyFile` helper that
derives a public-keys JSON from a freshly generated keypair file. Each
sub-command translates a `LicenseError` into
`ServerKeysError(SERVER_KEYS_LICENSE_FAILED)` with the original error
preserved as `cause`, so the top-level `runFunction` wrapper still
prints a stable code on failure. Input boundary errors raise
`SERVER_KEYS_INVALID_INPUT`, and missing key files raise
`SERVER_KEYS_FILE_NOT_FOUND` — all four error paths are exercised in the
spec-conformance tests.
The on-disk JSON envelopes (`LicenseKeypairFile`, `LicensePublicKeyFile`)
are deliberately minimal: hex-encoded secp256k1 secret + public, a
numeric key id, and an ISO-8601 creation timestamp. Operators can edit
them by hand without a tool. The revocation store is a JSON array of
hex license fingerprints — created on first revoke, append-only.
## Features
- `keygen(outputPath, keyId, options?)` — fresh secp256k1 keypair via
`crypto-curve`'s `secp256k1.randomPrivateKey()` + `getPublicKey()`.
- `generate(opts)` — `LicenseManager.generate` wrapper with hex-encoded
signing-key loading.
- `validate(licenseKey, opts)` — public-keys JSON loader, supports
either an object or an array layout.
- `parse(licenseKey, options?)` — synchronous, no signature check.
- `revoke(licenseKey, opts)` — appends to a JSON revocation-store
file; creates the file if absent.
- `exportPublicKeyFile(keypairFile, outputPath)` — derive the public-
keys file from a freshly generated keypair (used by the bin's
`--export-pub <path>` flag and by tests).
## Usage: Basic
```ts
import {
keygen,
generate,
validate,
parse,
revoke,
exportPublicKeyFile,
} from "@suaveplan/server-keys/commands/license/license";
import { PackageTier } from "@suaveplan/license";
const kp = await keygen("/tmp/lic/keys.json", 1);
await exportPublicKeyFile(kp, "/tmp/lic/public-keys.json");
const lic = await generate({
signingKeyPath: "/tmp/lic/keys.json",
productId: 42,
tier: PackageTier.PRO,
expirationDays: 365,
});
const valid = await validate(lic.key, {
publicKeysPath: "/tmp/lic/public-keys.json",
});
expect(valid.success).toBe(true);
const parsed = parse(lic.key);
expect(parsed.payload.productId).toBe(42);
await revoke(lic.key, { revocationStorePath: "/tmp/lic/revocations.json" });
const after = await validate(lic.key, {
publicKeysPath: "/tmp/lic/public-keys.json",
});
// `revoke` modifies the in-memory store only inside the same Manager;
// the file-backed store is observed by the next caller that constructs
// a Manager with that store path (see bin/license.ts for the wiring).
```
## Usage: Advanced
Flag a license as a TRIAL with a feature mask, then validate against a
fixed reference date for deterministic expiration windows:
```ts
import { generate, validate } from "@suaveplan/server-keys/commands/license/license";
import { LicenseFlags, PackageTier } from "@suaveplan/license";
const lic = await generate({
signingKeyPath: "/tmp/lic/keys.json",
productId: 1,
tier: PackageTier.BASIC,
expirationDays: 30,
flags: LicenseFlags.TRIAL,
featureMask: 0b1011, // features 0, 1, 3
useAbsoluteTime: false,
useFullSignature: true,
});
const checked = await validate(lic.key, {
publicKeysPath: "/tmp/lic/public-keys.json",
referenceDate: new Date("2026-01-01T00:00:00Z"),
});
```
## API Reference
- `keygen(outputPath, keyId, options?) => Promise<LicenseKeypairFile>`
- `generate(opts: GenerateLicenseOptions) =>
Promise<LicenseGenerationResult>`
- `validate(licenseKey, opts: ValidateLicenseOptions) =>
Promise<LicenseValidationResult>`
- `parse(licenseKey, options?: LicenseCommandOptions) =>
LicenseParsedResult`
- `revoke(licenseKey, opts: RevokeLicenseOptions) => Promise<string>`
- `exportPublicKeyFile(keypairFile, outputPath) =>
Promise<LicensePublicKeyFile>`
All option bags accept `{ logger?, signal? }` for injection.
## Implementation Notes
`generate` constructs a `LicenseManager` per call (instead of caching
one across sub-commands) because each call's signing key may be
different — the bin invocation is the granularity that owns a key. The
revocation store loader tolerates `ENOENT` (no file yet) but treats any
other I/O error as a `LICENSE_FAILED` failure so operators don't
silently revoke into a misconfigured path. The optional `signal`
parameter is checked at the boundary of each sub-command and at the
start of generate/validate so an aborted run never burns CPU on the
LicenseManager's hash + signature work.

View File

@@ -0,0 +1,98 @@
# `commands/license/license.types`
## Purpose
Type definitions for every license sub-command's options bag and for
the JSON file envelopes the bin reads and writes. Centralising these
here keeps the sub-command functions in `license.ts` focused on
behaviour and makes the boundary contract explicit — both for
operators editing the JSON files by hand and for downstream programmatic
consumers that want to write a typed wrapper around the bin's surface.
The file envelopes mirror the upstream layout exactly so operator
scripts that read or write these files continue to work without
modification.
## Features
- `LicenseLogger` — `Pick<LoggerLike, "info" | "error">`; injected.
- `LicenseCommandOptions` — shared `{ logger?, signal? }` base.
- `LicenseKeypairFile` — `{ keyId, signingKey (hex), publicKey (hex),
created (ISO 8601) }`. Persisted by `keygen`.
- `LicensePublicKeyFile` — `{ keyId, publicKey (hex) }`. Persisted by
`exportPublicKeyFile`; read by `validate`.
- `GenerateLicenseOptions`, `ValidateLicenseOptions`,
`RevokeLicenseOptions` — typed per-sub-command bags.
## Usage: Basic
```ts
import type {
LicenseKeypairFile,
GenerateLicenseOptions,
} from "@suaveplan/server-keys/commands/license/license.types";
import { PackageTier } from "@suaveplan/license";
const opts: GenerateLicenseOptions = {
signingKeyPath: "/tmp/keys.json",
productId: 42,
tier: PackageTier.PRO,
expirationDays: 365,
};
const file: LicenseKeypairFile = JSON.parse(
await fs.readFile("/tmp/keys.json", "utf-8"),
);
```
## Usage: Advanced
Construct a typed orchestration script that emits a generation request,
validates against a separately stored public-key file, and writes the
revocation back to a third path:
```ts
import type {
GenerateLicenseOptions,
ValidateLicenseOptions,
RevokeLicenseOptions,
} from "@suaveplan/server-keys/commands/license/license.types";
interface LicenseFlow {
generate: GenerateLicenseOptions;
validate: ValidateLicenseOptions;
revoke: RevokeLicenseOptions;
}
const flow: LicenseFlow = {
generate: {
signingKeyPath: "/etc/lic/keys.json",
productId: 1,
tier: 2,
expirationDays: 730,
},
validate: { publicKeysPath: "/etc/lic/public-keys.json" },
revoke: { revocationStorePath: "/var/lib/lic/revocations.json" },
};
```
## API Reference
- `interface LicenseLogger`
- `interface LicenseCommandOptions`
- `interface LicenseKeypairFile`
- `interface LicensePublicKeyFile`
- `interface GenerateLicenseOptions extends LicenseCommandOptions`
- `interface ValidateLicenseOptions extends LicenseCommandOptions`
- `interface RevokeLicenseOptions extends LicenseCommandOptions`
## Implementation Notes
All fields on `LicenseKeypairFile` and `LicensePublicKeyFile` are
`readonly` so accidental mutation after a JSON parse is a TS error.
`LicenseValidationOptions.publicKeysPath` is named verbosely (instead
of `publicKeyPath`) because the file MAY hold many keyed entries —
either as a single object or as an array — and the plural is more
honest about the multi-key layout. `ValidateLicenseOptions.referenceDate`
is `Date | undefined`; the bin's `--reference-date <iso>` parses to a
`Date` and forwards.

View File

@@ -0,0 +1,94 @@
# `error-codes`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the stable `SERVER_KEYS_*` error-code registry used throughout
`@suaveplan/server-keys`. Each code is a namespaced string registered via
`defineCodes("SERVER_KEYS", ...)` from `@suaveplan/error`, so the global
error registry is aware of every code the package may throw the moment
this module is imported. Every public failure path in the package — vault
decrypt, password mismatch, packet integrity, chain verify, license
delegation, CLI input validation, cooperative abort — raises a
`ServerKeysError` with one of these codes, never a bare `Error`.
The registry is part of the package contract: tests assert that every
declared code is raised from at least one production call site, and the
spec-conformance test enumerates the registry to keep it from drifting.
## Features
- 10 stable codes covering vault, packet, license, input, and abort paths.
- `ServerKeysError extends SuaveplanError<ServerKeysErrorCode>` so
`instanceof` narrows the `.code` field to the registry's literal union.
- Side-effect registration with the global `@suaveplan/error` table — any
consumer that imports `@suaveplan/server-keys/error-codes/error-codes`
picks up the codes globally.
- `serverKeysCode` accessor mirrors `code` for ergonomic narrowing.
## Usage: Basic
```ts
import {
SERVER_KEYS_CODES,
ServerKeysError,
} from "@suaveplan/server-keys/error-codes/error-codes";
throw new ServerKeysError(
SERVER_KEYS_CODES.VAULT_NOT_FOUND,
"Vault file does not exist at /etc/suaveplan/root.vault",
);
```
## Usage: Advanced
Chain a cause through the constructor and dispatch on the code in a
top-level handler:
```ts
import {
SERVER_KEYS_CODES,
ServerKeysError,
type ServerKeysErrorCode,
} from "@suaveplan/server-keys/error-codes/error-codes";
try {
await prepareFileCommand(...);
} catch (err) {
if (err instanceof ServerKeysError) {
const code: ServerKeysErrorCode = err.code;
if (code === SERVER_KEYS_CODES.PACKET_INTEGRITY_FAILED) {
// re-pack with a different signing key
}
}
throw err;
}
// Re-throw with cause chain preserved
throw new ServerKeysError(
SERVER_KEYS_CODES.VAULT_DECRYPT_FAILED,
"Failed to decrypt root vault",
{ cause: underlying, context: { vaultPath: "/etc/root.vault" } },
);
```
## API Reference
- `SERVER_KEYS_CODES` — frozen record of code constants.
- `ServerKeysErrorCode` — union of the registry's string-literal values.
- `class ServerKeysError extends SuaveplanError<ServerKeysErrorCode>` —
concrete error class with `.code`, `.serverKeysCode`, `.cause`,
`.context` (inherited from `SuaveplanError`).
## Implementation Notes
`defineCodes` returns a frozen object whose keys map to the namespaced
string codes (e.g. `SERVER_KEYS_CODES.VAULT_NOT_FOUND === "SERVER_KEYS.VAULT_NOT_FOUND"`).
The cast through `SuaveplanError<ServerKeysErrorCode>` keeps the literal
narrowing intact across error-catch boundaries. The class deliberately
does not introduce extra behavior beyond the discriminator field — error
shaping, redaction, and OTel span-event adapters live in
`@suaveplan/error`.

View File

@@ -0,0 +1,89 @@
# `otel/instruments`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Registers the OpenTelemetry counter and histogram that every CLI command
in `@suaveplan/server-keys` emits exactly once per invocation through the
`runFunction` wrapper. The instruments live behind the global meter
provider supplied by `@opentelemetry/api`, so production deployments can
plug in any exporter (OTLP, Prometheus, Jaeger) without changing the
package. Tests wire an in-memory `MeterProvider` in `tests/setup/polyfills.ts`
before any source module is imported, so the meter resolved at load time
is the real provider's meter, never the noop fallback.
The `recordCommand(command, outcome, durationMs, errorCode?)` helper is the
single call site every command goes through — keeping the attribute keys
consistent and the histogram unit (`ms`) authoritative.
## Features
- Counter `server_keys.command_total{command, command.outcome[, command.error_code]}`.
- Histogram `server_keys.command_duration_ms{command, command.outcome[, command.error_code]}`
with `unit: "ms"`.
- Stable attribute-key constants (`ATTR_COMMAND`, `ATTR_OUTCOME`,
`ATTR_ERROR_CODE`) so tests can assert by name.
- `recordCommand` records both instruments atomically so dashboards built
on the counter and the histogram never disagree.
## Usage: Basic
```ts
import { recordCommand } from "@suaveplan/server-keys/otel/instruments";
const start = performance.now();
try {
await doWork();
recordCommand("create-root-keys", "success", performance.now() - start);
} catch (err) {
recordCommand("create-root-keys", "error", performance.now() - start);
throw err;
}
```
## Usage: Advanced
Attach the typed error code when failing, so dashboards can break down
failures by reason without scraping log lines:
```ts
import { recordCommand } from "@suaveplan/server-keys/otel/instruments";
import { ServerKeysError } from "@suaveplan/server-keys/error-codes/error-codes";
try {
await prepareFileCommand(...);
recordCommand("prepare-file", "success", elapsed);
} catch (err) {
const code = err instanceof ServerKeysError ? err.code : "UNKNOWN";
recordCommand("prepare-file", "error", elapsed, code);
throw err;
}
```
Or query the in-memory test exporter to assert on emitted metrics:
```ts
await flushTestMetrics(); // from tests/setup/polyfills.ts
```
## API Reference
- `commandTotal: Counter` — total CLI commands executed.
- `commandDurationMs: Histogram` — per-command duration (ms).
- `ATTR_COMMAND`, `ATTR_OUTCOME`, `ATTR_ERROR_CODE` — string-literal
attribute keys.
- `CommandOutcome = "success" | "error"`.
- `recordCommand(command, outcome, durationMs, errorCode?)` — record one
observation on both instruments.
## Implementation Notes
`metrics.getMeter(name, version)` returns the meter from the current
global provider; if no provider is registered, the API package returns a
noop meter so production code never throws on missing telemetry. Setting
the meter provider AFTER `instruments.ts` is imported has no effect on
this module's meter handle, so the test polyfill must register the
provider before any `import` that transitively loads `instruments.ts`.

View File

@@ -0,0 +1,108 @@
# `prompt`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Thin, test-injectable wrappers around `@inquirer/prompts`' password prompt
for the two interactive flows every server-keys bin needs: a single
silent password entry (`promptForPassword`) and a confirm-twice new
password entry (`promptForNewPassword`). The wrappers add three things
upstream lacks: a structured `ServerKeysError(SERVER_KEYS_PASSWORD_MISMATCH)`
on mismatch (instead of a recursive re-prompt that bypasses the bin's
top-level handler), a typed dependency-injection seam for the prompt
function (so tests can run without touching stdin/stdout), and explicit
threading of `input/output/signal` into inquirer's `context` so the
prompts compose with the bin's `AbortController` and with non-TTY test
runners.
The minimum password length is exported as `MIN_PASSWORD_LENGTH` so callers
and tests share the constant.
## Features
- `mask: "*"` non-echo entry — passwords never appear in the buffered
terminal.
- `validate: (input) => ...` rejects empty input and (on new-password)
enforces a minimum length, surfacing the message through inquirer's
re-prompt loop.
- `SERVER_KEYS.PASSWORD_MISMATCH` raised on confirmation mismatch — the
caller's `runFunction` wrapper translates it into a process-exit
banner.
- Injectable `passwordPrompt` for fully deterministic tests; injectable
`inputStream`, `outputStream`, and `signal` for partial stubs that
still drive the real inquirer prompt under a non-TTY harness.
## Usage: Basic
```ts
import {
promptForPassword,
promptForNewPassword,
} from "@suaveplan/server-keys/prompt/prompt";
const existing = await promptForPassword("Enter root vault password");
const fresh = await promptForNewPassword("Enter password for the new vault");
```
## Usage: Advanced
Inject a fake prompt for tests, then exercise both happy and mismatch
paths without any TTY:
```ts
import { PassThrough } from "node:stream";
import {
promptForNewPassword,
type PasswordPrompt,
} from "@suaveplan/server-keys/prompt/prompt";
const calls: string[] = [];
const responses = ["correcthorse", "correcthorse"];
const fake: PasswordPrompt = async (cfg) => {
calls.push(cfg.message);
return responses.shift() as string;
};
const pw = await promptForNewPassword("Enter password", {
passwordPrompt: fake,
});
expect(pw).toBe("correcthorse");
expect(calls).toEqual(["Enter password", "Re-enter password to confirm"]);
// Mismatch raises a typed error:
await expect(
promptForNewPassword("x", {
passwordPrompt: async () =>
(["abcdefgh", "ABCDEFGH"].shift() ?? "") as string,
}),
).rejects.toMatchObject({ code: "SERVER_KEYS.PASSWORD_MISMATCH" });
// Production with a SIGINT-bound AbortController:
const ac = new AbortController();
process.once("SIGINT", () => ac.abort());
const pw2 = await promptForPassword("Enter vault password", {
signal: ac.signal,
});
```
## API Reference
- `MIN_PASSWORD_LENGTH = 8` — new-password length floor.
- `PasswordPrompt` — function type matching `@inquirer/prompts.password`.
- `PromptOptions = { passwordPrompt?, inputStream?, outputStream?, signal? }`.
- `promptForPassword(message?, options?) => Promise<string>`.
- `promptForNewPassword(message?, options?) => Promise<string>` — throws
`ServerKeysError(SERVER_KEYS_PASSWORD_MISMATCH)` on mismatch.
## Implementation Notes
The validators are kept in-prompt (instead of after the await) so
inquirer's re-prompt UX still works for empty / too-short input. The
mismatch check lives outside the prompt so the failure can be caught by
the top-level handler instead of trapping the user in an infinite re-ask
loop. `buildContext` only assigns keys that were actually provided so
inquirer's defaults apply for the rest, and returns `undefined` for the
empty case so the prompt can resolve its own stdin / stdout / signal
defaults.

View File

@@ -0,0 +1,106 @@
# `run-function`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`runFunction` is the single wrapper every server-keys CLI bin uses for its
commander `.action(...)` body. It standardises three concerns the upstream
package handled inconsistently: structured error reporting (always a
`ServerKeysError` with a stable code, never a bare `Error`), OpenTelemetry
emission (one counter increment + one histogram observation per call,
attribute `command.outcome` set), and process-exit semantics (`exit(1)` on
any failure, after the banner is printed). It also threads an optional
`AbortSignal` through to the body so long-running commands can co-operate
with `SIGINT` without changing every command signature.
The wrapper accepts an injectable `logger` and `exit` so tests can run
against an in-memory logger and assert on the exit code without
terminating the runner. Production bins call it with default options.
## Features
- Times the body with `performance.now()` and records both OTel
instruments via `recordCommand`.
- Normalises any thrown failure into a `ServerKeysError`; wraps non-
`Error` throws with `cause` preserved when possible.
- Pre-checks `signal.aborted` and short-circuits with
`SERVER_KEYS.ABORTED` before the body runs.
- Prints a single, structured stderr banner per failure (`[command] CODE:
message`).
- Returns the body's resolved value on success for non-bin callers (test
harnesses).
## Usage: Basic
```ts
import { Command } from "commander";
import { runFunction } from "@suaveplan/server-keys/run-function";
import { createRootKeysCommand } from "@suaveplan/server-keys/commands/keys/keys";
import { promptForNewPassword } from "@suaveplan/server-keys/prompt/prompt";
new Command()
.name("create-root-keys")
.option("-o, --output <dir>", "Output directory", "./keys")
.action((options) =>
runFunction("create-root-keys", async (signal) => {
const password = await promptForNewPassword("Enter vault password");
await createRootKeysCommand(password, {
output: options.output,
signal,
});
}),
)
.parse();
```
## Usage: Advanced
Inject a custom logger and a non-terminating exit for tests, and bind a
real `AbortController` to `SIGINT` in production:
```ts
const calls: string[] = [];
const fakeLogger = {
info: (m: string) => calls.push(`I:${m}`),
error: (m: string) => calls.push(`E:${m}`),
};
let exitCode: number | undefined;
await runFunction("test-cmd", async () => {
throw new ServerKeysError(SERVER_KEYS_CODES.VAULT_NOT_FOUND, "missing");
}, {
logger: fakeLogger,
exit: (c) => {
exitCode = c;
},
}).catch(() => undefined);
expect(exitCode).toBe(1);
expect(calls[0]).toMatch(/^E:\[test-cmd] SERVER_KEYS\.VAULT_NOT_FOUND/);
// Production SIGINT plumbing:
const ac = new AbortController();
process.once("SIGINT", () => ac.abort());
await runFunction("prepare-file", (signal) =>
prepareFileCommand(..., { signal }),
{ signal: ac.signal },
);
```
## API Reference
- `RunFunctionLogger = Pick<LoggerLike, "info" | "error">`
- `RunFunctionOptions = { logger?, exit?, signal? }`
- `runFunction<T>(command, body, options?) => Promise<T>` — the wrapper.
## Implementation Notes
`performance.now()` measures wall-clock duration in milliseconds with
sub-millisecond resolution, matching the histogram's `unit: "ms"`. The
function re-throws the wrapped error AFTER calling `exit(1)` so tests
that pass a non-terminating `exit` can `.catch(...)` the promise; in
production `exit` never returns, so the re-throw is dead code that keeps
the type signature honest. The pre-flight `signal.aborted` check uses the
same code path as the OTel + logger + exit flow so the test surface is
identical on the abort branch.