fix(auth): veto update-user name writes by value, not key presence - #2301
Merged
Conversation
better-auth's public POST /update-user handler always builds its adapter
payload as `{ name, image, ...additionalFields }` (api/routes/
update-user.mjs), so every update through that endpoint reaches
deriveOrRejectUserNameOnUpdate with the name key present — value
undefined whenever the client didn't send one. The key-presence veto
('name' in data) therefore aborted EVERY public profile update; the
first observable casualty was dj-site's experience switch
(updateUser({ appSkin }), dj-site PR #1279's E2E suite).
The veto now keys on the value: undefined reads as "not supplied",
while any supplied value — an explicit null included — is still an
attempted direct write and aborts the whole payload. The new wire spec
exercises the real endpoint and asserts against the database row, since
a vetoed write is silent at the HTTP layer (updateWithHooks returns
null and the route echoes session data with 200).
This was referenced Aug 28, 2026
jakebromberg
added a commit
that referenced
this pull request
Aug 28, 2026
The registry still described the auth_user.name conversion as pending. The databaseHooks derivation hooks shipped in #2297 (veto fix #2301), and the one-shot backfill ran against production on 2026-08-28: 139 of 144 non-anonymous rows rewritten to handle-else-username, with the 2 rows whose only legal-name copy lived in name preserved into real_name first (Track 2a). Updates the history paragraph, the auth_user.name row's classification/read-sites/enforcement cells, and nothing else.
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.
Closes #2299.
What
deriveOrRejectUserNameOnUpdate(thedatabaseHooks.user.update.beforename choke point from #2297) rejected on'name' in data— key presence. better-auth's publicPOST /update-userhandler always builds its adapter payload as{ name, image, ...additionalFields }, so every public profile update carriedname: undefinedand was vetoed wholesale. Live regression: dj-site'supdateUser({ appSkin })experience switch silently no-ops (the veto returns 200 —updateWithHooksyields null and the route echoes session data), which is how WXYC/dj-site#1279's E2E rerun caught it.The veto now keys on the value:
data.name !== undefined ? false : undefined.undefinedreads as "not supplied"; any supplied value — explicitnullincluded — is still an attempted direct write and aborts the whole payload. Rejection of realnamewrites, the derivation from a usable payloaddjName, and all #2297 behavior are otherwise unchanged.Tests
{ name: undefined, image: undefined, appSkin }shape must no-op;{ name: undefined, djName }still derives; explicitnullstill rejects. 20/20 in the hook suite, 8630/8630 across the unit tier.tests/integration/update-user-name-veto.spec.js): through the real auth service, asserting against theauth_userrow because a vetoed write is silent at the HTTP layer — an{ appSkin }-only update persists; a payload supplyingnameaborts entirely, sibling fields included.