1import type { PairedDevice } from "../shared/sync-types";2import {3 isRouteCapableMeshPeer,4 MESH_WAITER_RETRY_DELAY_MS,5 nextMeshWaiterRetryDelay,6 selectDialTargets,7 weDialMeshPeer,8} from "../shared/mesh-peer-role";9import {10 openRelaySyncResponderSession,11 type SyncPeerIdentity,12} from "../shared/sync-transport";13import {14 rendezvousSecretFromHex,15 waitingRendezvousIds,16} from "../shared/rendezvous";17import { runSyncResponder } from "./sync-coordinator";18import { createSyncVaultAdapter } from "./sync-vault-adapter";19import {20 createLockedSyncVaultAdapter,21 getLockedSyncIdentity,22} from "./locked-sync-spool";23import { getLocalPairingIdentity } from "./device-identity";24import {25 findPairedDeviceById,26 listPairedDevices,27 markLastSyncedAt,28} from "./paired-devices";29import { getSyncSettings, relayInForce } from "./sync-settings";30import { isUnlocked, isDecoyMode, getRootKeyHex } from "./vault";31import { getPairSecret } from "./pair-secrets";32import { recordInboundSyncCompletion } from "./sync-trigger";3334const RECONCILE_INTERVAL_MS = 60_000;3536interface MeshIdentity {37 identity: Omit<SyncPeerIdentity, "longTermPrivateKey">;38 privateKey: Uint8Array;39 publicKey: Uint8Array;40 signingPrivateKey?: CryptoKey;41}4243interface WaiterHandle {44 abort: AbortController;45}4647interface MeshListenerState {48 installed: boolean;49 reconcileQueued: boolean;50 timer: ReturnType<typeof setTimeout> | null;51 waiters: Map<string, WaiterHandle>;52 activeSessions: Set<{ close(): Promise<void> }>;53}5455function state(): MeshListenerState {56 const global = globalThis as typeof globalThis & {57 __peachMeshListenerState?: MeshListenerState;58 };59 if (!global.__peachMeshListenerState) {60 global.__peachMeshListenerState = {61 installed: false,62 reconcileQueued: false,63 timer: null,64 waiters: new Map(),65 activeSessions: new Set(),66 };67 }68 return global.__peachMeshListenerState;69}7071export async function selectProductionDialTargets(72 devices: PairedDevice[],73): Promise<PairedDevice[]> {74 const local = await getMeshIdentity();75 if (!local) return devices;76 try {77 return selectDialTargets(78 devices,79 local.identity.longTermPublicKeyHex,80 );81 } finally {82 local.privateKey.fill(0);83 local.publicKey.fill(0);84 }85}8687export function setupMeshRelayListener(): void {88 const listener = state();89 if (listener.installed) return;90 listener.installed = true;91 chrome.storage?.onChanged?.addListener(() => requestMeshRelayReconcile());92 requestMeshRelayReconcile();93}9495export function abortMeshRelaySessions(): void {96 const listener = state();97 abortAll(listener);98 for (const session of listener.activeSessions) void session.close().catch(() => undefined);99 listener.activeSessions.clear();100}101102export function requestMeshRelayReconcile(): void {103 const listener = state();104 if (listener.reconcileQueued) return;105 listener.reconcileQueued = true;106 queueMicrotask(() => {107 listener.reconcileQueued = false;108 void reconcile().catch(() => undefined);109 });110}111112async function reconcile(): Promise<void> {113 const listener = state();114 if (isDecoyMode()) {115 abortAll(listener);116 return;117 }118 if (listener.timer !== null) clearTimeout(listener.timer);119 listener.timer = setTimeout(requestMeshRelayReconcile, RECONCILE_INTERVAL_MS);120121 const local = await getMeshIdentity();122 if (!local) { abortAll(listener); return; }123 const desired = new Map<string, {124 peer: PairedDevice;125 relayUrl: string;126 rendezvousId: string;127 }>();128 try {129 const settings = await getSyncSettings();130 if (isDecoyMode()) { abortAll(listener); return; }131 const fallbackRelay = relayInForce(settings);132 const nowSeconds = Date.now() / 1000;133 for (const peer of await listPairedDevices()) {134 if (isDecoyMode()) { abortAll(listener); return; }135 if (136 !isRouteCapableMeshPeer(peer) ||137 weDialMeshPeer(138 local.identity.longTermPublicKeyHex,139 peer.longTermPublicKeyHex!,140 )141 ) {142 continue;143 }144 const relayUrl = peer.relayUrl ?? fallbackRelay;145 146 147 const secretHex = await getPairSecret(peer.deviceId, isUnlocked() ? getRootKeyHex() : null);148 if (!secretHex) continue;149 const secret = rendezvousSecretFromHex(secretHex);150 for (const rendezvousId of waitingRendezvousIds(secret, nowSeconds)) {151 desired.set(`${peer.deviceId}|${relayUrl}|${rendezvousId}`, {152 peer,153 relayUrl,154 rendezvousId,155 });156 }157 }158 } finally {159 local.privateKey.fill(0);160 local.publicKey.fill(0);161 }162163 for (const [key, handle] of listener.waiters) {164 if (desired.has(key)) continue;165 handle.abort.abort();166 listener.waiters.delete(key);167 }168 for (const [key, target] of desired) {169 if (listener.waiters.has(key)) continue;170 const abort = new AbortController();171 listener.waiters.set(key, { abort });172 void holdWaiter(target, abort.signal).finally(() => {173 if (listener.waiters.get(key)?.abort === abort) {174 listener.waiters.delete(key);175 }176 });177 }178}179180async function holdWaiter(181 target: { peer: PairedDevice; relayUrl: string; rendezvousId: string },182 signal: AbortSignal,183): Promise<void> {184 let retryDelayMs = MESH_WAITER_RETRY_DELAY_MS;185 while (!signal.aborted && !isDecoyMode()) {186 const current = await findPairedDeviceById(target.peer.deviceId);187 if (isDecoyMode()) return;188 if (!current || !isRouteCapableMeshPeer(current)) return;189 let session: Awaited<ReturnType<typeof openRelaySyncResponderSession>> | null = null;190 let local: MeshIdentity | null = null;191 let succeeded = false;192 try {193 local = await getMeshIdentity();194 if (!local) return;195 const identity: SyncPeerIdentity = {196 ...local.identity,197 longTermPrivateKey: local.privateKey,198 };199 session = await openRelaySyncResponderSession(200 target.relayUrl,201 identity,202 current.longTermPublicKeyHex!,203 target.rendezvousId,204 { signal },205 );206 state().activeSessions.add(session);207 if (isDecoyMode()) {208 state().activeSessions.delete(session);209 await session.close().catch(() => undefined);210 return;211 }212 const adapter = isUnlocked()213 ? createSyncVaultAdapter({ deviceName: current.deviceName })214 : await createLockedSyncVaultAdapter();215 if (!adapter || isDecoyMode()) return;216 const outcome = await runSyncResponder(session, adapter);217 if (isDecoyMode()) return;218 if (outcome.status === "completed") {219 if (isDecoyMode()) return;220 await markLastSyncedAt(current.deviceId, Date.now());221 recordInboundSyncCompletion(outcome.entriesReceived, outcome.entriesSent);222 succeeded = true;223 }224 if (!isDecoyMode()) requestMeshRelayReconcile();225 } catch {226 } finally {227 if (session) state().activeSessions.delete(session);228 local?.privateKey.fill(0);229 local?.publicKey.fill(0);230 await session?.close().catch(() => undefined);231 }232 await waitForRetry(signal, retryDelayMs);233 retryDelayMs = nextMeshWaiterRetryDelay(retryDelayMs, succeeded);234 }235}236237async function getMeshIdentity(): Promise<{238 identity: Omit<SyncPeerIdentity, "longTermPrivateKey">;239 privateKey: Uint8Array;240 publicKey: Uint8Array;241 signingPrivateKey?: CryptoKey;242} | null> {243 if (isUnlocked()) return getLocalPairingIdentity();244 const cached = await getLockedSyncIdentity();245 if (!cached) return null;246 return {247 identity: {248 deviceId: cached.deviceId,249 deviceName: cached.deviceName,250 deviceType: cached.deviceType,251 longTermPublicKeyHex: cached.longTermPublicKeyHex,252 },253 privateKey: cached.longTermPrivateKey,254 publicKey: hexToBytes(cached.longTermPublicKeyHex),255 };256}257258function hexToBytes(hex: string): Uint8Array {259 const out = new Uint8Array(hex.length / 2);260 for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);261 return out;262}263264function waitForRetry(signal: AbortSignal, delayMs: number): Promise<void> {265 if (signal.aborted) return Promise.resolve();266 return new Promise((resolve) => {267 const onAbort = () => {268 clearTimeout(timer);269 resolve();270 };271 const timer = setTimeout(() => {272 signal.removeEventListener("abort", onAbort);273 resolve();274 }, delayMs);275 signal.addEventListener("abort", onAbort, { once: true });276 });277}278279function abortAll(listener: MeshListenerState): void {280 for (const handle of listener.waiters.values()) handle.abort.abort();281 listener.waiters.clear();282 for (const session of listener.activeSessions) void session.close().catch(() => undefined);283 listener.activeSessions.clear();284}