UNPKG

crypto-id

Version:

Create a random id of any length with characters 0-9,A-Z,a-z using the browser's crypto.getRandomValues interface.

135 lines (119 loc) 4.25 kB
declare var module: any; const chars = ('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz').split(''); const crypto = typeof (globalThis as any).crypto !== 'undefined' && (globalThis as any).crypto || module['require']('node:crypto').webcrypto; let bytes = new Uint8Array(0); let position = 0; // State for ULID monotonicity let lastTimestamp = 0; let lastRandomPart = ''; /** * Creates a cryptographically secure random ID using base 62 encoding. * * Uses Node.js crypto.webcrypto or browser crypto.getRandomValues() API to generate * secure random bytes, then encodes them using characters 0-9, A-Z, a-z for URL-safe, readable IDs. * * @param length - The length of the ID to generate (default: 16) * @returns A random alphanumeric string of the specified length * * @example * ```typescript * createId(); // "RJPoz4veOGn9nbDI" (16 chars, ~4.7e28 possibilities) * createId(12); // "GS7rPnA0mmbv" (~3.22e21 possibilities) * createId(4); // "vMH6" (~14.7M possibilities) * ``` * * @remarks * The number of possible IDs is 62^length. The function uses a rejection sampling * approach to ensure uniform distribution across the character set. */ export function createId(length = 16) { let id = ''; while (id.length < length) { if (position === bytes.length) { bytes = crypto.getRandomValues(new Uint8Array(40)); position = 0; } for (; position < bytes.length; position++) { const b = bytes[position]; // Length of `chars` is 62. We only take bytes between 0 and 62*4-1 (both inclusive). The value is then evenly // mapped to indices of `chars` via a modulo operation. const maxValue = 62 * 4 - 1; if (b <= maxValue) { id += chars[b % 62]; } if (id.length >= length) break; } } return id; } /** * Creates a sortable identifier using base 62 encoding. * Format: [timestamp][randomness] * - First 8 characters: timestamp component (milliseconds since Unix epoch, good until year 8888) * - Last 8 characters: random component * Total length: 16 characters * * The IDs are lexicographically sortable and will naturally sort by creation time. * Implements monotonicity: multiple IDs generated in the same millisecond will be properly ordered. */ export function createSortableId(): string { const timestamp = Date.now(); // Convert timestamp to base 62 (8 characters, padded with leading zeros if needed) let timestampStr = encodeBase62(timestamp); // Pad to 8 characters for consistent sorting (good until year 8888) timestampStr = timestampStr.padStart(8, '0'); let randomPart: string; if (timestamp === lastTimestamp) { // Same millisecond - increment the random part for monotonicity randomPart = incrementBase62(lastRandomPart); if (!randomPart) { // Extremely unlikely overflow - but better safe than sorry throw new Error('Sortable ID overflow: too many IDs generated in the same millisecond'); } } else { // New millisecond - generate fresh random part randomPart = createId(8); lastTimestamp = timestamp; } lastRandomPart = randomPart; return timestampStr + randomPart; } /** * Increments a base 62 string by 1, handling carry operations. * Returns null if overflow occurs (all characters are 'z'). */ function incrementBase62(str: string): string | null { const arr = str.split(''); let carry = 1; // Start from the right-most character and work backwards for (let i = arr.length - 1; i >= 0 && carry > 0; i--) { const currentIndex = chars.indexOf(arr[i]); const newIndex = currentIndex + carry; if (newIndex < chars.length) { // No carry needed arr[i] = chars[newIndex]; carry = 0; } else { // Carry to next position arr[i] = chars[0]; // Reset to '0' carry = 1; } } // If we still have carry, it means overflow if (carry > 0) { return null; } return arr.join(''); } /** * Encodes a number to base 62 using the character set 0-9, A-Z, a-z */ function encodeBase62(num: number): string { if (num === 0) return '0'; let result = ''; while (num > 0) { result = chars[num % 62] + result; num = Math.floor(num / 62); } return result; }