# `adapter` _(no submodules)_ ## Purpose Stage-8 concrete `SecretsLike` implementation backed by a `VaultSecretsBackend` (real `@suaveplan/vault.Vault` via `fromVault`, or any backend honouring the structural contract). Suitable for multi-host / cloud deployments that need an encrypted, replicated container with a controlled unlock flow alongside per-plugin scoping. The vault is passed in unlocked by the host startup sequence; the adapter never sees the master key, mirrors the env adapter's allow- list semantics, and emits the canonical `api.secrets.reads_total{backend="vault"}` counter on every successful read. The adapter is structurally a `SecretsLike` from `@suaveplan/api-types` and passes the `runSecretsConformance` harness from `@suaveplan/api-secrets/testing` unchanged. ## Features - `createVaultSecretsAdapter({ vault, allowlist }): VaultSecretsAdapter` — factory returning a frozen-shaped `SecretsLike` plus an idempotent `dispose()`. - Per-plugin allow-list enforced BEFORE the vault is touched — rejections throw `SECRETS_VAULT.NOT_ALLOWED`. - Locked-vault detection: every `get` / `require` checks `vault.isLocked` and raises `SECRETS_VAULT.VAULT_LOCKED` with no plaintext fallback. - Backend error wrapping: an unexpected throw from the backend surfaces as `SECRETS_VAULT.VAULT_UNAVAILABLE` with the original error attached on `cause`. - `require` raises `SECRETS_VAULT.NOT_FOUND` for permitted-but- absent names; `get` resolves to `undefined`. - OTel `api.secrets.reads_total{backend="vault"}` counter increments on every successful resolution (both `get` and `require`). - Zod-validated options; `SECRETS_VAULT.INVALID_OPTIONS` on failure. ## Usage: Basic ```ts import { createVaultSecretsAdapter, fromVault } from "@suaveplan/api-secrets-vault"; import { Vault } from "@suaveplan/vault"; const vault = Vault.open(vaultConfig); await vault.unlock(password, secretKey); const secrets = createVaultSecretsAdapter({ vault: fromVault(vault), allowlist: ["STRIPE_SECRET_KEY", "JWT_SIGNING_KEY"], }); const stripeKey = await secrets.get("STRIPE_SECRET_KEY"); const jwt = await secrets.require("JWT_SIGNING_KEY"); ``` ## Usage: Advanced Wire a deterministic backend for tests (no vault unlock required): ```ts import { createVaultSecretsAdapter } from "@suaveplan/api-secrets-vault"; import type { VaultSecretsBackend } from "@suaveplan/api-secrets-vault/types"; function makeFixtureBackend(seed: Record): VaultSecretsBackend { const store = new Map(Object.entries(seed)); const listeners = new Set<(p: { name: string }) => void>(); return { get isLocked() { return false; }, async get(name) { return store.get(name); }, async set(name, value) { store.set(name, value); for (const l of listeners) l({ name }); }, async delete(name) { store.delete(name); }, async listNames() { return [...store.keys()]; }, onSecretUpdated(l) { listeners.add(l); return () => listeners.delete(l); }, }; } const adapter = createVaultSecretsAdapter({ vault: makeFixtureBackend({ STRIPE: "sk_test_123" }), allowlist: ["STRIPE"], }); ``` ## API Reference - `interface CreateVaultSecretsAdapterOptions` - `vault: VaultSecretsBackend` — pre-unlocked. - `allowlist: readonly string[]` - `interface VaultSecretsAdapter extends SecretsLike` - `readonly allowlist: Allowlist` - `dispose(): void` — idempotent - `createVaultSecretsAdapter(options): VaultSecretsAdapter` - Throws `SECRETS_VAULT.INVALID_OPTIONS` on bad options. - Returned `SecretsLike`: - `get(name): Promise` - `require(name): Promise` - `list(): readonly string[]` ## Implementation Notes The allow-list check ordering matters — running it BEFORE the locked-state check means a non-permitted name on a locked vault raises `NOT_ALLOWED`, not `VAULT_LOCKED`. This is deliberate: the allow-list rejection is the canonical "you cannot ask about this name" answer regardless of vault state, and the alternative would leak liveness information about the vault to plugins that aren't allowed to read from it. Backend exceptions are wrapped in `VAULT_UNAVAILABLE` only if they are not already a `SecretsVaultError` — a backend that throws our own typed error (e.g. via the `fromVault` write-path) surfaces through unchanged so codes don't double-wrap. `dispose()` is idempotent and currently a no-op at the adapter level; the watcher owns the live subscription. Kept on the surface so capability-adapter lifecycles look uniform across the family.