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}