7.5 KiB
@suaveplan/api-openapi-ui-scalar — Scalar UI adapter for the Suaveplan OpenAPI plugin system. Exposes createScalarUiHandler() and createPerPluginScalarUiHandler() — factories returning HTTP handlers that serve a self-contained Scalar HTML shell with strict CSP. Default OpenAPI explorer when operators want Scalar; sibling api-openapi-ui-swagger and api-openapi-ui-redoc cover the alternatives.
Scalar UI adapter for the Suaveplan OpenAPI plugin system.
createScalarUiHandler(opts) returns a frozen handler bundle that
serves a self-contained Scalar HTML shell at /docs/ui/scalar (or any
configured endpoint). The bundle is router-agnostic — host adapters
(api-router-hono, api-router-in-memory, worker bridge) wrap the
handler with their native request context. createPerPluginScalarUiHandler
is the per-plugin variant mounted under /docs/ui/scalar/<plugin>.
Documentation tree
- assets — The assets module owns the in-package bundled Scalar runtime that ships with the package — a small self-contained JS bo…
- assets — The assets module owns the in-package bundled Scalar runtime that ships with the package — a small self-contained JS bo…
- error-codes — This module owns the stable error-code registry for the
@suaveplan/api-openapi-ui-scalarpackage. The registry is bui…- error-codes — This module owns the stable error-code registry for the
@suaveplan/api-openapi-ui-scalarpackage. The registry is bui…
- error-codes — This module owns the stable error-code registry for the
- handler — The handler module owns the public
createScalarUiHandlerfactory the host calls whenhost.openapi.ui = "scalar". It…- handler — The handler module owns the public
createScalarUiHandlerfactory the host calls whenhost.openapi.ui = "scalar". It…
- handler — The handler module owns the public
- headers — The headers module builds the
Content-Security-Policyheader value emitted on every HTML response from the Scalar UI…- csp — The headers module builds the
Content-Security-Policyheader value emitted on every HTML response from the Scalar UI…
- csp — The headers module builds the
- html — The render-shell module emits the HTML body returned by the Scalar UI handler. It embeds the configured OpenAPI documen…
- render-shell — The render-shell module emits the HTML body returned by the Scalar UI handler. It embeds the configured OpenAPI documen…
- instruments — The instruments module wires the Scalar UI handler's OTel instruments into a frozen bundle constructed once at handler-…
- instruments — The instruments module wires the Scalar UI handler's OTel instruments into a frozen bundle constructed once at handler-…
- options — The options module owns the single Zod schema consumed by every Scalar UI handler factory. It is exposed both directly…
- options — The options module owns the single Zod schema consumed by every Scalar UI handler factory. It is exposed both directly…
- per-plugin — The per-plugin module exposes
createPerPluginScalarUiHandler, the variant the host mounts under `/docs/ui/scalar/<plu…- per-plugin — The per-plugin module exposes
createPerPluginScalarUiHandler, the variant the host mounts under `/docs/ui/scalar/<plu…
- per-plugin — The per-plugin module exposes
Why opt-in
Per the design doc (docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md
§7) the API foundation reserves /docs/ui/<vendor> for an interactive
OpenAPI explorer but does not bundle one by default — UI choice is
operator-chosen so the runtime image stays minimal for production
deployments that don't need it.
Operators wire ONE of:
@suaveplan/api-openapi-ui-scalar— canonical Scalar (this package).@suaveplan/api-openapi-ui-swagger— Swagger UI alternative.@suaveplan/api-openapi-ui-redoc— ReDoc alternative.
The OpenAPI emitter (api-openapi) does not require a UI plugin — the
JSON / YAML document is always available at /openapi.json regardless.
Quick start
import { createScalarUiHandler } from "@suaveplan/api-openapi-ui-scalar";
const { handler, options } = createScalarUiHandler({
openapiUrl: "/openapi.json",
});
// GET /docs/ui/scalar -> the explorer
const response = handler({ method: "GET", path: options.uiEndpoint });
Modules
handler—createScalarUiHandlerfactory and HTTP handler shape.per-plugin—createPerPluginScalarUiHandlervariant.options—scalarUiOptionsSchema+parseScalarUiOptions.assets— bundled JS / CSS registry andresolveAssetUrlresolver.headers—buildCspHeaderstrict CSP builder.html—renderShellHTML shell renderer.instruments— OTel counter wiring.error-codes—OPENAPI_UI_SCALAR.*registry.
Configuration
| Option | Default | Notes |
|---|---|---|
openapiUrl |
/openapi.json |
Embedded into the Scalar config block. |
uiEndpoint |
/docs/ui/scalar |
Must start with /. |
title |
"API Reference" |
HTML-escaped before embedding. |
theme |
"default" |
One of 11 built-in Scalar themes. |
layout |
"modern" |
"modern" / "classic". |
assetMode |
"cdn" |
"cdn" / "bundled". |
cdnUrl |
JSDelivr URL | Whitelisted in CSP when assetMode = "cdn". |
defaultOpenAllTags |
false |
Expand every tag on load. |
hideSearch |
false |
Hide the Scalar search box. |
darkMode |
false |
Force dark mode. |
editorOptions |
{} |
Open passthrough bag to Scalar. |
meterProvider |
OTel global | OTel MeterProvider for the counter. |
Security posture
- Strict CSP.
default-src 'self'; script-src 'self' [<cdn>]; style-src 'self' 'unsafe-inline' [<cdn>]; connect-src 'self' [<api-origin>]. Inline scripts are NEVER permitted — the Scalar bootstrap is loaded as an external<script src>so'unsafe-inline'would be a regression. - Per-request nonce passthrough. Hosts running nonce-based CSP pass
nonceon each request and the renderer threads it onto the inline config block. - Bundled-mode hardening.
assetMode = "bundled"keeps every fetch inside the host origin. A missing bundled asset raisesOPENAPI_UI_SCALAR.ASSETS_MISSINGat handler construction — no silent fall-back to CDN. - Cache-immutable assets. Bundled JS / CSS carries
Cache-Control: public, max-age=31536000, immutable. - Stale-hash tolerance. Hashed filenames resolve any matching stem + ext to the current asset bytes — rolling deploys don't 404 mid-flight.
OTel
openapi.ui.requests_total { ui: "scalar" }— counter incremented on each successful HTML response.openapi.ui.requests_total { ui: "scalar", plugin: "<name>" }— per- plugin fan-out emitted by the per-plugin handler in addition to the baseline observation.
Error codes
OPENAPI_UI_SCALAR.ASSETS_MISSING— bundled mode without a bundle.OPENAPI_UI_SCALAR.INVALID_OPTIONS— Zod schema rejected the config.OPENAPI_UI_SCALAR.ASSET_NOT_REGISTERED— internal asset lookup miss.
Tests
bunx --bun turbo lint typecheck test:coverage --filter=@suaveplan/api-openapi-ui-scalar
100% statements / branches / functions / lines required on touched
modules (provider: istanbul).