Artifact 33c3d6bacdc9dc458455139d6aec27a7993273c8346d2e9b1072020c3318e76a:
- File
internal/web/assets/copy.js
— part of check-in
[ef352402f6]
at
2026-08-31 20:02:16
on branch trunk
— a strict CSP: nonces, no unsafe-eval, no inline handlers
quarry served no page policy at all. The blocker was never the data-on:* attributes — script-src does not govern data attributes — it was unsafe-eval, because datastar compiled expressions with Function(). 1.0.3 takes a nonce instead.
Removed all 14 inline handlers: 11 identical onchange=this.form.submit() became data-on:change with requestSubmit (which validates, unlike submit), two selection handlers became expressions, and the copy-hash button moved to a delegated assets/copy.js. Scripts now carry content-hashed immutable URLs instead of revalidating daily.
The audit found the expressions already safe, for a reason worth pinning: html/template strips a data- prefix before classifying, so data-on:drop gets the JAVASCRIPT escaper. That matters because an HTML-escaped quote decodes back to a quote before any script reads the attribute, and a board column status comes from .quarry/tickets.json — upstream content on a mirror. (user: quarry, size: 2155) [annotate] [blame] [check-ins using]
// copy.js — the check-in page's "copy the full hash" button. // // This lived in an onclick attribute, which is inline script and the // one thing a strict Content-Security-Policy cannot allow. Delegated // from the document instead: one listener, no per-button wiring, and it // keeps working for buttons that arrive in a patched fragment. // // Selection first, on purpose. Selecting the text is the fallback that // always works — on a phone with no Clipboard API and no permission, // the hash ends up selected and ready for the native copy gesture. The // API is attempted after, and its failure changes nothing the reader // can see except the confirmation. (function () { "use strict"; var RESET_MS = 5000; function confirmCopied(button) { var use = button.firstChild && button.firstChild.firstChild; button.classList.add("done"); if (use) use.setAttribute("href", "#i-check"); button.setAttribute("aria-label", "full hash copied"); clearTimeout(button._resetTimer); button._resetTimer = setTimeout(function () { button.classList.remove("done"); if (use) use.setAttribute("href", "#i-copy"); button.setAttribute("aria-label", "copy the full hash"); }, RESET_MS); } document.addEventListener("click", function (ev) { var button = ev.target.closest && ev.target.closest(".ci-copy"); if (!button) return; var holder = button.parentNode && button.parentNode.nextElementSibling; var code = holder && holder.firstChild; if (!code) return; var selection = getSelection(); var range = document.createRange(); range.selectNodeContents(code); selection.removeAllRanges(); selection.addRange(range); var copied = false; try { copied = document.execCommand("copy"); } catch (e) { /* execCommand is gone or refused; the selection still stands. */ } if (navigator.clipboard) { navigator.clipboard.writeText(code.textContent).then( function () { confirmCopied(button); }, function () { if (copied) confirmCopied(button); } ); } else if (copied) { confirmCopied(button); } }); })();