Get unstuck

Hint 1: Start with the two sources of truth

caller.tenant and caller.id are trusted session data. invoice.tenant and invoice.owner are stored record data. Do not use requestedOwner from the request.

Hint 2: Write the two comparisons

Compare caller.tenant === invoice.tenant and caller.id === invoice.owner. Both must be true. Check for null before reading either object.

Reveal the reference patch and explanation
Fixedpolicy.mjsJavaScript
export function canReadInvoice(caller, invoice) {
  if (!caller || !invoice) return false;
  return caller.tenant === invoice.tenant
    && caller.id === invoice.owner;
}

For Alice/North reading invoice1, both comparisons are true and the adapter returns 200 with amount 120. Carol/North fails ownership; Alice/South fails tenant scope even though the id matches. Missing caller or invoice returns false. The export invokes the same predicate for each row, so its partial-result contract returns only invoice1. This differs from an atomic export contract that would reject a mixed-owner request entirely; choose that behavior explicitly in a real feature.

Compare it with your own fix, then rerun the tests. The editor is not changed when you open this explanation.

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.mjs
How 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

Use the lesson in your own system

This workshop models the permission predicate. Production enforcement belongs on a trusted server and must cover each operation, role, and route. The 403 response in the test adapter is illustrative; some systems deliberately conceal existence with 404.

Your next task: Find one alternate route to the same data—an export, background job, or update endpoint. Where will it call this policy, and which denied case will you add to its tests?

OWASP authorization guidance

Related guide: Enforce authorization for every object action