first commit
This commit is contained in:
137
docs/api-stack/api/api-secrets-vault/src/adapter/adapter.md
Normal file
137
docs/api-stack/api/api-secrets-vault/src/adapter/adapter.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# `adapter`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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<string, string>): 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<string | undefined>`
|
||||
- `require(name): Promise<string>`
|
||||
- `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.
|
||||
Reference in New Issue
Block a user