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