Initial Commit
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# 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)
|
||||
@@ -0,0 +1,45 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: api-types (foundation-fix-gap-3-secrets-slot)
|
||||
|
||||
**Package**: `@suaveplan/api-types`
|
||||
**Status**: PROPOSED
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: PluginContext shape
|
||||
|
||||
`PluginContext<Caps>` SHALL include a REQUIRED `secrets: SecretsLike`
|
||||
field referencing `@suaveplan/api-types/secrets`'s `SecretsLike`
|
||||
contract. Plugin authors implementing `install`/`start`/`stop`/
|
||||
`uninstall`/`health` hooks can resolve manifested secrets via
|
||||
`ctx.secrets.get(name)` or `ctx.secrets.require(name)` without a
|
||||
null-check; the host SHALL always wire a concrete adapter (or the
|
||||
`EmptySecrets` default).
|
||||
|
||||
#### Scenario: PluginContext.secrets is reachable
|
||||
|
||||
- **WHEN** a plugin hook destructures `{ secrets } = ctx`
|
||||
- **THEN** `secrets` is typed as `SecretsLike` and resolves at runtime to whichever adapter the host configured
|
||||
|
||||
### Requirement: RequestContext inherits secrets
|
||||
|
||||
`RequestContext<Caps>` SHALL extend `Omit<PluginContext<Caps>,
|
||||
"capabilities">` so the `secrets: SecretsLike` field flows through to
|
||||
route handlers unchanged. Same for `JobContext<Caps>` and
|
||||
`ConnectionContext<Caps>` — both inherit `secrets` from the base.
|
||||
|
||||
#### Scenario: route handler reads ctx.secrets
|
||||
|
||||
- **WHEN** a route handler types its argument as `RequestContext<readonly ["db"]>` and reads `await ctx.secrets.require("STRIPE_KEY")`
|
||||
- **THEN** the call typechecks AND returns the resolved value (or throws `SECRETS.SECRET_NOT_FOUND` if no source carries the key)
|
||||
|
||||
#### Scenario: job handler reads ctx.secrets
|
||||
|
||||
- **WHEN** a job invocation typed as `JobContext<Caps>` reads `await ctx.secrets.get("OPTIONAL_KEY")`
|
||||
- **THEN** the call typechecks AND returns `string | undefined`
|
||||
|
||||
#### Scenario: connection handler reads ctx.secrets
|
||||
|
||||
- **WHEN** a WebSocket/SSE handler typed as `ConnectionContext<Caps>` reads `ctx.secrets.list()`
|
||||
- **THEN** the call typechecks AND returns `readonly string[]`
|
||||
Reference in New Issue
Block a user