sync.ts

6.9 KB · raw

1type HostMessage =2  | { type: "ADVERTISE"; port: number; x25519Pubkey: string; ed25519Pubkey: string; name: string; pairing: boolean }3  | { type: "STOP_ADVERTISING" }4  | { type: "DISCOVER" }5  | { type: "CONNECT"; host: string; port: number }6  | { type: "DISCONNECT" }7  | { type: "HANDSHAKE_START"; isInitiator: boolean; remoteStaticKey: string }8  | { type: "HANDSHAKE_DATA"; data: string }9  | { type: "SYNC_SEND"; message: string }10  | { type: "SYNC_RECEIVE" }11  | { type: "STATUS"; isTrustedNetwork: boolean; isTailscale: boolean; ssid?: string };1213type HostResponse =14  | { type: "DISCOVERED"; host: string; port: number; name: string; pairing: boolean; x25519Pubkey: string }15  | { type: "CONNECTED" }16  | { type: "DISCONNECTED" }17  | { type: "HANDSHAKE_RESULT"; success: boolean; data?: string }18  | { type: "SYNC_MESSAGE"; message: string }19  | { type: "STATUS"; isTrustedNetwork: boolean; isTailscale: boolean; ssid?: string }20  | { type: "ERROR"; message: string };2122export interface PeerInfo {23  host: string;24  port: number;25  name: string;26  pairing: boolean;27  x25519Pubkey: string;28}2930let syncPort: chrome.runtime.Port | null = null;3132const peerDiscoveredHandlers: Array<(peer: PeerInfo) => void> = [];33const connectedHandlers: Array<() => void> = [];34const disconnectedHandlers: Array<() => void> = [];35const handshakeResultHandlers: Array<(success: boolean, data?: string) => void> = [];36const syncMessageHandlers: Array<(message: string) => void> = [];37const errorHandlers: Array<(error: string) => void> = [];3839let pendingDiscover: { resolve: (r: HostResponse) => void; reject: (e: Error) => void } | null = null;40let pendingConnect: { resolve: () => void; reject: (e: Error) => void } | null = null;41let pendingStatus: { resolve: (r: HostResponse) => void; reject: (e: Error) => void } | null = null;4243function handleHostMessage(response: HostResponse): void {44  switch (response.type) {45    case "DISCOVERED": {46      const peer: PeerInfo = {47        host: response.host,48        port: response.port,49        name: response.name,50        pairing: response.pairing,51        x25519Pubkey: response.x25519Pubkey,52      };53      if (pendingDiscover) {54        const { resolve } = pendingDiscover;55        pendingDiscover = null;56        resolve(response);57      }58      for (const handler of peerDiscoveredHandlers) handler(peer);59      break;60    }61    case "CONNECTED": {62      if (pendingConnect) {63        const { resolve } = pendingConnect;64        pendingConnect = null;65        resolve();66      }67      for (const handler of connectedHandlers) handler();68      break;69    }70    case "DISCONNECTED": {71      if (pendingConnect) {72        const { reject } = pendingConnect;73        pendingConnect = null;74        reject(new Error("Peer disconnected during connect"));75      }76      for (const handler of disconnectedHandlers) handler();77      break;78    }79    case "HANDSHAKE_RESULT": {80      for (const handler of handshakeResultHandlers) handler(response.success, response.data);81      break;82    }83    case "SYNC_MESSAGE": {84      for (const handler of syncMessageHandlers) handler(response.message);85      break;86    }87    case "STATUS": {88      if (pendingStatus) {89        const { resolve } = pendingStatus;90        pendingStatus = null;91        resolve(response);92      }93      break;94    }95    case "ERROR": {96      if (pendingDiscover) {97        const { reject } = pendingDiscover;98        pendingDiscover = null;99        reject(new Error(response.message));100      }101      if (pendingConnect) {102        const { reject } = pendingConnect;103        pendingConnect = null;104        reject(new Error(response.message));105      }106      if (pendingStatus) {107        const { reject } = pendingStatus;108        pendingStatus = null;109        reject(new Error(response.message));110      }111      for (const handler of errorHandlers) handler(response.message);112      break;113    }114  }115}116117function sendToHost(msg: HostMessage): void {118  syncPort?.postMessage(msg);119}120121export function connectSyncHost(): void {122  syncPort = chrome.runtime.connectNative("com.peachpasswords.sync");123  syncPort.onMessage.addListener(handleHostMessage);124  syncPort.onDisconnect.addListener(() => {125    syncPort = null;126    if (pendingConnect) {127      const { reject } = pendingConnect;128      pendingConnect = null;129      reject(new Error("Host disconnected"));130    }131    if (pendingDiscover) {132      const { reject } = pendingDiscover;133      pendingDiscover = null;134      reject(new Error("Host disconnected"));135    }136    if (pendingStatus) {137      const { reject } = pendingStatus;138      pendingStatus = null;139      reject(new Error("Host disconnected"));140    }141    for (const handler of disconnectedHandlers) handler();142  });143}144145export function disconnectSyncHost(): void {146  syncPort?.disconnect();147  syncPort = null;148}149150export function isHostConnected(): boolean {151  return syncPort !== null;152}153154export function advertise(155  port: number,156  x25519Pubkey: string,157  ed25519Pubkey: string,158  name: string,159  pairing: boolean,160): void {161  sendToHost({ type: "ADVERTISE", port, x25519Pubkey, ed25519Pubkey, name, pairing });162}163164export function stopAdvertising(): void {165  sendToHost({ type: "STOP_ADVERTISING" });166}167168export function discoverPeers(): Promise<HostResponse> {169  return new Promise((resolve, reject) => {170    pendingDiscover = { resolve, reject };171    sendToHost({ type: "DISCOVER" });172  });173}174175export function connectToPeer(host: string, port: number): Promise<void> {176  return new Promise((resolve, reject) => {177    pendingConnect = { resolve, reject };178    sendToHost({ type: "CONNECT", host, port });179  });180}181182export function disconnectFromPeer(): void {183  sendToHost({ type: "DISCONNECT" });184}185186export function sendHandshakeStart(isInitiator: boolean, remoteStaticKey: string): void {187  sendToHost({ type: "HANDSHAKE_START", isInitiator, remoteStaticKey });188}189190export function sendHandshakeData(data: string): void {191  sendToHost({ type: "HANDSHAKE_DATA", data });192}193194export function sendSyncMessage(message: string): void {195  sendToHost({ type: "SYNC_SEND", message });196}197198export function requestSyncMessage(): void {199  sendToHost({ type: "SYNC_RECEIVE" });200}201202export function getNetworkStatus(): Promise<HostResponse> {203  return new Promise((resolve, reject) => {204    pendingStatus = { resolve, reject };205    sendToHost({ type: "STATUS", isTrustedNetwork: false, isTailscale: false });206  });207}208209export function onPeerDiscovered(handler: (peer: PeerInfo) => void): void {210  peerDiscoveredHandlers.push(handler);211}212213export function onConnected(handler: () => void): void {214  connectedHandlers.push(handler);215}216217export function onDisconnected(handler: () => void): void {218  disconnectedHandlers.push(handler);219}220221export function onHandshakeResult(handler: (success: boolean, data?: string) => void): void {222  handshakeResultHandlers.push(handler);223}224225export function onSyncMessage(handler: (message: string) => void): void {226  syncMessageHandlers.push(handler);227}228229export function onError(handler: (error: string) => void): void {230  errorHandlers.push(handler);231}