109 lines
6.3 KiB
Markdown
109 lines
6.3 KiB
Markdown
# @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.
|
|
|
|
<!-- begin:auto-docs -->
|
|
## 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…
|
|
<!-- end:auto-docs -->
|
|
|
|
## 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.
|
|
|