Files
2026-05-27 23:29:53 +01:00

7.1 KiB

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<string | undefined>, require(name): Promise<string> (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): "<redacted>" returning the literal string "<redacted>" 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 "<redacted>"

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.freezed. The package SHALL also export SecretsError — a concrete SuaveplanError<SecretsCode> 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"