1234567891011121314151617181920212223import type { PairedDevice } from "../shared/sync-types";24import { constantTimeEqualHex } from "../shared/constant-time";2526const REGISTRY_KEY = "peach_paired_devices_v3";27const LAST_IP_KEY_PREFIX = "peach_last_known_ip_v3:";28const LAST_SYNC_AT_KEY = "peach_last_sync_at";29const LAST_SYNC_RESULT_KEY = "peach_last_sync_result";3031export async function listPairedDevices(): Promise<PairedDevice[]> {32 try {33 const result = await chrome.storage.local.get(REGISTRY_KEY);34 const devices = (result as Record<string, unknown>)[REGISTRY_KEY];35 if (Array.isArray(devices)) return devices.filter(isPairedDevice);36 return [];37 } catch {38 return [];39 }40}4142export async function findPairedDeviceById(deviceId: string): Promise<PairedDevice | null> {43 const devices = await listPairedDevices();44 return devices.find((d) => d.deviceId === deviceId) ?? null;45}4647export async function findPairedDeviceByPubkey(pubkeyHex: string): Promise<PairedDevice | null> {48 const devices = await listPairedDevices();49 return devices.find((d) =>50 d.longTermPublicKeyHex !== undefined &&51 constantTimeEqualHex(d.longTermPublicKeyHex, pubkeyHex)52 ) ?? null;53}54555657585960export async function upsertPairedDevice(device: PairedDevice): Promise<void> {61 if (!device.deviceId) throw new Error("device.deviceId is required");62 if (!device.longTermPublicKeyHex) throw new Error("device.longTermPublicKeyHex is required");63 if (!device.deviceName) throw new Error("device.deviceName is required");64 if (!device.deviceType) throw new Error("device.deviceType is required");6566 const devices = await listPairedDevices();67 const idx = devices.findIndex((d) => d.deviceId === device.deviceId);68 if (idx >= 0) {69 devices[idx] = { ...devices[idx], ...device };70 } else {71 devices.push(device);72 }73 await chrome.storage.local.set({ [REGISTRY_KEY]: devices });74}7576777879808182export async function setRelayUrlForAllPairedDevices(83 relayUrl: string,84): Promise<void> {85 const devices = await listPairedDevices();86 if (devices.length === 0) return;87 await chrome.storage.local.set({88 [REGISTRY_KEY]: devices.map((device) => ({ ...device, relayUrl })),89 });90}919293949596979899100101102103export async function removePairedDeviceById(deviceId: string): Promise<void> {104 const devices = await listPairedDevices();105 const filtered = devices.filter((d) => d.deviceId !== deviceId);106 107 108 109 await chrome.storage.local.set({ [REGISTRY_KEY]: filtered });110 111 const device = devices.find((d) => d.deviceId === deviceId);112 if (device?.longTermPublicKeyHex) {113 await chrome.storage.local.remove(LAST_IP_KEY_PREFIX + device.longTermPublicKeyHex);114 }115}116117export async function clearPairedDevices(): Promise<void> {118 await chrome.storage.local.remove(REGISTRY_KEY);119}120121export async function markLastSyncedAt(deviceId: string, when: number): Promise<void> {122 const devices = await listPairedDevices();123 const idx = devices.findIndex((d) => d.deviceId === deviceId);124 if (idx < 0) return;125 devices[idx] = { ...devices[idx], lastSyncedAt: when };126 await chrome.storage.local.set({ [REGISTRY_KEY]: devices });127}128129export async function getLastKnownIp(pubkeyHex: string): Promise<string | null> {130 try {131 const key = LAST_IP_KEY_PREFIX + pubkeyHex;132 const result = await chrome.storage.local.get(key);133 const value = (result as Record<string, unknown>)[key];134 return typeof value === "string" ? value : null;135 } catch {136 return null;137 }138}139140export async function setLastKnownIp(pubkeyHex: string, ip: string): Promise<void> {141 await chrome.storage.local.set({ [LAST_IP_KEY_PREFIX + pubkeyHex]: ip });142}143144export async function clearLastKnownIp(pubkeyHex: string): Promise<void> {145 await chrome.storage.local.remove(LAST_IP_KEY_PREFIX + pubkeyHex);146}147148export async function getLastSyncAt(): Promise<number | null> {149 try {150 const result = await chrome.storage.local.get(LAST_SYNC_AT_KEY);151 const value = (result as Record<string, unknown>)[LAST_SYNC_AT_KEY];152 if (typeof value === "number") return value;153 if (typeof value === "string" && value !== "") {154 const n = Number.parseInt(value, 10);155 return Number.isFinite(n) ? n : null;156 }157 return null;158 } catch {159 return null;160 }161}162163export async function setLastSyncAt(when: number): Promise<void> {164 await chrome.storage.local.set({ [LAST_SYNC_AT_KEY]: when });165}166167export interface SyncResult {168 entriesReceived: number;169 entriesSent: number;170}171172export async function getLastSyncResult(): Promise<SyncResult | null> {173 try {174 const result = await chrome.storage.local.get(LAST_SYNC_RESULT_KEY);175 const value = (result as Record<string, unknown>)[LAST_SYNC_RESULT_KEY];176 if (177 value &&178 typeof value === "object" &&179 typeof (value as Record<string, unknown>).entriesReceived === "number" &&180 typeof (value as Record<string, unknown>).entriesSent === "number"181 ) {182 return value as SyncResult;183 }184 return null;185 } catch {186 return null;187 }188}189190export async function setLastSyncResult(result: SyncResult): Promise<void> {191 await chrome.storage.local.set({ [LAST_SYNC_RESULT_KEY]: result });192}193194function isPairedDevice(x: unknown): x is PairedDevice {195 if (typeof x !== "object" || x === null) return false;196 const r = x as Record<string, unknown>;197 if (typeof r.deviceId !== "string" || !r.deviceId) return false;198 if (typeof r.pairedAt !== "number") return false;199 if (200 r.rendezvousSecretHex !== undefined &&201 (typeof r.rendezvousSecretHex !== "string" || !/^[0-9a-f]{64}$/.test(r.rendezvousSecretHex))202 ) {203 return false;204 }205 if (r.relayUrl !== undefined) {206 if (typeof r.relayUrl !== "string") return false;207 try {208 if (new URL(r.relayUrl).protocol !== "wss:") return false;209 } catch {210 return false;211 }212 }213 return true;214}