Skip to content

fix(platform-objects): the OAuth register form offers only Web and Native, and its Redirect URLs help states the rule for each type - #25011

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-24773-oauth-form-types
Oct 21, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-24773-oauth-form-types

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #24773: half 2b, the Console form's part of the maintainer's ruling D (6293344532). Half 1 landed as #24853. Half 2a (the plugin-auth register wrapper forwards the type as application_type) landed as #24992 (fbb42d900), which is merged into this branch. The 19.0 refusal of user-agent-based / public stays on the card.

Clause-②: no (a form's offering: the register route still accepts every type value it accepted, and nothing on the server reads the action's param options, so no accept set or served shape moves)

Cross-lane: packages/platform-objects is domain:engine. This PR comes from the domain:services seat 2 under the claim's widened half-2b block (6302570331).

The help describes the door as #24992 left it. The new Redirect URLs help says a Native application may use http://localhost. Before #24992 that was false: the route sent type, which the provider drops, so a Native client was judged as Web. Measured both ways on a live showcase (pnpm dev -- --fresh, OS_OIDC_PROVIDER_ENABLED=1, seeded admin), through POST /api/v1/auth/sys-oauth-application/register:

Registration At 2dc07452a (without #24992) At fd686be8e (with #24992)
Native + http://localhost:4001/callback 400 OAUTH_REGISTER_FAILED, "web clients require https redirect URIs on non-loopback hosts" 200, stored native
Native + http://127.0.0.1:4001/callback not run 200, stored native
Native + http://[::1]:4001/callback not run 200, stored native
Web + http://localhost:4001/callback 400, same refusal 400, same refusal
Web + https://app.example.com/callback 200, stored web 200, stored web
user-agent-based + https 200, stored web 200, stored web, one server warning
public + https 200, stored web 200, stored web, one server warning

What changes

sys_oauth_application.create_oauth_application (Setup › OAuth Applications › Register), in sys-oauth-application.object.ts:

Before After
Application Type options Web, Native, User-agent based, Public Web, Native (default Web, still required)
Redirect URLs help "One URL per line. Must use https:// in production." "One URL per line. Web applications need https:// on a host other than localhost or another loopback address. Native applications may also use http:// on exactly localhost, 127.0.0.1 or [::1], on any port."
Application Type help none "Choose Native for a desktop or mobile app, or for an app in local development that redirects to http://localhost."
  • The four objects.generated.ts bundles were regenerated with pnpm i18n:extract. The zh-CN, ja-JP and es-ES values of the two help texts were translated by hand, then extract was run again: the source-hash companions end unchanged, because the new leaves are translations, not copies.
  • Not changed: the stored type select (:449 at this head, :425 at 4b8f597ba), the public boolean column, the register route, and every other line of the file.

Stop condition: does anything on the server refuse a value the form no longer offers? No.

Measured on the same live showcase at 2dc07452a, and the register-route rows again at fd686be8e (table above):

  • POST /api/v1/auth/sys-oauth-application/register with type: 'user-agent-based' and with type: 'public' (https redirect): both 200, success: true.
  • The generic action door, POST /api/v1/actions/sys_oauth_application/create_oauth_application with type: 'user-agent-based': 400 VALIDATION_ERROR, "is type: 'api' — it dispatches on target, not through the action registry". The same answer for type: 'web' (control). The door refuses an api action by its type before its param contract runs (headlessActionTypeError in packages/runtime/src/domains/actions.ts, ahead of enforceActionParams). The MCP run_action door refuses it the same way (isHeadlessInvokableAction, action-execution.ts).
  • Source reading: the register route reads body.type from the raw request and never reads the object's metadata. The middleware seam hands middleware no body. Nothing else matches an action's target.
  • So only the form changes. GET /api/v1/meta/object/sys_oauth_application now serves the type param with options ["web", "native"] and the new help texts. With ?locale=zh-CN it serves the zh-CN texts and options Web 应用 / 原生应用.

The rule the help states, measured on the provider

@better-auth/oauth-provider 1.7.3, validateClientRedirectUri (dist/authorize-9whjxVLJ.mjs:1685). #24992's pins cover Web + http://localhost refused, Web + https accepted, Native + http://localhost:4001 and http://127.0.0.1:4001 accepted, and Native + http://127.0.0.2:4001 and http://app.example.com refused. The rest of what the help says was measured live through the provider's own POST /api/v1/auth/oauth2/create-client, at 2dc07452a:

application_type redirect URI Answer
native http://[::1]:4001/callback 201, stored native
native http://localhost:59999/callback 201 (any port)
native http://127.0.0.1/callback (no port) 201
native https://app.example.com/callback 201 (Native also takes what Web takes)
native https://localhost:4001/callback 400 invalid_redirect_uri, "native clients must not use https loopback redirect URIs"
native http://127.0.0.2:4001/callback 400, "may use http only on the exact loopback hosts localhost, 127.0.0.1, or [::1]"
web https://localhost:4001/callback 400, "web clients require https redirect URIs on non-loopback hosts"
web https://127.0.0.1:4001/callback 400, same
web https://app.example.com/callback 201

The help does not mention Native private-use URI schemes, which the rule also allows. Nothing here measured them through the route.

The stored columns, measured (for the 19.0 card)

  • What 1.7.3 writes to type. Only web or native. checkOAuthClient refuses any other application_type (:1770). One more case: a client-metadata-document registration that names no application_type stores null (:1956). Live, at 2dc07452a: the registrations above named user-agent-based and public were stored type: 'web', token_endpoint_auth_method: 'client_secret_basic' (8 rows read through GET /api/v1/data/sys_oauth_application). Again at fd686be8e: 6 rows, the same columns.
  • What 1.7.3 writes to public. Never. The client record has no such field (OAUTH_CLIENT_RECORD_FIELD_NAMES, :1034). No row carries a public value (8 at 2dc07452a, 6 at fd686be8e). A public client is token_endpoint_auth_method: 'none'.
  • What the stored type select actually declares. The values are web, native, useragentbased, public. The column uses Field.select's string-array shorthand, which snake-cases each value and strips the hyphens. So the third declared value has never been user-agent-based: not the value the form's action param sent, and not the one the provider's earlier lines accepted (from the SDK and dynamic registration, for public clients). A row holding user-agent-based is already outside the declared set today. Keeping the column four-valued in 18.x protects public, not user-agent-based. The pin below holds the column exactly as declared. Fixing it, or narrowing it, is the 19.0 card's decision.

Tests

New packages/platform-objects/src/identity/oauth-register-form-types.test.ts, 7 cases. It reads the object and the four bundles from source.

  • The type param is a required select, defaults to web, and offers exactly web and native. Every shipped locale's options are exactly web and native.
  • The Redirect URLs help names https://, http://, localhost, 127.0.0.1 and [::1]. The Application Type help names Native and http://localhost.
  • The en bundle mirrors the source. Every locale's help names the same scheme and hosts, and each translated locale differs from the English source.
  • The stored type select still declares its four options (labels web, native, user-agent-based, public; values as measured above). The public column is still a declared boolean.

Results at 2dc07452a. packages/platform-objects and this PR's changeset are byte-identical at fd686be8e: the second merge brought only #24992's three plugin-auth / changeset files.

  • pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2: Test Files 83 passed (83), Tests 1239 passed (1239).
  • pnpm --filter @objectstack/platform-objects typecheck: exit 0. check:test-typecheck: OK, 1 file / 3 errors / 2 pinned signatures held. No debt added. The new file is in the tsconfig.test.json program (--listFilesOnly, 1 hit).

Ablation (one-shot; no permanent file)

Each leg went through scripts/ablation-replace.mjs, in a runner with its own EXIT/INT/TERM trap. Each anchor hit once and fell 1 → 0, with the blob changed on disk. Each restore was verified as blob == HEAD with git diff HEAD empty (object 240ded11b, zh-CN bundle 5326f33a2). The test reads source, so no dist is involved.

Leg Mutation Result
A { label: 'User-agent based', value: 'user-agent-based' } put back on the action's type options 1 failed: expected [ 'web', 'native', 'user-agent-based' ] to deeply equal [ 'web', 'native' ]
B Redirect URLs help back to "Must use https:// in production." 2 failed: the source help no longer names http://; the en bundle no longer mirrors the source
C stored type select narrowed to ['web', 'native'] 1 failed: the stored-column pin
D zh-CN help back to its old translation 1 failed: zh-CN redirectURLs help ... to contain 'http://'
E public: "公共客户端" put back in zh-CN's action options 1 failed: zh-CN type options: expected [ 'web', 'native', 'public' ]
F Application Type help deleted 2 failed: the source help pin and the en-mirror pin
G public column renamed is_public 1 failed: expected undefined to be 'boolean'
control unmutated 7 passed

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at the final head fd686be8e derived 66 commands for 7 paths (208 changed lines, +185 / -23, under the 3000 human-merge threshold). Each ran as cmd > log 2>&1; rc=$?, after a workspace build (turbo run build --filter=!@objectstack/docs).

  • Result. 66/66 exit 0, among them check:adr-0087-registration --base origin/main ("this PR adds no declared-breaking changeset"), check:changeset-no-major, check:empty-changeset, check:i18n ("all bundles in sync, no undeclared authoring keys"), check:i18n-stale-fill ("0 stale-fill leaf/leaves"), check:nul-bytes, check:doc-authoring, check:closing-keyword-parity, check:issue-citations, check:type-check-debt and check:dual-build-cjs-loads.
  • Reconciliation. --ran with every exit code recorded: Run reconciliation — 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN.
  • The same 66 also ran at 2dc07452a, before fix(plugin-auth): the register form's Application Type reaches the OAuth provider as application_type #24992 was merged in: 66/66 exit 0, reconciled the same way.
  • Not run locally. test:repo is CI's Test Core. check:skill-frame-freshness does not apply: no frame file is touched. check:dev-prereqs passed as the first step of each pnpm dev boot. No exported symbol was renamed or removed, so check-platform-checklist --changed-from does not apply. The workspace type-check lanes and the CI jobs that dispatch-gates lists outside the 66 are CI's.

Acceptance notes

  • For the 19.0 card. The stored-column readings above: 1.7.3 writes web / native (or null for a metadata-document client) to type and never writes public. The declared third value is useragentbased, not user-agent-based.
  • Field.select's string shorthand strips hyphens from values (spaces become underscores; a hyphen is dropped). Measured from built dist: sys_oauth_application.type declares useragentbased. sys_metadata_audit.lock_state (packages/metadata-core/src/objects/sys-metadata-audit.object.ts:190) declares nooverlay / nodelete, while its writer (recordMetadataAudit, packages/metadata-protocol/src/protocol.ts) writes no-overlay / no-delete. That column is readonly: true, so the record validator judges only its shape and the audit writes are not refused. Source reading only; no wrong answer was measured through a door. Reported to the seat in the dev report, not filed here.

Generated by Claude Code

…tive, and its redirect-URL help states the provider's rule per type

Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y
Co-authored-by: Claude <noreply@anthropic.com>
…, the help's named hosts and the four-value stored column; add the changeset

Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y
Co-authored-by: Claude <noreply@anthropic.com>
…the register form's new help texts

Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y
Co-authored-by: Claude <noreply@anthropic.com>
…today, including its snake-cased third value

Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/platform-objects, touching 2 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/permissions/authorization.mdx (via sys_oauth_application (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/protocol/objectui/actions.mdx (via sys_oauth_application (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/ui/setup-app.mdx (via sys_oauth_application (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 93e6c1a5f8bf256310f9ed477523ff2c90bf1bff → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 3f42eb2396af9b026cb045e656dedb320ef8e13a — the merge of head fd686be8e05fc588d8ba2ee6538ec6a2a46e5b70 into base 93e6c1a5f8bf256310f9ed477523ff2c90bf1bff, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3f42eb2396af9b026cb045e656dedb320ef8e13a && git checkout 3f42eb2396af9b026cb045e656dedb320ef8e13a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 93e6c1a5f8bf256310f9ed477523ff2c90bf1bff fd686be8e05fc588d8ba2ee6538ec6a2a46e5b70 && git checkout -B drift-repro 93e6c1a5f8bf256310f9ed477523ff2c90bf1bff && git merge --no-ff fd686be8e05fc588d8ba2ee6538ec6a2a46e5b70

node scripts/docs-audit/affected-docs.mjs --json 93e6c1a5f8bf256310f9ed477523ff2c90bf1bff

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 93e6c1a5f8bf256310f9ed477523ff2c90bf1bff → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · #24773 half 2b · domain:services seat 2 · os-support-ai · session_01UmqzD97e14iws6hmVhgZ5y · 2026-10-21T00:46Z · head fd686be8e0.

  • This is the review of record for the claim 6302570331, as widened in place for half 2b.
  • Contract review: not owed. The PR touches no published schema and no governed path, and it declares Clause-②: no with no narrowing arm.
  • The register door's accept set is unchanged: the dev measured it live.
  1. Form:
    • The first line is Part of #24773. The seat's grep finds no closing keyword beside an issue number.
    • Clause-②: no is at line start in the body and in the platform-objects changeset (patch), with its reason.
    • 7 files, +185/−23.
    • Path face: check-governed-merges --pr 25011 reports 0 of 7 paths governed. Ordinary queue landing.
    • Cross-lane: platform-objects (domain:engine), declared in the widened claim.
  2. Premise and fix:
    • Ruling D (6293344532): the form offers only Web and Native (required, default Web). The Redirect URLs help states the provider's rule per type: Web needs https on a non-loopback host; Native may also use http on exactly localhost, 127.0.0.1 or [::1], any port. A new Application Type help says when to choose Native.
    • The stop condition, measured negative, live on a dev server. Nothing on the server reads the action's param options:
    • The stored type select and the public column are untouched.
    • Pins: 7 source-reading cases, among them the four locales' options and help, the stored column still declared four-valued, and the public column still a boolean.
    • Ablations A–G each went red as predicted and were restored to the HEAD blob.
  3. Gates:
    • CI on fd686be8e0, latest run per check name: 41 checks, 34 success, 7 skipped, 0 failed.
    • Every skip is on the roster in scripts/pm/check-expected-skips.mjs (read from the file): Packed-tarball smoke, the 18.x upgrade harness, the Dogfood Regression Gate matrix, Temporal Conformance, Dogfood Verify CLI, Console Pin Gate and Build Docs.
    • Lint & Repo Gates and TypeScript Type Check are both success.
    • Local: 66 derived, 66 run, 0 NOT-MEASURED. The platform-objects suite: 83 files, 1239 passed. Typecheck with check:test-typecheck exits 0. check:i18n and check:i18n-stale-fill are clean.
  4. Accepted deviations:
    • The action is create_oauth_application (label "Register OAuth Application"), not the name the dispatch used.
    • The help also states [::1] and any port, each measured live through the provider's own route.
    • The stored-column pin holds the declared values as they are (see 5).
    • One stale sentence, carried forward rather than reworked: the object's new code comment says refusing user-agent-based / public "waits for a major version". The maintainer's amendment of 「甲」 (6303865025), which came during this run, overtook that. Half 2c, dispatched next, lands the 18.x refusal and corrects that comment in the same region.
    • Prose read sentence by sentence against the diff: the changeset's Redirect URLs help, Application Type help and Unchanged bullets.
  5. Out of scope:
  6. Landing: ordinary queue landing. Every check on this head is green or an expected skip, so the seat flips it on this record. Half 2c (the 400, now 18.x) is dispatched when this lands.

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants