# emitter _(no submodules)_ ## 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, `.` becomes the component schema prefix. - **Deterministic operationIds** — every route receives `..`. 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.