sync-settings.ts

3.7 KB · raw

1import { DEFAULT_RELAY_URL, normalizeRelayUrl } from "../shared/rendezvous";2import { setRelayUrlForAllPairedDevices } from "./paired-devices";34/**5 * Sync is exactly three named choices (docs/sync-lan-retirement.md):6 *7 *  - `public`  — Peach's relay. The default, zero setup.8 *  - `private` — a self-hosted peach-relay the user points their devices at.9 *  - `off`     — no sync; the vault moves between devices by Codex/CSV export.10 *11 * LAN peer-to-peer sync is retired, not deprecated: there is no fourth mode and12 * no "local network only" opt-out. `public` and `private` differ only in which13 * relay URL is in force, so the transport below them is identical.14 */15export type SyncMode = "public" | "private" | "off";1617export interface SyncSettings {18  mode: SyncMode;19  /**20   * The relay in force. Always `DEFAULT_RELAY_URL` under `public`; the user's21   * URL under `private`. Retained (not blanked) under `off` so turning sync22   * back on does not lose a configured private relay.23   */24  relayUrl: string;25}2627const RELAY_URL_KEY = "peach_relay_url_v1";28const SYNC_MODE_KEY = "peach_sync_mode_v1";29/** Retired `lanOnly` flag, read once for migration and never written again. */30const LEGACY_LAN_ONLY_KEY = "peach_sync_lan_only_v1";3132export async function getSyncSettings(): Promise<SyncSettings> {33  try {34    const values = (await chrome.storage.local.get([35      RELAY_URL_KEY,36      SYNC_MODE_KEY,37      LEGACY_LAN_ONLY_KEY,38    ])) as Record<string, unknown>;3940    const storedUrl =41      typeof values[RELAY_URL_KEY] === "string"42        ? safeNormalize(values[RELAY_URL_KEY] as string)43        : DEFAULT_RELAY_URL;4445    const stored = values[SYNC_MODE_KEY];46    if (stored === "public" || stored === "private" || stored === "off") {47      return { mode: stored, relayUrl: relayFor(stored, storedUrl) };48    }4950    // Migration. Someone who set lanOnly chose "no relay involved", and LAN is51    // now gone, so the honest landing spot is `off` rather than silently52    // enrolling them in a relay they opted out of. Everyone else lands on53    // public/private by whether they had configured a custom URL.54    const mode: SyncMode =55      values[LEGACY_LAN_ONLY_KEY] === true56        ? "off"57        : storedUrl === DEFAULT_RELAY_URL58          ? "public"59          : "private";60    return { mode, relayUrl: relayFor(mode, storedUrl) };61  } catch {62    return { mode: "public", relayUrl: DEFAULT_RELAY_URL };63  }64}6566/** True when sync may run at all. */67export async function isSyncEnabled(): Promise<boolean> {68  return (await getSyncSettings()).mode !== "off";69}7071export async function setSyncMode(mode: SyncMode): Promise<SyncSettings> {72  const current = await getSyncSettings();73  const relayUrl = relayFor(mode, current.relayUrl);74  // The relay is mesh state: every paired device must agree on it, or a peer75  // keeps dialling a relay we no longer use.76  if (mode !== "off") await setRelayUrlForAllPairedDevices(relayUrl);77  await chrome.storage.local.set({ [SYNC_MODE_KEY]: mode, [RELAY_URL_KEY]: relayUrl });78  return { mode, relayUrl };79}8081/**82 * Point this mesh at a self-hosted relay. Implies `private` — choosing a custom83 * relay and not switching to it would be a trap.84 */85export async function setPrivateRelayUrl(input: string): Promise<SyncSettings> {86  const relayUrl = normalizeRelayUrl(input);87  await setRelayUrlForAllPairedDevices(relayUrl);88  await chrome.storage.local.set({89    [RELAY_URL_KEY]: relayUrl,90    [SYNC_MODE_KEY]: "private" satisfies SyncMode,91  });92  return { mode: "private", relayUrl };93}9495function relayFor(mode: SyncMode, configured: string): string {96  return mode === "public" ? DEFAULT_RELAY_URL : configured;97}9899function safeNormalize(input: string): string {100  try {101    return normalizeRelayUrl(input);102  } catch {103    return DEFAULT_RELAY_URL;104  }105}