sync-types.ts
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 * Connection id of the still-open pairing-only socket. The background's102 * RESPOND_INCOMING_PAIRING handler sends PAIR_ACCEPT or PAIR_REJECT over103 * this connection then disconnects. May be absent for synthetic test104 * injections that do not have a real connection behind them.105 */106 connectionId?: string;107}