Skip to content

fix(auth): veto update-user name writes by value, not key presence - #2301

Merged
jakebromberg merged 1 commit into
mainfrom
bugfix/update-user-undefined-name-veto
Aug 28, 2026
Merged

fix(auth): veto update-user name writes by value, not key presence#2301
jakebromberg merged 1 commit into
mainfrom
bugfix/update-user-undefined-name-veto

Conversation

@jakebromberg

Copy link
Copy Markdown
Member

Closes #2299.

What

deriveOrRejectUserNameOnUpdate (the databaseHooks.user.update.before name choke point from #2297) rejected on 'name' in data — key presence. better-auth's public POST /update-user handler always builds its adapter payload as { name, image, ...additionalFields }, so every public profile update carried name: undefined and was vetoed wholesale. Live regression: dj-site's updateUser({ appSkin }) experience switch silently no-ops (the veto returns 200 — updateWithHooks yields 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. undefined reads as "not supplied"; any supplied value — explicit null included — is still an attempted direct write and aborts the whole payload. Rejection of real name writes, the derivation from a usable payload djName, and all #2297 behavior are otherwise unchanged.

Tests

  • Unit (failing-first): the endpoint-injected { name: undefined, image: undefined, appSkin } shape must no-op; { name: undefined, djName } still derives; explicit null still rejects. 20/20 in the hook suite, 8630/8630 across the unit tier.
  • Wire (tests/integration/update-user-name-veto.spec.js): through the real auth service, asserting against the auth_user row because a vetoed write is silent at the HTTP layer — an { appSkin }-only update persists; a payload supplying name aborts entirely, sibling fields included.

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).
@jakebromberg
jakebromberg merged commit 4f2ee46 into main Aug 28, 2026
6 checks passed
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.
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.

update.before name veto aborts every public /update-user call (key-presence check vs endpoint-injected undefined name)

1 participant