21d3f786ea14
fix: device registry — move from chrome.storage.sync to chrome.storage.local with one-time migration; add peach_last_sync_result storage (v3.6.94)
- time
- 2026-05-22 18:21 UTC · 5 months ago
- machine
- koh2
- parent
- b92e86329db5
↓ peach-browser-21d3f786.tar.gzatlas at this save
files changed1 file
src/background/paired-devices.ts+64 −17src/background/paired-devices.ts+64 −17
@@ -1,33 +1,50 @@11 // Paired-device registry — v3 chirp-sync.22 //3-// Per the brief: long-term records live in `chrome.storage.sync` so the4-// registry roams across browser profiles signed into Chrome/Firefox sync.5-// `lastKnownIp` is network-specific and lives in `chrome.storage.local`6-// instead — bringing the IP across networks would do nothing but cause7-// the subnet probe to chase a stale address.3+// Long-term records live in `chrome.storage.local`. Keys must not leave4+// the device via a third-party sync service — `longTermPublicKeyHex` and5+// `signingPublicKeyHex` are cryptographic keys; routing them through6+// Google's or Mozilla's sync back-end violates the local-only trust model.87 //8+// `lastKnownIp` is network-specific and also lives in `chrome.storage.local`,9+// keyed by `longTermPublicKeyHex` (not on the device record itself) so10+// network-specific addresses don't roam across profiles.11+//912 // `deviceId` is the canonical primary key (UUID v4, stable for the1013 // lifetime of the installation). `longTermPublicKeyHex` is also unique1114 // per device but rotates if the user reinstalls before re-pairing, so we1215 // key on deviceId and update the key in place during re-pair.16+//17+// Migration note: earlier builds stored the registry in `chrome.storage.sync`.18+// `listPairedDevices` transparently migrates any existing sync-stored19+// registry to local storage on first read. This is a one-time, local-only20+// unpair is the security boundary — see removePairedDeviceById for details.1321 1422 import type { PairedDevice } from "../shared/sync-types";1523 1624 const REGISTRY_KEY = "peach_paired_devices_v3";1725 const LAST_IP_KEY_PREFIX = "peach_last_known_ip_v3:";1826 const LAST_SYNC_AT_KEY = "peach_last_sync_at";27+const LAST_SYNC_RESULT_KEY = "peach_last_sync_result";1928 20-// chrome.storage.sync has hard quotas: 100KB total, 8KB per item. A paired21-// device record is ~200 bytes (one X25519 hex pubkey + UUIDs + a name);22-// 100KB easily holds the realistic ~5-10 devices a user pairs. We store23-// the array under a single key to avoid eating the per-key quota.24-2529 export async function listPairedDevices(): Promise<PairedDevice[]> {2630 try {27- const result = await chrome.storage.sync.get(REGISTRY_KEY);31+ const result = await chrome.storage.local.get(REGISTRY_KEY);2832 const devices = (result as Record<string, unknown>)[REGISTRY_KEY];29- if (!Array.isArray(devices)) return [];30- return devices.filter(isPairedDevice);33+ if (Array.isArray(devices)) return devices.filter(isPairedDevice);34+ // One-time migration from the earlier chrome.storage.sync location.35+ // If local storage is empty but sync has data, move it over and clear36+ // the sync copy so keys stop leaving the device.37+ try {38+ const syncResult = await chrome.storage.sync.get(REGISTRY_KEY);39+ const syncDevices = (syncResult as Record<string, unknown>)[REGISTRY_KEY];40+ if (Array.isArray(syncDevices) && syncDevices.length > 0) {41+ const valid = syncDevices.filter(isPairedDevice);42+ await chrome.storage.local.set({ [REGISTRY_KEY]: valid });43+ await chrome.storage.sync.remove(REGISTRY_KEY).catch(() => undefined);44+ return valid;45+ }46+ } catch { /* sync unavailable in Firefox or context — skip migration */ }47+ return [];3148 } catch {3249 return [];3350 }@@ -61,13 +78,16 @@6178 } else {6279 devices.push(device);6380 }64- await chrome.storage.sync.set({ [REGISTRY_KEY]: devices });81+ await chrome.storage.local.set({ [REGISTRY_KEY]: devices });6582 }6683 6784 export async function removePairedDeviceById(deviceId: string): Promise<void> {6885 const devices = await listPairedDevices();6986 const filtered = devices.filter((d) => d.deviceId !== deviceId);70- await chrome.storage.sync.set({ [REGISTRY_KEY]: filtered });87+ // Unpairing is local-only — there is no remote unpair RPC. The remote88+ // device retains this device in its own registry. Noise XX rejection on89+ // the next connection attempt is the security boundary.90+ await chrome.storage.local.set({ [REGISTRY_KEY]: filtered });7191 // Also clear the per-network IP cache for this device.7292 const device = devices.find((d) => d.deviceId === deviceId);7393 if (device?.longTermPublicKeyHex) {@@ -76,7 +96,7 @@7696 }7797 7898 export async function clearPairedDevices(): Promise<void> {79- await chrome.storage.sync.remove(REGISTRY_KEY);99+ await chrome.storage.local.remove(REGISTRY_KEY);80100 }81101 82102 export async function markLastSyncedAt(deviceId: string, when: number): Promise<void> {@@ -84,7 +104,7 @@84104 const idx = devices.findIndex((d) => d.deviceId === deviceId);85105 if (idx < 0) return;86106 devices[idx] = { ...devices[idx], lastSyncedAt: when };87- await chrome.storage.sync.set({ [REGISTRY_KEY]: devices });107+ await chrome.storage.local.set({ [REGISTRY_KEY]: devices });88108 }89109 90110 export async function getLastKnownIp(pubkeyHex: string): Promise<string | null> {@@ -123,6 +143,33 @@123143 124144 export async function setLastSyncAt(when: number): Promise<void> {125145 await chrome.storage.local.set({ [LAST_SYNC_AT_KEY]: when });146+}147+148+export interface SyncResult {149+ entriesReceived: number;150+ entriesSent: number;151+}152+153+export async function getLastSyncResult(): Promise<SyncResult | null> {154+ try {155+ const result = await chrome.storage.local.get(LAST_SYNC_RESULT_KEY);156+ const value = (result as Record<string, unknown>)[LAST_SYNC_RESULT_KEY];157+ if (158+ value &&159+ typeof value === "object" &&160+ typeof (value as Record<string, unknown>).entriesReceived === "number" &&161+ typeof (value as Record<string, unknown>).entriesSent === "number"162+ ) {163+ return value as SyncResult;164+ }165+ return null;166+ } catch {167+ return null;168+ }169+}170+171+export async function setLastSyncResult(result: SyncResult): Promise<void> {172+ await chrome.storage.local.set({ [LAST_SYNC_RESULT_KEY]: result });126173 }127174 128175 function isPairedDevice(x: unknown): x is PairedDevice {