Skip to content

fix(relay): join invite claimers to open channels - #4196

Draft
brow wants to merge 1 commit into
mainfrom
tomb/invite-claim-auto-join-open-channels
Draft

fix(relay): join invite claimers to open channels#4196
brow wants to merge 1 commit into
mainfrom
tomb/invite-claim-auto-join-open-channels

Conversation

@brow

@brow brow commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

What changed?

Accepting a community invite previously granted relay-level membership only. The claimer joined the community but was not a member of any channel, so clients that derive their channel list from per-channel membership showed an empty community. This adds channel membership to the claim path: on a genuine new join, the claimer is added to the community's active open, non-DM channels, and NIP-29 discovery is republished for each so kind 39002 advertises the new membership. Kind 39002 is the event mobile's channel list is built from, so the discovery republish is the load-bearing half, not the database write alone.

Idempotent reclaims (AlreadyMember) run no channel-join side effects, and neither do exhausted, expired, invalid, policy-rejected, or rate-limited outcomes.

This covers future claimers. Users who already claimed before this change are in relay_members and get already_member with no membership write, so they are not healed by this PR. #4199 covers that population with client-side open-channel discovery. The two together cover both.

Why?

Reported on Buzz mobile: accepting a community invite added the community to the list, but no channels or messages ever loaded. The claim endpoint wrote relay_members and emitted relay-level NIP-43 events, and never called add_member for any channel.

Relay membership is already durable before these side effects run. A channel membership or discovery failure is therefore logged at warn and does not turn a successful claim into an error, matching the existing NIP-43 partial-failure handling in this flow. That is a deliberate availability tradeoff with a known cost: a partial failure returns 200 joined with an incomplete channel list, and because a retry becomes already_member it does not self-heal. No test covers that path.

How is it tested?

The new assertions are #[ignore = "requires Postgres"], so a plain cargo test -p buzz-relay does not execute them. They run in CI under Backend Integration (relay e2e), step Invite security tests, which selects the invite module with --run-ignored ignored-only (18 of 18 passing at this head).

Added tests, in api::invites::tests:

  • A new v2 claimer becomes a member of an active open channel, and kind 39002 advertises that membership.
  • Private and archived channels are excluded, and an AlreadyMember reclaim adds nothing.

Mutation results, per call site rather than in aggregate:

  • Removing the v2 claim-path call fails the open-channel membership assertion. Removing emit_group_discovery_events fails the kind 39002 assertion. Forcing side effects on AlreadyMember, and removing the archived guard, each fail their assertions.
  • Removing only the v1 call site leaves the suite green: the v1 auto-join has no committed coverage. It was verified working out of band with a throwaway probe driving the v1 HMAC path, whose control (same probe, v1 call site removed) fails as expected.

Claims that hold by code reading but have no test behind them at this head, so a future regression would not be caught by CI:

  • The loop joins every active open non-DM channel, but the fixture has exactly one such channel, so a break after the first join also passes. "Every" is not test-enforced.
  • Each membership write calls invalidate_membership; removing that call leaves the suite green.
  • DM channels are excluded by an explicit channel_type == "dm" guard; removing it leaves the suite green. It is not dead code: create_dm hardcodes visibility='private', but the kind 9002 metadata-edit path changes visibility with no channel-type guard, so an open DM is reachable.
  • Private exclusion is observed in the integration test, but it is enforced by add_member rejecting private admission when invited_by is None. Removing the Some("open") visibility filter still passes, so the list-level filter is not proven load-bearing.

Full package suite at this head: 835 passed, 37 ignored, with api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo failing (504 instead of 200). That failure reproduces identically at the base commit and is additionally flaky, so it is pre-existing and not from this branch. cargo clippy -p buzz-relay --all-targets -- -D warnings passes.

The uncovered-claim list and the per-call-site mutation split come from two independent blind reviews of this exact head, which converged on the Some("open") finding separately.

Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
@brow

brow commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

@builderbot review

@brow

brow commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant