Files
2026-05-27 23:29:53 +01:00

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-scalar package. The registry is bui…
    • error-codes — This module owns the stable error-code registry for the @suaveplan/api-openapi-ui-scalar package. The registry is bui…
  • handler — The handler module owns the public createScalarUiHandler factory the host calls when host.openapi.ui = "scalar". It…
    • handler — The handler module owns the public createScalarUiHandler factory the host calls when host.openapi.ui = "scalar". It…
  • headers — The headers module builds the Content-Security-Policy header value emitted on every HTML response from the Scalar UI…
    • csp — The headers module builds the Content-Security-Policy header value emitted on every HTML response from the Scalar UI…
  • 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…

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 — createScalarUiHandler factory and HTTP handler shape.
  • per-plugin — createPerPluginScalarUiHandler variant.
  • options — scalarUiOptionsSchema + parseScalarUiOptions.
  • assets — bundled JS / CSS registry and resolveAssetUrl resolver.
  • headers — buildCspHeader strict CSP builder.
  • html — renderShell HTML 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 nonce on 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 raises OPENAPI_UI_SCALAR.ASSETS_MISSING at 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).