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:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.