// Uniform random integers from the platform CSPRNG. // The only randomness source is crypto.getRandomValues(); there is deliberately no fallback. const RANGE = 2 ** 32; // getRandomValues() accepts at most 65,536 bytes per call. const MAX_DRAWS_PER_CALL = 65_536 / 4; function getRandomValues(): (array: T) => T { const source = globalThis.crypto; if (typeof source?.getRandomValues !== 'function') { throw new Error('Secure random numbers are unavailable: crypto.getRandomValues() is missing.'); } return (array) => source.getRandomValues(array); } /** * Returns `count` independent integers in [0, maxExclusive), every value exactly equally likely. * * Rejection sampling: 32-bit draws at or above the largest multiple of maxExclusive are * discarded and redrawn, so `value % maxExclusive` carries no modulo bias. Values are drawn * in batches (one CSPRNG call per batch rather than per value) for speed. */ export function randomInts(maxExclusive: number, count: number): number[] { if (!Number.isInteger(maxExclusive) || maxExclusive < 1 || maxExclusive > RANGE) { throw new RangeError(`maxExclusive must be an integer in [1, 2^32], got ${maxExclusive}`); } if (!Number.isInteger(count) || count < 0) { throw new RangeError(`count must be a non-negative integer, got ${count}`); } const fill = getRandomValues(); const limit = RANGE - (RANGE % maxExclusive); const buffer = new Uint32Array(Math.min(count, MAX_DRAWS_PER_CALL)); const result: number[] = []; while (result.length < count) { const draws = buffer.subarray(0, Math.min(count - result.length, MAX_DRAWS_PER_CALL)); fill(draws); for (const value of draws) if (value < limit) result.push(value % maxExclusive); } return result; } /** One uniform integer in [0, maxExclusive). */ export function randomInt(maxExclusive: number): number { return randomInts(maxExclusive, 1)[0]; } /** Picks one element uniformly at random. */ export function randomItem(items: ArrayLike): T { return items[randomInt(items.length)]; } /** * `length` bytes straight from the CSPRNG, every byte value equally likely. For keys and * secrets, which are random bytes first and only then encoded as text (see encoding.ts). */ export function randomBytes(length: number): Uint8Array { if (!Number.isInteger(length) || length < 0) { throw new RangeError(`length must be a non-negative integer, got ${length}`); } const fill = getRandomValues(); const bytes = new Uint8Array(length); for (let offset = 0; offset < length; offset += 65_536) { fill(bytes.subarray(offset, Math.min(length, offset + 65_536))); } return bytes; }