3.7 KiB
watcher
(no submodules)
Purpose
Live subscription that bridges the vault's secret.updated events
onto the host event bus as secret.changed. Plugins that cache
secret values register against the host bus and refresh on
broadcast; the watcher intersects the vault stream with the per-
plugin allow-list so a plugin never even learns that a non-allow-
listed secret rotated. Designed as a host-owned lifecycle: one
watcher per (vault, plugin) pair, disposed during plugin teardown.
Features
createSecretsVaultWatcher({ vault, allowlist, bus })— Zod-validated factory subscribing once tovault.onSecretUpdatedand returning aSecretsVaultWatcherhandle.- Per-plugin allow-list filter —
vault.secret.updatedpayloads whosenameis not in the allow-list are silently dropped. - Allow-listed updates forward as
bus.emit("secret.changed", { name })carrying ONLY the name; the value is never broadcast. Listeners refresh by callingadapter.get(name)which re-enters the allow-list gate. - OTel
api.secrets.watcher_emits_totalcounter increments on every forwarded event. - Idempotent
dispose()— releases the underlying subscription. - Single subscription per watcher: the listener guards against
post-dispose deliveries even if the backend's
unsubscribelags.
Usage: Basic
import {
createAllowlist,
createSecretsVaultWatcher,
} from "@suaveplan/api-secrets-vault";
import { EventBus } from "@suaveplan/events";
const bus = new EventBus<{ "secret.changed": { name: string } }>();
const allow = createAllowlist({ names: ["STRIPE"] });
const watcher = createSecretsVaultWatcher({ vault: backend, allowlist: allow, bus });
bus.on("secret.changed", (p) => {
console.log(`Refreshing cache for ${p.name}`);
});
// ...later, during plugin teardown:
watcher.dispose();
Usage: Advanced
Combine with the adapter so plugins see a uniform "read + refresh" loop:
import {
createSecretsVaultWatcher,
createVaultSecretsAdapter,
} from "@suaveplan/api-secrets-vault";
const adapter = createVaultSecretsAdapter({
vault: backend,
allowlist: ["STRIPE"],
});
const watcher = createSecretsVaultWatcher({
vault: backend,
allowlist: adapter.allowlist,
bus: hostBus,
});
let cached = await adapter.require("STRIPE");
hostBus.on("secret.changed", async (p) => {
if (p.name === "STRIPE") {
cached = await adapter.require("STRIPE");
}
});
API Reference
interface HostEventBusEmitteremit(type: "secret.changed", payload: { name: string }): unknown
interface CreateSecretsVaultWatcherOptionsvault: VaultSecretsBackendallowlist: Allowlistbus: HostEventBusEmitter
createSecretsVaultWatcher(options): SecretsVaultWatcher- Throws
SECRETS_VAULT.INVALID_OPTIONSon bad options.
- Throws
interface SecretsVaultWatcherdispose(): void— idempotentreadonly isDisposed: boolean
Implementation Notes
The watcher checks its own disposed flag inside the listener AND
calls the backend's unsubscribe handle on dispose. The double-
guard handles backends whose unsubscribe is implemented lazily —
if a secret.updated is in flight when we dispose, the disposed-
guard catches the stale delivery so the host bus never receives a
post-dispose secret.changed.
Listener errors swallowed at the host bus boundary cannot crash the
producer — that's @suaveplan/events.EventBus policy. The watcher
itself does not try/catch around bus.emit because the host bus
is the canonical place for emit-time error handling, and re-
throwing here would block the rest of the listener fan-out at the
vault backend.