# foundation-fix-gap-3-secrets-slot - **Status**: PROPOSED - **Spec deltas**: - ./specs/api-host/spec.md - ./specs/api-types/spec.md ## What & why The post-Stage-4 foundation-coherence audit (Gap #3) flagged that `HostAdapters` had no `secrets` slot, so even though `@suaveplan/api-secrets-env` builds a `SecretsLike` cleanly, the host never threaded it through to plugin handlers. The e2e foundation fixture worked around this by closing over a `createEnvSecrets(...)` binding inside the route module (`buildSecrets()` in `e2e/foundation-stack/src/fixture-plugin.ts`). Real plugins would have to do the same thing — there was no canonical wiring. This change closes the gap by: 1. **`HostAdapters` gains optional `secrets?: SecretsLike`.** When omitted, the host injects `EmptySecrets` — a no-op default whose `get()` returns `undefined`, `require()` throws `SECRETS.SECRET_NOT_FOUND`, and `list()` returns the frozen empty array. Optional so legacy bootstraps stay source-compatible. 2. **`PluginContext` and (by inheritance) `RequestContext`, `JobContext`, `ConnectionContext` gain `secrets: SecretsLike`.** Plugin authors can destructure `ctx.secrets` without a null guard at every call-site; the host always supplies a value. 3. **`RequestPipelineOptions` gains required `secrets: SecretsLike`.** `createHost` wires `config.adapters.secrets ?? new EmptySecrets()` into the pipeline options and into the lifecycle driver, so both route handlers (`RequestContext.secrets`) and lifecycle hooks (`PluginContext.secrets`) see the same instance. 4. **`@suaveplan/api-host/empty-secrets` subpath export.** The class ships under a dedicated subpath so the host's `EmptySecrets` is importable for explicit fallback wiring (e.g. mode-conditional choice between `createEnvSecrets` and `EmptySecrets`). 5. **e2e/foundation-stack** drops the closure-bound `buildSecrets` helper; the route handler now reads via `ctx.secrets.require(...)` directly and the secrets adapter is wired into `HostAdapters.secrets` once at boot. ## Impact ### Affected dependents - `@suaveplan/api-types` — `PluginContext.secrets` is a new REQUIRED field on every context flavour. The 6 in-tree consumers that reference `RequestContext` via cast-narrowing or generic parameter (api-graphql, api-router, api-router-hono — `as RequestContext`, `Parameters[2]` typing) are unaffected because they do not construct context literals. The only literal-construction site is `@suaveplan/api-host/request-pipeline-helpers.ts`'s `buildRequestContext`, which is updated atomically. - `@suaveplan/api-host` — gains `EmptySecrets` class + `./empty-secrets` subpath export. `HostAdapters.secrets` is optional. Pipeline options + lifecycle driver options both gain a threaded `secrets` slot. Dependency on `@suaveplan/api-secrets` is added so the host can reuse the `SECRETS_CODES.SECRET_NOT_FOUND` error code namespace and `SecretsError` subclass — no new error namespaces are introduced. - `@suaveplan/e2e-foundation-stack` — `buildAdapters` wires `createEnvSecrets({ allowedKeys: ["FOUNDATION_SECRET"], source: ... })` into `HostAdapters.secrets`. The fixture plugin's `/api/secret-key` route handler now resolves via `ctx.secrets.require("FOUNDATION_SECRET")` instead of a hand-rolled closure. ### Backward compatibility `HostAdapters.secrets` is optional — bootstraps that do not set it receive the `EmptySecrets` default. `RequestContext.secrets` / `PluginContext.secrets` are new REQUIRED fields on the public type, but the only literal-construction site in the monorepo is owned by api-host itself (updated atomically). All other consumers use type-narrowing casts or generic parameter typing and are unaffected at compile time. The new pipeline `secrets` option is REQUIRED on `RequestPipelineOptions` so the host cannot accidentally construct a pipeline without threading the slot. Tests + the lifecycle driver construct an `EmptySecrets()` explicitly in the same commit. ## Parity Checklist This is a genesis-original fix; no upstream parity table required. ## Verification `bunx turbo lint typecheck test build --filter=@suaveplan/api-host --filter=@suaveplan/api-types --filter=@suaveplan/e2e-foundation-stack` → green. `bunx turbo test:coverage --filter=@suaveplan/api-host --filter=@suaveplan/api-types` → touched files at 100% line coverage; `empty-secrets.ts` has 75% bun function coverage (3 of 4 — the constructor counts as a function under `bun:test`'s naive counter even though every test instantiates it). Vitest/istanbul function coverage is 100%. E2E (Playwright) — all 10 foundation-stack tests pass post-shim removal, including the secrets-redaction test that now exercises the pipeline-threaded `ctx.secrets.require(...)` path. Tests added: - `request-pipeline-secrets.test.ts` — handler reads `ctx.secrets.require("FOO")` via a wired adapter (round-trip); default `EmptySecrets` throws `SECRETS.SECRET_NOT_FOUND` on `require`; `ctx.secrets` is the same reference passed via options. - `empty-secrets.test.ts` — get returns undefined, require throws `SECRETS.SECRET_NOT_FOUND` with `{ name, source }` context, list returns a frozen empty array stable across calls.