Keep DMs canonical across transport aliases (#1461)

Joins the two authenticated aliases of one account — the full noise-key conversation ID used by the Nostr path and its derived 16-hex mesh short ID — into one canonical conversation, migrating messages and handing off an open DM sheet (short ID while connected, stable ID offline). Delivery/read-ack outbox removal is scoped to the authenticated peer's aliases with per-peer tombstones, so a colliding message ID queued for a different peer survives, including across protected-data cold-load recovery. Geohash DMs never enter the alias path (their conversation keys carry no noise key).
This commit is contained in:
jack
2026-07-26 15:03:58 +02:00
committed by GitHub
parent 660632ef6b
commit a4d294015a
6 changed files with 558 additions and 22 deletions
@@ -207,6 +207,42 @@ struct MessageOutboxStoreTests {
#expect(MessageOutboxStore(keychain: keychain, fileURL: fileURL).load().isEmpty)
}
@Test func deferredScopedRemovalTombstoneFiltersOnlySelectedPeer() {
let fileURL = makeTempURL()
defer { try? FileManager.default.removeItem(at: fileURL) }
let keychain = MockKeychain()
let acknowledgedPeer = PeerID(str: "0000000000000001")
let otherPeer = PeerID(str: "0000000000000002")
MessageOutboxStore(keychain: keychain, fileURL: fileURL).save([
acknowledgedPeer: [makeMessage("shared-id", content: "for acknowledged peer")],
otherPeer: [makeMessage("shared-id", content: "for other peer")]
])
var protectedDataUnavailable = true
let restored = MessageOutboxStore(
keychain: keychain,
fileURL: fileURL,
readData: { url in
if protectedDataUnavailable {
throw NSError(domain: NSCocoaErrorDomain, code: NSFileReadNoPermissionError)
}
return try Data(contentsOf: url)
}
)
#expect(restored.load().isEmpty)
restored.recordRemoval(messageID: "shared-id", for: [acknowledgedPeer])
restored.save([:])
protectedDataUnavailable = false
let recovered = restored.retryDeferredLoad()
#expect(recovered?[acknowledgedPeer] == nil)
#expect(recovered?[otherPeer]?.map(\.messageID) == ["shared-id"])
let relaunched = MessageOutboxStore(keychain: keychain, fileURL: fileURL).load()
#expect(relaunched[acknowledgedPeer] == nil)
#expect(relaunched[otherPeer]?.map(\.messageID) == ["shared-id"])
}
@Test func wipeRemovesFileAndKey() {
let fileURL = makeTempURL()
let keychain = MockKeychain()