Skip to content

邀请注册链路丢失 redirect:signUp 不传 callbackURL,被邀请人验证邮箱后被带去「创建工作区」 #10893

Description

@hotlong

来源

2026-09-28 cloud 本地全链路验收:cloud 96eb092f,objectui pin efead6c6。已对照 objectui main(24845c4f1),问题代码未变。测试角色:陈晓邀请一个还没有账号的同事 wangwei@example.com 加入「晓光科技」。

现象

被邀请人点击邀请邮件里的链接 /_console/accept-invitation/<id>:

  1. 没登录,跳到 /_console/login?redirect=/accept-invitation/<id>。页面上看不出这是一个邀请,只是普通登录页。
  2. 点「注册」后进入 /_console/register?redirect=…,redirect 还在 ✓
  3. 注册提交后进入 /_console/verify-email-prompt?email=…&redirect=…,redirect 还在 ✓
  4. 但验证邮件里的链接是 …/verify-email?token=…&callbackURL=%2F。被邀请人点击验证后落到 /_console/organizations,弹出「创建工作区」对话框,页面写着「尚无工作区」,没有任何待处理邀请的提示。

被邀请人会自然地去新建自己的工作区,邀请就此丢失。只有回到邮箱、再点一次原始邀请链接,才会看到「您收到一份邀请 → 接受邀请」(这一步本身工作正常)。

机制

packages/auth/src/createAuthClient.ts:401 的 signUp() 调 betterAuth.signUp.email({ email, password, name }),没有传 callbackURL。better-auth 因此用默认的 / 生成验证链接,注册页手上的 redirect 就这样断了。

修复方向

注册时如果带有 redirect,就把它换算成 console 的绝对路径,作为 callbackURL 传给 signUp.email(与 LoginForm.tsx:339 发验证邮件时传 callbackURL 的做法对齐)。另外可以考虑两个兜底:

  • 已登录用户若存在待处理邀请,在「您的工作区 / 创建工作区」页面给出显眼的提示;
  • 从邀请链接进来的登录页 / 注册页,说明「你被邀请加入 X」。

验收

  1. 无账号的被邀请人走完「点邀请链接 → 注册 → 点验证邮件」,最终落在接受邀请页面(或直接完成加入),而不是「创建工作区」。
  2. 单测:signUp 在 redirect 存在时把 callbackURL 带到 /sign-up/email 的请求体里。

相关

