Skip to content

Search docs

Jump between documentation pages.

Browse docs

Docs UI asset integrity (SRI)

The built-in /docs page renders Scalar (default), Swagger UI, or Redoc by loading their JavaScript and CSS bundles from the jsDelivr CDN. A CDN keeps the framework dependency-free and means no build step for the docs UI, but it also means the browser will execute whatever bytes the CDN serves. If a CDN asset were ever poisoned, that code would run in the context of your docs page.

DaloyJS ships version-exact default assets with matching Subresource Integrity (SRI) hashes. It emits an integrity="…" attribute plus a crossorigin attribute on the matching <script> / <link>tag, so the browser refuses to execute an asset whose bytes don't match the pinned hash. The docs UI inherits the same supply-chain posture as the rest of the framework.

SRI-pinned docs asset
  1. 01jsDelivrVersion-exact CDN asset@scalar/api-reference@1.62.5
  2. 02Pinned integrity hashintegrity + crossorigin
  3. 03Browser hashes the bytessha384 of fetched file
  4. 04Bytes match: executedocs UI renders
  5. 05Mismatch: refusepoisoned asset blocked
DaloyJS emits an integrity and crossorigin attribute on the script or link tag for the pinned, version-exact URL. The browser hashes the downloaded bytes and refuses to execute anything that does not match, so a poisoned CDN asset never runs.

Secure defaults

SRI only works against a version-pinned, byte-stable URL. DaloyJS therefore pins the default Scalar, Swagger UI, Redoc, and AsyncAPI versions together with their SHA-384 digests. The default /docs and /asyncapi pages are protected without configuration; framework releases update each URL and hash as one reviewed pair.

Override the default assets

Use assetsonly when you want another version, another CDN, or self-hosting. When changing a URL, provide the digest of those exact bytes; a custom URL does not inherit the default asset's hash.

ts
import { App } from "@daloyjs/core";

const app = new App({
  docs: {
    assets: {
      // Override with the exact version you verified...
      scalarScriptUrl:
        "https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.62.5",
      // ...and the SRI hash of that exact file.
      scalarScriptIntegrity: "sha384-<base64-digest>",
    },
  },
});

The same assets object works for the Swagger UI renderer, which loads two assets (a stylesheet and a bundle):

ts
const app = new App({
  docs: {
    ui: "swagger",
    assets: {
      swaggerUiCssUrl:
        "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.32.8/swagger-ui.css",
      swaggerUiCssIntegrity: "sha384-<css-digest>",
      swaggerUiBundleUrl:
        "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.32.8/swagger-ui-bundle.js",
      swaggerUiBundleIntegrity: "sha384-<bundle-digest>",
    },
  },
});

Redoc loads a single standalone bundle, so it takes one URL/hash pair:

ts
const app = new App({
  docs: {
    ui: "redoc",
    assets: {
      redocScriptUrl:
        "https://cdn.jsdelivr.net/npm/redoc@2.5.3/bundles/redoc.standalone.js",
      redocScriptIntegrity: "sha384-<redoc-digest>",
    },
  },
});

Computing the hash

Download the exact pinned file and hash it. The output is exactly what goes into the *Integrity field:

sh
curl -sSL https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.62.5 \
  | openssl dgst -sha384 -binary \
  | openssl base64 -A \
  | sed 's/^/sha384-/'

jsDelivr also surfaces a copy-paste SRI snippet on each file's page, which is a convenient cross-check. Re-run this whenever you bump the pinned version.

Self-hosting instead

If your Content-Security-Policy forbids third-party CDNs, point the same assets URLs at copies you serve yourself. A custom URL may omit SRI (for example for same-origin assets under your control), but pinning a hash still adds defense in depth.

ts
const app = new App({
  docs: {
    assets: {
      scalarScriptUrl: "/docs-assets/scalar.js",
    },
  },
});

Malformed hashes fail loudly

A typo in an SRI value is dangerous: browsers silently ignore an unparseable integrityattribute and load the asset anyway, giving you a false sense of protection. To prevent that, DaloyJS validates every hash when it builds the docs HTML. A value that isn't one or more space-separated sha256- / sha384- / sha512- base64 digests throws a TypeError rather than shipping an unprotected page: immediately from the scalarHtml() / swaggerUiHtml() / redocHtml() helpers, and on the auto-mounted /docs route as a loud 500 (carrying this message) the first time the page renders.

ts
import { scalarHtml } from "@daloyjs/core/docs";

// Throws synchronously: Invalid Subresource Integrity value: "md5-nope". ...
scalarHtml({
  specUrl: "/openapi.json",
  assets: {
    scalarScriptUrl:
      "https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.62.5",
    scalarScriptIntegrity: "md5-nope",
  },
});

// The same invalid hash on the auto-mounted docs route
// (new App({ docs: { assets } })) makes GET /docs fail with a 500.

Low-level helpers

The same options flow through the scalarHtml(), swaggerUiHtml(), and redocHtml() helpers (from the @daloyjs/core/docs subpath) if you render the docs page yourself. Multiple digests are supported: separate them with whitespace, and the strongest one the browser understands wins. The crossOrigin field defaults to "anonymous"; set it to "use-credentials" only when the asset host needs credentialed requests.

ts
import { scalarHtml } from "@daloyjs/core/docs";

const html = scalarHtml({
  specUrl: "/openapi.json",
  assets: {
    scalarScriptUrl:
      "https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.62.5",
    scalarScriptIntegrity:
      "sha384-<primary> sha512-<fallback>",
    crossOrigin: "anonymous",
  },
});