# Capability Spec Delta: api-host (foundation-fix-gap-3-secrets-slot) **Package**: `@suaveplan/api-host` **Status**: PROPOSED ## MODIFIED Requirements ### Requirement: HostAdapters shape The package SHALL export `HostAdapters` with a `secrets?: SecretsLike` optional slot. When the operator omits `secrets`, `createHost` SHALL inject `new EmptySecrets()` into the pipeline options and lifecycle driver options so every `ctx.secrets` access on a route handler or plugin lifecycle hook resolves to a defined `SecretsLike` value. #### Scenario: secrets omitted → EmptySecrets default - **WHEN** `createHost({ adapters: { router, auth, tenant, capabilities, rateLimit, idempotency, csrf, audit, span, db } })` is called without a `secrets` slot - **THEN** every `RequestContext.secrets.require()` and `PluginContext.secrets.require()` throws `SECRETS.SECRET_NOT_FOUND` #### Scenario: secrets supplied → adapter is threaded - **WHEN** `createHost({ adapters: { ..., secrets: createEnvSecrets({ allowedKeys: ["FOO"], source: () => ({ FOO: "bar" }) }) } })` is called - **THEN** a handler's `await ctx.secrets.require("FOO")` resolves to `"bar"` ### Requirement: EmptySecrets default The package SHALL ship `EmptySecrets` implementing `SecretsLike` from `@suaveplan/api-types/secrets`. `get(name)` MUST always resolve to `undefined`. `require(name)` MUST throw `SecretsError` carrying the canonical `SECRETS.SECRET_NOT_FOUND` code (sourced from `@suaveplan/api-secrets`'s `SECRETS_CODES`) and a `context` field of shape `{ name, source: "host.default.empty-secrets" }`. `list()` MUST return a frozen empty `readonly string[]`. The class MUST be reachable via `import { EmptySecrets } from "@suaveplan/api-host/empty-secrets"` and from the root barrel. #### Scenario: get returns undefined - **WHEN** `await new EmptySecrets().get("anything")` is called - **THEN** the resolved value is `undefined` #### Scenario: require throws SECRETS.SECRET_NOT_FOUND - **WHEN** `await new EmptySecrets().require("X_API_KEY")` is called - **THEN** a `SecretsError` is thrown with `code === "SECRETS.SECRET_NOT_FOUND"` and `context.name === "X_API_KEY"` and `context.source === "host.default.empty-secrets"` #### Scenario: list is the frozen empty array - **WHEN** `new EmptySecrets().list()` is called twice - **THEN** both calls return the same frozen `readonly string[]` reference whose length is 0 ### Requirement: RequestPipeline secrets wiring `RequestPipelineOptions` SHALL include a REQUIRED `secrets: SecretsLike` field. The pipeline driver SHALL set `RequestContext.secrets` to the configured `secrets` value on every constructed handler context. #### Scenario: pipeline threads secrets into ctx - **WHEN** a route handler accesses `ctx.secrets` inside the handler body - **THEN** the value is referentially identical to `pipelineOptions.secrets` ### Requirement: Lifecycle driver secrets wiring `LifecycleDriverOptions` SHALL include an optional `secrets?: SecretsLike` field. When omitted the driver SHALL default to `new EmptySecrets()`. The driver's `callUserHook` SHALL construct a minimal `PluginContext`- shaped object including the `plugin: { name, version }` identity and `secrets` slot before invoking the user-supplied hook. #### Scenario: lifecycle hook receives secrets - **WHEN** a plugin's `start(ctx)` hook is invoked by the lifecycle driver against a host configured with `adapters.secrets` - **THEN** `ctx.secrets` is referentially identical to that adapter (or `EmptySecrets` when no adapter is configured)