const te = new TextEncoder(); function b64uToBytes(input: string): Uint8Array { // pad and replace input = input.replace(/-/g, "+").replace(/_/g, "/"); const pad = input.length % 4; if (pad) input += "=".repeat(4 - pad); return new Uint8Array(Buffer.from(input, "base64")); } function bytesToB64u(bytes: Uint8Array | ArrayBuffer): string { const buf = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes); return Buffer.from(buf) .toString("base64") .replace(/=/g, "") .replace(/\+/g, "-") .replace(/\//g, "_"); } async function importKey(secret: string): Promise { return await crypto.subtle.importKey( "raw", te.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"], ); } export async function signPayload( payload: unknown, secret: string, ): Promise { const payloadJson = typeof payload === "string" ? payload : JSON.stringify(payload); const payloadB64u = bytesToB64u(te.encode(payloadJson)); const key = await importKey(secret); const sig = await crypto.subtle.sign("HMAC", key, te.encode(payloadB64u)); const sigB64u = bytesToB64u(new Uint8Array(sig)); return `${payloadB64u}.${sigB64u}`; } export async function verifyAndDecode( token: string, secret: string, ): Promise<{ valid: boolean; payload?: T }> { const [payloadB64u, sigB64u] = token.split("."); if (!payloadB64u || !sigB64u) return { valid: false }; const key = await importKey(secret); const expected = await crypto.subtle.sign( "HMAC", key, te.encode(payloadB64u), ); const given = b64uToBytes(sigB64u); const expectedBytes = new Uint8Array(expected); if (given.length !== expectedBytes.length) return { valid: false }; // Constant-time compare let diff = 0; for (let i = 0; i < given.length; i++) diff |= given[i] ^ expectedBytes[i]; if (diff !== 0) return { valid: false }; try { const json = Buffer.from(b64uToBytes(payloadB64u)).toString("utf8"); return { valid: true, payload: JSON.parse(json) as T }; } catch { return { valid: false }; } }