diff --git a/packages/cli-kit/src/public/common/array.test.ts b/packages/cli-kit/src/public/common/array.test.ts index 720becbac0b..02e8e906f58 100644 --- a/packages/cli-kit/src/public/common/array.test.ts +++ b/packages/cli-kit/src/public/common/array.test.ts @@ -1,6 +1,30 @@ -import {difference, uniq, uniqBy} from './array.js' +import {difference, takeRandomFromArray, uniq, uniqBy} from './array.js' import {describe, test, expect} from 'vitest' +describe('takeRandomFromArray', () => { + test('returns an element from the array', () => { + // Given + const array = [1, 2, 3, 4, 5] + + // When + const got = takeRandomFromArray(array) + + // Then + expect(array).toContain(got) + }) + + test('handles arrays of various sizes', () => { + // Given + const arrays = [[1], [1, 2], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]] + + // When/Then + arrays.forEach((array) => { + const got = takeRandomFromArray(array) + expect(array).toContain(got) + }) + }) +}) + describe('uniqBy', () => { test('removes duplicates', () => { // When diff --git a/packages/cli-kit/src/public/common/array.ts b/packages/cli-kit/src/public/common/array.ts index 8b22c0b7b93..18e2da7b960 100644 --- a/packages/cli-kit/src/public/common/array.ts +++ b/packages/cli-kit/src/public/common/array.ts @@ -9,7 +9,22 @@ import type {List, ValueIteratee} from 'lodash' * @returns A random element from the array. */ export function takeRandomFromArray(array: T[]): T { - return array[Math.floor(Math.random() * array.length)]! + if (array.length === 0) return undefined as T + + // We use a cryptographically secure PRNG to ensure the selection is non-predictable. + // We use rejection sampling to eliminate modulo bias. + const arrayLength = array.length + const range = 4294967296 + const limit = range - (range % arrayLength) + + const buffer = new Uint32Array(1) + let randomIndex + do { + globalThis.crypto.getRandomValues(buffer) + randomIndex = buffer[0]! + } while (randomIndex >= limit) + + return array[randomIndex % arrayLength]! } /**