123456789101112131415161718import { Capacitor, registerPlugin } from "@capacitor/core";19import type { PluginListenerHandle } from "@capacitor/core";20import type { DiscoveredPeer } from "./sync-types";21import type { SyncSessionResult } from "../background/sync-coordinator";2223export interface SyncBridgePeer {24 deviceId: string;25 deviceName: string;26 host: string;27 port: number;28 wsPort?: number;29 x25519PublicKey: string;30 pairing: boolean;31}3233export interface SyncBridge {34 35 startDiscovery(): Promise<void>;36 stopDiscovery(): Promise<void>;37 onPeerDiscovered(handler: (peer: SyncBridgePeer) => void): () => void;38 onPeerLost(handler: (deviceId: string) => void): () => void;39 404142434445 syncWithPeer?(peer: DiscoveredPeer): Promise<SyncSessionResult>;46}4748interface SyncPluginShape {49 startDiscovery(): Promise<{ alreadyDiscovering?: boolean } | void>;50 stopDiscovery(): Promise<void>;51 getDiscoveredPeers(): Promise<{ peers: SyncBridgePeer[] }>;52 addListener(53 eventName: "peerDiscovered",54 listener: (peer: SyncBridgePeer) => void55 ): Promise<PluginListenerHandle>;56 addListener(57 eventName: "peerLost",58 listener: (event: { deviceId: string }) => void59 ): Promise<PluginListenerHandle>;60 addListener(61 eventName: "syncError",62 listener: (event: { code: string; errorCode?: number }) => void63 ): Promise<PluginListenerHandle>;64}6566class CapacitorSyncBridge implements SyncBridge {67 private plugin = registerPlugin<SyncPluginShape>("Sync");68 private peerDiscoveredHandlers: Array<(peer: SyncBridgePeer) => void> = [];69 private peerLostHandlers: Array<(deviceId: string) => void> = [];70 private listenerHandles: PluginListenerHandle[] = [];7172 async startDiscovery(): Promise<void> {73 await this.plugin.startDiscovery();74 if (this.listenerHandles.length === 0) {75 const found = await this.plugin.addListener("peerDiscovered", (peer) => {76 for (const h of this.peerDiscoveredHandlers) h(peer);77 });78 const lost = await this.plugin.addListener("peerLost", (event) => {79 for (const h of this.peerLostHandlers) h(event.deviceId);80 });81 this.listenerHandles.push(found, lost);82 }83 }8485 async stopDiscovery(): Promise<void> {86 for (const handle of this.listenerHandles) {87 try {88 await handle.remove();89 } catch {90 91 }92 }93 this.listenerHandles = [];94 await this.plugin.stopDiscovery();95 }9697 onPeerDiscovered(handler: (peer: SyncBridgePeer) => void): () => void {98 this.peerDiscoveredHandlers.push(handler);99 return () => {100 const i = this.peerDiscoveredHandlers.indexOf(handler);101 if (i >= 0) this.peerDiscoveredHandlers.splice(i, 1);102 };103 }104105 onPeerLost(handler: (deviceId: string) => void): () => void {106 this.peerLostHandlers.push(handler);107 return () => {108 const i = this.peerLostHandlers.indexOf(handler);109 if (i >= 0) this.peerLostHandlers.splice(i, 1);110 };111 }112113 async syncWithPeer(peer: DiscoveredPeer): Promise<SyncSessionResult> {114 const [{ getAndroidSyncIdentity }, { createCapacitorSyncTransport }, { runBlobSyncSession }, { storageGet }] =115 await Promise.all([116 import("./android-sync-identity"),117 import("./capacitor-sync-transport"),118 import("../background/sync-coordinator"),119 import("./storage"),120 ]);121122 const identity = await getAndroidSyncIdentity();123 const transport = await createCapacitorSyncTransport(124 peer,125 identity.privateKey,126 identity.publicKey,127 );128129 return runBlobSyncSession("initiator", transport, {130 131 132 133 134 isPro: async () => true,135 isUnlocked: () => true,136 getVaultBlobs: async () => ({137 vault_a: (await storageGet("peach_vault_a")) ?? "",138 vault_b: (await storageGet("peach_vault_b")) ?? "",139 }),140 });141 }142}143144class NativeMessagingSyncBridge implements SyncBridge {145 private peerDiscoveredHandlers: Array<(peer: SyncBridgePeer) => void> = [];146 private peerLostHandlers: Array<(deviceId: string) => void> = [];147 private unsubscribers: Array<() => void> = [];148149 async startDiscovery(): Promise<void> {150 const sync = await import("../background/sync");151 sync.connectSyncHost();152 const offDiscovered = sync.onPeerDiscovered((peer) => {153 for (const h of this.peerDiscoveredHandlers) {154 h({155 deviceId: peer.deviceId,156 deviceName: peer.name,157 host: peer.host,158 port: peer.port,159 wsPort: peer.wsPort,160 x25519PublicKey: peer.x25519PublicKey,161 pairing: peer.pairing,162 });163 }164 });165 const offLost = sync.onPeerLost((deviceId) => {166 for (const h of this.peerLostHandlers) h(deviceId);167 });168 this.unsubscribers.push(offDiscovered, offLost);169 sync.discoverPeers();170 }171172 async stopDiscovery(): Promise<void> {173 for (const off of this.unsubscribers) off();174 this.unsubscribers = [];175 try {176 const sync = await import("../background/sync");177 if (sync.isHostConnected()) {178 await sync.stopDiscovery();179 }180 } catch {181 182 }183 }184185 onPeerDiscovered(handler: (peer: SyncBridgePeer) => void): () => void {186 this.peerDiscoveredHandlers.push(handler);187 return () => {188 const i = this.peerDiscoveredHandlers.indexOf(handler);189 if (i >= 0) this.peerDiscoveredHandlers.splice(i, 1);190 };191 }192193 onPeerLost(handler: (deviceId: string) => void): () => void {194 this.peerLostHandlers.push(handler);195 return () => {196 const i = this.peerLostHandlers.indexOf(handler);197 if (i >= 0) this.peerLostHandlers.splice(i, 1);198 };199 }200}201202let cached: SyncBridge | null = null;203204export function getSyncBridge(): SyncBridge {205 if (cached) return cached;206 cached = Capacitor.isNativePlatform()207 ? new CapacitorSyncBridge()208 : new NativeMessagingSyncBridge();209 return cached;210}211212213export function toDiscoveredPeer(214 peer: SyncBridgePeer,215 alreadyPaired: boolean216): DiscoveredPeer {217 return {218 deviceId: peer.deviceId,219 deviceName: peer.deviceName,220 host: peer.host,221 port: peer.port,222 wsPort: peer.wsPort,223 x25519PublicKey: peer.x25519PublicKey,224 alreadyPaired,225 };226}227228229230231232233export function peerWebSocketUrl(peer: SyncBridgePeer): string {234 if (peer.wsPort === undefined) {235 throw new Error("peer did not advertise a WebSocket port");236 }237 return `ws://${peer.host}:${peer.wsPort}/`;238}