Files
2026-05-27 23:29:53 +01:00

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 a secrets slot
  • THEN every RequestContext.secrets.require(<name>) and PluginContext.secrets.require(<name>) 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)