Secure Token Generator — TypeScript source
Generate cryptographically-secure random tokens in your browser. Pick the entropy size and format - hex, base32, base64, base62, or alphanumeric - and see the real strength in bits. Runs entirely client-side.
This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Secure token generation - pure, deterministic logic with an injectable RNG so
// generation is unit-testable without touching Web Crypto. The interactive
// island passes the default CSPRNG; tests pass a seeded generator for exact,
// reproducible outputs. No React, no DOM.
/** Output character sets. `custom` reads its symbols from `opts.customAlphabet`. */
export type Alphabet =
| 'hex'
| 'hex-upper'
| 'base32'
| 'base32-crockford'
| 'base64'
| 'base64url'
| 'base62'
| 'alphanumeric'
| 'custom';
/** Convenience aliases that map 1:1 onto an `Alphabet` (the common encodings). */
export type Encoding =
| 'hex'
| 'base32'
| 'base64'
| 'base64url'
| 'base62'
| 'alphanumeric';
/** The fixed alphabet strings. `custom` is intentionally absent - it is caller-supplied. */
export const ALPHABETS: Record<Exclude<Alphabet, 'custom'>, string> = {
hex: '0123456789abcdef',
'hex-upper': '0123456789ABCDEF',
base32: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', // RFC 4648
'base32-crockford': '0123456789ABCDEFGHJKMNPQRSTVWXYZ', // Crockford (no I/L/O/U)
base64: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
base64url: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',
base62: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
alphanumeric: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
};
const ENCODING_TO_ALPHABET: Record<Encoding, Alphabet> = {
hex: 'hex',
base32: 'base32',
base64: 'base64',
base64url: 'base64url',
base62: 'base62',
alphanumeric: 'alphanumeric',
};
export interface GenerateOptions {
/** Output character set. Wins over `encoding` when both are given. */
alphabet?: Alphabet;
/** Used only when `alphabet === 'custom'`. Ignored otherwise. */
customAlphabet?: string;
/** Uniform float generator in [0, 1). Defaults to a crypto-backed CSPRNG. */
rng?: () => number;
}
/** Default CSPRNG-backed [0, 1) float, used when no `rng` is injected. */
const defaultRng = (): number => {
const buf = new Uint32Array(1);
crypto.getRandomValues(buf);
return buf[0] / 0x100000000;
};
/**
* Resolve the effective alphabet string from options and an optional encoding.
* Precedence: explicit `alphabet` → `encoding` → `hex`. A `custom` alphabet
* with no/empty `customAlphabet` resolves to '' (an invalid, empty set).
*/
export function resolveAlphabet(opts?: GenerateOptions, encoding?: Encoding): string {
if (opts?.alphabet === 'custom') return opts.customAlphabet ?? '';
if (opts?.alphabet) return ALPHABETS[opts.alphabet];
if (encoding) return ALPHABETS[ENCODING_TO_ALPHABET[encoding]];
return ALPHABETS.hex;
}
/**
* Build an `n`-character string from `alphabet`, selecting each symbol without
* modulo bias via rejection sampling: a 32-bit draw is rejected if it falls in
* the uneven remainder, so every symbol stays equally likely - important for
* non-power-of-two alphabets like base62. The rejection cap (`guard`) keeps a
* pathological `rng` from looping forever.
*/
export function constantTimeSelect(
alphabet: string,
n: number,
rng: () => number = defaultRng,
): string {
const size = alphabet.length;
if (size < 1 || !Number.isFinite(n) || n < 1) return '';
const limit = Math.floor(0xffffffff / size) * size; // largest multiple of size ≤ 2^32-1
let out = '';
for (let i = 0; i < n; i++) {
let x = rng() * 0x100000000; // [0, 2^32)
let guard = 0;
while (x >= limit && guard < 64) {
x = rng() * 0x100000000;
guard++;
}
out += alphabet[Math.floor(x) % size];
}
return out;
}
/** Output characters needed to carry `bytes` bytes of entropy in a `size`-symbol alphabet. */
export function outputLength(bytes: number, alphabetSize: number): number {
if (!Number.isFinite(bytes) || bytes < 1 || alphabetSize < 2) return 0;
return Math.ceil((bytes * 8) / Math.log2(alphabetSize));
}
/**
* Generate a token with `bytes` bytes of underlying entropy, rendered through
* `opts.alphabet` (or `encoding`). Each character is sampled uniformly without
* modulo bias, so the output is unbiased even for base62/alphanumeric. Returns
* '' for invalid input (non-positive/non-finite bytes, empty alphabet).
*
* Example: `generateToken(16, { alphabet: 'hex' })` → 32 hex chars (128 bits).
*/
export function generateToken(
bytes: number,
opts?: GenerateOptions,
encoding?: Encoding,
): string {
if (!Number.isFinite(bytes) || bytes < 1) return '';
const alphabet = resolveAlphabet(opts, encoding);
if (alphabet.length < 2) return '';
const n = outputLength(bytes, alphabet.length);
const rng = opts?.rng ?? defaultRng;
return constantTimeSelect(alphabet, n, rng);
}
/**
* Entropy (in bits) of a token of `bytes` entropy in a `size`-symbol alphabet.
* Equals `outputLength * log2(size)`, which is ≥ `bytes * 8` because the char
* count is rounded up.
*/
export function estimateEntropy(bytes: number, alphabetSize: number): number {
if (!Number.isFinite(bytes) || bytes < 1 || alphabetSize < 2) return 0;
return outputLength(bytes, alphabetSize) * Math.log2(alphabetSize);
}
export type StrengthLabel = 'weak' | 'fair' | 'strong' | 'very strong';
/**
* Bucket an entropy estimate (bits) into a human strength label.
* Tiers: weak <64 · fair 64-127 · strong 128-255 · very strong ≥256.
*/
export function strengthLabel(entropyBits: number): StrengthLabel {
if (!Number.isFinite(entropyBits) || entropyBits < 64) return 'weak';
if (entropyBits < 128) return 'fair';
if (entropyBits < 256) return 'strong';
return 'very strong';
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →