// Text encodings for random bytes (RFC 4648). Encoding changes how a secret is written, never // how much randomness it holds: 32 random bytes carry 256 bits in every encoding. export type ByteEncoding = 'hex' | 'base64' | 'base64url'; const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; export function toHex(bytes: Uint8Array): string { let hex = ''; for (const byte of bytes) hex += byte.toString(16).padStart(2, '0'); return hex; } /** Standard base64 with "=" padding (RFC 4648, section 4). */ export function toBase64(bytes: Uint8Array): string { let out = ''; for (let i = 0; i < bytes.length; i += 3) { const [a, b = 0, c = 0] = [bytes[i], bytes[i + 1], bytes[i + 2]]; const chunk = (a << 16) | (b << 8) | c; out += BASE64[(chunk >> 18) & 63] + BASE64[(chunk >> 12) & 63]; out += i + 1 < bytes.length ? BASE64[(chunk >> 6) & 63] : '='; out += i + 2 < bytes.length ? BASE64[chunk & 63] : '='; } return out; } /** URL- and filename-safe base64 without padding (RFC 4648, section 5). */ export function toBase64Url(bytes: Uint8Array): string { return toBase64(bytes).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, ''); } export function encodeBytes(bytes: Uint8Array, encoding: ByteEncoding): string { if (encoding === 'hex') return toHex(bytes); return encoding === 'base64' ? toBase64(bytes) : toBase64Url(bytes); }