Skip to content

Mobile hand-off: a signed-out page navigation goes to the sign-in page, so the app never has to name the Console's login route #29997

Open

Description

@hotlong

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:

  1. A non-navigation request still gets 401. The app's anonymous probe sends Accept: application/json and reads 401 as "this server registered me". Telling the two apart is a design question for the PR body: Sec-Fetch-Dest: document is the obvious signal, Accept: text/html the fallback. Neither has been measured in a real ASWebAuthenticationSession or Custom Tabs.
  2. Every refusal keeps its place, ahead of the new redirect. Sec-Fetch-Site: cross-site is judged first, then the closed query set, the registered scheme and the challenge. An invalid request gets 400 and never reaches the sign-in page.
  3. 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.getConsoleUrl composes from the canonical origin, so a deployment reached on another host would move the browser. Say in the PR how uiBasePath is honoured without that move.
  4. redirect is rebuilt from the validated parameters, root-relative under the deployment's base path, in the form handoffNavigationTarget in fix(console): the sign-in page finishes the mobile sign-in hand-off instead of landing on a missing Console page #29979 matches.
  5. 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.
  6. 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 the 302; a JSON request gets 401; an unregistered scheme, a bad challenge and an unknown parameter get 400 with no redirect; cross-site gets 400 first.
  • Update the Answers list on OneTimeTokenHandoffQuerySchema, the route's module docblock, and its auth-route-ledger.ts note, which say "session required".
  • An end-to-end browser test in this repository: open the hand-off signed out, sign in, and see the 302 to 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 the os serve half).
  • 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

  1. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    Contributor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: first-touch audit · p2 · domain:server · pm:queue · 2026-11-04T01:54Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    Contributor

    domain:server#2 · hotlong · session_c967d7a472b0485c94711bfcb22159c8

    Claim: PM loop round 1
    Session: session_c967d7a472b0485c94711bfcb22159c8
    Account: hotlong
    Branch: claude/issue-29997-handoff-signin-entry
    Worktree: objectstack-issue-29997
    Domain: domain:server
    Seat: domain:server#2
    File surface: packages/plugins/plugin-auth/src/one-time-token-handoff.ts and 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 under ui/e2e or 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: xhigh
    Clause-②: no
    Metadata-protocol: none
    Responsibility: n/a — not a defect card
    Thread-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 or auth-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-②: no reasoning: the route gains an answer for a navigation it already refused with 401. 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:subagent although the surface names packages/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: holds is the seat's own reading on origin/main f19fbb8, not a read-only subagent's: runOneTimeTokenHandoff answers a missing or old session with 401 JSON, 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 hotlong token, not through scripts/pm/. That was a mistake by this session: the fleet credentials were available. Later writes use the fleet tools.

    Posted 2026-11-04T03:21Z.

  3. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    Contributor

    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"
    ]
    }

  4. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    Contributor

    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.

  5. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    Contributor

    domain:server#2 · hotlong · session_c967d7a472b0485c94711bfcb22159c8

    Claim update. #29979 and #30004 have merged, so the wait written on this card is over and the patch round on PR #30010 starts now, by the same dev on the same branch.

    File surface now adds: ui/apps/console/src/pages/auth/handoffRedirect.ts and its tests under ui/apps/console/src/pages/auth/__tests__/ (the page half of constraint 5), plus examples/app-showcase/playwright.config.ts and the doc comment of examples/app-showcase/test/showcase-smoke.live.test.ts, both already in the PR. This is the arrangement in #29979's comment 6620189624, where the UI seat said this card carries both halves if #29979 lands first. No open domain:ui PR is known to touch that file; the dev re-checks against origin/main.

    Posted 2026-11-04T13:48Z.

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions