Skip to content

River UI: structurally prevent re-accept orphaning in the invitation GET handler (follow-up to #365) #367

Description

@sanity

Background

PR #366 fixes the user-visible #365 bug — accepting an invite to a room you're already in joins you twice and orphans the original membership — with an entry-level guard in receive_invitation_modal::accept_invitation (plus the check_membership_status modal routing). That guard closes every realistic path to the symptom.

This issue tracks a defense-in-depth gap one layer deeper, in the invitation-accept GET handler (ui/src/components/app/freenet_api/response_handler/get_response.rs).

The gap

If the entry-level guard is ever skipped — e.g. ROOMS.try_read() returns Err at accept time, or some future caller populates PENDING_INVITES for a room the user is already in — the GET handler still runs the full new-member accept for an already-member room:

Why a partial backstop is NOT enough

A first attempt in PR #366 guarded only the self_sk overwrite ("don't clobber self_sk if the held key is already a member"). External (Codex) review correctly flagged this as worse, not better: it preserves the old self_sk while the rest of the handler still records the new key's credentials and PUTs the new member. That leaves self_sk and self_authorized_member/self_member_info pointing at different keys — a split/inconsistent local state that breaks identity export and rejoin/member-info flows. The original (unconditional-clobber) behavior is at least internally consistent. So the partial guard was reverted.

The correct fix

Detect "already a member under the held self_sk" early — before build_state_for_put — and short-circuit the entire accept: do not PUT a duplicate member, do not record new self-credentials, do not change self_sk. Treat it as a no-op / refresh (merge the retrieved state, repopulate secrets, clear the pending invite). This needs a synchronous ROOMS read near the top of the is_pending_invite branch, with a fall-through only when ROOMS is genuinely unreadable.

Add a regression/pin test for the short-circuit. This is the structural backstop that makes #365 impossible regardless of how the GET was triggered.

Refs: get_response.rs (~249 build_state_for_put, ~335 self_sk overwrite, ~411 record_invite_credentials). Follow-up to #365 / PR #366.

[AI-assisted - Claude]

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions