Get unstuck

Hint 1: Unpredictable needs a CSPRNG

Math.random is fast and statistically fine for games, but it is not built to resist someone who sees its output. Every character must come from randomBytes.

Hint 2: Remainders are only fair on whole multiples

byte % 62 is uniform only when the byte is below 248. Above that, the values 0 to 7 get one extra chance. Discard those bytes and draw more instead of rescaling.

Reveal the reference patch and explanation
Fixedapi-keys.mjsJavaScript
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const LENGTH = 32; // 32 characters of 62: about 190 bits
const LIMIT = 248; // 4 × 62, the largest multiple of 62 below 256

export function newApiKey(randomBytes) {
  let key = '';
  while (key.length < LENGTH) {
    for (const byte of randomBytes(LENGTH)) {
      // Bytes 248 to 255 would make A to H more likely than the rest: discard them.
      if (byte < LIMIT && key.length < LENGTH) key += ALPHABET[byte % ALPHABET.length];
    }
  }
  return 'ak_' + key;
}

Bytes 0, 1, 2, … 31 give ak_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef. Byte 247 gives "9" (247 % 62 = 61), and 248 to 255 are skipped wherever they appear, including forty of them in a row at the start, which needs a second call to randomBytes. Two keys in a row, and twenty in a row, each come only from the bytes returned during their own call, so they differ whenever the generator’s output differs. Math.random, a byte scaled with Math.floor(byte / 256 * 62), byte % 62 without the discard, a cutoff of 249 and a key that stops after one call all fail a case.

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

randomBytes stands in for the operating system’s generator; in your code, call crypto.getRandomValues or crypto.randomBytes directly, and prefer encoding raw bytes (base64url of 32 bytes) when you do not need a particular alphabet. The lab does not cover storing the key: keep only a hash of it, compare in constant time, scope it, and let users revoke and rotate it. A key prefix helps secret scanners find leaked keys but adds no entropy.

Your next task: Search your code for Math.random, random.random, java.util.Random and math/rand near words such as token, key, code, nonce or password. Which of them produce values that someone must not be able to guess?

MDN: Math.random() (security note)

Related guide: Choose cryptographic algorithms and parameters by job