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

4.2 KiB

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"