{"record":{"id":"00f78482b44936fa","repo":"paperclipai/paperclip","slug":"invalid-or-oversized-announcement-animation","errorCode":null,"errorMessage":"Invalid or oversized announcement animation","messagePattern":"Invalid or oversized announcement animation","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/announcement-animation.ts","lineNumber":10,"sourceCode":"import createDOMPurify from \"dompurify\";\nimport { JSDOM } from \"jsdom\";\nimport { ANNOUNCEMENT_ANIMATION_MAX_BYTES } from \"@paperclipai/shared\";\n\n// A visual HTML/CSS document, never an application. JSDOM does not execute\n// scripts or load resources. DOMPurify handles HTML parsing/normalization;\n// CSP on delivery also blocks all network requests, including CSS URLs.\nexport function validateAnnouncementAnimation(bytes: Uint8Array): string {\n  if (!bytes.length || bytes.byteLength > ANNOUNCEMENT_ANIMATION_MAX_BYTES) {\n    throw new Error(\"Invalid or oversized announcement animation\");\n  }\n  const source = new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes);\n  const dom = new JSDOM(\"\");\n  try {\n    const purifier = createDOMPurify(dom.window as unknown as Parameters<typeof createDOMPurify>[0]);\n    const html = purifier.sanitize(source, {\n      WHOLE_DOCUMENT: true,\n      ALLOWED_TAGS: [\"html\", \"head\", \"body\", \"style\", \"div\", \"span\", \"p\", \"br\", \"strong\", \"em\", \"b\", \"i\",\n        \"svg\", \"g\", \"path\", \"circle\", \"ellipse\", \"rect\", \"line\", \"polyline\", \"polygon\", \"text\", \"tspan\", \"title\", \"desc\"],\n      ALLOWED_ATTR: [\"class\", \"id\", \"style\", \"viewBox\", \"xmlns\", \"width\", \"height\", \"x\", \"y\", \"x1\", \"x2\", \"y1\", \"y2\",\n        \"cx\", \"cy\", \"r\", \"rx\", \"ry\", \"d\", \"points\", \"fill\", \"stroke\", \"stroke-width\", \"stroke-linecap\",\n        \"stroke-linejoin\", \"stroke-dasharray\", \"stroke-dashoffset\", \"opacity\", \"transform\", \"text-anchor\"],\n      ALLOW_DATA_ATTR: false,\n      ALLOW_ARIA_ATTR: false,\n    });\n    if (purifier.removed.length) {\n      throw new Error(\"Animation must contain only visual HTML/CSS or inline SVG; scripts, navigation, resources and interactive elements are not supported\");\n    }","sourceCodeStart":1,"sourceCodeEnd":28,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/announcement-animation.ts#L1-L28","documentation":"validateAnnouncementAnimation checks an uploaded announcement animation before sanitization: it must be non-empty and at most ANNOUNCEMENT_ANIMATION_MAX_BYTES (128 KiB), and must decode as strict UTF-8. Anything empty, oversized, or not valid UTF-8 is rejected before DOMPurify ever runs, bounding sanitize work and preventing abuse via giant or binary uploads.","triggerScenarios":"Publishing an announcement whose animation asset is 0 bytes, exceeds 128 KiB, or contains bytes that are not valid UTF-8 (e.g. a binary file, gzip archive, or latin-1 encoded HTML).","commonSituations":"Uploading a minified CSS/HTML bundle that quietly grew past 128 KiB; accidentally uploading a .zip/.gif as the animation; tooling that writes the file in a non-UTF-8 encoding; an empty artifact from a failed export step.","solutions":["Shrink the animation below 128 KiB — minify CSS/HTML, remove whitespace, split into multiple announcements.","Ensure the file is valid UTF-8 text (convert encodings; never upload binary formats).","Check the build/export step produced a non-empty file before publishing.","Re-run scripts/publish-announcements.ts after fixing the asset; it enforces the same limit locally via prepareAnnouncementPublish."],"exampleFix":"// before\nconst bytes = new Uint8Array(await fs.readFile(\"animation.bin\"));\nawait publishAnnouncement({ kind: \"animation\", bytes }); // throws\n// after\nconst bytes = new Uint8Array(await fs.readFile(\"animation.html\"));\nif (!bytes.length || bytes.byteLength > 128 * 1024) {\n  throw new Error(\"animation must be 1..131072 bytes of UTF-8 HTML/CSS\");\n}\nawait publishAnnouncement({ kind: \"animation\", bytes });","handlingStrategy":"validation","validationCode":"const bytes = new Uint8Array(buf);\nif (!bytes.length || bytes.byteLength > 128 * 1024) {\n  throw new Error(`animation must be 1..${128*1024} bytes`);\n}\nnew TextDecoder(\"utf-8\", { fatal: true }).decode(bytes); // throws on non-UTF-8","typeGuard":"null","tryCatchPattern":"try { await publishAnnouncement({ kind: \"animation\", bytes }); }\ncatch (e) {\n  if (e.message === \"Invalid or oversized announcement animation\") {\n    return minifyAndRetry(assetPath); // compress, re-encode UTF-8\n  }\n  throw e;\n}","preventionTips":["Check file size and UTF-8 validity before publishing","Minify animation HTML/CSS to stay under 128 KiB","Never upload binary formats as animations","Use scripts/publish-announcements.ts which enforces limits locally"],"tags":["validation","file-size","announcements"],"backgroundTag":"file-size-limit-exceeded","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}