import type { SyncMessage } from "./sync-types"; const textEncoder = new TextEncoder(); const textDecoder = new TextDecoder("utf-8", { fatal: true }); export function encodeSyncMessage(message: SyncMessage): string { return base64urlEncode(textEncoder.encode(JSON.stringify(message))); } export function decodeSyncMessage(data: string): SyncMessage { try { const raw = textDecoder.decode(base64urlDecode(data)); const parsed = JSON.parse(raw) as unknown; if (isSyncMessage(parsed)) { return parsed; } } catch { // Normalize malformed transport payloads so callers do not branch on parser details. } throw new Error("invalid sync message"); } export function isSyncMessage(value: unknown): value is SyncMessage { if (!isRecord(value) || typeof value.type !== "string") return false; switch (value.type) { case "SYNC_VAULT_SEND": return ( isString(value.vault_a) && isString(value.vault_b) && isFiniteNumber(value.sent_at) ); case "SYNC_VAULT_ACK": return isFiniteNumber(value.received_at); case "SYNC_ERROR": return isString(value.reason); case "PAIR_REQUEST": return isString(value.public_key_hex) && isString(value.device_name); case "PAIR_ACCEPT": return isString(value.public_key_hex) && isString(value.device_name); case "PAIR_REJECT": return isString(value.reason); default: return false; } } function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function isString(value: unknown): value is string { return typeof value === "string"; } function isFiniteNumber(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value); } function base64urlEncode(bytes: Uint8Array): string { let binary = ""; for (const byte of bytes) binary += String.fromCharCode(byte); return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); } function base64urlDecode(value: string): Uint8Array { if (!/^[A-Za-z0-9_-]*$/.test(value) || value.length % 4 === 1) { throw new Error("invalid sync message"); } const padded = value .replace(/-/g, "+") .replace(/_/g, "/") .padEnd(Math.ceil(value.length / 4) * 4, "="); const binary = atob(padded); const out = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i); return out; }