sync-types.ts

4.4 KB · raw

1/**2 * Types for Peach sync — full encrypted vault blob transfer.3 *4 * The protocol used to be per-entry (manifest / diff / conflict), but at5 * 200KB for an 800-entry vault local-WiFi transfer takes under 200ms, so6 * the simpler model wins: the sender ships the whole encrypted blob, the7 * receiver applies it if its `sent_at` is newer than the last sync, and8 * vault history is the safety net for last-write-wins.9 *10 * The `vault_a` / `vault_b` fields are JSON-stringified `StoredVault`11 * records — already AES-256-GCM encrypted under the user's master password.12 * The Noise NK transport adds a second encryption layer; the sync layer13 * never sees plaintext vault contents.14 */1516export interface TrustedNetwork {17  ssid?: string;18  bssid?: string;19  isTailscale: boolean;20  addedAt: number;21}2223export interface PairedDevice {24  deviceId: string;25  displayName: string;26  x25519PublicKey: string;27  pairedAt: number;28  lastSeenAt?: number;29  lastSyncAt?: number;30}3132/**33 * Wire protocol for an active Noise NK session.34 *35 * SYNC_VAULT_SEND  — sender pushes the full encrypted vault blobs36 * SYNC_VAULT_ACK   — receiver acknowledges; receipt is non-fatal but lets37 *                    the sender measure round-trip and disconnect cleanly38 * SYNC_ERROR       — preflight or session error (vault locked, pro_required,39 *                    invalid frame, timeout)40 * PAIR_REQUEST     — initiator asks an unpaired responder to add it as a41 *                    trusted device (sent only when the responder is in42 *                    pairing mode)43 * PAIR_ACCEPT      — responder confirms; both sides add each other to their44 *                    trust stores45 * PAIR_REJECT      — user declined the pair, or the request expired46 */47export type SyncMessage =48  | {49      type: "SYNC_VAULT_SEND";50      vault_a: string;51      vault_b: string;52      sent_at: number;53    }54  | { type: "SYNC_VAULT_ACK"; received_at: number }55  | { type: "SYNC_ERROR"; reason: string }56  | { type: "PAIR_REQUEST"; public_key_hex: string; device_name: string }57  | { type: "PAIR_ACCEPT"; public_key_hex: string; device_name: string }58  | { type: "PAIR_REJECT"; reason: string };5960export type SyncStatus = "synced" | "syncing" | "no_peers" | "not_trusted" | "offline";6162export interface SyncState {63  status: SyncStatus;64  lastSyncAt?: number;65  pairedDevices: PairedDevice[];66  trustedNetworks: TrustedNetwork[];67  isPro: boolean;68}6970export type PairingStep =71  | { stage: "idle" }72  | { stage: "generating_code"; code: string; expiresAt: number }73  | { stage: "waiting_for_peer" }74  | { stage: "entering_code" }75  | { stage: "connecting"; peerName: string }76  | { stage: "verifying" }77  | { stage: "done"; device: PairedDevice }78  | { stage: "error"; message: string };7980export interface NoiseHandshakeResult {81  sessionKey: Uint8Array;82  sendNonce: number;83  receiveNonce: number;84  isInitiator: boolean;85}8687/**88 * Pairing request received from a peer that needs user confirmation.89 *90 * The banner expires after 60 seconds (`expiresAt - receivedAt = 60_000`);91 * after that the popup hides the banner and the background drops the92 * request. The publicKeyHex is the peer's static X25519 identity — it93 * gets written to trusted devices on accept and discarded on reject.94 */95export interface IncomingPairingRequest {96  deviceName: string;97  publicKeyHex: string;98  receivedAt: number;99  expiresAt: number;100  /**101   * Native-host connection id of the still-open pairing-only connection. The102   * background's RESPOND_INCOMING_PAIRING handler sends PAIR_ACCEPT or103   * PAIR_REJECT over this connection then disconnects. May be absent for104   * synthetic test injections that do not have a real connection behind them.105   */106  connectionId?: string;107}108109/**110 * A peer the native sync host has seen on mDNS. The popup pairing flow lists111 * these and routes a tap to `INITIATE_PAIR_WITH_PEER` with the same shape.112 *113 * `alreadyPaired` is computed against `getPairedDevices()` at broadcast time114 * so the popup can render "Already paired ✓" without a second round-trip.115 */116export interface DiscoveredPeer {117  deviceId: string;118  deviceName: string;119  host: string;120  port: number;121  /**122   * WebSocket port advertised by the peer (mDNS `wsport`). The Capacitor123   * Android bridge connects through this; the desktop extension uses raw124   * TCP via the native messaging host and ignores it.125   */126  wsPort?: number;127  x25519PublicKey: string;128  alreadyPaired: boolean;129}