Initial Commit
This commit is contained in:
55
openspec/archive/2026-05-26-api-secrets-env/proposal.md
Normal file
55
openspec/archive/2026-05-26-api-secrets-env/proposal.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# `@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.
|
||||
@@ -0,0 +1,85 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: api-secrets-env (api-secrets-env)
|
||||
|
||||
**Package**: `@suaveplan/api-secrets-env`
|
||||
**Status**: PROPOSED
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Read secrets from environment
|
||||
|
||||
The package SHALL implement `SecretsLike.get(name)` by reading from `Bun.env` first, falling back to `process.env`. Resolution honours the configured `prefix` (e.g. `BILLING_` → `get("STRIPE_KEY")` reads `BILLING_STRIPE_KEY`) and `caseSensitive` flag.
|
||||
|
||||
#### Scenario: present env var returns its value
|
||||
|
||||
- **WHEN** `STRIPE_KEY=sk_test_123` is set AND `get("STRIPE_KEY")` is called
|
||||
- **THEN** the result is `"sk_test_123"`
|
||||
|
||||
#### Scenario: missing env var returns undefined
|
||||
|
||||
- **WHEN** `NOT_SET` is not in env AND `get("NOT_SET")` is called
|
||||
- **THEN** the result is `undefined`
|
||||
|
||||
#### Scenario: prefix is honoured
|
||||
|
||||
- **WHEN** the adapter is constructed with `prefix: "BILLING_"` AND env contains `BILLING_STRIPE_KEY=...`
|
||||
- **THEN** `get("STRIPE_KEY")` returns the value
|
||||
|
||||
**Verified by:** `src/factory/create-env-secrets.test.ts::"returns env value when set"` AND `src/factory/create-env-secrets.test.ts::"returns undefined when not set"` AND `src/factory/create-env-secrets.test.ts::"honours configured prefix"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Fail-fast against plugin manifest
|
||||
|
||||
The package SHALL expose `validateAgainstManifest(manifest)` which iterates `manifest.secrets[]` and throws `SECRETS_ENV_REQUIRED_MISSING` (with `details.missing: string[]`) when any `required: true` entry resolves to undefined. Optional entries that resolve to undefined MUST NOT trigger.
|
||||
|
||||
#### Scenario: missing required secret throws with named list
|
||||
|
||||
- **WHEN** manifest declares `secrets: [{ name: "A", required: true }, { name: "B", required: true }]` AND only `A` is in env
|
||||
- **THEN** `validateAgainstManifest()` throws `SECRETS_ENV_REQUIRED_MISSING` AND `error.details.missing === ["B"]`
|
||||
|
||||
#### Scenario: missing optional secret is silent
|
||||
|
||||
- **WHEN** manifest declares `secrets: [{ name: "X", required: false }]` AND `X` is not in env
|
||||
- **THEN** `validateAgainstManifest()` returns without throwing
|
||||
|
||||
**Verified by:** `src/manifest-validation/validate-against-manifest.test.ts::"throws with named list when required secrets are missing"` AND `src/manifest-validation/validate-against-manifest.test.ts::"is silent when only optional secrets are missing"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Secret values never appear in logs or telemetry
|
||||
|
||||
The package SHALL route every error message, log line, and metric label through `@suaveplan/data-masking`. Raw secret values MUST NEVER appear in `error.message`, `error.details`, OTel metric attribute values, or trace event attributes. Only secret NAMES are loggable.
|
||||
|
||||
#### Scenario: error stringification redacts values
|
||||
|
||||
- **WHEN** an internal failure occurs while reading `SECRET=raw_value` AND the resulting error is stringified
|
||||
- **THEN** the resulting string contains `SECRET` AND does NOT contain `raw_value`
|
||||
|
||||
#### Scenario: metric labels carry only names
|
||||
|
||||
- **WHEN** `api.secrets.get_total{source=env,outcome=hit,name=SECRET}` is emitted
|
||||
- **THEN** the attribute set contains `name=SECRET` AND no attribute contains the value
|
||||
|
||||
**Verified by:** `src/redaction/redaction.test.ts::"error messages redact secret values"` AND `src/redaction/redaction.test.ts::"metric labels carry names but never values"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Direct and worker-proxy surfaces both pass conformance
|
||||
|
||||
The worker-proxy surface MUST NOT read env in the worker process — every `get()` round-trips to the main process so secrets stay materialised on one side.
|
||||
|
||||
#### Scenario: direct conforms
|
||||
|
||||
- **WHEN** `runSecretsConformance("env-direct", () => createEnvSecrets(...))` runs
|
||||
- **THEN** every invariant passes
|
||||
|
||||
#### Scenario: worker-proxy conforms AND worker never reads env
|
||||
|
||||
- **WHEN** `runSecretsConformance("env-worker-proxy", ...)` runs in a real Worker with `Bun.env = {}` enforced on the worker side
|
||||
- **THEN** every `get()` still returns the host's env value (proving the proxy round-trips) AND every invariant passes
|
||||
|
||||
**Verified by:** `src/factory/create-env-secrets.spec-conformance.test.ts::"env-direct conforms to Secrets contract"` AND `src/proxy/create-env-secrets-worker-proxy.spec-conformance.test.ts::"env-worker-proxy conforms to Secrets contract AND worker env is never read"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
70
openspec/archive/2026-05-26-api-secrets-env/tasks.md
Normal file
70
openspec/archive/2026-05-26-api-secrets-env/tasks.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Tasks — `@suaveplan/api-secrets-env`
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change.
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||||
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||||
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read design doc (§3, §4, §5.2a, §12.2, §13.4)
|
||||
- [x] 1.2 Read `openspec/changes/api-secrets/` for upstream contract
|
||||
- [x] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14
|
||||
- [x] 1.4 Draft `proposal.md`
|
||||
- [x] 1.5 Draft `specs/api-secrets-env/spec.md`
|
||||
- [x] 1.6 Every requirement carries `**Verified by:**`
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [ ] 2.1 Scaffold `packages/api/api-secrets-env/` (deps: api-secrets, api-types, api-worker-rpc, error, data-masking, zod)
|
||||
- [ ] 2.2 `src/options/env-secrets-options.ts` — Zod schema
|
||||
- [ ] 2.3 `src/factory/create-env-secrets.ts` — direct surface; Bun.env → process.env fallback
|
||||
- [ ] 2.4 `src/proxy/create-env-secrets-worker-proxy.ts` — proxy surface; worker never reads env directly
|
||||
- [ ] 2.5 `src/manifest-validation/validate-against-manifest.ts` — fail-fast checker
|
||||
- [ ] 2.6 `src/redaction/redaction.ts` — wires `@suaveplan/data-masking` for log/error output
|
||||
- [ ] 2.7 `src/error-codes.ts` — `SECRETS_ENV_*` codes via `defineCodes`
|
||||
- [ ] 2.8 `src/index.ts` — explicit named re-exports
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [ ] 3.1 `src/factory/create-env-secrets.test.ts`
|
||||
- [ ] 3.2 `src/factory/create-env-secrets.spec-conformance.test.ts` — direct conformance
|
||||
- [ ] 3.3 `src/proxy/create-env-secrets-worker-proxy.test.ts`
|
||||
- [ ] 3.4 `src/proxy/create-env-secrets-worker-proxy.spec-conformance.test.ts` — proxy conformance
|
||||
- [ ] 3.5 `src/manifest-validation/validate-against-manifest.test.ts`
|
||||
- [ ] 3.6 `src/redaction/redaction.test.ts`
|
||||
- [ ] 3.7 Every `it(...)` carries `{ timeout: 5000 }`
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [ ] 4.1 Sibling `.md` per implementation `.ts`
|
||||
- [ ] 4.2 README documents the fail-fast contract and the `prefix` pattern for namespacing
|
||||
- [ ] 4.3 README explicitly says: "values never appear in logs/errors — only names"
|
||||
|
||||
## 5. Pre-archive verification (5-gate triple)
|
||||
|
||||
- [ ] 5.1 `bun run check` — clean
|
||||
- [ ] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/api-secrets-env` — green
|
||||
- [ ] 5.3 `bunx turbo test:coverage --filter=@suaveplan/api-secrets-env` — 100/100/100/100
|
||||
- [ ] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/api-secrets-env` — exit 0
|
||||
- [ ] 5.5 `bunx turbo build --filter=@suaveplan/api-secrets-env` — green
|
||||
|
||||
## 6. Pre-archive 7-row checklist
|
||||
|
||||
- [ ] 6.1 `expectTypeOf` asserts `get(name)` returns `string | undefined`
|
||||
- [ ] 6.2 Zod schema at `EnvSecretsOptions` boundary AND at manifest validation
|
||||
- [ ] 6.3 Every `SECRETS_ENV_*` code thrown by at least one production path
|
||||
- [ ] 6.4 Strict-mode (`caseSensitive` on/off) tested both ways
|
||||
- [ ] 6.5 OTel instruments (`api.secrets.get_total{source=env,outcome}`, names only — never values) asserted with in-memory `MeterProvider`
|
||||
- [ ] 6.6 Manifest validation dedup tested (running twice with identical manifest produces identical outcome)
|
||||
- [ ] 6.7 No retroactive tick
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [ ] 7.1 Fold into `openspec/specs/api-secrets-env/spec.md`
|
||||
- [ ] 7.2 Move to `openspec/archive/<yyyy-mm-dd>-api-secrets-env/`
|
||||
- [ ] 7.3 Tick Stage 4 box in umbrella tasks
|
||||
- [ ] 7.4 Refresh `MANIFEST.md`
|
||||
Reference in New Issue
Block a user