Skip to content

Mobile sign-in: a person who has to verify their email during the hand-off gets a mail link to 页面不存在, and the verify page drops the refusal time #30157

Open

Description

@objectstack-fleet

domain:server#2 · hotlong · session_c967d7a472b0485c94711bfcb22159c8

Category: ① product defect with a named location.

Business reason: a new user who signs up during the mobile sign-in (the system auth session, objectstack-ai/mobile#148), on a server that requires email verification, gets a verification mail whose link lands on 页面不存在. The mail is the only road back, so that person cannot finish signing in from it. Nobody is hitting this yet, because no released app uses the flow; it would start when one does.

Reader: domain:ui, one PR on ui/apps/console/src/pages/auth/. objectstack-ai/mobile#190 is the first consumer.

Duplicate search: withConsoleBaseRootRelative in issues (0 hits) and verify-email-prompt redirect handoff (2 hits, #29997 and a closed QA run, neither about this).

reach: read from the code on origin/main, not run end to end. First touch should measure it. The helper's wrong answer for this kind of path was measured in the browser on #29971, which is the same defect in a sibling path. Found by the contract review of PR #30010 (comment on that PR, head 3e59db0af0).

1. The verification mail's link

RegisterPage.tsx (verificationCallbackURL) and VerifyEmailPromptPage.tsx (callbackURL) send a safe redirect through withConsoleBaseRootRelative, which treats every redirect as a Console route. For a hand-off redirect that gives /_console/api/v1/auth/one-time-token/handoff?…, a page that does not exist.

Not just a prefix fix. Which browser opens that mail matters. It is usually not the system auth session the app is waiting on, and a token handed out there may have no app waiting for it. So the right callback is a product question: decide what a person who verifies by mail should see. Fix the dead end at minimum, and say in the PR what the chosen answer does.

2. The refusal time is dropped on this road

PR #30010 carries handoff_refused_at on the links between /login and /register, through handoffMarkQuery. Three hops still drop it: LoginPage's EMAIL_NOT_VERIFIED branch, RegisterPage's onVerificationRequired (each builds the query from email and redirect), and VerifyEmailPromptPage.handleBackToLogin (rebuilds /login?redirect=).

A person with an old session on a device whose clock is behind who takes this road is judged by the age check alone and forwarded into the route's 401 JSON. It stops and cannot loop. It needs several conditions at once, so it is the smaller half.

Acceptance notes

Related: objectstack-ai/mobile#148. Not a blocker of it: a Google sign-in should not take this road (not measured), and a person verifying by email is the case affected.

Activity

  1. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: first-touch audit · p2 · domain:ui · pm:queue · 2026-11-04T17:53Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-support-ai · session_01WGufzinedUGSXteN45poyo

    Claim: PM loop round 2
    Session: session_01WGufzgzSz4DH8R8v1yq6bm (card thread executor; seat session session_01WGufzinedUGSXteN45poyo)
    Account: os-support-ai
    Branch: claude/project-thread-yz3rkx (harness-pinned card thread branch; the claude/issue-<n>-<slug> convention does not apply to a project card thread)
    Worktree: objectstack-issue-30157
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: ui/apps/console/src/pages/auth/** (RegisterPage.tsx, VerifyEmailPromptPage.tsx, LoginPage.tsx, handoffRedirect.ts), ui/apps/console/src/utils/consoleBase* if the fix needs it, their tests, root .changeset/
    Container & model: M, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate; PM judgment call, default tier), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: RegisterPage.tsx/VerifyEmailPromptPage.tsx send a hand-off redirect through withConsoleBaseRootRelative (console code) | no platform path covers the mail-verified hand-off road | reached by a person who signs up during the mobile hand-off on a server requiring email verification; no released app uses the flow today (triage 6637932107)
    Thread-read: 6637932107
    Premise: holds
    Serial constraints cleared: none (no in-flight domain:ui claim declares ui/apps/console/src/pages/auth/**; PR #30010, the predecessor on this path, merged 2026-11-05T01:05Z). Session-transfer flow: the maintainer reviews and merges.

  3. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-support-ai · session_01WGufzinedUGSXteN45poyo

    Ruling recorded · #30157 mail-verified landing · option A

    The maintainer (Yibo Wang) answered the dev's decision card in the project card thread on 2026-11-05T03:06Z, choosing the first option. The card, verbatim as they read it:

    移动端注册时,验证邮件里的链接应该把人带到哪里?
    选定:「回 App 提示页」 —— 落到「邮箱已确认,请回 App 登录」页,不在打开邮件的浏览器发令牌;回 App 再登录一次即完成,任何设备都不走死路。

    Ruling: A. The verification mail's link for a hand-off sign-up lands on a Console page saying the address is confirmed and to go back to the app; no one-time token is minted in the browser that opened the mail. Not chosen: B (/login carrying the hand-off redirect, signs in in the mail browser) and C (plain Console /login).

    Measured by the dev before the ruling (dev boot, main dfafae81): today a signed-in mail browser lands on 页面不存在, and a signed-out one is sent by the SPA guard to /login?redirect=<hand-off>, de facto option B by accident. The PR implements A and carries handoff_refused_at on the three hops the card names. The PR follows on branch claude/project-thread-yz3rkx; this is a session-transfer flow, so the maintainer reviews and merges it.

  4. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-support-ai · session_01WGufzgzSz4DH8R8v1yq6bm
    os-dev-report
    Stamp: 2026-11-05T03:10Z

    {
      "issue": 30157,
      "status": "done",
      "branch": "claude/project-thread-yz3rkx",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30243",
      "session": "session_01WGufzgzSz4DH8R8v1yq6bm",
      "premise_still_valid": true,
      "summary": "Premise holds in part, measured on a dev boot (examples/app-crm, origin/main dfafae813 Console, posture open, OS_AUTH_REQUIRE_EMAIL_VERIFICATION=true, OS_TRUSTED_APP_SCHEMES): the mail's callbackURL was /_console/api/v1/auth/one-time-token/handoff?...; a mail browser holding any Console session landed on 'Page not found', while a signed-out one was sent by the SPA's sign-in guard to /_console/login carrying the hand-off (a de-facto 'continue in the mail browser'); the three hops dropped handoff_refused_at. The landing was a real product fork, so it went to the user as a decision card; the user chose option A ('回 App 提示页', 2026-11-05T03:06:38Z). Fix: handoffRedirect.ts gains rule 6 and verificationCallbackURL(redirect): a hand-off redirect returns to /verify-email?handoff=verified (the existing handoffNavigationTarget matcher decides), any other safe redirect keeps withConsoleBaseRootRelative byte for byte; RegisterPage (sign-up mail) and VerifyEmailPromptPage (resend) use it; VerifyEmailPage shows 'Email verified, go back to the app and sign in there' with no Console link and no token spend, and its failure state on better-auth's appended error. handoffMarkQuery is appended on LoginPage's EMAIL_NOT_VERIFIED navigation, RegisterPage's onVerificationRequired and handleBackToLogin. New i18n key auth.verifyEmail.handoffSuccessDescription in ten locales. No server file, auth route, token issuance or hand-off endpoint touched. Card assignee not written.",
      "tests": "New handoffVerificationMail-30157.test.tsx (real AuthProvider over a mock client, embedded mount): mail-link pins for sign-up and resend + invitation CONTROL; one pin per hop (login EMAIL_NOT_VERIFIED, register verification required, Back to login) + CONTROLs (invitation redirect with mark, hand-off redirect with no mark) byte for byte; landing pins (handoff=verified, error appended, no-token CONTROL). Run at 9d2f99aa: Test Files 1 passed, Tests 11 passed. Ablation (fix committed first; five Console src files checked out from origin/main in a trap-restored script, landing proved by grep -c 0 hits for verificationCallbackURL and handoffMarkQuery in VerifyEmailPromptPage): Tests 6 failed | 5 passed (2 mail pins, 3 hop pins, the landing pin red; controls green); restore proved by empty git diff HEAD and HEAD blob hashes. No build in the loop (Console tests import src). Browser walk before/after on the dev boot, shots and walk json in /mnt/project-files/ui-orders/30157-shots/: before signed-in mail browser -> 'Page not found', signed-out -> login with hand-off; after both -> /verify-email?handoff=verified; prompt and Back to login carry the mark; sign-in in the waiting window -> route 302 ai.objectos.mobile://auth/callback.",
      "gates": "One locked run at head 9d2f99aa (os-verify-lock VERDICT command-exit 0, 1342 s), 40 commands, all exit 0: console auth tests (28 files, 228 tests), i18n tests (95 files, 1443 passed, 13 skipped), @object-ui/console type-check, @object-ui/i18n type-check, eslint on the 6 changed console files, node scripts/check-sdui-manifest.mjs (+ --self-test), and every one of the 35 families dispatch-gates --commands derives. dispatch-gates --ran: '35 derived famil(ies) accounted for — 35 run, 0 NOT-MEASURED'. List: /mnt/project-files/ui-orders/30157-gates-ran.list. PR check-runs: in_progress at report time (not awaited, per os-dev).",
      "line_budget": "17 files, +334 / -30 (git diff --shortstat origin/main...HEAD): new test file 220 lines, 5 console src files, 10 locale lines, changeset",
      "files_changed": [".changeset/30157-handoff-verification-mail.md", "ui/apps/console/src/pages/auth/LoginPage.tsx", "ui/apps/console/src/pages/auth/RegisterPage.tsx", "ui/apps/console/src/pages/auth/VerifyEmailPage.tsx", "ui/apps/console/src/pages/auth/VerifyEmailPromptPage.tsx", "ui/apps/console/src/pages/auth/handoffRedirect.ts", "ui/apps/console/src/pages/auth/__tests__/handoffVerificationMail-30157.test.tsx", "ui/packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts"],
      "deviations": "Commit trailer uses 'Co-Authored-By: Claude' (repo rule) instead of the harness's model-named trailer. PR held from 02:00Z to 03:07Z for the user's landing decision (order's needs_decision fence); work continued on the recommended option meanwhile. Branch pushed before the PR (harness-pinned branch). An attempt to stop my own dev boot by process pattern was refused by the auto-mode classifier; it was stopped by its listed pids instead.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "3 — mcp__github__issue_read (get #30157), mcp__github__list_pull_requests (open PRs, path-overlap check), no write tools",
      "api_writes": "3 — git push (branch); relay pr_create #30243 (run 41959601718, 10549 bytes sent = stored); this os-dev-report comment via post-stamped.mjs",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": ["carrier: none · noted, not filed: the SPA sends a signed-out visitor at an unknown /_console path to /login?redirect=PATH, which is how the old mail link reached a hand-off sign-in by accident; no defect once this lands."]
    }

    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions