# @suaveplan/api-secrets-env — SecretsLike adapter backed by process.env / Bun.env with per-plugin allowed-keys enforcement. Reads from an injectable source function so tests never mutate the real environment; an optional prefix scopes lookups for multi-tenant single-host deployments. Stage-4 `SecretsLike` adapter backed by `process.env` / `Bun.env` with per-plugin allowed-keys enforcement. The simplest secrets source: read from the host's environment. Suitable for local development, single-host deployments, container orchestrators that inject secrets as env vars, and anywhere the operator already has tooling around env-var injection. Per-plugin allow-list keeps each plugin scoped to its manifest-declared secrets — a worker or untrusted plugin cannot enumerate or read any env var beyond what its `manifest.secrets[]` declared. Values never appear in logs or errors — only names. ## Documentation tree - [adapter](./src/adapter/adapter.md) — Stage-4 concrete `SecretsLike` implementation that resolves secret names against the host process's environment variabl… - [adapter](./src/adapter/adapter.md) — Stage-4 concrete `SecretsLike` implementation that resolves secret names against the host process's environment variabl… - [descriptor-builder](./src/descriptor-builder/descriptor-builder.md) — Convenience builder that turns a compact allow-list declaration into a fully-typed `SecretDescriptor[]` suitable for em… - [descriptor-builder](./src/descriptor-builder/descriptor-builder.md) — Convenience builder that turns a compact allow-list declaration into a fully-typed `SecretDescriptor[]` suitable for em… - [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry and dedicated `SuaveplanError` subclass for the env-backed secrets adapter. Every failure mo… - [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry and dedicated `SuaveplanError` subclass for the env-backed secrets adapter. Every failure mo… ## Install ```bash bun add @suaveplan/api-secrets-env ``` ## Usage ```ts import { createEnvSecrets } from "@suaveplan/api-secrets-env"; const secrets = createEnvSecrets({ allowedKeys: ["STRIPE_SECRET_KEY", "JWT_SIGNING_KEY"], }); const key = await secrets.require("STRIPE_SECRET_KEY"); ``` ### Prefix for multi-tenant single-host deployments ```ts const secrets = createEnvSecrets({ allowedKeys: ["API_KEY", "WEBHOOK_SECRET"], prefix: "BILLING_", // Reads BILLING_API_KEY, BILLING_WEBHOOK_SECRET from env. }); ``` ### Manifest-driven allow-list The companion `buildEnvSecretDescriptors` builder lets a plugin derive its `manifest.secrets[]` and the adapter's `allowedKeys` from a single declaration so they cannot drift: ```ts import { buildEnvSecretDescriptors, descriptorAllowedKeys, createEnvSecrets, } from "@suaveplan/api-secrets-env"; const descriptors = buildEnvSecretDescriptors([ "STRIPE_SECRET_KEY", { name: "OPTIONAL_WEBHOOK_KEY", required: false }, ]); const secrets = createEnvSecrets({ allowedKeys: descriptorAllowedKeys(descriptors), }); // `descriptors` goes on the plugin manifest's `secrets[]`. ``` ## Fail-fast contract The plugin manifest declares each required secret with `{ name, required: true }`. The host's installer reads the manifest's `secrets[]` and, at install or start time, calls `enforceRequiredSecrets` (from `@suaveplan/api-secrets`) against the adapter's `list()`. Any `required: true` secret missing from the host's env triggers `SECRETS.SECRET_REQUIRED_MISSING_AT_START`, and the installer refuses to install / refuses to start. This adapter contributes to that contract by: 1. enumerating the plugin's permitted names via `list()` — the same names the manifest declares; 2. returning `undefined` from `get()` for a permitted-but-absent name so `enforceRequiredSecrets` can detect it; 3. throwing `SECRETS_ENV.KEY_NOT_FOUND` from `require()` so a plugin that chooses to defer the check past install also fails loudly. ## Allowed-keys enforcement Every `get` / `require` checks the allow-list BEFORE consulting the env source. A non-permitted name throws `SECRETS_ENV.KEY_NOT_PERMITTED` and never reads from the snapshot — the env source can be expensive or have side effects without affecting this ordering. `list()` returns the frozen `allowedKeys` array, so the adapter never surfaces any name outside the plugin's manifest. ## Secret-value redaction Secret VALUES never appear in error messages, log lines, or telemetry emitted by this package — only NAMES. Errors carry the name on `error.context.name` for diagnostic surfaces; the value (when one was resolved) never leaves the call site. ## API | Export | Subpath | Purpose | | --- | --- | --- | | `createEnvSecrets` | `./adapter` | Build a `SecretsLike` over env. | | `CreateEnvSecretsOptions` | `./adapter` | Option-bag type. | | `buildEnvSecretDescriptors` | `./descriptor-builder` | Compact → `SecretDescriptor[]`. | | `descriptorAllowedKeys` | `./descriptor-builder` | Extract names for `allowedKeys`. | | `SECRETS_ENV_CODES` | `./error-codes` | Frozen `SECRETS_ENV.*` code registry. | | `SecretsEnvError` | `./error-codes` | Thrown by every failure mode. | ## Conformance Passes `runSecretsConformance` from `@suaveplan/api-secrets/testing` unchanged — see `src/adapter/adapter.spec-conformance.test.ts`. ## License MIT