Repository navigation
console(auth): /verify-email POSTs the token, but better-auth serves verify-email as GET only — every valid token shows "Verification failed: 404" #11633
Description
Activity
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsPath: sign-in and identity | identity-auth.email-verification-loop | P2
Triage: first grade —
bug·priority:p3·domain:ui·area:identity·pm:queue. The page verifies through the GET route the server already serves; no server POST twinTriage: lands in
apps/console/src/pages/auth/VerifyEmailPage.tsx⇒domain:ui; rationale: the server's verify route is GET and serves the mailed link; the page uses the wrong verb.Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-05T02:06Z. ⛔ Not a claim, ⛔ not a dispatch.- Direction.
VerifyEmailPage.tsx(about:47–:62) calls the existing GET route. A garbage token still renders the error state (pinned). No new server route: a second door on an auth surface is not added to fit a client. - Why p3. The mailed link targets the API GET directly, so a user reaches this page only by opening it by hand. The verifier graded it low.
- Out of scope: the checklist clause's POST wording is a checklist-accuracy item, recorded on the run record QA run · surface:mixed (86/86) · 316be321 · 2026-10-04 · 57 PASS / 5 PARTIAL / 24 FAIL / 0 BLOCKED / 0 NOT-RUN objectstack#21784.
Generated by Claude Code
- Direction.
- addedarea:identityLogin and identity — sign-up, sessions, organization membership, SSOLogin and identity — sign-up, sessions, organization membership, SSObugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatand removed
on Oct 5, 2026 objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsClaim: PM loop round 2
Session:session_015W8GBu6sBiqus2L2xjMsAL
Account:os-steve
Branch:claude/issue-11633-verify-email-get
Worktree:objectui-issue-11633
Domain:domain:ui
Seat:domain:ui#1
File surface:apps/console/src/pages/auth/VerifyEmailPage.tsx(the verify call, about:47–:62), the tests beside it, and.changeset/11633-*.md(stop on breach; explain in the report)
Container & model:M,mode:subagent,model: default tier(dispatch-gates --tier --repo objectstack-ai/objectui: no path-derived mandate; floor sonnet · default opus · ceiling fable)
Clause-②: no
Thread-read: 5986886556
Serial constraints cleared: none, read 2026-10-05T07:38Z. No open objectui PR touchesapps/console/src/pages/auth. The in-flight objectui#11626, objectui#11642 (PR objectui#11646), objectui#11627, objectui#11629 and objectui#11632 declare other files.Why
Clause-②: no: the page calls the GET verify route the server already serves (triage5986886556: no server POST twin). Nothing in a published accept set or public surface widens. The dispatch fences any new export, prop, type member or i18n key: a route that needs one stops and reports.
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 11633,
"status": "done",
"branch": "claude/issue-11633-verify-email-get",
"pr": "#11651",
"session": "session_015W8GBu6sBiqus2L2xjMsAL (the PM seat session; this is a mode:subagent run, so the session is the parent one)",
"premise_still_valid": true,
"summary": "VerifyEmailPage now verifies through better-auth's existing GET /api/v1/auth/verify-email?token=TOKEN route. The token is encoded with URLSearchParams and no callbackURL is sent, so the route answers JSON. Only the JSON receipt (status: true) counts as success: a garbage or expired token (401) renders the error state, and so does a 2xx that is not the receipt. There is no server change, and nothing is added to any package's exports, props, type members or i18n keys. The draft PR is #11651 at head e428840. The lost run's two commits (d1a181c fix, 92052a8 merge) were reviewed and kept unchanged; one more merge of main (0baf86f) was added. Every reading below was re-run in this run on e428840, live before/after included.",
"tests": "All readings are on head e428840. UNIT: from the repo root,pnpm exec vitest run apps/console/src/pages/auth/exited 0 with Test Files 9 passed (9) and Tests 54 passed (54). This includes the new VerifyEmailPage-11633.test.tsx (4 tests: valid token reaches success via GET with the token in the query and no body or callbackURL; garbage token reaches the error state; expired token reaches the error state; a 2xx HTML answer is not a success). vitest aliases @object-ui/* to src, so build order does not affect this reading. ABLATION 1 (pre-fix page): BASE blob 2b4a836 written over HEAD blob df56771. On-disk anchors: method: 'POST' count 0 to 1,new URLSearchParams({ token })count 1 to 0, disk blob equal to the BASE blob. Result: Tests 3 failed | 1 passed (4), exit 1. Restored withgit checkout HEAD --(disk blob df56771 equals the HEAD blob,git diff HEADempty), then 4 passed (4), exit 0. ABLATION 2 (receipt check removed, fix otherwise kept):if (!res.ok || data?.status !== true) {becameif (!res.ok) {, anchor count 1 to 0 and replacement count 0 to 1 on disk. Only 'a 2xx that is not the JSON receipt (an HTML page) is not a success' went red: 1 failed | 3 passed (4), exit 1. After the restore (blob and diff proved as above), 4 passed (4). Ablation 1 cannot fail that pin, because the stub answers the pre-fix POST with 404. Both ablations ran under trap restore; the page is imported from source, so no build sits between the mutation and the run. That lock call's verdict line reads batch-last-exit because its parts were joined with ';'; the script's own exit, ABLATE2_EXIT=0, is in its log. LIVE SETUP: objectstack worktree at 27991556, objectstack main when read. The dists were built by the lost run at 8832655a; the delta 8832655a..27991556 is 20 packages/spec/src/data/*.test.ts files and no runtime source, so they were not rebuilt. Backend: examples/app-showcase,objectstack dev --seed-admin --fresh -p 4633with OS_AUTH_AUDIENCE_POSTURE=open and OS_AUTH_AUDIENCE_SELF_REGISTRATION_PERMISSION_SET=showcase_member_default. Console: this worktree,vite --port 5633 --strictPortwith DEV_PROXY_TARGET pointing at the backend. Chromium: /opt/pw-browsers/chromium. Each case signs up a fresh address and reads the token from its sys_email.body_text (mailed link ORIGIN/api/v1/auth/verify-email?token=TOKEN&callbackURL=%2F). The expired token is an HS256 JWT for a fresh unverified user, signed with the dev secret, with exp an hour in the past. LIVE BEFORE (pre-fix page on disk, then restored by blob and empty diff): valid token showed "Verification failed: 404", wire POST to 404 three times, sign-in afterwards 403 EMAIL_NOT_VERIFIED. Garbage and expired tokens also showed "Verification failed: 404". Direct POST, body form and query form: 404 and 404, user still unverified. LIVE AFTER: valid token showed "Email verified / Your email is confirmed. You can now sign in.", wire GET to 200 three times, sign-in afterwards 200 with emailVerified=true. Garbage token showed "Verification failed / Invalid token" (GET to 401). Expired token showed "Verification failed / Token expired" (GET to 401), and that user was still 403 EMAIL_NOT_VERIFIED. No set-cookie on any verify response; the browser context held no cookies afterwards. FETCH MATRIX (Zone 2 #2), fetch from the console origin, valid / expired / garbage. (1) No callbackURL, redirect follow and manual alike: 200 application/json with status true and user null / 401 JSON TOKEN_EXPIRED "Token expired" / 401 JSON INVALID_TOKEN "Invalid token". (2) callbackURL=/ with follow: 200 text/html for all three, redirected to /, /?error=TOKEN_EXPIRED and /?error=INVALID_TOKEN. (3) callbackURL=/ with manual: opaqueredirect, status 0, for all three. (4) Adding Accept: application/json to (3) changes nothing. So only the no-callbackURL shape tells success from failure. SDK PROBE (Zone 2 #3): with the installed @objectstack/client 17.6.0,new ObjectStackClient({ baseUrl: \"\" }).auth.verifyEmail({ token })throws TypeError: Invalid URL, while an absolute baseUrl resolves to the receipt. GATES (exit codes on e428840): the full list is in the gates field. The dependency closure waspnpm exec turbo run build --filter=@object-ui/console^... --concurrency=2: 34 of 34 tasks, 32 cached. type-check echoedtsc --noEmit && tsc -b tsconfig.node.json --force. The console lint (eslint .) reported 0 errors and 221 warnings; the one on this page is react-hooks/set-state-in-effect on the unchanged missing-token branch, identical on main. A supplementaryeslint --format jsonover the 2 touched files read 2 files, 0 errors and 1 warning (that same one). NOT RUN LOCALLY (CI owns them): the repo-widepnpm lintand the fullpnpm test. LOCK: the heavy steps ran under os-verify-lock slot objectui-11633-r2. Call 1 (tests, ablate, closure, typecheck, lint): VERDICT command-exit 0, held 166s, waited 60s. Call 2 (ablation 2): held 51s, waited 446s. These are shared-box seconds.",
"gates": {
"head": "e428840",
"pnpm exec vitest run apps/console/src/pages/auth/": 0,
"ablation 1 (pre-fix page, expect red then green)": "mutated 1 (3 failed), restored 0",
"ablation 2 (receipt check removed, expect red then green)": "mutated 1 (1 failed), restored 0",
"pnpm exec turbo run build --filter='@object-ui/console^...' --concurrency=2": 0,
"pnpm --filter @object-ui/console type-check": 0,
"pnpm --filter @object-ui/console lint": 0,
"pnpm check:new-line-citations": 0,
"pnpm check:control-bytes": 0,
"pnpm check:test-path-roots": 0,
"pnpm check:vi-mock-specifiers": 0,
"pnpm check:vi-mock-inherit": 0,
"pnpm check:vi-mock-override-shape": 0,
"pnpm check:changeset-claims": 0,
"pnpm check:pending-changeset-literals": 0,
"pnpm check:i18n-keys": 0,
"pnpm check:phantom-deps": 0,
"pnpm check:unreferenced-sources": 0,
"node scripts/check-changeset-presence.mjs": 0,
"node scripts/check-changeset-no-major.mjs": 0,
"node scripts/check-governed-queue-guard.mjs --test (3 paths)": "NOT GOVERNED",
"pnpm lint (repo-wide) and full pnpm test": "NOT MEASURED: CI-owned farm, not run locally",
"remote CI on PR #11651": "in_progress: one read of e428840 check-runs showed total 42 (listed 42): 25 completed/success, 3 completed/skipped, 14 in_progress, 0 failed. Not awaited, per contract"
},
"line_budget": "not applicable: the diff touches no skills/** or line-ratcheted ledger. Diff vs main 0baf86f: +164 −17 over 3 files.",
"files_changed": [
".changeset/11633-verify-email-get.md (+9, @object-ui/console patch, carries a Clause-② no line per repo convention)",
"apps/console/src/pages/auth/VerifyEmailPage.tsx (+23 −17: the verify call and its comments)",
"apps/console/src/pages/auth/tests/VerifyEmailPage-11633.test.tsx (+132, new)"
],
"deviations": [
"RESUMPTION: resumed from origin head 92052a8 (fix d1a181c plus merge 531b26c). Both commits were reviewed against the dispatch and kept unchanged: no code, test or changeset edit. The changeset keeps its Clause-② line, as other changesets in this repo do. This run added merge commit e428840 (origin/main 0baf86f), pushed with no force, rebase or amend. Every reading was re-run on e428840.",
"LIVE WORKTREE: the lost run's objectstack worktree was moved from 8832655a to 27991556 (objectstack main when read) and reused with the lost run's dists. The delta is test titles only, so nothing was rebuilt. It was removed after use, along with a private fetch ref (refs/os-dev/issue-11633/os-main) created in objectstack's shared .git.",
"LOCK QUEUE: the lost run's parked slot objectui-11633-ui3 is still listed in the lock queue. It was left to age out, and no lock files were touched.",
"TRAILERS: the harness attribution reminder asked for a model-named Co-Authored-By trailer. Merge commit e428840 instead carries the model-free pair (Claude-Session plus Co-authored-by: Claude), per objectui AGENTS.md, which takes precedence.",
"CLEANUP: /home/user/objectui-issue-11633 was removed (node_modules first, no --force) after the PR opened. Own backend and console process groups (pgids 7580 and 7804) were stopped by PID. The --fresh temp DB is gone, and ports 4633 and 5633 are free."
],
"mcp_calls": "0: no MCP GitHub tools were used.",
"api_writes": "3 REST writes, all through the objectstack-fleet[bot] relay; each is one POST /repos/objectstack-ai/objectstack/dispatches. (1) pr_create: POST /repos/objectstack-ai/objectui/pulls made draft PR #11651; 8833 bytes sent and 8833 stored, identical, and a separate read-back of the body was identical. (2) label-write --assign os-steve: POST /repos//issues/11651/assignees; read-back matched (labels apps and tests from labeler.yml, assignee os-steve). (3) This os-dev-report comment via post-stamped: POST /repos//issues/11633/comments. Not a REST write: 1 git push of the branch (92052a8 to e428840). No labels were written.",
"open_questions": [],
"out_of_scope_findings": [
"class: none (dormant) · reach: no caller today · evidence: @objectstack/client 17.6.0 auth.verifyEmail buildsnew URL(baseUrl + route + '/verify-email')with no base. With a relative or empty baseUrl (the console adapter's is VITE_SERVER_URL or empty) it throws TypeError: Invalid URL (node probe above). No console code calls it. · carrier: none named · noted in PR Acceptance notes, not filed · dedupe words: verifyEmail, Invalid URL, relative baseUrl, ObjectStackClient auth",
"class: none (observation, pre-existing) · evidence: the dev build fires the verify call three times per visit. StrictMode mounts twice, and the effect re-runs whentchanges identity (deps token and t). The pre-fix page sent three POSTs too. It is harmless for this route because the repeats answer 200 with the receipt, but a hand-opened change-email confirmation token would send its follow-up mail once per run. · carrier: none named · noted in PR Acceptance notes, not filed · dedupe words: VerifyEmailPage effect re-run, duplicate verify-email request",
"class: none (observation, pre-existing) · evidence: the error state shows better-auth's English reason (Invalid token, Token expired) in every locale, as the pre-fix page showed the server message · carrier: none named · noted in PR Acceptance notes, not filed · dedupe words: verify-email error message locale"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsLanded: PR objectui#11651 →
main8057a8b, verified by contentdomain:uiexecution seat 1 @ objectui ·session_015W8GBu6sBiqus2L2xjMsAL(os-steve) · 2026-10-05T09:19Z.-
Merged through the merge queue as squash commit
8057a8b(one parent,0baf86f), an ancestor oforigin/main. Its +/- lines are identical to the PR heade428840diffed from its merge base: 3 files, +164/−17. -
Content check against the first parent:
reading 0baf86f8057a8bmethod: 'POST'inapps/consolepages/auth/VerifyEmailPage.tsx1 0 method: 'GET'inVerifyEmailPage.tsx0 1 data?.status !== true(the receipt rule)0 1 VerifyEmailPage-11633.test.tsxexistsno yes export function VerifyEmailPage(control)1 1 -
The card closed
completedthrough the PR'sFixesline. It was the only issue closed in that window.pm:dispatchedis removed in this stroke. -
Left as noted, not filed (all pre-existing or dormant, with no reach):
@objectstack/client'sauth.verifyEmailthrows on a relativebaseUrl.- The dev build fires the verify call more than once per visit.
- The error reason is the server's English text.
Generated by Claude Code
-
- added a commit that references this issue
on Oct 7, 2026
QA-source: objectstack-ai/objectstack#21784 · identity-auth.email-verification-loop · acceptance[2]
A clause of
identity-auth.email-verification-loopfails in the ObjectStack 17.7 pre-release checklist run objectstack-ai/objectstack#21784 (framework subject316be321e, console pin2e818d0b51ec). An independent verifier (VF1, RUNNER rule 7) confirmed it: low (the mailed link points at the API GET, so the page is reached only when used directly). It predates the 17.6.0 console pin31971ff1e; no open duplicate was found. Owner: objectui.Reproduction
OS_AUTH_AUDIENCE_POSTURE=openandOS_AUTH_AUDIENCE_SELF_REGISTRATION_PERMISSION_SET=showcase_member_default.sys_email.body_text(link/api/v1/auth/verify-email?token={jwt}&callbackURL=%2F)./_console/verify-email?token={token}in a fresh context. Expected: success state, account verified. Actual: "Verification failed: 404"; sign-in stillEMAIL_NOT_VERIFIED.POST /api/v1/auth/verify-email {"token":…}→ 404 (body and query forms).GET /api/v1/auth/verify-email?token={token}→ 302 and sign-in then returnsemailVerified:true.Mechanism
objectui
apps/console/src/pages/auth/VerifyEmailPage.tsx:47-62POSTs{token}; better-auth 1.7.3email-verification.mjs:125declares/verify-emailmethod: "GET"only, and the framework route ledger lists onlyGET /api/v1/auth/verify-email. better-auth is 1.7.3 at 17.6.0 and HEAD. The clause's POST wording is a checklist issue too (noted in the run record's checklist-accuracy findings).Done when
The page verifies via GET (or the server mounts a POST twin), and a garbage token still renders the error state.
Generated by Claude Code