sync-types.ts

7.3 KB · raw

1// Vault sync — type definitions (Master Brief v2).2//3// v3 — code 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// Device types9// ---------------------------------------------------------------------------1011// "desktop" — speculative scaffolding for a possible future desktop app. There12// is no daemon running anywhere; the only real peers are this extension and13// Android. Do not build against it (see the browser implementation doc §6).14export type DeviceType = "extension" | "android" | "ios" | "desktop";1516// Numeric tags for DeviceType on the wire — not the string form.17export const DEVICE_TYPE_EXTENSION = 0x01 as const;18export const DEVICE_TYPE_ANDROID = 0x02 as const;19export const DEVICE_TYPE_IOS = 0x03 as const;20export const DEVICE_TYPE_DESKTOP = 0x04 as const;2122export type DeviceTypeByte =23  | typeof DEVICE_TYPE_EXTENSION24  | typeof DEVICE_TYPE_ANDROID25  | typeof DEVICE_TYPE_IOS26  | typeof DEVICE_TYPE_DESKTOP;2728// ---------------------------------------------------------------------------29// Long-term paired device registry30// ---------------------------------------------------------------------------3132/**33 * Trust source of a paired-device entry — per v2 brief §4.2.34 *35 *   "direct"  — established via a pairing-code session with this device.36 *   "vouched" — introduced by a directly-paired peer via a signed device37 *               manifest during a normal sync, then confirmed by the user38 *               (MV-7). Functionally equivalent to direct for sync purposes;39 *               surfaced with an "Added automatically" badge in Settings.40 */41export type TrustSource = "direct" | "vouched";4243/**44 * v3 paired-device record. Stored in `chrome.storage.local` — cryptographic45 * keys must not leave the device via a third-party cloud service.46 *47 * `longTermPublicKeyHex` is the counterpart's vault-derived X25519 sync48 * identity static key — see `deriveSyncIdentityKeypair`. Every WebSocket49 * reconnect runs Noise NK against this key (authenticating the responder50 * only) followed by a PAIR_IDENTITY exchange checked against this registry —51 * see HANDSHAKE_STANDARDIZE_ON_NK.md.52 *53 * `lastKnownIp` is intentionally NOT stored here — it's per-network and54 * lives in `chrome.storage.local` keyed by `longTermPublicKeyHex`. See55 * `getLastKnownIp` / `setLastKnownIp` in `paired-devices.ts`.56 */57export interface PairedDevice {58  deviceId: string;59  deviceName?: string;60  deviceType?: DeviceType;61  longTermPublicKeyHex?: string;62  /**63   * Long-term Ed25519 verification key for the peer's device-manifest64   * signatures (v2 brief §4.3). Derived deterministically from the same65   * master key as `longTermPublicKeyHex`, via `deriveSyncSigningKeypair`.66   */67  signingPublicKeyHex?: string;68  /** v2 brief §4.2 — distinguishes direct-paired vs vouched-via-manifest. */69  trustSource?: TrustSource;70  /**71   * The peer's WebSocket sync port. Mobile hosts pick a random high port72   * (49152–65535) at first launch and announce it inside the encrypted73   * pairing channel; the extension caches it here.74   */75  wsPort?: number;76  pairedAt: number;77  lastSyncedAt?: number;78}7980// ---------------------------------------------------------------------------81// v3 wire protocol — phase-2 WebSocket manifest sync82// ---------------------------------------------------------------------------8384/**85 * v3 wire protocol — after Noise NK is open and PAIR_IDENTITY has been86 * exchanged (see HANDSHAKE_STANDARDIZE_ON_NK.md).87 *88 * Every frame is JSON, then ChaCha20-Poly1305 encrypted by Noise NK. The89 * WebSocket transport sees only ciphertext.90 *91 * The manifest carries per-entry timestamps; the receiver requests entries92 * where the remote timestamp beats its own. Entries traverse as opaque93 * encrypted blobs — the sync layer never decrypts vault data.94 */95export type SyncMessage =96  | { type: "SYNC_HELLO"; vaultId: string; deviceId: string; version: number; deviceTime: number }97  | { type: "SYNC_MANIFEST"; entries: Record<string, number>; tombstones: Record<string, number> }98  | { type: "SYNC_REQUEST"; entryIds: string[] }99  | { type: "SYNC_ENTRIES"; entries: Record<string, string> }100  | { type: "SYNC_TOMBSTONES"; tombstones: Record<string, number> }101  | { type: "SYNC_DEVICE_MANIFEST"; manifest: SignedDeviceManifest | null }102  | { type: "SYNC_DONE"; entriesReceived?: number; entriesSent?: number }103  | { type: "SYNC_ERROR"; error: string };104105// ---------------------------------------------------------------------------106// Browser-to-browser relay frames (phone forwards; see peach-android's107// RelayFrames.kt / RelayHub.kt, the source of truth this mirrors — NOT the108// older 4-frame PEACH_SYNC_RELAY_DESIGN.md sketch, which is stale).109//110// Type-only for now: nothing in this repo sends or handles these yet — no111// browser can listen, so relaying laptop-to-laptop traffic through a shared112// phone is unbuilt browser-side work, tracked separately. This exists so113// that work starts from the real wire contract instead of the stale doc.114// ---------------------------------------------------------------------------115116/** RelayMessage.targetDeviceId/peerDeviceId are the destination's Noise NK117 *  static public key as lowercase hex — NOT `PairedDevice.deviceId`. */118export type RelayMessage =119  | { type: "RELAY_REQUEST"; targetDeviceId: string }120  | { type: "RELAY_ACCEPT" }121  | { type: "RELAY_UNAVAILABLE" }122  | { type: "RELAY_DISABLED" }123  | { type: "RELAY_OPEN"; peerDeviceId: string }124  | { type: "RELAY_FRAME"; payload: string }125  | { type: "RELAY_CLOSE" };126127// ---------------------------------------------------------------------------128// v3 device manifest — propagates trust between mobile devices (v2 brief §4.3)129// ---------------------------------------------------------------------------130131export const DEVICE_MANIFEST_VERSION = 0x01 as const;132/** Manifests older than this are silently rejected (v2 brief §4.3). */133export const DEVICE_MANIFEST_FRESHNESS_MS = 5 * 60_000;134135/**136 * One device entry inside a signed manifest. `longTermPublicKeyHex` is the137 * X25519 sync identity key the receiver will use for Noise XX, exactly as138 * if the entry had been audio-paired locally. `signingPublicKeyHex` is the139 * peer's Ed25519 manifest-verification key — needed so a vouched mobile140 * can in turn verify manifests issued by the same vault from a different141 * device.142 */143export interface ManifestDeviceEntry {144  deviceId: string;145  deviceName: string;146  deviceType: DeviceType;147  longTermPublicKeyHex: string;148  signingPublicKeyHex: string;149  /**150   * Optional sync WebSocket port for native/mobile hosts. Preserving this in151   * vouched entries lets mobile peers connect without another audio pairing.152   */153  wsPort?: number;154  pairedAt: number;155}156157/** Body that gets canonicalised + signed. Wire format JSON-stable. */158export interface DeviceManifestBody {159  manifestVersion: number;160  issuerDeviceId: string;161  issuedAt: number;162  devices: ManifestDeviceEntry[];163}164165export interface SignedDeviceManifest {166  body: DeviceManifestBody;167  /** Hex-encoded 64-byte Ed25519 signature over canonicalised body. */168  signatureHex: string;169}170171export type SyncStatusV3 = "up_to_date" | "syncing" | "unreachable" | "never";