Hint 1: A signature authenticates bytes, not JSON meaning
JSON.stringify(JSON.parse(rawBody)) can remove whitespace or change representation. Pass rawBody unchanged to verify.
Hint 1 unlocks after your first failed run.
Hint 2: A valid tag is not proof of a new event
Replace the unconditional record call with the supplied atomic applyOnce operation. Return its result so the tests can distinguish processing from a skipped duplicate.
Hint 2 unlocks after 2 failed runs.
Reveal the reference patch and explanation
export async function handleDelivery(delivery, services) {
const {verify, validateEvent, ledger} = services;
if (!await verify(delivery.rawBody, delivery.tag)) {
return 'rejected';
}
let event;
try {
event = validateEvent(JSON.parse(delivery.rawBody));
} catch {
return 'rejected';
}
return await ledger.applyOnce(event.id, event);
}The formatted body has a valid tag for its whitespace, so verifying its original text succeeds while re-serialization fails. Two copies of evt_042 return processed then duplicate and produce one payment. Changing the unsigned delivery header still produces duplicate. The signed evt_043 is genuinely new and creates a second payment. A signed wrong account or unexpected event type passes cryptographic verification but fails validation and produces no payment.
Compare it with your own fix, then rerun the tests. The editor is not changed when you open this explanation.
The reference patch unlocks after you run code you have edited.
Inspect the actual test harness
The browser and the lab download run the same cases, including cases that catch common wrong fixes, such as comparing with one fixture value instead of the real rule. The download includes the complete test adapter and cryptographic fixtures.
Download cases.mjsHow the browser sandbox runs your code
Your code runs in QuickJS 2025-09-13 (quickjs-emscripten 0.32.0) inside a Web Worker, isolated from the page, with a 32 MiB memory limit and a 2.5-second time limit. The test cases and the pass or fail verdict run outside that sandbox, so changing built-ins such as Array.prototype.push cannot change the result. The lab download runs the same cases under Node.js, where your code runs directly.
The same as in current Node.js:
- ES2025 syntax and built-ins, including Set methods such as intersection, iterator helpers, Promise.try, RegExp.escape and the RegExp v flag
Added to the sandbox so that it behaves like Node.js:
- structuredClone, Array.fromAsync, queueMicrotask, atob and btoa
- URL and URLSearchParams, parsed by your browser’s own URL implementation
- TextEncoder and TextDecoder for UTF-8
- Intl.Collator and a locale-aware String.prototype.localeCompare
- console.log and related methods, whose output appears with your results
Not available in the browser sandbox:
- Timers (setTimeout, setInterval), fetch and other network or storage APIs; the exercises are deterministic and offline
- Node.js modules and globals: import, require, process and Buffer
- Web Crypto (crypto.getRandomValues, crypto.subtle); each workshop supplies the cryptographic operations it needs
- Intl other than Collator: toLocaleString and toLocaleDateString ignore the locale, so numbers have no digit grouping and dates use one fixed format in the device’s time zone
- using declarations, Symbol.dispose and DisposableStack (explicit resource management)
- Uint8Array.fromBase64 and toBase64, JSON.rawJSON, Atomics and Error.captureStackTrace
This exercise signs only the body. A production integration must follow its provider’s exact envelope, signed timestamp and freshness rules, size limits, key rotation, and account authorization. applyOnce is a supplied model; its durable transaction and recovery design must be implemented in the real service.
Your next task: Trace a real handler from raw input to side effect. Which values are authenticated, how are retries identified, and can two simultaneous deliveries both claim the same event?