sync-settings.ts

4.6 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";2930export async function getSyncSettings(): Promise<SyncSettings> {31  try {32    const values = (await chrome.storage.local.get([33      RELAY_URL_KEY,34      SYNC_MODE_KEY,35    ])) as Record<string, unknown>;3637    const storedUrl =38      typeof values[RELAY_URL_KEY] === "string"39        ? safeNormalize(values[RELAY_URL_KEY] as string)40        : DEFAULT_RELAY_URL;4142    const stored = values[SYNC_MODE_KEY];43    if (stored === "public" || stored === "private" || stored === "off") {44      return { mode: stored, relayUrl: relayFor(stored, storedUrl) };45    }4647    // A missing mode is a fresh install or an incomplete local store. Infer the48    // current mode from the configured relay; retired LAN settings are no49    // longer interpreted.50    const mode: SyncMode =51      storedUrl === DEFAULT_RELAY_URL ? "public" : "private";52    return { mode, relayUrl: relayFor(mode, storedUrl) };53  } catch {54    return { mode: "public", relayUrl: DEFAULT_RELAY_URL };55  }56}5758/**59 * `private` chosen but no relay entered yet. The stored URL still reads as the60 * public default, so running sync here would quietly put the user on Peach's61 * relay under a "Private relay" label — the exact trap the retired `lanOnly`62 * flag set. Every sync entry point fails closed on this instead.63 */64export function isPrivateRelayUnconfigured(settings: SyncSettings): boolean {65  return settings.mode === "private" && settings.relayUrl === DEFAULT_RELAY_URL;66}6768/**69 * The relay to dial, or a reason not to. Single gate for every sync and pairing70 * entry point, so a new caller cannot forget one of the two ways sync can be71 * unusable.72 */73export function relayInForce(settings: SyncSettings): string {74  if (settings.mode === "off") throw new Error("sync_disabled");75  if (isPrivateRelayUnconfigured(settings)) throw new Error("private_relay_unset");76  return settings.relayUrl;77}7879export async function setSyncMode(mode: SyncMode): Promise<SyncSettings> {80  const current = await getSyncSettings();81  const next: SyncSettings = { mode, relayUrl: relayFor(mode, current.relayUrl) };82  // The relay is mesh state: every paired device must agree on it, or a peer83  // keeps dialling a relay we no longer use. Only propagate one we will84  // actually use — switching to `private` before a URL is typed must not push85  // the public relay back out to the mesh.86  if (mode !== "off" && !isPrivateRelayUnconfigured(next)) {87    await setRelayUrlForAllPairedDevices(next.relayUrl);88  }89  await chrome.storage.local.set({ [SYNC_MODE_KEY]: mode, [RELAY_URL_KEY]: next.relayUrl });90  return next;91}9293/**94 * Point this mesh at a relay by URL. A custom URL implies `private` — offering95 * one and not switching to it would be a trap. Peach's own relay stays96 * `public`: adoption hands the source's relay over this same path, so calling97 * that "private" would mislabel every device that joined over the public relay98 * and strand it in the unconfigured-private state above.99 */100export async function setRelayUrl(input: string): Promise<SyncSettings> {101  const relayUrl = normalizeRelayUrl(input);102  const mode: SyncMode = relayUrl === DEFAULT_RELAY_URL ? "public" : "private";103  await setRelayUrlForAllPairedDevices(relayUrl);104  await chrome.storage.local.set({ [RELAY_URL_KEY]: relayUrl, [SYNC_MODE_KEY]: mode });105  return { mode, relayUrl };106}107108function relayFor(mode: SyncMode, configured: string): string {109  return mode === "public" ? DEFAULT_RELAY_URL : configured;110}111112function safeNormalize(input: string): string {113  try {114    return normalizeRelayUrl(input);115  } catch {116    return DEFAULT_RELAY_URL;117  }118}