Repository navigation
邀请注册链路丢失 redirect:signUp 不传 callbackURL,被邀请人验证邮箱后被带去「创建工作区」 #10893
Description
Activity
最新版复测(2026-09-28 第二轮:objectstack main
862b6ce8· objectui main35d68c4c· cloud main96eb092f,本地合入 cloud#2423 的配套修改;没有这些修改,cloud main 在 framework main 上起不来)在 objectui main
35d68c4c上仍然复现。 被邀请人注册时,redirect 一直带到了 verify-email-prompt,但验证邮件里是callbackURL=%2F;验证完落到「创建工作区」。一个小变化:重新打开邀请链接并接受后,现在会落到云 welcome 页(锁定版本那一轮落在通用的/home)。- addedpm:epicParent delegated to a dedicated epic PM — other PMs never dispatch into its subtreeParent delegated to a dedicated epic PM — other PMs never dispatch into its subtree
on Sep 28, 2026 - addeddomain: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 seat
on Sep 28, 2026 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 viapm:epic
Seat: epic PM for objectstack-ai/cloud#2440 (seat 1)
File surface:packages/auth/src/**(createAuthClient.tssignUp,RegisterForm.tsxif 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 touchespackages/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).
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
Review: ACCEPT (PR #10904 at
3fd6dcd7e)Implemented-by: os-dev subagent of session
786273a0-0246-4bb2-b026-bb37ba5d7295
Reviewed-by: epic PM, session786273a0-0246-4bb2-b026-bb37ba5d7295(objectstack-ai/cloud#2440)Diff read by the reviewer.
createAuthClient.signUpforwards an optionalcallbackURL(+6);SignUpData/RegisterForm/useAuth().signUpcarry it additively.- The console
RegisterPagepasses a safe?redirect=through the new root-relativewithConsoleBaseRootRelative. VerifyEmailPromptPageResend 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 needsapps/console/src/pages/auth/{RegisterPage,VerifyEmailPromptPage}.tsxandapps/console/src/utils/consoleBase.tsplus 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
callbackURLgives/; - 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.
- an absent
- 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-shabump on objectstack-ai/cloud#2441.
Findings. The invite-context copy, the pending-invitation hint and
sendOnSignInare noted in the PR's Acceptance notes; nothing is filed.Landing: ready + auto-merge; it lands through the queue when checks are green.
Landed: PR #10904 merged as
7ea8118f7c. Verified thatobjectuimaincontains it:compareanswers ahead/identical. Epic PM, session786273a0-0246-4bb2-b026-bb37ba5d7295.
来源
2026-09-28 cloud 本地全链路验收:cloud
96eb092f,objectui pinefead6c6。已对照 objectuimain(24845c4f1),问题代码未变。测试角色:陈晓邀请一个还没有账号的同事 wangwei@example.com 加入「晓光科技」。现象
被邀请人点击邀请邮件里的链接
/_console/accept-invitation/<id>:/_console/login?redirect=/accept-invitation/<id>。页面上看不出这是一个邀请,只是普通登录页。/_console/register?redirect=…,redirect 还在 ✓/_console/verify-email-prompt?email=…&redirect=…,redirect 还在 ✓…/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的做法对齐)。另外可以考虑两个兜底:验收
signUp在 redirect 存在时把callbackURL带到/sign-up/email的请求体里。相关
&同样会丢掉 callbackURL