a2a086b69cf1
TRIGGER_SYNC sequential multi-peer + mDNS onPeerDiscovered auto-sync (60s debounce) (v2.14.4)
- time
- 2026-04-30 20:39 UTC · 6 months ago
- machine
- koh2
- parent
- 89df69da73ab
↓ peach-browser-a2a086b6.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/messages.ts+67 −1src/background/sync-lifecycle.ts+79 −0manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.14.3",4+ "version": "2.14.4",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.14.3",4+ "version": "2.14.4",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "2.14.3",3+ "version": "2.14.4",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/background/messages.ts+67 −1
@@ -658,7 +658,73 @@658658 case "TRIGGER_SYNC": {659659 const gate = await requirePro("sync");660660 if (!gate.allowed) return { ok: false, error: "pro_required" };661- return { ok: true };661+ if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };662+663+ const { getPairedDevices, updatePairedDevice } = await import('./sync-storage');664+ const paired = await getPairedDevices();665+ if (paired.length === 0) return { ok: false, error: "no_paired_devices" };666+667+ const lifecycle = await import('./sync-lifecycle');668+ const sync = await import('./sync');669+ const { storageGet, storageSet } = await import('../shared/storage');670+ const { createNativeSyncTransport, runBlobSyncSession } = await import('./sync-coordinator');671+672+ const discovered = lifecycle.getDiscoveredPeerCache();673+ const reachable = paired.filter((device) =>674+ discovered.some((peer) => peer.x25519PublicKey === device.x25519PublicKey),675+ );676+ if (reachable.length === 0) return { ok: false, error: "no_devices_reachable" };677+678+ // Sequential — each session may update peach_vault_a, and the next679+ // peer should receive the freshest blob (which may already include680+ // changes from the previous device in this same pass).681+ const results: Array<{682+ deviceName: string;683+ status: "completed" | "errored" | "closed";684+ error?: string;685+ }> = [];686+ for (const device of reachable) {687+ const peer = discovered.find((p) => p.x25519PublicKey === device.x25519PublicKey)!;688+ try {689+ const connectionId = await sync.connectToPeer(peer.host, peer.port, peer.x25519PublicKey);690+ const transport = await createNativeSyncTransport(connectionId);691+ const result = await runBlobSyncSession("initiator", transport, {692+ isPro: async () => true,693+ isUnlocked: () => vault.isUnlocked(),694+ getVaultBlobs: async () => ({695+ vault_a: (await storageGet("peach_vault_a")) ?? "",696+ vault_b: (await storageGet("peach_vault_b")) ?? "",697+ }),698+ });699+ results.push({700+ deviceName: device.displayName,701+ status: result.status,702+ error: result.error,703+ });704+ if (result.status === "completed") {705+ await updatePairedDevice(device.deviceId, {706+ lastSeenAt: Date.now(),707+ lastSyncAt: Date.now(),708+ });709+ }710+ } catch (err) {711+ results.push({712+ deviceName: device.displayName,713+ status: "errored",714+ error: err instanceof Error ? err.message : String(err),715+ });716+ }717+ }718+719+ await storageSet("peach_last_sync_at", String(Date.now()));720+ return {721+ ok: true,722+ data: {723+ results,724+ syncedCount: results.filter((r) => r.status === "completed").length,725+ totalReachable: reachable.length,726+ },727+ };662728 }663729 case "START_PAIRING": {664730 const gate = await requirePro("sync");src/background/sync-lifecycle.ts+79 −0
@@ -27,7 +27,79 @@2727 let advertisingActive = false;2828 let nativeHostConnected = false;2929 let incomingSubscriberInstalled = false;30+let peerCacheInstalled = false;31+const peerCache = new Map<string, sync.PeerInfo>();32+const LAST_SYNC_KEY = "peach_last_sync_at";33+const AUTO_SYNC_DEBOUNCE_MS = 60_000;3034 35+function ensurePeerCache(): void {36+ if (peerCacheInstalled) return;37+ peerCacheInstalled = true;38+ sync.onPeerDiscovered((peer) => {39+ peerCache.set(peer.x25519PublicKey, peer);40+ void maybeAutoSync(peer);41+ });42+ sync.onPeerLost((deviceId) => {43+ for (const [key, peer] of peerCache) {44+ if (peer.deviceId === deviceId) {45+ peerCache.delete(key);46+ break;47+ }48+ }49+ });50+}51+52+export function getDiscoveredPeerCache(): sync.PeerInfo[] {53+ return Array.from(peerCache.values());54+}55+56+async function maybeAutoSync(peer: sync.PeerInfo): Promise<void> {57+ if (Capacitor.isNativePlatform()) return;58+ try {59+ const paired = await getPairedDevices();60+ if (!paired.some((d) => d.x25519PublicKey === peer.x25519PublicKey)) return;61+ if (!vault.isUnlocked()) return;62+ if (!(await isProUser())) return;63+64+ // 60s debounce — don't hammer a peer that just appeared after a short blip.65+ const lastRaw = await readStorage(LAST_SYNC_KEY);66+ if (lastRaw) {67+ const last = Number.parseInt(lastRaw, 10);68+ if (Number.isFinite(last) && Date.now() - last < AUTO_SYNC_DEBOUNCE_MS) return;69+ }70+71+ const { createNativeSyncTransport, runBlobSyncSession } = await import(72+ "./sync-coordinator"73+ );74+ const { storageGet, storageSet } = await import("../shared/storage");75+76+ const connectionId = await sync.connectToPeer(peer.host, peer.port, peer.x25519PublicKey);77+ const transport = await createNativeSyncTransport(connectionId);78+ await runBlobSyncSession("initiator", transport, {79+ isPro: () => isProUser(),80+ isUnlocked: () => vault.isUnlocked(),81+ getVaultBlobs: async () => ({82+ vault_a: (await storageGet("peach_vault_a")) ?? "",83+ vault_b: (await storageGet("peach_vault_b")) ?? "",84+ }),85+ });86+ await storageSet(LAST_SYNC_KEY, String(Date.now()));87+ } catch (err) {88+ // Auto-sync is silent — user can retry manually from Settings.89+ console.warn("auto-sync failed:", err);90+ }91+}92+93+async function readStorage(key: string): Promise<string | null> {94+ try {95+ const result = await chrome.storage.local.get(key);96+ const value = (result as Record<string, string | undefined>)[key];97+ return typeof value === "string" ? value : null;98+ } catch {99+ return null;100+ }101+}102+31103 async function getOrCreateDeviceId(): Promise<string> {32104 try {33105 const stored = await chrome.storage.local.get(DEVICE_ID_KEY);@@ -203,6 +275,13 @@203275 export function onNativeHostConnected(): void {204276 nativeHostConnected = true;205277 setupIncomingPairingSubscriber();278+ ensurePeerCache();279+ // Kick off discovery so the cache populates and auto-sync can fire.280+ try {281+ sync.discoverPeers();282+ } catch {283+ // best-effort284+ }206285 void syncLifecycleTick();207286 }208287