Files
skills/docs/api-stack/api/api-secrets-vault/src/watcher/watcher.md
2026-05-29 08:20:56 +01:00

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 to vault.onSecretUpdated and returning a SecretsVaultWatcher handle.
  • Per-plugin allow-list filter — vault.secret.updated payloads whose name is 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 calling adapter.get(name) which re-enters the allow-list gate.
  • OTel api.secrets.watcher_emits_total counter 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 unsubscribe lags.

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 HostEventBusEmitter
    • emit(type: "secret.changed", payload: { name: string }): unknown
  • interface CreateSecretsVaultWatcherOptions
    • vault: VaultSecretsBackend
    • allowlist: Allowlist
    • bus: HostEventBusEmitter
  • createSecretsVaultWatcher(options): SecretsVaultWatcher
    • Throws SECRETS_VAULT.INVALID_OPTIONS on bad options.
  • interface SecretsVaultWatcher
    • dispose(): void — idempotent
    • readonly 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.