sync.ts

7.2 KB · raw

1/**2 * DORMANT — peach-sync native host client.3 *4 * Not wired to any live code path. The native host (native/peach-sync-host)5 * has an open critical bug (SYN-001, Projects.md §3.9). Do not import from6 * this module. Do not re-enable sync without completing the SYN-001 fix7 * sprint and a full re-audit of the native host first.8 */910type HostMessage =11  | { type: "ADVERTISE"; port: number; x25519Pubkey: string; ed25519Pubkey: string; name: string; pairing: boolean }12  | { type: "STOP_ADVERTISING" }13  | { type: "DISCOVER" }14  | { type: "CONNECT"; host: string; port: number }15  | { type: "DISCONNECT" }16  | { type: "HANDSHAKE_START"; isInitiator: boolean; remoteStaticKey: string }17  | { type: "HANDSHAKE_DATA"; data: string }18  | { type: "SYNC_SEND"; message: string }19  | { type: "SYNC_RECEIVE" }20  | { type: "STATUS"; isTrustedNetwork: boolean; isTailscale: boolean; ssid?: string };2122type HostResponse =23  | { type: "DISCOVERED"; host: string; port: number; name: string; pairing: boolean; x25519Pubkey: string }24  | { type: "CONNECTED" }25  | { type: "DISCONNECTED" }26  | { type: "HANDSHAKE_RESULT"; success: boolean; data?: string }27  | { type: "SYNC_MESSAGE"; message: string }28  | { type: "STATUS"; isTrustedNetwork: boolean; isTailscale: boolean; ssid?: string }29  | { type: "ERROR"; message: string };3031export interface PeerInfo {32  host: string;33  port: number;34  name: string;35  pairing: boolean;36  x25519Pubkey: string;37}3839let syncPort: chrome.runtime.Port | null = null;4041const peerDiscoveredHandlers: Array<(peer: PeerInfo) => void> = [];42const connectedHandlers: Array<() => void> = [];43const disconnectedHandlers: Array<() => void> = [];44const handshakeResultHandlers: Array<(success: boolean, data?: string) => void> = [];45const syncMessageHandlers: Array<(message: string) => void> = [];46const errorHandlers: Array<(error: string) => void> = [];4748let pendingDiscover: { resolve: (r: HostResponse) => void; reject: (e: Error) => void } | null = null;49let pendingConnect: { resolve: () => void; reject: (e: Error) => void } | null = null;50let pendingStatus: { resolve: (r: HostResponse) => void; reject: (e: Error) => void } | null = null;5152function handleHostMessage(response: HostResponse): void {53  switch (response.type) {54    case "DISCOVERED": {55      const peer: PeerInfo = {56        host: response.host,57        port: response.port,58        name: response.name,59        pairing: response.pairing,60        x25519Pubkey: response.x25519Pubkey,61      };62      if (pendingDiscover) {63        const { resolve } = pendingDiscover;64        pendingDiscover = null;65        resolve(response);66      }67      for (const handler of peerDiscoveredHandlers) handler(peer);68      break;69    }70    case "CONNECTED": {71      if (pendingConnect) {72        const { resolve } = pendingConnect;73        pendingConnect = null;74        resolve();75      }76      for (const handler of connectedHandlers) handler();77      break;78    }79    case "DISCONNECTED": {80      if (pendingConnect) {81        const { reject } = pendingConnect;82        pendingConnect = null;83        reject(new Error("Peer disconnected during connect"));84      }85      for (const handler of disconnectedHandlers) handler();86      break;87    }88    case "HANDSHAKE_RESULT": {89      for (const handler of handshakeResultHandlers) handler(response.success, response.data);90      break;91    }92    case "SYNC_MESSAGE": {93      for (const handler of syncMessageHandlers) handler(response.message);94      break;95    }96    case "STATUS": {97      if (pendingStatus) {98        const { resolve } = pendingStatus;99        pendingStatus = null;100        resolve(response);101      }102      break;103    }104    case "ERROR": {105      if (pendingDiscover) {106        const { reject } = pendingDiscover;107        pendingDiscover = null;108        reject(new Error(response.message));109      }110      if (pendingConnect) {111        const { reject } = pendingConnect;112        pendingConnect = null;113        reject(new Error(response.message));114      }115      if (pendingStatus) {116        const { reject } = pendingStatus;117        pendingStatus = null;118        reject(new Error(response.message));119      }120      for (const handler of errorHandlers) handler(response.message);121      break;122    }123  }124}125126function sendToHost(msg: HostMessage): void {127  syncPort?.postMessage(msg);128}129130export function connectSyncHost(): void {131  syncPort = chrome.runtime.connectNative("com.peachpasswords.sync");132  syncPort.onMessage.addListener(handleHostMessage);133  syncPort.onDisconnect.addListener(() => {134    syncPort = null;135    if (pendingConnect) {136      const { reject } = pendingConnect;137      pendingConnect = null;138      reject(new Error("Host disconnected"));139    }140    if (pendingDiscover) {141      const { reject } = pendingDiscover;142      pendingDiscover = null;143      reject(new Error("Host disconnected"));144    }145    if (pendingStatus) {146      const { reject } = pendingStatus;147      pendingStatus = null;148      reject(new Error("Host disconnected"));149    }150    for (const handler of disconnectedHandlers) handler();151  });152}153154export function disconnectSyncHost(): void {155  syncPort?.disconnect();156  syncPort = null;157}158159export function isHostConnected(): boolean {160  return syncPort !== null;161}162163export function advertise(164  port: number,165  x25519Pubkey: string,166  ed25519Pubkey: string,167  name: string,168  pairing: boolean,169): void {170  sendToHost({ type: "ADVERTISE", port, x25519Pubkey, ed25519Pubkey, name, pairing });171}172173export function stopAdvertising(): void {174  sendToHost({ type: "STOP_ADVERTISING" });175}176177export function discoverPeers(): Promise<HostResponse> {178  return new Promise((resolve, reject) => {179    pendingDiscover = { resolve, reject };180    sendToHost({ type: "DISCOVER" });181  });182}183184export function connectToPeer(host: string, port: number): Promise<void> {185  return new Promise((resolve, reject) => {186    pendingConnect = { resolve, reject };187    sendToHost({ type: "CONNECT", host, port });188  });189}190191export function disconnectFromPeer(): void {192  sendToHost({ type: "DISCONNECT" });193}194195export function sendHandshakeStart(isInitiator: boolean, remoteStaticKey: string): void {196  sendToHost({ type: "HANDSHAKE_START", isInitiator, remoteStaticKey });197}198199export function sendHandshakeData(data: string): void {200  sendToHost({ type: "HANDSHAKE_DATA", data });201}202203export function sendSyncMessage(message: string): void {204  sendToHost({ type: "SYNC_SEND", message });205}206207export function requestSyncMessage(): void {208  sendToHost({ type: "SYNC_RECEIVE" });209}210211export function getNetworkStatus(): Promise<HostResponse> {212  return new Promise((resolve, reject) => {213    pendingStatus = { resolve, reject };214    sendToHost({ type: "STATUS", isTrustedNetwork: false, isTailscale: false });215  });216}217218export function onPeerDiscovered(handler: (peer: PeerInfo) => void): void {219  peerDiscoveredHandlers.push(handler);220}221222export function onConnected(handler: () => void): void {223  connectedHandlers.push(handler);224}225226export function onDisconnected(handler: () => void): void {227  disconnectedHandlers.push(handler);228}229230export function onHandshakeResult(handler: (success: boolean, data?: string) => void): void {231  handshakeResultHandlers.push(handler);232}233234export function onSyncMessage(handler: (message: string) => void): void {235  syncMessageHandlers.push(handler);236}237238export function onError(handler: (error: string) => void): void {239  errorHandlers.push(handler);240}