# @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. ## 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… ## 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