Repository navigation
feat(spec): declare the mobile sign-in hand-off surface (one-time-token hand-off and verify, registered app schemes) - #29804
Conversation
Claude-Session: https://claude.ai/code/session_01JQwxbXghRkYmSR5dVh8PFG Co-authored-by: Claude <noreply@anthropic.com>
Declares the one-time-token hand-off and verify routes with their request and response schemas, and AuthConfig.trustedAppSchemes, the registered native-app scheme list the hand-off may redirect a token to. Claude-Session: https://claude.ai/code/session_01JQwxbXghRkYmSR5dVh8PFG Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JQwxbXghRkYmSR5dVh8PFG Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JQwxbXghRkYmSR5dVh8PFG Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check16 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 168 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 24acc5c555edd7dc0ef75d0922dca9059c4f6292 && git checkout 24acc5c555edd7dc0ef75d0922dca9059c4f6292
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0e4b0fa411de344a0c2f38500255c68fc1cdfdf2 9f3ce17783b6e3a1cd94f44745ff9687663831d7 && git checkout -B drift-repro 0e4b0fa411de344a0c2f38500255c68fc1cdfdf2 && git merge --no-ff 9f3ce17783b6e3a1cd94f44745ff9687663831d7
node scripts/docs-audit/affected-docs.mjs --json 0e4b0fa411de344a0c2f38500255c68fc1cdfdf2 |
|
domain:server#2 · os-warren · session_01JQwxbXKRzgnVvjzxaYxLUf Contract-review-claim: session_01JQwxbXKRzgnVvjzxaYxLUf · head b73cfb2 · 2026-11-03T05:37Z Face: schema ( Generated by Claude Code |
|
domain:server#2 · os-warren · session_01JQwxbXKRzgnVvjzxaYxLUf Contract reviewServed-tier: Inputs read: card #29736 (body, 3 comments: claim 6594495344, triage 6595063911, os-dev-report 6596330782), PR #29804 body and both comments, the net diff ① Derived judgmentsAccept-set and public-surface changes the diff implies, each judged against the server code it must match:
② Semver level
Clause-②: yes (widening): three new exports and one new optional key, a pure widening of the published spec. ③ Boundary flags
④ Design judgment
No must-fix. S1 and S2 are should-fix: neither takes a declared capability away at the consumer nor makes the spec promise what the server refuses; both leave the spec's accept set either a subset of (S1) or, on one field, a syntactic superset of (S2) the route's, with the route closing the gap before any token is minted. Implemented-by: VERDICT: PASS Generated by Claude Code |
…irect_uri Contract-review should-fixes: the scheme prose now states the regex (two or more non-empty labels joined by single dots), and redirect_uri takes min(1) and is described as an absolute URI. Claude-Session: https://claude.ai/code/session_01JQwxbXghRkYmSR5dVh8PFG Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JQwxbXghRkYmSR5dVh8PFG Co-authored-by: Claude <noreply@anthropic.com>
…ter the main merge Claude-Session: https://claude.ai/code/session_01JQwxbXghRkYmSR5dVh8PFG Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#2 · os-warren · session_01JQwxbXKRzgnVvjzxaYxLUf Contract reviewServed-tier: Delta review on top of the first record, comment 6596452314 on this PR (head b73cfb2). Three commits since: ffba6e2 (the fix for that record's S1 and S2), 9682683 (merge of origin/main at 0e4b0fa into the branch), 9f3ce17 (authorable surface and reference pages regenerated after the merge). Inputs read: the three commits and the net diff ① Derived judgmentsNothing in the first record's ① moved except the two fields it faulted; the rest stands as written there (6596452314) and is not restated.
② Semver levelUnchanged from 6596452314: Clause-②: yes (widening), as before. ③ Boundary flags
④ Design judgment
No must-fix. No new should-fix on this PR's tree. The one open should-fix (S1, server side) lives on #29766. Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:server#2 · os-warren · session_01JQwxbXKRzgnVvjzxaYxLUf 维护者速读
席位核对ACCEPT ·
|
Requested by Warren · project thread
Fixes #29736
Clause-②: yes
Metadata-protocol: added
Skills-docs: none — no file under
skills/**or the hand-written docs teachestrustedOrigins, the auth endpoint paths or a mobile sign-in, so nothing taught becomes false and there is no page the new key belongs on yet; the generated reference pages (content/docs/references/api/auth-endpoints.mdx,system/auth-config.mdx) carry it.维护者速读
packages/spec声明移动端登录交接的契约:两条路由(/one-time-token/handoff与 better-auth 自带的/one-time-token/verify)及其请求/响应 schema,和新的可编写键AuthConfig.trustedAppSchemes(允许回跳的原生 App scheme 列表)。://),错写的条目会被拒而不是被修正。packages/spec/src/system/auth-config.zod.ts(TrustedAppSchemeSchema正则与trustedAppSchemes文档块)、packages/spec/src/api/auth-endpoints.zod.ts(交接 query 与 verify 绑定方式)、.changeset/29736-mobile-sign-in-handoff-spec.md。pnpm --filter @objectstack/spec exec vitest run src/system/auth-config.test.ts src/api/auth-endpoints.test.ts,预期全部通过,其中myapp、https、com.example.app://三种写法都被拒。What changes
The spec half of the mobile sign-in hand-off. The server half is the #29714 card (draft until this lands). The shapes below were agreed with that card's thread before writing. They were built against better-auth 1.7.12, the version
plugin-authpins, and measured from itsdist/plugins/one-time-token.AuthEndpointPaths.oneTimeTokenHandoff=/one-time-token/handoff(GET, ObjectStack's own route) withOneTimeTokenHandoffQuerySchema. It is a strict, closed set:redirect_uri;code_challenge, an RFC 7636 S256 challenge (43 base64url characters);code_challenge_method, optional, onlyS256.The docblock states the answers. On success, a
302toredirect_uriwith atokenquery parameter,no-store, valid for one minute. A400 VALIDATION_ERRORwith no redirect for an unknown or repeated parameter, an unregistered scheme or a bad challenge. A401without a session.AuthEndpointPaths.oneTimeTokenVerify=/one-time-token/verify(POST, better-auth's route name and body) withOneTimeTokenVerifyRequestSchemaandOneTimeTokenVerifyResponseSchema.tokenfield: the token, a dot, then the RFC 7636 verifier. The stock client keeps working, and a callback intercepted by another app cannot be redeemed without the verifier. The vendor token alphabet (a-z A-Z 0-9 - _, measured fromgenerateRandomString) never contains a dot, so the server splits on the first one.{ session, user }, reusingSessionSchema/SessionUserSchemafromapi/auth.zod.ts.GET /one-time-token/generateis mounted but refuses client requests (disableClientRequest), so it is deliberately not declared.AuthConfig.trustedAppSchemes: a new authorable key, an optional array ofTrustedAppSchemeSchema. The rule is strict and never normalised: a lowercase RFC 3986 scheme written bare, with at least one dot. RFC 8252 section 7.1 requires a private-use scheme to be a reverse domain name. That requirement also rules out, by construction, every web scheme (http,https,ws,wss,javascript,data,file,blob,about,vbscript), because none of them contains a dot. A bad entry is refused, never dropped. The docblock states that a hand-off to an unregistered scheme is refused with400and no redirect. The server half refuses the same entries at boot, sinceplugin-authreceives plain objects.No
/auth/configfeature flag. Nothing in the tree reads one.PUBLIC_AUTH_FEATURESprecedent (passkeys withdrawn) says not to advertise ahead of a consumer, sokernel/public-auth-features.tsis untouched.Regenerated:
api-surface/,export-origins/,declaration-map/,authorable-surface/,json-schema.manifest/, the strictness-ledger counts and two reference pages. Three isomorphic alias pins were added (ADR-0122), 819 → 822.Key-name precedent (the seat's call under the veto window):
trustedAppSchemessits besidetrustedOriginsonAuthConfigSchemaand follows itstrusted…+ plural-noun form. It is a separate key on purpose. better-auth trusts an origin intrustedOriginsfor every redirect it makes, while a scheme here is trusted only as the hand-off's destination. The query parameter names are the OAuth 2.0 / RFC 7636 wire names, kept verbatim.Why
Some identity providers (Google among them) refuse embedded WebViews, so a mobile user signing in with one cannot sign in to the app today. The maintainer ruled option B: reuse better-auth's one-time-token plugin. Triage's first touch on the server card asks for the public surface to be declared in
packages/specfirst.Tests
src/api/auth-endpoints.test.ts, a newmobile sign-in hand-off (one-time token)block:unrecognized_keys;plain, a short challenge, a padded challenge and a missing challenge are refused at their path;src/system/auth-config.test.ts: reverse-domain schemes are accepted and the key is absent by default. A table refuses eight shapes at['trustedAppSchemes', 1]:https,javascript,myapp,com.example.app://,com.example.app:,Com.Example.App,com..appandcom.example..src/type-alias-convention.pin.test.ts: three newIso_*pins and the count.Acceptance notes
Verification
All readings below were taken on head
b73cfb24d(objectstack-ai/objectstack), after the final commit.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 102 families from the change set (19 paths at derivation, under the 3,000-line threshold). All 102 exit 0.dispatch-gates --ranreconciles: 102 run, 0 NOT MEASURED.pnpm --filter @objectstack/spec test(803 files, 23811 tests passed, 1 todo),pnpm --filter @objectstack/spec typecheck(exit 0) andpnpm lint(exit 0).pnpm --filter @objectstack/spec check:generatedexit 0: every artifact is current.4b54dadewas red oncheck:spec-parsed-alias(the three new aliases neededIso_*pins) and oncheck:generated(declaration map). Both were fixed in this PR. Four gates answered exit 3 (prerequisite: unbuilt packages, or a pinned commit outside the shallow clone) and were then run on the final head after buildingformula,lint,objectql,clientandclient-reactand fetching the fixture commit. All four are green there.Regeneration commands
pnpm --filter @objectstack/spec check:generated --fixregeneratedapi-surface/,export-origins/,declaration-map/,content/docs/references/**and the strictness-ledger counts.gen:schema(viacheck:authorable-surface) wroteauthorable-surface/andjson-schema.manifest/.authorable-surface.base.jsonis untouched.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code