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
- Point
searchByUsernamePrefixat the same$orshapesearchUsersForAdminuses, or have the invite endpoint call a shared search. Addnameto the$oralongsideusernameandemail. - Drop the
query.includes("@")short-circuit in the island so an email prefix queries the endpoint too. - Consider whether matching on
emailshould 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.
Open
No activity in this phase yet.
Sign in to post a ripple.