You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PR #2297 (merged, auto-deployed) added deriveOrRejectUserNameOnUpdate as the databaseHooks.user.update.before choke point. Its rejection predicate is 'name' in data — key presence. But better-auth's public POST /update-user handler (api/routes/update-user.mjs, v1.6.26) always builds its adapter payload as { name, image, ...additionalFields }, so every update through that endpoint reaches the hook with the name key present, value undefined whenever the client didn't send one. The hook therefore returns false and aborts every public profile update — currently live in production.
First observable casualty: dj-site's experience switch (authClient.updateUser({ appSkin }), src/hooks/experienceSwitchHooks.ts), caught by dj-site PR WXYC/dj-site#1279's E2E rerun — [setup] › e2e/auth.setup.ts times out waiting for #classic-container because the appSkin write is silently dropped. The veto is silent at the HTTP layer: updateWithHooks returns null and the route falls back to echoing session data with { status: true } (200), so no client sees an error.
Writers that construct their payloads without a name key (complete-onboarding.ts's markOnboardingComplete, admin-plugin routes) are unaffected — which is why PR #2297's integration tests passed.
Fix
shared/authentication/src/derive-user-display-name.ts: veto on value, not key presence — 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. Docblock updated with the endpoint-shape constraint.
tests/unit/authentication/derive-user-display-name.test.ts: regression tests for the endpoint-injected { name: undefined, image: undefined, ...rest } shape.
tests/integration/update-user-name-veto.spec.js: new wire spec through the real auth service — asserts against the auth_user row (not the response, which lies): an { appSkin }-only update persists; a payload supplying name aborts entirely (sibling fields don't land).
Problem
PR #2297 (merged, auto-deployed) added
deriveOrRejectUserNameOnUpdateas thedatabaseHooks.user.update.beforechoke point. Its rejection predicate is'name' in data— key presence. But better-auth's publicPOST /update-userhandler (api/routes/update-user.mjs, v1.6.26) always builds its adapter payload as{ name, image, ...additionalFields }, so every update through that endpoint reaches the hook with thenamekey present, valueundefinedwhenever the client didn't send one. The hook therefore returnsfalseand aborts every public profile update — currently live in production.First observable casualty: dj-site's experience switch (
authClient.updateUser({ appSkin }),src/hooks/experienceSwitchHooks.ts), caught by dj-site PR WXYC/dj-site#1279's E2E rerun —[setup] › e2e/auth.setup.tstimes out waiting for#classic-containerbecause the appSkin write is silently dropped. The veto is silent at the HTTP layer:updateWithHooksreturns null and the route falls back to echoing session data with{ status: true }(200), so no client sees an error.Writers that construct their payloads without a
namekey (complete-onboarding.ts'smarkOnboardingComplete, admin-plugin routes) are unaffected — which is why PR #2297's integration tests passed.Fix
shared/authentication/src/derive-user-display-name.ts: veto on value, not key presence —data.name !== undefined ? false : undefined.undefinedreads as "not supplied"; any supplied value (explicitnullincluded) is still an attempted direct write and aborts the whole payload. Docblock updated with the endpoint-shape constraint.tests/unit/authentication/derive-user-display-name.test.ts: regression tests for the endpoint-injected{ name: undefined, image: undefined, ...rest }shape.tests/integration/update-user-name-veto.spec.js: new wire spec through the real auth service — asserts against theauth_userrow (not the response, which lies): an{ appSkin }-only update persists; a payload supplyingnameaborts entirely (sibling fields don't land).Acceptance criteria
provision classic-preference identity) passes after this deploys.namepayloads (sentinel spec and hook rejection tests untouched).