Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.3 KiB
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
SecretsLikeimplementation backed by aVaultSecretsBackend(real@suaveplan/vault.Vaultvia `fro…- adapter — Stage-8 concrete
SecretsLikeimplementation backed by aVaultSecretsBackend(real@suaveplan/vault.Vaultvia `fro…
- adapter — Stage-8 concrete
- allowlist — Compiled per-plugin allow-list of secret names. The vault-backed adapter calls
assertAllowedBEFORE consulting the ba…- allowlist — Compiled per-plugin allow-list of secret names. The vault-backed adapter calls
assertAllowedBEFORE consulting the ba…
- allowlist — Compiled per-plugin allow-list of secret names. The vault-backed adapter calls
- error-codes — Frozen registry of
SECRETS_VAULT.*error codes thrown by the vault-backedSecretsLikeadapter and its watcher / all…- error-codes — Frozen registry of
SECRETS_VAULT.*error codes thrown by the vault-backedSecretsLikeadapter and its watcher / all…
- error-codes — Frozen registry of
- from-vault — Bridge a live
@suaveplan/vault.Vaultinstance to the structuralVaultSecretsBackendthe adapter consumes. The bridg…- from-vault — Bridge a live
@suaveplan/vault.Vaultinstance to the structuralVaultSecretsBackendthe adapter consumes. The bridg…
- from-vault — Bridge a live
- 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.updatedevents onto the host event bus assecret.changed. Plugins…- watcher — Live subscription that bridges the vault's
secret.updatedevents onto the host event bus assecret.changed. Plugins…
- watcher — Live subscription that bridges the vault's
Modules
./adapter—createVaultSecretsAdapter({ vault, allowlist })factory. Returns aVaultSecretsAdapter(extendsSecretsLike) enforcing the per-plugin allow-list BEFORE every read, raisingSECRETS_VAULT.VAULT_LOCKEDon a locked vault without plaintext fallback, wrapping unexpected backend throws asSECRETS_VAULT.VAULT_UNAVAILABLE, emittingapi.secrets.reads_total{backend="vault"}on success../allowlist—createAllowlist({ names }). Zod-validated factory returning a frozenAllowlist(isAllowed,assertAllowed,names). Compiled into an O(1)Setfor the hot read path. Shared between the adapter and the watcher../watcher—createSecretsVaultWatcher({ vault, allowlist, bus }). Subscribes once to the vault'ssecret.updatedchannel, forwards allow-listed updates onto the host bus assecret.changedcarrying ONLY{ name }(value is never broadcast). Idempotentdispose()with an in-listener disposed-flag guard that survives lazy backend unsubscribes../error-codes—SECRETS_VAULT_CODESregistry:NOT_FOUND,NOT_ALLOWED,VAULT_LOCKED,VAULT_UNAVAILABLE,WRITE_FAILED,INVALID_OPTIONS.SecretsVaultErrorsubclass with stableerror.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.Vaultso tests can wire deterministic backends../from-vault—fromVault(vault)bridges a live@suaveplan/vault.Vaultinstance to the structural backend. Secrets are stored as items withcategory === "secret"and a single concealed field"value"so the underlying field-encryption layer covers them at rest. In-bridgesecret.updatedfan-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.