feat(backup): add ConversationBackup domain models (BackupMessageRecord, BackupCursor, BackupManifest, RestoreRequest)
This commit is contained in:
parent
14bfbd04c8
commit
6b469357c1
|
|
@ -0,0 +1,84 @@
|
||||||
|
package app.closer.domain.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain models for the E2EE conversation backup & restore system.
|
||||||
|
*
|
||||||
|
* The backup is a couple-key-encrypted snapshot of the couple's conversation history (messages +
|
||||||
|
* media refs) so a new/wiped device can restore it, and either partner can restore for the other.
|
||||||
|
* See the plan + `docs/Engineering_Reference_Manual.md` (backup/restore).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Current backup wire-format version. Bump when the record/manifest shape changes. */
|
||||||
|
const val BACKUP_SCHEMA_VERSION = 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One backed-up message. The `text` field keeps the message's existing `enc:v1:` value **verbatim**
|
||||||
|
* (no decrypt/re-encrypt on backup) — the whole chunk is then wrapped in a couple-key `enc:v1:`
|
||||||
|
* envelope, so metadata (sender/timestamp/type) is also ciphertext at rest.
|
||||||
|
*/
|
||||||
|
data class BackupMessageRecord(
|
||||||
|
val messageId: String,
|
||||||
|
val conversationId: String,
|
||||||
|
val conversationType: String = "main",
|
||||||
|
val authorUserId: String = "",
|
||||||
|
val type: String = "text", // text | image | voice
|
||||||
|
val encText: String? = null, // existing enc:v1: body for text messages
|
||||||
|
val mediaUrl: String? = null, // tokenized Storage URL for image/voice
|
||||||
|
val durationMs: Long? = null,
|
||||||
|
val createdAt: Long = 0L, // resolved server timestamp (millis)
|
||||||
|
val deleted: Boolean = false,
|
||||||
|
val reactions: Map<String, String> = emptyMap()
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cursor into the message stream: incremental backup appends everything strictly after this point.
|
||||||
|
* Ordered by (createdAt, messageId) so ties are stable across both partners' devices.
|
||||||
|
*/
|
||||||
|
data class BackupCursor(
|
||||||
|
val createdAt: Long = 0L,
|
||||||
|
val messageId: String = ""
|
||||||
|
) {
|
||||||
|
fun isAfter(other: BackupCursor): Boolean =
|
||||||
|
createdAt != other.createdAt && createdAt > other.createdAt ||
|
||||||
|
(createdAt == other.createdAt && messageId > other.messageId)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val ZERO = BackupCursor(0L, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backup manifest (`couples/{id}/backup/manifest`). `generation` drives optimistic concurrency so the
|
||||||
|
* two partners' devices can both write without clobbering each other (CAS in a transaction).
|
||||||
|
*/
|
||||||
|
data class BackupManifest(
|
||||||
|
val schemaVersion: Int = BACKUP_SCHEMA_VERSION,
|
||||||
|
val generation: Long = 0L,
|
||||||
|
val snapshotUrl: String? = null,
|
||||||
|
val snapshotOwner: String = "", // uid whose Storage path holds the snapshot (for targeted cleanup)
|
||||||
|
val snapshotChecksum: String? = null,
|
||||||
|
val snapshotThroughCursor: BackupCursor = BackupCursor.ZERO,
|
||||||
|
val latestChunkSeq: Long = 0L,
|
||||||
|
val messageCount: Int = 0, // for the "Last backed up: N messages" indicator + rollback cross-check
|
||||||
|
val updatedAt: Long = 0L,
|
||||||
|
val updatedBy: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Lifecycle of a partner-assisted restore request (`couples/{id}/restore_requests/{recipientUid}`). */
|
||||||
|
enum class RestoreStatus { REQUESTED, READY, RESTORED, DECLINED, EXPIRED }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A recovering device (the recipient) asks its partner to re-share the couple key + refresh the backup.
|
||||||
|
* The partner writes [keybox] (the couple key ECIES-wrapped to [recipientPublicKey]) after confirming
|
||||||
|
* the out-of-band [verificationCode]. Never contains plaintext key material readable by the server.
|
||||||
|
*/
|
||||||
|
data class RestoreRequest(
|
||||||
|
val recipientUid: String = "",
|
||||||
|
val recipientPublicKey: String = "", // pub:v1:...
|
||||||
|
val requestNonce: String = "",
|
||||||
|
val keybox: String? = null, // keybox:v1:... (partner-written)
|
||||||
|
val status: RestoreStatus = RestoreStatus.REQUESTED,
|
||||||
|
val createdAt: Long = 0L,
|
||||||
|
val expiresAt: Long = 0L,
|
||||||
|
val fulfilledAt: Long? = null
|
||||||
|
)
|
||||||
Loading…
Reference in New Issue