fix(profile): a rename recorded nothing, so the old handle died silently - #833
Merged
Conversation
Reported by a user: asked to change their handle, the Cat replied that usernames "cannot be changed once set" because it "would break all existing links". It was not inventing that — its brief said "Never update username (it breaks public URLs)" and the handler excluded the field for the same reason. Both were true until profile_username_history (20260826160000), which exists precisely so a rename breaks nothing: /profiles/<old> 301s to the new handle and <old>@orangecat.ch keeps resolving. Neither the prompt nor the handler was corrected afterwards, so the product went on refusing something it could do. Underneath the wrong answer was a real bug. PUT /api/profile has always accepted a new username, and the profile editor has always shown an editable handle field — but neither wrote the history row. The only writer was the one-off admin script. So a user who renamed themselves through the UI got exactly the breakage they were warned about: old profile URL 404s, Lightning address stops resolving, no error to anyone, and a payment sent to the address they had published simply does not arrive. Measured against production before this change: renaming a live profile recorded zero history rows. The rule now lives in the database, not in the route. A username is a payment identifier, so "a rename is recorded" has to hold for the route, for this repo's SQL scripts, for a psql session, and for whatever gets written next — and there is exactly one place a username can change. The same trigger refuses a handle another account retired: the profile page and LNURL both resolve live profiles FIRST, so reissuing one would hand the new holder the previous owner's inbound links and payments. 20260826160000 named that risk in a comment; nothing enforced it, and availability was checked against profiles alone, so every retired handle read as free. Verified against the production schema in a rolled-back transaction: a rename records, a reissue is refused, reclaiming your own handle clears its retired row, and neither an unchanged save nor a case-only change retires anything. Without the migration the first check fails on real data. The Cat can now do it too. Folded into update_profile rather than a new action: the static prompt sits one character under a budget that only ratchets down, so the capability is paid for by compressing the same line instead of raising the ceiling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WqKqMnHQHSmkGFfc5t7Rxn
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What a user hit
Asked the Cat to change their handle, and got:
Both sentences are wrong, and the Cat was not inventing them. Its brief said
Never update username (it breaks public URLs), and theupdate_profilehandler excluded the field with the same comment.
That was true until
profile_username_history(20260826160000), which existsprecisely so that a rename breaks nothing:
/profiles/<old>301s to the newhandle, and
<old>@orangecat.chkeeps resolving through it. Neither the promptnor the handler was corrected when the database gained the capability, so the
product kept refusing something it could already do.
The bug underneath the wrong answer
PUT /api/profilehas always accepted a new username, and the profile editorhas always shown an editable handle field — and neither wrote the history
row. The only writer was the one-off admin script.
So renaming yourself through the product got you exactly the breakage the Cat
warned about: the old profile URL 404s, the Lightning address stops resolving,
and nobody sees an error until a payment fails to arrive.
Measured against production before this change — renaming a live profile
recorded zero history rows:
A second hole, named in a comment in 20260826160000 and never enforced:
availability was checked against
profilesalone, so a handle someone elseretired read as free. Both the profile page and LNURL resolve live profiles
first, so taking one would have silently handed the new holder the previous
owner's inbound links and payments.
The fix
The rule lives in the database, not in the route. A username is a payment
identifier, so "a rename is recorded" has to hold for the API route, this
repo's SQL scripts, a psql session, and whatever gets written next — and there
is exactly one place a username can change. Fixing only the route would leave
the same bug one new caller away.
profiles_username_rename_guard— records the retired handle on everyrename, and refuses a handle another account retired.
checkUsernameAvailabilitynow consults retired handles, so the persontyping it learns from the form rather than from a failed save.
Verification
Trigger proved against the production schema in a rolled-back transaction —
plpgsql plans at run time, so a unit test could not have caught a bad column:
Run without the migration, check (a) fails on real data — the proof is not
vacuous. Both new unit suites were mutation-tested the same way: removing the
guard fails 1 test, removing the rename write fails 3.
npm run verifygreen: 269 suites, 2566 tests.Note on the prompt budget
The static Cat prompt sits one character under a budget that only ratchets
down (it is why platform Groq cannot serve Cat). So this does not add a new
action — the rename folds into
update_profile, and the new wording is paidfor by compressing the same line. The ceiling is untouched.
🤖 Generated with Claude Code
https://claude.ai/code/session_01WqKqMnHQHSmkGFfc5t7Rxn