export function hexToBytes(hex: string): Uint8Array { const bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < bytes.length; i++) { bytes[i] = Number.parseInt(hex.substring(i * 2, i * 2 + 2), 16); } return bytes; } export function bytesToHex(bytes: Uint8Array): string { return Array.from(bytes) .map((b) => b.toString(16).padStart(2, "0")) .join(""); } export async function hmacHex(data: string, secret: string): Promise { const key = await crypto.subtle.importKey( "raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"], ); const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(data)); return bytesToHex(new Uint8Array(signature)); } // crypto.subtle.timingSafeEqual throws on mismatched-length inputs rather // than returning false, so length is checked explicitly first. export function timingSafeEqualHex(a: string, b: string): boolean { if (a.length !== b.length) return false; return crypto.subtle.timingSafeEqual(hexToBytes(a), hexToBytes(b)); }