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// ---------------------------------------------------------------------------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`. Mutual Noise XX49 * authentication on every WebSocket connection rejects any peer not50 * present in this registry.51 *52 * `lastKnownIp` is intentionally NOT stored here — it's per-network and53 * lives in `chrome.storage.local` keyed by `longTermPublicKeyHex`. See54 * `getLastKnownIp` / `setLastKnownIp` in `paired-devices.ts`.55 */56export interface PairedDevice {57 deviceId: string;58 deviceName?: string;59 deviceType?: DeviceType;60 longTermPublicKeyHex?: string;61 /**62 * Long-term Ed25519 verification key for the peer's device-manifest63 * signatures (v2 brief §4.3). Derived deterministically from the same64 * master key as `longTermPublicKeyHex`, via `deriveSyncSigningKeypair`.65 */66 signingPublicKeyHex?: string;67 /** v2 brief §4.2 — distinguishes direct-paired vs vouched-via-manifest. */68 trustSource?: TrustSource;69 /**70 * The peer's WebSocket sync port. Mobile hosts pick a random high port71 * (49152–65535) at first launch and announce it inside the encrypted72 * pairing channel; the extension caches it here.73 */74 wsPort?: number;75 pairedAt: number;76 lastSyncedAt?: number;77}7879// ---------------------------------------------------------------------------80// v3 wire protocol — phase-2 WebSocket manifest sync81// ---------------------------------------------------------------------------8283/**84 * v3 wire protocol — after Noise XX is open.85 *86 * Every frame is JSON, then ChaCha20-Poly1305 encrypted by Noise XX. The87 * WebSocket transport sees only ciphertext.88 *89 * The manifest carries per-entry timestamps; the receiver requests entries90 * where the remote timestamp beats its own. Entries traverse as opaque91 * encrypted blobs — the sync layer never decrypts vault data.92 */93export type SyncMessage =94 | { type: "SYNC_HELLO"; vaultId: string; deviceId: string; version: number; deviceTime: number }95 | { type: "SYNC_MANIFEST"; entries: Record<string, number>; tombstones: Record<string, number> }96 | { type: "SYNC_REQUEST"; entryIds: string[] }97 | { type: "SYNC_ENTRIES"; entries: Record<string, string> }98 | { type: "SYNC_TOMBSTONES"; tombstones: Record<string, number> }99 | { type: "SYNC_DEVICE_MANIFEST"; manifest: SignedDeviceManifest }100 | { type: "SYNC_DONE"; entriesReceived?: number; entriesSent?: number }101 | { type: "SYNC_ERROR"; error: string };102103// ---------------------------------------------------------------------------104// v3 device manifest — propagates trust between mobile devices (v2 brief §4.3)105// ---------------------------------------------------------------------------106107export const DEVICE_MANIFEST_VERSION = 0x01 as const;108/** Manifests older than this are silently rejected (v2 brief §4.3). */109export const DEVICE_MANIFEST_FRESHNESS_MS = 5 * 60_000;110111/**112 * One device entry inside a signed manifest. `longTermPublicKeyHex` is the113 * X25519 sync identity key the receiver will use for Noise XX, exactly as114 * if the entry had been audio-paired locally. `signingPublicKeyHex` is the115 * peer's Ed25519 manifest-verification key — needed so a vouched mobile116 * can in turn verify manifests issued by the same vault from a different117 * device.118 */119export interface ManifestDeviceEntry {120 deviceId: string;121 deviceName: string;122 deviceType: DeviceType;123 longTermPublicKeyHex: string;124 signingPublicKeyHex: string;125 /**126 * Optional sync WebSocket port for native/mobile hosts. Preserving this in127 * vouched entries lets mobile peers connect without another audio pairing.128 */129 wsPort?: number;130 pairedAt: number;131}132133/** Body that gets canonicalised + signed. Wire format JSON-stable. */134export interface DeviceManifestBody {135 manifestVersion: number;136 issuerDeviceId: string;137 issuedAt: number;138 devices: ManifestDeviceEntry[];139}140141export interface SignedDeviceManifest {142 body: DeviceManifestBody;143 /** Hex-encoded 64-byte Ed25519 signature over canonicalised body. */144 signatureHex: string;145}146147export type SyncStatusV3 = "up_to_date" | "syncing" | "unreachable" | "never";