fix(console): /verify-email verifies through better-auth's GET route (objectui#11633) - #11651
Merged
Merged
Conversation
The page POSTed `{ token }` to /api/v1/auth/verify-email, which better-auth
serves as GET only, so every valid token showed "Verification failed: 404"
and the account stayed unverified. It now calls GET ?token= without a
callbackURL, so the route answers its JSON receipt; only that receipt counts
as success, and a garbage or expired token still renders the error state.
Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contributor
Author
ACCEPT: PR objectui#11651, head
|
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.
Fixes #11633
Clause-②: no
What changed
VerifyEmailPage(apps/console/src/pages/auth/VerifyEmailPage.tsx) sent the token asPOST /api/v1/auth/verify-emailwith a JSON body. better-auth 1.7.3 declares/verify-emailasmethod: "GET"only, so the server answered 404. Every valid token showed "Verification failed: 404" and the account stayed unverified.The page now calls
GET /api/v1/auth/verify-email?token=TOKEN, with the token encoded throughURLSearchParamsand nocallbackURL. Without acallbackURLthe route answers JSON instead of a 302. The page counts only that JSON receipt (status: true) as success. A garbage or expired token is a 401 carryingcodeandmessage, and the page renders its error state with the server's reason. A 2xx that is not the receipt, such as an HTML page, is an error too. The page's states, copy and links are unchanged. There is no server change and no new route (triage direction, comment 5986886556).The fence holds: no export, prop, type member or i18n key is added to any
@object-ui/*package. The diff is the body of the verify call, one new test file and the changeset.Live before/after: real backend, real Chromium, head
e42884027991556(main when read),examples/app-showcase,objectstack dev --seed-admin --fresh, withOS_AUTH_AUDIENCE_POSTURE=openandOS_AUTH_AUDIENCE_SELF_REGISTRATION_PERMISSION_SET=showcase_member_default.apps/consoleunder Vite, proxied to that backend.sys_email.body_text. The mailed link readsORIGIN/api/v1/auth/verify-email?token=TOKEN&callbackURL=%2F. The expired token is a JWT for a fresh unverified user, signed with the dev secret, whoseexpis an hour in the past.2b4a836) is put on disk under the running dev server, probed, and then restored fromHEAD. The restore is proved by blob hash and an emptygit diff HEAD.POST→ 404; sign-in afterwards 403EMAIL_NOT_VERIFIEDGET→ 200; sign-in afterwards 200,emailVerified: truePOST→ 404)GET→ 401)POST→ 404)GET→ 401); sign-in afterwards still 403EMAIL_NOT_VERIFIEDThe same server answers
POSTwith 404 in both the body form and the query form, and the user stays unverified.Which call shape tells success from failure
The probe ran
fetchfrom the console origin through the proxy, with one fresh token per cell:callbackURL,redirectfollow or manual (same answers)application/jsonwithstatus: trueanduser: nullTOKEN_EXPIREDand "Token expired"INVALID_TOKENand "Invalid token"callbackURL=/, followtext/html, redirected to/text/html, redirected to/?error=TOKEN_EXPIREDtext/html, redirected to/?error=INVALID_TOKENcallbackURL=/, manualopaqueredirect, status 0callbackURL=/, manual,Accept: application/jsonopaqueredirect, status 0Only the shape without
callbackURLtells the three cases apart from the response alone. A followed redirect is a 200 HTML page for all three, and a manual one is an opaque status 0 for all three. AnAcceptheader does not change the answer, which matches the vendor source:ctx.redirectis thrown whenevercallbackURLis present.Measured points from the dispatch
set-cookie.autoSignInAfterVerificationis not configured, so better-auth's default (off) applies. The success state's "You can now sign in" and its link to/loginare therefore accurate, and the page does not route anywhere new.@object-ui/auth'sAuthClient(whatuseAuth()exposes) has no verify-email member, and adding one would add a type member, which the fence forbids.@objectstack/client17.6.0 hasauth.verifyEmail, but the public auth routes render outsideConnectedShell, so there is no adapter or client instance there. The method also buildsnew URL(baseUrl + route + '/verify-email')with no base. With the console'sbaseUrl(VITE_SERVER_URLor empty) it throwsTypeError: Invalid URL, measured in node against the installed 17.6.0. The page therefore keeps its directfetch.Tests (head
e428840)pnpm exec vitest run apps/console/src/pages/auth/:Test Files 9 passed (9),Tests 54 passed (54).apps/console/src/pages/auth/__tests__/VerifyEmailPage-11633.test.tsx(4 tests). The stub answers like the live route: the JSON receipt for a valid token, a 401 withcodeandmessagefor a garbage or expired one, 404 for any other method, and an HTML page for a request that carriescallbackURL. The valid token contains+,/and=, so query encoding is pinned too.2b4a836,method: 'POST'count 0 → 1). Result:Tests 3 failed | 1 passed (4). It is restored fromHEAD(blobdf56771,git diff HEADempty), and then4 passed (4).if (!res.ok || data?.status !== true)becomesif (!res.ok)(anchor 1 → 0). Only "a 2xx that is not the JSON receipt (an HTML page) is not a success" turns red:1 failed | 3 passed (4). After the restore,4 passed (4). Ablation 1 cannot make that pin fail, because the pre-fix page never sees a 2xx from the stub. Neither ablation left a permanent test file.Gates (head
e428840)turbo run build --filter='@object-ui/console^...', 34/34 tasks),pnpm --filter @object-ui/console type-check(the scripttsc --noEmit && tsc -b tsconfig.node.json --forceechoed) andpnpm --filter @object-ui/console lint.eslint .reports 0 errors and 221 warnings. One warning is on this page:react-hooks/set-state-in-effectat the missing-token branch, an unchanged line that is the same onmain.check:new-line-citations(0 new citations),check:control-bytes,check:test-path-roots,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:changeset-claims,check:pending-changeset-literals,check:i18n-keys,check:phantom-deps,check:unreferenced-sources,scripts/check-changeset-presence.mjsandscripts/check-changeset-no-major.mjs.pnpm lintand the fullpnpm testbelong to CI.scripts/check-governed-queue-guard.mjs --teston the three paths answers NOT GOVERNED.Acceptance notes
These were observed and are not changed here.
tchanges identity (its dependencies aretokenandt). This is not new: the pre-fix page sent three POSTs. It is harmless for this route, because the second and third GETs answer 200 with the receipt: the route returnsstatus: truefor an already-verified address (measured). A change-email confirmation token would send its follow-up mail once per run. That path is reachable only by opening this page by hand with such a token, since the mailed links target the API route.message.auth.verifyEmailthrows on a relativebaseUrl.@objectstack/client'sauth.verifyEmailthrowsTypeError: Invalid URLwhen the client'sbaseUrlis relative or empty, as described above. Nothing calls it today. Owner if it gets one: none named.Resumption
This branch was resumed from
92052a8after a container restart. That head held the fixd1a181cand a merge ofmain(531b26c). This run reviewed both commits against the dispatch and kept them unchanged. It added one merge ofmain(0baf86f) and re-measured everything above one428840. Run session:https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL.Generated by Claude Code