Repository navigation
Description
Category: ③ a task the maintainer ordered directly.
Maintainer, 2026-11-04, in the session working on objectstack-ai/mobile#148, choosing how the app enters the mobile sign-in: "改成服务端入口".
Business reason: people on a provider that refuses embedded WebViews (Google among them) sign in through this flow, and an installed app stays in use for months. Today the app has to know the Console's sign-in route and how that page treats redirect. If a Console upgrade changes either one, those people are stranded on a page with no way back, and the app cannot detect it. 18.3.0 already showed this failure: the route shipped and the Console could not reach it (#29971). This flow has not shipped in any app yet, so the entry can change now with nothing to keep compatible.
Reader: domain:server, one PR on packages/plugins/plugin-auth/src/one-time-token-handoff.ts, with the spec docblock and the route ledger. objectstack-ai/mobile#190 then changes its entry URL.
Duplicate search: "one-time-token/handoff" in issues (4 hits) and handoff "sign-in page" redirect anonymous (5 hits). None asks for this.
Today
The app opens /_console/login?redirect=<hand-off> in the system auth session (signInEntryUrl in objectstack-ai/mobile#190). GET /api/v1/auth/one-time-token/handoff answers a missing or old session with 401 as raw JSON, so opening the route directly leaves the user looking at JSON.
What changes
A page navigation to the hand-off with no session, or with a session older than five minutes, answers 302 to this deployment's Console sign-in page, with redirect set to the same hand-off request. After the sign-in, the page forwards back as #29971 specifies. The app then opens only the hand-off URL, a published route, and the sign-in route stays internal to this repository and its release.
Constraints:
- A non-navigation request still gets
401. The app's anonymous probe sendsAccept: application/jsonand reads401as "this server registered me". Telling the two apart is a design question for the PR body:Sec-Fetch-Dest: documentis the obvious signal,Accept: text/htmlthe fallback. Neither has been measured in a realASWebAuthenticationSessionor Custom Tabs. - Every refusal keeps its place, ahead of the new redirect.
Sec-Fetch-Site: cross-siteis judged first, then the closed query set, the registered scheme and the challenge. An invalid request gets400and never reaches the sign-in page. - The browser stays on the origin the request arrived on. The session cookie and fix(console): the sign-in page finishes the mobile sign-in hand-off instead of landing on a missing Console page #29979's same-origin match both depend on it.
AuthManager.getConsoleUrlcomposes from the canonical origin, so a deployment reached on another host would move the browser. Say in the PR howuiBasePathis honoured without that move. redirectis rebuilt from the validated parameters, root-relative under the deployment's base path, in the formhandoffNavigationTargetin fix(console): the sign-in page finishes the mobile sign-in hand-off instead of landing on a missing Console page #29979 matches.- It must not loop. fix(console): the sign-in page finishes the mobile sign-in hand-off instead of landing on a missing Console page #29979 judges a session's age on the device's clock and this route judges it on the server's. On a device whose clock is behind, the page forwards a session the route calls old, and the route would send it straight back. Today that case stops on JSON; with this change it would bounce forever. This is read from the code, not measured. One way out: the route marks its redirect so the page keeps the form up until a sign-in happens on that page. That touches fix(console): the sign-in page finishes the mobile sign-in hand-off instead of landing on a missing Console page #29979, so agree it there.
- A deployment with no Console mounted. Say what the route does there. It should not redirect to a page that does not exist.
Security
This is a session-transfer flow, so the maintainer reviews and merges. Ruling A on #29714 stays as it is: a token is minted only for a session under five minutes old, and a cross-site navigation is refused. The new answer adds no capability, since anyone can already open /_console/login?redirect=<hand-off>.
Acceptance notes
- Unit tests on
runOneTimeTokenHandoff: page navigation with no session and with an old session gets the302; a JSON request gets401; an unregistered scheme, a bad challenge and an unknown parameter get400with no redirect; cross-site gets400first. - Update the
Answerslist onOneTimeTokenHandoffQuerySchema, the route's module docblock, and itsauth-route-ledger.tsnote, which say "session required". - An end-to-end browser test in this repository: open the hand-off signed out, sign in, and see the
302to the registered scheme. It should run in CI, because it is what catches a Console upgrade breaking this for installed apps. It needs a registered scheme in the test server's config (os serve has no way to register trustedAppSchemes, so the mobile hand-off refuses every app #29972 is theos servehalf). - Say in the PR which release carries this. The app can only change its entry once this has shipped, because an older server answers the app's new entry with JSON.
Blocks: objectstack-ai/mobile#148
Activity
objectstack-fleet commentedon Nov 4, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: first-touch audit · p2 ·
domain:server·pm:queue· 2026-11-04T01:54Z⛔ Not a claim.
302to the Console sign-in page, so the app no longer has to know the Console's login route. It has not shipped in any app yet and gates no release, so p2.plugin-auth'sone-time-token-handoff.ts, with the spec docblock and the route ledger. Constraint 5 (no redirect loop) touches PR fix(console): the sign-in page finishes the mobile sign-in hand-off instead of landing on a missing Console page #29979, so agree it there withdomain:ui.Generated by Claude Code
objectstack-fleet commentedon Nov 4, 2026
domain:server#2 · hotlong · session_c967d7a472b0485c94711bfcb22159c8
Claim: PM loop round 1
Session:
session_c967d7a472b0485c94711bfcb22159c8Account:
hotlongBranch:
claude/issue-29997-handoff-signin-entryWorktree:
objectstack-issue-29997Domain:
domain:serverSeat:
domain:server#2File surface:
packages/plugins/plugin-auth/src/one-time-token-handoff.tsand its tests,packages/plugins/plugin-auth/src/auth-plugin.ts(the route wiring),packages/plugins/plugin-auth/src/auth-route-ledger.ts,packages/spec/src/api/auth-endpoints.zod.ts(docblock only), one browser test underui/e2eor wherever the repo's auth end-to-end tests live, one root changeset (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate; the seat's call, default),effort: xhighClause-②: no
Metadata-protocol: none
Responsibility:
n/a — not a defect cardThread-read: 6620419266
Premise: holds
Serial constraints cleared: PR #30004 (
packages/cli/src/commands/serve.ts, disjoint files; the browser test may need its setting or its own test config). PR #29979 (ui/apps/console/src/pages/auth/**, disjoint files, but constraint 5 on this card touches it; agree that there). No other open PR touches the hand-off, the ledger orauth-endpoints.zod.ts(read 2026-11-04 across open PRs, limit 100).Direct order. Source: the maintainer, in session
session_c967d7a472b0485c94711bfcb22159c8, 2026-11-04. Words: "改成服务端入口" and "你应该用pm-dispatch 技能". This session takes this card only, not the lane queue.Clause-②: noreasoning: the route gains an answer for a navigation it already refused with401. That is a runtime authentication behaviour, on the security-boundary floor rather than clause ②, so the maintainer reviews and merges. The spec change is prose describing that answer. The dev confirms no schema or export moves.mode:subagentalthough the surface namespackages/spec/src/**and another package: the spec edit is docblock only, so it does not force the rebuild that makes such a card build-heavy.Premise: holdsis the seat's own reading onorigin/mainf19fbb8, not a read-only subagent's:runOneTimeTokenHandoffanswers a missing or old session with401JSON, and no commit since has changed it.Correction: this card and this session's comments on #29979, objectstack-ai/mobile#148 and objectstack-ai/mobile#190 went out under the
hotlongtoken, not throughscripts/pm/. That was a mistake by this session: the fleet credentials were available. Later writes use the fleet tools.Posted 2026-11-04T03:21Z.
objectstack-fleet commentedon Nov 4, 2026
domain:server#2 · hotlong · session_c967d7a472b0485c94711bfcb22159c8
os-dev-report
{
"issue": 29997,
"status": "done",
"branch": "claude/issue-29997-handoff-signin-entry",
"pr": "#30010",
"session": "session_c967d7a472b0485c94711bfcb22159c8",
"premise_still_valid": true,
"summary": "A page navigation to the hand-off with no session or one older than five minutes now answers 302 to this deployment's Console sign-in page, root-relative, with redirect rebuilt from the validated parameters; any other request and a deployment serving no sign-in page keep the 401, and every 400 stays ahead. The loop is real, measured: 228 hand-off requests in 20 s with the guard removed, with a session really over five minutes old and the device clock 15 minutes behind. The route half is here: a same-origin navigation the route still refuses gets the 401, and the redirect carries handoff_refused_at, the server's time of the refusal. The page half is NOT here (PR #29979 owns that file): PM to carry it across; the exact change is in PR #30010 under 'What #29979 still has to do', measured working (1 request, form held, sign-in ends at the app).",
"tests": "plugin-auth full suite at fe2b8b4: 219 files, 3824 passed, 10 skipped. one-time-token-handoff.test.ts: 83 passed. Typecheck exit 0 for plugin-auth, example-showcase and spec; eslint exit 0 on 8 touched source files. Browser spec on a scratch tree (this branch + #29979 c6ac92e + #30004 3b20bb0): 1 passed, Chromium 151 headless. Spec ablations: route redirect removed, rebuilt, dist preflight clean: red at #login-email (25 s). Page no longer recognises the hand-off, Console rebuilt: red waiting for the 302 to the app (45 s). Unit ablations through scripts/ablation-replace.mjs, each restored with blob equal to HEAD: navigation guard 5 red, same-origin stop 1, served check 5, the 302 8, Location path 6, handoff_refused_at 2. Run under os-verify-lock.sh in declared UNLOCKED mode (no flock on this Mac), disclosed in the PR body.",
"gates": "Local at head fe2b8b4, merged with origin/main 217ba78: dispatch-gates --commands 85 commands, 85 exit 0, exit codes captured before any pipe. dispatch-gates --ran: 85 derived famil(ies) accounted for, 85 run, 0 NOT-MEASURED, exit 0. Roster gates under a touched directory (check:meta-url-spelling, check:authz-resolver, check:changeset-fixed, check:error-code-casing, check:filter-alias-parity): exit 0 each. check:generated: exit 0, 14 artifacts up to date. check-platform-checklist --changed-from origin/main: exit 0. An earlier pass found check:auth-mount-ledger red on a rawApp.fetch call (fixed in bf94338) and check:dual-build-cjs-loads exit 3 on an unbuilt tree (a full turbo build cleared it). PR check-runs at report time: 16 in_progress, 3 queued, 2 skipped; not awaited. NOT MEASURED locally: pnpm lint over the whole repo and test:repo (CI's).",
"line_budget": "no skills/** or ratcheted ledger touched. PR size 763 changed lines (+717/-46) across 9 files against the human-merge threshold 3000: under.",
"deviations": [
"Browser test is at examples/app-showcase/e2e, not ui/e2e. Live E2E UI serves the Console from vite preview at the root and its backend has no built Console, so the route's redirect has nothing to land on. It therefore runs in the nightly live tier, not per PR.",
"File surface beyond the claim: examples/app-showcase/playwright.config.ts (one env line) and the doc comment of examples/app-showcase/test/showcase-smoke.live.test.ts.",
"Zone 2 scheme assumption: a test-only stack config needs the app root and composeStacks, and every smoke spec would boot the composed stack (not run to the end after those two refusals). The spec uses OS_TRUSTED_APP_SCHEMES on the webServer instead, so it needs #30004 on main.",
"Landing order the PM must carry: merge #29979 and #30004 first, or the nightly live tier goes red and files a card; PR CI never collects the spec. The page half must ship in the same release, before objectstack-ai/mobile#190 changes its entry. Release: 18.4.0 as pending changesets stand.",
"Marker differs from what the #29979 thread agreed: a server time, not a bare mark. SocialSignInButtons defaults callbackURL to window.location.href, so a bare mark survives a provider round trip and would hold the form over the new session.",
"Narrow carve-out from 'a page navigation gets the 302': Sec-Fetch-Site same-origin keeps the 401. It is the landing-order-independent half of constraint 5. A browser with no Fetch Metadata can still bounce on a skewed clock until the page half lands (read from code, not measured).",
"NOT MEASURED: headers of a real ASWebAuthenticationSession or Custom Tabs. Playwright's WebKit and Firefox builds on this Mac do not match its version, so they were not read either. Headless Chromium read: Sec-Fetch-Dest document, Sec-Fetch-Site none.",
"PR opened with scripts/pm/with-fleet.sh -- gh pr create --draft: the relay refused this desktop session (dispatch.mjs: PREREQUISITE NOT MET, no GITHUB_TOKEN / GH_TOKEN; nothing sent), as the dispatch provided for.",
"The page-half patch used for the measurement is not on any branch; the scratch copy is /private/tmp/claude-501/-Users-zhuangjianguo-Documents-GitHub-objectstack--claude-worktrees-heuristic-cori-f2a888/c967d7a4-72b0-485c-9471-1bfcb22159c8/scratchpad/page-half.patch."
],
"files_changed": [
".changeset/29997-handoff-signin-entry.md",
"examples/app-showcase/e2e/mobile-signin-handoff.spec.ts",
"examples/app-showcase/playwright.config.ts",
"examples/app-showcase/test/showcase-smoke.live.test.ts",
"packages/plugins/plugin-auth/src/auth-plugin.ts",
"packages/plugins/plugin-auth/src/auth-route-ledger.ts",
"packages/plugins/plugin-auth/src/one-time-token-handoff.test.ts",
"packages/plugins/plugin-auth/src/one-time-token-handoff.ts",
"packages/spec/src/api/auth-endpoints.zod.ts"
],
"dogfood_battery": "n/a — no
!in the PR title and no (narrowing) arm","mcp_calls": "0 — no MCP GitHub call",
"api_writes": "2 — POST pulls through with-fleet (gh pr create, PR 30010), POST issues/29997/comments (this report) through post-stamped. Git pushes are not counted. The relay attempt sent nothing.",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: PR #29979 · the page half of constraint 5; the #29979 seat offered that #29997 carry it, and this dispatch keeps it out of this branch",
"carrier: cloud · objectstack-ai/cloud main 419ee51b: the per-environment AuthPlugin has registerRoutes false, and git grep finds no trustedAppSchemes or one-time-token/handoff under packages/ or apps/, so cloud environment hosts do not offer this flow; noted, not filed",
"carrier: none · packages/qa/dogfood/test/principal-session-console-boot.dogfood.test.ts skips on every CI run as the workflows read: none sets its Chromium path or builds the Console on that job; in PR acceptance notes, not filed"
]
}
objectstack-fleet commentedon Nov 4, 2026
domain:server#2 · hotlong · session_c967d7a472b0485c94711bfcb22159c8
Dev reported: PR #30010, draft. Not accepted yet: the seat record on that PR lists what is owed.
Waiting on: #29979 and #30004 to merge, since 2026-11-04. Then one patch round on #30010, then the contract review, then the maintainer. Waiting belongs to this claim; the card is not free to pick up.
Posted 2026-11-04T04:34Z.