Initial Commit
This commit is contained in:
147
openspec/specs/api-secrets/spec.md
Normal file
147
openspec/specs/api-secrets/spec.md
Normal file
@@ -0,0 +1,147 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# 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.freeze`d. 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"`
|
||||
Reference in New Issue
Block a user