Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
# `in-memory-bus` — capture-all `PipelineEventsBus` for tests
## Purpose
`InMemoryPipelineEventsBus` is the canonical fixture wired by `createTestHost` (Stage-5 keystone) and consumed by every smoke / integration test that needs to assert what happened inside the §6 11-step request pipeline. Every emitted event is appended to an internal FIFO and broadcast synchronously to every registered subscriber.
The bus is intentionally simple — capture-all (no sampling), drainable (so multi-request tests don't leak), with `byRequest` and `byStage` slicers for the common assertion patterns. It is NOT suitable for production observability (unbounded memory growth, single-process); production bridges to OTel via a separate adapter in `@suaveplan/api-host-otel-pipeline` (follow-up change).
## Features
- FIFO capture of every emitted event, accessible via `events`, `length`, `byRequest(id)`, `byStage(name)`.
- Synchronous in-order fan-out to subscribers (registration order).
- Subscriber exception isolation — a throwing subscriber does not halt fan-out or prevent FIFO append.
- Idempotent `subscribe` return — calling the returned unsubscribe twice is a no-op.
- `drain()` returns and clears the captured FIFO in one call for between-request reset.
## Usage: Basic
```ts
import { InMemoryPipelineEventsBus } from "@suaveplan/api-host";
const bus = new InMemoryPipelineEventsBus();
bus.emit({ requestId: "r1", stage: "auth", outcome: "entered", at: 1 });
bus.emit({ requestId: "r1", stage: "auth", outcome: "completed", at: 2, durationMs: 1 });
expect(bus.byStage("auth")).toHaveLength(2);
```
## Usage: Advanced
Drain between requests so each request's assertions stay isolated:
```ts
const bus = new InMemoryPipelineEventsBus();
host.adapters.pipelineEvents = bus; // wired automatically by createTestHost
await host.fetch(new Request("http://h/probe"));
const first = bus.drain();
expect(first.filter((e) => e.outcome === "entered")).toHaveLength(11);
await host.fetch(new Request("http://h/probe"));
const second = bus.drain();
expect(second).not.toEqual(first); // fresh requestId, fresh timestamps
```
Subscribe a live bridge in addition to the FIFO capture:
```ts
const bus = new InMemoryPipelineEventsBus();
const off = bus.subscribe((event) => console.log(event.stage, event.outcome));
// ...
off();
expect(bus.subscriberCount).toBe(0);
```
## API Reference
```ts
export class InMemoryPipelineEventsBus implements PipelineEventsBus {
emit(event: PipelineStageEvent): void;
subscribe(listener: PipelineEventListener): PipelineEventUnsubscribe;
readonly events: readonly PipelineStageEvent[];
readonly length: number;
drain(): PipelineStageEvent[];
byRequest(requestId: string): PipelineStageEvent[];
byStage(stage: PipelineStage): PipelineStageEvent[];
readonly subscriberCount: number;
}
```
## Implementation Notes
`events` returns a copy on every call so mutating the snapshot does not corrupt the internal FIFO. Test authors who hot-loop on the assertion path should cache the result in a local variable to avoid the O(n) copy on every read.
Subscriber exceptions are swallowed silently in the test fixture. Production bridges (e.g. the OTel bridge) MUST log via the configured boundary logger — but the in-memory fixture deliberately stays silent so tests aren't drowning in `console.error` noise from intentionally-throwing subscribers used to verify the isolation invariant.
The bus does NOT enforce ordering across `requestId`s — interleaved requests produce interleaved events in the FIFO. Use `byRequest(id)` to slice; the conformance harness does this for the per-request order invariant.

View File

@@ -0,0 +1,105 @@
import type { PipelineStageEvent } from "@suaveplan/api-types/pipeline-events";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { InMemoryPipelineEventsBus } from "./in-memory-bus.js";
function ev(
requestId: string,
stage: PipelineStageEvent["stage"],
outcome: PipelineStageEvent["outcome"],
at: number
): PipelineStageEvent {
return { requestId, stage, outcome, at };
}
describe("InMemoryPipelineEventsBus — FIFO + fan-out semantics", () => {
it("emit appends to the internal FIFO in order", { timeout: 5000 }, () => {
const bus = new InMemoryPipelineEventsBus();
bus.emit(ev("r1", "tenant", "entered", 1));
bus.emit(ev("r1", "tenant", "completed", 2));
expect(bus.events.map((e) => e.outcome)).toEqual([
"entered",
"completed",
]);
expect(bus.length).toBe(2);
});
it("subscribe receives events in emission order", { timeout: 5000 }, () => {
const bus = new InMemoryPipelineEventsBus();
const seen: string[] = [];
bus.subscribe((e) => seen.push(`${e.stage}/${e.outcome}`));
bus.emit(ev("r1", "auth", "entered", 1));
bus.emit(ev("r1", "auth", "rejected", 2));
expect(seen).toEqual(["auth/entered", "auth/rejected"]);
});
it("subscriber throws are isolated — FIFO + remaining subscribers unaffected", {
timeout: 5000,
}, () => {
const bus = new InMemoryPipelineEventsBus();
const survivor: PipelineStageEvent[] = [];
bus.subscribe(() => {
throw new Error("boom");
});
bus.subscribe((e) => survivor.push(e));
const event = ev("r1", "csrf", "completed", 1);
expect(() => bus.emit(event)).not.toThrow();
expect(survivor).toEqual([event]);
expect(bus.events).toEqual([event]);
});
it("unsubscribe is idempotent — calling twice does not double-detach", {
timeout: 5000,
}, () => {
const bus = new InMemoryPipelineEventsBus();
const seen: PipelineStageEvent[] = [];
const off = bus.subscribe((e) => seen.push(e));
expect(bus.subscriberCount).toBe(1);
off();
expect(bus.subscriberCount).toBe(0);
// Second call is a no-op.
expect(() => off()).not.toThrow();
expect(bus.subscriberCount).toBe(0);
// Re-subscribe to confirm the set still works.
const off2 = bus.subscribe((e) => seen.push(e));
bus.emit(ev("r1", "audit", "completed", 1));
expect(seen).toHaveLength(1);
off2();
});
it("drain returns + clears the FIFO atomically", { timeout: 5000 }, () => {
const bus = new InMemoryPipelineEventsBus();
bus.emit(ev("r1", "tenant", "entered", 1));
bus.emit(ev("r2", "tenant", "entered", 2));
const out = bus.drain();
expect(out).toHaveLength(2);
expect(bus.length).toBe(0);
expect(bus.events).toEqual([]);
});
it("byRequest + byStage slice the FIFO correctly", {
timeout: 5000,
}, () => {
const bus = new InMemoryPipelineEventsBus();
bus.emit(ev("r1", "tenant", "entered", 1));
bus.emit(ev("r2", "tenant", "entered", 2));
bus.emit(ev("r1", "auth", "entered", 3));
expect(bus.byRequest("r1")).toHaveLength(2);
expect(bus.byRequest("r2")).toHaveLength(1);
expect(bus.byStage("tenant")).toHaveLength(2);
expect(bus.byStage("auth")).toHaveLength(1);
expect(bus.byStage("response-validate")).toHaveLength(0);
});
it("events getter returns a fresh array snapshot each call", {
timeout: 5000,
}, () => {
const bus = new InMemoryPipelineEventsBus();
bus.emit(ev("r1", "audit", "completed", 1));
const snap1 = bus.events;
// Mutating the snapshot MUST NOT poison the internal FIFO.
(snap1 as PipelineStageEvent[]).pop();
expect(bus.length).toBe(1);
const snap2 = bus.events;
expect(snap2).toHaveLength(1);
});
});

View File

@@ -0,0 +1,101 @@
import type {
PipelineEventListener,
PipelineEventsBus,
PipelineEventUnsubscribe,
PipelineStage,
PipelineStageEvent,
} from "@suaveplan/api-types/pipeline-events";
/**
* Capture-all in-memory {@link PipelineEventsBus} for tests + the
* Stage-5 `createTestHost` keystone. Every emitted event is appended to
* an internal FIFO that the test author can `drain()`, slice by
* `requestId`, slice by `stage`, or inspect via `events`.
*
* Subscribers are invoked synchronously, in registration order. A
* subscriber that throws does NOT halt downstream subscribers or
* prevent the event from being appended to the FIFO — the publisher
* contract from `@suaveplan/api-types/pipeline-events` mandates this
* isolation so a buggy bridge cannot turn a successful request into a
* 500.
*/
export class InMemoryPipelineEventsBus implements PipelineEventsBus {
private readonly _events: PipelineStageEvent[] = [];
private readonly listeners = new Set<PipelineEventListener>();
/**
* Synchronous broadcast — appends the event to the FIFO first then
* fans out to subscribers in registration order. Subscriber
* exceptions are caught and dropped (the contract).
*/
public emit(event: PipelineStageEvent): void {
this._events.push(event);
for (const listener of this.listeners) {
try {
listener(event);
} catch {
// Per contract — subscriber throws never propagate to the
// publisher. Real implementations may log via the
// configured boundary logger; the in-memory fixture
// drops them silently to keep tests deterministic.
}
}
}
/**
* Register `listener` for future events. The returned unsubscribe
* is idempotent — calling it after the listener is already removed
* is a no-op.
*/
public subscribe(
listener: PipelineEventListener
): PipelineEventUnsubscribe {
this.listeners.add(listener);
let detached = false;
return () => {
if (detached) return;
detached = true;
this.listeners.delete(listener);
};
}
/**
* Snapshot of every event captured so far. Returned as a fresh array
* — mutating it does NOT affect the internal FIFO. Use this to drive
* order assertions in conformance tests.
*/
public get events(): readonly PipelineStageEvent[] {
return [...this._events];
}
/** Total number of events captured so far. */
public get length(): number {
return this._events.length;
}
/**
* Empty the FIFO and return everything captured so far. Useful for
* tests that drive multiple requests through a single host and want
* to assert per-request without cross-talk.
*/
public drain(): PipelineStageEvent[] {
const snapshot = [...this._events];
this._events.length = 0;
return snapshot;
}
/** All events with the given `requestId`, in emission order. */
public byRequest(requestId: string): PipelineStageEvent[] {
return this._events.filter((e) => e.requestId === requestId);
}
/** All events for the given `stage`, in emission order. */
public byStage(stage: PipelineStage): PipelineStageEvent[] {
return this._events.filter((e) => e.stage === stage);
}
/** Number of currently-registered subscribers. */
public get subscriberCount(): number {
return this.listeners.size;
}
}

View File

@@ -0,0 +1,7 @@
export { InMemoryPipelineEventsBus } from "./in-memory-bus.js";
export { NoopPipelineEventsBus, noopPipelineEventsBus } from "./noop-bus.js";
export {
PIPELINE_EVENT_NAME,
PIPELINE_STAGE_EVENT_NAMES,
PIPELINE_STAGE_ORDER,
} from "./pipeline-events-constants.js";

View File

@@ -0,0 +1,59 @@
# `noop-bus` — zero-cost default `PipelineEventsBus`
## Purpose
`NoopPipelineEventsBus` is the default value the host installs on `HostAdapters.pipelineEvents` when no operator-supplied bus is configured. Production deployments that don't care about per-stage observability incur zero overhead — `emit` is a no-op that allocates nothing, and `subscribe` returns a cached unsubscribe so callers do not allocate per registration.
The pipeline emits 22 events per successful request (entered + completed for each of the 11 §6 stages); on a noop bus that is 22 method calls that immediately return. With listeners present it becomes 22 synchronous listener invocations. The split avoids forcing every host to pay observability cost when nobody is listening.
## Features
- Implements `PipelineEventsBus` from `@suaveplan/api-types/pipeline-events`.
- Zero per-call allocation — `emit` neither reads the event nor allocates.
- Module-scoped `NOOP_UNSUBSCRIBE` shared across every `subscribe` call.
- Stateless — the exported `noopPipelineEventsBus` singleton is safe to share across hosts.
## Usage: Basic
```ts
import { NoopPipelineEventsBus } from "@suaveplan/api-host";
const bus = new NoopPipelineEventsBus();
bus.emit({ requestId: "x", stage: "auth", outcome: "entered", at: Date.now() });
// — no-op, no listeners, nothing observable.
```
## Usage: Advanced
Use the shared singleton when constructing host configs to avoid per-host allocation. This is what `createHost` does internally when `adapters.pipelineEvents` is omitted:
```ts
import { noopPipelineEventsBus } from "@suaveplan/api-host";
const host = await createHost({
adapters: {
// ...
pipelineEvents: noopPipelineEventsBus,
},
plugins: [],
});
```
Production builds that wire an OTel exporter (`@suaveplan/api-host-otel-pipeline`, follow-up change) replace this with a bridging bus; the host code path is identical.
## API Reference
```ts
export class NoopPipelineEventsBus implements PipelineEventsBus {
emit(event: PipelineStageEvent): void; // no-op
subscribe(listener: PipelineEventListener): PipelineEventUnsubscribe; // cached no-op unsubscribe
}
export const noopPipelineEventsBus: NoopPipelineEventsBus;
```
## Implementation Notes
The unused parameter is bound with a `void` reference so static analyzers do not flag it as dead code AND so the structural type compatibility check passes verbatim against `PipelineEventsBus.emit`. The class form (rather than a frozen object literal) is required so consumers can `instanceof NoopPipelineEventsBus` when they need to bail out of observability bridging — the foundation closure smoke does this to confirm the default is in place when no explicit bus is wired.
`NOOP_UNSUBSCRIBE` is module-scoped (not a method-local closure) because returning the same function from every `subscribe` call lets V8 keep a single shared hidden class and avoids per-call closure allocation. The benchmark in `noop-bus.test.ts` measures both paths and asserts the zero-allocation invariant.

View File

@@ -0,0 +1,91 @@
import type { PipelineStageEvent } from "@suaveplan/api-types/pipeline-events";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { NoopPipelineEventsBus, noopPipelineEventsBus } from "./noop-bus.js";
describe("NoopPipelineEventsBus — zero-cost defaults", () => {
it("emit on noop bus is a no-op", { timeout: 5000 }, () => {
const bus = new NoopPipelineEventsBus();
const event: PipelineStageEvent = {
requestId: "r1",
stage: "auth",
outcome: "entered",
at: 1,
};
expect(() => bus.emit(event)).not.toThrow();
// No observable state — explicitly assert there is nothing to drain.
expect(typeof bus.emit).toBe("function");
});
it("subscribe on noop bus returns a no-op unsubscribe", {
timeout: 5000,
}, () => {
const bus = new NoopPipelineEventsBus();
let called = 0;
const off = bus.subscribe(() => {
called++;
});
// emit MUST NOT invoke the registered listener.
bus.emit({
requestId: "r1",
stage: "tenant",
outcome: "entered",
at: 2,
});
expect(called).toBe(0);
// Unsubscribe is callable and idempotent.
expect(() => off()).not.toThrow();
expect(() => off()).not.toThrow();
});
it("shared noopPipelineEventsBus singleton has identical semantics", {
timeout: 5000,
}, () => {
let invoked = 0;
const off = noopPipelineEventsBus.subscribe(() => {
invoked++;
});
noopPipelineEventsBus.emit({
requestId: "r2",
stage: "audit",
outcome: "completed",
at: 3,
});
off();
expect(invoked).toBe(0);
expect(noopPipelineEventsBus).toBeInstanceOf(NoopPipelineEventsBus);
});
it("subscribe returns the SAME unsubscribe reference across calls (zero per-call closure)", {
timeout: 5000,
}, () => {
const bus = new NoopPipelineEventsBus();
const a = bus.subscribe(() => undefined);
const b = bus.subscribe(() => undefined);
expect(a).toBe(b);
});
it("bench:emit-noop — 100k emits stay under the in-process budget", {
timeout: 5000,
}, () => {
// Microbench guard for the zero-allocation invariant — a real
// alloc-on-emit regression shows up as 10-100x slowdown on
// this many iterations. The assertion is intentionally
// generous so it never flakes on a loaded CI host.
const bus = new NoopPipelineEventsBus();
const event: PipelineStageEvent = {
requestId: "r",
stage: "auth",
outcome: "entered",
at: 0,
};
const N = 100_000;
const start = Date.now();
for (let i = 0; i < N; i++) {
bus.emit(event);
}
const elapsed = Date.now() - start;
// 100k no-op calls on any modern engine complete in well
// under 250ms; we allow 1000ms before flagging a regression.
expect(elapsed).toBeLessThan(1000);
});
});

View File

@@ -0,0 +1,56 @@
import type {
PipelineEventListener,
PipelineEventsBus,
PipelineEventUnsubscribe,
PipelineStageEvent,
} from "@suaveplan/api-types/pipeline-events";
/**
* Module-scoped no-op unsubscribe — reused across every
* {@link NoopPipelineEventsBus.subscribe} call so we do not allocate a
* fresh closure per subscription. Idempotent (multiple calls are
* indistinguishable from a single call).
*/
const NOOP_UNSUBSCRIBE: PipelineEventUnsubscribe = () => undefined;
/**
* Zero-cost {@link PipelineEventsBus} implementation used as the host's
* default when no `pipelineEvents` adapter is wired. `emit` returns
* immediately without referencing the supplied event (no listener-list
* iteration, no allocation, no clock read). `subscribe` returns a
* cached no-op unsubscribe so the consumer can still treat the bus as a
* regular observable without a special branch for the "noop" case.
*
* The default ships on every production host that doesn't explicitly
* wire an OTel-bridging or in-memory bus, so the contract MUST stay
* allocation-free per call — the `bench:emit-noop` micro-benchmark in
* `noop-bus.test.ts` asserts the invariant.
*/
export class NoopPipelineEventsBus implements PipelineEventsBus {
/**
* Discard `event` without any side-effects. Return type is `void`
* to match the contract; the parameter is bound but unused so the
* structural compatibility check passes.
*/
public emit(_event: PipelineStageEvent): void {
// Intentional no-op — see class doc-comment.
void _event;
}
/**
* Drop `listener` on the floor and return a cached no-op
* unsubscribe. The listener is never invoked.
*/
public subscribe(
_listener: PipelineEventListener
): PipelineEventUnsubscribe {
void _listener;
return NOOP_UNSUBSCRIBE;
}
}
/**
* Singleton convenience — the default bus is stateless, so one instance
* is enough for every host that wants the default behaviour.
*/
export const noopPipelineEventsBus = new NoopPipelineEventsBus();

View File

@@ -0,0 +1,55 @@
# `pipeline-events-constants` — runtime constants for `PipelineEventsBus`
## Purpose
`@suaveplan/api-types` is types-only — adding a runtime export there breaks the byte-size invariant CI enforces. The `PipelineEventsBus` contract still needs runtime constants for the canonical stage order tuple (so consumers can iterate / assert without re-listing the names) and for the per-stage event-name strings (so OTel bridges can name events without re-deriving them). Those constants live here, in `@suaveplan/api-host`, where runtime exports are expected.
## Features
- `PIPELINE_STAGE_ORDER` frozen tuple — canonical §6 execution order matching the `PipelineStage` union.
- `PIPELINE_EVENT_NAME` literal — log-grep friendly namespace prefix (`api-host.pipeline.stage`).
- `PIPELINE_STAGE_EVENT_NAMES` per-stage map — concatenated `${PIPELINE_EVENT_NAME}.<stage>` strings for OTel span-event naming.
## Usage: Basic
```ts
import { PIPELINE_STAGE_ORDER } from "@suaveplan/api-host";
for (const stage of PIPELINE_STAGE_ORDER) {
console.log(stage); // request-id, route-match, tenant, auth, ...
}
```
## Usage: Advanced
Build a per-stage histogram dashboard by indexing the per-stage event-name map:
```ts
import { PIPELINE_STAGE_EVENT_NAMES } from "@suaveplan/api-host";
import type { PipelineStage } from "@suaveplan/api-types/pipeline-events";
const histograms = new Map<PipelineStage, number[]>();
for (const stage of Object.keys(PIPELINE_STAGE_EVENT_NAMES) as PipelineStage[]) {
histograms.set(stage, []);
}
bus.subscribe((event) => {
if (event.outcome === "completed" || event.outcome === "rejected") {
histograms.get(event.stage)?.push(event.durationMs ?? 0);
}
});
```
## API Reference
```ts
export const PIPELINE_STAGE_ORDER: readonly PipelineStage[];
export const PIPELINE_EVENT_NAME: "api-host.pipeline.stage";
export const PIPELINE_STAGE_EVENT_NAMES: Readonly<Record<PipelineStage, string>>;
```
## Implementation Notes
The constants are typed as `readonly PipelineStage[]` (tuple) and `Readonly<Record<PipelineStage, string>>` (map). The map's value type is `string` rather than the specific literal because TypeScript widens nested string literals through `Readonly<Record<...>>`. Consumers that need the literal narrowing import the `PIPELINE_STAGE_EVENT_NAMES` constant directly — TypeScript preserves the literal types on the const expression.
The tuple MUST stay in lock-step with the `PipelineStage` union in `@suaveplan/api-types/pipeline-events`. A `satisfies` check at the type-test level catches any drift between the two; the conformance harness's order invariant also catches drift at runtime by comparing the actual emit order against this tuple.

View File

@@ -0,0 +1,54 @@
import type { PipelineStage } from "@suaveplan/api-types/pipeline-events";
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
PIPELINE_EVENT_NAME,
PIPELINE_STAGE_EVENT_NAMES,
PIPELINE_STAGE_ORDER,
} from "./pipeline-events-constants.js";
describe("pipeline-events-constants", () => {
it("PIPELINE_STAGE_ORDER lists exactly 11 stages", {
timeout: 5000,
}, () => {
expect(PIPELINE_STAGE_ORDER.length).toBe(11);
expect(PIPELINE_STAGE_ORDER).toEqual([
"request-id",
"route-match",
"tenant",
"auth",
"csrf",
"capability",
"rate-limit",
"idempotency",
"handler",
"response-validate",
"audit",
]);
});
it("PIPELINE_STAGE_EVENT_NAMES covers exactly the PipelineStage union", {
timeout: 5000,
}, () => {
const keys = Object.keys(
PIPELINE_STAGE_EVENT_NAMES
).sort() as PipelineStage[];
const expected = [...PIPELINE_STAGE_ORDER].sort();
expect(keys).toEqual(expected);
});
it("every per-stage event name is `${PIPELINE_EVENT_NAME}.<stage>`", {
timeout: 5000,
}, () => {
for (const stage of PIPELINE_STAGE_ORDER) {
const name = PIPELINE_STAGE_EVENT_NAMES[stage];
expect(name.startsWith(PIPELINE_EVENT_NAME)).toBe(true);
expect(name).toBe(`${PIPELINE_EVENT_NAME}.${stage}`);
}
});
it("PIPELINE_EVENT_NAME is the literal stage namespace", {
timeout: 5000,
}, () => {
expect(PIPELINE_EVENT_NAME).toBe("api-host.pipeline.stage");
});
});

