Skip to content

fix(plugin-auth)!: the OAuth register door refuses an application type the provider does not judge with 400 INVALID_REQUEST, where it registered it as web - #25038

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-24773-oauth-type-refusal
Oct 21, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-24773-oauth-type-refusal

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #24773

Clause-②: no (narrowing) — POST /api/v1/auth/sys-oauth-application/register accepted any type value and registered every one other than web / native as a web client with a secret (with one server warning since half 2a); it now refuses each of those values with 400 INVALID_REQUEST, so it accepts exactly web, native and no type.

Half 2c, the last half of the maintainer's ruling D (6293344532) on this card, under the claim 6302570331 as widened for half 2c. Half 1 landed as #24853, half 2a (the door forwards application_type and warns) as #24992, half 2b (the form offers Web / Native) as #25011. This PR turns half 2a's warning branch into the refusal, corrects the register action's code comment in platform-objects, and carries one BREAKING minor changeset. It is a narrowing, so it owes a review at CONTRACT_REVIEW_TIER before the queue.

「甲」 reading (amended)

What changes

packages/plugins/plugin-auth/src/auth-plugin.ts, the register wrapper's application-type branch only:

type in the body Before (73432299c) After
web, native forwarded as application_type unchanged
absent, null web, no warning unchanged
user-agent-based, public, any other string, '', a non-string registered and judged as web, with a client secret, plus one warn 400 { success: false, error: { code: 'INVALID_REQUEST', message } }, the route's existing envelope (its redirectURLs refusal uses the same one). The refusal comes before createOAuthClient is called, so nothing is stored. No warning, no new error code.

The message names the value (its first 64 characters, then an ellipsis when cut, as the warning truncated it; a non-string is named by its type, such as (number)) and the two accepted types, and says to choose native for an http://localhost, http://127.0.0.1 or http://[::1] redirect URI:

type "user-agent-based" is not an application type the OAuth provider judges: send 'web' or 'native', or no type for 'web'. Choose 'native' for an http://localhost, http://127.0.0.1 or http://[::1] redirect URI; 'web' needs https on a host that is not a loopback address.

packages/platform-objects/src/identity/sys-oauth-application.object.ts: the create_oauth_application action's code comment only. It said the refusal "waits for a major version"; it now states the 18.x refusal and the stored column's readings (M2). No option, param or field changes. The comment keeps its line count. The generated bundles do not carry comments, so the i18n tooling regenerates nothing (check:i18n green). esbuild keeps comments, though: a rebuild changed dist/index.{js,mjs} and dist/identity/index.{js,mjs} (the comment text only; the .map and .d.ts files are byte-identical). platform-objects is not private, so its published output changes, and the changeset names it with patch. That also puts this entry in platform-objects' CHANGELOG, beside half 2b's entry.

Absent, null and the empty string

Measured at 73432299c: absent and null registered web with no warning; '' registered web with the warning, like any other unhonoured value. This PR refuses '' and keeps absent and null:

  • Absent and null stay web. Neither names a type: half 2a's branch read null as absent, and JSON null is the body's spelling of no value. The provider's own default is web, and the form's declared default is web. Refusing null would narrow the door past the ruling, which refuses present values.
  • '' is refused. It is a present string that names no type, and it already took the warning branch, whose text said "A future major version refuses this value". So the set this PR refuses is exactly the set that warned. No caller sends it: the Console dialog will not submit an empty required param (M3).

Measured first

M1: the register door, value by value

