sync-types.ts
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// ---------------------------------------------------------------------------1011export type DeviceType = "extension" | "android" | "ios";1213// Numeric tags for DeviceType on the wire — not the string form.14export const DEVICE_TYPE_EXTENSION = 0x01 as const;15export const DEVICE_TYPE_ANDROID = 0x02 as const;16export const DEVICE_TYPE_IOS = 0x03 as const;1718export type DeviceTypeByte =19 | typeof DEVICE_TYPE_EXTENSION20 | typeof DEVICE_TYPE_ANDROID21 | typeof DEVICE_TYPE_IOS;2223// ---------------------------------------------------------------------------24// Long-term paired device registry25// ---------------------------------------------------------------------------2627/**28 * Trust source of a paired-device entry — per v2 brief §4.2.29 *30 * "direct" — established via a pairing-code session with this device.31 * "vouched" — introduced by a directly-paired peer via a signed device32 * manifest during a normal sync, then confirmed by the user33 * (MV-7). Functionally equivalent to direct for sync purposes;34 * surfaced with an "Added automatically" badge in Settings.35 */36export type TrustSource = "direct" | "vouched";3738/**39 * v3 paired-device record. Stored in `chrome.storage.local` — cryptographic40 * keys must not leave the device via a third-party cloud service.41 *42 * `longTermPublicKeyHex` is the counterpart's vault-derived X25519 sync43 * identity static key — see `deriveSyncIdentityKeypair`. Every WebSocket44 * reconnect runs Noise NK against this key (authenticating the responder45 * only) followed by a PAIR_IDENTITY exchange checked against this registry —46 * see HANDSHAKE_STANDARDIZE_ON_NK.md.47 *48 * `lastKnownIp` is intentionally NOT stored here — it's per-network and49 * lives in `chrome.storage.local` keyed by `longTermPublicKeyHex`. See50 * `getLastKnownIp` / `setLastKnownIp` in `paired-devices.ts`.51 */52export interface PairedDevice {53 deviceId: string;54 deviceName?: string;55 deviceType?: DeviceType;56 longTermPublicKeyHex?: string;57 /**58 * Long-term Ed25519 verification key for the peer's device-manifest59 * signatures (v2 brief §4.3). Derived deterministically from the same60 * master key as `longTermPublicKeyHex`, via `deriveSyncSigningKeypair`.61 */62 signingPublicKeyHex?: string;63 /** v2 brief §4.2 — distinguishes direct-paired vs vouched-via-manifest. */64 trustSource?: TrustSource;65 /**66 * The peer's WebSocket sync port. Mobile hosts pick a random high port67 * (49152–65535) at first launch and announce it inside the encrypted68 * pairing channel; the extension caches it here.69 */70 wsPort?: number;71 /** Pair-only 32-byte reconnect capability, derived from the first handshake. */72 rendezvousSecretHex?: string;73 /** Validated wss relay used by this pair. */74 relayUrl?: string;75 pairedAt: number;76 lastSyncedAt?: number;77}7879// ---------------------------------------------------------------------------80// v3 wire protocol — phase-2 WebSocket manifest sync81// ---------------------------------------------------------------------------8283/**84 * v3 wire protocol — after Noise NK is open and PAIR_IDENTITY has been85 * exchanged (see HANDSHAKE_STANDARDIZE_ON_NK.md).86 *87 * Every frame is JSON, then ChaCha20-Poly1305 encrypted by Noise NK. The88 * WebSocket transport sees only ciphertext.89 *90 * The manifest carries per-entry timestamps; the receiver requests entries91 * where the remote timestamp beats its own. Entries traverse as opaque92 * encrypted blobs — the sync layer never decrypts vault data.93 */94export type SyncMessage =95 | { type: "SYNC_HELLO"; vaultId: string; deviceId: string; version: number; deviceTime: number }96 | { type: "SYNC_MANIFEST"; entries: Record<string, number>; tombstones: Record<string, number> }97 | { type: "SYNC_REQUEST"; entryIds: string[] }98 | { type: "SYNC_ENTRIES"; entries: Record<string, string> }99 | { type: "SYNC_TOMBSTONES"; tombstones: Record<string, number> }100 | { type: "SYNC_DEVICE_MANIFEST"; manifest: SignedDeviceManifest | null }101 | { type: "SYNC_DONE"; entriesReceived?: number; entriesSent?: number }102 | { type: "SYNC_ERROR"; error: string };103104// ---------------------------------------------------------------------------105// Browser-to-browser relay frames (phone forwards; see peach-android's106// RelayFrames.kt / RelayHub.kt, the source of truth this mirrors — NOT the107// older 4-frame PEACH_SYNC_RELAY_DESIGN.md sketch, which is stale).108//109// Type-only for now: nothing in this repo sends or handles these yet — no110// browser can listen, so relaying laptop-to-laptop traffic through a shared111// phone is unbuilt browser-side work, tracked separately. This exists so112// that work starts from the real wire contract instead of the stale doc.113// ---------------------------------------------------------------------------114115/** RelayMessage.targetDeviceId/peerDeviceId are the destination's Noise NK116 * static public key as lowercase hex — NOT `PairedDevice.deviceId`. */117export type RelayMessage =118 | { type: "RELAY_REQUEST"; targetDeviceId: string }119 | { type: "RELAY_ACCEPT" }120 | { type: "RELAY_UNAVAILABLE" }121 | { type: "RELAY_DISABLED" }122 | { type: "RELAY_OPEN"; peerDeviceId: string }123 | { type: "RELAY_FRAME"; payload: string }124 | { type: "RELAY_CLOSE" };125126// ---------------------------------------------------------------------------127// v3 device manifest — propagates trust between mobile devices (v2 brief §4.3)128// ---------------------------------------------------------------------------129130export const DEVICE_MANIFEST_VERSION = 0x01 as const;131/** Manifests older than this are silently rejected (v2 brief §4.3). */132export const DEVICE_MANIFEST_FRESHNESS_MS = 5 * 60_000;133134/**135 * One device entry inside a signed manifest. `longTermPublicKeyHex` is the136 * X25519 sync identity key the receiver will use for Noise NK, exactly as137 * if the entry had been audio-paired locally. `signingPublicKeyHex` is the138 * peer's Ed25519 manifest-verification key — needed so a vouched mobile139 * can in turn verify manifests issued by the same vault from a different140 * device.141 */142export interface ManifestDeviceEntry {143 deviceId: string;144 deviceName: string;145 deviceType: DeviceType;146 longTermPublicKeyHex: string;147 signingPublicKeyHex: string;148 /**149 * Optional sync WebSocket port for native/mobile hosts. Preserving this in150 * vouched entries lets mobile peers connect without another audio pairing.151 */152 wsPort?: number;153 pairedAt: number;154}155156/** Body that gets canonicalised + signed. Wire format JSON-stable. */157export interface DeviceManifestBody {158 manifestVersion: number;159 issuerDeviceId: string;160 issuedAt: number;161 devices: ManifestDeviceEntry[];162}163164export interface SignedDeviceManifest {165 body: DeviceManifestBody;166 /** Hex-encoded 64-byte Ed25519 signature over canonicalised body. */167 signatureHex: string;168}169170export type SyncStatusV3 = "up_to_date" | "syncing" | "unreachable" | "never";