/** * Types for Peach sync — full encrypted vault blob transfer. * * The protocol used to be per-entry (manifest / diff / conflict), but at * 200KB for an 800-entry vault local-WiFi transfer takes under 200ms, so * the simpler model wins: the sender ships the whole encrypted blob, the * receiver applies it if its `sent_at` is newer than the last sync, and * vault history is the safety net for last-write-wins. * * The `vault_a` / `vault_b` fields are JSON-stringified `StoredVault` * records — already AES-256-GCM encrypted under the user's master password. * The Noise NK transport adds a second encryption layer; the sync layer * never sees plaintext vault contents. */ export interface TrustedNetwork { ssid?: string; bssid?: string; isTailscale: boolean; addedAt: number; } export interface PairedDevice { deviceId: string; displayName: string; x25519PublicKey: string; pairedAt: number; lastSeenAt?: number; lastSyncAt?: number; } /** * Wire protocol for an active Noise NK session. * * SYNC_VAULT_SEND — sender pushes the full encrypted vault blobs * SYNC_VAULT_ACK — receiver acknowledges; receipt is non-fatal but lets * the sender measure round-trip and disconnect cleanly * SYNC_ERROR — preflight or session error (vault locked, pro_required, * invalid frame, timeout) * PAIR_REQUEST — initiator asks an unpaired responder to add it as a * trusted device (sent only when the responder is in * pairing mode) * PAIR_ACCEPT — responder confirms; both sides add each other to their * trust stores * PAIR_REJECT — user declined the pair, or the request expired */ export type SyncMessage = | { type: "SYNC_VAULT_SEND"; vault_a: string; vault_b: string; sent_at: number; } | { type: "SYNC_VAULT_ACK"; received_at: number } | { type: "SYNC_ERROR"; reason: string } | { type: "PAIR_REQUEST"; public_key_hex: string; device_name: string } | { type: "PAIR_ACCEPT"; public_key_hex: string; device_name: string } | { type: "PAIR_REJECT"; reason: string }; export type SyncStatus = "synced" | "syncing" | "no_peers" | "not_trusted" | "offline"; export interface SyncState { status: SyncStatus; lastSyncAt?: number; pairedDevices: PairedDevice[]; trustedNetworks: TrustedNetwork[]; isPro: boolean; } export type PairingStep = | { stage: "idle" } | { stage: "generating_code"; code: string; expiresAt: number } | { stage: "waiting_for_peer" } | { stage: "entering_code" } | { stage: "connecting"; peerName: string } | { stage: "verifying" } | { stage: "done"; device: PairedDevice } | { stage: "error"; message: string }; export interface NoiseHandshakeResult { sessionKey: Uint8Array; sendNonce: number; receiveNonce: number; isInitiator: boolean; } /** * Pairing request received from a peer that needs user confirmation. * * The banner expires after 60 seconds (`expiresAt - receivedAt = 60_000`); * after that the popup hides the banner and the background drops the * request. The publicKeyHex is the peer's static X25519 identity — it * gets written to trusted devices on accept and discarded on reject. */ export interface IncomingPairingRequest { deviceName: string; publicKeyHex: string; receivedAt: number; expiresAt: number; /** * Native-host connection id of the still-open pairing-only connection. The * background's RESPOND_INCOMING_PAIRING handler sends PAIR_ACCEPT or * PAIR_REJECT over this connection then disconnects. May be absent for * synthetic test injections that do not have a real connection behind them. */ connectionId?: string; } /** * A peer the native sync host has seen on mDNS. The popup pairing flow lists * these and routes a tap to `INITIATE_PAIR_WITH_PEER` with the same shape. * * `alreadyPaired` is computed against `getPairedDevices()` at broadcast time * so the popup can render "Already paired ✓" without a second round-trip. */ export interface DiscoveredPeer { deviceId: string; deviceName: string; host: string; port: number; /** * WebSocket port advertised by the peer (mDNS `wsport`). The Capacitor * Android bridge connects through this; the desktop extension uses raw * TCP via the native messaging host and ignores it. */ wsPort?: number; x25519PublicKey: string; alreadyPaired: boolean; }