Skip to content

fix(profiles): resolve a retired handle through an RPC, not a query string - #776

Merged
catomean merged 1 commit into
mainfrom
fix/username-history-rpc
Aug 26, 2026
Merged

fix(profiles): resolve a retired handle through an RPC, not a query string#776
catomean merged 1 commit into
mainfrom
fix/username-history-rpc

Conversation

@catomean

Copy link
Copy Markdown
Collaborator

Follow-up to #773. The alias mechanism was failing on the first accounts it had to protect.

What broke

PostgREST reads + in a query string as a space, and supabase-js sends the
character raw. So .eq('old_username', 'butaeff+ocauth2') searched for
"butaeff ocauth2". Measured against production:

...&old_username=eq.butaeff+ocauth2    -> []
...&old_username=eq.butaeff%2Bocauth2  -> [{"profile_id": "..."}]

Not hypothetical. Two live profiles carry a + in their legacy handle —
minted back when the whole email address went in verbatim — and for those the
profile redirect returned 404 and the Lightning-address fallback could not
find its owner. That is precisely the silent breakage
profile_username_history exists to prevent.

How it was found

By renaming one canary account and checking the result instead of assuming
the mechanism worked:

probe result
/profiles/butaeff%2Bocauth2 (renamed canary) 404
/profiles/zzz-alias-probe (clean synthetic alias) 308 → /profiles/user_cbd30e0570d3

The clean alias isolated the fault to the +, not the mechanism. Had I renamed
all 75 accounts first and reported success, two of them would have been quietly
unreachable.

The fix

resolve_username_history(handle) takes its argument in a JSON body, so
nothing needs escaping and no future handle can be mangled by the transport. It
lowercases and trims server-side, so callers cannot drift from the stored
form either.

Public execute: resolving an old handle is exactly as public as resolving a
current one — both the profile page and the LNURL endpoint are unauthenticated —
and it returns an id, never a list.

Tests

Updated rather than bypassed: the lnurl stub now models .rpc(), plus a new
case pinning that a + handle resolves to its owner and never goes through a
query-string filter
, which is the thing that mangled it.

npm run verify green; 2378 tests pass.

…tring

PostgREST reads `+` in a query string as a space, and supabase-js sends
the character raw. So `.eq('old_username', 'butaeff+ocauth2')` searched
for "butaeff ocauth2" and found nothing. Measured against production:

  ...&old_username=eq.butaeff+ocauth2    -> []
  ...&old_username=eq.butaeff%2Bocauth2  -> [{profile_id: ...}]

Not hypothetical. Two live profiles carry a '+' in their legacy handle —
minted back when the whole email address went in verbatim — and for those
the profile redirect returned 404 and the Lightning-address fallback
could not find its owner. That is exactly the silent breakage
profile_username_history was added to prevent, so the mechanism was
failing on the first accounts it had to protect.

Found by renaming one canary account and checking the result rather than
assuming it worked: /profiles/<old> 404'd. A clean synthetic alias
redirected 308 correctly, which isolated the fault to the '+' rather than
the mechanism.

resolve_username_history(handle) takes its argument in a JSON body, so
nothing needs escaping and no future handle can be mangled by the
transport. It lowercases and trims server-side, so callers cannot drift
from the stored form either. Public execute: resolving an old handle is
exactly as public as resolving a current one — both the profile page and
the LNURL endpoint are unauthenticated — and it returns an id, never a
list.

Tests updated rather than bypassed: the lnurl suite's stub now models
.rpc(), and a new case pins the '+' handle resolving to its owner AND
never going through a query-string filter, which is the thing that
mangled it.

npm run verify green; 2378 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@catomean
catomean merged commit fe9282f into main Aug 26, 2026
3 of 6 checks passed
@catomean
catomean deleted the fix/username-history-rpc branch August 26, 2026 21:30
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