sync-types.ts

7.3 KB · raw

1// Chirp Sync — type definitions (Master Brief v2).2//3// v3 — audio pairing once per device pair (Phase 1). All ongoing vault sync4// after that runs over a direct WebSocket on the local network (Phase 2).5// The two phases never overlap.67// ---------------------------------------------------------------------------8// v3 chirp pairing — on-air audio frame9// ---------------------------------------------------------------------------1011export type DeviceType = "extension" | "android" | "ios" | "desktop";1213// Numeric tags used in the on-air chirp payload. The audio frame is a14// fixed 64-byte struct (see chirp-payload.ts); these are the byte values15// that go on the wire — not the string DeviceType.16export const DEVICE_TYPE_EXTENSION = 0x01 as const;17export const DEVICE_TYPE_ANDROID = 0x02 as const;18export const DEVICE_TYPE_IOS = 0x03 as const;19export const DEVICE_TYPE_DESKTOP = 0x04 as const;2021export type DeviceTypeByte =22  | typeof DEVICE_TYPE_EXTENSION23  | typeof DEVICE_TYPE_ANDROID24  | typeof DEVICE_TYPE_IOS25  | typeof DEVICE_TYPE_DESKTOP;2627export const CHIRP_MAGIC_BYTES = Uint8Array.of(0x50, 0x45, 0x41, 0x43); // "PEAC"28export const CHIRP_PROTOCOL_VERSION = 0x01 as const;29export const CHIRP_PAYLOAD_LENGTH = 64 as const;3031export function deviceTypeFromByte(b: DeviceTypeByte): DeviceType {32  switch (b) {33    case DEVICE_TYPE_EXTENSION: return "extension";34    case DEVICE_TYPE_ANDROID: return "android";35    case DEVICE_TYPE_IOS: return "ios";36    case DEVICE_TYPE_DESKTOP: return "desktop";37  }38}3940export function deviceTypeToByte(t: DeviceType): DeviceTypeByte {41  switch (t) {42    case "extension": return DEVICE_TYPE_EXTENSION;43    case "android": return DEVICE_TYPE_ANDROID;44    case "ios": return DEVICE_TYPE_IOS;45    case "desktop": return DEVICE_TYPE_DESKTOP;46  }47}4849export interface ChirpPayload {50  version: number;51  deviceType: DeviceTypeByte;52  ephemeralPublicKey: Uint8Array; // 32 bytes53  sessionNonce: Uint8Array; // 16 bytes54}5556// ---------------------------------------------------------------------------57// v3 chirp pairing — long-term paired device registry58// ---------------------------------------------------------------------------5960/**61 * Trust source of a paired-device entry — per v2 brief §4.2.62 *63 *   "direct"  — established via an audio pairing session with this device.64 *               The extension only ever has direct entries; mobiles can65 *               also direct-pair with other mobiles (allowed but unusual).66 *   "vouched" — received via a signed device-manifest from the extension67 *               during a normal sync. Functionally equivalent to direct68 *               for sync purposes; surfaced with an "Added automatically"69 *               badge in Settings.70 */71export type TrustSource = "direct" | "vouched";7273/**74 * v3 paired-device record. Stored in `chrome.storage.sync` so the registry75 * roams across browser profiles signed into Chrome/Firefox sync.76 *77 * `longTermPublicKeyHex` is the counterpart's vault-derived X25519 sync78 * identity static key — see `deriveSyncIdentityKeypair`. Mutual Noise XX79 * authentication on every WebSocket connection rejects any peer not80 * present in this registry.81 *82 * `lastKnownIp` is intentionally NOT stored here — it's per-network and83 * lives in `chrome.storage.local` keyed by `longTermPublicKeyHex`. See84 * `getLastKnownIp` / `setLastKnownIp` in `paired-devices.ts`.85 */86export interface PairedDevice {87  deviceId: string;88  deviceName?: string;89  deviceType?: DeviceType;90  longTermPublicKeyHex?: string;91  /**92   * Long-term Ed25519 verification key for the peer's device-manifest93   * signatures (v2 brief §4.3). Derived deterministically from the same94   * master key as `longTermPublicKeyHex`, via `deriveSyncSigningKeypair`.95   */96  signingPublicKeyHex?: string;97  /** v2 brief §4.2 — distinguishes direct-paired vs vouched-via-manifest. */98  trustSource?: TrustSource;99  /**100   * The peer's WebSocket sync port. Mobile hosts pick a random high port101   * (49152–65535) at first launch and announce it inside the encrypted102   * pairing channel; the extension caches it here.103   */104  wsPort?: number;105  pairedAt: number;106  lastSyncedAt?: number;107}108109// ---------------------------------------------------------------------------110// v3 chirp pairing — phase-1 UI state111// ---------------------------------------------------------------------------112113/**114 * Phase 1 pairing state, surfaced to the chirp pairing view. The view115 * binds its bird-animation acts directly to these states: `listening` and116 * `chirping` map to Act 1, `handshake` maps to Act 2, `paired` to Act 3.117 */118export type PairingPhase =119  | "idle"120  | "listening"121  | "chirping"122  | "handshake"123  | "paired"124  | "timeout"125  | "mic_denied"126  | "version_mismatch"127  | "handshake_failed";128129export interface PairingProgress {130  phase: PairingPhase;131  peerDeviceName?: string;132  errorMessage?: string;133}134135// ---------------------------------------------------------------------------136// v3 wire protocol — phase-2 WebSocket manifest sync137// ---------------------------------------------------------------------------138139/**140 * v3 wire protocol — after Noise XX is open.141 *142 * Every frame is JSON, then ChaCha20-Poly1305 encrypted by Noise XX. The143 * WebSocket transport sees only ciphertext.144 *145 * The manifest carries per-entry timestamps; the receiver requests entries146 * where the remote timestamp beats its own. Entries traverse as opaque147 * encrypted blobs — the sync layer never decrypts vault data.148 */149export type ChirpSyncMessage =150  | { type: "SYNC_HELLO"; vaultId: string; deviceId: string }151  | { type: "SYNC_MANIFEST"; entries: Record<string, number>; tombstones: Record<string, number> }152  | { type: "SYNC_REQUEST"; entryIds: string[] }153  | { type: "SYNC_ENTRIES"; entries: Record<string, string> }154  | { type: "SYNC_TOMBSTONES"; tombstones: Record<string, number> }155  | { type: "SYNC_DEVICE_MANIFEST"; manifest: SignedDeviceManifest }156  | { type: "SYNC_DONE" }157  | { type: "SYNC_ERROR"; error: string };158159// ---------------------------------------------------------------------------160// v3 device manifest — propagates trust between mobile devices (v2 brief §4.3)161// ---------------------------------------------------------------------------162163export const DEVICE_MANIFEST_VERSION = 0x01 as const;164/** Manifests older than this are silently rejected (v2 brief §4.3). */165export const DEVICE_MANIFEST_FRESHNESS_MS = 5 * 60_000;166167/**168 * One device entry inside a signed manifest. `longTermPublicKeyHex` is the169 * X25519 sync identity key the receiver will use for Noise XX, exactly as170 * if the entry had been audio-paired locally. `signingPublicKeyHex` is the171 * peer's Ed25519 manifest-verification key — needed so a vouched mobile172 * can in turn verify manifests issued by the same vault from a different173 * device.174 */175export interface ManifestDeviceEntry {176  deviceId: string;177  deviceName: string;178  deviceType: DeviceType;179  longTermPublicKeyHex: string;180  signingPublicKeyHex: string;181  pairedAt: number;182}183184/** Body that gets canonicalised + signed. Wire format JSON-stable. */185export interface DeviceManifestBody {186  manifestVersion: number;187  issuerDeviceId: string;188  issuedAt: number;189  devices: ManifestDeviceEntry[];190}191192export interface SignedDeviceManifest {193  body: DeviceManifestBody;194  /** Hex-encoded 64-byte Ed25519 signature over canonicalised body. */195  signatureHex: string;196}197198export type SyncStatusV3 = "up_to_date" | "syncing" | "unreachable" | "never";