first commit
This commit is contained in:
195
docs/security-stack/security/server-keys/README.md
Normal file
195
docs/security-stack/security/server-keys/README.md
Normal 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.
|
||||
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
57
docs/security-stack/security/server-keys/src/bin/index.md
Normal file
57
docs/security-stack/security/server-keys/src/bin/index.md
Normal 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`.
|
||||
57
docs/security-stack/security/server-keys/src/bin/license.md
Normal file
57
docs/security-stack/security/server-keys/src/bin/license.md
Normal 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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
108
docs/security-stack/security/server-keys/src/prompt/prompt.md
Normal file
108
docs/security-stack/security/server-keys/src/prompt/prompt.md
Normal 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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user