Files
2026-05-27 23:29:53 +01:00

6.3 KiB

@suaveplan/api-secrets-vault — SecretsLike adapter backed by @suaveplan/vault. The vault is passed in unlocked; the adapter never sees the master key. Per-plugin allow-list enforcement, vault secret.updated → host bus secret.changed forwarding, OTel api.secrets.reads_total{backend="vault"}, idempotent dispose. Real vault instance, no mocks.

Stage-8 SecretsLike adapter backed by @suaveplan/vault. The vault is passed in already unlocked by the host's startup sequence; the adapter never sees the master key. Per-plugin allow-list enforcement, vault secret.updated → host event-bus secret.changed forwarding, OTel api.secrets.reads_total{backend="vault"} counter, idempotent dispose. Real vault instance, no mocks.

Documentation tree

  • adapter — Stage-8 concrete SecretsLike implementation backed by a VaultSecretsBackend (real @suaveplan/vault.Vault via `fro…
    • adapter — Stage-8 concrete SecretsLike implementation backed by a VaultSecretsBackend (real @suaveplan/vault.Vault via `fro…
  • allowlist — Compiled per-plugin allow-list of secret names. The vault-backed adapter calls assertAllowed BEFORE consulting the ba…
    • allowlist — Compiled per-plugin allow-list of secret names. The vault-backed adapter calls assertAllowed BEFORE consulting the ba…
  • error-codes — Frozen registry of SECRETS_VAULT.* error codes thrown by the vault-backed SecretsLike adapter and its watcher / all…
    • error-codes — Frozen registry of SECRETS_VAULT.* error codes thrown by the vault-backed SecretsLike adapter and its watcher / all…
  • from-vault — Bridge a live @suaveplan/vault.Vault instance to the structural VaultSecretsBackend the adapter consumes. The bridg…
    • from-vault — Bridge a live @suaveplan/vault.Vault instance to the structural VaultSecretsBackend the adapter consumes. The bridg…
  • otel — OpenTelemetry instrument bundle for the vault-backed secrets adapter. Owns the three counters the package emits and a l…
    • instruments — OpenTelemetry instrument bundle for the vault-backed secrets adapter. Owns the three counters the package emits and a l…
  • types — Structural type contracts the vault-backed secrets adapter consumes. Decoupling the adapter from the concrete `@suavepl…
    • types — Structural type contracts the vault-backed secrets adapter consumes. Decoupling the adapter from the concrete `@suavepl…
  • watcher — Live subscription that bridges the vault's secret.updated events onto the host event bus as secret.changed. Plugins…
    • watcher — Live subscription that bridges the vault's secret.updated events onto the host event bus as secret.changed. Plugins…

Modules

  • ./adapter — createVaultSecretsAdapter({ vault, allowlist }) factory. Returns a VaultSecretsAdapter (extends SecretsLike) enforcing the per-plugin allow-list BEFORE every read, raising SECRETS_VAULT.VAULT_LOCKED on a locked vault without plaintext fallback, wrapping unexpected backend throws as SECRETS_VAULT.VAULT_UNAVAILABLE, emitting api.secrets.reads_total{backend="vault"} on success.
  • ./allowlist — createAllowlist({ names }). Zod-validated factory returning a frozen Allowlist (isAllowed, assertAllowed, names). Compiled into an O(1) Set for the hot read path. Shared between the adapter and the watcher.
  • ./watcher — createSecretsVaultWatcher({ vault, allowlist, bus }). Subscribes once to the vault's secret.updated channel, forwards allow-listed updates onto the host bus as secret.changed carrying ONLY { name } (value is never broadcast). Idempotent dispose() with an in-listener disposed-flag guard that survives lazy backend unsubscribes.
  • ./error-codes — SECRETS_VAULT_CODES registry: NOT_FOUND, NOT_ALLOWED, VAULT_LOCKED, VAULT_UNAVAILABLE, WRITE_FAILED, INVALID_OPTIONS. SecretsVaultError subclass with stable error.name === "SecretsVaultError".
  • ./otel — getSecretsVaultInstruments() / resetSecretsVaultInstruments(). Counters: api.secrets.reads_total{backend}, api.secrets.writes_total{op}, api.secrets.watcher_emits_total.
  • ./types — VaultSecretsBackend, VaultSecretListener, VaultUnsubscribe, SecretChangedPayload. Structural surface the adapter consumes — decoupled from @suaveplan/vault.Vault so tests can wire deterministic backends.
  • ./from-vault — fromVault(vault) bridges a live @suaveplan/vault.Vault instance to the structural backend. Secrets are stored as items with category === "secret" and a single concealed field "value" so the underlying field-encryption layer covers them at rest. In-bridge secret.updated fan-out fires on every mutation that goes through the bridge.

Quickstart

import {
    createSecretsVaultWatcher,
    createVaultSecretsAdapter,
    fromVault,
} from "@suaveplan/api-secrets-vault";
import { EventBus } from "@suaveplan/events";
import { Vault } from "@suaveplan/vault";

const vault = Vault.open(vaultConfig);
await vault.unlock(password, secretKey);

const backend = fromVault(vault);
const adapter = createVaultSecretsAdapter({
    vault: backend,
    allowlist: ["STRIPE_SECRET_KEY", "JWT_SIGNING_KEY"],
});

const stripeKey = await adapter.require("STRIPE_SECRET_KEY");

const hostBus = new EventBus<{ "secret.changed": { name: string } }>();
const watcher = createSecretsVaultWatcher({
    vault: backend,
    allowlist: adapter.allowlist,
    bus: hostBus,
});
hostBus.on("secret.changed", (p) => {
    console.log(`refresh ${p.name}`);
});

Conformance

Passes runSecretsConformance from @suaveplan/api-secrets/testing — see src/adapter/adapter.spec-conformance.test.ts. The tests/integration.test.ts suite drives a real @suaveplan/vault.Vault end-to-end with Argon2id parameters set to the minimum viable values so the lifecycle fits inside vitest's 60s timeout. No mocks.