Real stack in-process: real better-auth 1.7.3 with the OIDC provider on, behind the plugin's real route registration on a Hono app, over a real ObjectQL engine with SqlDriver (better-sqlite3 :memory:) and the auth manifest's objects. Stored rows were read at driver level. Each value was sent with an https redirect (https://app.example.com/callback) and with a loopback one (http://localhost:4001/callback). The probe was a throwaway file and is not in this PR.

type Before, https Before, loopback After, https After, loopback
web 200, stored web 400 OAUTH_REGISTER_FAILED (invalid_redirect_uri) same same
native 200, stored native 200, stored native same same
user-agent-based 200, stored web, 1 warning 400 OAUTH_REGISTER_FAILED, 1 warning 400 INVALID_REQUEST, nothing stored, no warning 400 INVALID_REQUEST
public 200, stored web, 1 warning 400 OAUTH_REGISTER_FAILED, 1 warning 400 INVALID_REQUEST 400 INVALID_REQUEST
spa (unknown string) 200, stored web, 1 warning 400, 1 warning 400 INVALID_REQUEST 400 INVALID_REQUEST
42, true, { a: 1 } 200, stored web, 1 warning each ((number) / (boolean) / (object)) 400, 1 warning 400 INVALID_REQUEST 400 INVALID_REQUEST
absent 200, stored web, no warning 400 OAUTH_REGISTER_FAILED same same
null 200, stored web, no warning 400 OAUTH_REGISTER_FAILED same same
'' 200, stored web, 1 warning 400, 1 warning 400 INVALID_REQUEST 400 INVALID_REQUEST
200 characters 200, stored web, 1 warning (value cut to 64) 400, 1 warning 400 INVALID_REQUEST, message carries 64 characters 400 INVALID_REQUEST

Every stored row, before and after, had public null and token_endpoint_auth_method client_secret_basic, with a client secret.

M2: the stored sys_oauth_application.type column

Not narrowed: the select and the public column are unchanged.

  • What the installed provider writes. @better-auth/oauth-provider 1.7.3 writes only web or native, or null for a client-metadata-document client that names none (dist/authorize-9whjxVLJ.mjs:1956). Every creation path goes through persistOAuthClientRegistration and checkOAuthClient, which refuses any other application_type (:1770): dynamic registration /oauth2/register (the MCP clients' route), /oauth2/create-client (this door and the SDK), /admin/oauth2/create-client, and the client-metadata document. Both update paths (/oauth2/update-client, /admin/oauth2/update-client) declare application_type as web | native (:2885, :2923). better-auth 1.7.3 itself ships no oidc-provider or mcp plugin. The generic data API cannot write the table (apiMethods: ['get', 'list']), and no in-tree code writes the column except through the provider (the applicationType: 'type' mapping in auth-schema-config.ts).
  • What this door wrote before fix(plugin-auth): the register form's Application Type reaches the OAuth provider as application_type #24992. It sent type: safeType, with safeType one of web / native / user-agent-based or else web (fbb42d900^:auth-plugin.ts:3640). 1.7.x strips type, so every row was web. Under the earlier lines below, the door sent no token_endpoint_auth_method (a confidential client), so user-agent-based was refused there and public became web.
  • What earlier provider lines could write, through other doors. @better-auth/oauth-provider 1.6.33 and 1.7.0-rc.2 (npm tarballs, read with grep only) declared type as web | native | user-agent-based, required native or user-agent-based for a public client and web for a confidential one, and wrote public: isPublic. plugin-auth's package.json history (245 commits, read through the commits API): the provider arrived as ^1.6.9 on 2026-05-08, ran ^1.6.11–^1.6.23 to 2026-07-16, 1.7.0-rc.1 from 2026-07-22 and 1.7.0-rc.2 from 2026-08-19, then ^1.7.1 from 2026-08-27. So a deployment that ran plugin-auth between 2026-05-08 and 2026-08-27 can hold rows with type = 'user-agent-based' (a public client registered by name through dynamic registration or create-client) and public set.
  • type = 'public'. No writer found. No oauth-provider line declares it. better-auth 1.6.x's own mcp plugin wrote type: 'public', but plugin-auth never imported it: auth-manager.ts history from 2026-02-01 to 2026-05-09, 31 commits read, 0 hits for plugins/mcp or mcp( (control: oidc-provider / oidcProvider(, 8 hits). The deprecated core oidc-provider wrote web on registration.
  • What the select declares. web, native, useragentbased, public (half 2b's reading: the string-array shorthand strips hyphens). So the one non-web/native value a writer could ever produce, user-agent-based, is not a declared value, and the declared useragentbased and public have no writer in the tree, in the installed provider or in the lines above.
  • Readers. No in-tree reader branches on the column's values (git grep: only the select declaration, the form and this wrapper). The installed 1.7.3 has no user-agent-based token (grep -c 0) and reads no public field; a public client is token_endpoint_auth_method: 'none'. The earlier lines did branch, requiring PKCE for native, user-agent-based or public === true (1.6.33 utils-Cdzs0w5-.mjs:420). One branch remains on legacy data, from source reading only: 1.7.3's two update paths re-run checkOAuthClient over the stored client merged with the update, so a legacy row holding user-agent-based would answer application_type must be web or native to an update that does not also set application_type. That is not measured on a row (Acceptance notes).

M3: who sends a type other than web / native to this door

None found, so this PR breaks no in-repo sender.

  • This repo, at 73432299c. Every caller of the door sends no type or web: in-process-session-renewal.pin.test.ts, request-json-body-refusal.test.ts, oauth-register-refusal-status.test.ts (web) and this PR's test file. Docs: content/docs/protocol/objectui/actions.mdx shows the action with no type param; the platform checklist (identity-auth.json) registers with no type. examples/ and packages/qa: 0 hits for the door. The SDK never builds this URL (disposition: 'server-only' in the auth route ledger).
  • The Console form. Since fix(platform-objects): the OAuth register form offers only Web and Native, and its Redirect URLs help states the rule for each type #25011 it offers only web and native, required, default web. objectui's ActionParamDialog pre-fills defaultValue and refuses to submit a required param that is undefined, null, empty or whitespace (packages/app-shell/src/views/ActionParamDialog.tsx:503, :515, :537, the same at the pinned .objectui-sha cf153eb2e and at objectui main a91f6bbb1). So the form sends web or native only.
  • objectui a91f6bbb1. 0 files for the door path, user-agent-based or application_type (control: sys_oauth_application, 5 files).
  • HotCRM d723f6f. 0 files for the door path, user-agent-based, application_type, oauth2/create-client or applications.register (control: oauth, case-insensitive, 18 files).

Tests

packages/plugins/plugin-auth/src/oauth-register-application-type.test.ts (half 2a's file, the same real stack as M1). The provider's createOAuthClient is wrapped in a pass-through spy, and each request clears it.

  • Half 2a's warning describe is replaced by the refusal pins. user-agent-based, public, spa, '', 42, true and an object, each with an https and with a loopback redirect: 400, ADR-0112 envelope clean, error.code INVALID_REQUEST, the provider never called, no row written, no warning and no error logged.
  • The message pin: a 64-character value plus a tail is echoed up to the 64 characters and never the tail; the message names 'web', 'native', http://localhost, http://127.0.0.1 and http://[::1]; a short value is named whole; a non-string is named (number).
  • Added: type: null registers web with no warning. Every accepted registration now also asserts exactly one provider call, carrying the application_type it stored.
  • Half 2a's pins (provider preconditions, web / native forwarding, the loopback rules, absent → web) pass unchanged.

Results:

  • At ee660f7cd (the fix, before merging main): this file plus oauth-register-refusal-status.test.ts: Test Files 2 passed (2), Tests 42 passed (42). The full plugin-auth suite: Test Files 174 passed (174), Tests 3344 passed | 10 skipped (3354). platform-objects: Test Files 83 passed (83), Tests 1239 passed (1239), half 2b's oauth-register-form-types.test.ts included. platform-objects typecheck: exit 0, check:test-typecheck: OK, 2 pinned signatures held.
  • At 3f934a772 (after merging main 4d991588f, which touched plugin-auth's auth-manager.ts): a rebuilt closure, then the full plugin-auth suite: Test Files 175 passed (175), Tests 3348 passed | 10 skipped (3358). plugin-auth typecheck: exit 0, check:test-typecheck: OK, 23 pinned signatures held. No debt was added, and the test file is in the tsconfig.test.json program (--listFilesOnly, 1 hit). platform-objects is untouched by the merge.

Ablation (one-shot; no permanent file)

Every leg went through scripts/ablation-replace.mjs in WRAP mode, in a runner with its own EXIT/INT/TERM trap that re-proved each restore. Each anchor hit once and fell from 1 to 0, with the blob changed on disk. Each restore was proven as blob d065fc855 == HEAD, with git diff HEAD empty and git status --porcelain empty at the end. The test imports ./auth-plugin from source, so there is no dist leg. The unmutated control: 25 passed.

Leg Mutation Result
A half 2a's warning branch put back: warn, then register as web 15 failed, 10 passed: every refusal case (expected 200 to be 400; on loopback, expected 'OAUTH_REGISTER_FAILED' to be 'INVALID_REQUEST') and the message pin
G silently mapped to native instead of refused 15 failed, the same cases
B the refusal kept, plus a warning 14 failed: the door still warns instead of (or as well as) refusing: expected [ [ …(2) ] ] to deeply equal []
C the provider called before the refusal 14 failed: the door called the provider for a type it refuses: expected [ { client_name: 'Typed', …(2) } ] to deeply equal []
D the value echoed untruncated 1 failed: the message echoes the value past its first 64 characters: expected 'type "aaaa…' not to contain 'TAIL'
E null refused 1 failed: the null → web pin, expected 400 to be 200
F absent refused too 2 failed: the absent and null pins

The first runs of legs B and C were refused by the tool and ran nothing ("the anchor count moved 1 -> 1": the replacement contained its anchor). They were redone with an anchor the replacement does not contain. The figures above are from the redone runs.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at the final head 3f934a772 derived 74 commands (the seat's 57 at 73432299c, plus 17 that this diff's paths and its changeset add). The diff is 190 changed lines (+138 / -52) across 4 files, under the 3000 human-merge threshold. Each command ran as cmd > log 2>&1; status=$?, recorded byte for byte with :: exit N.

  • First pass. 72 of 74 exited 0. check:dual-build-cjs-loads and check:i18n exited 3 (PREREQUISITE NOT MET: no whole-workspace dist, and no built CLI), so they measured nothing. After turbo run build --filter=!@objectstack/docs --concurrency=2 (76 tasks), both exited 0: "112 published require entry point(s) across 69 package(s)" and check-i18n-bundles: OK (10 package(s) — all bundles in sync, no undeclared authoring keys).

  • Reconciliation. --ran: Run reconciliation — 74 derived, 74 run, 0 NOT-MEASURED, 0 UNRUN.

  • Verdicts among the 74. check-adr-0087-registration --base origin/main: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition, read as [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription). Also green: check-changeset-no-major, and check-empty-changeset ("No changeset from the merge base modified or deleted"). check:error-status-conformance: "every derivable runtime status is documented, and every documented status is reachable". check:issue-citations, check:doc-authoring, check:nul-bytes, check:route-envelope, check:auth-mount-ledger, check:closing-keyword-parity, check:type-check-debt and check:engine-double-contract are green too.

  • Not run locally. test:repo is CI's Test Core. check:dev-prereqs and check:skill-frame-freshness do not apply: no packages/spec build input or frame file is touched. No exported symbol is renamed or removed, so check-platform-checklist --changed-from does not apply. The type-check lanes and the CI jobs that dispatch-gates lists outside the 74 belong to CI.

  • Dogfood. This PR has a (narrowing) arm, so the full dogfood battery runs once on this head after the PR opens. Its reading is in the dev report on OAuth app registration rejects http://localhost and http://127.0.0.1 redirect URIs as "non-loopback", with HTTP 500 #24773.

  • After the patch rounds: check-empty-changeset is red by design at the final head 48857fcc6 (see ## Pending release notes corrected below). The re-verification at 2da95c6da, after merging main dfe60e3a0:

    • the full plugin-auth suite: Test Files 175 passed (175), Tests 3348 passed | 10 skipped (3358);
    • plugin-auth typecheck: check:test-typecheck: OK, 23 pinned signatures held;
    • --ran: 74 derived, 74 run, 0 NOT-MEASURED, 0 UNRUN. The 21 families that the edited changesets trigger were re-run at 2da95c6da: 20 exit 0, and check-empty-changeset exits 1 as above. The other 53 are carried from 3f934a772.
  • r3, 8eba09f8c (prose only, this PR's own changeset). .changeset/24773-oauth-type-refusal.md now describes the change from the last published state, 18.0.0, where the door registered such a type as web silently. The FROM bullet drops ", and the server logged one warning naming the value", and the TO bullet drops ": the refusal replaces that warning": half 2a's warning never reached a published release. dispatch-gates --commands over that one path derived the same 21 families. All 21 re-ran at 8eba09f8c: 20 exit 0 (check-adr-0087-registration --base origin/main: "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition"), and check-empty-changeset --base origin/main exits 1, the expected red naming only the two corrected foreign changesets. --ran: 74 derived, 74 run, 0 NOT-MEASURED, 0 UNRUN, with the other 53 carried from 3f934a772.

  • r4, 48857fcc6 (prose only, this PR's own changeset). .changeset/24773-oauth-type-refusal.md was re-read against the last published state, 18.0.0. In 18.0.0 the door sent the type as type, which the provider stripped, so every client was web. Provider refusals answered 500, and the form offered four types. Three phrases stated this release's intermediate behaviour as the FROM state, and were changed:

    • FROM. "answered the web rule's 400 OAUTH_REGISTER_FAILED for a loopback redirect URI" became "was refused by the web rule (OAUTH_REGISTER_FAILED) for a loopback redirect URI". In 18.0.0 that refusal answered 500.
    • Unchanged. "Unchanged: web and native are forwarded as the provider's application_type, and the provider's own redirect-URI rule decides. ... The other refusals keep their codes and statuses." was split into two bullets. The first is "Accepted: web and native are forwarded as the provider's application_type (this release's Application Type entry), and the provider's own redirect-URI rule decides." The second is "Unchanged: ... The other refusals keep their codes (their statuses are this release's refusal-status entry)." The reason: in 18.0.0, native was judged and stored as web, and the statuses change in this release's refusal-status entry.
    • The form. "The form already offers only Web and Native." became "The form offers only Web and Native (this release's register-form entry)." In 18.0.0 the form offered four types.
    • Gates: dispatch-gates --commands over that path derived the same 21 families, re-run at 48857fcc6. 20 exit 0 (check-adr-0087-registration --base origin/main: "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition"); check-empty-changeset --base origin/main exits 1, the expected red naming only the two corrected foreign changesets. --ran: 74 derived, 74 run, 0 NOT-MEASURED, 0 UNRUN, with the other 53 carried from 3f934a772.
  • The dogfood battery is not re-run. The merge moved driver-sql, objectql, service-settings, service-messaging, cli and core, none of it under this diff, and this round's delta is prose only.

Pending release notes corrected

This PR's refusal made sentences in two of the card's own pending changesets false. They are corrected in 2da95c6da (final head 48857fcc6). Every still-true sentence is kept, and each file keeps its frontmatter (package and bump). check-empty-changeset refuses the edit as a foreign-changeset modification. That is its DELIBERATE CORRECTION class, red by design at the final head 48857fcc6, as since 2da95c6da (CI Check Changeset: "exists on the merge base and was not added by this PR"), for the seat's same-head contract review to confirm. The evidence for every correction is M1 AFTER above: user-agent-based, public, spa, '' and non-strings answer 400 INVALID_REQUEST, with no row stored and no warning logged.

.changeset/24773-oauth-application-type.md (@objectstack/plugin-auth: patch, unchanged)

  • The bullet label changes from "Unchanged, with a warning." to "Refused.".
  • BEFORE: "It keeps today's outcome: registered and judged as web, with a client secret. The server now logs one warning per such registration, naming the value."
  • AFTER: "The register route refuses it with 400 INVALID_REQUEST and registers nothing (this release's breaking entry for the route)."
  • Why the original is false: M1 AFTER. These types are refused with 400; no web client is registered and no warning is logged.
  • Kept verbatim: "Any other type, including the form's user-agent-based and public, has no application_type in the provider." and "To use a loopback redirect URI, register the client as native."

.changeset/24773-oauth-form-types.md (@objectstack/platform-objects: patch, unchanged)

  • The Clause-② reason.
    • BEFORE: "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)"
    • AFTER: "Clause-②: no (a form's offering: nothing on the server reads the action's param options, so this change moves no accept set or served shape; the register route's refusal of the other types is its own breaking entry in this release)"
    • Why the original is false: M1 AFTER; the route no longer accepts every value it accepted. The value stays no, because the form change itself moves no accept set. The parenthetical does not start with an arm word, so the line's reading is unchanged.
  • BEFORE: "The OAuth provider judges only web and native, so the register route registers the other two as web, with a client secret."
    • AFTER: "The OAuth provider judges only web and native, and the register route refuses any other type with 400 INVALID_REQUEST."
    • Why the original is false: M1 AFTER.
  • BEFORE: "The register route accepts the same values as before, including user-agent-based and public." (the first sentence of the Unchanged. bullet)
    • AFTER: removed. The bullet keeps "The stored sys_oauth_application.type column still lists all four values, so a row written under an earlier provider line keeps its value. The public column is unchanged."
    • Why the original is false: M1 AFTER. The refusal is stated in this PR's own entry and in the corrected sentence above.

.changeset/24773-oauth-register-refusal-status.md (@objectstack/plugin-auth: patch) is not edited, because none of its sentences is false. Each one speaks of the refusals the provider raises (400 for invalid_redirect_uri, 401 for a missing session, 500 for a throw that declares no status, the body-schema 400), and those are unchanged. The new type refusal comes before the provider is called. So do the route's own name / redirectURLs refusals, which this note never covered either. Its closing sentence, "a web client still needs an https URI on a non-loopback host", stays true.

Acceptance notes

  • Legacy rows and the update paths. Rows written under @better-auth/oauth-provider 1.6.x / 1.7.0-rc (2026-05-08 to 2026-08-27) can hold type = 'user-agent-based' and a public value. 1.7.3's update paths re-validate the stored client, so such a row would answer application_type must be web or native to an update that does not also set application_type. This comes from reading source; it was not measured on a row, and no deployment's data is in reach.
  • The stored select declares two values no writer produces. useragentbased and public (M2). Reported to the seat in the dev report, not narrowed here.

Generated by Claude Code

…e the provider does not judge

POST /api/v1/auth/sys-oauth-application/register now accepts exactly
'web' and 'native'. Any other value present in the body (user-agent-based,
public, an unknown or empty string, a non-string) answers the route's own
400 INVALID_REQUEST before createOAuthClient is called, so nothing is
stored; it was registered as 'web' with one server warning. An absent or
null type stays the provider's default, 'web'.

Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y
Co-authored-by: Claude <noreply@anthropic.com>
…ster door's 18.x refusal and the stored type column's readings; changeset for the plugin-auth narrowing

Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y
Co-authored-by: Claude <noreply@anthropic.com>
… in its JavaScript, so it is named with a patch

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

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/error-catalog.mdx (via INVALID_REQUEST (literal, a string literal in registerAuthRoutes))
  • content/docs/api/metadata-api.mdx (via INVALID_REQUEST (literal, a string literal in registerAuthRoutes))
  • content/docs/automation/webhooks.mdx (via INVALID_REQUEST (literal, a string literal in registerAuthRoutes))
  • content/docs/data-modeling/import-mappings.mdx (via INVALID_REQUEST (literal, a string literal in registerAuthRoutes))
  • content/docs/permissions/authentication.mdx (via INVALID_REQUEST (literal, a string literal in registerAuthRoutes))
  • content/docs/permissions/sso.mdx (via INVALID_REQUEST (literal, a string literal in registerAuthRoutes))
  • content/docs/ui/forms.mdx (via INVALID_REQUEST (literal, a string literal in registerAuthRoutes))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via INVALID_REQUEST (literal, a string literal in registerAuthRoutes))
  • content/docs/releases/v17/17-6.mdx (via INVALID_REQUEST (literal, a string literal in registerAuthRoutes))
  • content/docs/releases/v18/18-0.mdx (via INVALID_REQUEST (literal, a string literal in registerAuthRoutes))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

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 — 19 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 eddffc3390cf7e2cb9fd39960abc7fef4c40c902 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 8c8b065c6641e9a2e227f94dd3d991539f9d0460 — the merge of head 48857fcc67456e5da3f9169285a9e49784c820f9 into base eddffc3390cf7e2cb9fd39960abc7fef4c40c902, 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 8c8b065c6641e9a2e227f94dd3d991539f9d0460 && git checkout 8c8b065c6641e9a2e227f94dd3d991539f9d0460
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eddffc3390cf7e2cb9fd39960abc7fef4c40c902 48857fcc67456e5da3f9169285a9e49784c820f9 && git checkout -B drift-repro eddffc3390cf7e2cb9fd39960abc7fef4c40c902 && git merge --no-ff 48857fcc67456e5da3f9169285a9e49784c820f9

node scripts/docs-audit/affected-docs.mjs --json eddffc3390cf7e2cb9fd39960abc7fef4c40c902

⚠️ 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 eddffc3390cf7e2cb9fd39960abc7fef4c40c902 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…ister route's refusal of other application types

The Application Type and register-form entries said the route registers
user-agent-based / public as web with a warning and accepts the same
values as before. In the same release the route refuses any type other
than web / native with 400 INVALID_REQUEST, so those sentences are
rewritten; every still-true sentence is kept.

Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y
Co-authored-by: Claude <noreply@anthropic.com>
… from the last published state, without the unreleased warning

Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y
Co-authored-by: Claude <noreply@anthropic.com>
…its unchanged parts against the last published release

Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 48857fcc67456e5da3f9169285a9e49784c820f9
Local-runs: none
Scope: packages/plugins/plugin-auth/src/auth-plugin.ts, packages/plugins/plugin-auth/src/oauth-register-application-type.test.ts, packages/platform-objects/src/identity/sys-oauth-application.object.ts + .changeset/24773-oauth-type-refusal.md, .changeset/24773-oauth-application-type.md, .changeset/24773-oauth-form-types.md, .changeset/24773-oauth-register-refusal-status.md (unedited, judged), .changeset/24813-adopt-orphans-readonly-package.md (the BREAKING-minor shape), scripts/pm/clause2-line.mjs (readArmToken), and at the 18.0.0 tag commit 06cab4849: the register route in auth-plugin.ts and the form's type options in sys-oauth-application.object.ts

Isolated contract review (narrowing face) of PR #25038 for card #24773, rendered 2026-10-21T04:48Z. Inputs: the card's body and all 18 comments (rulings D 6293344532, 「甲」 6292005854 and its amendment 6303865025, the claim 6302570331 as widened for half 2c, every dev report and seat record); the PR body, its 6-file list and the net diff against the merge base dfe60e3a0 (143+ / 56-); the check-runs on the head, read last. No path of the diff moved on origin/main since the merge base. Nothing was built, run or re-run.

① Derived judgments

1. The accept set of POST /api/v1/auth/sys-oauth-application/register (auth-plugin.ts :3612-:3739 at the head) — right.

  • Order in the route: body read → name required → redirectURLs required and non-empty → the application-type branch (:3661-:3679) → getApi() and the 503 provider check → createOAuthClient → the catch that maps provider refusals to OAUTH_REGISTER_FAILED → 200.
  • type === 'web' || type === 'native' is forwarded as application_type (:3699). type === undefined or type === null falls through to applicationType = 'web'. Every other present value, strings ('' included) and non-strings alike, returns c.json({ success: false, error: { code: 'INVALID_REQUEST', message } }, 400) at :3669 — before getApi() is awaited and before createOAuthClient is called, so nothing is stored and nothing is logged. The envelope is the one the route's own name / redirectURLs refusals use at :3626, :3629, :3637, and INVALID_REQUEST is a code the route already answered: no new error code, no new status.
  • The warning branch half 2a added (ctx.logger.warn with registeredAs: 'web') is removed, not kept beside the refusal. Right: the refusal replaces the loud half, as the amendment of 「甲」 says (rule 2: the warning half alone is no longer the 18.x deliverable).
  • Placing the type refusal before the 503 "OIDC provider is not enabled" check matches the two request-shape refusals that already precede it; on a stack without the provider a malformed type is refused as malformed, which is the right order for a request-shape refusal.
  • The accept set after this diff is exactly: web, native, and no type (absent or JSON null). Judged right against ruling D (an unknown type is refused loudly with 400, the silent coercion to web ends) and the amendment (the refusal ships in 18.x).

2. The dev's reading that null is absent and '' is present — right.

  • Ruling D and 裁后执行 (6289866018) refuse "an unknown type" (未知类型拒 400); the amendment re-times that refusal. Neither text names null or '', so the question is which values name a type.
  • '' is a present string that names no known type: under D it is an unknown type, and half 2a already put it in the warning branch (the loud half whose refusal the amendment ships now). Refusing it is the ruling as written.
  • JSON null is the body's spelling of no value: half 2a read it as absent (no warning), the provider's own default is web, and the form's declared default is web. Refusing it would narrow past the set the loud half announced, i.e. past what D and the amendment cover. Keeping it as web is right.
  • So the refused set equals exactly the set that warned at the merge base, which is the cleanest reading of "the refusal replaces the warning". Pinned both ways: type: null → 200 stored web, no warning (:332-:336 of the test file); '' → 400 INVALID_REQUEST, no provider call (the REFUSED table, :347).

3. The message — right.

  • shown is JSON.stringify(type.length > 64 ? type.slice(0, 64) + '…' : type) for a string, else (typeof type) (:3665-:3668). So the message carries at most the value's first 64 characters (UTF-16 units; JSON.stringify escapes but never extends what is echoed), plus a single ellipsis that is not from the value, and names a non-string by its type ((number), (boolean), (object)). It names 'web', 'native', http://localhost, http://127.0.0.1 and http://[::1].
  • Pinned at :375-:390: 64 a characters echoed, the tail TAIL-NEVER-ECHOED absent, the five subjects present, a short value named whole, 42 named (number).

4. The test file — the pins match the accept set.

  • Half 2a's pins remain unchanged: the two provider preconditions, web + loopback refused by the provider's web rule (OAUTH_REGISTER_FAILED, invalid_redirect_uri), native + localhost / 127.0.0.1 stored native, native + plain http on a non-loopback host refused by the body schema, native + 127.0.0.2 refused by the native rule, web + https stored web, no type → web.
  • Added: null → web; seven refused values × https and loopback, each asserting 400, a clean ADR-0112 envelope, INVALID_REQUEST, zero provider calls (a pass-through vi.spyOn on the one api object getApi() hands out), zero new rows at driver level, zero warn and zero error calls; the message pin. expectStoredAs now also asserts exactly one provider call carrying the stored application_type, which pins the forwarding itself.

5. The stored column — untouched, and the finding is routed.

  • type: Field.select(['web', 'native', 'user-agent-based', 'public'], ...) at :449 and public: Field.boolean(...) at :456 of sys-oauth-application.object.ts are byte-identical between the merge base and the head; the file's only hunk is the action's code comment (:151-:164, 8+ / 8-, same line count), whose new text states the 18.x refusal, the absent / null → web rule, and the stored-column readings as the card's half 2a and 2b reports measured them. No param, option, label or field changes.
  • The PR's Acceptance notes carry both residual readings (the select declares useragentbased / public with no writer; legacy user-agent-based rows and 1.7.3's update paths, source reading only), marked "not narrowed here" and reported to the seat, and the seat's rework note 6307609026 names both as routed at ACCEPT. Right.

6. The deliberate correction — confirmed, sentence by sentence. BEFORE is read at the merge base (git show dfe60e3a0:PATH), AFTER at the head. The falsifying fact for every pair is item 1 above: a present type other than web / native answers 400 INVALID_REQUEST, nothing is stored, nothing is logged.

.changeset/24773-oauth-application-type.md (@objectstack/plugin-auth: patch, frontmatter unchanged). One bullet rewritten:

  • BEFORE label "Unchanged, with a warning." → AFTER "Refused.". The BEFORE label is falsified (neither unchanged nor warned); the AFTER label is true at the end of this release.
  • Kept verbatim: "Any other type, including the form's user-agent-based and public, has no application_type in the provider." Still true (the provider's application_type is web | native). "The form's" reads against this entry's own FROM state, the four-option form, which the register-form entry in the same release retires; not falsified by this diff.
  • BEFORE "It keeps today's outcome: registered and judged as web, with a client secret." → falsified: such a request is refused, no web client is issued, no secret is minted. AFTER "The register route refuses it with 400 INVALID_REQUEST and registers nothing (this release's breaking entry for the route)." → true: the status, the code, the no-store and the cross-reference (this PR's own entry, same release) all hold.
  • BEFORE "The server now logs one warning per such registration, naming the value." → falsified: the warn call is gone and the pins assert zero warnings. Removed, with no replacement needed: the AFTER sentence above states the whole outcome.
  • Kept verbatim: "To use a loopback redirect URI, register the client as native." Still true and pinned.
  • Every unedited sentence stays true: the title, the Clause-②: no — line, the FROM paragraph, the "Now." bullet, and "Unchanged. No type at all is web. Refusals keep the codes and statuses they had: 400 OAUTH_REGISTER_FAILED ..." (this diff adds a refusal before the provider and changes no existing code or status). No still-true sentence was lost.

.changeset/24773-oauth-form-types.md (@objectstack/platform-objects: patch, frontmatter unchanged). Three sentences changed:

  • The Clause-② line. BEFORE "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)" → the clause "still accepts every type value it accepted" is falsified. AFTER "Clause-②: no (a form's offering: nothing on the server reads the action's param options, so this change moves no accept set or served shape; the register route's refusal of the other types is its own breaking entry in this release)" → true: the form change itself moves no accept set, and the narrowing is carried by this PR's entry. The value stays no, which is right for a form's offering. Read against readArmToken in scripts/pm/clause2-line.mjs (:280-:293): the parenthetical's first word is a, not a widen / narrow word, so it is ordinary reasoning and the line's reading is unchanged; the Check Changeset job's notice annotation on this head counts exactly one declared-breaking changeset, this PR's own.
  • BEFORE "The OAuth provider judges only web and native, so the register route registers the other two as web, with a client secret." → falsified. AFTER "The OAuth provider judges only web and native, and the register route refuses any other type with 400 INVALID_REQUEST." → true at the end of this release ("any other type" reads as a present type; no type stays web, as the plugin-auth entries say).
  • BEFORE "Unchanged. The register route accepts the same values as before, including user-agent-based and public." → falsified; removed. The bullet keeps "The stored sys_oauth_application.type column still lists all four values, so a row written under an earlier provider line keeps its value. The public column is unchanged." → both still true (item 5). Removing rather than rewriting is right: the refusal is stated in the corrected sentence above and in this PR's entry, and the label "Unchanged." still heads two true sentences.
  • Every unedited sentence stays true: the title, the history sentence ("offered four application types"), "The form now offers Web and Native, and still defaults to Web.", the Redirect URLs help, the Application Type help, the translations line. No still-true sentence was lost.

.changeset/24773-oauth-register-refusal-status.md (unedited) holds no sentence this diff falsifies. Its FROM (provider refusals answered 500), its "Now." (400 for invalid_redirect_uri, 401 for a missing session, the code stays OAUTH_REGISTER_FAILED with details.upstreamError), and its "Unchanged." (a no-status throw stays 500, the body-schema refusal stays 400, "a web client still needs an https URI on a non-loopback host") all describe the catch block at :3703-:3729, which this diff does not touch; the new refusal comes before the provider is called, and the note's examples use web, an accepted type.

7. This PR's own note, .changeset/24773-oauth-type-refusal.md, judged against 18.0.0 and against the end of this release. The 18.0.0 state was read at the tag commit 06cab4849 (@objectstack/plugin-auth@18.0.0 and @objectstack/platform-objects@18.0.0 both point there): the route computed safeType from allowedTypes = {web, native, user-agent-based} else 'web' and forwarded it as type, which the provider strips, so every client was judged and stored as web, with no warning; the form offered four types.

  • Frontmatter plugin-auth: minor, platform-objects: patch — right (② below).
  • Title "refuses an application type the OAuth provider does not judge with 400 INVALID_REQUEST, where it registered it as a web client" — true of 18.0.0 (registered as web) and of the head (refused).
  • Clause-②: no (narrowing) — the register door accepted any type value and registered every one other than web / native as a web client with a secret; it now refuses each of those values with 400, so it accepts exactly web, native and no type. — true: 18.0.0 accepted any value and registered the non-web / native ones as web (it also registered native as web, which this line does not deny and the Application Type entry covers); the head accepts exactly the three.
  • The adr-0087 marker: not-required (no-migration-prescription) — right: no spec key, authorable spelling, object definition, stored shape or export moves; the remedy is a different request value. The marker's sentence that the form "already offers only Web and Native" is true at the end of this release, not of 18.0.0; it is the gate-read disposition reason inside a hidden comment, not a FROM statement. Nit, no change required.
  • "BREAKING (a REST door's accept set narrows), shipped as minor under the launch-window convention" — the packages: POST /packages/:id/adopt-orphans skips requireWritablePackage, so it can bind loose items into a read-only package #24813 shape, as the amendment prescribes.
  • The paragraph: the provider judges web and native; the door forwards the form's choice as application_type (end-of-release, present tense); "The door used to register such a request as a confidential web client, with a client secret and the web redirect-URI rule" — true of 18.0.0; "It now refuses the request instead." — true.
  • FROM: "a request whose type is present and is neither web nor native (user-agent-based, public, any other string including the empty string, or a value that is not a string) answered 200 and stored a web client, or was refused by the web rule (OAUTH_REGISTER_FAILED) for a loopback redirect URI." — true of 18.0.0 for every listed value: safeType maps a non-string, '', public and any unknown string to 'web', and user-agent-based is stripped to web by the provider; an https redirect registered web (200), a loopback one was refused by the web rule with OAUTH_REGISTER_FAILED. The r3 removal of the warning phrase was right: 18.0.0 logged nothing. The r4 rewrite names no status, which is right (see the observation below).
  • TO: 400, INVALID_REQUEST, the first 64 characters, both accepted types, the three loopback hosts, no provider call, nothing stored, no warning — each true of the head (items 1 and 3). For a non-string the message names the type rather than the value; the note's "names the value" is a slight simplification, not a false sentence.
  • Accepted: web and native forwarded as application_type "(this release's Application Type entry)" — true at the end of this release and correctly not stated as the FROM (18.0.0 stored native as web).
  • Unchanged: no type or type: null registers web — true in 18.0.0 (null is not a string → 'web') and at the head. "The other refusals keep their codes (their statuses are this release's refusal-status entry)" — the codes are untouched, and the statuses are pointed at the pending note, which ships in this release's CHANGELOG. "Clients already registered are not touched." — true: no row is read or written.
  • @objectstack/platform-objects: comment only, ships in the package's JavaScript; "The form offers only Web and Native (this release's register-form entry)." — true at the end of this release, correctly cross-referenced rather than claimed of 18.0.0.
  • The fix for an integrator: matches the provider's rule as pinned (native for loopback http, web for https on a non-loopback host, omitted type → web).
  • Observation, no FAIL: the dev's r4 rationale says the loopback refusal "answered 500 in 18.0.0". The 18.0.0 tag commit 06cab4849 has half 1's merge 950f949a9 as an ancestor, and its register route already reads resolveThrownHttpError(err, 500), so a build from that commit answers 400 there — while the 18.0.0 CHANGELOG.md of plugin-auth carries no OAuth app registration rejects http://localhost and http://127.0.0.1 redirect URIs as "non-loopback", with HTTP 500 #24773 entry and .changeset/24773-oauth-register-refusal-status.md is still pending at the tag and on main (the version-packages snapshot predates that merge). The card's own 500 was measured on 18.0.0-next.5, which predates the fix. The r4 sentence names no status, so it is true under either reading; the pending refusal-status note will describe, in this release's notes, a change already present in a tarball built from the 18.0.0 tag commit. For the seat, not this PR's.

② Semver level

  • What the diff publishes: @objectstack/plugin-auth narrows one REST door's accept set (a present type other than web / native was accepted, now refused) — BREAKING under Clause-②: no (narrowing); no ADR-0087 conversion carries it (no authored metadata moves), so under 「甲」 as amended (6303865025, rule 2) it ships now in 18.x as a BREAKING minor with ! in the title, the BREAKING banner, and an adr-0087 not-required (no-migration-prescription) disposition — the 24813-adopt-orphans-readonly-package shape, matched line for line. @objectstack/platform-objects publishes a changed dist JavaScript (esbuild keeps the comment text, per the dev's measured sha256 comparison; the package is not private), so a patch entry is right and skip-changeset does not apply.
  • The changeset frontmatter (plugin-auth: minor, platform-objects: patch) matches that; no major is introduced. The two corrected notes keep their patch levels and their Clause-②: no lines, and are not re-read as breaking: the Check Changeset job's notice annotation on this head reads exactly one ADR-0087 exemption, on .changeset/24773-oauth-type-refusal.md.
  • Clause-②: line — the PR body's Clause-②: no (narrowing) — ... and the changeset's line carry the same arm and the same reason, and match the half-2c dispatch's Clause-② (2c): no (narrowing). A narrowing owes this record at CONTRACT_REVIEW_TIER before the queue, which is what this is.

③ Boundary flags

  • open_questions: none in any 2c round (6307562409, 6307878565, 6308016949, 6308135792). Nothing to answer.
  • Dev deviations, each answered: (a) the dogfood battery ran once at 3f934a772 and was not re-run after the merge of main dfe60e3a0 or the prose rounds — accepted: that merge moved no path of this diff, r2-r4 are changeset prose only, and the head's Dogfood Regression Gate check-run is success; (b) platform-objects patch for a comment-only source change — right (②); (c) '' refused, null kept — right (① 2); (d) an ellipsis after the 64th character where the warning cut silently — fine, the marker is not from the value and the pin asserts the tail never appears; (e) packages: POST /packages/:id/adopt-orphans skips requireWritablePackage, so it can bind loose items into a read-only package #24813 is an issue, the shape was read from its changeset file — confirmed against that file; (f) the PR was opened at 3f934a772 without re-merging, then merged dfe60e3a0 in r2 — the net diff's paths have not moved on origin/main since that merge base, and the PR reads mergeable; (g) the throwaway probe file is not in the diff (6 files, none new under src/ besides the edited test); (h) sibling-repo reads through owned refs and grep-only tarball reads — process notes, no surface breach; (i) r2's Clause-② parenthetical rewrite — verified against readArmToken (① 6); (j) r4's two extra phrase changes beyond the proposed one — judged right (① 7); (k) the adr-0087 marker's "already offers" left as written — accepted as a nit (① 7).
  • File surface of the widened claim 6302570331 (2c): auth-plugin.ts's application-type branch only — the file's single hunk is :3647-:3680; sys-oauth-application.object.ts's code comment only — same line count, select and public byte-identical; test files under plugin-auth/src/; one new changeset in the packages: POST /packages/:id/adopt-orphans skips requireWritablePackage, so it can bind loose items into a read-only package #24813 shape; the card's pending changesets, falsified sentences only — the refusal-status note is untouched. No packages/spec/**, no workflow, no other region of auth-plugin.ts, no generated bundle. Held.
  • Routed to the seat (not blockers): the class-b finding on the stored select's two unwritten declared values, and the legacy-row / update-path reading (both in the PR's Acceptance notes and the rework note 6307609026); and the 18.0.0 tag-versus-CHANGELOG observation in ① 7.
  • Check-runs on 48857fcc6 (47 runs, two workflow runs on the same head):
    • Check Changeset: failure, twice (both runs). The annotations name exactly .changeset/24773-oauth-form-types.md and .changeset/24773-oauth-application-type.md as "exists on the merge base and was not added by this PR", and nothing else. Both files are this card's own notes, written under the same claim by halves 2a and 2b; no other PR drew either filename, so this is the DELIBERATE CORRECTION class, not a COLLISION, and this record is its confirmation. Designed red, confirmed.
    • pending: Test Core (1/6), Test Core (2/6), Test Core (3/6), Test Core (4/6), Test Core (6/6) (in progress). Test Core (5/6) is success. A pending run is not a FAIL; convergence is CI's.
    • success: Build Core, TypeScript Type Check, Lint & Repo Gates, every Lint · and Type Check · lane, Governed Surface Queue Guard, Dogfood Regression Gate, Check Documentation Links, Flag docs affected by code changes, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, filter, and (first run) Auto Label and Check PR Size.
    • skipped by their conditions on a PR: 18.x upgrade harness (opt-in), Packed-tarball smoke (opt-in), Build Docs, Console Pin Gate, Dogfood Verify CLI, Temporal Conformance (live PG + MySQL), the Dogfood Regression Gate shard matrix, and (second run) Auto Label and Check PR Size.

Implemented-by: claude/issue-24773-oauth-type-refusal
Reviewed-by: session_01UmqzD97e14iws6hmVhgZ5y

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · #24773 half 2c (the last half) · domain:services seat 2 · os-support-ai · session_01UmqzD97e14iws6hmVhgZ5y · 2026-10-21T05:04Z · head 48857fcc67.

  • This is the review of record for the claim 6302570331, as widened in place for half 2c and its patch round.
  • Contract review: owed on the narrowing face, and PASS at CONTRACT_REVIEW_TIER on this same head (6308302124, isolated reviewer, Local-runs: none).
  1. Form:
  2. Premise and fix:
    • Under ruling D (6293344532) and the maintainer's amendment of 「甲」 (6303865025), the register route refuses with 400 INVALID_REQUEST any present type other than web / native: user-agent-based, public, '', any other string, a non-string.
    • The refusal comes before the provider is called, so nothing is stored and nothing is logged.
    • Absent and null stay the provider default, web.
    • The message echoes at most 64 characters of the value.
    • Half 2a's warning branch is removed.
    • Measured first (M1–M3): every value was run through the real route, before and after. The provider's writers and the column's readers were read. No in-repo, objectui or HotCRM sender of another type exists.
    • Pins: 7 values × https and loopback, the message, null. Ablations A–G each went red and were restored to blob == HEAD.
    • The dogfood battery passed at 3f934a772.
  3. Gates:
    • CI on 48857fcc67, latest run per check name: 41 checks, 31 success, 9 skipped, 1 failure. Every skip is on the roster in scripts/pm/check-expected-skips.mjs: Auto Label, Check PR Size, Console Pin Gate, Temporal Conformance, the Dogfood Regression Gate matrix, Dogfood Verify CLI, Build Docs, Packed-tarball smoke and the 18.x upgrade harness. Lint & Repo Gates, TypeScript Type Check, Build Core, all six Test Core shards and Dogfood Regression Gate are success. The one failure is Check Changeset.
    • Check Changeset is red by design, and all three conditions for landing with it hold:
      • (a) the gate's source says the DELIBERATE CORRECTION class "stays red either way" (scripts/check-empty-changeset.mjs);
      • (b) it runs only on pull_request (.github/workflows/pr-automation.yml), never on merge_group;
      • (c) this record names the gate and the cause: this PR corrects the falsified sentences of the card's own pending notes .changeset/24773-oauth-application-type.md (half 2a) and .changeset/24773-oauth-form-types.md (half 2b), and the annotations name exactly those two files.
    • The confirmation the gate asks for: the contract review on this head judged every changed sentence BEFORE → AFTER. No still-true sentence was lost, and .changeset/24773-oauth-register-refusal-status.md is unedited and holds nothing this diff falsifies. This PR's own note was judged against 18.0.0 after rounds r3 and r4.
    • Local: 74 derived, 74 run, 0 NOT-MEASURED. The plugin-auth suite: 175 files, 3348 passed. Typecheck and check:test-typecheck are OK.
  4. Accepted deviations:
    • '' is refused and null is kept as absent: the refused set equals exactly the set that warned at the merge base (the review's ① 2).
    • The ellipsis after the 64th character.
    • The platform-objects patch.
    • The patch rounds (r2: the card's pending notes; r3 and r4: this PR's own note stated against 18.0.0).
    • The dogfood battery was not re-run after prose-only rounds and a merge that moved no path of the diff. The head's Dogfood Regression Gate is success.
  5. Out of scope:
  6. Landing: ordinary queue landing with the designed red above. Every other check on this head is green or an expected skip, so the seat flips it on this record. It closes OAuth app registration rejects http://localhost and http://127.0.0.1 redirect URIs as "non-loopback", with HTTP 500 #24773.

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.

OAuth app registration rejects http://localhost and http://127.0.0.1 redirect URIs as "non-loopback", with HTTP 500

2 participants