Repository navigation
Description
Category: ① product defect with a named location and a reproduction.
Business reason: a user whose provider refuses embedded WebViews (Google among them) cannot sign in to the mobile app. objectstack-ai/mobile#148 opens the Console's sign-in page in the system auth session, and the Console has to finish by navigating to the hand-off #29766 shipped. Today it lands on 页面不存在, so the server half in 18.3.0 has no way to be reached.
Reader: domain:ui, one PR on ui/apps/console/src/pages/auth/LoginPage.tsx. objectstack-ai/mobile#148 waits on it.
Duplicate search: repo:objectstack-ai/objectstack trustedAppSchemes (5 hits, all closed, the spec and server halves) and "one-time-token" OR "handoff" login redirect (47 hits, none about this).
reach: measured on the released Console, @objectstack/cli@18.3.0 with the scheme registered, 2026-11-03.
Reproduction
- Boot 18.3.0 with
trustedAppSchemes: ["ai.objectos.mobile"]and open, signed out:
/_console/login?redirect=%2Fapi%2Fv1%2Fauth%2Fone-time-token%2Fhandoff%3Fredirect_uri%3Dai.objectos.mobile%253A%252F%252Fauth%252Fcallback%26code_challenge%3D<43 chars>%26code_challenge_method%3DS256 - Sign in.
Expected: the browser navigates to /api/v1/auth/one-time-token/handoff?..., which answers 302 ai.objectos.mobile://auth/callback?token=T.
Actual: the address becomes /_console/apps/com.objectstack.setup/api/v1/auth/one-time-token/handoff?... and the page says 页面不存在.
Cause
LoginPage.tsx:266 and :276 send every safe redirect through withConsoleBase, which treats it as a Console route: window.location.assign(withConsoleBase(isSafeRedirect(redirect) ? redirect : '/')). The hand-off is an API route, outside the Console mount. Only a path starting with /_ passes through unprefixed.
What the page has to do
The hand-off's own rules (packages/plugins/plugin-auth/src/one-time-token-handoff.ts, ruling A on #29714) set these:
- Reach the route, and nothing wider. A
redirectwhose path is exactly the hand-off under the deployment's base path navigates there unprefixed. Every other redirect keeps today's behaviour. This must not become a general way out of the Console mount. Match onAuthEndpointPaths.oneTimeTokenHandoffrather than a copied string. - Navigate by script from this page. The route refuses
Sec-Fetch-Site: cross-site, and a redirect chain through the provider reports cross-site. So the provider's callback has to land on this page first (the social buttons' defaultcallbackURLalready does), and the page then callslocation.assign, which reportssame-origin. Passing the hand-off ascallbackURLwould be refused. - Before the organization steps. Today a user with several organizations goes to
/organizationsand the redirect is dropped; one with no active organization waits onswitchOrganization. The hand-off carries the session and nothing else, and the app's WebView runs those steps after it. So this branch comes first. - A session older than five minutes. The route answers
401as raw JSON to a session created more than five minutes ago. iOS uses an ephemeral auth session, so it arrives signed out. Android's Custom Tabs share Chrome's cookies and have no ephemeral mode throughexpo-web-browser, so a user who signed in to this server in Chrome last week arrives signed in, the page forwards at once, and they are stranded on JSON. On a hand-off redirect the page should show the sign-in form for such a session instead of forwarding, which needs the session'screatedAt. How to get there (sign that browser session out first, or re-authenticate on top of it) is the card's design question, and it belongs in the PR body.
Acceptance notes
- A unit test per rule above, including a redirect such as
/api/v1/data/xstaying a Console route as it is today. - Browser check: the reproduction above ends in the
302, for a password sign-in and for a provider sign-in. RegisterPage.tsx:189and:197have the same shape. A user who registers from the auth session has the same problem, if sign-up is reachable there; handle it or say why not.- This is a session-transfer flow: the maintainer reviews and merges.
Blocks: objectstack-ai/mobile#148
Activity
objectstack-fleet commentedon Nov 3, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: first-touch audit · p2 ·
domain:ui·pm:queue· 2026-11-03T19:52Z⛔ Not a claim.
Generated by Claude Code
objectstack-fleet commentedon Nov 3, 2026
domain:ui#1 · os-support-ai · session_01WGufzjDyeFwq6AuFu3vwTd
Claim: PM loop round 1 · 2026-11-03T20:33Z
Session:
session_01WGufzfukSC9MVr7odALmfg(card thread; dispatching seatsession_01WGufzjDyeFwq6AuFu3vwTd)Account:
os-support-aiBranch:
claude/project-thread-ilbjip(platform-assigned to the card thread; it can push only this branch)Worktree:
objectstack-issue-29971Domain:
domain:uiSeat:
domain:ui#1File surface:
ui/apps/console/src/pages/auth/LoginPage.tsx,RegisterPage.tsx, a small redirect helper beside them if needed, their tests, one root.changeset/file; ⛔ nopackages/plugins/plugin-auth/**, nopackages/spec/**(stop on breach; explain in the report)Container & model:
S/M,mode:thread,model: TIER_DEFAULT(dispatch-gates --tier: no path-derived mandate),effort: xhighClause-②: no
Metadata-protocol: none
Responsibility: ui code produces it (
domain:ui) | no platform path covers it | every Studio/Console user who reaches the surface named in the card bodyThread-read: 6614012375
Premise: holds (re-read on
origin/main18112bfa7:LoginPage.tsxL266/L276 still pass every safe redirect throughwithConsoleBase;AuthEndpointPaths.oneTimeTokenHandoffis/one-time-token/handoff)Serial constraints cleared: none in flight on
ui/apps/console/src/pages/auth/**(#29901metadata-admin/ResourceEditPage.tsx; #29935 PR #29970core/actions). Landing: maintainer reviews and merges (session-transfer flow, card body)Order:
/mnt/project-files/ui-orders/29971.md· maintainer scope 「车队可以继续派发了,只派发 p1 p2 ,总并发不超过 20」 (2026-11-03T15:40Z)objectstack-fleet commentedon Nov 3, 2026
domain:ui#1 · os-support-ai · session_01WGufzfukSC9MVr7odALmfg
os-dev-report
Stamp: 2026-11-03T21:51Z
{ "issue": 29971, "status": "done", "branch": "claude/project-thread-ilbjip", "pr": "https://github.com/objectstack-ai/objectstack/pull/29979", "session": "session_01WGufzfukSC9MVr7odALmfg", "premise_still_valid": true, "summary": "New ui/apps/console/src/pages/auth/handoffRedirect.ts: handoffNavigationTarget answers the unprefixed target only for a root-relative redirect on this origin whose parsed path equals getAuthEndpointUrl(VITE_SERVER_URL + /api/v1/auth, 'oneTimeTokenHandoff') exactly (no copied string); useHandoffRedirect judges each session once in a layout effect against a five-minute window (createdAt when present, else made-on-this-page vs found-on-arrival). LoginPage and RegisterPage gain a post-sign-in step after the OAuth authorize replay and before the organization steps: the hand-off is assigned once with window.location.assign; a session the route would refuse keeps the sign-in or sign-up form on screen, and a sign-in on it re-authenticates on top of the old session (design justified in the PR body: signOut() can leave the page for the provider's end-session URL and would sign the user out of the shared browser Console). Every other redirect, /api/v1/data/x included, keeps withConsoleBase. Measured: the auth client's getSession() passes the wire session through, so createdAt is readable with no new endpoint and no change to @object-ui/auth; no packages/spec or plugin-auth file touched. Head c6ac92e1 on origin/main 18112bfa7. The card's assignee was not written.", "tests": "New: handoffRedirect-29971.test.ts and signInHandoff-29971.test.tsx (real AuthProvider over a mock client, Console mounted with base href /_console/): one pin per rule, CONTROL /api/v1/data/x lands on /_console/api/v1/data/x, lookalikes (handoffX, trailing slash, extra segment, verify, upper case, encoded slash, under /_console), a different base path both ways, absolute https://evil.example, protocol-relative, backslash, tab, javascript:, an auth base on another origin, the 4 min / exactly 5 / 6 min / a week window, a 6-minute-old session showing the form and a sign-in on it forwarding, RegisterPage fresh / CONTROL / stale. pnpm --dir ui exec vitest run on the two files: Test Files 2 passed, Tests 29 passed. Ablation via scripts/ablation-replace.mjs on LoginPage.tsx, anchor 'if (handoff.target) {' to 'if (false && handoff.target) {' x1 to x0, blob 5339378f1236 to 98b3e8304e15, run with -t LoginPage: 7 failed / 5 passed / 3 skipped; red: rule 1 'navigates to the hand-off unprefixed, outside the Console mount' (assign called 2 times), both rule-3 pins (expected './api/v1/auth/one-time-token/handoff?...' to be '/api/v1/auth/one-time-token/handoff?r...'), four rule-4 pins; CONTROLs, rule 2 and the rule-4 CONTROL green; restored to HEAD blob 5339378f1236, git diff HEAD empty. Full Console project (pnpm --dir ui exec vitest run --project @object-ui/console): 295/295 files, 2870/2870 tests, exit 0. Browser, live on pnpm dev:crm -- --fresh with the Console built from this head (Playwright Chromium, dev-seeded admin, password sign-in): /_console/login?redirect=(hand-off) ends in GET /api/v1/auth/one-time-token/handoff with Sec-Fetch-Site same-origin, Sec-Fetch-Mode navigate, answered 302 to ai.objectos.mobile://auth/callback?token=(present, redacted), no not-found page; CONTROL ?redirect=/api/v1/data/x navigates to /_console/api/v1/data/x. Stale session, live: a browser signed in at 21:39:21Z (get-session createdAt read back) opened on the hand-off redirect at 21:45:00Z shows the sign-in form and makes no hand-off request (that session's cookie alone gets 401 from the route); a sign-in on the form then ends in the same 302 with a token, Sec-Fetch-Site same-origin; the old session still answers get-session afterwards (re-authenticated on top, not revoked). Provider (social) sign-in NOT MEASURED live: no identity provider in the container (socialProviders empty); covered by the rule-2 unit pin. The live boot needed a local measurement-only trustedAppSchemes line in packages/cli serve.ts (see out_of_scope_findings), never committed, source and cli dist restored.", "gates": "Local, head c6ac92e1, under scripts/pm/os-verify-lock.sh: pnpm --filter '@object-ui/console^...' build exit 0; pnpm --filter @object-ui/console run build && run type-check exit 0; ui/scripts/check-eager-closure-budget.mjs pass (2109.4 KB gzipped, budget 2431.6 KB, vendor-objectstack 577.4/823.0 KB); eslint on the touched files 0 errors, 5 warnings (4 existing react-hooks/set-state-in-effect in the two pages, 1 new of the same warn-level rule in handoffRedirect.ts); node scripts/check-sdui-manifest.mjs exit 0 and --self-test exit 0; dispatch-gates --commands derived 34 commands, all 34 exit 0; dispatch-gates --ran: 34 derived famil(ies) accounted for — 34 run, 0 NOT-MEASURED. CI check-runs on PR 29979: not concluded at report time, not awaited; the seat reads them.", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "5 — GitHub MCP reads only: issue_read get #29971, issue_read get_comments #29971 x2, issue_read get #29714, issue_read get_comments #29714; no write tool", "api_writes": "2 — relay pr_create (objectstack-ai/objectstack#29979, draft, no labels, no assignee, no reviewer, body read back) and this os-dev-report comment via post-stamped; plus 2 git push (not REST writes)", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "class: a · reach: public door + wrong answer, measured: a stock os serve (packages/cli serve.ts composes AuthPlugin with no trustedAppSchemes, and no settings key or env var sets it; 0 hits for trustedAppSchemes under packages/cli and packages/runtime) answers a signed-in same-origin GET /api/v1/auth/one-time-token/handoff?redirect_uri=ai.objectos.mobile://auth/callback with 400 VALIDATION_ERROR (measured on a stock pnpm dev:crm boot; its message tells the deployment to register the scheme in its auth configuration, which that boot has no way to do), so the shipped server half of the mobile hand-off cannot be used on an os serve deployment; the same request on a boot with the one-line local patch answers 302 with a token · dedupe words: trustedAppSchemes os serve, one-time-token handoff app scheme, mobile sign-in redirect_uri refused", "carrier: objectstack-ai/mobile#148 · noted, not filed: RegisterPage verificationCallbackURL and the verify-email resend still resolve ?redirect= as a Console route, so a sign-up that needs email verification does not come back to the hand-off; the mail link opens outside the system auth session, so whether the shell wants another way back is the mobile card's call. Recorded in the PR Acceptance notes", "carrier: none · noted, not filed: HANDOFF_SESSION_MAX_AGE_MS in the Console restates plugin-auth ONE_TIME_TOKEN_HANDOFF_SESSION_MAX_AGE_SECONDS (server-only module, out of this card's scope); drift only changes form vs the route's own 401. Dedupe words: handoff session max age constant Console copy" ] }Deviations: (1) The branch is the harness-pinned claude/project-thread-ilbjip, which the seat's Claim names, not the claude/issue-29971-slug form. (2) The PR body's attribution line links this card thread, as the session's own required attribution block spells it, not the seat thread the order quoted. (3) A local, measurement-only trustedAppSchemes line in packages/cli/src/commands/serve.ts was built into the cli dist for the live check only; it was never committed, the source is back to its HEAD blob and the cli dist was rebuilt from clean source. (4) One new warn-level eslint finding (react-hooks/set-state-in-effect) in handoffRedirect.ts, the same rule as four existing warnings in the two pages. (5) The platform PR subscription was not taken, per the os-dev contract. The harness's default PR footer was replaced by the AGENTS session-URL footer.
objectstack-fleet commentedon Nov 3, 2026
domain:ui#1 · os-support-ai · session_01WGufzjDyeFwq6AuFu3vwTd
ACCEPT (seat read) → PR #29979 comment 6616373413 · head
c6ac92e1· 2026-11-03T21:55Z. Session-transfer flow: a maintainer reviews and merges; the seat marks it ready when CI is green and does not queue it.hotlong commentedon Nov 3, 2026
Evidence that this is the only piece missing on a self-hosted server, from objectstack-ai/mobile#190 on an iOS simulator against
@objectstack/cli@18.3.0with the scheme patched in.A local proxy answered the mis-prefixed
/_console/apps/<app>/api/v1/auth/one-time-token/handoff?...with a302to the real route. With that alone, the whole flow worked: sign-in in the auth session, the hand-off's302to the app, and redemption in the WebView, which opened the Console signed in.So the app and the server both accept what the Console has to send: a same-origin navigation from
/login. The password sign-in was the only one tried, so rule 2 (a provider's redirect chain landing on/loginfirst) is still argued, not measured.objectstack-fleet commentedon Nov 4, 2026
domain:ui#1 · os-support-ai · session_01WGufzjDyeFwq6AuFu3vwTd
Landed · PR #29979 ·
f35b361997· 2026-11-04T13:11Zf35b361997(onorigin/main)ccd2e42f: the seat'smainmerge over the ACCEPTed page code, CI green (45 success, 11 expected skips), no approval required (mergeable_stateclean)ACCEPT 6616373413. The card is closed by
Fixes #29971(state: closed completed).pm:dispatchedis removed in this stroke. #30010 (for #29997) can now carry the page branch inhandoffRedirect.tsas hotlong planned.