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,83 @@
# @suaveplan/api-openapi-ui-redoc — ReDoc UI adapter for the Suaveplan api-host. Mounts the ReDoc HTML shell at /docs/ui/redoc (or any caller-chosen path) via createRedocUiHandler({ openapiUrl, title, theme, expandResponses }) and at /docs/{plugin}/ui/redoc via createPerPluginRedocUiHandler. Validates RedocUiOptions through a strict Zod schema, resolves the ReDoc runtime bundle from a cdn / bundled / inline strategy, embeds the configured options into the shell as a JSON island, and raises the OPENAPI_UI_REDOC_* error-code family on collision, missing asset, or invalid options. Structural sibling of api-graphql-ui-altair and api-graphql-ui-graphiql.
Stage-8 ReDoc UI adapter for the Suaveplan api-host. Mounts the ReDoc HTML
shell at `/docs/ui/redoc` (or any caller-chosen path) via
`createRedocUiHandler` and at `/docs/{plugin}/ui/redoc` via
`createPerPluginRedocUiHandler`. Validates options through a strict Zod
schema, resolves the runtime bundle from a cdn / bundled / inline strategy,
and raises the `OPENAPI_UI_REDOC_*` error-code family on missing assets or
invalid options.
Structural sibling of `@suaveplan/api-graphql-ui-altair` and
`@suaveplan/api-graphql-ui-graphiql`.
<!-- begin:auto-docs -->
## Documentation tree
- [assets](./src/assets/assets.md) — This module resolves the runtime asset URL the ReDoc HTML shell references. The package supports three asset strategies…
- [assets](./src/assets/assets.md) — This module resolves the runtime asset URL the ReDoc HTML shell references. The package supports three asset strategies…
- [error-codes](./src/error-codes/error-codes.md) — This module defines the frozen error-code namespace shared by the ReDoc UI adapter. Every fault the package raises carr…
- [error-codes](./src/error-codes/error-codes.md) — This module defines the frozen error-code namespace shared by the ReDoc UI adapter. Every fault the package raises carr…
- [handler](./src/handler/handler.md) — This module exposes `createRedocUiHandler`, the factory the api-host calls to obtain the HTTP handler serving the ReDoc…
- [handler](./src/handler/handler.md) — This module exposes `createRedocUiHandler`, the factory the api-host calls to obtain the HTTP handler serving the ReDoc…
- [html](./src/html/render-shell.md) — This module exposes `renderRedocShell`, the pure function that produces the ReDoc HTML shell consumed by both the globa…
- [render-shell](./src/html/render-shell.md) — This module exposes `renderRedocShell`, the pure function that produces the ReDoc HTML shell consumed by both the globa…
- [options](./src/options/options.md) — This module defines the Zod schema and parser for `RedocUiOptions`, the configuration bundle the ReDoc UI handler accep…
- [options](./src/options/options.md) — This module defines the Zod schema and parser for `RedocUiOptions`, the configuration bundle the ReDoc UI handler accep…
- [per-plugin](./src/per-plugin/per-plugin.md) — This module exposes `createPerPluginRedocUiHandler`, the sibling of `createRedocUiHandler` for hosts that mount per-plu…
- [per-plugin](./src/per-plugin/per-plugin.md) — This module exposes `createPerPluginRedocUiHandler`, the sibling of `createRedocUiHandler` for hosts that mount per-plu…
<!-- end:auto-docs -->
## Install
```bash
bun add @suaveplan/api-openapi-ui-redoc
```
## Quick start
```ts
import { createRedocUiHandler } from "@suaveplan/api-openapi-ui-redoc";
const handler = createRedocUiHandler({
openapiUrl: "/openapi.json",
title: "Acme API",
theme: "dark",
expandResponses: "200,201",
});
const res = handler();
// res.body is the rendered HTML shell, ready to send.
```
## Modules
| Module | Subpath | Description |
| ---------------------------- | --------------------------------------------- | ----------- |
| `handler/handler.ts` | `@suaveplan/api-openapi-ui-redoc/handler` | `createRedocUiHandler` + `RedocHttpResponse` |
| `per-plugin/per-plugin.ts` | `@suaveplan/api-openapi-ui-redoc/per-plugin` | `createPerPluginRedocUiHandler` |
| `options/options.ts` | `@suaveplan/api-openapi-ui-redoc/options` | `RedocUiOptions` Zod schema + parser |
| `assets/assets.ts` | `@suaveplan/api-openapi-ui-redoc/assets` | cdn / bundled / inline strategy resolver |
| `html/render-shell.ts` | `@suaveplan/api-openapi-ui-redoc/html` | Pure HTML shell renderer |
| `error-codes/error-codes.ts` | `@suaveplan/api-openapi-ui-redoc/error-codes` | `OPENAPI_UI_REDOC_*` registry |
## Asset strategies
- **`cdn`** (default) — point the shell at the canonical Redocly CDN URL
(`https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js`). Zero-byte
deploy; one network round-trip per first-paint.
- **`bundled`** — operators supply the runtime bytes; the package serves
them from a cache-busted local URL.
- **`inline`** — embed the runtime body directly into the HTML shell. One
network round-trip total, at the cost of a larger first-paint payload.
## Error codes
- `OPENAPI_UI_REDOC_ASSETS_MISSING` — `bundled` or `inline` mode requested
without a payload.
- `OPENAPI_UI_REDOC_INVALID_OPTIONS` — Zod rejected the option bundle.
## License
MIT

