Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
# `@suaveplan/api-secrets` — abstract Secrets contract
**Package**: `@suaveplan/api-secrets`
**Status**: PROPOSED
**Type**: new-package
**Stage**: 1 (Contracts) — see umbrella `openspec/changes/api-plugin-foundation/`
## Why
Every plugin needs to read secrets — Stripe keys, JWT signing keys, OAuth client secrets, database passwords. Different deployments want different sources: `process.env` / `Bun.env` (dev + simple prod), `Bun.secrets` (OS keystore), `@suaveplan/vault` (the genesis vault), HashiCorp Vault, AWS Secrets Manager. This package owns the abstract contract: `SecretsLike` (read-only, typed against the manifest's `secrets[]` declarations) plus `SecretDescriptor` (the manifest-side declaration). Concrete sources plug behind it (`api-secrets-env`, `api-secrets-bun`, `api-secrets-vault`).
Per design §5.2: "every adapter exports `defineCapability({ id, version, scope, provider, disposer })`." Per §5.1: `ctx.secrets: SecretsLike; // typed against manifest.secrets[]`.
## What changes
Net-new package. Contract + Zod schema for `SecretDescriptor` + conformance harness.
- ADDED: `./secrets-like` — abstract `SecretsLike`: `get(name: string): Promise<string>` (returns the secret value); `has(name: string): Promise<boolean>`; `dispose(): Promise<void>`. The type MUST be generic over a `Names extends string` so `ctx.secrets.get(name)` autocompletes against the manifest's declared secret names.
- ADDED: `./descriptor` — `SecretDescriptor` shape: `{ name: string, required: boolean, description?: string, format?: "string" | "base64" | "json" }`. Zod schema for parsing manifest entries.
- ADDED: `./redaction` — `redactSecret(value: string): string` returning `"<redacted>"` for log/audit use. Helper, not enforced — adapters / loggers may use it.
- ADDED: `./capability-id` — adds `"secrets"` (and ensures `"secrets.bun"` is reserved for the Stage-8 adapter) to `CapabilityId`.
- ADDED: `./error-codes` — frozen `SECRETS_CODES`: `SECRET_NOT_FOUND`, `SECRET_REQUIRED_MISSING_AT_START`, `SECRET_INVALID_FORMAT`, `SOURCE_UNAVAILABLE`.
- ADDED: `./testing` — `runSecretsConformance(factory)` reusable harness.
## CapabilityId additions
This package contributes `"secrets"` to the `CapabilityId` union. The Bun-specific `"secrets.bun"` capability id is reserved here (declared in the union) for the Stage-8 `api-secrets-bun` adapter, so plugins can declare `requiredCapabilities: ["secrets.bun"]` at type-check time before the adapter ships.
## Out of scope
- Concrete sources (`api-secrets-env`, `api-secrets-bun`, `api-secrets-vault`) — Stages 4 and 8.
- Secret rotation — out of scope for v1 (separate change).
- Encryption-at-rest — source-specific.
## Source mapping
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
| Requirement | Design doc section |
|---|---|
| `ctx.secrets: SecretsLike` typed against manifest | §5.1, §5.2 |
| Required-secret enforcement at start | §3 manifest example (`"required": true`) + §4 install hook order |
| Per-secret format declaration | §3 manifest example |
| `"secrets.bun"` capability | §3 manifest example (`"optionalCapabilities": ["events.bounded-queue", "secrets.bun"]`) |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| `redactSecret` helper | Logging plugins / OTel exporters need a canonical redaction marker; pinning the string `"<redacted>"` here means every downstream tool agrees. |
| `runSecretsConformance` harness | Stage-5 deliverable (§13.4). |
## Verification approach
- `src/secrets-like/secrets-like.spec-conformance.test.ts` — `get`/`has`/`dispose` shape; `Names extends string` generic narrows return type.
- `src/descriptor/descriptor.test.ts` — Zod accept/reject suite over `SecretDescriptor`.
- `src/redaction/redaction.test.ts` — input → `"<redacted>"`; empty string handled.
- `src/capability-id/capability-id.spec-conformance.test.ts` — `"secrets"` AND `"secrets.bun"` in `CapabilityId`.
- `tests/dep-graph.test.ts` — deps: `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/types`, `zod`.
## Migration impact
None — additive only.
## Impact
### Affected dependents
- (none) — [x] no action needed (reason: net-new; concrete sources land Stage 4/8)

View 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"`

View File

@@ -0,0 +1,69 @@
# Tasks — `@suaveplan/api-secrets`
> Every checkbox MUST reflect work actually done in this change.
## 0. Pre-flight (non-negotiable)
- [x] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
- [x] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
- [x] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
## 1. Spec authoring
- [x] 1.1 Read design doc §3 (manifest secrets block), §5.1 (`ctx.secrets`), §5.2 (`defineCapability` shape)
- [x] 1.2 Read `openspec/specs/api-secrets/spec.md` (does not exist yet)
- [x] 1.3 Read `openspec/AGENTS.md` Sections 13, 14
- [x] 1.4 Draft `proposal.md`
- [x] 1.5 Source-mapping table
- [x] 1.6 Draft `specs/api-secrets/spec.md`
- [x] 1.7 `**Verified by:**` line on every requirement
## 2. Implementation
- [x] 2.1 Scaffold `packages/api/api-secrets/` (`"version": "0.1.0"`, `"sideEffects": false`, deps: `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/types`, `zod`)
- [x] 2.2 `src/secrets-like/` — `SecretsLike<Names extends string>`
- [x] 2.3 `src/descriptor/` — `SecretDescriptor` + Zod schema (`import z from "zod"`)
- [x] 2.4 `src/redaction/` — `redactSecret`
- [x] 2.5 `src/capability-id/` — augmentation adding `"secrets"` AND `"secrets.bun"`
- [x] 2.6 `src/error-codes/` — `SECRETS_CODES`
- [x] 2.7 `src/testing/` — `runSecretsConformance(factory)`
- [x] 2.8 `src/index.ts` — explicit named re-exports
- [x] 2.9 `package.json` `exports` per subpath
## 3. Tests
- [x] 3.1 Unit tests per feature
- [x] 3.2 Spec-conformance type tests (generic `Names` narrows return type)
- [x] 3.3 Property test on redaction (always returns the canonical marker)
- [x] 3.4 Every `it(...)` carries `{ timeout: 5000 }`
## 4. Documentation
- [x] 4.1 Sibling `.md` for every implementation `.ts`
- [x] 4.2 `README.md` linking every `.md`
- [x] 4.3 No CLAUDE.md update
## 5. Pre-archive verification
- [x] 5.1 `bun run check` — 0/0/0
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/api-secrets` — green
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/api-secrets` — 100/100/100/100
- [x] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/api-secrets` — exit 0
- [x] 5.5 `bunx turbo build --filter=@suaveplan/api-secrets` — green
## 6. Pre-archive 7-row checklist
- [x] 6.1 Generics on `SecretsLike<Names>`; `expectTypeOf` proves autocomplete
- [x] 6.2 Zod wired at descriptor boundary
- [x] 6.3 Every `SECRETS_CODES` entry thrown
- [x] 6.4 Required-at-start enforcement on / off tested
- [x] 6.5 OTel — N/A (sources emit)
- [x] 6.6 Lifecycle dedup — `dispose` idempotent; tested
- [x] 6.7 No retroactive tick
## 7. Archive
- [x] 7.1 Fold spec delta
- [x] 7.2 Move to `openspec/archive/<yyyy-mm-dd>-api-secrets/`
- [x] 7.3 Tick umbrella `1.9`
- [x] 7.4 Refresh `MANIFEST.md`