package com.peachpasswords.sync import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json /** * Chirp Sync wire protocol — exact port of the browser `ChirpSyncMessage` * union (shared/sync-types.ts). Each frame is JSON with a `type` discriminator, * sent over the Noise-NK-encrypted session. Entries travel as opaque base64 * ciphertext envelopes; the coordinator never sees plaintext. */ @Serializable sealed class SyncMessage { @Serializable @SerialName("SYNC_HELLO") data class Hello( val vaultId: String, val deviceId: String, val version: Long, val deviceTime: Long, ) : SyncMessage() @Serializable @SerialName("SYNC_MANIFEST") data class Manifest( val entries: Map, val tombstones: Map, ) : SyncMessage() @Serializable @SerialName("SYNC_REQUEST") data class Request(val entryIds: List) : SyncMessage() @Serializable @SerialName("SYNC_ENTRIES") data class Entries(val entries: Map) : SyncMessage() @Serializable @SerialName("SYNC_TOMBSTONES") data class Tombstones(val tombstones: Map) : SyncMessage() @Serializable @SerialName("SYNC_DEVICE_MANIFEST") data class DeviceManifest(val manifest: SignedDeviceManifest?) : SyncMessage() @Serializable @SerialName("SYNC_DONE") data class Done( val entriesReceived: Int = 0, val entriesSent: Int = 0, ) : SyncMessage() @Serializable @SerialName("SYNC_ERROR") data class Error(val error: String) : SyncMessage() } @Serializable data class ManifestDeviceEntry( val deviceId: String, val deviceName: String, val deviceType: String, val longTermPublicKeyHex: String, val signingPublicKeyHex: String, val wsPort: Int? = null, val pairedAt: Long, ) @Serializable data class DeviceManifestBody( val manifestVersion: Int, val issuerDeviceId: String, val issuedAt: Long, val devices: List, ) @Serializable data class SignedDeviceManifest( val body: DeviceManifestBody, /** Hex-encoded 64-byte Ed25519 signature over the canonicalised body. */ val signatureHex: String, ) /** * The wire codec. `classDiscriminator = "type"` reproduces the browser's flat * `{ "type": "SYNC_HELLO", ... }` shape exactly; `encodeDefaults` keeps the * optional SYNC_DONE counters on the wire. */ val SyncJson: Json = Json { classDiscriminator = "type" encodeDefaults = true ignoreUnknownKeys = true } fun SyncMessage.encodeToBytes(): ByteArray = SyncJson.encodeToString(SyncMessage.serializer(), this).toByteArray(Charsets.UTF_8) fun decodeSyncMessage(bytes: ByteArray): SyncMessage = SyncJson.decodeFromString(SyncMessage.serializer(), String(bytes, Charsets.UTF_8))