sync-types.ts
1// Chirp Sync — type definitions.2//3// This file is in the middle of a v2.14 → v3 migration. The v3 chirp-sync4// architecture (audio-bootstrapped pairing + manifest-diff WebSocket sync)5// is being added alongside the v2.14 full-blob-over-native-host types.6// The v2.14 types are scheduled for removal once every consumer in7// messages.ts has been migrated; do not write new code against them.8//9// v3 — audio pairing once per device pair (Phase 1). All ongoing vault sync10// after that runs over a direct WebSocket on the local network (Phase 2).11// The two phases never overlap.1213// ---------------------------------------------------------------------------14// v3 chirp pairing — on-air audio frame15// ---------------------------------------------------------------------------1617export type DeviceType = "extension" | "android" | "ios" | "desktop";1819// Numeric tags used in the on-air chirp payload. The audio frame is a20// fixed 64-byte struct (see chirp-payload.ts); these are the byte values21// that go on the wire — not the string DeviceType.22export const DEVICE_TYPE_EXTENSION = 0x01 as const;23export const DEVICE_TYPE_ANDROID = 0x02 as const;24export const DEVICE_TYPE_IOS = 0x03 as const;25export const DEVICE_TYPE_DESKTOP = 0x04 as const;2627export type DeviceTypeByte =28 | typeof DEVICE_TYPE_EXTENSION29 | typeof DEVICE_TYPE_ANDROID30 | typeof DEVICE_TYPE_IOS31 | typeof DEVICE_TYPE_DESKTOP;3233export const CHIRP_MAGIC_BYTES = Uint8Array.of(0x50, 0x45, 0x41, 0x43); // "PEAC"34export const CHIRP_PROTOCOL_VERSION = 0x01 as const;35export const CHIRP_PAYLOAD_LENGTH = 64 as const;3637export function deviceTypeFromByte(b: DeviceTypeByte): DeviceType {38 switch (b) {39 case DEVICE_TYPE_EXTENSION: return "extension";40 case DEVICE_TYPE_ANDROID: return "android";41 case DEVICE_TYPE_IOS: return "ios";42 case DEVICE_TYPE_DESKTOP: return "desktop";43 }44}4546export function deviceTypeToByte(t: DeviceType): DeviceTypeByte {47 switch (t) {48 case "extension": return DEVICE_TYPE_EXTENSION;49 case "android": return DEVICE_TYPE_ANDROID;50 case "ios": return DEVICE_TYPE_IOS;51 case "desktop": return DEVICE_TYPE_DESKTOP;52 }53}5455export interface ChirpPayload {56 version: number;57 deviceType: DeviceTypeByte;58 ephemeralPublicKey: Uint8Array; // 32 bytes59 sessionNonce: Uint8Array; // 16 bytes60}6162// ---------------------------------------------------------------------------63// v3 chirp pairing — long-term paired device registry64// ---------------------------------------------------------------------------6566/**67 * v3 paired-device record. Stored in `chrome.storage.sync` so the registry68 * roams across browser profiles signed into Chrome/Firefox sync.69 *70 * `longTermPublicKeyHex` is the counterpart's vault-derived sync identity71 * static X25519 public key — see `deriveSyncIdentityKeypair`. Mutual Noise72 * XX authentication on every WebSocket connection rejects any peer not73 * present in this registry.74 *75 * `lastKnownIp` is intentionally NOT stored here — it's per-network and76 * lives in `chrome.storage.local` keyed by `longTermPublicKeyHex`. See77 * `getLastKnownIp` / `setLastKnownIp` in sync-storage.ts.78 *79 * Legacy v2.14 fields `displayName`, `x25519PublicKey`, `lastSeenAt`,80 * `lastSyncAt`, `lastKnownIp` are kept for migration only; new code reads81 * `deviceName`, `longTermPublicKeyHex`, `lastSyncedAt`.82 */83export interface PairedDevice {84 deviceId: string;85 deviceName?: string;86 deviceType?: DeviceType;87 longTermPublicKeyHex?: string;88 /**89 * The peer's WebSocket sync port. Android picks a random high port90 * (49152–65535) at first launch and announces it inside the encrypted91 * pairing channel; the extension caches it here. iOS / Tauri sync hosts92 * follow the same convention.93 */94 wsPort?: number;95 pairedAt: number;96 lastSyncedAt?: number;9798 // v2.14 legacy — kept readable so the migration sweep doesn't crash99 // every test that still constructs the old shape. New writers must use100 // the v3 fields above.101 displayName?: string;102 x25519PublicKey?: string;103 lastSeenAt?: number;104 lastSyncAt?: number;105 lastKnownIp?: string;106}107108// ---------------------------------------------------------------------------109// v3 chirp pairing — phase-1 UI state110// ---------------------------------------------------------------------------111112/**113 * Phase 1 pairing state, surfaced to the SyncPairing view. The view binds114 * its bird-animation acts directly to these states: `listening` and115 * `chirping` map to Act 1, `handshake` maps to Act 2, `paired` to Act 3.116 */117export type PairingPhase =118 | "idle"119 | "listening"120 | "chirping"121 | "handshake"122 | "paired"123 | "timeout"124 | "mic_denied"125 | "version_mismatch"126 | "handshake_failed";127128export interface PairingProgress {129 phase: PairingPhase;130 peerDeviceName?: string;131 errorMessage?: string;132}133134// ---------------------------------------------------------------------------135// v3 wire protocol — phase-2 WebSocket manifest sync136// ---------------------------------------------------------------------------137138/**139 * v3 wire protocol — after Noise XX is open.140 *141 * Every frame is JSON, then ChaCha20-Poly1305 encrypted by Noise XX. The142 * WebSocket transport sees only ciphertext.143 *144 * The manifest carries per-entry timestamps; the receiver requests entries145 * where the remote timestamp beats its own. Entries traverse as opaque146 * encrypted blobs — the sync layer never decrypts vault data.147 */148export type ChirpSyncMessage =149 | { type: "SYNC_HELLO"; vaultId: string; deviceId: string }150 | { type: "SYNC_MANIFEST"; entries: Record<string, number>; tombstones: Record<string, number> }151 | { type: "SYNC_REQUEST"; entryIds: string[] }152 | { type: "SYNC_ENTRIES"; entries: Record<string, string> }153 | { type: "SYNC_TOMBSTONES"; tombstones: Record<string, number> }154 | { type: "SYNC_DONE" }155 | { type: "SYNC_ERROR"; error: string };156157export type SyncStatusV3 = "up_to_date" | "syncing" | "unreachable" | "never";158159// ---------------------------------------------------------------------------160// v2.14 legacy types — DO NOT USE IN NEW CODE161//162// These remain only because messages.ts and the v2.14 coordinator/network163// modules import them. Once the migration sweep lands, this whole block164// goes away.165// ---------------------------------------------------------------------------166167export interface TrustedNetwork {168 ssid?: string;169 bssid?: string;170 isTailscale: boolean;171 addedAt: number;172}173174export interface SyncableDevice {175 publicKeyHex: string;176 deviceName: string;177 pairedAt: number;178}179180export type SyncMessage =181 | { type: 'SYNC_HELLO' }182 | { type: 'SYNC_VAULT_SEND'; blob: string; timestamp: number; license?: string; devices: SyncableDevice[] }183 | { type: 'SYNC_VAULT_ACK'; applied: boolean }184 | { type: 'SYNC_DONE' }185 | { type: 'SYNC_ERROR'; error: string };186187export type SyncStatus = "synced" | "syncing" | "no_peers" | "not_trusted" | "offline";188189export interface IncomingPairingRequest {190 deviceName: string;191 publicKeyHex: string;192 receivedAt: number;193 expiresAt: number;194 connectionId?: string;195}