Repository navigation
Description
repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv
Filing category ③: a direct task from a maintainer ruling. objectstack-ai/mobile#148 comment 6593861138: Ruling: batch #1 item 4 · letter B · maintainer 「同意」. Option B reuses the auth library's existing plugin instead of hand-writing a code-exchange endpoint.
Why
Some identity providers (Google among them) refuse embedded WebViews. A mobile user who signs in with such a provider cannot sign in to the app today: objectstack-ai/mobile#176 detects the refusal and shows "not supported in the app yet".
The ruled flow:
- The shell opens the sign-in in the system auth session (
ASWebAuthenticationSessionon iOS, Custom Tabs on Android). - The server issues a single-use, short-lived code to the app scheme.
- The Console in the WebView exchanges that code for its own session.
The ruling's premise, measured by the seat
better-auth@1.7.12 is the version packages/plugins/plugin-auth pins. Its npm tarball ships ./plugins/one-time-token, with these endpoints:
/one-time-token/generate: an authenticated session issues a token;/one-time-token/verify: the token is exchanged for a session.
Read from dist/plugins/one-time-token:
- default expiry 3 minutes (
expiresIn ?? 3); storeToken: "hashed"is available;disableClientRequestexists.
On objectstack main, git grep -nE "oneTimeToken|@better-auth/expo" -- packages/plugins/plugin-auth returns 0 hits, so the plugin is not enabled today.
Ask
- Enable the plugin in
plugin-auth's plugin list:- hashed storage;
- a short expiry (state the value and why);
- generation only by a server-side redirect handler, not by a client request, if
disableClientRequestsupports that shape.
- Add the redirect step. After the provider sign-in completes in the system browser, the server redirects to the app scheme callback with the token. The mobile card designs the shell half; this card only needs the server to emit the redirect for a registered app scheme.
- Interception (human floor): the plugin is not PKCE-bound to the attempt that started the sign-in. The PR must state how a callback intercepted by another app on the device is kept from being redeemed. One option is to bind the token to a verifier the shell generated, sent at the start and required at verify. The maintainer reviews and merges this PR.
- Declare the new public surface in
packages/spec, as the contract-first rule requires. The spec lane owns that half.
Who acts
The domain:server seat, with the spec half to the domain:spec seat. Triage first-touch decides the grade.
Duplicate check (2026-11-03)
| Query | Hits |
|---|---|
is:issue oneTimeToken plugin |
0 |
is:issue one-time-token |
2121 tokenised hits, none on this plugin in the first page |
Activity
objectstack-fleet commentedon Nov 3, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: first-touch audit · p2 ·
domain:server·pm:queue· 2026-11-03T02:55Z⛔ Not a claim.
packages/specby adomain:speccard or PR before the server PR goes ready. The server seat files it at claim, unless the spec lane takes it from here.Generated by Claude Code
objectstack-fleet commentedon Nov 3, 2026
domain:server#2 · os-warren · session_01JQwxbXKRzgnVvjzxaYxLUf
Claim: PM loop round 1 · card thread · 2026-11-03T03:06Z
Session:
session_01JQwxbXKRzgnVvjzxaYxLUf(the seat); the card thread issession_01JQwxbZkE4E6sE72j23AfwxAccount:
os-warrenBranch:
claude/project-thread-p4ozlwWorktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#2File surface:
packages/plugins/plugin-auth/(plugin list, the app-scheme redirect, the verify binding), its route ledger / envelope / authz rows and tests, one.changeset/29714-*.md; ⛔ nopackages/specpath (that is #29736)Container & model: card thread,
mode:thread,model: TIER_DEFAULT(--tiernames no path mandate),effort: project defaultClause-②: yes
Metadata-protocol: none
Responsibility:
plugin-auth (this lane)|a provider that refuses embedded WebViews blocks mobile sign-in|mobile users signing in with such a provider; reachable today (mobile#176)Thread-read: 6594269680
Premise: holds (card author measured better-auth 1.7.12 ships the plugin; 0 hits for oneTimeToken in plugin-auth on main)
Serial constraints cleared: spec slice filed as #29736 (contract first): this PR stays draft until #29736 lands
Order: project file pm-orders/server/seat2/order-29714.md.
Generated by Claude Code
objectstack-fleet commentedon Nov 3, 2026
domain:server#2 · os-warren · session_01JQwxbZkE4E6sE72j23Afwx
os-dev-report
{ "issue": 29714, "status": "done", "branch": "claude/project-thread-p4ozlw", "pr": "https://github.com/objectstack-ai/objectstack/pull/29766", "session": "session_01JQwxbZkE4E6sE72j23Afwx", "premise_still_valid": true, "summary": "plugin-auth enables better-auth 1.7.12's one-time-token plugin (expiresIn 1 minute, disableClientRequest, storeToken custom-hasher) when trustedAppSchemes registers at least one app scheme, and mounts GET /api/v1/auth/one-time-token/handoff: session required, closed query set {redirect_uri, code_challenge, code_challenge_method}, 302 to a registered scheme only with token=T, 400 VALIDATION_ERROR and no redirect otherwise, 401 signed out. Interception: the plugin's stored key is SHA-256(T:C) for the S256 challenge given at hand-off (bound through an AsyncLocalStorage scope around the in-process vendor generate), and the vendor verify recomputes it from a token sent as T.code_verifier, so an intercepted T is refused with the vendor 400 and consumes nothing. trustedAppSchemes is strict as agreed with the #29736 spec thread (lowercase reverse-domain, no colon suffix, web and script schemes refused at construction and applyConfigPatch); it is declared locally on AuthManagerOptions until #29736 lands, then that line goes on rebase. The PR stays draft until #29736 lands; the maintainer reviews and merges (human floor).", "tests": "head 766bb724. pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/one-time-token-handoff.test.ts src/auth-route-ledger.conformance.test.ts: Test Files 2 passed, Tests 40 passed (30 new in one-time-token-handoff.test.ts, real AuthManager plus the real registerAuthRoutes mount over the shared in-memory engine). plugin-auth typecheck (src, examples, test layer) under the lock: VERDICT command-exit 0. Ablation, fix committed first, via scripts/ablation-replace.mjs (anchor 1 to 0, blob 71e9b78c to 96549e57, restored to the HEAD blob, git diff HEAD empty): the hasher keyed on T alone (the vendor hashed behaviour, no binding) turned the interception pin and the outside-scope pin red, 28 others green. Before and after measured: on main all three one-time-token routes answer 404; after, generate answers 401 anonymous and 400 Client requests are disabled signed in, verify of a bogus token 400 Invalid token.", "gates": "node scripts/pm/dispatch-gates.mjs --commands on 766bb724 derived 64 families; 63 run, exit 0 each; pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (reads every package dist, this container built only the plugin-auth closure): NOT MEASURED locally, CI's. check-plugin-teardown-shape --self-test first exit 3 (shallow clone lacked its pinned fixture commit), exit 0 after fetching that one commit. --ran reconciliation exit 0: 64 derived accounted for, 63 run, 1 NOT-MEASURED. check:auth-mount-ledger: 24 mounts accounted for. eslint on the 7 touched source files exit 0; full pnpm lint exit 0 on 766bb724. PR check-runs: not yet concluded when this report was written; the seat reads them.", "dogfood_battery": "n/a: no exclamation mark in the PR title and the Clause-② line carries the widening arm, not narrowing", "mcp_calls": "0", "api_writes": "2: POST /repos/objectstack-ai/objectstack/dispatches (fleet-write relay pr_create, created PR 29766, body read back 15569 of 15569 bytes identical), and this os-dev-report comment through post-stamped.mjs. git push is not a REST write.", "open_questions": [ { "question": "A malicious app can start its OWN hand-off (its own challenge, a registered-looking scheme) in a system browser that already holds the user's session; the binding stops interception of someone else's callback, not this. iOS ASWebAuthenticationSession shows a consent sheet unless ephemeral; Android Custom Tabs show nothing. Which mitigation?", "options": [ "A: refuse a hand-off from a session older than a few minutes (fresh sign-in required); a few lines on the route, no UI, but an existing browser session must sign in again", "B: an interstitial Continue-to-app page the hand-off renders, which POSTs to mint; UI plus a CSRF-safe POST leg", "C: accept, relying on the platform consent sheet and the registered-scheme list" ], "recommendation": "A, because it closes the silent case with no UI; not built here because it changes the shell's flow, which is the mobile card's call too. Stated in the PR's Acceptance notes for the maintainer's review." } ], "security_findings": "none", "out_of_scope_findings": [ "carrier: the mobile shell card · the redeemed session IS the system browser's session (the vendor stores the session token), so signing out in the app signs out the browser and the ADR-0069 gate holding one holds the other; noted in the PR, not filed", "carrier: none · a redemption writes no sign-in audit event and no session row (the after-hooks key on the sign-in paths); noted in the PR, not filed" ], "deviations": [ "The dispatch order asks for pnpm lint; os-dev assigns repo-wide lint to CI. Ran both the touched-file eslint and the full pnpm lint, both exit 0.", "The harness attribution reminder asks for a model-named Co-Authored-By trailer; commits carry the model-free pair AGENTS.md requires. The merge commit of origin/main carries git's default message.", "No packages/spec edit. The new key is declared on AuthManagerOptions until #29736 lands; on rebase that declaration is deleted and the spec's wins." ], "body_amendments": [ "After #29736 lands and this branch is rebased onto it: in What changes, replace the sentence that begins Until #29736 lands with one saying the key is read from the spec's AuthConfig.trustedAppSchemes, and cite the new head in Verification." ] }objectstack-fleet commentedon Nov 3, 2026
domain:server#2 · os-warren · session_01JQwxbXKRzgnVvjzxaYxLUf
Ruling: decision card cmsg_01U5RdnAAmkSQmZbqh6FtqZ8X5ext5gvz9uoXDzxE3L2Ei (thread "Decide mobile sign-in session freshness") · letter A · maintainer tapped 「A · 要求会话新鲜(推荐)」 · recorded 2026-11-03T06:25Z
Tapped at 2026-11-03T06:23Z on the decision card.
What letter A binds (PR #29766, residual risk 1 of its Acceptance notes):
GET /api/v1/auth/one-time-token/handoffrefuses unless the caller's session was created within about 5 minutes (a freshness check on the route; no UI).Sec-Fetch-Site: cross-site, checked only when that header is present (a browser that sends no such header is not refused for its absence).Clause-②unchanged by the ruling (yes (widening), the route itself; the freshness rule narrows only the route this PR adds).Generated by Claude Code
objectstack-fleet commentedon Nov 3, 2026
domain:server#2 · os-warren · session_01JQwxbXKRzgnVvjzxaYxLUf
For repo:mobile#1 (the filer of this card, os-justin), at 2026-11-03T06:30Z: ruling A on this card (6597001931) changes the shell half of the hand-off.
GET /api/v1/auth/one-time-token/handoffrefuses unless the caller's session was created within 5 minutes:401 UNAUTHENTICATEDwith a "sign in again" message, the same answer as no session, so the shell needs one branch for both.Sec-Fetch-Site: cross-sitewith400 VALIDATION_ERROR. A request with no such header, or withnone,same-originorsame-site, hands off.Server side: PR #29766 (built at 4b3e1fd, lands after the spec slice #29804). Please carry this into the mobile card that designs the shell half (from objectstack-ai/mobile#148 item 4 / #176).
Generated by Claude Code
objectstack-fleet commentedon Nov 3, 2026
domain:server#2 · os-warren · session_01JQwxbXKRzgnVvjzxaYxLUf
Landing record · #29714 · 2026-11-03T08:23Z
2b15e302fa45af2dcb122005c3752aec2377c023; head01e2f33e; this card closed byFixes.TIER_DEFAULT(declaration face; the spec half spec(auth): declare the mobile sign-in hand-off surface (one-time-token generate/verify paths, the registered app-scheme list, any advertised feature flag) before #29714 enables it #29736 / PR feat(spec): declare the mobile sign-in hand-off surface (one-time-token hand-off and verify, registered app schemes) #29804 had its contract review) → ruling A (6597001931) built at4b3e1fd5→ rebased onto feat(spec): declare the mobile sign-in hand-off surface (one-time-token hand-off and verify, registered app schemes) #29804 at01e2f33e(specTrustedAppSchemeSchemaimported, route paths fromAuthEndpointPaths) → PR body amended by the seat → seat ACCEPT 6598400823 →pr_ready+automerge_enableby the seat.Clause-②: yes (widening)(three auth routes reachable that were 404),Metadata-protocol: none,@objectstack/plugin-authchangeset.pm:dispatchedremoved in the same stroke as this record.Generated by Claude Code