Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
# @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>`.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [assets](./src/assets/assets.md) — The assets module owns the in-package bundled Scalar runtime that ships with the package — a small self-contained JS bo…
|
||||
- [assets](./src/assets/assets.md) — The assets module owns the in-package bundled Scalar runtime that ships with the package — a small self-contained JS bo…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module owns the stable error-code registry for the `@suaveplan/api-openapi-ui-scalar` package. The registry is bui…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module owns the stable error-code registry for the `@suaveplan/api-openapi-ui-scalar` package. The registry is bui…
|
||||
- [handler](./src/handler/handler.md) — The handler module owns the public `createScalarUiHandler` factory the host calls when `host.openapi.ui = "scalar"`. It…
|
||||
- [handler](./src/handler/handler.md) — The handler module owns the public `createScalarUiHandler` factory the host calls when `host.openapi.ui = "scalar"`. It…
|
||||
- [headers](./src/headers/csp.md) — The headers module builds the `Content-Security-Policy` header value emitted on every HTML response from the Scalar UI…
|
||||
- [csp](./src/headers/csp.md) — The headers module builds the `Content-Security-Policy` header value emitted on every HTML response from the Scalar UI…
|
||||
- [html](./src/html/render-shell.md) — The render-shell module emits the HTML body returned by the Scalar UI handler. It embeds the configured OpenAPI documen…
|
||||
- [render-shell](./src/html/render-shell.md) — The render-shell module emits the HTML body returned by the Scalar UI handler. It embeds the configured OpenAPI documen…
|
||||
- [instruments](./src/instruments/instruments.md) — The instruments module wires the Scalar UI handler's OTel instruments into a frozen bundle constructed once at handler-…
|
||||
- [instruments](./src/instruments/instruments.md) — The instruments module wires the Scalar UI handler's OTel instruments into a frozen bundle constructed once at handler-…
|
||||
- [options](./src/options/options.md) — The options module owns the single Zod schema consumed by every Scalar UI handler factory. It is exposed both directly…
|
||||
- [options](./src/options/options.md) — The options module owns the single Zod schema consumed by every Scalar UI handler factory. It is exposed both directly…
|
||||
- [per-plugin](./src/per-plugin/per-plugin.md) — The per-plugin module exposes `createPerPluginScalarUiHandler`, the variant the host mounts under `/docs/ui/scalar/<plu…
|
||||
- [per-plugin](./src/per-plugin/per-plugin.md) — The per-plugin module exposes `createPerPluginScalarUiHandler`, the variant the host mounts under `/docs/ui/scalar/<plu…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
```bash
|
||||
bunx --bun turbo lint typecheck test:coverage --filter=@suaveplan/api-openapi-ui-scalar
|
||||
```
|
||||
|
||||
100% statements / branches / functions / lines required on touched
|
||||
modules (`provider: istanbul`).
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Scalar bundled-asset registry
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The assets module owns the in-package bundled Scalar runtime that
|
||||
ships with the package — a small self-contained JS bootstrap and a tiny
|
||||
companion stylesheet — plus the asset-URL resolver consumed by the
|
||||
handler factory. Air-gapped hosts toggle `assetMode = "bundled"` and the
|
||||
shell loads JS / CSS from `<uiEndpoint>/assets/<hash>.<ext>`; hosts on
|
||||
the public internet toggle `assetMode = "cdn"` and the shell loads the
|
||||
upstream `@scalar/api-reference` bundle from JSDelivr. Either way the
|
||||
shell embeds the bootstrap config block at render time so the runtime
|
||||
target is fixed at handler construction.
|
||||
|
||||
## Features
|
||||
|
||||
- `SCALAR_ASSETS` — frozen registry keyed by logical asset name, every
|
||||
entry carrying its bytes, MIME type, and an FNV-1a-32 cache-busting
|
||||
hash.
|
||||
- `hashedAssetPath(name)` — formats the `<stem>.<hash>.<ext>` filename
|
||||
embedded in the rendered shell.
|
||||
- `resolveAsset(filename)` — reverse lookup tolerating stale hashes
|
||||
(rolling deploy) and missing-hash bare filenames.
|
||||
- `resolveAssetUrl({ mode, cdnUrl, uiEndpoint, probe })` — returns the
|
||||
Scalar runtime URL the shell should embed. `bundled` + missing asset
|
||||
raises `OPENAPI_UI_SCALAR.ASSETS_MISSING`.
|
||||
- `defaultBundledAssetProbe` — checks the in-package registry is
|
||||
non-empty (default behaviour); tests inject a stub to simulate the
|
||||
stripped-bundle case.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { resolveAssetUrl } from "@suaveplan/api-openapi-ui-scalar/assets";
|
||||
|
||||
const runtime = resolveAssetUrl({
|
||||
mode: "cdn",
|
||||
cdnUrl: "https://cdn.jsdelivr.net/npm/@scalar/api-reference",
|
||||
uiEndpoint: "/docs/ui/scalar",
|
||||
});
|
||||
console.log(runtime.url);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
resolveAssetUrl,
|
||||
type BundledAssetProbe,
|
||||
} from "@suaveplan/api-openapi-ui-scalar/assets";
|
||||
|
||||
const stubbedProbe: BundledAssetProbe = { hasBundle: () => false };
|
||||
|
||||
// Simulate the air-gapped "asset stripped from dist" failure case —
|
||||
// `resolveAssetUrl` throws OPENAPI_UI_SCALAR.ASSETS_MISSING.
|
||||
try {
|
||||
resolveAssetUrl({
|
||||
mode: "bundled",
|
||||
cdnUrl: "",
|
||||
uiEndpoint: "/docs/ui/scalar",
|
||||
probe: stubbedProbe,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("bundled asset is missing", err);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ScalarAsset` interface — `{ name, content, contentType, hash }`.
|
||||
- `SCALAR_ASSETS` `ReadonlyMap<string, ScalarAsset>` — frozen registry.
|
||||
- `fnv1a32(input)` — 8-char hex FNV-1a-32 of the asset bytes.
|
||||
- `hashedAssetPath(name)` — throws `ASSET_NOT_REGISTERED` when missing.
|
||||
- `resolveAsset(filename)` — returns the asset entry or throws
|
||||
`ASSET_NOT_REGISTERED`.
|
||||
- `resolveAssetUrl(input)` — returns `{ mode, url }`; throws
|
||||
`ASSETS_MISSING` in bundled mode without a bundle.
|
||||
- `listScalarAssetNames()` — listing of registered names for tests.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The bundled JS bootstrap is intentionally minimal — it parses the
|
||||
`<script id="scalar-config" type="application/json">` block and renders
|
||||
a status line + path listing so air-gapped hosts have a functional
|
||||
fallback. Operators that need the full Scalar React UI install the
|
||||
upstream `@scalar/api-reference` and overwrite `dist/assets/scalar.js`
|
||||
at build time; the FNV-1a hash refreshes automatically. The hash is NOT
|
||||
security-relevant — it exists only to invalidate browser caches.
|
||||
@@ -0,0 +1,84 @@
|
||||
# `OPENAPI_UI_SCALAR.*` error-code registry
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns the stable error-code registry for the
|
||||
`@suaveplan/api-openapi-ui-scalar` package. The registry is built via
|
||||
`defineCodes` so every key projects to a frozen `OPENAPI_UI_SCALAR.<NAME>`
|
||||
string the rest of the suite (logger, error bus, OTel attributes) can
|
||||
discriminate on. `OpenapiUiScalarError` extends the shared
|
||||
`SuaveplanError` base so call sites can branch on
|
||||
`instanceof OpenapiUiScalarError` or — more durably — on `error.code`.
|
||||
|
||||
Each code maps to a single production throw site that is exercised in the
|
||||
test suite by a real failure path, not a manual rethrow.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen `OPENAPI_UI_SCALAR_CODES` registry with three stable keys:
|
||||
`ASSETS_MISSING`, `INVALID_OPTIONS`, `ASSET_NOT_REGISTERED`.
|
||||
- Typed `OpenapiUiScalarCode` union derived from the registry values.
|
||||
- `OpenapiUiScalarError` subclass carrying the typed `code` + `scalarCode`
|
||||
twin so consumers can pattern-match without a runtime `as` cast.
|
||||
- Cause chain + context bag honoured via the shared base — useful for
|
||||
wrapping the underlying `ZodError` on `INVALID_OPTIONS`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
OPENAPI_UI_SCALAR_CODES,
|
||||
OpenapiUiScalarError,
|
||||
} from "@suaveplan/api-openapi-ui-scalar/error-codes";
|
||||
|
||||
throw new OpenapiUiScalarError(
|
||||
OPENAPI_UI_SCALAR_CODES.ASSETS_MISSING,
|
||||
"Scalar bundled asset is not present"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
OPENAPI_UI_SCALAR_CODES,
|
||||
OpenapiUiScalarError,
|
||||
} from "@suaveplan/api-openapi-ui-scalar/error-codes";
|
||||
|
||||
try {
|
||||
parseHostConfig(input);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof OpenapiUiScalarError &&
|
||||
err.code === OPENAPI_UI_SCALAR_CODES.INVALID_OPTIONS
|
||||
) {
|
||||
// Surface the underlying ZodError issues to the operator console.
|
||||
logger.error("Scalar UI host config rejected", {
|
||||
cause: err.cause,
|
||||
issues: err.context?.issues,
|
||||
});
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `OPENAPI_UI_SCALAR_CODES` — frozen object with the three code keys.
|
||||
- `OpenapiUiScalarCode` — typed union over the code values.
|
||||
- `OpenapiUiScalarError` — `SuaveplanError<OpenapiUiScalarCode>` subclass.
|
||||
Constructor signature: `(code, message, options?)`. `options` carries
|
||||
`cause` and `context` per the shared error contract.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The registry is intentionally closed — adding a new code requires an
|
||||
OpenSpec change so the audit + dashboards (`openapi_ui_scalar.errors_total`
|
||||
labels) get refreshed in lockstep. The `error-codes.test.ts` conformance
|
||||
file walks every registry entry and asserts a corresponding production
|
||||
throw site lives in the package, so dead codes can never accumulate.
|
||||
@@ -0,0 +1,91 @@
|
||||
# `createScalarUiHandler` — Scalar HTTP handler factory
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The handler module owns the public `createScalarUiHandler` factory the
|
||||
host calls when `host.openapi.ui = "scalar"`. It validates the
|
||||
caller-supplied options through the Zod schema, eagerly resolves the
|
||||
Scalar runtime asset URL (CDN or bundled), constructs the OTel
|
||||
instrument bundle, and returns a frozen `ScalarUiHandlerBundle` whose
|
||||
`handler` field is the actual HTTP handler callable. The bundle also
|
||||
exposes an `assetHandler` for the bundled-mode `<uiEndpoint>/assets/`
|
||||
sub-route the host mounts alongside the main shell.
|
||||
|
||||
## Features
|
||||
|
||||
- Eager validation — invalid options or missing bundled assets fail at
|
||||
handler construction rather than at first request.
|
||||
- Strict CSP — `Content-Security-Policy` header value is derived once at
|
||||
construction time so request handling never has to recompute it. The
|
||||
policy widens to whitelist the CDN origin only when `assetMode = "cdn"`.
|
||||
- Per-request CSP nonce passthrough — the handler accepts an optional
|
||||
`nonce` on each request which the renderer threads onto the inline
|
||||
`<script>` blocks.
|
||||
- Idempotent — repeated handler invocations on the same request input
|
||||
return byte-identical body / headers.
|
||||
- OTel — every successful HTML response increments the
|
||||
`openapi.ui.requests_total` counter with `{ ui: "scalar" }`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createScalarUiHandler } from "@suaveplan/api-openapi-ui-scalar/handler";
|
||||
|
||||
const { handler } = createScalarUiHandler({
|
||||
openapiUrl: "/openapi.json",
|
||||
});
|
||||
|
||||
const response = handler({ method: "GET", path: "/docs/ui/scalar" });
|
||||
console.log(response.status); // 200
|
||||
console.log(response.headers["Content-Security-Policy"]);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { createScalarUiHandler } from "@suaveplan/api-openapi-ui-scalar/handler";
|
||||
|
||||
const bundle = createScalarUiHandler({
|
||||
openapiUrl: "/openapi.json",
|
||||
theme: "deepSpace",
|
||||
layout: "classic",
|
||||
assetMode: "bundled",
|
||||
});
|
||||
|
||||
// Per-request nonce — wire into your edge / Hono / Worker middleware.
|
||||
const html = bundle.handler({
|
||||
method: "GET",
|
||||
path: "/docs/ui/scalar",
|
||||
nonce: crypto.randomUUID(),
|
||||
});
|
||||
|
||||
// Bundled-mode hosts ALSO mount the asset handler at
|
||||
// `<uiEndpoint>/assets/:filename`.
|
||||
const js = bundle.assetHandler({
|
||||
method: "GET",
|
||||
path: `${bundle.options.uiEndpoint}/assets/scalar.js`,
|
||||
});
|
||||
console.log(js.headers["Content-Type"]); // "application/javascript; charset=utf-8"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `CreateScalarUiHandlerInput` — `ScalarUiOptionsInput` plus
|
||||
`meterProvider?` and `bundledAssetProbe?` DI hooks.
|
||||
- `createScalarUiHandler(input)` — `ScalarUiHandlerBundle`.
|
||||
- `ScalarUiHandlerBundle` — `{ name, version, options, instruments,
|
||||
runtimeUrl, handler, assetHandler }` (frozen).
|
||||
- `ScalarUiHandler` — `(request) => ScalarHttpResponse`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The factory pre-computes the CSP header value, the resolved runtime
|
||||
URL, and the OTel instrument bundle so request handling is a constant-
|
||||
time render + counter-increment. Mount-time validation catches every
|
||||
operator-config failure mode before the host accepts traffic. Errors
|
||||
raised here carry the stable `OPENAPI_UI_SCALAR.*` codes so host
|
||||
boot-up can discriminate without unpacking the underlying Zod issues.
|
||||
@@ -0,0 +1,81 @@
|
||||
# Strict Content-Security-Policy for the Scalar UI shell
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The headers module builds the `Content-Security-Policy` header value
|
||||
emitted on every HTML response from the Scalar UI handler. The default
|
||||
posture is conservative: `default-src 'self'` plus the minimum set of
|
||||
directive relaxations the Scalar shell needs to render. In
|
||||
`assetMode = "cdn"` the policy is widened (per-handler) to whitelist the
|
||||
configured Scalar CDN origin. In `assetMode = "bundled"` the policy
|
||||
stays strict — every script/style/font load resolves to the host's own
|
||||
origin under `<uiEndpoint>/assets/`.
|
||||
|
||||
## Features
|
||||
|
||||
- `buildCspHeader({ assetMode, cdnOrigin?, openapiOrigin? })` — emits
|
||||
the `Content-Security-Policy` header pair.
|
||||
- `originOf(url)` — extracts the origin portion of a URL or returns
|
||||
`undefined` for non-URL inputs (used at handler construction to derive
|
||||
the CDN + OpenAPI origins from the parsed options).
|
||||
- `cspAllowsInlineScripts(headerValue)` — conformance predicate the
|
||||
handler test uses to prove the rendered policy never permits inline
|
||||
scripts. The Scalar bootstrap is loaded as an external `<script src>`
|
||||
so `'unsafe-inline'` in `script-src` would be a regression.
|
||||
- `'unsafe-inline'` allowed ONLY in `style-src` for the upstream Scalar
|
||||
React app's runtime style injection.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { buildCspHeader } from "@suaveplan/api-openapi-ui-scalar/headers";
|
||||
|
||||
const csp = buildCspHeader({ assetMode: "bundled" });
|
||||
console.log(csp.name); // "Content-Security-Policy"
|
||||
console.log(csp.value); // strict "default-src 'self'; script-src 'self'; …"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildCspHeader,
|
||||
cspAllowsInlineScripts,
|
||||
originOf,
|
||||
} from "@suaveplan/api-openapi-ui-scalar/headers";
|
||||
|
||||
const cdnOrigin = originOf("https://cdn.jsdelivr.net/npm/@scalar/api-reference");
|
||||
const openapiOrigin = originOf("https://api.example.com/openapi.json");
|
||||
const { name, value } = buildCspHeader({
|
||||
assetMode: "cdn",
|
||||
cdnOrigin,
|
||||
openapiOrigin,
|
||||
});
|
||||
|
||||
if (cspAllowsInlineScripts(value)) {
|
||||
throw new Error("Scalar UI must never emit unsafe-inline scripts");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `CSP_HEADER_NAME` — the canonical header name string.
|
||||
- `BuildCspInput` — `{ assetMode, cdnOrigin?, openapiOrigin? }`.
|
||||
- `buildCspHeader(input)` — `{ name, value }`.
|
||||
- `originOf(url)` — `string | undefined`.
|
||||
- `cspAllowsInlineScripts(headerValue)` — `boolean`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The policy structure is built directive-by-directive rather than via a
|
||||
single template string so future per-environment relaxations
|
||||
(e.g. nonce-based `script-src`) can be added without changing every
|
||||
call site. `script-src` never includes `'unsafe-inline'`; this is a
|
||||
load-bearing invariant enforced by the conformance predicate exposed
|
||||
above. The CDN origin is computed once at handler construction so a
|
||||
malicious `cdnUrl` value cannot leak into the response header — Zod
|
||||
rejects non-URL inputs at the options layer before `originOf` runs.
|
||||
@@ -0,0 +1,83 @@
|
||||
# Scalar HTML shell renderer
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The render-shell module emits the HTML body returned by the Scalar UI
|
||||
handler. It embeds the configured OpenAPI document URL into a
|
||||
`<script id="scalar-config" type="application/json">` block (inert under
|
||||
strict CSP — JSON is not a script-src vector), references the resolved
|
||||
Scalar runtime URL (CDN or bundled), and renders a small status / path
|
||||
listing placeholder so even the air-gapped bundled bootstrap has
|
||||
something to populate. When the operator runs nonce-based CSP, the
|
||||
`renderShell` API accepts a `nonce` value the renderer threads onto the
|
||||
inline `<script>` tags.
|
||||
|
||||
## Features
|
||||
|
||||
- `escapeHtml(input)` — replaces `& < > " '` with their entity
|
||||
equivalents. Used on every caller-supplied string embedded in the
|
||||
shell.
|
||||
- `renderShell({ options, openapiUrl, runtimeUrl, nonce? })` — emits the
|
||||
full HTML body. The body references `openapiUrl` exactly once in the
|
||||
Scalar configuration block per the requirement spec.
|
||||
- Bundled-mode shells include a `<link rel="stylesheet">` to the local
|
||||
CSS asset; cdn-mode shells omit the link because the upstream Scalar
|
||||
bundle ships its own styles.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { renderShell } from "@suaveplan/api-openapi-ui-scalar/html";
|
||||
import { parseScalarUiOptions } from "@suaveplan/api-openapi-ui-scalar/options";
|
||||
|
||||
const options = parseScalarUiOptions({ openapiUrl: "/openapi.json" });
|
||||
const html = renderShell({
|
||||
options,
|
||||
openapiUrl: "/openapi.json",
|
||||
runtimeUrl: "https://cdn.jsdelivr.net/npm/@scalar/api-reference",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { renderShell } from "@suaveplan/api-openapi-ui-scalar/html";
|
||||
import { parseScalarUiOptions } from "@suaveplan/api-openapi-ui-scalar/options";
|
||||
|
||||
const options = parseScalarUiOptions({
|
||||
openapiUrl: "/openapi.json?plugin=billing",
|
||||
theme: "deepSpace",
|
||||
assetMode: "bundled",
|
||||
});
|
||||
|
||||
// Per-request nonce for hosts running nonce-based CSP.
|
||||
const html = renderShell({
|
||||
options,
|
||||
openapiUrl: options.openapiUrl,
|
||||
runtimeUrl: "/docs/ui/scalar/assets/scalar.<hash>.js",
|
||||
nonce: "n-9b2f7c2a",
|
||||
});
|
||||
|
||||
// The body references the configured OpenAPI URL exactly once.
|
||||
console.assert(html.split(options.openapiUrl).length - 1 === 1);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `RenderShellInput` — `{ options, openapiUrl, runtimeUrl, nonce? }`.
|
||||
- `renderShell(input)` — returns the full HTML body string.
|
||||
- `escapeHtml(input)` — entity-escapes the five hazardous characters.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The configuration block JSON is post-processed to replace every `<` with
|
||||
its unicode escape so string values like `"foo</script>"` cannot
|
||||
terminate the script tag. This is the same defence applied by the
|
||||
sibling `api-graphql-ui-graphiql` package; the technique is documented
|
||||
by OWASP and is the strictest path that keeps the policy free of
|
||||
`'unsafe-inline'`. The OpenAPI URL is embedded inside the JSON block
|
||||
exactly once — verified by the handler conformance test.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Scalar UI OTel instruments
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The instruments module wires the Scalar UI handler's OTel instruments
|
||||
into a frozen bundle constructed once at handler-mount time. The bundle
|
||||
defaults to the OTel global meter provider, which itself falls back to
|
||||
a no-op implementation when no SDK is installed, so the package works
|
||||
in test environments without ceremony. The bundle is intentionally
|
||||
small — UI shells stay passive and the bulk of observability is
|
||||
delegated to the surrounding router request metrics.
|
||||
|
||||
## Features
|
||||
|
||||
- `createScalarInstruments(config?)` — builds the meter + counter.
|
||||
- `recordUiRequest(instruments, { plugin? })` — adds a single
|
||||
observation. Always carries `{ ui: "scalar" }`; the per-plugin handler
|
||||
fans out a second observation labelled with `{ plugin }`.
|
||||
- `UI_LABEL_VALUE` — exported so siblings (`api-openapi-ui-swagger`,
|
||||
`api-openapi-ui-redoc`) share the naming convention.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
createScalarInstruments,
|
||||
recordUiRequest,
|
||||
} from "@suaveplan/api-openapi-ui-scalar/instruments";
|
||||
|
||||
const instruments = createScalarInstruments();
|
||||
recordUiRequest(instruments);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { metrics, type MeterProvider } from "@opentelemetry/api";
|
||||
import { createScalarInstruments } from "@suaveplan/api-openapi-ui-scalar/instruments";
|
||||
|
||||
const meterProvider: MeterProvider = metrics.getMeterProvider();
|
||||
const instruments = createScalarInstruments({ meterProvider });
|
||||
|
||||
// Per-plugin observation — labelled with both `ui` and `plugin` for
|
||||
// dashboard fan-out across multiple Scalar mounts.
|
||||
instruments.requestsTotal.add(1, { ui: "scalar", plugin: "billing" });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ScalarInstrumentsConfig` — `{ meterProvider? }`.
|
||||
- `ScalarInstruments` — `{ meter, requestsTotal }` (frozen).
|
||||
- `createScalarInstruments(config?)` — returns frozen `ScalarInstruments`.
|
||||
- `recordUiRequest(instruments, attrs?)` — labels each observation with
|
||||
`{ ui: "scalar" }` plus the optional `plugin` attribute.
|
||||
- `UI_LABEL_VALUE` — the string literal `"scalar"`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The counter name `openapi.ui.requests_total` is shared across every
|
||||
sibling Scalar / Swagger / Redoc UI handler so dashboards can sum across
|
||||
operator choices via the `{ ui }` attribute. Per-plugin shells emit a
|
||||
second observation rather than overriding the bare-UI one so the
|
||||
top-line total stays correct. The handler does NOT increment on asset
|
||||
sub-requests — router request metrics cover those, and double-counting
|
||||
the assets would inflate the per-request total.
|
||||
@@ -0,0 +1,79 @@
|
||||
# `ScalarUiOptions` — Zod-validated configuration
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The options module owns the single Zod schema consumed by every Scalar
|
||||
UI handler factory. It is exposed both directly (`scalarUiOptionsSchema`)
|
||||
and via the convenience `parseScalarUiOptions(input)` helper that wraps
|
||||
the underlying `ZodError` in an `OpenapiUiScalarError` tagged with
|
||||
`OPENAPI_UI_SCALAR.INVALID_OPTIONS`. This double-surface lets host
|
||||
config ingestion decide whether to surface raw Zod issues or rely on the
|
||||
package's stable error-code contract.
|
||||
|
||||
## Features
|
||||
|
||||
- Required `openapiUrl` field (with default `/openapi.json`).
|
||||
- Endpoint defaults match design doc §7 (`/docs/ui/scalar`).
|
||||
- Eleven built-in `theme` literals matching the upstream Scalar palette.
|
||||
- `layout` literals `"modern" | "classic"`.
|
||||
- `assetMode` literals `"cdn" | "bundled"` toggling the runtime asset
|
||||
resolution path.
|
||||
- `cdnUrl` defaults to the upstream JSDelivr URL of
|
||||
`@scalar/api-reference`.
|
||||
- `editorOptions` is an open passthrough bag for upstream Scalar config
|
||||
options the schema does not enumerate.
|
||||
- `.strict()` rejects unknown keys so typos surface at host-config load.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { parseScalarUiOptions } from "@suaveplan/api-openapi-ui-scalar/options";
|
||||
|
||||
const opts = parseScalarUiOptions({ openapiUrl: "/openapi.json" });
|
||||
console.log(opts.uiEndpoint); // "/docs/ui/scalar"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
parseScalarUiOptions,
|
||||
scalarUiOptionsSchema,
|
||||
} from "@suaveplan/api-openapi-ui-scalar/options";
|
||||
|
||||
// Schema can be composed with host-config layers — every default is
|
||||
// safe in the empty-object case so the schema can be merged with other
|
||||
// host config without surprises.
|
||||
const hostSchema = scalarUiOptionsSchema.extend({
|
||||
enabled: scalarUiOptionsSchema.shape.darkMode,
|
||||
});
|
||||
|
||||
const parsed = parseScalarUiOptions({
|
||||
openapiUrl: "/v1/openapi.json",
|
||||
theme: "deepSpace",
|
||||
layout: "classic",
|
||||
assetMode: "bundled",
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `scalarUiOptionsSchema` — Zod object schema, `.strict()`.
|
||||
- `parseScalarUiOptions(input?)` — returns the resolved `ScalarUiOptions`
|
||||
or throws `OpenapiUiScalarError(INVALID_OPTIONS)`.
|
||||
- `SCALAR_THEMES` / `SCALAR_LAYOUTS` / `SCALAR_ASSET_MODES` — literal
|
||||
unions backing the schema enums.
|
||||
- `ScalarUiOptions` / `ScalarUiOptionsInput` — output / input types.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schema uses Zod `.default()` on every optional field so the empty
|
||||
`{}` case round-trips to a complete options object. The `.strict()` mode
|
||||
is essential — host operators frequently mistype keys (e.g. `themes` vs
|
||||
`theme`) and the strict guard surfaces those at boot rather than at
|
||||
first request. `INVALID_OPTIONS` wraps `safeParse` failures so the error
|
||||
chain preserves the underlying issue list for log/telemetry consumers.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Per-plugin Scalar UI handler
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The per-plugin module exposes `createPerPluginScalarUiHandler`, the
|
||||
variant the host mounts under `/docs/ui/scalar/<plugin>` so each plugin
|
||||
gets its own Scalar shell pointing at a filtered OpenAPI document
|
||||
(typically `/openapi.json?plugin=<name>`). It composes the standard
|
||||
`createScalarUiHandler` factory but rewrites the OpenAPI URL passthrough
|
||||
and fans out the OTel observation with a `{ plugin }` attribute so
|
||||
dashboards summing per-plugin mounts get distinct buckets without
|
||||
losing the top-line UI total.
|
||||
|
||||
## Features
|
||||
|
||||
- Defaults `uiEndpoint` to `/docs/ui/scalar/<plugin>` when the caller
|
||||
does not override it — keeps the host's `/docs/ui/scalar` namespace
|
||||
collision-free across N plugins.
|
||||
- Embeds the host-computed filtered URL verbatim — the host is
|
||||
responsible for query-string composition so this package never has to
|
||||
re-implement the OpenAPI filter semantics.
|
||||
- Same CSP + asset + Zod validation guarantees as
|
||||
`createScalarUiHandler`.
|
||||
- Per-plugin OTel attribute fan-out — every render emits both the
|
||||
baseline `{ ui: "scalar" }` and `{ ui: "scalar", plugin }` observations
|
||||
so dashboards can sum either way without losing the top-line total.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createPerPluginScalarUiHandler } from "@suaveplan/api-openapi-ui-scalar/per-plugin";
|
||||
|
||||
const { handler } = createPerPluginScalarUiHandler({
|
||||
plugin: "billing",
|
||||
openapiUrlForPlugin: "/openapi.json?plugin=billing",
|
||||
});
|
||||
|
||||
const response = handler({
|
||||
method: "GET",
|
||||
path: "/docs/ui/scalar/billing",
|
||||
});
|
||||
console.log(response.status); // 200
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { createPerPluginScalarUiHandler } from "@suaveplan/api-openapi-ui-scalar/per-plugin";
|
||||
|
||||
const bundle = createPerPluginScalarUiHandler({
|
||||
plugin: "billing",
|
||||
openapiUrlForPlugin: "/openapi.json?plugin=billing",
|
||||
theme: "deepSpace",
|
||||
assetMode: "bundled",
|
||||
uiEndpoint: "/docs/billing", // override the default mount path
|
||||
});
|
||||
|
||||
console.log(bundle.plugin); // "billing"
|
||||
console.log(bundle.options.uiEndpoint); // "/docs/billing"
|
||||
|
||||
// The shell embeds the filtered URL exactly once.
|
||||
const html = bundle.handler({
|
||||
method: "GET",
|
||||
path: "/docs/billing",
|
||||
}).body;
|
||||
console.assert(html.includes("/openapi.json?plugin=billing"));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `CreatePerPluginScalarUiHandlerInput` — `Omit<ScalarUiOptionsInput,
|
||||
"openapiUrl">` plus `{ plugin, openapiUrlForPlugin, meterProvider?,
|
||||
bundledAssetProbe? }`.
|
||||
- `createPerPluginScalarUiHandler(input)` —
|
||||
`PerPluginScalarUiHandlerBundle`.
|
||||
- `PerPluginScalarUiHandlerBundle` extends `ScalarUiHandlerBundle` with
|
||||
the resolved `plugin` field.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The per-plugin factory deliberately delegates to the base
|
||||
`createScalarUiHandler` so the per-plugin path inherits every
|
||||
validation + CSP + asset-resolution guarantee. The only divergences are
|
||||
the rewritten OpenAPI URL, the default `uiEndpoint`, and the extra
|
||||
counter observation for the per-plugin attribute. Doubling the counter
|
||||
is intentional — the bare `{ ui: "scalar" }` total stays correct while
|
||||
the per-plugin breakdown remains queryable from the same series.
|
||||
Reference in New Issue
Block a user