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