# Capability Spec Delta: api-secrets (api-secrets) **Package**: `@suaveplan/api-secrets` **Status**: SHIPPED 2026-05-25 > Every requirement carries a `**Verified by:**` line. ## ADDED Requirements ### Requirement: SecretsLike re-export from api-types The package SHALL re-export the canonical `SecretsLike` interface from `@suaveplan/api-types/secrets`. The canonical shape exposes `get(name): Promise`, `require(name): Promise` (throws `SECRETS.SECRET_NOT_FOUND` on miss), and `list(): readonly string[]` (enumerates declared names). #### Scenario: get returns the value when present - **WHEN** a stub source seeded with `{ A: "alpha" }` calls `get("A")` - **THEN** the promise resolves to `"alpha"` #### Scenario: get returns undefined when absent - **WHEN** the same source calls `get("MISSING")` - **THEN** the promise resolves to `undefined` #### Scenario: require throws SECRET_NOT_FOUND on miss - **WHEN** the source calls `require("MISSING")` - **THEN** a `SecretsError` is thrown carrying `code === "SECRETS.SECRET_NOT_FOUND"` **Verified by:** `src/secrets-like/secrets-like.test.ts::"SecretsLike contract — stub implementation > get returns the value when present"` AND `src/secrets-like/secrets-like.test.ts::"SecretsLike contract — stub implementation > get returns undefined when absent"` AND `src/secrets-like/secrets-like.test.ts::"SecretsLike contract — stub implementation > require throws SECRET_NOT_FOUND when absent"` --- ### Requirement: `import z from "zod"` everywhere Every Zod import MUST use the default form. #### Scenario: no named import of z - **WHEN** the test greps `import { z } from "zod"` in `src/**` - **THEN** zero matches **Verified by:** `tests/dep-graph.test.ts::"zod-import-form > every src file uses default-import z"` --- ### Requirement: SecretDescriptor schema The package SHALL export `secretDescriptorSchema` describing `{ name: string, required: boolean, description?: string, format?: "string" | "base64" | "json" }`. Unknown keys MUST be rejected. The list variant `secretDescriptorListSchema` MUST accept the empty array. #### Scenario: minimal descriptor parses - **WHEN** `{ name: "STRIPE_KEY", required: true }` is parsed - **THEN** it succeeds AND `format` is undefined #### Scenario: unknown format rejected - **WHEN** `{ name: "X", required: true, format: "weird" }` is parsed - **THEN** Zod reports an issue at `format` #### Scenario: unknown keys rejected - **WHEN** `{ name: "X", required: true, requried: true }` (typo) is parsed - **THEN** the parse fails **Verified by:** `src/descriptor/descriptor.test.ts::"secretDescriptorSchema > minimal descriptor parses"` AND `src/descriptor/descriptor.test.ts::"secretDescriptorSchema > unknown format rejected"` AND `src/descriptor/descriptor.test.ts::"secretDescriptorSchema > unknown keys rejected"` --- ### Requirement: redactSecret returns the canonical marker The package SHALL export `redactSecret(value: string): ""` returning the literal string `""` for any input — including the empty string — and `REDACTED_MARKER` exposing the same literal. #### Scenario: any input maps to canonical marker - **WHEN** any string is passed to `redactSecret` - **THEN** the result equals `""` **Verified by:** `src/redaction/redaction.test.ts::"redactSecret > any input maps to canonical marker"` --- ### Requirement: CapabilityId — secrets and secrets.bun The package SHALL export `SECRETS_CAPABILITY_ID` (literal `"secrets"`) and `SECRETS_BUN_CAPABILITY_ID` (literal `"secrets.bun"`), both typed as `CapabilityId` via `as const satisfies CapabilityId`. If api-types ever drops either id, this package's typecheck MUST fail. #### Scenario: both ids assignable to CapabilityId - **WHEN** the test assigns each constant to a `CapabilityId`-typed variable - **THEN** the assignment compiles **Verified by:** `src/capability-id/capability-id.test.ts::"capability-id constants > both ids are valid CapabilityId values"` --- ### Requirement: Required-secret enforcement at start The package SHALL export `checkRequiredSecrets` (returns a discriminated `{ ok }` result) and `enforceRequiredSecrets` (throws `SecretsError(SECRETS.SECRET_REQUIRED_MISSING_AT_START)` listing missing names on `context.missing`). Hosts use these to gate the `installing → installed` transition. #### Scenario: missing required secret throws SECRET_REQUIRED_MISSING_AT_START - **WHEN** a stub source has no entry for `"X"` AND descriptors include `{ name: "X", required: true }` - **THEN** `enforceRequiredSecrets` throws `SecretsError` with `code === "SECRETS.SECRET_REQUIRED_MISSING_AT_START"` AND `context.missing` lists `["X"]` #### Scenario: optional descriptors ignored - **WHEN** descriptors include `{ name: "OPT", required: false }` AND the source has no entry for `"OPT"` - **THEN** `checkRequiredSecrets` returns `{ ok: true }` **Verified by:** `src/secrets-like/secrets-like.test.ts::"enforceRequiredSecrets > throws SECRET_REQUIRED_MISSING_AT_START with missing list"` AND `src/secrets-like/secrets-like.test.ts::"checkRequiredSecrets > ignores optional descriptors"` --- ### Requirement: Error codes registry frozen The package SHALL export `SECRETS_CODES` containing exactly: `SECRET_NOT_FOUND`, `SECRET_REQUIRED_MISSING_AT_START`, `SECRET_INVALID_FORMAT`, `SOURCE_UNAVAILABLE`. The registry MUST be `Object.freeze`d. The package SHALL also export `SecretsError` — a concrete `SuaveplanError` subclass with a public constructor. #### Scenario: every documented code present - **WHEN** the test enumerates `Object.keys(SECRETS_CODES)` - **THEN** the sorted keys equal `["SECRET_INVALID_FORMAT", "SECRET_NOT_FOUND", "SECRET_REQUIRED_MISSING_AT_START", "SOURCE_UNAVAILABLE"]` #### Scenario: registry is frozen - **WHEN** `Object.isFrozen(SECRETS_CODES)` is evaluated - **THEN** it equals `true` **Verified by:** `src/error-codes/error-codes.test.ts::"SECRETS_CODES > contains exactly four keys"` AND `src/error-codes/error-codes.test.ts::"SECRETS_CODES > is frozen"` --- ### Requirement: Conformance harness for sources The package SHALL export `runSecretsConformance({ label, factory, isNotFound? })`. The harness MUST register five scenarios per call: `get` resolves seeded value, `get` resolves undefined for missing, `require` resolves seeded value, `require` throws (per `isNotFound`) on miss, and `list()` enumerates seeded names. Every concrete source in Stage 4/8 MUST pass it. #### Scenario: stub source passes default-predicate suite - **WHEN** a stub `SecretsLike` is run through `runSecretsConformance` with the default predicate - **THEN** all five scenarios pass #### Scenario: custom isNotFound predicate honoured - **WHEN** a source represents missing names as `throw new Error("CUSTOM_MISSING")` AND `isNotFound` checks for that message - **THEN** the harness still passes **Verified by:** `src/testing/conformance.test.ts::"stub source — default predicate — SecretsLike conformance > require throws SECRET_NOT_FOUND for missing names"` AND `src/testing/conformance.test.ts::"stub source — custom predicate — SecretsLike conformance > require throws SECRET_NOT_FOUND for missing names"`