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
5 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/messages.ts+67 −1src/background/sync-lifecycle.ts+79 −0
manifest.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": {

view file

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",

view file

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": {

view file

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");

view file

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 

view file