View File

@@ -0,0 +1,81 @@
# `assets` — ReDoc runtime asset strategy
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module resolves the runtime asset URL the ReDoc HTML shell references.
The package supports three asset strategies — `cdn`, `bundled`, and `inline` —
so operators can choose between zero-byte deploys (CDN), air-gapped
deployments (bundled), and single-request first-paint (inline) without
changing the handler call site. The resolver returns a typed
`ResolvedRedocAsset` whose `url` slots straight into the shell renderer.
## Features
- `REDOC_CDN_URL` — the canonical Redocly CDN URL for the `redoc.standalone.js`
bundle.
- `defineRedocAsset(name, contentType, body)` builds a bundled-asset record
with a SHA-256-prefix cache-bust hash derived from the payload bytes.
- `resolveRedocAsset({ strategy, bundled, inline })` returns the asset
descriptor for the chosen strategy. Missing payloads raise
`OPENAPI_UI_REDOC_ASSETS_MISSING`.
- Bundled URLs are cache-busted (`./assets/<hash>/<name>`) so re-deploys with
unchanged payloads keep their CDN warm-cache entries.
## Usage: Basic
```ts
import { resolveRedocAsset, REDOC_CDN_URL } from "@suaveplan/api-openapi-ui-redoc/assets";
const cdn = resolveRedocAsset({ strategy: "cdn" });
console.log(cdn.url === REDOC_CDN_URL); // true
```
## Usage: Advanced
Ship the runtime alongside the host binary for air-gapped operation:
```ts
import { readFileSync } from "node:fs";
import {
defineRedocAsset,
resolveRedocAsset,
} from "@suaveplan/api-openapi-ui-redoc/assets";
const bundle = readFileSync("./vendor/redoc.standalone.js", "utf8");
const asset = defineRedocAsset(
"redoc.standalone.js",
"application/javascript; charset=utf-8",
bundle
);
const resolved = resolveRedocAsset({ strategy: "bundled", bundled: asset });
// resolved.url === `./assets/${asset.hash}/redoc.standalone.js`
```
The host then wires a `GET ./assets/:hash/:name` route that serves the
asset's `body` with its `contentType` and `Cache-Control: public, max-age=31536000, immutable`.
## API Reference
| Symbol | Kind | Description |
| ------------------------- | ------ | ----------- |
| `REDOC_CDN_URL` | const | Upstream Redocly CDN URL |
| `REDOC_BUNDLED_URL` | const | Conventional relative URL for bundled mode |
| `RedocAssetStrategy` | type | `"cdn" \| "bundled" \| "inline"` |
| `BundledRedocAsset` | type | `{ name, contentType, body, hash }` |
| `ResolvedRedocAsset` | type | `{ strategy, url, inlineBody? }` |
| `defineRedocAsset` | fn | Build a bundled-asset record with a content-derived cache-bust hash |
| `resolveRedocAsset` | fn | Resolve `{ strategy, bundled?, inline? }` → `ResolvedRedocAsset` |
## Implementation Notes
`defineRedocAsset` hashes the payload via `node:crypto` SHA-256 and keeps the
first 16 hex characters — collision-resistant for cache-busting and short
enough to keep URLs readable. The `inline` strategy returns a sentinel
`data:inline` URL plus the body itself; the shell renderer detects this
sentinel and inlines a `<script>` tag rather than emitting a `<script src=...>`.
This avoids leaking the literal payload into a query string.

