1234567891011121314151617181920212223import type { PairedDevice } from "../shared/sync-types";2425const REGISTRY_KEY = "peach_paired_devices_v3";26const LAST_IP_KEY_PREFIX = "peach_last_known_ip_v3:";27const LAST_SYNC_AT_KEY = "peach_last_sync_at";28const LAST_SYNC_RESULT_KEY = "peach_last_sync_result";2930export async function listPairedDevices(): Promise<PairedDevice[]> {31 try {32 const result = await chrome.storage.local.get(REGISTRY_KEY);33 const devices = (result as Record<string, unknown>)[REGISTRY_KEY];34 if (Array.isArray(devices)) return devices.filter(isPairedDevice);35 return [];36 } catch {37 return [];38 }39}4041export async function findPairedDeviceById(deviceId: string): Promise<PairedDevice | null> {42 const devices = await listPairedDevices();43 return devices.find((d) => d.deviceId === deviceId) ?? null;44}4546export async function findPairedDeviceByPubkey(pubkeyHex: string): Promise<PairedDevice | null> {47 const devices = await listPairedDevices();48 return devices.find((d) => d.longTermPublicKeyHex === pubkeyHex) ?? null;49}50515253545556export async function upsertPairedDevice(device: PairedDevice): Promise<void> {57 if (!device.deviceId) throw new Error("device.deviceId is required");58 if (!device.longTermPublicKeyHex) throw new Error("device.longTermPublicKeyHex is required");59 if (!device.deviceName) throw new Error("device.deviceName is required");60 if (!device.deviceType) throw new Error("device.deviceType is required");6162 const devices = await listPairedDevices();63 const idx = devices.findIndex((d) => d.deviceId === device.deviceId);64 if (idx >= 0) {65 devices[idx] = { ...devices[idx], ...device };66 } else {67 devices.push(device);68 }69 await chrome.storage.local.set({ [REGISTRY_KEY]: devices });70}7172737475767778export async function setRelayUrlForAllPairedDevices(79 relayUrl: string,80): Promise<void> {81 const devices = await listPairedDevices();82 if (devices.length === 0) return;83 await chrome.storage.local.set({84 [REGISTRY_KEY]: devices.map((device) => ({ ...device, relayUrl })),85 });86}87888990919293949596979899export async function removePairedDeviceById(deviceId: string): Promise<void> {100 const devices = await listPairedDevices();101 const filtered = devices.filter((d) => d.deviceId !== deviceId);102 103 104 105 await chrome.storage.local.set({ [REGISTRY_KEY]: filtered });106 107 const device = devices.find((d) => d.deviceId === deviceId);108 if (device?.longTermPublicKeyHex) {109 await chrome.storage.local.remove(LAST_IP_KEY_PREFIX + device.longTermPublicKeyHex);110 }111}112113export async function clearPairedDevices(): Promise<void> {114 await chrome.storage.local.remove(REGISTRY_KEY);115}116117export async function markLastSyncedAt(deviceId: string, when: number): Promise<void> {118 const devices = await listPairedDevices();119 const idx = devices.findIndex((d) => d.deviceId === deviceId);120 if (idx < 0) return;121 devices[idx] = { ...devices[idx], lastSyncedAt: when };122 await chrome.storage.local.set({ [REGISTRY_KEY]: devices });123}124125export async function getLastKnownIp(pubkeyHex: string): Promise<string | null> {126 try {127 const key = LAST_IP_KEY_PREFIX + pubkeyHex;128 const result = await chrome.storage.local.get(key);129 const value = (result as Record<string, unknown>)[key];130 return typeof value === "string" ? value : null;131 } catch {132 return null;133 }134}135136export async function setLastKnownIp(pubkeyHex: string, ip: string): Promise<void> {137 await chrome.storage.local.set({ [LAST_IP_KEY_PREFIX + pubkeyHex]: ip });138}139140export async function clearLastKnownIp(pubkeyHex: string): Promise<void> {141 await chrome.storage.local.remove(LAST_IP_KEY_PREFIX + pubkeyHex);142}143144export async function getLastSyncAt(): Promise<number | null> {145 try {146 const result = await chrome.storage.local.get(LAST_SYNC_AT_KEY);147 const value = (result as Record<string, unknown>)[LAST_SYNC_AT_KEY];148 if (typeof value === "number") return value;149 if (typeof value === "string" && value !== "") {150 const n = Number.parseInt(value, 10);151 return Number.isFinite(n) ? n : null;152 }153 return null;154 } catch {155 return null;156 }157}158159export async function setLastSyncAt(when: number): Promise<void> {160 await chrome.storage.local.set({ [LAST_SYNC_AT_KEY]: when });161}162163export interface SyncResult {164 entriesReceived: number;165 entriesSent: number;166}167168export async function getLastSyncResult(): Promise<SyncResult | null> {169 try {170 const result = await chrome.storage.local.get(LAST_SYNC_RESULT_KEY);171 const value = (result as Record<string, unknown>)[LAST_SYNC_RESULT_KEY];172 if (173 value &&174 typeof value === "object" &&175 typeof (value as Record<string, unknown>).entriesReceived === "number" &&176 typeof (value as Record<string, unknown>).entriesSent === "number"177 ) {178 return value as SyncResult;179 }180 return null;181 } catch {182 return null;183 }184}185186export async function setLastSyncResult(result: SyncResult): Promise<void> {187 await chrome.storage.local.set({ [LAST_SYNC_RESULT_KEY]: result });188}189190function isPairedDevice(x: unknown): x is PairedDevice {191 if (typeof x !== "object" || x === null) return false;192 const r = x as Record<string, unknown>;193 if (typeof r.deviceId !== "string" || !r.deviceId) return false;194 if (typeof r.pairedAt !== "number") return false;195 if (196 r.rendezvousSecretHex !== undefined &&197 (typeof r.rendezvousSecretHex !== "string" || !/^[0-9a-f]{64}$/.test(r.rendezvousSecretHex))198 ) {199 return false;200 }201 if (r.relayUrl !== undefined) {202 if (typeof r.relayUrl !== "string") return false;203 try {204 if (new URL(r.relayUrl).protocol !== "wss:") return false;205 } catch {206 return false;207 }208 }209 return true;210}