12345678910type 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}