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

137 lines
5.8 KiB
Markdown

# @suaveplan/api-secrets-bun — SecretsLike adapter backed by Bun.secrets (macOS Keychain / Linux libsecret / Windows Credential Manager). Per-plugin allowlist enforcement, locked-keystore detection, idempotent dispose, OTel api.secrets.reads_total{backend="bun"} counter.
Stage-8 `SecretsLike` adapter backed by `Bun.secrets` (macOS Keychain,
Linux libsecret, Windows Credential Manager) with per-plugin allow-list
enforcement, OS-keystore failure-mode classification, and an idempotent
lifecycle.
The right default for local-dev and single-host operator setups: secrets
never touch disk in plaintext and rotate via the OS UI.
Per-plugin allow-list keeps each plugin scoped to its manifest-declared
secrets — a leaked plugin cannot enumerate or read any keystore entry
beyond what its `manifest.secrets[]` declared. Values never appear in
logs or errors — only names.
<!-- begin:auto-docs -->
## Documentation tree
- [adapter](./src/adapter/adapter.md) — The adapter module builds a concrete `SecretsLike` over the `Bun.secrets` API — the cross-platform wrapper Bun ships ar…
- [adapter](./src/adapter/adapter.md) — The adapter module builds a concrete `SecretsLike` over the `Bun.secrets` API — the cross-platform wrapper Bun ships ar…
- [allowlist](./src/allowlist/allowlist.md) — The allow-list module encapsulates the per-plugin scoping rule that defines the package's security boundary: a plugin c…
- [allowlist](./src/allowlist/allowlist.md) — The allow-list module encapsulates the per-plugin scoping rule that defines the package's security boundary: a plugin c…
- [descriptor-builder](./src/descriptor-builder/descriptor-builder.md) — This module produces the `SecretDescriptor[]` shape the plugin manifest emits in its `secrets[]` block from a compact a…
- [descriptor-builder](./src/descriptor-builder/descriptor-builder.md) — This module produces the `SecretDescriptor[]` shape the plugin manifest emits in its `secrets[]` block from a compact a…
- [error-codes](./src/error-codes/error-codes.md) — This module owns the stable `SECRETS_BUN.*` code registry and the `SecretsBunError` subclass thrown by every failure pa…
- [error-codes](./src/error-codes/error-codes.md) — This module owns the stable `SECRETS_BUN.*` code registry and the `SecretsBunError` subclass thrown by every failure pa…
- [otel](./src/otel/instruments.md) — This module owns the OpenTelemetry instrument bundle the `Bun.secrets`-backed adapter emits and a lazy cache that re-bi…
- [instruments](./src/otel/instruments.md) — This module owns the OpenTelemetry instrument bundle the `Bun.secrets`-backed adapter emits and a lazy cache that re-bi…
<!-- end:auto-docs -->
## Install
```bash
bun add @suaveplan/api-secrets-bun
```
## Usage
```ts
import { createBunSecretsAdapter } from "@suaveplan/api-secrets-bun";
const adapter = createBunSecretsAdapter({
service: "billing-plugin",
allowlist: ["STRIPE_SECRET_KEY", "JWT_SIGNING_KEY"],
});
const key = await adapter.secrets.require("STRIPE_SECRET_KEY");
```
### Manifest-driven allow-list
The companion `buildBunSecretDescriptors` builder lets a plugin derive
its `manifest.secrets[]` and the adapter's `allowlist` from a single
declaration so they cannot drift:
```ts
import {
buildBunSecretDescriptors,
descriptorAllowlist,
createBunSecretsAdapter,
} from "@suaveplan/api-secrets-bun";
const descriptors = buildBunSecretDescriptors([
"STRIPE_SECRET_KEY",
{ name: "OPTIONAL_WEBHOOK_KEY", required: false },
]);
const adapter = createBunSecretsAdapter({
service: "billing-plugin",
allowlist: descriptorAllowlist(descriptors),
});
// `descriptors` goes on the plugin manifest's `secrets[]`.
```
## Capability
This package declares `secrets.bun`. Plugins that specifically require
the Bun keystore (e.g. desktop runtimes that bundle Bun) can pin the
backend in `requiredCapabilities`; otherwise the host's `host.secrets.backend`
config selects this adapter alongside the abstract `secrets` capability.
## Failure modes
Distinguishes:
- `SECRETS_BUN.NOT_FOUND` — permitted name; OS keystore returned `null`.
`get()` resolves to `undefined`; `require()` throws.
- `SECRETS_BUN.NOT_ALLOWED` — name not in the plugin's allow-list.
Raised BEFORE any keystore round-trip.
- `SECRETS_BUN.KEYSTORE_LOCKED` — OS keystore is locked. The adapter
does NOT fall back silently — operator must unlock.
- `SECRETS_BUN.KEYSTORE_UNAVAILABLE` — `Bun.secrets` is missing
(running under Node without the shim), the OS keystore daemon is not
running, or the adapter has been `dispose()`d.
- `SECRETS_BUN.WRITE_FAILED` — `set`/`delete` failed at the OS layer.
Always wraps the underlying error as `cause`.
- `SECRETS_BUN.INVALID_OPTIONS` — adapter construction failed Zod
validation.
## OpenTelemetry
Emits:
- `api.secrets.reads_total{backend="bun"}` — successful reads.
- `api.secrets.not_found_total{backend="bun"}` — permitted-but-absent.
- `api.secrets.denied_total{backend="bun"}` — allow-list rejections.
Metric names are backend-agnostic so dashboards roll up across every
`api.secrets.*` source via the `backend` label.
## Lifecycle
`adapter.dispose()` is idempotent — repeat calls are no-ops. Post-dispose
reads / writes throw `KEYSTORE_UNAVAILABLE` rather than leaking the
keystore reference.
## Conformance
Passes `runSecretsConformance` from `@suaveplan/api-secrets/testing`
unchanged — see `src/adapter/adapter.spec-conformance.test.ts`.
## Modules
| Module | Subpath | Purpose |
| --- | --- | --- |
| `adapter` | `./adapter` | `createBunSecretsAdapter` factory + types. |
| `allowlist` | `./allowlist` | Per-plugin allow-list with `assertAllowed`. |
| `descriptor-builder` | `./descriptor-builder` | Compact → `SecretDescriptor[]`. |
| `error-codes` | `./error-codes` | `SECRETS_BUN.*` registry + `SecretsBunError`. |
| `otel` | `./otel` | OTel instruments + reset hook. |
## License
MIT