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_123is set ANDget("STRIPE_KEY")is called - THEN the result is
"sk_test_123"
Scenario: missing env var returns undefined
- WHEN
NOT_SETis not in env ANDget("NOT_SET")is called - THEN the result is
undefined
Scenario: prefix is honoured
- WHEN the adapter is constructed with
prefix: "BILLING_"AND env containsBILLING_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 onlyAis in env - THEN
validateAgainstManifest()throwsSECRETS_ENV_REQUIRED_MISSINGANDerror.details.missing === ["B"]
Scenario: missing optional secret is silent
- WHEN manifest declares
secrets: [{ name: "X", required: false }]ANDXis 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_valueAND the resulting error is stringified - THEN the resulting string contains
SECRETAND does NOT containraw_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=SECRETAND 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 withBun.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"