1import type { SyncMessage } from "./sync-types";23const textEncoder = new TextEncoder();4const textDecoder = new TextDecoder("utf-8", { fatal: true });56export function encodeSyncMessage(message: SyncMessage): string {7 return base64urlEncode(textEncoder.encode(JSON.stringify(message)));8}910export function decodeSyncMessage(data: string): SyncMessage {11 try {12 const raw = textDecoder.decode(base64urlDecode(data));13 const parsed = JSON.parse(raw) as unknown;14 if (isSyncMessage(parsed)) {15 return parsed;16 }17 } catch {18 19 }20 throw new Error("invalid sync message");21}2223export function isSyncMessage(value: unknown): value is SyncMessage {24 if (!isRecord(value) || typeof value.type !== "string") return false;2526 switch (value.type) {27 case "SYNC_VAULT_SEND":28 return (29 isString(value.vault_a) &&30 isString(value.vault_b) &&31 isFiniteNumber(value.sent_at)32 );33 case "SYNC_VAULT_ACK":34 return isFiniteNumber(value.received_at);35 case "SYNC_ERROR":36 return isString(value.reason);37 case "PAIR_REQUEST":38 return isString(value.public_key_hex) && isString(value.device_name);39 case "PAIR_ACCEPT":40 return isString(value.public_key_hex) && isString(value.device_name);41 case "PAIR_REJECT":42 return isString(value.reason);43 default:44 return false;45 }46}4748function isRecord(value: unknown): value is Record<string, unknown> {49 return Boolean(value) && typeof value === "object" && !Array.isArray(value);50}5152function isString(value: unknown): value is string {53 return typeof value === "string";54}5556function isFiniteNumber(value: unknown): value is number {57 return typeof value === "number" && Number.isFinite(value);58}5960function base64urlEncode(bytes: Uint8Array): string {61 let binary = "";62 for (const byte of bytes) binary += String.fromCharCode(byte);63 return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");64}6566function base64urlDecode(value: string): Uint8Array {67 if (!/^[A-Za-z0-9_-]*$/.test(value) || value.length % 4 === 1) {68 throw new Error("invalid sync message");69 }70 const padded = value71 .replace(/-/g, "+")72 .replace(/_/g, "/")73 .padEnd(Math.ceil(value.length / 4) * 4, "=");74 const binary = atob(padded);75 const out = new Uint8Array(binary.length);76 for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i);77 return out;78}