/** * Isomorphic base64url (RFC 4648 §5, no padding) over bytes and UTF-8 strings. * Hand-rolled (no Buffer / no atob) so both the Node relay (P1) and the browser * bundle (P6) import it unchanged and agree byte-for-byte on the §4.3 token subprotocol. */ import { ContractDecodeError } from './errors.js' const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_' const DECODE_MAP: Readonly> = Object.fromEntries( [...ALPHABET].map((ch, i) => [ch, i]), ) const textEncoder = new TextEncoder() const textDecoder = new TextDecoder('utf-8', { fatal: true }) /** Encode raw bytes to a base64url string (no `=` padding). */ export function encodeBase64UrlBytes(bytes: Uint8Array): string { let out = '' for (let i = 0; i < bytes.length; i += 3) { const b0 = bytes[i]! const b1 = i + 1 < bytes.length ? bytes[i + 1]! : 0 const b2 = i + 2 < bytes.length ? bytes[i + 2]! : 0 const triple = (b0 << 16) | (b1 << 8) | b2 const remaining = bytes.length - i out += ALPHABET[(triple >> 18) & 0x3f] out += ALPHABET[(triple >> 12) & 0x3f] if (remaining > 1) out += ALPHABET[(triple >> 6) & 0x3f] if (remaining > 2) out += ALPHABET[triple & 0x3f] } return out } /** Decode a base64url string (padding tolerated) back to raw bytes. */ export function decodeBase64UrlBytes(value: string): Uint8Array { const clean = value.replace(/=+$/, '') if (/[^A-Za-z0-9\-_]/.test(clean)) { throw new ContractDecodeError('invalid base64url: contains non-alphabet characters') } const bytes: number[] = [] for (let i = 0; i < clean.length; i += 4) { const c0 = DECODE_MAP[clean[i]!]! const c1 = i + 1 < clean.length ? DECODE_MAP[clean[i + 1]!]! : 0 const c2 = i + 2 < clean.length ? DECODE_MAP[clean[i + 2]!] : undefined const c3 = i + 3 < clean.length ? DECODE_MAP[clean[i + 3]!] : undefined const triple = (c0 << 18) | (c1 << 12) | ((c2 ?? 0) << 6) | (c3 ?? 0) bytes.push((triple >> 16) & 0xff) if (c2 !== undefined) bytes.push((triple >> 8) & 0xff) if (c3 !== undefined) bytes.push(triple & 0xff) } return Uint8Array.from(bytes) } /** Encode a UTF-8 string to base64url. */ export function encodeBase64UrlString(value: string): string { return encodeBase64UrlBytes(textEncoder.encode(value)) } /** Decode a base64url string to a UTF-8 string. */ export function decodeBase64UrlString(value: string): string { try { return textDecoder.decode(decodeBase64UrlBytes(value)) } catch (err) { if (err instanceof ContractDecodeError) throw err throw new ContractDecodeError('base64url payload is not valid UTF-8') } }