/** * Dormant SYN-001 coordinator helpers. * * This module owns extension-side sync preflight and cancellation rules before * any live background path enables native messaging. Entry payload exchange is * intentionally left to the next phase so the encrypted-entry envelope can be * reviewed on its own. */ import { buildSyncManifest, diffSyncManifests, selectEntriesForSync, } from "../shared/sync-protocol"; import type { SyncDiff } from "../shared/sync-protocol"; import { decryptSyncEntry, encryptSyncEntry, importSyncEntryKey, } from "../shared/sync-entry-payload"; import type { SyncManifestEntry, SyncMessage } from "../shared/sync-types"; import { encodeSyncMessage } from "../shared/sync-wire"; import type { VaultEntry } from "../shared/types"; export type SyncPreflightError = "pro_required" | "vault_locked"; export interface SyncPreflightResult { ok: boolean; error?: SyncPreflightError; } export interface SyncSnapshot { entries: VaultEntry[]; manifest: SyncManifestEntry[]; } export interface SyncPlan { localManifest: SyncManifestEntry[]; diff: SyncDiff; } export interface SyncCoordinatorDeps { isProUser: () => Promise; isUnlocked: () => boolean; getEntries: () => VaultEntry[]; restoreEntries: ( entries: VaultEntry[], mode: "replace" | "merge" ) => Promise<{ added: number; updated: number; skipped: number }>; sendSyncData: (connectionId: string, data: string) => void; } async function loadDefaultDeps(): Promise { const [license, sync, vault] = await Promise.all([ import("./license"), import("./sync"), import("./vault"), ]); return { isProUser: license.isProUser, isUnlocked: vault.isUnlocked, getEntries: vault.getEntries, restoreEntries: vault.restoreEntries, sendSyncData: sync.sendSyncData, }; } export async function checkSyncPreflight( deps?: Pick ): Promise { const readyDeps = deps ?? (await loadDefaultDeps()); if (!(await readyDeps.isProUser())) return { ok: false, error: "pro_required" }; if (!readyDeps.isUnlocked()) return { ok: false, error: "vault_locked" }; return { ok: true }; } export async function buildLocalSyncSnapshot( deps?: Pick ): Promise { const readyDeps = deps ?? (await loadDefaultDeps()); const preflight = await checkSyncPreflight(readyDeps); if (!preflight.ok) throw new Error(preflight.error); const entries = readyDeps.getEntries(); return { entries, manifest: await buildSyncManifest(entries), }; } export async function planManifestSync( remoteManifest: SyncManifestEntry[], deps?: Pick ): Promise { const snapshot = await buildLocalSyncSnapshot(deps); return { localManifest: snapshot.manifest, diff: diffSyncManifests(snapshot.manifest, remoteManifest), }; } export async function buildSyncEntriesMessage( ids: string[], rawEntryKey: Uint8Array, deps?: Pick ): Promise> { const readyDeps = deps ?? (await loadDefaultDeps()); const preflight = await checkSyncPreflight(readyDeps); if (!preflight.ok) throw new Error(preflight.error); const key = await importSyncEntryKey(rawEntryKey); const selected = selectEntriesForSync(readyDeps.getEntries(), ids); return { type: "SYNC_ENTRIES", entries: await Promise.all(selected.map((entry) => encryptSyncEntry(entry, key))), }; } export async function applySyncEntriesMessage( message: Extract, rawEntryKey: Uint8Array, deps?: Pick ): Promise<{ added: number; updated: number; skipped: number }> { const readyDeps = deps ?? (await loadDefaultDeps()); const preflight = await checkSyncPreflight(readyDeps); if (!preflight.ok) throw new Error(preflight.error); const key = await importSyncEntryKey(rawEntryKey); const entries = await Promise.all( message.entries.map((payload) => decryptSyncEntry(payload, key)) ); return readyDeps.restoreEntries(entries, "merge"); } export async function cancelSyncIfVaultLocked( connectionId: string, deps?: Pick ): Promise { const readyDeps = deps ?? (await loadDefaultDeps()); if (readyDeps.isUnlocked()) return false; readyDeps.sendSyncData( connectionId, encodeSyncMessage({ type: "SYNC_ERROR", code: "vault_locked", message: "Vault is locked; unlock Peach before syncing.", }) ); return true; }