snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
This commit is contained in:
1
packages/random/.gitignore
vendored
Normal file
1
packages/random/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.build*
|
||||
25
packages/random/CHANGELOG.md
Normal file
25
packages/random/CHANGELOG.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# @rocket.chat/random
|
||||
|
||||
## 1.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#38989](https://github.com/RocketChat/Rocket.Chat/pull/38989)) chore(eslint): Upgrades ESLint and its configuration
|
||||
|
||||
## 1.2.3-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#38989](https://github.com/RocketChat/Rocket.Chat/pull/38989)) chore(eslint): Upgrades ESLint and its configuration
|
||||
|
||||
## 1.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#31138](https://github.com/RocketChat/Rocket.Chat/pull/31138)) feat(uikit): Move `@rocket.chat/ui-kit` package to the main monorepo
|
||||
|
||||
## 1.2.2-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b223cbde14: feat(uikit): Move `@rocket.chat/ui-kit` package to the main monorepo
|
||||
24
packages/random/README.md
Normal file
24
packages/random/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# random
|
||||
|
||||
The `random` package provides several functions for generating random
|
||||
numbers. It uses a cryptographically strong pseudorandom number generator when
|
||||
possible, but falls back to a weaker random number generator when
|
||||
cryptographically strong randomness is not available (on older browsers or on
|
||||
servers that don't have enough entropy to seed the cryptographically strong
|
||||
generator).
|
||||
|
||||
- `Random.id([n])` - Returns a unique identifier, such as `"Jjwjg6gouWLXhMGKW"`, that is
|
||||
likely to be unique in the whole world. The optional argument `n`
|
||||
specifies the length of the identifier in characters and defaults to 17.
|
||||
|
||||
- `Random.secret([n])` - Returns a random string of printable characters with 6 bits of
|
||||
entropy per character. The optional argument `n` specifies the length of
|
||||
the secret string and defaults to 43 characters, or 256 bits of
|
||||
entropy. Use `Random.secret` for security-critical secrets that are
|
||||
intended for machine, rather than human, consumption.
|
||||
|
||||
- `Random.fraction()` - Returns a number between 0 and 1, like `Math.random`.
|
||||
|
||||
- `Random.choice(arrayOrString)` - Returns a random element of the given array or string.
|
||||
|
||||
- `Random.hexString(n)` - Returns a random string of `n` hexadecimal digits.
|
||||
18
packages/random/jest.config.ts
Normal file
18
packages/random/jest.config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import client from '@rocket.chat/jest-presets/client';
|
||||
import server from '@rocket.chat/jest-presets/server';
|
||||
import type { Config } from 'jest';
|
||||
|
||||
export default {
|
||||
projects: [
|
||||
{
|
||||
displayName: 'client',
|
||||
preset: client.preset,
|
||||
testMatch: ['<rootDir>/src/**/*.client.spec.[jt]s?(x)'],
|
||||
},
|
||||
{
|
||||
displayName: 'server',
|
||||
preset: server.preset,
|
||||
testMatch: ['<rootDir>/src/**/*.server.spec.[jt]s?(x)'],
|
||||
},
|
||||
],
|
||||
} satisfies Config;
|
||||
28
packages/random/package.json
Normal file
28
packages/random/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@rocket.chat/random",
|
||||
"version": "1.2.3",
|
||||
"private": true,
|
||||
"description": "Random number generator and utilities; Fork of Meteor's Random package",
|
||||
"main": "./dist/main.server.js",
|
||||
"browser": "./dist/main.client.js",
|
||||
"types": "./dist/main.server.d.ts",
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
||||
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"test": "jest",
|
||||
"testunit": "jest",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rocket.chat/jest-presets": "workspace:~",
|
||||
"@rocket.chat/tsconfig": "workspace:*",
|
||||
"eslint": "~9.39.5",
|
||||
"jest": "~30.2.0",
|
||||
"typescript": "~5.9.3"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
105
packages/random/src/AleaRandomGenerator.ts
Normal file
105
packages/random/src/AleaRandomGenerator.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { RandomGenerator } from './RandomGenerator';
|
||||
|
||||
// Alea PRNG, which is not cryptographically strong
|
||||
// see http://baagoe.org/en/wiki/Better_random_numbers_for_javascript
|
||||
// for a full discussion and Alea implementation.
|
||||
function createAlea(seeds: readonly unknown[]) {
|
||||
function createMash() {
|
||||
let n = 0xefc8249d;
|
||||
|
||||
const mash = (data: unknown) => {
|
||||
data = String(data);
|
||||
if (typeof data !== 'string') {
|
||||
throw new Error('Expected a string');
|
||||
}
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
n += data.charCodeAt(i);
|
||||
let h = 0.02519603282416938 * n;
|
||||
n = h >>> 0;
|
||||
h -= n;
|
||||
h *= n;
|
||||
n = h >>> 0;
|
||||
h -= n;
|
||||
n += h * 0x100000000; // 2^32
|
||||
}
|
||||
return (n >>> 0) * 2.3283064365386963e-10; // 2^-32
|
||||
};
|
||||
|
||||
mash.version = 'Mash 0.9';
|
||||
return mash;
|
||||
}
|
||||
|
||||
let s0 = 0;
|
||||
let s1 = 0;
|
||||
let s2 = 0;
|
||||
let c = 1;
|
||||
if (seeds.length === 0) {
|
||||
seeds = [+new Date()];
|
||||
}
|
||||
const mash = createMash();
|
||||
s0 = mash(' ');
|
||||
s1 = mash(' ');
|
||||
s2 = mash(' ');
|
||||
|
||||
for (let i = 0; i < seeds.length; i++) {
|
||||
s0 -= mash(seeds[i]);
|
||||
if (s0 < 0) {
|
||||
s0 += 1;
|
||||
}
|
||||
s1 -= mash(seeds[i]);
|
||||
if (s1 < 0) {
|
||||
s1 += 1;
|
||||
}
|
||||
s2 -= mash(seeds[i]);
|
||||
if (s2 < 0) {
|
||||
s2 += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const random = () => {
|
||||
const t = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32
|
||||
s0 = s1;
|
||||
s1 = s2;
|
||||
s2 = t - (c = t | 0);
|
||||
return s2;
|
||||
};
|
||||
|
||||
random.uint32 = () => random() * 0x100000000; // 2^32
|
||||
random.fract53 = () => random() + ((random() * 0x200000) | 0) * 1.1102230246251565e-16; // 2^-53
|
||||
|
||||
random.version = 'Alea 0.9';
|
||||
random.args = seeds;
|
||||
return random;
|
||||
}
|
||||
|
||||
// options:
|
||||
// - seeds: an array
|
||||
// whose items will be `toString`ed and used as the seed to the Alea
|
||||
// algorithm
|
||||
export class AleaRandomGenerator extends RandomGenerator {
|
||||
private readonly alea: () => number;
|
||||
|
||||
constructor({ seeds = [] }: { seeds?: readonly unknown[] } = {}) {
|
||||
super();
|
||||
if (!seeds) {
|
||||
throw new Error('No seeds were provided for Alea PRNG');
|
||||
}
|
||||
this.alea = createAlea(seeds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @name Random.fraction
|
||||
* @summary Return a number between 0 and 1, like `Math.random`.
|
||||
* @locus Anywhere
|
||||
*/
|
||||
fraction() {
|
||||
return this.alea();
|
||||
}
|
||||
|
||||
protected safelyCreateWithSeeds(...seeds: readonly unknown[]): RandomGenerator {
|
||||
return new AleaRandomGenerator({ seeds });
|
||||
}
|
||||
|
||||
insecure: RandomGenerator = this;
|
||||
}
|
||||
23
packages/random/src/BrowserRandomGenerator.ts
Normal file
23
packages/random/src/BrowserRandomGenerator.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { AleaRandomGenerator } from './AleaRandomGenerator';
|
||||
import { RandomGenerator } from './RandomGenerator';
|
||||
import { createAleaGeneratorWithGeneratedSeed } from './createAleaGenerator';
|
||||
|
||||
// cryptographically strong PRNGs available in modern browsers
|
||||
export class BrowserRandomGenerator extends RandomGenerator {
|
||||
/**
|
||||
* @name Random.fraction
|
||||
* @summary Return a number between 0 and 1, like `Math.random`.
|
||||
* @locus Anywhere
|
||||
*/
|
||||
fraction() {
|
||||
const array = new Uint32Array(1);
|
||||
window.crypto.getRandomValues(array);
|
||||
return array[0] * 2.3283064365386963e-10; // 2^-32
|
||||
}
|
||||
|
||||
protected safelyCreateWithSeeds(...seeds: readonly unknown[]) {
|
||||
return new AleaRandomGenerator({ seeds });
|
||||
}
|
||||
|
||||
insecure: RandomGenerator = createAleaGeneratorWithGeneratedSeed();
|
||||
}
|
||||
76
packages/random/src/NodeRandomGenerator.ts
Normal file
76
packages/random/src/NodeRandomGenerator.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
import { AleaRandomGenerator } from './AleaRandomGenerator';
|
||||
import { RandomGenerator } from './RandomGenerator';
|
||||
import { createAleaGeneratorWithGeneratedSeed } from './createAleaGenerator';
|
||||
|
||||
export class NodeRandomGenerator extends RandomGenerator {
|
||||
/**
|
||||
* @name Random.fraction
|
||||
* @summary Return a number between 0 and 1, like `Math.random`.
|
||||
* @locus Anywhere
|
||||
*/
|
||||
fraction() {
|
||||
const numerator = Number.parseInt(this.hexString(8), 16);
|
||||
return numerator * 2.3283064365386963e-10; // 2^-3;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name Random.hexString
|
||||
* @summary Return a random string of `n` hexadecimal digits.
|
||||
* @locus Anywhere
|
||||
* @param digits Length of the string
|
||||
*/
|
||||
override hexString(digits: number) {
|
||||
const numBytes = Math.ceil(digits / 2);
|
||||
const bytes = this.randomBytes(numBytes);
|
||||
const result = bytes.toString('hex');
|
||||
// If the number of digits is odd, we'll have generated an extra 4 bits
|
||||
// of randomness, so we need to trim the last digit.
|
||||
return result.substring(0, digits);
|
||||
}
|
||||
|
||||
override _randomString(charsCount: number, alphabet: string) {
|
||||
// Generate all the randomness in a single call instead of one
|
||||
// crypto.randomBytes() call per character. Each character consumes 4
|
||||
// bytes read as a big-endian uint32, matching what fraction() would
|
||||
// have produced, so the output distribution is unchanged.
|
||||
// Normalized like the base-class loop: NaN/negative counts yield an
|
||||
// empty string, fractional counts round up.
|
||||
const count = Number.isNaN(charsCount) ? 0 : Math.max(0, Math.ceil(charsCount));
|
||||
const bytes = this.randomBytes(count * 4);
|
||||
let result = '';
|
||||
for (let i = 0; i < count; i++) {
|
||||
const fraction = bytes.readUInt32BE(i * 4) * 2.3283064365386963e-10; // 2^-32
|
||||
result += alphabet.charAt(Math.floor(fraction * alphabet.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private randomBytes(numBytes: number): Buffer {
|
||||
// Try to get cryptographically strong randomness. Fall back to
|
||||
// non-cryptographically strong if not available.
|
||||
try {
|
||||
return crypto.randomBytes(numBytes);
|
||||
} catch (e) {
|
||||
// XXX should re-throw any error except insufficient entropy
|
||||
return crypto.pseudoRandomBytes(numBytes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @name Random.between Returns a random integer between min and max, inclusive.
|
||||
* @param min Minimum value (inclusive)
|
||||
* @param max Maximum value (inclusive)
|
||||
* @returns A random integer between min and max, inclusive.
|
||||
*/
|
||||
between(min: number, max: number) {
|
||||
return Math.floor(this.fraction() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
protected safelyCreateWithSeeds(...seeds: readonly unknown[]) {
|
||||
return new AleaRandomGenerator({ seeds });
|
||||
}
|
||||
|
||||
insecure: RandomGenerator = createAleaGeneratorWithGeneratedSeed();
|
||||
}
|
||||
107
packages/random/src/RandomGenerator.ts
Normal file
107
packages/random/src/RandomGenerator.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
// We use cryptographically strong PRNGs (crypto.getRandomBytes() on the server,
|
||||
// window.crypto.getRandomValues() in the browser) when available. If these
|
||||
// PRNGs fail, we fall back to the Alea PRNG, which is not cryptographically
|
||||
// strong, and we seed it with various sources such as the date, Math.random,
|
||||
// and window size on the client. When using crypto.getRandomValues(), our
|
||||
// primitive is hexString(), from which we construct fraction(). When using
|
||||
// window.crypto.getRandomValues() or alea, the primitive is fraction and we use
|
||||
// that to construct hex string.
|
||||
|
||||
const UNMISTAKABLE_CHARS = '23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz';
|
||||
const BASE64_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_';
|
||||
|
||||
// `type` is one of `RandomGenerator.Type` as defined below.
|
||||
//
|
||||
// options:
|
||||
// - seeds: (required, only for RandomGenerator.Type.ALEA) an array
|
||||
// whose items will be `toString`ed and used as the seed to the Alea
|
||||
// algorithm
|
||||
export abstract class RandomGenerator {
|
||||
/**
|
||||
* @name Random.fraction
|
||||
* @summary Return a number between 0 and 1, like `Math.random`.
|
||||
* @locus Anywhere
|
||||
*/
|
||||
abstract fraction(): number;
|
||||
|
||||
/**
|
||||
* Create a non-cryptographically secure PRNG with a given seed (using
|
||||
* the Alea algorithm)
|
||||
*/
|
||||
createWithSeeds(...seeds: readonly unknown[]): RandomGenerator {
|
||||
if (seeds.length === 0) {
|
||||
throw new Error('No seeds were provided');
|
||||
}
|
||||
|
||||
return this.safelyCreateWithSeeds(...seeds);
|
||||
}
|
||||
|
||||
protected abstract safelyCreateWithSeeds(...seeds: readonly unknown[]): RandomGenerator;
|
||||
|
||||
/**
|
||||
* Used like `Random`, but much faster and not cryptographically secure
|
||||
*/
|
||||
abstract insecure: RandomGenerator;
|
||||
|
||||
/**
|
||||
* @name Random.hexString
|
||||
* @summary Return a random string of `n` hexadecimal digits.
|
||||
* @locus Anywhere
|
||||
* @param digits Length of the string
|
||||
*/
|
||||
hexString(digits: number) {
|
||||
return this._randomString(digits, '0123456789abcdef');
|
||||
}
|
||||
|
||||
_randomString(charsCount: number, alphabet: string) {
|
||||
let result = '';
|
||||
for (let i = 0; i < charsCount; i++) {
|
||||
result += this.choice(alphabet);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name Random.id
|
||||
* @summary Return a unique identifier, such as `"Jjwjg6gouWLXhMGKW"`, that is
|
||||
* likely to be unique in the whole world.
|
||||
* @locus Anywhere
|
||||
* @param charsCount Optional length of the identifier in characters
|
||||
* (defaults to 17)
|
||||
*/
|
||||
id(charsCount = 17) {
|
||||
// 17 characters is around 96 bits of entropy, which is the amount of state in the Alea PRNG.
|
||||
|
||||
return this._randomString(charsCount, UNMISTAKABLE_CHARS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @name Random.secret
|
||||
* @summary Return a random string of printable characters with 6 bits of
|
||||
* entropy per character. Use `Random.secret` for security-critical secrets
|
||||
* that are intended for machine, rather than human, consumption.
|
||||
* @locus Anywhere
|
||||
* @param charsCount Optional length of the secret string (defaults to 43
|
||||
* characters, or 256 bits of entropy)
|
||||
*/
|
||||
secret(charsCount = 43) {
|
||||
// Default to 256 bits of entropy, or 43 characters at 6 bits per character.
|
||||
|
||||
return this._randomString(charsCount, BASE64_CHARS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @name Random.choice
|
||||
* @summary Return a random element of the given array or string.
|
||||
* @locus Anywhere
|
||||
* @param {Array|String} arrayOrString Array or string to choose from
|
||||
*/
|
||||
choice<TArrayLike extends unknown[] | string>(arrayOrString: TArrayLike) {
|
||||
const index = Math.floor(this.fraction() * arrayOrString.length);
|
||||
if (typeof arrayOrString === 'string') {
|
||||
return arrayOrString.substr(index, 1);
|
||||
}
|
||||
|
||||
return arrayOrString[index];
|
||||
}
|
||||
}
|
||||
25
packages/random/src/createAleaGenerator.ts
Normal file
25
packages/random/src/createAleaGenerator.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { AleaRandomGenerator } from './AleaRandomGenerator';
|
||||
|
||||
// instantiate RNG. Heuristically collect entropy from various sources when a
|
||||
// cryptographic PRNG isn't available.
|
||||
|
||||
// client sources
|
||||
const height =
|
||||
(typeof window !== 'undefined' && window.innerHeight) ||
|
||||
(typeof document !== 'undefined' && document.documentElement && document.documentElement.clientHeight) ||
|
||||
(typeof document !== 'undefined' && document.body && document.body.clientHeight) ||
|
||||
1;
|
||||
|
||||
const width =
|
||||
(typeof window !== 'undefined' && window.innerWidth) ||
|
||||
(typeof document !== 'undefined' && document.documentElement && document.documentElement.clientWidth) ||
|
||||
(typeof document !== 'undefined' && document.body && document.body.clientWidth) ||
|
||||
1;
|
||||
|
||||
const agent = (typeof navigator !== 'undefined' && navigator.userAgent) || '';
|
||||
|
||||
export function createAleaGeneratorWithGeneratedSeed() {
|
||||
return new AleaRandomGenerator({
|
||||
seeds: [new Date(), height, width, agent, Math.random()],
|
||||
});
|
||||
}
|
||||
52
packages/random/src/main.client.spec.ts
Normal file
52
packages/random/src/main.client.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Random } from './main.client';
|
||||
|
||||
it('which should generate the same sequence in all environments', () => {
|
||||
const random = Random.createWithSeeds(0);
|
||||
|
||||
expect(random.id()).toBe('cp9hWvhg8GSvuZ9os');
|
||||
expect(random.id()).toBe('3f3k6Xo7rrHCifQhR');
|
||||
expect(random.id()).toBe('shxDnjWWmnKPEoLhM');
|
||||
expect(random.id()).toBe('6QTjB8C5SEqhmz4ni');
|
||||
});
|
||||
|
||||
describe('format', () => {
|
||||
it('should output id in the right format', () => {
|
||||
expect(Random.id(17)).toHaveLength(17);
|
||||
expect(Random.id(29)).toHaveLength(29);
|
||||
});
|
||||
|
||||
it('should output hexString in the right format', () => {
|
||||
const hexString = Random.hexString(9);
|
||||
expect(hexString).toHaveLength(9);
|
||||
expect(Number.parseInt(hexString, 16)).not.toBeNaN();
|
||||
});
|
||||
|
||||
it('should output fraction in the right range', () => {
|
||||
const frac = Random.fraction();
|
||||
expect(frac).toBeLessThan(1.0);
|
||||
expect(frac).toBeGreaterThanOrEqual(0.0);
|
||||
});
|
||||
|
||||
it('should output secret in the right format', () => {
|
||||
expect(Random.secret().length).toBe(43);
|
||||
expect(Random.secret(13).length).toBe(13);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Alea', () => {
|
||||
it('should be undefined if window.crypto?.getRandomValues is available', () => {
|
||||
const useGetRandomValues = !!(typeof window !== 'undefined' && window.crypto?.getRandomValues);
|
||||
|
||||
if (useGetRandomValues) {
|
||||
expect(Random).not.toHaveProperty('alea');
|
||||
} else {
|
||||
expect(Random).toHaveProperty('alea');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('createWithSeeds', () => {
|
||||
it('should throw if no seeds are provided', () => {
|
||||
expect(() => Random.createWithSeeds()).toThrow();
|
||||
});
|
||||
});
|
||||
19
packages/random/src/main.client.ts
Normal file
19
packages/random/src/main.client.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// We use cryptographically strong PRNGs (window.crypto.getRandomValues())
|
||||
// when available. If these PRNGs fail, we fall back to the Alea PRNG, which is
|
||||
// not cryptographically strong, and we seed it with various sources
|
||||
// such as the date, Math.random, and window size on the client.
|
||||
// When using window.crypto.getRandomValues() or alea, the primitive is fraction
|
||||
// and we use that to construct hex string.
|
||||
|
||||
import { BrowserRandomGenerator } from './BrowserRandomGenerator';
|
||||
import type { RandomGenerator } from './RandomGenerator';
|
||||
import { createAleaGeneratorWithGeneratedSeed } from './createAleaGenerator';
|
||||
|
||||
let generator: RandomGenerator;
|
||||
if (typeof window !== 'undefined' && !!window.crypto?.getRandomValues) {
|
||||
generator = new BrowserRandomGenerator();
|
||||
} else {
|
||||
generator = createAleaGeneratorWithGeneratedSeed();
|
||||
}
|
||||
|
||||
export const Random = generator;
|
||||
46
packages/random/src/main.server.spec.ts
Normal file
46
packages/random/src/main.server.spec.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Random } from './main.server';
|
||||
|
||||
it('which should generate the same sequence in all environments', () => {
|
||||
const random = Random.createWithSeeds(0);
|
||||
|
||||
expect(random.id()).toBe('cp9hWvhg8GSvuZ9os');
|
||||
expect(random.id()).toBe('3f3k6Xo7rrHCifQhR');
|
||||
expect(random.id()).toBe('shxDnjWWmnKPEoLhM');
|
||||
expect(random.id()).toBe('6QTjB8C5SEqhmz4ni');
|
||||
});
|
||||
|
||||
describe('format', () => {
|
||||
it('should output id in the right format', () => {
|
||||
expect(Random.id(17)).toHaveLength(17);
|
||||
expect(Random.id(29)).toHaveLength(29);
|
||||
});
|
||||
|
||||
it('should output hexString in the right format', () => {
|
||||
const hexString = Random.hexString(9);
|
||||
expect(hexString).toHaveLength(9);
|
||||
expect(Number.parseInt(hexString, 16)).not.toBeNaN();
|
||||
});
|
||||
|
||||
it('should output fraction in the right range', () => {
|
||||
const frac = Random.fraction();
|
||||
expect(frac).toBeLessThan(1.0);
|
||||
expect(frac).toBeGreaterThanOrEqual(0.0);
|
||||
});
|
||||
|
||||
it('should output secret in the right format', () => {
|
||||
expect(Random.secret().length).toBe(43);
|
||||
expect(Random.secret(13).length).toBe(13);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Alea', () => {
|
||||
it('should be undefined', () => {
|
||||
expect(Random).not.toHaveProperty('alea');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createWithSeeds', () => {
|
||||
it('should throw if no seeds are provided', () => {
|
||||
expect(() => Random.createWithSeeds()).toThrow();
|
||||
});
|
||||
});
|
||||
7
packages/random/src/main.server.ts
Normal file
7
packages/random/src/main.server.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// We use cryptographically strong PRNGs (crypto.getRandomBytes())
|
||||
// When using crypto.getRandomValues(), our primitive is hexString(),
|
||||
// from which we construct fraction().
|
||||
|
||||
import { NodeRandomGenerator } from './NodeRandomGenerator';
|
||||
|
||||
export const Random = new NodeRandomGenerator();
|
||||
12
packages/random/tsconfig.build.json
Normal file
12
packages/random/tsconfig.build.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"target": "ES2015",
|
||||
"module": "CommonJS",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
},
|
||||
"exclude": ["**/*.spec.ts"]
|
||||
}
|
||||
7
packages/random/tsconfig.json
Normal file
7
packages/random/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "@rocket.chat/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user