Initial Commit
This commit is contained in:
@@ -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 -->
|
||||
Reference in New Issue
Block a user