sync-coordinator.ts

3.1 KB · raw

1/**2 * Dormant SYN-001 coordinator helpers.3 *4 * This module owns extension-side sync preflight and cancellation rules before5 * any live background path enables native messaging. Entry payload exchange is6 * intentionally left to the next phase so the encrypted-entry envelope can be7 * reviewed on its own.8 */910import { buildSyncManifest, diffSyncManifests } from "../shared/sync-protocol";11import type { SyncDiff } from "../shared/sync-protocol";12import type { SyncManifestEntry } from "../shared/sync-types";13import { encodeSyncMessage } from "../shared/sync-wire";14import type { VaultEntry } from "../shared/types";1516export type SyncPreflightError = "pro_required" | "vault_locked";1718export interface SyncPreflightResult {19  ok: boolean;20  error?: SyncPreflightError;21}2223export interface SyncSnapshot {24  entries: VaultEntry[];25  manifest: SyncManifestEntry[];26}2728export interface SyncPlan {29  localManifest: SyncManifestEntry[];30  diff: SyncDiff;31}3233export interface SyncCoordinatorDeps {34  isProUser: () => Promise<boolean>;35  isUnlocked: () => boolean;36  getEntries: () => VaultEntry[];37  sendSyncData: (connectionId: string, data: string) => void;38}3940async function loadDefaultDeps(): Promise<SyncCoordinatorDeps> {41  const [license, sync, vault] = await Promise.all([42    import("./license"),43    import("./sync"),44    import("./vault"),45  ]);46  return {47    isProUser: license.isProUser,48    isUnlocked: vault.isUnlocked,49    getEntries: vault.getEntries,50    sendSyncData: sync.sendSyncData,51  };52}5354export async function checkSyncPreflight(55  deps?: Pick<SyncCoordinatorDeps, "isProUser" | "isUnlocked">56): Promise<SyncPreflightResult> {57  const readyDeps = deps ?? (await loadDefaultDeps());58  if (!(await readyDeps.isProUser())) return { ok: false, error: "pro_required" };59  if (!readyDeps.isUnlocked()) return { ok: false, error: "vault_locked" };60  return { ok: true };61}6263export async function buildLocalSyncSnapshot(64  deps?: Pick<SyncCoordinatorDeps, "isProUser" | "isUnlocked" | "getEntries">65): Promise<SyncSnapshot> {66  const readyDeps = deps ?? (await loadDefaultDeps());67  const preflight = await checkSyncPreflight(readyDeps);68  if (!preflight.ok) throw new Error(preflight.error);6970  const entries = readyDeps.getEntries();71  return {72    entries,73    manifest: await buildSyncManifest(entries),74  };75}7677export async function planManifestSync(78  remoteManifest: SyncManifestEntry[],79  deps?: Pick<SyncCoordinatorDeps, "isProUser" | "isUnlocked" | "getEntries">80): Promise<SyncPlan> {81  const snapshot = await buildLocalSyncSnapshot(deps);82  return {83    localManifest: snapshot.manifest,84    diff: diffSyncManifests(snapshot.manifest, remoteManifest),85  };86}8788export async function cancelSyncIfVaultLocked(89  connectionId: string,90  deps?: Pick<SyncCoordinatorDeps, "isUnlocked" | "sendSyncData">91): Promise<boolean> {92  const readyDeps = deps ?? (await loadDefaultDeps());93  if (readyDeps.isUnlocked()) return false;9495  readyDeps.sendSyncData(96    connectionId,97    encodeSyncMessage({98      type: "SYNC_ERROR",99      code: "vault_locked",100      message: "Vault is locked; unlock Peach before syncing.",101    })102  );103  return true;104}