/** * @module universal/uuid * @description Framework-friendly UUID utilities used across Brainy — works in * Node, Bun, Deno, and browsers (relies only on the global Web Crypto API, with * pure-JS fallbacks). * * Three generators + helpers: * - {@link v4} — random UUID (legacy default). * - {@link v7} — time-ordered UUID (RFC 9562). The 8.0 default for new ids: * lexicographically sortable by creation time, which keeps the id↔int mapper * and any range scan locality-friendly. * - {@link v5} — deterministic namespaced UUID (RFC 4122, SHA-1). Used by the * 8.0 id-normalization layer to map a caller's non-UUID string key to a STABLE * UUID, so a native engine (which requires UUID ids for its int mapper) always * sees a valid UUID while the caller can keep using their natural key. */ /** Render 16 bytes as a canonical hyphenated UUID string. */ function bytesToUuid(b: Uint8Array): string { const h: string[] = [] for (let i = 0; i < 16; i++) h.push(b[i].toString(16).padStart(2, '0')) return ( `${h[0]}${h[1]}${h[2]}${h[3]}-${h[4]}${h[5]}-${h[6]}${h[7]}-` + `${h[8]}${h[9]}-${h[10]}${h[11]}${h[12]}${h[13]}${h[14]}${h[15]}` ) } /** Parse a hyphenated UUID string into its 16 bytes. */ function uuidToBytes(uuid: string): Uint8Array { const hex = uuid.replace(/-/g, '') const b = new Uint8Array(16) for (let i = 0; i < 16; i++) b[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16) return b } /** Fill a byte array with cryptographically-strong (or Math.random fallback) randomness. */ function randomBytes(n: number): Uint8Array { const out = new Uint8Array(n) if (typeof crypto !== 'undefined' && crypto.getRandomValues) { crypto.getRandomValues(out) } else { for (let i = 0; i < n; i++) out[i] = Math.floor(Math.random() * 256) } return out } /** * @description Canonical UUID-format check (any version/variant). NOT a version * assertion — just "is this string shaped like a UUID". * @param value - The string to test. * @returns `true` if `value` matches the 8-4-4-4-12 hex UUID format. */ export function isUUID(value: string): boolean { return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value) } /** * @description Random (version 4) UUID. * @returns A random UUID string. */ export function v4(): string { if (typeof crypto !== 'undefined' && crypto.randomUUID) { return crypto.randomUUID() } return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0 const v = c === 'x' ? r : (r & 0x3) | 0x8 return v.toString(16) }) } /** * @description Time-ordered (version 7) UUID — 48-bit Unix-ms timestamp prefix * + random tail. Two ids minted in order sort in order as strings. * @returns A version-7 UUID string. */ export function v7(): string { const ms = Date.now() const b = new Uint8Array(16) // 48-bit big-endian millisecond timestamp. b[0] = Math.floor(ms / 0x10000000000) & 0xff b[1] = Math.floor(ms / 0x100000000) & 0xff b[2] = Math.floor(ms / 0x1000000) & 0xff b[3] = Math.floor(ms / 0x10000) & 0xff b[4] = Math.floor(ms / 0x100) & 0xff b[5] = ms & 0xff const rand = randomBytes(10) b[6] = (rand[0] & 0x0f) | 0x70 // version 7 b[7] = rand[1] b[8] = (rand[2] & 0x3f) | 0x80 // variant 10xx for (let i = 9; i < 16; i++) b[i] = rand[i - 6] // rand[3..9] return bytesToUuid(b) } /** Left-rotate a 32-bit word. */ function rotl(n: number, s: number): number { return ((n << s) | (n >>> (32 - s))) >>> 0 } /** * @description SHA-1 of a byte array → 20 bytes. Pure JS so it runs synchronously * everywhere (Web Crypto's digest is async-only). Used solely by {@link v5}. * @param msg - Input bytes. * @returns The 20-byte SHA-1 digest. */ function sha1(msg: Uint8Array): Uint8Array { const ml = msg.length * 8 const withByte = msg.length + 1 const padLen = withByte % 64 <= 56 ? 56 - (withByte % 64) : 120 - (withByte % 64) const total = msg.length + 1 + padLen + 8 const buf = new Uint8Array(total) buf.set(msg, 0) buf[msg.length] = 0x80 const hi = Math.floor(ml / 0x100000000) const lo = ml >>> 0 buf[total - 8] = (hi >>> 24) & 0xff buf[total - 7] = (hi >>> 16) & 0xff buf[total - 6] = (hi >>> 8) & 0xff buf[total - 5] = hi & 0xff buf[total - 4] = (lo >>> 24) & 0xff buf[total - 3] = (lo >>> 16) & 0xff buf[total - 2] = (lo >>> 8) & 0xff buf[total - 1] = lo & 0xff let h0 = 0x67452301 let h1 = 0xefcdab89 let h2 = 0x98badcfe let h3 = 0x10325476 let h4 = 0xc3d2e1f0 const w = new Array(80) for (let i = 0; i < total; i += 64) { for (let t = 0; t < 16; t++) { w[t] = ((buf[i + t * 4] << 24) | (buf[i + t * 4 + 1] << 16) | (buf[i + t * 4 + 2] << 8) | buf[i + t * 4 + 3]) >>> 0 } for (let t = 16; t < 80; t++) { w[t] = rotl(w[t - 3] ^ w[t - 8] ^ w[t - 14] ^ w[t - 16], 1) } let a = h0 let b = h1 let c = h2 let d = h3 let e = h4 for (let t = 0; t < 80; t++) { let f: number let k: number if (t < 20) { f = (b & c) | (~b & d) k = 0x5a827999 } else if (t < 40) { f = b ^ c ^ d k = 0x6ed9eba1 } else if (t < 60) { f = (b & c) | (b & d) | (c & d) k = 0x8f1bbcdc } else { f = b ^ c ^ d k = 0xca62c1d6 } const tmp = (rotl(a, 5) + (f >>> 0) + e + k + w[t]) >>> 0 e = d d = c c = rotl(b, 30) b = a a = tmp } h0 = (h0 + a) >>> 0 h1 = (h1 + b) >>> 0 h2 = (h2 + c) >>> 0 h3 = (h3 + d) >>> 0 h4 = (h4 + e) >>> 0 } const out = new Uint8Array(20) const hs = [h0, h1, h2, h3, h4] for (let i = 0; i < 5; i++) { out[i * 4] = (hs[i] >>> 24) & 0xff out[i * 4 + 1] = (hs[i] >>> 16) & 0xff out[i * 4 + 2] = (hs[i] >>> 8) & 0xff out[i * 4 + 3] = hs[i] & 0xff } return out } /** * @description Deterministic version-5 (SHA-1, namespaced) UUID. The same * `(name, namespace)` always yields the same UUID — the property the * id-normalization layer relies on to turn a stable string key into a stable * UUID id. * @param name - The name to hash (e.g. a caller-supplied string id). * @param namespace - A UUID-format namespace (defaults to {@link BRAINY_ID_NAMESPACE}). * @returns A version-5 UUID string. */ export function v5(name: string, namespace: string = BRAINY_ID_NAMESPACE): string { const ns = uuidToBytes(namespace) const nameBytes = new TextEncoder().encode(name) const data = new Uint8Array(16 + nameBytes.length) data.set(ns, 0) data.set(nameBytes, 16) const hash = sha1(data) const b = hash.slice(0, 16) b[6] = (b[6] & 0x0f) | 0x50 // version 5 b[8] = (b[8] & 0x3f) | 0x80 // variant 10xx return bytesToUuid(b) } /** * @description The fixed namespace for Brainy's string→UUID normalization. A * stable constant (spells "brainy" in the first bytes) so the same caller string * maps to the same UUID across processes and machines. */ export const BRAINY_ID_NAMESPACE = '62726169-6e79-5000-8000-000000000000' // Named export to match the `uuid` package API. export { v4 as uuidv4, v7 as uuidv7, v5 as uuidv5 } export default { v4, v7, v5, isUUID, BRAINY_ID_NAMESPACE }