A federated thread travels under its address, not this app's key for it

This app keeps one `threads` map and namespaces it with prefixes: `group:`
for a group, `addr:` for somebody on another relay. `toShared` strips the
group one and never stripped the address one, so a backup written here
said `addr:ada@relay.example`, which is not an address. The iPhone writes
the bare one.

Neither side could read the other's federated conversations, and the
damaging direction is iPhone to Android: a bare address arriving here
lands in a map whose unprefixed keys are user threads.

Same mistake as the story id one commit ago, one field over, and the group
prefix beside it was already being handled correctly, which makes it a
slip rather than a decision. An internal storage detail does not belong in
a shared shape.

Coming back in, the prefix goes on unless the file already carries it, so
an older backup does not end up with two.

One test was pinning the bug: it asserted the shared shape contained the
prefixed key. It now asserts the bare address, and that the prefix is
absent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-16 15:09:06 -04:00
co-authored by Claude Opus 5
parent 88397a8bda
commit 3607a85459
2 changed files with 29 additions and 5 deletions
@@ -69,7 +69,17 @@ object BackupPayload {
}
tid.startsWith(ADDRESS_PREFIX) -> {
addressed.put(tid, mapArray(messages, ::messageToShared))
// The prefix comes OFF, exactly as the group one does
// above. It is how this app namespaces one `threads`
// map; the shared shape keys these by the address
// itself. Leaving it on wrote `addr:ada@relay.example`
// into the file, which is not an address, and the
// iPhone writes the bare one, so neither side could
// read the other's federated conversations.
addressed.put(
tid.removePrefix(ADDRESS_PREFIX),
mapArray(messages, ::messageToShared),
)
}
else -> {
@@ -302,9 +312,15 @@ object BackupPayload {
}
}
shared.optJSONObject(ADDRESS_THREADS)?.let { all ->
// Already prefixed when written, so they go back under their own id.
for (tid in all.keys()) {
all.optJSONArray(tid)?.let { threads.put(tid, mapArray(it, ::messageFromShared)) }
for (address in all.keys()) {
// The prefix goes back on. A file written before it was
// stripped carries it already, and adding a second one would
// make a thread nobody can find.
val tid =
if (address.startsWith(ADDRESS_PREFIX)) address else ADDRESS_PREFIX + address
all.optJSONArray(address)?.let {
threads.put(tid, mapArray(it, ::messageFromShared))
}
}
}