Repository navigation
fix(plugin-auth): the register form's Application Type reaches the OAuth provider as application_type - #24992
Conversation
…lication_type The sys-oauth-application/register wrapper sent the form's choice as `type`, which @better-auth/oauth-provider's create-client body does not declare, so every client was judged and stored as web. `web` and `native` now reach the provider as `application_type`; any other value keeps today's outcome (web) and logs one warning per registration. Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y Co-authored-by: Claude <noreply@anthropic.com>
…placeholder Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y Co-authored-by: Claude <noreply@anthropic.com>
…uth-application-type
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 17 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 6585d1652dc8b93f1b2680b86de9c8fb8fd8edb1 && git checkout 6585d1652dc8b93f1b2680b86de9c8fb8fd8edb1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e4d604efd39f6388dda50b86009d0006e50761d2 e9f4debc1bbcb891884d6f95b249c06d69ff1c27 && git checkout -B drift-repro e4d604efd39f6388dda50b86009d0006e50761d2 && git merge --no-ff e9f4debc1bbcb891884d6f95b249c06d69ff1c27
node scripts/docs-audit/affected-docs.mjs --json e4d604efd39f6388dda50b86009d0006e50761d2 |
席位核对ACCEPT · #24773 half 2a ·
|
Part of #24773: half 2a, the
plugin-authpart of the maintainer's ruling D (6293344532). Half 1 (a refusal answered 500) landed as #24853. Half 2b (the form's options and help text inplatform-objects) stays on the card.Clause-②: no — the declared type now takes effect under the OAuth provider's own redirect-URI rule, an authentication behaviour the maintainer ruled on; nothing published widens.
What changes
POST /api/v1/auth/sys-oauth-application/register(Setup → OAuth Applications → Register) forwarded the form's Application Type astype. The installed@better-auth/oauth-provider1.7.3 declaresapplication_type(web|native) on its/oauth2/create-clientbody and notype, so the member was stripped and every client was judged and stored asweb.The register wrapper now maps the value onto
application_type:webapplication_type: 'web')nativehttp://localhost:4001/callbackrefusedlocalhost,127.0.0.1or[::1](any port) accepted and stored asnative; other http refuseduser-agent-based,public, any other valueresolveThrownHttpError(err, 500)and the400 OAUTH_REGISTER_FAILEDrefusal, withdetails.upstreamError.ctx.logger.warn, meta{ requestedType, registeredAs: 'web' }). No response field was added. This door answers{ success, data: { client } }, which has no warnings member. Thewarningsmember on the data-layer write answers (CreateDataResponseSchema,packages/spec/src/api/protocol.zod.ts) is declared as advisory validation-rule hits, so it does not fit here. The warning does not reach the Console caller.auth-plugin.ts(the register wrapper only, +32 / -3), one new test file, one changeset. 382 changed lines in total.「甲」 reading
Standing rule 「甲」 (#6015
6292005854) applied as ruled in6293344532:webandnativeare forwarded, and Native gets the provider's RFC 8252 loopback rule. Other values get the loud half: one warning per registration.user-agent-based/public/ any other value narrows what the door accepts, and no ADR-0087 conversion carries it. Removing the two options from the form is half 2b, not this PR.public, measured first. 1.7.3 has a real counterpart. A public client istoken_endpoint_auth_method: 'none', andpersistOAuthClientRegistrationthen issues and stores no client secret. It is a separate axis fromapplication_type, which has nopublicvalue. Per the dispatch, it is not forwarded or mapped. The mapping question goes to the seat, with options, in the dev report on the card. Herepublicbehaves likeuser-agent-based: registered as a confidential web client with a secret, plus the warning.user-agent-based/public. The answer does not establish the exception.78ab6474cand on this head, a registration withuser-agent-based,publicorspastorestype = 'web',publicnull andtoken_endpoint_auth_method = 'client_secret_basic'. No column holds the form's value. 1.7.3 accepts onlyweb/nativeforapplication_typeon create and on update.@better-auth/oauth-provider1.7.0-rc.2 and 1.6.33 declaredtype(web|native|user-agent-based) but refused any non-webtype on a confidential client. The door never sendstoken_endpoint_auth_method, souser-agent-basedwas refused there, and the wrapper coercedpublictoweb.oidc-providerwrotetype: "web"on every registration.ac76425f0, this checkout's shallow root) was not read.user-agent-basedfor public clients.packages/qa, docs and the SDK (which never builds this URL).main1cc6861a3: zero hits for the door path,user-agent-based,application_type,oauth2/create-clientandapplications.register(control: case-insensitiveoauth, 18 files).origin/main12cde0492: zero hardcoded values (control:sys_oauth_application, 5 files).sys-oauth-application.object.ts:153-158offers both values to every admin. Whether an admin ever picked one cannot be measured, because there is no request log.Tests
New
packages/plugins/plugin-auth/src/oauth-register-application-type.test.ts, 15 cases. It runs real better-auth (OIDC provider on) behind the plugin's real route registration, on a real Hono app, over a real ObjectQL engine on SQLite. "Stored as" is read fromsys_oauth_application.typeat driver level.type: 'native'is stripped: the loopback URI is refused withinvalid_redirect_uri.application_type: 'native'accepts the same URI and storesnative.http://localhost:4001/callback: refused,400 OAUTH_REGISTER_FAILED,invalid_redirect_uri. Nothing stored, no warning.http://localhost:4001/callbackand +http://127.0.0.1:4001/callback:200, storednative, no warning.http://app.example.com/callback: refused,400, nothing stored. The provider's body schema (SafeUrlSchema, https except on loopback hosts) refuses it before its per-type rule.http://127.0.0.2:4001/callback: refused by the provider's exact-host native rule (invalid_redirect_uri).web, no warning.user-agent-based,public,spa+ https: storedweb, no column holds the value, no public client (no secret-less row), exactly one warning with meta{ requestedType, registeredAs: 'web' }.user-agent-based,public,spa+ loopback: refused as web, nothing stored, exactly one warning.Before the fix, on
78ab6474c:Tests 9 failed | 5 passed (14). The native-localhost and native-127.0.0.1 cases got the web refusal (expected 400 to be 200). The six unhonoured-type cases failed only on the warning count (expected [] to have a length of 1 but got +0), after their storage assertions had passed. The ninth failure was this file's own first assertion on the non-loopback native case. It expectedinvalid_redirect_uri, but the provider's body schema refuses that URI before its per-type rule, both before and after the fix. The assertion was corrected after this run.After, at head
e9f4debc1:oauth-register-refusal-status.test.ts:Test Files 2 passed (2),Tests 32 passed (32).pnpm --filter @objectstack/plugin-auth exec vitest run, atd674628b9:Test Files 174 passed (174),Tests 3334 passed | 10 skipped (3344). The plugin-auth tree is byte-identical ate9f4debc1; the merge ofmainbrought only the objectui pin.pnpm --filter @objectstack/plugin-auth typecheck: exit 0.check:test-typecheck: OK, 23 pinned signatures held, no new debt. The new file is in thetsconfig.test.jsonprogram (--listFilesOnly, 1 hit).Ablation (one-shot; no permanent file)
Each leg went through
scripts/ablation-replace.mjs: the anchor hit exactly once, and the mutation was verified on disk (anchor 1 → 0, blob changed). The runner had its own EXIT/INT/TERM trap. Each restore was verified as blob01004ce16== HEAD, withgit diff HEADempty. The test imports./auth-pluginfrom source, so nodistis involved.application_type: applicationType→type: applicationType(the oldtypeforwarding)Tests 2 failed | 13 passed: native + localhost / 127.0.0.1,expected 400 to be 200(the web refusal)applicationType = type→applicationType = 'native'(web judged as native)2 failed: web + localhostexpected 200 to be 400; web + https storednativenative6 failed: all six unhonoured-type caseshttp://app.→https://app.,127.0.0.2→127.0.0.1)2 failed: both native negative pins,expected 200 to be 400The first attempt at leg 3 used an anchor that the replacement still contained. The tool refused it (
anchor count moved 1 -> 1), restored, and ran nothing. It was redone with a non-overlapping anchor. The reading above is from the redone run.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat heade9f4debc1derived 72 commands. That list is identical to the seat's derivation at78ab6474c. Each command was run withcmd > log 2>&1; rc=$?.check:adr-0087-registration --base origin/main,check:changeset-no-major,check:empty-changeset,check:closing-keyword-parity,check:error-status-conformance,check:route-envelope,check:auth-mount-ledger,check:durability-log-level,check:issue-citations,check:nul-bytes,check:test-source-alias,check:engine-double-contractandcheck:type-check-debt.check:dual-build-cjs-loads. It first exited 3 (PREREQUISITE NOT MET, no whole-repodist). It passed afterturbo run build --filter=!@objectstack/docs(76 tasks, 75 from cache): 112 require entry points across 69 packages load.--ranprintedRun reconciliation — 72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN.test:repo: CITest Core.check:dev-prereqsandcheck:skill-frame-freshness: nopackages/specbuild input or frame file is touched.dispatch-gateslists outside the 72: CI.Acceptance notes
user-agent-basedhas a partial counterpart too. In the provider's rc.2 and 1.6.x lines,user-agent-basedwas valid only for a public client (token_endpoint_auth_method: 'none'). In 1.7.x a browser public client isapplication_type: 'web'plustoken_endpoint_auth_method: 'none'. So thepublicmapping question covers both form values. MCP clients already register as public native clients through DCR (auth-manager.mcp-oauth-resource.test.ts:235).oauth.applications.register(packages/client/src/index.ts:4535) does not declareapplication_typeon its request type. It does declaretoken_endpoint_auth_method: 'none', and its response typeOAuthApplicationdeclaresapplication_type. The body passes through unchanged, so a typed caller can register a native client only through a cast. Noted, not filed. Carrier: none (承接者:无).sys_oauth_application.type(sys-oauth-application.object.ts:425) also listsuser-agent-basedandpublic, which 1.7.3 never writes.publicboolean column (:432) is never written by 1.7.3: its record has no such field, and it was null in every stored row here.Generated by Claude Code