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