@suluk/scalar
Render an OpenAPI v4 'Suluk' document with the Scalar API Reference UI — natively as v4, or downgraded for vanilla Scalar.
Part of Suluk — one typed OpenAPI v4 contract projecting into every full-stack layer.
CANDIDATE tooling — not official OpenAPI. Suluk is a single-contributor candidate for OpenAPI Specification v4.0 ("Moonwalk"), unaffiliated with the OpenAPI Initiative and unable to ratify anything on the SIG's behalf.
Install
bun add @suluk/scalar
What it does
Takes a parsed v4 document and returns a self-contained Scalar API Reference HTML page (string or Response) — no build step, no server-side React. Two tracks:
Native v4 (
scalarV4*). Feeds the v4 doc as v4 to the suluk-forked Scalar standalone, which projectsrequests→operations internally, shows the4.0.0-candidateversion badge, and renders the v4-only shapes (multi-request-per-method, request-name identity viashowOperationId). The fork bundle is served from@suluk/scalar-standaloneon jsDelivr by default, so it works out of the box.3.1 downgrade (
scalar*). Projects the v4 doc down to OpenAPI 3.1 (via@suluk/openapi-compat) and feeds it to vanilla Scalar — the baseline view that runs against any pinned upstream Scalar bundle.v4 facets, surfaced either way.
x-suluk-costandx-suluk-accessbecome Scalarx-badgeson each operation (+ a💳/⏳/🎁settlement badge); a "Suluk v4 contract" intro with a cost-coverage + hardening-grade tally is prepended to the doc description.Route economics, per operation. Expanding an operation reveals the full economics — not just a number:
- Cost — the per-call floor plus every DYNAMIC/metered component with its rate + unit + description (
+ 6000µ$ / 1k tokens — openai:whisper (the AI model),+ 90µ$ / MB — r2-egress (file size)), so file-size / token / model / compute costs read at a glance. - Settlement — how the user pays:
💳 credits(N debited) ·⏳ rate-limited(free within the cap) ·🎁 free(operator-absorbed) — the C044 axis. - Accrues / Triggers — when a non-synchronous cost fires (webhook-received / scheduled / queue-consumed / callback-completed) + who pays, and the reverse: the downstream cost-bearing events this route triggers (
Triggers — \billingSync` (webhook-received, ~$0.0002)`).
- Cost — the per-call floor plus every DYNAMIC/metered component with its rate + unit + description (
Native collapsible hardening report, per route. Every operation carries an inline
<details>with its@suluk/hardeninput-hardening grade (A–F) + score + each finding (severity · schema path · message · fix) — collapsed by default, expand to audit the route.
Declare it all inline on the route — RouteContract.cost (@suluk/hono) carries the full CostModel next to scopes/errors/rateLimit, so a backend dev considers the economics while writing the code, and it renders here + audits in @suluk/cost + reports elsewhere.
- Pinned, never
@latest.SCALAR_VERSIONandSULUK_FORK_CDNare exported so the UI never drifts under you; overridecdnto self-host the bytes.
When to reach for it
Reach for @suluk/scalar when you want the Scalar UI for your contract. Use scalarV4Response for the full native-v4 reference (badges, the "View as" role projector, an insights drawer); use scalarResponse for a plain, dependency-light 3.1 fallback.
Siblings for the same job:
@suluk/reference— the native v4 renderer (suluk's own UI, not Scalar's chrome).@suluk/swagger— render via Swagger UI (always through the 3.1 downgrade).@suluk/editor— an editable two-pane authoring surface (uses this package for its live preview).
Usage
Native v4 reference (the common case)
import { scalarV4Response } from "@suluk/scalar";
import type { OpenAPIv4Document } from "@suluk/core";
// In a Bun.serve / Hono / fetch handler:
app.get("/reference", () =>
scalarV4Response(document, {
pageTitle: "saasuluk — OpenAPI v4 reference",
brand: "saasuluk",
// optional: a per-role "View as" projector wired to an endpoint that returns the projected spec
specUrl: "/reference/spec",
views: [
{ label: "Anonymous", value: "anon" },
{ label: "Signed-in", value: "user" },
{ label: "Admin", value: "admin" },
],
// optional: an embeddable superpowers panel opened as an in-page drawer
insightsUrl: "/reference/insights",
}),
);
// Picking a role re-fetches the projected spec and re-mounts Scalar:
app.get("/reference/spec", (c) => c.json(enrichedV4(projectFor(c)).spec));
scalarV4Response defaults cdn to the published fork (SULUK_FORK_CDN). To self-host, vendor the fork bundle and pass cdn:
scalarV4Response(document, { cdn: "/vendor/scalar/standalone-suluk.js", brand: "saasuluk" });
Vanilla Scalar via the 3.1 downgrade (the baseline)
import { scalarResponse, SCALAR_VERSION } from "@suluk/scalar";
app.get("/scalar", () =>
scalarResponse(document, {
cdn: `/vendor/scalar/standalone-${SCALAR_VERSION}.js`, // or omit for the jsDelivr default
facetBadges: false, // plain 4→3 downgrade, no suluk superpowers
}),
);
As a string
Both renderers have an HTML-string sibling (scalarV4Html, scalarHtml) returning { html, diagnostics }, where diagnostics are the lossy-conversion notes from the v4→3.1 downgrade (the native-v4 path produces none):
import { scalarHtml } from "@suluk/scalar";
const { html, diagnostics } = scalarHtml(document, { facetBadges: false });
if (diagnostics.length) console.warn("downgrade losses:", diagnostics);
Enrich a spec without rendering a page
When you need the facet-enriched spec object (e.g. to serve as JSON for the "View as" endpoint above), use the enriched* helpers directly:
import { enrichedV4, enrichedSpec } from "@suluk/scalar";
const { spec } = enrichedV4(document); // v4 shape, badges + detail stamped, NOT downgraded
const { spec, diagnostics } = enrichedSpec(document); // downgraded to 3.1, then facet-enriched
Neither mutates the input document. Pass { facetBadges: false } to skip the cost/access enrichment.
API
| Export | What it does |
|---|---|
scalarV4Response(doc, opts?) |
Native-v4 Scalar page as a text/html Response (the fork). |
scalarV4Html(doc, opts?) |
Same, as { html, diagnostics }. |
scalarResponse(doc, opts?) |
3.1-downgrade Scalar page as a Response (vanilla Scalar). |
scalarHtml(doc, opts?) |
Same, as { html, diagnostics }. |
enrichedV4(doc, opts?) |
{ spec } — v4 doc enriched with facet badges/detail/intro, no downgrade. |
enrichedSpec(doc, opts?) |
{ spec, diagnostics } — downgraded to 3.1, then facet-enriched. |
enrichFacetBadges(spec) |
Mutate a 3.1 spec: attach x-badges from x-suluk-cost/x-suluk-access. |
enrichFacetDetail(spec) |
Mutate a 3.1 spec: append the cost/access detail to each op's description. |
enrichV4Facets(doc) |
Mutate a v4 doc: stamp badges + detail on each request + the intro. |
v4Intro(spec) |
Prepend the "Suluk v4 contract" note + cost-coverage tally to info.description. |
SCALAR_VERSION |
The pinned upstream Scalar version (string). |
SULUK_FORK_CDN / SULUK_FORK_STANDALONE_VERSION |
The default fork-bundle CDN URL + its version. |
Key opts (full set in ScalarOptions / ScalarV4Options): pageTitle, cdn, facetBadges, customCss, configuration (merged into createApiReference); and for the v4 reference: brand, specUrl / specParam / views (the role projector), insightsUrl / insightsLabel (the drawer).
Boundary
This package only renders — the L3 line: render/generate, never host. It takes a doc you already parsed and validated and hands back HTML; it never fetches your spec, owns your routes, or mounts a server. You inject the doc (and, for the role projector, the endpoint that returns each role's projected spec).
It also does not fork Scalar. The patched standalone — the part that teaches Scalar the native v4 shapes — lives at tooling/ts/scalar-fork/ (clone-upstream + patches/*.patch → standalone-suluk.js, see FORK.md); this package only feeds that bundle and surfaces the x-suluk-* facets as badges. To teach Scalar a new v4 shape, add a patch there, not here.
License
Apache-2.0