first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View 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.