mgmt: self-serve link of an invite-only room is unsatisfiable — the invite gate rejects the invite the link reply instructs #111
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
dark/nether-voicebridge#111
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while shaping
fed_link_v12(#109). Thelinkprobe-join failure reply says:But for a self-serve user that instruction is unsatisfiable: their invite is auto-rejected by the invite gate (mgmt.rs:1263 — "rejecting non-direct invite from self-serve user (DMs only)"), leaving a
leavemembership that then blocks the probe join outright (live-confirmed on the fixture: v12 invite-only room, bot membershipleave, probe join 403 "cannot join a room that is not public"). Operators are unaffected (their invites are accepted).So self-serve
linkcurrently only works for publicly-joinable (or restricted-with-allow) rooms. Options, not mutually exclusive: (a) reword the failure reply for self-serve users (honest: "make the room public/joinable, or ask the operator"); (b) a narrow invite-gate exemption — accept a non-direct invite from a user with a PENDING link naming that exact room (time-boxed, reap on abandon, #83/#87 semantics); (c) leave as policy, document. The e2e scenario sidesteps it with a public v12 room, which still covers the whole #103 surface.Design proposal: pending-link invite-gate exemption (the #111 remainder)
The beta.3 reword made the self-serve join-failure reply honest ("make the room publicly joinable, or ask the operator"). This proposal makes it unnecessary for invite-only/restricted rooms: let a self-serve user invite the bot when — and only when — the invite matches a link attempt they just made.
Flow (user's view)
link <name> <room> <guild> <channel>on their invite-only room."I can't join that room on my own. Invite me (@bot) to it within 10 minutes and I'll continue this link automatically."
/bridge approve, #87 reap, etc.) is unchanged.Mechanics (coordinates in
crates/matrix-rtc/src/mgmt.rs)awaiting_invite: StdMutex<HashMap<(String /*requester*/, String /*room_id*/), AwaitingInvite>>onBotCtx, sibling ofpending(mgmt.rs:192).AwaitingInvitecarries the parsedNewTenant,origin_room, andcreated: Instant. Not merged intoPendingLink— different lifecycle: no approval code exists yet, no room membership exists yet, so expiry is a pure map-drop (nothing to reap), vs. #87's leave-on-expiry.handle_link, only on the join-failure branch (probe_room_prereqs's first match arm), only for self-serve requesters, and only after every existing gate has already passed (ban, per-owner/per-guild caps,max_link_attempts, global budget — they all run before the probe today, so the intent inherits their protection). Re-runninglinkfor the same room replaces the intent (idempotent). The tier-awarejoin_failure_reply(beta.3) gains the "invite me within N minutes" text for the self-serve+intent-created case.handle_invite_gated(mgmt.rs:1256), a new arm in the self-serve branch, checked after ban / global-budget, replacing the flat!i.is_directreject (mgmt.rs:1286) with: non-direct invite + matching intent whereintent key == (i.sender, room_id)→ accept, remove intent, resume the link. Non-direct invite with no matching intent → rejected exactly as today.probe_joined = trueexplicitly — otherwiseprobe_room_prereqs'salready_memberread (mgmt.rs:2038) would see the invite-created join and the #83 fail-safe would refuse to reap it on a failed probe, letting a hostile user pin the bot in a room via a link that then fails the authority check.Two traps this design explicitly avoids
linkis a state-changing command and stamps the per-sender cooldown; the follow-up invite would arrive inside that window and be declined by the cooldown gate (mgmt.rs:1332) — the exemption would be stillborn, flakily, only whenmgmt_cooldown_secsis armed (which is exactly the fixture config, not default prod — the kind of gap #110 taught us about). Fix: a matched-intent invite bypasses the #89a cooldown check and does not stamp it. Safe because it is not a churn vector: it consumes a single-use intent whose creation is already velocity-gated bymax_link_attempts+ the global budget.probe_joined = trueabove; a resumed link that fails its probe leaves the room like any probe-created join (#83).Bounds & expiry
origin_room("invite window expired; runlinkagain"). Pruned lazily on insert/lookup + by the existing 300 s maintenance sweep.linkkeeps it 1), global cap sharing the spirit ofPENDING_LINK_CAP(16).audit()-logged like the existing invite decisions (#78).Security summary (open-registration surface)
The invite path gains no authority: everything a matched invite unlocks was already reachable through a public room. Ban list and global budget still run first; the intent can only exist for a requester who passed every
linkgate seconds earlier; matching is exact (sender, room); the intent is single-use with a 10-min TTL; failed resumption reaps the join (#83); unapproved success reaps on the normal #87 path.Test plan
mgmt_link_invite_only— self-serve user mints an invite-only room,link→ awaiting-invite reply, invites the bot → pending approval code arrives; plus assert the negative (non-direct invite with no intent still rejected — guards today's behaviour). Regression:mgmt_self_serve_link,fed_link_v12,mgmt_invite_offline.fed_link_invite_onlyon the nether.autos fixture) as a follow-up once the same-server scenario is green.Scope
~150–200 lines in
mgmt.rs+ one e2e scenario. No config changes, no migration, no effect on operator-tier flows. Closes #111 when shipped.Status: proposal only — awaiting operator review before implementation ships.
Shipped in two stages, both merged to master; closing.
linkof an un-joinable room arms a single-use(requester, room_id)intent (TTL[limits] invite_intent_ttl_secs, default 30 min); the requester's own invite for that exact room is accepted through the gate (ban check first; #89a cooldown + global budget bypassed for the matched, already-velocity-gated intent) and the link resumes automatically to the pending Discord-approval code. Failed resumes reap the join (#83); unapproved pendings reap on the #87 path.listnow surfaces pending approvals + intents. Unified across tiers.Evidence: live scenario
mgmt_link_invite_only(suite → 27) PASS first try, proving the full chain including the cooldown bypass withmgmt_cooldown_secsarmed; regressionsmgmt_self_serve_link/mgmt_invite_offline/mgmt_dm_e2ee/fed_link_v12green; 124 unit tests + clippy-D warnings. Docs updated to the invite-after-link flow (self-serve walkthrough + hosted-instance).Follow-up noted in PR #119 (not this issue):
greet_when_joined's #85 anti-spoof leave appears to break the manual operator invite-then-relink flow for >2-member rooms — the intent path sidesteps it, but the manual path deserves its own issue.