first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
# emitter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createOpenapiEmitter` is the aggregator the spec calls for: it ingests
every plugin's manifest, every registered route (with its Zod-converted
schemas), the active auth adapter's security scheme, and the host
config, then produces a single OpenAPI 3.1 document covering the entire
mounted plugin set. The emitter is the single source of truth served at
`/openapi.json` and consumed by the docs UI sibling packages.
By centralising emission, the host eliminates drift: every plugin's
routes flow through the same namespacing rules, the same strictness
policy, and the same security inheritance. Plugins cannot hand-author
OpenAPI fragments that disagree with what their Hono runtime actually
serves.
## Features
- **Manifest-driven tags + components** — `displayName` becomes the tag,
`<pluginName>.<TypeName>` becomes the component schema prefix.
- **Deterministic operationIds** — every route receives
`<plugin>.<method>.<oasPath>`. Collisions raise the moment they
appear.
- **Security inheritance** — every operation lists the active auth
adapter's scheme except for routes listed in any plugin's
`publicRoutes`, which receive `security: []`.
- **Strictness gate** — routes missing request + response schemas warn
(with an OTel counter) or refuse install, per `host.openapi.strictness`.
- **Per-plugin filtered view** — `getDocumentForPlugin(name)` returns
only operations tagged with that plugin; misses raise
`OPENAPI.UNKNOWN_PLUGIN_IN_FILTER`.
- **JSON + YAML serialisation** — `getJson()` / `getYaml()` produce
serialised bodies for the HTTP endpoints.
- **OTel emission + conversion-error counters** — visibility on
document production and Zod→OAS conversion failures.
- **Lifecycle dedup** — the constructed document is memoised; repeated
`getDocument()` calls return the same reference.
## Usage: Basic
```ts
import { createOpenapiEmitter } from "@suaveplan/api-openapi/emitter";
const emitter = createOpenapiEmitter({
manifests: [
{ name: "billing", displayName: "Billing", description: "Invoices" },
],
routes: [
{
pluginName: "billing",
route: { method: "GET", path: "/invoices/:id", tags: [] },
responseSchema: { type: "object" },
},
],
authAdapter: {
name: "Bearer",
scheme: { type: "http", scheme: "bearer" },
},
hostConfig: {
info: { title: "host", version: "1.0.0" },
},
});
emitter.getJson();
```
## Usage: Advanced
```ts
import { createOpenapiEmitter } from "@suaveplan/api-openapi/emitter";
import { z } from "zod";
const Invoice = z.object({ id: z.string() });
const emitter = createOpenapiEmitter({
manifests: [{ name: "billing", displayName: "Billing" }],
routes: [
{
pluginName: "billing",
route: { method: "GET", path: "/invoices/:id", tags: [] },
// Thunk → emitter wraps a thrown converter error in
// OPENAPI.SCHEMA_CONVERSION_FAILED and increments the
// conversion counter.
responseSchema: () => ({ $ref: "#/components/schemas/billing.Invoice" }),
components: { Invoice: { type: "object" } },
},
],
hostConfig: {
info: { title: "host", version: "1.0.0" },
environment: "production",
openapi: { strictness: "refuse-install" },
},
});
emitter.getDocumentForPlugin("billing");
```
## API Reference
- `createOpenapiEmitter(input) → OpenapiEmitter`.
- `OpenapiEmitter.getDocument() → OpenAPIDocument` — memoised.
- `OpenapiEmitter.getJson() / getYaml() → string`.
- `OpenapiEmitter.getDocumentForPlugin(name) → OpenAPIDocument`.
- `OpenapiEmitter.getPluginNames() → readonly string[]`.
- `OpenapiEmitter.getStrictness() → StrictnessMode`.
- `EmitterPluginManifest`, `EmitterRouteInput`, `EmitterAuthAdapter`,
`EmitterHostConfig` — input bag types.
- `__resetEmitterCounters()` — test-only seam.
- Constants: `EMISSION_COUNTER_NAME`, `CONVERSION_ERROR_COUNTER_NAME`.
## Implementation Notes
- Schemas may be supplied as a pre-converted object or as a thunk; the
thunk form gives the emitter a chance to catch + wrap conversion
errors with `OPENAPI.SCHEMA_CONVERSION_FAILED`. Pre-converted objects
bypass the catch (the caller has already succeeded).
- Strictness application is per route and bound to whichever slot is
primary for the method — `request` for non-GET, `response` for GET —
so the failure mode mirrors what API clients actually rely on.
- Per-plugin filtered views are cached on first request. The filter
matches by the resolved tag (`displayName` or `name`), not by the raw
plugin name, so two plugins sharing a `displayName` are merged into a
single filtered view (and the host's manifest validator should reject
such collisions upstream).
- Memoisation: `getDocument()` returns the same object reference on
every call; mutations to the returned document leak across callers
by design — callers MUST `structuredClone` if they need to mutate.