Skip to content

Commit

Permalink
Require Node.js 14.16 (#44)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <[email protected]>
  • Loading branch information
Richienb and sindresorhus authored May 10, 2022
1 parent 62e78ab commit 2123d11
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 124 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ jobs:
fail-fast: false
matrix:
node-version:
- 14
- 12
- 18
- 16
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm install
Expand Down
35 changes: 12 additions & 23 deletions browser.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,23 @@
/* eslint-env browser */
import {createStringGenerator, createAsyncStringGenerator} from './core.js';

const toHex = uInt8Array => uInt8Array.map(byte => byte.toString(16).padStart(2, '0')).join('');

const decoder = new TextDecoder('utf8');
const toBase64 = uInt8Array => btoa(decoder.decode(uInt8Array));
const toHex = uInt8Array => [...uInt8Array].map(byte => byte.toString(16).padStart(2, '0')).join('');
const toBase64 = uInt8Array => btoa(String.fromCodePoint(...uInt8Array));

// `crypto.getRandomValues` throws an error if too much entropy is requested at once. (https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues#exceptions)
const maxEntropy = 65536;
const maxEntropy = 65_536;

function getRandomValues(byteLength) {
const generatedBytes = [];
const generatedBytes = new Uint8Array(byteLength);

while (byteLength > 0) {
const bytesToGenerate = Math.min(byteLength, maxEntropy);
generatedBytes.push(crypto.getRandomValues(new Uint8Array({length: bytesToGenerate})));
byteLength -= bytesToGenerate;
for (let totalGeneratedBytes = 0; totalGeneratedBytes < byteLength; totalGeneratedBytes += maxEntropy) {
generatedBytes.set(
crypto.getRandomValues(new Uint8Array(Math.min(maxEntropy, byteLength - totalGeneratedBytes))),
totalGeneratedBytes,
);
}

const result = new Uint8Array(generatedBytes.reduce((sum, {byteLength}) => sum + byteLength, 0)); // eslint-disable-line unicorn/no-array-reduce
let currentIndex = 0;

for (const bytes of generatedBytes) {
result.set(bytes, currentIndex);
currentIndex += bytes.byteLength;
}

return result;
return generatedBytes;
}

function specialRandomBytes(byteLength, type, length) {
Expand All @@ -36,7 +27,5 @@ function specialRandomBytes(byteLength, type, length) {
return convert(generatedBytes).slice(0, length);
}

const cryptoRandomString = createStringGenerator(specialRandomBytes, getRandomValues);
cryptoRandomString.async = createAsyncStringGenerator(specialRandomBytes, getRandomValues);

export default cryptoRandomString;
export default createStringGenerator(specialRandomBytes, getRandomValues);
export const cryptoRandomStringAsync = createAsyncStringGenerator(specialRandomBytes, getRandomValues);
26 changes: 14 additions & 12 deletions core.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
const urlSafeCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~'.split('');
const numericCharacters = '0123456789'.split('');
const distinguishableCharacters = 'CDEHKMPRTUWXY012458'.split('');
const asciiPrintableCharacters = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'.split('');
const alphanumericCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.split('');
const urlSafeCharacters = [...'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~'];
const numericCharacters = [...'0123456789'];
const distinguishableCharacters = [...'CDEHKMPRTUWXY012458'];
const asciiPrintableCharacters = [...'!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'];
const alphanumericCharacters = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'];

const readUInt16LE = (uInt8Array, offset) => uInt8Array[offset] + (uInt8Array[offset + 1] << 8); // eslint-disable-line no-bitwise

const generateForCustomCharacters = (length, characters, randomBytes) => {
// Generating entropy is faster than complex math operations, so we use the simplest way
const characterCount = characters.length;
const maxValidSelector = (Math.floor(0x10000 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division
const maxValidSelector = (Math.floor(0x1_00_00 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division
const entropyLength = 2 * Math.ceil(1.1 * length); // Generating a bit more than required so chances we need more than one pass will be really low
let string = '';
let stringLength = 0;
Expand All @@ -17,7 +19,7 @@ const generateForCustomCharacters = (length, characters, randomBytes) => {
let entropyPosition = 0;

while (entropyPosition < entropyLength && stringLength < length) {
const entropyValue = entropy.readUInt16LE(entropyPosition);
const entropyValue = readUInt16LE(entropy, entropyPosition);
entropyPosition += 2;
if (entropyValue > maxValidSelector) { // Skip values which will ruin distribution when using modular division
continue;
Expand All @@ -34,7 +36,7 @@ const generateForCustomCharacters = (length, characters, randomBytes) => {
const generateForCustomCharactersAsync = async (length, characters, randomBytesAsync) => {
// Generating entropy is faster than complex math operations, so we use the simplest way
const characterCount = characters.length;
const maxValidSelector = (Math.floor(0x10000 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division
const maxValidSelector = (Math.floor(0x1_00_00 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division
const entropyLength = 2 * Math.ceil(1.1 * length); // Generating a bit more than required so chances we need more than one pass will be really low
let string = '';
let stringLength = 0;
Expand All @@ -44,7 +46,7 @@ const generateForCustomCharactersAsync = async (length, characters, randomBytesA
let entropyPosition = 0;

while (entropyPosition < entropyLength && stringLength < length) {
const entropyValue = entropy.readUInt16LE(entropyPosition);
const entropyValue = readUInt16LE(entropy, entropyPosition);
entropyPosition += 2;
if (entropyValue > maxValidSelector) { // Skip values which will ruin distribution when using modular division
continue;
Expand All @@ -66,7 +68,7 @@ const allowedTypes = new Set([
'numeric',
'distinguishable',
'ascii-printable',
'alphanumeric'
'alphanumeric',
]);

const createGenerator = (generateForCustomCharacters, specialRandomBytes, randomBytes) => ({length, type, characters}) => {
Expand Down Expand Up @@ -122,11 +124,11 @@ const createGenerator = (generateForCustomCharacters, specialRandomBytes, random
throw new TypeError('Expected `characters` string length to be greater than or equal to 1');
}

if (characters.length > 0x10000) {
if (characters.length > 0x1_00_00) {
throw new TypeError('Expected `characters` string length to be less or equal to 65536');
}

return generateForCustomCharacters(length, characters.split(''), randomBytes);
return generateForCustomCharacters(length, characters, randomBytes);
};

export function createStringGenerator(specialRandomBytes, randomBytes) {
Expand Down
54 changes: 25 additions & 29 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,42 +68,38 @@ interface CharactersOption {

export type Options = BaseOptions & MergeExclusive<TypeOption, CharactersOption>;

declare const cryptoRandomString: {
/**
Generate a [cryptographically strong](https://en.wikipedia.org/wiki/Strong_cryptography) random string.
@returns A randomized string.
/**
Generate a [cryptographically strong](https://en.wikipedia.org/wiki/Strong_cryptography) random string.
@example
```
import cryptoRandomString from 'crypto-random-string';
@returns A randomized string.
cryptoRandomString({length: 10});
//=> '2cf05d94db'
```
*/
(options: Options): string;
@example
```
import cryptoRandomString from 'crypto-random-string';
/**
Asynchronously generate a [cryptographically strong](https://en.wikipedia.org/wiki/Strong_cryptography) random string.
cryptoRandomString({length: 10});
//=> '2cf05d94db'
```
*/
export default function cryptoRandomString(options: Options): string;

For most use-cases, there's really no good reason to use this async version. From the Node.js docs:
/**
Asynchronously generate a [cryptographically strong](https://en.wikipedia.org/wiki/Strong_cryptography) random string.
> The `crypto.randomBytes()` method will not complete until there is sufficient entropy available. This should normally never take longer than a few milliseconds. The only time when generating the random bytes may conceivably block for a longer period of time is right after boot, when the whole system is still low on entropy.
For most use-cases, there's really no good reason to use this async version. From the Node.js docs:
In general, anything async comes with some overhead on it's own.
> The `crypto.randomBytes()` method will not complete until there is sufficient entropy available. This should normally never take longer than a few milliseconds. The only time when generating the random bytes may conceivably block for a longer period of time is right after boot, when the whole system is still low on entropy.
@returns A promise which resolves to a randomized string.
In general, anything async comes with some overhead on it's own.
@example
```
import cryptoRandomString from 'crypto-random-string';
@returns A promise which resolves to a randomized string.
await cryptoRandomString.async({length: 10});
//=> '2cf05d94db'
```
*/
async(options: Options): Promise<string>;
};
@example
```
import {cryptoRandomStringAsync} from 'crypto-random-string';
export default cryptoRandomString;
await cryptoRandomStringAsync({length: 10});
//=> '2cf05d94db'
```
*/
export function cryptoRandomStringAsync(options: Options): Promise<string>;
14 changes: 7 additions & 7 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// TODO: When targeting Node.js 16, remove `cryptoRandomStringAsync` and use `crypto.webcrypto.getRandomValues` to interop with the browser code.
// TODO: Later, when targeting Node.js 18, only use the browser code
import {promisify} from 'node:util';
import crypto from 'node:crypto';
import {randomBytes} from 'node:crypto';
import {createStringGenerator, createAsyncStringGenerator} from './core.js';

const randomBytesAsync = promisify(crypto.randomBytes);
const randomBytesAsync = promisify(randomBytes);

const cryptoRandomString = createStringGenerator((byteLength, type, length) => crypto.randomBytes(byteLength).toString(type).slice(0, length), crypto.randomBytes);
cryptoRandomString.async = createAsyncStringGenerator(async (byteLength, type, length) => {
export default createStringGenerator((byteLength, type, length) => randomBytes(byteLength).toString(type).slice(0, length), size => new Uint8Array(randomBytes(size)));
export const cryptoRandomStringAsync = createAsyncStringGenerator(async (byteLength, type, length) => {
const buffer = await randomBytesAsync(byteLength);
return buffer.toString(type).slice(0, length);
}, randomBytesAsync);

export default cryptoRandomString;
}, async size => new Uint8Array(await randomBytesAsync(size)));
4 changes: 2 additions & 2 deletions index.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {expectType, expectError} from 'tsd';
import cryptoRandomString from './index.js';
import cryptoRandomString, {cryptoRandomStringAsync} from './index.js';

expectType<string>(cryptoRandomString({length: 10}));
expectType<string>(cryptoRandomString({length: 10, type: 'url-safe'}));
expectType<string>(cryptoRandomString({length: 10, type: 'numeric'}));
expectType<string>(cryptoRandomString({length: 10, characters: '1234'}));
expectType<Promise<string>>(cryptoRandomString.async({length: 10}));
expectType<Promise<string>>(cryptoRandomStringAsync({length: 10}));

expectError(cryptoRandomString({type: 'url-safe'}));
expectError(cryptoRandomString({length: 10, type: 'url-safe', characters: '1234'}));
Expand Down
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"browser": "./browser.js"
},
"engines": {
"node": ">=12"
"node": ">=14.16"
},
"scripts": {
"test": "xo && ava && tsd"
Expand Down Expand Up @@ -45,11 +45,12 @@
"protect"
],
"dependencies": {
"type-fest": "^1.0.1"
"type-fest": "^2.12.2"
},
"devDependencies": {
"ava": "^3.15.0",
"tsd": "^0.14.0",
"xo": "^0.38.2"
"ava": "^4.2.0",
"dot-prop": "^7.2.0",
"tsd": "^0.20.0",
"xo": "^0.48.0"
}
}
9 changes: 8 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ cryptoRandomString({length: 10, characters: 'abc'});

Returns a randomized string. [Hex](https://en.wikipedia.org/wiki/Hexadecimal) by default.

### cryptoRandomString.async(options)
### cryptoRandomStringAsync(options)

Returns a promise which resolves to a randomized string. [Hex](https://en.wikipedia.org/wiki/Hexadecimal) by default.

Expand All @@ -58,6 +58,13 @@ For most use-cases, there's really no good reason to use this async version. Fro
In general, anything async comes with some overhead on it's own.

```js
import {cryptoRandomStringAsync} from 'crypto-random-string';

await cryptoRandomStringAsync({length: 10});
//=> '2cf05d94db'
```

#### options

Type: `object`
Expand Down
Loading

0 comments on commit 2123d11

Please sign in to comment.