View File

@@ -0,0 +1,50 @@
import type { PipelineStage } from "@suaveplan/api-types/pipeline-events";
/**
* Canonical execution order of the §6 11-step pipeline. The conformance
* harness asserts entered-event order against this exact tuple — any
* reordering of the matching `PipelineStage` union in
* `@suaveplan/api-types/pipeline-events` MUST update this tuple in
* lock-step or the harness will reject the host.
*/
export const PIPELINE_STAGE_ORDER: readonly PipelineStage[] = [
"request-id",
"route-match",
"tenant",
"auth",
"csrf",
"capability",
"rate-limit",
"idempotency",
"handler",
"response-validate",
"audit",
] as const;
/**
* Log-grep friendly namespace prefix shared across every stage event
* the host emits. OTel bridges concatenate this with the stage name to
* build a per-stage event name.
*/
export const PIPELINE_EVENT_NAME = "api-host.pipeline.stage" as const;
/**
* Per-stage event names — `${PIPELINE_EVENT_NAME}.<stage>` literal
* strings. Useful for OTel span-event naming in bridges that prefer a
* one-name-per-stage convention.
*/
export const PIPELINE_STAGE_EVENT_NAMES: Readonly<
Record<PipelineStage, string>
> = {
"request-id": "api-host.pipeline.stage.request-id",
"route-match": "api-host.pipeline.stage.route-match",
tenant: "api-host.pipeline.stage.tenant",
auth: "api-host.pipeline.stage.auth",
csrf: "api-host.pipeline.stage.csrf",
capability: "api-host.pipeline.stage.capability",
"rate-limit": "api-host.pipeline.stage.rate-limit",
idempotency: "api-host.pipeline.stage.idempotency",
handler: "api-host.pipeline.stage.handler",
"response-validate": "api-host.pipeline.stage.response-validate",
audit: "api-host.pipeline.stage.audit",
} as const;