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 assets only 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 integrity attribute 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",
  },
});