Activity

  1. hotlong commented on Sep 28, 2026

    @hotlong
    ContributorAuthor

    最新版复测(2026-09-28 第二轮:objectstack main 862b6ce8 · objectui main 35d68c4c · cloud main 96eb092f,本地合入 cloud#2423 的配套修改;没有这些修改,cloud main 在 framework main 上起不来)

    在 objectui main 35d68c4c 上仍然复现。 被邀请人注册时,redirect 一直带到了 verify-email-prompt,但验证邮件里是 callbackURL=%2F;验证完落到「创建工作区」。一个小变化:重新打开邀请链接并接受后,现在会落到云 welcome 页(锁定版本那一轮落在通用的 /home)。

  2. added
    pm:epicParent delegated to a dedicated epic PM — other PMs never dispatch into its subtree
    on Sep 28, 2026
  3. self-assigned this
    on Sep 28, 2026
  4. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 28, 2026
  5. hotlong commented on Sep 28, 2026

    @hotlong
    ContributorAuthor

    Claim: PM loop round 1 (epic objectstack-ai/cloud#2440)
    Session: 786273a0-0246-4bb2-b026-bb37ba5d7295
    Branch: claude/issue-10893-signup-carries-invite-callback
    Worktree: ~/Documents/GitHub/objectui-issue-10893
    Domain: domain:ui, routed through the maintainer direct-dispatch channel (a cross-repo card of epic objectstack-ai/cloud#2440; central triage had not routed it yet), reserved via pm:epic
    Seat: epic PM for objectstack-ai/cloud#2440 (seat 1)
    File surface: packages/auth/src/** (createAuthClient.ts signUp, RegisterForm.tsx if it owns the redirect) and their tests; the invite-context copy on login/register only if it lives in the same package (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (default judgment tier)
    Clause-②: no
    Thread-read: 5863762640
    Serial constraints cleared: No open objectui PR touches packages/auth/ (scanned 60 open PRs at dispatch time).

    Direct dispatch by the maintainer, verbatim: 「使用专题派发解决这些问题」 and, raising the in-flight ceiling, 「把并发上限提到 5,其余卡一起派」 (epic session, 2026-09-28).

  6. hotlong commented on Sep 28, 2026

    @hotlong
    ContributorAuthor

    os-dev-report
    {
    "issue": 10893,
    "status": "done",
    "branch": "claude/issue-10893-signup-carries-invite-callback",
    "pr": "#10904",
    "session": "session_786273a0-0246-4bb2-b026-bb37ba5d7295 (subagent; parent PM session id)",
    "premise_still_valid": true,
    "summary": "Premise confirmed on origin/main 775e079: createAuthClient.signUp sent no callbackURL, so better-auth mailed callbackURL=/ . PM hypothesis measured with an in-process better-auth server probe on BOTH 1.7.2 (cloud objectstack pin bdea10a1) and 1.7.3 (objectstack main). /sign-up/email honours body.callbackURL for the mail, and the link 302s to it with the session cookie set. A document-relative ./accept-invitation/ID is refused 403 INVALID_CALLBACK_URL, which fails the whole sign-up; that is what withConsoleBase returns in the embedded build, so the join had to be root-relative. Fix: @object-ui/auth forwards SignUpData.callbackURL and exposes it as useAuth().signUp 4th arg and RegisterForm.verificationCallbackURL (additive, minor). @object-ui/console RegisterPage passes a safe ?redirect= through the new withConsoleBaseRootRelative. Bounded in-place fix, same class: VerifyEmailPromptPage Resend sent the bare router path, which lands at the origin root outside /_console; it now sends the same value. FILE SURFACE BEYOND THE CLAIM: ?redirect= is owned by apps/console/src/pages/auth/RegisterPage.tsx, not RegisterForm, so a packages/auth-only change could not fix the bug; stated in the PR body. PR is draft, assigned to hotlong. CI at report time: 21 pass, 19 pending, 3 skipping, 0 fail (in_progress). E2E invite-register-verify NOT MEASURED live; cloud reach needs a later .objectui-sha bump (currently efead6c).",
    "tests": "All at HEAD 3fd6dcd. Server probe: .rig-logs/probe-signup-callback.mjs against better-auth 1.7.2 and 1.7.3. absent → mail callbackURL=/ ; /_console/accept-invitation/inv_1 → 200, mail carries it, GET 302 to it + session cookie ; ./accept-invitation/inv_1 → 403 INVALID_CALLBACK_URL ; /accept-invitation/inv_1 → 302 to origin-root path. Gates: pnpm --filter @object-ui/auth build 0, type-check 0 (tsconfig.test.json --listFiles includes both new or edited tests), test 0 (27 files / 277 tests). turbo build --filter=@object-ui/console^... 34/34, then pnpm --filter @object-ui/console type-check 0 and test 0 (126 files / 1419 tests). check-changeset-presence 0 ; changeset:check 0 ; check:changeset-claims 0 (report-only, 2 pending changesets name touched files, both still true) ; check:pending-changeset-literals 0 ; check:new-line-citations 0 (0 new) ; check:test-path-roots 0 ; check:readme-exports 0 ; check:doc-snippets 0 (679/679) ; check:doc-fences 0 ; check-vi-mock-specifiers/-inherit/-override-shape 0 ; check-control-bytes 0 ; check-shell-escape-residue 0 ; check-governed-queue-guard --test NOT GOVERNED. Lint narrowed: eslint --no-inline-config over the 12 changed source files, 0 errors / 27 warnings, identical to the same 10 files at base (27); no type-aware linting in eslint.config.js (0 projectService/parserOptions); full pnpm lint is left to CI. Verify lock: DECLARED UNLOCKED MODE (macOS, no flock). Ablations via objectstack ablation-replace.mjs (anchor 1→0, blob moved, restored blob == HEAD, git diff HEAD empty): (1) drop the callbackURL spread in createAuthClient.signUp → 5 red (must-have pin, form-to-wire pin, 3 console RegisterPage mounts), negatives green ; (2) withConsoleBaseRootRelative → withConsoleBase in RegisterPage → 1 red (embedded received ./accept-invitation/inv_1) ; (3) restore pre-fix Resend redirect||'/' → 2 red (embedded received /accept-invitation/inv_1 ; no-redirect received '/', a byte-level key-absent pin) ; (4) reverse type check: prop typo in RegisterPage → console tsc TS2322 against RegisterFormProps, and the README typo → check:doc-snippets red. The ablation-replace self-test fails 15 cases on macOS (tmpdir /var vs /private/var realpath); real runs in the worktree printed their own landed/restored proofs.",
    "mcp_calls": "0",
    "api_writes": "3 REST writes: POST /repos/objectstack-ai/objectui/pulls (draft PR 10904) ; POST /repos//issues/10904/assignees (hotlong) ; POST /repos//issues/10893/comments (this os-dev-report). Plus 4 git pushes (empty branch + 3 commits). No labels, no issue-assignee writes.",
    "open_questions": [
    {
    "question": "The claim's file surface was packages/auth/src/**. The fix also touches apps/console (RegisterPage.tsx, VerifyEmailPromptPage.tsx, utils/consoleBase.ts and tests), because ?redirect= is owned there. Keep it in one PR?",
    "options": [
    "A keep one PR; the seat records the surface extension on the claim",
    "B split: packages/auth API in one PR, console wiring in a second PR blocked on it"
    ],
    "recommendation": "A. Real business need: B's first half alone changes nothing a user sees. Long-term: one contract (SignUpData.callbackURL) and its only producer land together. AI-proofing: an explicit prop fed by the page that owns ?redirect= beats the library sniffing the URL. No scope spread: +3 console files, and no new gate or surface beyond one exported helper."
    },
    {
    "question": "The Resend fix in VerifyEmailPromptPage went in as a bounded in-place fix (same defect class, same helper, no other open PR on the file). Keep it or drop it back to a card?",
    "options": [
    "A keep it",
    "B revert that hunk and file a separate card"
    ],
    "recommendation": "A. Without it, an invitee who clicks Resend gets a link to /accept-invitation/ID at the origin root, outside /_console (probe row 4). That is the same user path as acceptance item 1."
    }
    ],
    "out_of_scope_findings": [
    "class: none (unmeasured reach) · if a deployment sets emailVerification.sendOnSignIn, an unverified user's sign-in mails callbackURL=/ because LoginForm/signIn send none; on /sign-in/email a callbackURL also makes the response carry redirect:true + url, so it is a design question, not a one-liner · whether cloud sets sendOnSignIn: NOT MEASURED · dedupe words: sendOnSignIn, sign-in verification callbackURL, EMAIL_NOT_VERIFIED redirect",
    "carrier: none (承接者:无) · noted in the PR Acceptance notes, not filed: the invite-context copy on login/register lives in apps/console/src/pages/auth/LoginPage.tsx and RegisterPage.tsx; better-auth /organization/get-invitation needs a session (UNAUTHORIZED otherwise), so 'invited to X' before sign-in needs a server-side public read",
    "carrier: none (承接者:无) · noted in the PR Acceptance notes, not filed: the pending-invitation hint on the workspace screen would live in packages/app-shell/src/console/organizations/OrganizationsPage.tsx; useAuth().listUserInvitations exists and nothing in app-shell or console reads it",
    "carrier: none (承接者:无) · noted, not filed: packages/app-shell DefaultRegisterPage has no ?redirect= handling; it has no consumer in this repo (console routes /register to its own RegisterPage)"
    ]
    }


    Generated by Claude Code

  7. hotlong commented on Sep 28, 2026

    @hotlong
    ContributorAuthor

    Review: ACCEPT (PR #10904 at 3fd6dcd7e)

    Implemented-by: os-dev subagent of session 786273a0-0246-4bb2-b026-bb37ba5d7295
    Reviewed-by: epic PM, session 786273a0-0246-4bb2-b026-bb37ba5d7295 (objectstack-ai/cloud#2440)

    Diff read by the reviewer.

    • createAuthClient.signUp forwards an optional callbackURL (+6); SignUpData / RegisterForm / useAuth().signUp carry it additively.
    • The console RegisterPage passes a safe ?redirect= through the new root-relative withConsoleBaseRootRelative.
    • VerifyEmailPromptPage Resend sends the same value.
    • Changeset included. The path surface has no governed files (the dev ran check-governed-queue-guard --test: NOT GOVERNED).

    File-surface extension, recorded on the claim. The claim named packages/auth/src/**. The fix also needs apps/console/src/pages/auth/{RegisterPage,VerifyEmailPromptPage}.tsx and apps/console/src/utils/consoleBase.ts plus their tests, because ?redirect= is owned by the console page. Accepted as one PR: open question 1, option A.

    Open question 2. Keep the Resend fix (option A). It is the same defect class on the same user path, and the ablation pins it.

    Evidence re-read.

    • A server probe on better-auth 1.7.2 and 1.7.3 checked three cases:
      • an absent callbackURL gives /;
      • a root-relative one is honoured and 302s with the session cookie;
      • a document-relative one is refused 403 INVALID_CALLBACK_URL. That is why the root-relative helper exists, and an ablation pins it.
    • Gates: auth build / type-check / test, and console type-check / test, all exit 0.
    • End-to-end invite → register → verify was not run. It reaches cloud only with the .objectui-sha bump on objectstack-ai/cloud#2441.

    Findings. The invite-context copy, the pending-invitation hint and sendOnSignIn are noted in the PR's Acceptance notes; nothing is filed.

    Landing: ready + auto-merge; it lands through the queue when checks are green.

  8. hotlong commented on Sep 28, 2026

    @hotlong
    ContributorAuthor

    Landed: PR #10904 merged as 7ea8118f7c. Verified that objectui main contains it: compare answers ahead/identical. Epic PM, session 786273a0-0246-4bb2-b026-bb37ba5d7295.

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:donepm:epicParent delegated to a dedicated epic PM — other PMs never dispatch into its subtree

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions