// Chirp Sync — type definitions. // // This file is in the middle of a v2.14 → v3 migration. The v3 chirp-sync // architecture (audio-bootstrapped pairing + manifest-diff WebSocket sync) // is being added alongside the v2.14 full-blob-over-native-host types. // The v2.14 types are scheduled for removal once every consumer in // messages.ts has been migrated; do not write new code against them. // // v3 — audio pairing once per device pair (Phase 1). All ongoing vault sync // after that runs over a direct WebSocket on the local network (Phase 2). // The two phases never overlap. // --------------------------------------------------------------------------- // v3 chirp pairing — on-air audio frame // --------------------------------------------------------------------------- export type DeviceType = "extension" | "android" | "ios" | "desktop"; // Numeric tags used in the on-air chirp payload. The audio frame is a // fixed 64-byte struct (see chirp-payload.ts); these are the byte values // that go on the wire — not the string DeviceType. export const DEVICE_TYPE_EXTENSION = 0x01 as const; export const DEVICE_TYPE_ANDROID = 0x02 as const; export const DEVICE_TYPE_IOS = 0x03 as const; export const DEVICE_TYPE_DESKTOP = 0x04 as const; export type DeviceTypeByte = | typeof DEVICE_TYPE_EXTENSION | typeof DEVICE_TYPE_ANDROID | typeof DEVICE_TYPE_IOS | typeof DEVICE_TYPE_DESKTOP; export const CHIRP_MAGIC_BYTES = Uint8Array.of(0x50, 0x45, 0x41, 0x43); // "PEAC" export const CHIRP_PROTOCOL_VERSION = 0x01 as const; export const CHIRP_PAYLOAD_LENGTH = 64 as const; export function deviceTypeFromByte(b: DeviceTypeByte): DeviceType { switch (b) { case DEVICE_TYPE_EXTENSION: return "extension"; case DEVICE_TYPE_ANDROID: return "android"; case DEVICE_TYPE_IOS: return "ios"; case DEVICE_TYPE_DESKTOP: return "desktop"; } } export function deviceTypeToByte(t: DeviceType): DeviceTypeByte { switch (t) { case "extension": return DEVICE_TYPE_EXTENSION; case "android": return DEVICE_TYPE_ANDROID; case "ios": return DEVICE_TYPE_IOS; case "desktop": return DEVICE_TYPE_DESKTOP; } } export interface ChirpPayload { version: number; deviceType: DeviceTypeByte; ephemeralPublicKey: Uint8Array; // 32 bytes sessionNonce: Uint8Array; // 16 bytes } // --------------------------------------------------------------------------- // v3 chirp pairing — long-term paired device registry // --------------------------------------------------------------------------- /** * v3 paired-device record. Stored in `chrome.storage.sync` so the registry * roams across browser profiles signed into Chrome/Firefox sync. * * `longTermPublicKeyHex` is the counterpart's vault-derived sync identity * static X25519 public key — see `deriveSyncIdentityKeypair`. Mutual Noise * XX authentication on every WebSocket connection rejects any peer not * present in this registry. * * `lastKnownIp` is intentionally NOT stored here — it's per-network and * lives in `chrome.storage.local` keyed by `longTermPublicKeyHex`. See * `getLastKnownIp` / `setLastKnownIp` in sync-storage.ts. * * Legacy v2.14 fields `displayName`, `x25519PublicKey`, `lastSeenAt`, * `lastSyncAt`, `lastKnownIp` are kept for migration only; new code reads * `deviceName`, `longTermPublicKeyHex`, `lastSyncedAt`. */ export interface PairedDevice { deviceId: string; deviceName?: string; deviceType?: DeviceType; longTermPublicKeyHex?: string; /** * The peer's WebSocket sync port. Android picks a random high port * (49152–65535) at first launch and announces it inside the encrypted * pairing channel; the extension caches it here. iOS / Tauri sync hosts * follow the same convention. */ wsPort?: number; pairedAt: number; lastSyncedAt?: number; // v2.14 legacy — kept readable so the migration sweep doesn't crash // every test that still constructs the old shape. New writers must use // the v3 fields above. displayName?: string; x25519PublicKey?: string; lastSeenAt?: number; lastSyncAt?: number; lastKnownIp?: string; } // --------------------------------------------------------------------------- // v3 chirp pairing — phase-1 UI state // --------------------------------------------------------------------------- /** * Phase 1 pairing state, surfaced to the SyncPairing view. The view binds * its bird-animation acts directly to these states: `listening` and * `chirping` map to Act 1, `handshake` maps to Act 2, `paired` to Act 3. */ export type PairingPhase = | "idle" | "listening" | "chirping" | "handshake" | "paired" | "timeout" | "mic_denied" | "version_mismatch" | "handshake_failed"; export interface PairingProgress { phase: PairingPhase; peerDeviceName?: string; errorMessage?: string; } // --------------------------------------------------------------------------- // v3 wire protocol — phase-2 WebSocket manifest sync // --------------------------------------------------------------------------- /** * v3 wire protocol — after Noise XX is open. * * Every frame is JSON, then ChaCha20-Poly1305 encrypted by Noise XX. The * WebSocket transport sees only ciphertext. * * The manifest carries per-entry timestamps; the receiver requests entries * where the remote timestamp beats its own. Entries traverse as opaque * encrypted blobs — the sync layer never decrypts vault data. */ export type ChirpSyncMessage = | { type: "SYNC_HELLO"; vaultId: string; deviceId: string } | { type: "SYNC_MANIFEST"; entries: Record; tombstones: Record } | { type: "SYNC_REQUEST"; entryIds: string[] } | { type: "SYNC_ENTRIES"; entries: Record } | { type: "SYNC_TOMBSTONES"; tombstones: Record } | { type: "SYNC_DONE" } | { type: "SYNC_ERROR"; error: string }; export type SyncStatusV3 = "up_to_date" | "syncing" | "unreachable" | "never"; // --------------------------------------------------------------------------- // v2.14 legacy types — DO NOT USE IN NEW CODE // // These remain only because messages.ts and the v2.14 coordinator/network // modules import them. Once the migration sweep lands, this whole block // goes away. // --------------------------------------------------------------------------- export interface TrustedNetwork { ssid?: string; bssid?: string; isTailscale: boolean; addedAt: number; } export interface SyncableDevice { publicKeyHex: string; deviceName: string; pairedAt: number; } export type SyncMessage = | { type: 'SYNC_HELLO' } | { type: 'SYNC_VAULT_SEND'; blob: string; timestamp: number; license?: string; devices: SyncableDevice[] } | { type: 'SYNC_VAULT_ACK'; applied: boolean } | { type: 'SYNC_DONE' } | { type: 'SYNC_ERROR'; error: string }; export type SyncStatus = "synced" | "syncing" | "no_peers" | "not_trusted" | "offline"; export interface IncomingPairingRequest { deviceName: string; publicKeyHex: string; receivedAt: number; expiresAt: number; connectionId?: string; }