// Chirp Sync — type definitions (Master Brief v2). // // 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 // --------------------------------------------------------------------------- // "desktop" — peach-sync daemon (kepr.uk/peach-sync). Already a full member: // chirp byte tag 0x04 below, deviceTypeFrom/ToByte cases, and the paired-device // registry all accept it; the helper announces device_type "desktop" verbatim. 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 // --------------------------------------------------------------------------- /** * Trust source of a paired-device entry — per v2 brief §4.2. * * "direct" — established via an audio pairing session with this device. * The extension only ever has direct entries; mobiles can * also direct-pair with other mobiles (allowed but unusual). * "vouched" — received via a signed device-manifest from the extension * during a normal sync. Functionally equivalent to direct * for sync purposes; surfaced with an "Added automatically" * badge in Settings. */ export type TrustSource = "direct" | "vouched"; /** * v3 paired-device record. Stored in `chrome.storage.local` — cryptographic * keys must not leave the device via a third-party cloud service. * * `longTermPublicKeyHex` is the counterpart's vault-derived X25519 sync * identity static 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 `paired-devices.ts`. */ export interface PairedDevice { deviceId: string; deviceName?: string; deviceType?: DeviceType; longTermPublicKeyHex?: string; /** * Long-term Ed25519 verification key for the peer's device-manifest * signatures (v2 brief §4.3). Derived deterministically from the same * master key as `longTermPublicKeyHex`, via `deriveSyncSigningKeypair`. */ signingPublicKeyHex?: string; /** v2 brief §4.2 — distinguishes direct-paired vs vouched-via-manifest. */ trustSource?: TrustSource; /** * The peer's WebSocket sync port. Mobile hosts pick a random high port * (49152–65535) at first launch and announce it inside the encrypted * pairing channel; the extension caches it here. */ wsPort?: number; pairedAt: number; lastSyncedAt?: number; } // --------------------------------------------------------------------------- // 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; version: number; deviceTime: number } | { type: "SYNC_MANIFEST"; entries: Record; tombstones: Record } | { type: "SYNC_REQUEST"; entryIds: string[] } | { type: "SYNC_ENTRIES"; entries: Record } | { type: "SYNC_TOMBSTONES"; tombstones: Record } | { type: "SYNC_DEVICE_MANIFEST"; manifest: SignedDeviceManifest } | { type: "SYNC_DONE"; entriesReceived?: number; entriesSent?: number } | { type: "SYNC_ERROR"; error: string }; // --------------------------------------------------------------------------- // v3 device manifest — propagates trust between mobile devices (v2 brief §4.3) // --------------------------------------------------------------------------- export const DEVICE_MANIFEST_VERSION = 0x01 as const; /** Manifests older than this are silently rejected (v2 brief §4.3). */ export const DEVICE_MANIFEST_FRESHNESS_MS = 5 * 60_000; /** * One device entry inside a signed manifest. `longTermPublicKeyHex` is the * X25519 sync identity key the receiver will use for Noise XX, exactly as * if the entry had been audio-paired locally. `signingPublicKeyHex` is the * peer's Ed25519 manifest-verification key — needed so a vouched mobile * can in turn verify manifests issued by the same vault from a different * device. */ export interface ManifestDeviceEntry { deviceId: string; deviceName: string; deviceType: DeviceType; longTermPublicKeyHex: string; signingPublicKeyHex: string; /** * Optional sync WebSocket port for native/mobile hosts. Preserving this in * vouched entries lets mobile peers connect without another audio pairing. */ wsPort?: number; pairedAt: number; } /** Body that gets canonicalised + signed. Wire format JSON-stable. */ export interface DeviceManifestBody { manifestVersion: number; issuerDeviceId: string; issuedAt: number; devices: ManifestDeviceEntry[]; } export interface SignedDeviceManifest { body: DeviceManifestBody; /** Hex-encoded 64-byte Ed25519 signature over canonicalised body. */ signatureHex: string; } export type SyncStatusV3 = "up_to_date" | "syncing" | "unreachable" | "never";