Files
mantra-kmp/composeApp
Kgothatso Ngako 679d5249b2 fix(groups): refuse a welcome for a group another profile on this device holds, and say so in the room
Phase 6 of docs/curated-to-mantra-profiles.md, the fourth decision's
follow-up, written natively before the several-profiles line reaches
users. ChatRoom is keyed by the group id alone, and the MLS state, the
DKG sessions and the FROST shares all hang off the room; before several
profiles on a device the second local member could not exist, and after
it a user can invite their own second profile into their own collective.

What the DAO did with that Welcome was already a refusal, by accident:
the welcome branch of indexNostrEvent checks for an existing room before
it writes one, so the joiner's state never overwrote the holder's -- but
the branch could not tell a redelivered Welcome for our own room from a
Welcome for a room another profile holds, logged "Chat Room already
exists" at warning level for both, and told nobody. The invitee never
joined, the inviter's device believed the invite had landed, and the
failure had the exact shape of "the other device never got it".

The branch now distinguishes the two by the room's userPublicKey. The
same profile's redelivery stays the quiet no-op it was. Another profile's
Welcome is refused with a warning naming both keys, and a membership line
is written into the room that exists -- the holder's, because it is the
only room on the device the group has and the holder is the one who can
act on it: leave, or tell the inviter which profile to invite instead.
The line is TYPE_WELCOME_REFUSED_OTHER_PROFILE, in MEMBERSHIP_TYPES so the
transcript draws it as a notice rather than a bubble, with the error tint
and a PersonOff icon since the person and not the invite is what it is
about; its content is a whole sentence naming the inviter, the invitee
and the holder, resolved at write time the way the other membership lines
are. The invitee's key package bundle is left unconsumed, as the branch
always left it; a second Welcome for the same package arrives here again
and writes a second line.

The test is two devices for real: an MLS group on the inviter's database,
a key package whose private keys the invitee's database holds -- the
fixture now returns the bundle beside the public package, and
marmotKeyPackageFor is a projection of it -- and the Welcome the invite
queued, sealed the way sealGiftWrapPayload seals one and stored through
storeNostrEvent under the invitee's key. Three cases: the group held by
another profile is refused, its row and MLS state untouched, the line in
the room naming all three and the bundle unconsumed; the same profile
welcomed twice writes nothing; and with no profile in the way the Welcome
is a join, which is what shows the fixture is real. jvmTest 868 -> 871,
both compilers clean, every audit budget met.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 16:07:45 +02:00
..