cargo test and the consumer test both run in node, which is neither a browser nor a service worker. Two probes cover the rest. The automated one is a module Worker served under the exact extension_pages CSP from both manifests. A module Worker has no window and no document, which is the property that matters — an MV3 service worker has neither either. On Firefox: hasDOM: false hasWindow: false initWasm: ok (9.0 ms cold) keygen: 1.0 ms account: matches quantus-cli sign: 3.0 ms (4627 bytes) verify: 1.0 ms ok ctx sep: ok (rejected under spec-147 ctx) 9 ms to base64-decode 146 KB, inflate it to 234 KB and instantiate. That is the number the MV3 lifetime question turns on — a worker killed between messages pays it on every wake — and it settles the cold-start concern raised in quantus/wasm#1. Those are ML-DSA-87 timings, the larger parameter set, so 65 is cheaper still. The CSP is enforced, not merely declared: an earlier version of the page used an inline script and Firefox blocked it, which is why main.js is a separate file. The manual one is a real MV3 extension whose service worker imports the package at module scope. Loading an unpacked extension needs an OS file dialog, so it cannot be driven from here and is documented for a human to load. It covers chrome.runtime messaging and the real kill-and-restart lifecycle rather than a stand-in for it. Refs quantus/wasm#1 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
Browser probes
cargo test and the consumer test both run in node, and node is neither a
browser nor a service worker. These two probes cover what node cannot: does the
WASM instantiate with no DOM, under the extension's own CSP, and what does
a cold start cost.
Build first with ./scripts/build-quantus.sh, stage vendor/ (below), then run
node serve.mjs from this directory.
index.html + worker.js — automated
A module Worker, served with the exact extension_pages CSP from both extension
manifests:
script-src 'self' 'wasm-unsafe-eval'; object-src 'self'
A module Worker has no window and no document, which is the property that
matters — an MV3 service worker has neither either. Result on Firefox:
hasDOM: false hasWindow: false
initWasm: ok (9.0 ms cold)
keygen: 1.0 ms
account: matches quantus-cli
sign: 3.0 ms (4627 bytes)
verify: 1.0 ms ok
ctx sep: ok (rejected under spec-147 ctx)
9 ms to base64-decode 146 KB, zlib-inflate it to 234 KB and instantiate. That is the number the MV3 lifetime question turns on: a service worker killed between messages pays this on every wake, and 9 ms is not a problem. Those are ML-DSA-87 timings — the larger parameter set — so ML-DSA-65 is cheaper still.
The CSP is genuinely enforced here, not merely declared: an earlier version of
this page used an inline <script> and Firefox blocked it, which is why
main.js exists as a separate file.
manifest.json + sw.js — manual
The real thing: an MV3 extension whose background service worker imports the
package at module scope and signs once. Loading an unpacked extension needs an OS
file dialog, so this cannot be driven from a script — load it by hand via
chrome://extensions → Developer mode → Load unpacked, then click the toolbar
icon.
It covers what the Worker probe cannot: chrome.runtime messaging, and the
actual MV3 kill-and-restart lifecycle rather than a stand-in for it.
Staging vendor/
Both probes import from ./vendor/, which is not checked in. Populate it from a
build:
mkdir -p vendor
cp -r ../../build vendor/quantus-crypto
cp -r ../../../wasm-util/build vendor/wasm-util
sed -i "s|from '@polkadot/wasm-util/base64'|from '../wasm-util/base64.js'|; \
s|from '@polkadot/wasm-util/fflate'|from '../wasm-util/fflate.js'|" \
vendor/quantus-crypto/init.js
The rewrite is needed because a browser cannot resolve bare specifiers. A real
extension build does this with a bundler; here it is one sed rather than a
build step, because the probe exists to test the WASM, not the bundler.