Skip to content

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

@objectstack-fleet

QA-source: objectstack-ai/objectstack#21784 · identity-auth.email-verification-loop · acceptance[2]

A clause of identity-auth.email-verification-loop fails in the ObjectStack 17.7 pre-release checklist run objectstack-ai/objectstack#21784 (framework subject 316be321e, console pin 2e818d0b51ec). 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 pin 31971ff1e; no open duplicate was found. Owner: objectui.

Reproduction

  1. Boot with OS_AUTH_AUDIENCE_POSTURE=open and OS_AUTH_AUDIENCE_SELF_REGISTRATION_PERMISSION_SET=showcase_member_default.
  2. Sign up a fresh address; read the token from its sys_email.body_text (link /api/v1/auth/verify-email?token={jwt}&callbackURL=%2F).
  3. Open /_console/verify-email?token={token} in a fresh context. Expected: success state, account verified. Actual: "Verification failed: 404"; sign-in still EMAIL_NOT_VERIFIED. POST /api/v1/auth/verify-email {"token":…} → 404 (body and query forms).
  4. Control: GET /api/v1/auth/verify-email?token={token} → 302 and sign-in then returns emailVerified:true.

Mechanism

objectui apps/console/src/pages/auth/VerifyEmailPage.tsx:47-62 POSTs {token}; better-auth 1.7.3 email-verification.mjs:125 declares /verify-email method: "GET" only, and the framework route ledger lists only GET /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

Activity

  1. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: 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 twin

    Triage: 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.


    Generated by Claude Code

  2. added
    area:identityLogin and identity — sign-up, sessions, organization membership, SSO
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Oct 5, 2026
  3. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: 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 touches apps/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 (triage 5986886556: 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

  4. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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 with git checkout HEAD -- (disk blob df56771 equals the HEAD blob, git diff HEAD empty), then 4 passed (4), exit 0. ABLATION 2 (receipt check removed, fix otherwise kept): if (!res.ok || data?.status !== true) { became if (!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 4633 with OS_AUTH_AUDIENCE_POSTURE=open and OS_AUTH_AUDIENCE_SELF_REGISTRATION_PERMISSION_SET=showcase_member_default. Console: this worktree, vite --port 5633 --strictPort with 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 was pnpm exec turbo run build --filter=@object-ui/console^... --concurrency=2: 34 of 34 tasks, 32 cached. type-check echoed tsc --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 supplementary eslint --format json over the 2 touched files read 2 files, 0 errors and 1 warning (that same one). NOT RUN LOCALLY (CI owns them): the repo-wide pnpm lint and the full pnpm 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 builds new 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 when t changes 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

  5. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR objectui#11651 → main 8057a8b, verified by content

    domain:ui execution 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 of origin/main. Its +/- lines are identical to the PR head e428840 diffed from its merge base: 3 files, +164/−17.

    • Content check against the first parent:

      reading 0baf86f 8057a8b
      method: 'POST' in apps/console pages/auth/VerifyEmailPage.tsx 1 0
      method: 'GET' in VerifyEmailPage.tsx 0 1
      data?.status !== true (the receipt rule) 0 1
      VerifyEmailPage-11633.test.tsx exists no yes
      export function VerifyEmailPage (control) 1 1
    • The card closed completed through the PR's Fixes line. It was the only issue closed in that window. pm:dispatched is removed in this stroke.

    • Left as noted, not filed (all pre-existing or dormant, with no reach):

      • @objectstack/client's auth.verifyEmail throws on a relative baseUrl.
      • 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:identityLogin and identity — sign-up, sessions, organization membership, SSObugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions