Skip to main content
← Back to list
01Issue
BugOpenSwamp ClubPublic
AssigneesNone

Relationships

#1809 Invite typeahead searches username prefix only, so an operative cannot be found by name or email

Opened by keeb · 8/24/2026

Found by code scan of the collective invite flow.

The defect

/api/v1/operatives/search backs the combobox in islands/InviteOperative.tsx. It resolves through searchByUsernamePrefix in lib/infrastructure/better-auth-operative-queries.ts:105:

{ username: { $regex: `^${escaped}` }, banned: { $ne: true } }

That filter is anchored to the start of the string, case-sensitive (no $options: "i"), and covers the username field alone. Consequences:

  • Typing an operative name (user.name) matches nothing — the field is never queried.
  • Typing an operative email matches nothing — the field is never queried.
  • Typing a mid-string fragment of a real username matches nothing, because of the ^ anchor.
  • Typing a username in the wrong case matches nothing.

An operative whose username bears no resemblance to their display name or the local part of their email is therefore unreachable through the typeahead. The person inviting them has to already know the exact username, lowercased, from its first character — which is the one thing they are least likely to have.

A correct implementation already exists in the same file

searchUsersForAdmin at lib/infrastructure/better-auth-operative-queries.ts:188 does what the invite box needs:

$or: [
  { username: { $regex: escaped, $options: "i" } },
  { email:    { $regex: escaped, $options: "i" } },
]

Unanchored, case-insensitive, spans username and email. The admin panel gets this; an operative inviting a teammate gets the crippled one.

Compounding: the island suppresses the dropdown for email input

islands/InviteOperative.tsx:71

if (query.length < 2 || query.includes("@")) {
  suggestions.value = [];
  showDropdown.value = false;
  return;
}

Anything containing @ short-circuits before the fetch. So even if the endpoint did search by email, the client would never ask. The two identifiers a person actually knows about a colleague — their name and their email address — both yield an empty dropdown.

Why the failure is quiet

An empty dropdown is indistinguishable from "no such operative." The field accepts free text and submits it, so the user then gets Operative not found from routes/api/v1/collectives/[slug]/invite.ts:69 — which reads as "this person is not on swamp-club" when the truth is "the search cannot see them."

Suggested fix

  1. Point searchByUsernamePrefix at the same $or shape searchUsersForAdmin uses, or have the invite endpoint call a shared search. Add name to the $or alongside username and email.
  2. Drop the query.includes("@") short-circuit in the island so an email prefix queries the endpoint too.
  3. Consider whether matching on email should return the username only (not the address) in the response, so the endpoint does not become an email-disclosure oracle for arbitrary substrings. Requiring a longer minimum query length for email matches would limit enumeration.
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED

Open

8/24/2026, 11:56:46 PM

No activity in this phase yet.

03Sludge Pulse

Sign in to post a ripple.