Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

#40

The celebration never draws: the library offloads to a worker that the CSP blocks

closed · opened by keenan 13d ago

keenan opened this 13d ago

The celebration introduced in #01M5XZDTV4R3EE44RA8P9BGRG4 never drew anything. The merge redirect carried ?merged=1, static/app.js fired, the flag was stripped from the address bar, the 25 KB bundle was fetched (200) — and the page showed no confetti, with nothing in the console.

Cause

canvas-confetti’s default export is constructed as confettiCannon(null, { useWorker: true, resize: true }). So every call to window.confetti(...) tries to run the animation in a Web Worker built from a blob: URL. This page’s CSP is default-src 'self' with no worker-src, so that worker is blocked — but the library does not fall back to the main thread. Its fire() does:

if (worker && !initialized) { worker.init(canvas); }   // transferControlToOffscreen
initialized = true;
if (worker) { canvas.__confetti_initialized = true; }
...
if (worker) { return worker.fire(options, size, done); }

So the canvas is created at full size, transferred to a worker that will never run, and left permanently empty. The failure is silent: the only trace is a console.warn("🎊 Could not load worker") on some browsers, and on Firefox the worker constructor does not even throw.

Reproduction

A headless Firefox on a scratch instance, after a real Merge click, reading the canvas:

{"canvas":true,"w":1360,"h":682,"pixels":null,"err":"InvalidStateError: ...",
 "markedTransferred":true}

__confetti_initialized is only ever set on the worker path, and getContext throws because the canvas was transferred away. Counting non-transparent pixels is the only check that distinguishes “drew” from “created and never drawn” — counting <canvas> elements calls this a success.

Fix

Create our own instance with useWorker: false, so the burst is drawn on the main thread: 120 particles for ~2s is nothing, and the CSP stays as it is. Widening it with worker-src 'self' blob: to keep the offscreen path would trade a real boundary for a party trick, and it is the wrong direction for a page whose whole markdown input is untrusted.

Acceptance

  • Clicking Merge on a request lands on the request page and a burst is visibly drawn, measured as non-zero painted pixels in a real browser, with no canvas handed to a worker.
  • prefers-reduced-motion: reduce still yields the page and no animation.
  • The CSP is unchanged, and no page without ?merged=1 fetches the bundle.

Comments (0)

No comments yet.