3.5 KiB
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 asecretsslot - THEN every
RequestContext.secrets.require(<name>)andPluginContext.secrets.require(<name>)throwsSECRETS.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
SecretsErroris thrown withcode === "SECRETS.SECRET_NOT_FOUND"andcontext.name === "X_API_KEY"andcontext.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.secretsinside 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 withadapters.secrets - THEN
ctx.secretsis referentially identical to that adapter (orEmptySecretswhen no adapter is configured)