Files
suaveplan/openspec/archive/2026-05-26-api-secrets-env/proposal.md
2026-05-27 23:29:53 +01:00

56 lines
3.6 KiB
Markdown

# `@suaveplan/api-secrets-env` — Environment-variable secrets adapter
**Package**: `@suaveplan/api-secrets-env`
**Status**: PROPOSED
**Type**: new-package
**Stage**: 4 (Base adapters) — see umbrella `openspec/changes/api-plugin-foundation/`
## Why
The simplest secrets source: `process.env` (or `Bun.env`). 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 design doc, plugins declare their secrets in `plugin.json` under `manifest.secrets[]` (each entry has `name`, `required: boolean`, `description`). The installer's fail-fast contract (§4) is: any required secret missing at install or start time → refuse to install / refuse to start. `api-secrets-env` implements that contract by reading from env at construction and at `validateAgainstManifest(manifest)` time.
Stage 4 invariant: ships direct AND worker-proxy surfaces; both pass `runSecretsConformance` (§5.2a, §13.4).
## What changes
Net-new package depending on `@suaveplan/api-secrets` (contract), `@suaveplan/api-types`, `@suaveplan/api-worker-rpc`, `@suaveplan/error`, `@suaveplan/data-masking` (for redaction in logs), `zod`.
- ADDED: `createEnvSecrets(opts)` — direct-surface factory returning a `SecretsLike`. Reads via `Bun.env` first then falls back to `process.env`.
- ADDED: `createEnvSecretsWorkerProxy(port, opts)` — proxy-surface factory; the worker NEVER touches host env directly — every `get()` round-trips through the proxy so secret materialisation stays in the main process.
- ADDED: `EnvSecretsOptions` — Zod schema (`prefix?: string` (default ""), `caseSensitive?: boolean` (default true), `fallbackSource?: "process" | "bun" | "both"` (default `"both"`)).
- ADDED: `validateAgainstManifest(manifest)` — given a plugin manifest's `secrets[]`, checks every `required: true` entry is present; missing → throws `SECRETS_ENV_REQUIRED_MISSING` listing every missing name.
- ADDED: `SECRETS_ENV_*` error codes (`SECRETS_ENV_REQUIRED_MISSING`, `SECRETS_ENV_INVALID_OPTIONS`).
- ADDED: Log redaction integration — secret VALUES never appear in error messages or telemetry; only NAMES.
## Out of scope
- Watched/reactive secrets — env vars are read once at construction; rotation requires plugin reload (delegated to host hot reload §8).
- Encrypted secrets at rest — `api-secrets-vault` Stage 8.
- Per-tenant secrets — env-based source is host-global by definition.
## Source mapping
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
| Requirement | Design doc section |
|---|---|
| Manifest secrets installer fail-fast | §3 (manifest schema), §4 (lifecycle FSM) |
| Worker-proxy variant required | §5.2a |
| Conformance both surfaces | §13.4 |
| Value redaction | §12.2 |
## Verification approach
- `src/factory/create-env-secrets.test.ts` — `get(name)` returns env value; missing → undefined; redaction never leaks values.
- `src/factory/create-env-secrets.spec-conformance.test.ts` — `runSecretsConformance("env-direct", ...)`.
- `src/proxy/create-env-secrets-worker-proxy.test.ts` — proxy unit tests; worker side has no env access.
- `src/proxy/create-env-secrets-worker-proxy.spec-conformance.test.ts` — `runSecretsConformance("env-worker-proxy", ...)`.
- `src/manifest-validation/validate-against-manifest.test.ts` — required missing → throw with named list; optional missing → silent.
- `src/redaction/redaction.test.ts` — error messages and metrics never contain raw secret values.
## Migration impact
None — additive only.