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

**Diagram: SRI-pinned docs asset**

1. **Version-exact CDN asset** (jsDelivr) - @scalar/api-reference@1.62.5
2. **Pinned integrity hash** - integrity + crossorigin
3. **Browser hashes the bytes** - sha384 of fetched file
4. **Bytes match: execute** - docs UI renders
5. **Mismatch: refuse** - poisoned 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",
  },
});
```

---

Source: https://daloyjs.dev/docs/docs-asset-integrity