import Foundation
import IrdP2PCrypto
import IrdP2PSignaling
/// A replayed, decrypted mailbox message.
public struct SealedItem: Sendable, Equatable {
public var seq: MailboxSeq
public var id: String
public var date: Date
public var message: Message
}
/// The offline mailbox of one room: deposits are sealed under the room-wide
/// mailbox key (random IVs) and filed under a mailbox room id the node cannot
/// correlate with the relay room. Undecryptable entries are dropped silently.
public actor SealedMailbox {
public nonisolated let mailboxRoomId: String
public nonisolated let roomId: RoomId
private let cipher: AtRestCipher
private let client: MailboxClient
public let outbox: MailboxOutbox
init(secret: RoomSecret, appSalt: String, client: MailboxClient, clock: any P2PClock) {
let crypto = RoomCrypto(secret: secret, appSalt: appSalt, peerId: .random())
roomId = crypto.roomId
mailboxRoomId = crypto.mailboxRoomId
cipher = crypto.mailboxCipher
self.client = client
outbox = MailboxOutbox(client: client, clock: clock)
}
/// Seals and deposits now.
public func deposit(_ message: Message) async throws -> DepositResult {
try await client.deposit(room: mailboxRoomId, seal(message))
}
/// Seals and queues for `outbox.flush()` (retries with backoff).
public func enqueue(_ message: Message) async {
await outbox.enqueue(room: mailboxRoomId, payload: seal(message))
}
public func replay(after: MailboxSeq? = nil, before: MailboxSeq? = nil, limit: Int = 100) async throws -> (items: [SealedItem], page: ReplayPage) {
let page = try await client.replay(room: mailboxRoomId, after: after, before: before, limit: limit)
return (page.messages.compactMap(open), page)
}
public func replayAll(after: MailboxSeq?) async throws -> (items: [SealedItem], cursor: MailboxSeq?, gap: Bool) {
let r = try await client.replayAll(room: mailboxRoomId, after: after)
return (r.messages.compactMap(open), r.cursor, r.gap)
}
private func seal(_ message: Message) -> Data {
let inner: InnerMessage
switch message {
case .json(let v): inner = .msg(v)
case .data(let d): inner = .bin(d)
}
return cipher.seal(inner.json.serializedData)
}
private func open(_ m: MailboxMessage) -> SealedItem? {
guard let plain = cipher.open(m.payload), let v = try? JSONValue.parse(plain), let inner = InnerMessage(json: v) else { return nil }
switch inner {
case .msg(let payload): return SealedItem(seq: m.seq, id: m.id, date: m.date, message: .json(payload))
case .bin(let data): return SealedItem(seq: m.seq, id: m.id, date: m.date, message: .data(data))
default: return nil
}
}
}