123456789101112131415161718import { runSyncInitiator, type SyncVaultAdapter } from "./sync-coordinator";19import { createSyncVaultAdapter } from "./sync-vault-adapter";20import {21 openRelaySyncSession,22 type SyncSession,23 type SyncPeerIdentity,24} from "../shared/sync-transport";25import { hexToPubkey } from "../shared/sync-identity";26import { buildLocalIdentity, deriveLocalSyncKeypair } from "./device-identity";27import { constantTimeEqualHex } from "../shared/constant-time";28import { getRootKeyHex, isUnlocked } from "./vault";29import type { PairedDevice } from "../shared/sync-types";30import type { SyncRunResult } from "./sync-trigger";31import {32 dialingRendezvousIds,33 rendezvousSecretFromHex,34} from "../shared/rendezvous";35import { getSyncSettings, relayInForce, type SyncSettings } from "./sync-settings";363738394041export interface SyncRunnerDeps {42 openRelaySession: (43 relayUrl: string,44 ourIdentity: SyncPeerIdentity,45 expectedRemoteHex: string,46 rendezvousId: string,47 ) => Promise<SyncSession>;48 getSyncSettings: () => Promise<SyncSettings>;49 createAdapter: (context?: { deviceName?: string }) => SyncVaultAdapter;50}5152const productionDeps: SyncRunnerDeps = {53 openRelaySession: (relayUrl, identity, expected, rendezvousId) =>54 openRelaySyncSession(relayUrl, identity, expected, rendezvousId),55 getSyncSettings,56 createAdapter: (context) => createSyncVaultAdapter(context),57};5859export async function productionSyncRunner(device: PairedDevice): Promise<SyncRunResult> {60 return runWithDeps(device, productionDeps);61}6263646566export async function runWithDeps(67 device: PairedDevice,68 deps: SyncRunnerDeps,69): Promise<SyncRunResult> {70 if (!isUnlocked()) return { ok: false, error: "VAULT_LOCKED" };71 if (!device.longTermPublicKeyHex) {72 throw new Error("paired_device_missing_pubkey");73 }74 if (!device.rendezvousSecretHex) {75 throw new Error("paired_device_missing_rendezvous_secret");76 }77 const expectedRemoteHex = device.longTermPublicKeyHex;78 const reconnectIds = dialingRendezvousIds(79 rendezvousSecretFromHex(device.rendezvousSecretHex),80 Date.now() / 1000,81 );82 83 84 const masterKeyHex = getRootKeyHex();85 if (!masterKeyHex) return { ok: false, error: "VAULT_LOCKED" };86 const masterKey = hexToBytesLocal(masterKeyHex);87 const publicIdentity = await buildLocalIdentity(masterKey);88 const localKeys = deriveLocalSyncKeypair(masterKey, publicIdentity.deviceId);89 if (!constantTimeEqualHex(90 publicIdentity.longTermPublicKeyHex,91 bytesToHex(localKeys.publicKey),92 )) {93 masterKey.fill(0);94 localKeys.secretKey.fill(0);95 throw new Error("derived reconnect identity mismatch");96 }97 const ourIdentity: SyncPeerIdentity = {98 ...publicIdentity,99 longTermPrivateKey: localKeys.secretKey,100 };101102 103 void hexToPubkey(expectedRemoteHex);104105 106 const adapter = deps.createAdapter({ deviceName: device.deviceName });107 let session: SyncSession;108 try {109 const settings = await deps.getSyncSettings();110 const configuredRelay = relayInForce(settings);111 session = await connectRelay(112 device.relayUrl ?? configuredRelay,113 expectedRemoteHex,114 ourIdentity,115 reconnectIds,116 deps.openRelaySession,117 );118 } finally {119 masterKey.fill(0);120 localKeys.secretKey.fill(0);121 }122123 try {124 const outcome = await runSyncInitiator(session, adapter);125 if (outcome.status !== "completed") {126 console.info(127 `[Peach sync] protocol ended ${outcome.status} with ${device.deviceName ?? device.deviceId}${128 outcome.error ? `: ${outcome.error}` : ""129 }`,130 );131 }132 if (outcome.status === "completed") {133 return {134 ok: true,135 entriesReceived: outcome.entriesReceived,136 entriesSent: outcome.entriesSent,137 };138 }139 if (outcome.status === "vault_mismatch") {140 throw new Error("vault_id_mismatch");141 }142 if (outcome.status === "timeout") {143 throw new Error(outcome.error ?? "sync_timeout");144 }145 throw new Error(outcome.error ?? "sync_errored");146 } finally {147 148 149 await session.close().catch(() => undefined);150 }151}152153async function connectRelay(154 relayUrl: string,155 expectedRemoteHex: string,156 ourIdentity: SyncPeerIdentity,157 rendezvousIds: string[],158 openRelaySession: SyncRunnerDeps["openRelaySession"],159): Promise<SyncSession> {160 let lastError: unknown;161 for (const rendezvousId of rendezvousIds) {162 try {163 return await openRelaySession(164 relayUrl,165 ourIdentity,166 expectedRemoteHex,167 rendezvousId,168 );169 } catch (error) {170 lastError = error;171 172 173 174 175 console.info(176 `[Peach sync] relay dial failed — reason=${177 error instanceof Error ? error.message : String(error)178 }`,179 );180 }181 }182 const detail = lastError instanceof Error ? lastError.message : "unreachable";183 throw new Error(`relay_sync_failed:${detail}`);184}185186function hexToBytesLocal(hex: string): Uint8Array {187 const out = new Uint8Array(hex.length / 2);188 for (let i = 0; i < out.length; i++) {189 out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);190 }191 return out;192}193194function bytesToHex(bytes: Uint8Array): string {195 return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");196}