View File

@@ -0,0 +1,83 @@
# `error-codes` — `OPENAPI_UI_REDOC.*` registry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module defines the frozen error-code namespace shared by the ReDoc UI
adapter. Every fault the package raises carries one of the two codes registered
here, so operators can map ReDoc-adapter failures to dashboards, alerts, and
runbooks without spelunking through stack traces. The registry is wired into
the cross-package `defineCodes` registry so callers that bare-import the
package automatically surface the namespace alongside every other suaveplan
code prefix.
## Features
- Two stable codes: `OPENAPI_UI_REDOC_ASSETS_MISSING` for asset-strategy
faults and `OPENAPI_UI_REDOC_INVALID_OPTIONS` for Zod-rejected option
bundles.
- A concrete `OpenapiUiRedocError` subclass that narrows `code` so consumers
can `instanceof`-discriminate the package's faults from other Suaveplan
errors.
- A `redocCode` getter that re-exposes the code at a typed alias for
pattern-matching in dashboards.
- Frozen tuple via `defineCodes(...)` so the literal types survive bundling
and tree-shaking.
## Usage: Basic
```ts
import {
OPENAPI_UI_REDOC_CODES,
OpenapiUiRedocError,
} from "@suaveplan/api-openapi-ui-redoc/error-codes";
throw new OpenapiUiRedocError(
OPENAPI_UI_REDOC_CODES.ASSETS_MISSING,
"uiAssets='bundled' but no payload supplied"
);
```
## Usage: Advanced
Pattern-match codes in a host fault sink to send INVALID_OPTIONS to the
operator dashboard and ASSETS_MISSING to the deploy-pipeline alarm:
```ts
import {
OPENAPI_UI_REDOC_CODES,
OpenapiUiRedocError,
} from "@suaveplan/api-openapi-ui-redoc/error-codes";
function handleFault(err: unknown): void {
if (!(err instanceof OpenapiUiRedocError)) return;
switch (err.redocCode) {
case OPENAPI_UI_REDOC_CODES.ASSETS_MISSING:
alertDeployPipeline(err);
return;
case OPENAPI_UI_REDOC_CODES.INVALID_OPTIONS:
alertOperatorConsole(err);
return;
}
}
```
## API Reference
| Symbol | Kind | Description |
| -------------------------- | ------ | ---------------------------------------- |
| `OPENAPI_UI_REDOC_CODES` | const | Frozen `Record<"ASSETS_MISSING"\|"INVALID_OPTIONS", string>` |
| `OpenapiUiRedocErrorCode` | type | Union of the two namespaced literals |
| `OpenapiUiRedocError` | class | `SuaveplanError<OpenapiUiRedocErrorCode>` subclass with `redocCode` |
## Implementation Notes
Codes are registered exactly once at module init via `defineCodes("OPENAPI_UI_REDOC", […])`.
Importing the package's root barrel surfaces this module as a side-effect so
subpath consumers (e.g. `…/handler`) still benefit from the registration even
if they never reach the top-level export. The concrete class re-stores the
typed `redocCode` so narrowed `instanceof` checks expose the code on a typed
property rather than the loosely-typed `SuaveplanError.code`.

View File

@@ -0,0 +1,96 @@
# `handler` — `createRedocUiHandler`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module exposes `createRedocUiHandler`, the factory the api-host calls
to obtain the HTTP handler serving the ReDoc HTML shell. The returned
handler is a zero-argument function the host invokes per request. It is
intentionally router-agnostic — adapters in the api-host layer translate
the returned `RedocHttpResponse` into Hono / api-router envelopes — so the
package can be wired into any HTTP surface without dragging a router peer
dependency through every consumer.
## Features
- Validates options at construction; invalid input raises
`OPENAPI_UI_REDOC_INVALID_OPTIONS` before any handler runs.
- Resolves the runtime asset URL up-front; missing bundled / inline payloads
raise `OPENAPI_UI_REDOC_ASSETS_MISSING` at construction, not at request time.
- Pre-renders the HTML shell once at construction — every subsequent request
is a constant-time return of the frozen response object.
- The response carries `Content-Type: text/html; charset=utf-8`,
`Cache-Control: no-cache`, and `X-Content-Type-Options: nosniff`.
- The handler is idempotent — the same call always returns the same frozen
object, so adapters can safely cache it.
## Usage: Basic
```ts
import { createRedocUiHandler } from "@suaveplan/api-openapi-ui-redoc/handler";
const handler = createRedocUiHandler({ openapiUrl: "/openapi.json" });
const res = handler();
// res.body is the rendered HTML shell.
```
## Usage: Advanced
Wire the bundled-asset strategy into a Hono app, including the cache-busted
asset route:
```ts
import { Hono } from "hono";
import {
createRedocUiHandler,
defineRedocAsset,
} from "@suaveplan/api-openapi-ui-redoc";
const asset = defineRedocAsset(
"redoc.standalone.js",
"application/javascript; charset=utf-8",
runtimeBody
);
const handler = createRedocUiHandler({
openapiUrl: "/openapi.json",
uiAssets: "bundled",
bundledAsset: asset,
title: "Acme API",
expandResponses: "200,201",
theme: "dark",
});
const app = new Hono();
app.get("/docs/ui/redoc", (c) => {
const r = handler();
Object.entries(r.headers).forEach(([k, v]) => c.header(k, v));
return c.body(r.body, r.status);
});
app.get(`/docs/ui/redoc/assets/${asset.hash}/${asset.name}`, (c) => {
c.header("Content-Type", asset.contentType);
c.header("Cache-Control", "public, max-age=31536000, immutable");
return c.body(asset.body as string);
});
```
## API Reference
| Symbol | Kind | Description |
| --------------------------- | ---- | ----------- |
| `RedocHttpResponse` | type | `{ status, headers, body }` — router-agnostic response |
| `RedocHttpHandler` | type | `() => RedocHttpResponse` |
| `CreateRedocUiHandlerInput` | type | `RedocUiOptionsInput & { bundledAsset?, inlineAsset? }` |
| `createRedocUiHandler` | fn | Build the handler; pre-renders the shell |
## Implementation Notes
The handler pre-renders the HTML once at construction. This keeps the
per-request hot path a constant-time return of a frozen object — the same
shape the api-router would otherwise have to allocate per call. Pre-rendering
also means every option-level fault surfaces at host start-up, not at the
first request, which is the failure mode operators want for misconfigured
deploys.

View File

