123456789101112131415161718192021import { runSyncInitiator, type SyncVaultAdapter } from "./sync-coordinator";22import { createSyncVaultAdapter } from "./sync-vault-adapter";23import {24 getLastKnownIp,25 setLastKnownIp,26} from "./paired-devices";27import {28 openSyncSession,29 probeSubnetForPeer,30 type SyncSession,31 type SyncEndpoint,32 type SyncPeerIdentity,33} from "../shared/sync-transport";34import { hexToPubkey } from "../shared/sync-identity";35import { getLocalSubnet } from "../shared/local-network";36import { buildLocalIdentity } from "./device-identity";37import { PAIRING_PORT } from "../shared/pairing-dial";38import { getRootKeyHex, isUnlocked } from "./vault";39import type { PairedDevice } from "../shared/sync-types";40import type { SyncRunResult } from "./sync-trigger";414243const DEFAULT_PORT = PAIRING_PORT;444546474849export interface SyncRunnerDeps {50 openSession: (51 endpoint: SyncEndpoint,52 ourIdentity: SyncPeerIdentity,53 expectedRemoteHex: string,54 ) => Promise<SyncSession>;55 probeSubnet: (56 subnet: string,57 port: number,58 ourIdentity: SyncPeerIdentity,59 expectedRemoteHex: string,60 ) => Promise<SyncEndpoint | null>;61 getSubnet: () => Promise<string | null>;62 createAdapter: (context?: { deviceName?: string }) => SyncVaultAdapter;63}6465const productionDeps: SyncRunnerDeps = {66 openSession: (endpoint, identity, expected) =>67 openSyncSession(endpoint, identity, expected),68 probeSubnet: (subnet, port, identity, expected) =>69 probeSubnetForPeer(subnet, port, identity, expected),70 getSubnet: getLocalSubnet,71 createAdapter: (context) => createSyncVaultAdapter(context),72};7374export async function productionSyncRunner(device: PairedDevice): Promise<SyncRunResult> {75 return runWithDeps(device, productionDeps);76}7778798081export async function runWithDeps(82 device: PairedDevice,83 deps: SyncRunnerDeps,84): Promise<SyncRunResult> {85 if (!isUnlocked()) return { ok: false, error: "VAULT_LOCKED" };86 if (!device.longTermPublicKeyHex) {87 throw new Error("paired_device_missing_pubkey");88 }89 const expectedRemoteHex = device.longTermPublicKeyHex;90 const port = device.wsPort ?? DEFAULT_PORT;9192 93 94 95 96 const masterKeyHex = getRootKeyHex();97 if (!masterKeyHex) return { ok: false, error: "VAULT_LOCKED" };98 const masterKey = hexToBytesLocal(masterKeyHex);99 const ourIdentity = await buildLocalIdentity(masterKey);100101 102 void hexToPubkey(expectedRemoteHex);103104 105 const adapter = deps.createAdapter({ deviceName: device.deviceName });106 const session = await connect(expectedRemoteHex, port, ourIdentity, deps);107108 try {109 const outcome = await runSyncInitiator(session, adapter);110 if (outcome.status === "completed") {111 return {112 ok: true,113 entriesReceived: outcome.entriesReceived,114 entriesSent: outcome.entriesSent,115 };116 }117 if (outcome.status === "vault_mismatch") {118 throw new Error("vault_id_mismatch");119 }120 if (outcome.status === "timeout") {121 throw new Error("sync_timeout");122 }123 throw new Error(outcome.error ?? "sync_errored");124 } finally {125 126 127 await session.close().catch(() => undefined);128 }129}130131async function connect(132 expectedRemoteHex: string,133 port: number,134 ourIdentity: SyncPeerIdentity,135 deps: SyncRunnerDeps,136): Promise<SyncSession> {137 const cached = await getLastKnownIp(expectedRemoteHex);138 if (cached) {139 try {140 const session = await deps.openSession(141 { ip: cached, port },142 ourIdentity,143 expectedRemoteHex,144 );145 return session;146 } catch {147 148 }149 }150151 const subnet = await deps.getSubnet();152 if (!subnet) {153 throw new Error(cached ? "host_unreachable" : "subnet_unknown");154 }155156 const found = await deps.probeSubnet(subnet, port, ourIdentity, expectedRemoteHex);157 if (!found) throw new Error("host_unreachable");158159 const session = await deps.openSession(found, ourIdentity, expectedRemoteHex);160 await setLastKnownIp(expectedRemoteHex, found.ip).catch(() => undefined);161 return session;162}163164function hexToBytesLocal(hex: string): Uint8Array {165 const out = new Uint8Array(hex.length / 2);166 for (let i = 0; i < out.length; i++) {167 out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);168 }169 return out;170}