{"record":{"id":"d4fc06cb557252a6","repo":"paperclipai/paperclip","slug":"a-trusted-viewer-build-is-required-for-public-chat","errorCode":null,"errorMessage":"A trusted viewer build is required for public chat reports","messagePattern":"A trusted viewer build is required for public chat reports","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/scripts/public-eval-viewer.mjs","lineNumber":27,"sourceCode":"const ASSET = /^[A-Za-z0-9][A-Za-z0-9._-]*\\.(?:js|css|woff2)$/;\n\nexport function publicViewerShell(index, encodedPayload) {\n  return index\n    .replaceAll('\"./assets/', '\"../../viewer/assets/')\n    .replace(\n      \"<head>\",\n      `<head>\\n    <meta http-equiv=\"Content-Security-Policy\" content=\"${PUBLIC_VIEWER_CSP}\">`,\n    )\n    .replace(\n      '<script type=\"module\"',\n      `<script type=\"application/json\" id=\"paperclip-eval-report\">${encodedPayload}</script>\\n    <script type=\"module\"`,\n    );\n}\n\nexport async function trustedViewerFiles(viewerRoot) {\n  const rootStat = viewerRoot ? await lstat(viewerRoot) : null;\n  if (!rootStat || rootStat.isSymbolicLink() || !rootStat.isDirectory())\n    throw new Error(\n      \"A trusted viewer build is required for public chat reports\",\n    );\n  const indexStat = await lstat(join(viewerRoot, \"index.html\"));\n  const assetsStat = await lstat(join(viewerRoot, \"assets\"));\n  if (\n    indexStat.isSymbolicLink() ||\n    !indexStat.isFile() ||\n    assetsStat.isSymbolicLink() ||\n    !assetsStat.isDirectory()\n  )\n    throw new Error(\"Trusted viewer must not use symlinks\");\n  const index = await readFile(join(viewerRoot, \"index.html\"), \"utf8\");\n  const files = new Map();\n  for (const entry of await readdir(join(viewerRoot, \"assets\"), {\n    withFileTypes: true,\n  })) {\n    if (!entry.isFile() || entry.isSymbolicLink() || !ASSET.test(entry.name))\n      throw new Error(\"Unexpected trusted viewer asset\");","sourceCodeStart":9,"sourceCodeEnd":45,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/scripts/public-eval-viewer.mjs#L9-L45","documentation":"trustedViewerFiles prepares a viewer build for public chat reports and requires a real, pre-built, trusted viewer directory: an existing non-symlink directory. A missing, non-directory, or symlinked root means no trusted build is available, so serving public reports is refused.","triggerScenarios":"Calling trustedViewerFiles with viewerRoot undefined/null, a path that does not exist, a path that is a file, or a symlinked path, when generating a public chat report.","commonSituations":"The viewer build step was skipped in CI so the dist directory doesn't exist; the env var/CLI flag pointing at the viewer build is unset or mistyped; the path was replaced by a symlink (rejected for security).","solutions":["Build the viewer first so <viewerRoot>/index.html and assets/ exist, and pass that directory as viewerRoot.","Check the flag/env var supplying viewerRoot for typos or an unset value; pass an absolute path.","Replace any symlink with the real directory (copy/bind-mount), since symlinks are rejected for security.","Verify with 'ls <viewerRoot>/index.html <viewerRoot>/assets' before invoking."],"exampleFix":"// before\nawait trustedViewerFiles(process.env.VIEWER_ROOT); // unset\n// after\nconst viewerRoot = path.resolve(process.env.VIEWER_ROOT ?? \"ui/dist\");\nawait fs.access(join(viewerRoot, \"index.html\"));\nawait trustedViewerFiles(viewerRoot);","handlingStrategy":"validation","validationCode":"import { lstat } from 'node:fs/promises';\nconst st = await lstat(viewerRoot);\nif (!st.isDirectory() || st.isSymbolicLink()) throw new Error('viewerRoot must be a real built viewer directory');","typeGuard":"async function isTrustedViewerRoot(p) {\n  try { const st = await lstat(p); return st.isDirectory() && !st.isSymbolicLink(); } catch { return false; }\n}","tryCatchPattern":"try {\n  const files = await trustedViewerFiles(viewerRoot);\n} catch (err) {\n  if (err.message === 'A trusted viewer build is required for public chat reports') {\n    throw new Error(`Viewer build missing at ${viewerRoot}; run the viewer build step first`);\n  }\n  throw err;\n}","preventionTips":["Run the viewer build as a required CI step before generating public reports.","Pass an absolute, verified path to viewerRoot (fail fast if the env var is unset).","Never point viewerRoot at a symlink; stage real copies.","Smoke-check index.html and assets/ exist before invoking."],"tags":["security","filesystem","configuration"],"backgroundTag":"missing-required-config-field","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}