@@ -0,0 +1,81 @@
# `html/render-shell` — ReDoc HTML shell renderer
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module exposes `renderRedocShell`, the pure function that produces the
ReDoc HTML shell consumed by both the global and per-plugin handlers. It
embeds the OpenAPI document URL into a `<redoc spec-url="…">` custom element,
ships a JSON option island for the runtime to read, and injects the runtime
runtime asset reference based on the chosen asset strategy. Keeping the
renderer separate from the handler factories lets host operators embed the
shell in custom adapters (e.g. SSR, Edge functions) without reaching for the
handler builder.
## Features
- HTML-escapes every user-supplied value (`title`, `openapiUrl`, etc.) so
option strings cannot break out of attribute or text-node scope.
- Inlines the runtime body when `strategy === "inline"` and emits a
`<script src="…">` tag for `cdn` / `bundled` strategies.
- Embeds `theme`, `hideDownloadButton`, `nativeScrollbars`, and
`expandResponses` into the JSON options island the runtime reads.
- Tags the body element with `data-redoc-theme` so external stylesheets can
override the colour scheme.
- Pure — no I/O, no clocks, no globals.
## Usage: Basic
```ts
import { renderRedocShell } from "@suaveplan/api-openapi-ui-redoc/html";
import {
parseRedocUiOptions,
resolveRedocAsset,
} from "@suaveplan/api-openapi-ui-redoc";
const html = renderRedocShell({
options: parseRedocUiOptions({ openapiUrl: "/openapi.json" }),
resolvedAsset: resolveRedocAsset({ strategy: "cdn" }),
});
```
## Usage: Advanced
Pre-render the shell for a multi-tenant host that picks up the OpenAPI URL
from the request path:
```ts
import { renderRedocShell } from "@suaveplan/api-openapi-ui-redoc/html";
const options = parseRedocUiOptions({ openapiUrl: "/openapi.json", theme: "dark" });
const resolvedAsset = resolveRedocAsset({ strategy: "cdn" });
function htmlForTenant(tenant: string): string {
return renderRedocShell({
options,
resolvedAsset,
openapiUrlOverride: `/${tenant}/openapi.json`,
});
}
```
## API Reference
| Symbol | Kind | Description |
| ------------------------ | ---- | ----------- |
| `RenderRedocShellInput` | type | `{ options, resolvedAsset, openapiUrlOverride? }` |
| `renderRedocShell` | fn | Render the HTML shell as a string |
## Implementation Notes
The escape function covers the five HTML special characters (`&`, `<`, `>`,
`"`, `'`) plus the apostrophe — the same surface as the WHATWG HTML
serialization-attribute rules. The JSON island is serialised via
`JSON.stringify` and HTML-escaped so embedded `</script>` sequences in
option values cannot escape the `<script>` context. The shell's bottom-of-
body initialiser calls `window.Redoc.init` when the runtime defines it,
falling back gracefully when the runtime is unavailable (e.g. during DOM
unit tests that don't load the standalone bundle).

View File

@@ -0,0 +1,82 @@
# `options` — `RedocUiOptions` schema + parser
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module defines the Zod schema and parser for `RedocUiOptions`, the
configuration bundle the ReDoc UI handler accepts. It is the single boundary
where caller-supplied option values transition from untrusted input to a
fully-defaulted, immutable typed record the rest of the package treats as
ground truth. Refusing malformed input here means the rendered HTML shell
never has to defend against half-validated values at request time.
## Features
- Strict schema — unknown keys are refused so typos surface immediately
instead of silently producing a misconfigured shell.
- Sensible defaults so the bare `createRedocUiHandler()` call still produces
a working `/docs/ui/redoc` shell against `/openapi.json`.
- `expandResponses` is a regex-validated comma-separated list of three-character
HTTP status codes (or `"all"`), matching the official ReDoc option.
- `uiAssets` strategy literal — one of `"cdn"`, `"bundled"`, `"inline"`.
- Zod failures are wrapped as `OpenapiUiRedocError(INVALID_OPTIONS)` with the
original `ZodError` attached as `cause`.
## Usage: Basic
```ts
import { parseRedocUiOptions } from "@suaveplan/api-openapi-ui-redoc/options";
const opts = parseRedocUiOptions({
openapiUrl: "/openapi.json",
theme: "dark",
});
```
## Usage: Advanced
Combine with the host-config layer to validate `host.openapi.ui = "redoc"`
options at ingestion time, before any handler is constructed:
```ts
import {
parseRedocUiOptions,
redocUiOptionsSchema,
type RedocUiOptionsInput,
} from "@suaveplan/api-openapi-ui-redoc/options";
const hostConfigSchema = z.object({
openapi: z.object({
ui: z.literal("redoc"),
redoc: redocUiOptionsSchema.partial().default({}),
}),
});
function buildHandler(raw: unknown) {
const cfg = hostConfigSchema.parse(raw);
const opts: RedocUiOptionsInput = cfg.openapi.redoc;
return parseRedocUiOptions(opts);
}
```
## API Reference
| Symbol | Kind | Description |
| ------------------------- | ------ | ----------- |
| `redocUiOptionsSchema` | const | `z.ZodObject` of the strict options shape |
| `RedocUiOptions` | type | `z.infer<typeof redocUiOptionsSchema>` (defaulted) |
| `RedocUiOptionsInput` | type | `z.input<typeof redocUiOptionsSchema>` (caller-facing) |
| `parseRedocUiOptions` | fn | `(input?) => RedocUiOptions` — throws `OpenapiUiRedocError(INVALID_OPTIONS)` |
## Implementation Notes
The schema is `.strict()` so additional keys raise the catch-all
`INVALID_OPTIONS` code rather than silently passing through. We use
`safeParse` internally so we can wrap the error rather than re-throw the raw
`ZodError`; the wrapper carries the registry code and stable message but the
underlying issue list remains accessible via `err.cause`. Defaults are baked
into the schema so a `parseRedocUiOptions({})` call returns a fully-formed
options record without callers having to reach for any other layer.

View File

@@ -0,0 +1,70 @@
# `per-plugin` — per-plugin ReDoc handler factory
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module exposes `createPerPluginRedocUiHandler`, the sibling of
`createRedocUiHandler` for hosts that mount per-plugin documentation surfaces
at `/docs/{plugin}/ui/redoc`. The rendered shell references the plugin-filtered
OpenAPI URL (typically `/openapi.json?plugin=foo`) so a single host can serve
both the global aggregate documentation and per-plugin slices through the
same UI adapter.
## Features
- Plugin-specific URL resolver — operators inject a custom `openapiUrlForPlugin`
if their host filters by path segment rather than query string.
- Sensible default resolver appends `?plugin=<encoded>` to `options.openapiUrl`
(or `&plugin=…` if the URL already has a query string).
- Validates options through the same Zod schema as the global handler.
- Pre-renders the shell once per plugin handler so per-request cost is zero.
## Usage: Basic
```ts
import { createPerPluginRedocUiHandler } from "@suaveplan/api-openapi-ui-redoc/per-plugin";
const handler = createPerPluginRedocUiHandler({
plugin: "contacts",
openapiUrl: "/openapi.json",
});
// handler().body references "/openapi.json?plugin=contacts"
```
## Usage: Advanced
Use a path-segment resolver when the host serves filtered documents at
`/openapi/<plugin>.json`:
```ts
const handler = createPerPluginRedocUiHandler({
plugin: "billing",
openapiUrl: "/openapi.json",
openapiUrlForPlugin: (p) => `/openapi/${p}.json`,
title: "Billing API",
expandResponses: "all",
});
```
The api-host typically lazy-builds one per-plugin handler per request, but
because construction validates options + asset strategy up-front, callers can
also pre-build the per-plugin handlers at start-up and dispatch by slug.
## API Reference
| Symbol | Kind | Description |
| ------------------------------------- | ---- | ----------- |
| `OpenapiUrlForPlugin` | type | `(plugin: string) => string` |
| `CreatePerPluginRedocUiHandlerInput` | type | `RedocUiOptionsInput & { plugin, openapiUrlForPlugin?, bundledAsset?, inlineAsset? }` |
| `createPerPluginRedocUiHandler` | fn | Build the per-plugin handler |
## Implementation Notes
The default resolver URL-encodes the plugin slug via `encodeURIComponent` so
slugs containing `/` or other delimiters cannot break out of the query
parameter. The shell is rendered with the resolved URL substituted in via
`openapiUrlOverride` on `renderRedocShell`, leaving the rest of the options
record (theme, expandResponses, …) untouched.