// Paired-device registry — v3 chirp-sync. // // Per the brief: long-term records live in `chrome.storage.sync` so the // registry roams across browser profiles signed into Chrome/Firefox sync. // `lastKnownIp` is network-specific and lives in `chrome.storage.local` // instead — bringing the IP across networks would do nothing but cause // the subnet probe to chase a stale address. // // `deviceId` is the canonical primary key (UUID v4, stable for the // lifetime of the installation). `longTermPublicKeyHex` is also unique // per device but rotates if the user reinstalls before re-pairing, so we // key on deviceId and update the key in place during re-pair. import type { PairedDevice } from "../shared/sync-types"; const REGISTRY_KEY = "peach_paired_devices_v3"; const LAST_IP_KEY_PREFIX = "peach_last_known_ip_v3:"; const LAST_SYNC_AT_KEY = "peach_last_sync_at"; // chrome.storage.sync has hard quotas: 100KB total, 8KB per item. A paired // device record is ~200 bytes (one X25519 hex pubkey + UUIDs + a name); // 100KB easily holds the realistic ~5-10 devices a user pairs. We store // the array under a single key to avoid eating the per-key quota. export async function listPairedDevices(): Promise { try { const result = await chrome.storage.sync.get(REGISTRY_KEY); const devices = (result as Record)[REGISTRY_KEY]; if (!Array.isArray(devices)) return []; return devices.filter(isPairedDevice); } catch { return []; } } export async function findPairedDeviceById(deviceId: string): Promise { const devices = await listPairedDevices(); return devices.find((d) => d.deviceId === deviceId) ?? null; } export async function findPairedDeviceByPubkey(pubkeyHex: string): Promise { const devices = await listPairedDevices(); return devices.find((d) => d.longTermPublicKeyHex === pubkeyHex) ?? null; } /** * Add or update a device by `deviceId`. On re-pair the new pubkey * silently replaces the old one; the deviceId is stable across the * other device's reinstall. */ export async function upsertPairedDevice(device: PairedDevice): Promise { if (!device.deviceId) throw new Error("device.deviceId is required"); if (!device.longTermPublicKeyHex) throw new Error("device.longTermPublicKeyHex is required"); if (!device.deviceName) throw new Error("device.deviceName is required"); if (!device.deviceType) throw new Error("device.deviceType is required"); const devices = await listPairedDevices(); const idx = devices.findIndex((d) => d.deviceId === device.deviceId); if (idx >= 0) { devices[idx] = { ...devices[idx], ...device }; } else { devices.push(device); } await chrome.storage.sync.set({ [REGISTRY_KEY]: devices }); } export async function removePairedDeviceById(deviceId: string): Promise { const devices = await listPairedDevices(); const filtered = devices.filter((d) => d.deviceId !== deviceId); await chrome.storage.sync.set({ [REGISTRY_KEY]: filtered }); // Also clear the per-network IP cache for this device. const device = devices.find((d) => d.deviceId === deviceId); if (device?.longTermPublicKeyHex) { await chrome.storage.local.remove(LAST_IP_KEY_PREFIX + device.longTermPublicKeyHex); } } export async function clearPairedDevices(): Promise { await chrome.storage.sync.remove(REGISTRY_KEY); } export async function markLastSyncedAt(deviceId: string, when: number): Promise { const devices = await listPairedDevices(); const idx = devices.findIndex((d) => d.deviceId === deviceId); if (idx < 0) return; devices[idx] = { ...devices[idx], lastSyncedAt: when }; await chrome.storage.sync.set({ [REGISTRY_KEY]: devices }); } export async function getLastKnownIp(pubkeyHex: string): Promise { try { const key = LAST_IP_KEY_PREFIX + pubkeyHex; const result = await chrome.storage.local.get(key); const value = (result as Record)[key]; return typeof value === "string" ? value : null; } catch { return null; } } export async function setLastKnownIp(pubkeyHex: string, ip: string): Promise { await chrome.storage.local.set({ [LAST_IP_KEY_PREFIX + pubkeyHex]: ip }); } export async function clearLastKnownIp(pubkeyHex: string): Promise { await chrome.storage.local.remove(LAST_IP_KEY_PREFIX + pubkeyHex); } export async function getLastSyncAt(): Promise { try { const result = await chrome.storage.local.get(LAST_SYNC_AT_KEY); const value = (result as Record)[LAST_SYNC_AT_KEY]; if (typeof value === "number") return value; if (typeof value === "string" && value !== "") { const n = Number.parseInt(value, 10); return Number.isFinite(n) ? n : null; } return null; } catch { return null; } } export async function setLastSyncAt(when: number): Promise { await chrome.storage.local.set({ [LAST_SYNC_AT_KEY]: when }); } function isPairedDevice(x: unknown): x is PairedDevice { if (typeof x !== "object" || x === null) return false; const r = x as Record; if (typeof r.deviceId !== "string" || !r.deviceId) return false; if (typeof r.pairedAt !== "number") return false; return true; }