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} from "../shared/sync-transport";33import { deriveSyncIdentityKeypair, hexToPubkey } from "../shared/sync-identity";34import { getLocalSubnet } from "../shared/local-network";35import { getOrCreateDeviceId } from "./device-identity";36import { getRootKeyHex, isUnlocked } from "./vault";37import type { PairedDevice } from "../shared/sync-types";38import type { SyncRunResult } from "./sync-trigger";3940const DEFAULT_PORT = 8765;414243444546export interface SyncRunnerDeps {47 openSession: (48 endpoint: SyncEndpoint,49 ourPriv: Uint8Array,50 ourPub: Uint8Array,51 expectedRemoteHex: string,52 ) => Promise<SyncSession>;53 probeSubnet: (54 subnet: string,55 port: number,56 ourPriv: Uint8Array,57 ourPub: Uint8Array,58 expectedRemoteHex: string,59 ) => Promise<SyncEndpoint | null>;60 getSubnet: () => Promise<string | null>;61 createAdapter: (context?: { deviceName?: string }) => SyncVaultAdapter;62}6364const productionDeps: SyncRunnerDeps = {65 openSession: (endpoint, priv, pub, expected) =>66 openSyncSession(endpoint, priv, pub, expected),67 probeSubnet: (subnet, port, priv, pub, expected) =>68 probeSubnetForPeer(subnet, port, priv, pub, expected),69 getSubnet: getLocalSubnet,70 createAdapter: (context) => createSyncVaultAdapter(context),71};7273export async function productionSyncRunner(device: PairedDevice): Promise<SyncRunResult> {74 return runWithDeps(device, productionDeps);75}7677787980export async function runWithDeps(81 device: PairedDevice,82 deps: SyncRunnerDeps,83): Promise<SyncRunResult> {84 if (!isUnlocked()) return { ok: false, error: "VAULT_LOCKED" };85 if (!device.longTermPublicKeyHex) {86 throw new Error("paired_device_missing_pubkey");87 }88 const expectedRemoteHex = device.longTermPublicKeyHex;89 const port = device.wsPort ?? DEFAULT_PORT;9091 92 93 const masterKeyHex = getRootKeyHex();94 if (!masterKeyHex) return { ok: false, error: "VAULT_LOCKED" };95 const masterKey = hexToBytesLocal(masterKeyHex);96 const ourDeviceId = await getOrCreateDeviceId();97 const ourKeypair = deriveSyncIdentityKeypair(masterKey, ourDeviceId);9899 100 void hexToPubkey(expectedRemoteHex);101102 103 const adapter = deps.createAdapter({ deviceName: device.deviceName });104 const session = await connect(device, expectedRemoteHex, port, ourKeypair, deps);105106 try {107 const outcome = await runSyncInitiator(session, adapter);108 if (outcome.status === "completed") {109 return {110 ok: true,111 entriesReceived: outcome.entriesReceived,112 entriesSent: outcome.entriesSent,113 };114 }115 if (outcome.status === "vault_mismatch") {116 throw new Error("vault_id_mismatch");117 }118 if (outcome.status === "timeout") {119 throw new Error("sync_timeout");120 }121 throw new Error(outcome.error ?? "sync_errored");122 } finally {123 124 125 await session.close().catch(() => undefined);126 }127}128129async function connect(130 _device: PairedDevice,131 expectedRemoteHex: string,132 port: number,133 ourKeypair: { secretKey: Uint8Array; publicKey: Uint8Array },134 deps: SyncRunnerDeps,135): Promise<SyncSession> {136 const cached = await getLastKnownIp(expectedRemoteHex);137 if (cached) {138 try {139 const session = await deps.openSession(140 { ip: cached, port },141 ourKeypair.secretKey,142 ourKeypair.publicKey,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(157 subnet,158 port,159 ourKeypair.secretKey,160 ourKeypair.publicKey,161 expectedRemoteHex,162 );163 if (!found) throw new Error("host_unreachable");164165 const session = await deps.openSession(166 found,167 ourKeypair.secretKey,168 ourKeypair.publicKey,169 expectedRemoteHex,170 );171 await setLastKnownIp(expectedRemoteHex, found.ip).catch(() => undefined);172 return session;173}174175function hexToBytesLocal(hex: string): Uint8Array {176 const out = new Uint8Array(hex.length / 2);177 for (let i = 0; i < out.length; i++) {178 out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);179 }180 return out;181}