paperclipai/paperclip · error · Error

Unexpected trusted viewer asset

Error message

Unexpected trusted viewer asset

What it means

trustedViewerFiles() reads a pre-built trusted viewer bundle (index.html plus an assets/ directory) that will be published as a public eval report. Because that bundle is served publicly under a strict CSP, every asset entry must be a regular, non-symlink file whose name matches a strict allowlist regex (ASCII-alphanumeric start, then [A-Za-z0-9._-], ending in .js, .css, or .woff2). If any directory entry fails that check, the function throws "Unexpected trusted viewer asset" to stop potentially unsafe or unexpected files from being shipped.

Source

Thrown at packages/paperclip-runner/scripts/public-eval-viewer.mjs:45

    throw new Error(
      "A trusted viewer build is required for public chat reports",
    );
  const indexStat = await lstat(join(viewerRoot, "index.html"));
  const assetsStat = await lstat(join(viewerRoot, "assets"));
  if (
    indexStat.isSymbolicLink() ||
    !indexStat.isFile() ||
    assetsStat.isSymbolicLink() ||
    !assetsStat.isDirectory()
  )
    throw new Error("Trusted viewer must not use symlinks");
  const index = await readFile(join(viewerRoot, "index.html"), "utf8");
  const files = new Map();
  for (const entry of await readdir(join(viewerRoot, "assets"), {
    withFileTypes: true,
  })) {
    if (!entry.isFile() || entry.isSymbolicLink() || !ASSET.test(entry.name))
      throw new Error("Unexpected trusted viewer asset");
    files.set(
      `viewer/assets/${entry.name}`,
      await readFile(join(viewerRoot, "assets", entry.name)),
    );
  }
  if (
    ![...files.keys()].some((name) => name.endsWith(".js")) ||
    !index.includes('<script type="module"')
  )
    throw new Error("Incomplete trusted viewer build");
  return { index, files };
}

export function validatePublicChatPayload(payload) {
  if (
    payload?.publication?.schema !== PUBLIC_CHAT_SCHEMA ||
    payload.view?.sessionId !== "public-report" ||
    payload.view?.composer?.state !== "disabled" ||

View on GitHub (pinned to 01ad858492)

Solutions

  1. Remove or move out any files in viewerRoot/assets that do not match ^[A-Za-z0-9][A-Za-z0-9._-]*\.(js|css|woff2)$ (especially .map sourcemaps and dotfiles).
  2. Rebuild the viewer with sourcemaps disabled (e.g. build.sourcemap: false in the Vite config) so only .js/.css/.woff2 are emitted.
  3. Replace any symlinks or subdirectories inside assets/ with real regular files flattened into assets/.
  4. If a new asset type is genuinely required, update the ASSET regex in public-eval-viewer.mjs and get the change reviewed as a CSP/publication surface change.

Example fix

// before: assets/ contains app.js, app.js.map, .DS_Store
// after: clean the build output so only allowlisted assets remain
rm viewer/assets/app.js.map viewer/assets/.DS_Store
pnpm --filter @paperclipai/paperclip-runner build:viewer
Defensive patterns

Strategy: validation

Validate before calling

import { readdir } from "node:fs/promises";
const ASSET = /^[A-Za-z0-9][A-Za-z0-9._-]*\.(?:js|css|woff2)$/;
const bad = (await readdir(join(root, "assets"))).filter((n) => !ASSET.test(n));
if (bad.length) throw new Error(`Non-publishable assets: ${bad.join(", ")}`);

Type guard

const isPublishableAsset = (entry) => entry.isFile() && !entry.isSymbolicLink() && /^[A-Za-z0-9][A-Za-z0-9._-]*\.(?:js|css|woff2)$/.test(entry.name);

Prevention

When it happens

Trigger: Calling trustedViewerFiles(viewerRoot) when the assets/ directory contains: a subdirectory, a symlink, a file with a disallowed extension (e.g. .map, .png, .txt), a file starting with a dot or non-ASCII/odd character, or a name with characters outside [A-Za-z0-9._-].

Common situations: Vite build outputs sourcemaps (.js.map) or license files into assets/; a developer drops a README or screenshot into assets/; a misconfigured build with assetsInclude emits images/fonts outside the allowlist; symlinks left by a copied node_modules-based build.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/86351e8c0e8baeec. Report an issue: GitHub.