Initial Commit
This commit is contained in:
69
openspec/archive/2026-05-25-api-secrets/proposal.md
Normal file
69
openspec/archive/2026-05-25-api-secrets/proposal.md
Normal 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)
|
||||
@@ -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"`
|
||||
69
openspec/archive/2026-05-25-api-secrets/tasks.md
Normal file
69
openspec/archive/2026-05-25-api-secrets/tasks.md
Normal 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`
|
||||
Reference in New Issue
Block a user