Skip to content

fix(platform-objects)!: sys_oauth_application.type declares only web and native, the two application types its writer produces - #25099

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-25053-oauth-application-type-options
Oct 21, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-25053-oauth-application-type-options

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #25053
Clause-②: no (narrowing): the declared option set that /meta serves and the write path judges loses useragentbased and public, which no writer produces. It owes a CONTRACT_REVIEW_TIER review before the queue.

sys_oauth_application.type now declares exactly the two application types its one writer produces, web and native, under ADR-0049 enforce-or-remove. No stored row is rewritten, and every door that answers today for a row holding any type answers the same after the change (M1). One BREAKING minor changeset for @objectstack/platform-objects.

What changes

packages/platform-objects/src/identity/sys-oauth-application.object.ts, the stored type column's declaration and two comments:

Before (a39389eb5) After
declaration Field.select(['web', 'native', 'user-agent-based', 'public'], ...) Field.select(['web', 'native'], ...)
served options (GET /api/v1/meta/object/sys_oauth_application) web, native, useragentbased (label user-agent-based), public web, native
required / defaultValue false / web unchanged

Measured first

M1: every door, before and after, over rows holding each type

Real composition: bootShowcase (the dogfood harness, OS_OIDC_PROVIDER_ENABLED=true), signed in as the seeded platform admin. Six rows were seeded at driver level, owned by the admin, holding type web, native, user-agent-based, null, useragentbased and public. The probe was a throwaway file in packages/qa/dogfood/test, run once before the change (closure built at a39389eb5) and once after (platform-objects rebuilt), and is not in this PR.

Door Before After
GET /api/v1/meta/object/sys_oauth_application, the type options 4 options (above) web, native
GET /api/v1/data/sys_oauth_application (list) 200; all six rows listed, each with its stored type identical
GET /api/v1/data/sys_oauth_application/:id 200 for all six, stored type returned identical
GET /api/v1/auth/oauth2/get-client 200 for all six; application_type is the stored value (absent for null) identical
POST /api/v1/data/sys_oauth_application, PATCH .../:id, POST .../import 405 for every value identical
POST /api/v1/auth/oauth2/update-client (rename only) web, native, null: 200. user-agent-based, useragentbased, public: 400 invalid_client_metadata from the provider, row unchanged identical
PATCH /api/v1/auth/admin/oauth2/update-client 404 for all (not mounted at this boot) identical
POST /api/v1/auth/admin/oauth2/toggle-disabled 200 for all six; disabled set, type unchanged identical
POST /api/v1/auth/oauth2/client/rotate-secret 200 for all six; secret changed, type unchanged identical
engine insert / update SENDING type (system context, as plugin-auth's adapter writes) web, native, null, useragentbased, public accepted; user-agent-based refused VALIDATION_FAILED / invalid_option web, native, null accepted; user-agent-based, useragentbased, public refused VALIDATION_FAILED / invalid_option
engine update of another field on each row accepted for all six, type unchanged identical

So the only movements are the served option list and the engine's verdict on a write that sends useragentbased or public. No door sends either value: the provider sends web, native or no type, and the generic data API answers 405. H4 holds, so this is done, not needs_decision: no door that works today with such a row refuses it after, and no mapping of user-agent-based is needed.

M2: the writers (H1)

Re-measured on origin/main from source and the installed dist:

  • @better-auth/oauth-provider 1.7.3 (packages/plugins/plugin-auth/package.json, exact pin). Every creation path goes through persistOAuthClientRegistration and checkOAuthClient, which refuses any application_type other than web / native (dist/authorize-9whjxVLJ.mjs:1770). The default is web, or null for a client-metadata-document registration that names none (:1956). Both update paths declare application_type as z.enum(["web", "native"]) (:2885, :2923) and re-run checkOAuthClient over the stored client merged with the update (:2420), which is the 400 in M1.
  • The mapping: applicationType: 'type' in auth-schema-config.ts:489. The generic data API: apiMethods: ['get', 'list'] (sys-oauth-application.object.ts:657), measured as 405 above.
  • The client-metadata-document seam (registerClientMetadataDocument) is exported only from @better-auth/oauth-provider/internal, and nothing in plugin-auth/src imports it (grep -r for the function and the subpath: 0 hits), so the null writer is not reachable in this composition (see Acceptance notes).

M3: null and required (H3)

type is required: false today (M1's /meta reading) and stays so. A null row reads and lists (M1), and an engine update that sends null is accepted and stores null, before and after. The new pin asserts required: false.

M4: the console

Read from objectui at the pinned .objectui-sha cf153eb2e: SelectCellRenderer (packages/fields/src/index.tsx) renders a value that matches no option as a humanized label of the raw value, and an empty value as the empty placeholder. A user-agent-based row matched no option before (the option value was useragentbased) and matches none after, so it renders the same. Only a useragentbased or public row, which no writer produces, now renders humanized instead of with an option label. The register form carries its own options (unchanged), and the record has no edit door (405).

Tests

  • packages/platform-objects/src/identity/sys-oauth-application.type-options.test.ts (new, 5 tests): the options are exactly web and native, each value equal to its label; required: false and default web; the register form offers the same values; all four bundles translate exactly those two; the public column is kept.
  • packages/platform-objects/src/identity/oauth-register-form-types.test.ts: half 2b's describe that pinned the four options is removed (its header now points at the new file).
  • packages/plugins/plugin-auth/src/oauth-application-type-column.test.ts (new, 39 tests), on a real ObjectQL engine over SQLite with the auth manifest's objects and real better-auth with the OIDC provider:
    • a row holding each of web, native, null, user-agent-based, useragentbased, public reads and lists with its stored value, and the provider's getOAuthClient returns it;
    • the bridge's disabled write and the provider's rotateClientSecret are accepted on every such row, the type unchanged;
    • the provider's updateOAuthClient refuses a rename that leaves an undeclared type in place (invalid_client_metadata, row unchanged), and accepts one that also sets application_type: 'native', storing native (the remedy the changeset names);
    • an insert or update SENDING user-agent-based, useragentbased or public is refused: resolveThrownHttpError gives status 400 and code VALIDATION_FAILED, details.fields holds one invalid_option on type, nothing is written; web, native and null are accepted.
    • These pins live in plugin-auth rather than beside the platform-objects identity tests because platform-objects cannot import @objectstack/objectql, which depends on it (plugin.test.ts records the cycle). The new file imports platform-objects through its dist, a pair already in KNOWN_UNALIASED_TEST_IMPORTS.

Results (os-verify-lock VERDICT command-exit 0 on each):

  • platform-objects at 2d158a9dd: Test Files 84 passed (84), Tests 1242 passed (1242). Typecheck exit 0, check:test-typecheck: OK, 2 pinned signatures held; the new test is in the tsconfig.test.json program (--listFilesOnly: 1 hit).
  • plugin-auth at 2d158a9dd: the full suite Test Files 178 passed (178), Tests 3415 passed | 10 skipped (3425). Typecheck exit 0, check:test-typecheck: OK, 23 pinned signatures held; the new file is in the program (1 hit).
  • After merging origin/main 4feccc268 (head 17458f50d): the ablation's control legs below re-ran both new files green (5/5, 39/39), and check:i18n stayed in sync.

Ablation (one-shot, no permanent file)

One leg restored the retired options: scripts/ablation-replace.mjs --hold replaced the declaration with Field.select(['web', 'native', 'user-agent-based', 'public'], ...) (anchor 1 to 0, blob 2f185ac16441 to 1af41cbfc3ae), inside a runner with its own EXIT/INT/TERM restore trap. Predicted before the run: leg 1 turns 2 of 5 red, leg 2 turns 4 of 39 red.

  • Leg 1, source: sys-oauth-application.type-options.test.ts: Tests 2 failed | 3 passed (5). Red: the option set (expected [ 'web', 'native', …(2) ] to deeply equal [ 'web', 'native' ]) and the form parity. Green as predicted: the default, the bundles (not regenerated), the public column.
  • Leg 2, dist: platform-objects rebuilt; ablation-dist-preflight.mjs found the marker in 4 built files (its dist reading passed; its tree reading refused because the dist spelling uses double quotes and no --source-marker was passed, exit 3). Then oauth-application-type-column.test.ts: Tests 4 failed | 35 passed (39). Red: insert and update with useragentbased and with public, accepted again. user-agent-based stayed refused, because the shorthand still strips it.
  • Restore: blob 2f185ac16441 equals HEAD, git diff HEAD empty; rebuilt; the preflight --absent found the marker in none of 66 built files and the tree clean; both files green (5/5, 39/39); git status --porcelain empty.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at the final head 17458f50d derived 69 commands (9 paths, 467 changed lines, under the 3000 threshold). Each ran after a whole-workspace build (turbo run build --filter=!@objectstack/docs, 76 tasks), as eval cmd > log 2>&1; status=$?, recorded byte for byte with :: exit N.

  • All 69 exit 0. --ran: Run reconciliation — 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN.
  • Verdicts among them: 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). check-changeset-no-major: no major bump. check-empty-changeset: no merge-base changeset modified or deleted at 17458f50d; at a72b7acf8 it exits 1 by design, naming only .changeset/24773-oauth-form-types.md (the deliberate correction above). check:i18n: all 10 packages in sync. Also green: check:cross-package-test-inputs, check:dual-build-cjs-loads, check:nul-bytes, check:issue-citations, check:doc-authoring, check:test-source-alias, check:engine-double-contract, check:type-check-debt.
  • The five roster families whose roster sits under one of these paths, run besides: check-changeset-fixed, check:auth-mount-ledger, check:authz-resolver, check:error-code-casing, check:filter-alias-parity, all exit 0.
  • Not run locally: test:repo (CI's Test Core); the type-check lanes and CI-only jobs the derivation lists outside the 69; check:dev-prereqs and check:skill-frame-freshness (no packages/spec build input or frame file touched); check-platform-checklist --changed-from (no exported symbol renamed or removed).

Patch round 1 (a72b7acf8, two .changeset lines only): the same 69 commands re-derived and re-run after a workspace build; 68 exit 0 and check-empty-changeset --base origin/main exits 1 as above. --ran: Run reconciliation — 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN.

Dogfood

The Clause-② line carries (narrowing), so the full battery ran once on the final head 17458f50d, as CI spells it: the closure built (turbo run build --filter=!@objectstack/docs, 76 tasks, a superset of @objectstack/dogfood^...), then pnpm --filter @objectstack/dogfood test under the lock, with no OS_TEST_SHARD and no path.

  • os-verify-lock: VERDICT command-exit 0 · held the lock 1130s (18m50s) (shared-box seconds).
  • Test Files 286 passed | 3 skipped (289), Tests 2387 passed | 18 skipped (2405). No failure.
  • The derivation notes that Dogfood Regression Gate does not run on a PR or in the merge queue, so this is the only pre-merge reading of it.

The head merges origin/main 4feccc268. main has since moved to 1862d8d13 (two commits in packages/spec, driver-turso and service-datasource), none of which touches a path of this diff, so the head was not re-merged before opening.

Patch round 1 re-ran the full battery on a72b7acf8: os-verify-lock: VERDICT command-exit 0 · held the lock 1284s (21m24s), Test Files 286 passed | 3 skipped (289), Tests 2387 passed | 18 skipped (2405). The head still merges origin/main 4feccc268; main is at 8f59304dd, with no path of this diff moved.

Acceptance notes

  • A pending changeset sentence this change made false, corrected here (patch round 1, a72b7acf8). .changeset/24773-oauth-form-types.md (half 2b, still pending) said: "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." That bullet now reads: "A row written under an earlier provider line keeps its stored type, and the public column is unchanged; the stored column's options narrow to Web and Native in this release's sys_oauth_application.type entry." No other line of that note changed. This PR's own changeset now says "The register-form entry in this release refers to this entry for the stored column's options." where it said it superseded that sentence. Check Changeset is red by design: check-empty-changeset's DELIBERATE CORRECTION class, naming only .changeset/24773-oauth-form-types.md, for the seat's same-head contract review.
  • A null the provider sends on insert is stored as web. Measured in M1: an engine insert with type: null stores web (the declared default fills the blank), while an update that sends null stores null. The provider sends null only for a client-metadata-document registration, whose seam this composition does not wire. Unchanged by this PR; noted, not filed.
  • Legacy rows and the provider's update path. A row holding user-agent-based (or any type other than web / native) is refused by the provider's own update path unless the update also sets application_type, before and after this change (M1, and pinned). No deployment's data is in reach to count such rows.
  • spec: Field.select's string-array shorthand silently strips hyphens from option values, so a written 'no-overlay' or 'user-agent-based' is not the declared value #25013 (the shorthand that strips hyphens) remains open; this PR only spells its values so the shorthand cannot change them.

Generated by Claude Code

…and native, the two values its writer produces

The stored column declared web, native, useragentbased and public through
the string shorthand. No writer produces the last two: the OAuth provider
writes web or native (or null), and the generic data API cannot write the
table. Rows are not rewritten; a legacy row still reads, lists and takes
every write that leaves type out.

Claude-Session: https://claude.ai/code/session_01QK6pdbxKZiHdFApL8RYVh9
Co-authored-by: Claude <noreply@anthropic.com>
… undeclared type; add the changeset

Claude-Session: https://claude.ai/code/session_01QK6pdbxKZiHdFApL8RYVh9
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 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 8f59304ddc45825f1f4d63f6956e07a75df68c9d → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 8f59304ddc45825f1f4d63f6956e07a75df68c9d

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

…nce this release makes false, and re-point this entry's cross-reference

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a72b7acf8fc300caa0466ec481711a79b7c06fd8
Local-runs: none
Scope: packages/platform-objects/src/identity/sys-oauth-application.object.ts, packages/platform-objects/src/apps/translations/en.objects.generated.ts, packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts, packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts, packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts, packages/platform-objects/src/identity/oauth-register-form-types.test.ts, packages/platform-objects/src/identity/sys-oauth-application.type-options.test.ts, packages/plugins/plugin-auth/src/oauth-application-type-column.test.ts, .changeset/24773-oauth-form-types.md, .changeset/25053-oauth-application-type-options.md + packages/plugins/plugin-auth/src/auth-schema-config.ts (AUTH_OAUTH_CLIENT_SCHEMA, applicationType: 'type'), packages/plugins/plugin-auth/src/auth-plugin.ts (the register wrapper and the toggle-disabled bridge), packages/runtime/src/api-exposure.ts (apiMethods, the 405), the installed @better-auth/oauth-provider 1.7.3 dist/authorize-9whjxVLJ.mjs (checkOAuthClient, persistOAuthClientRegistration, both update paths), packages/spec/src/data/field.zod.ts (selectWithOptions, toSnakeCase), packages/objectql/src/validation/record-validator.ts (the select arm, validateRecordInScope), packages/objectql/src/engine.ts (applyFieldDefaults and its insert and update call sites), packages/drivers/driver-sql/src/builtin-column-collision.ts (options emits no DDL), the three *.source-hashes.generated.ts companions, packages/rest/src/meta-item-read-routes.ts, objectui packages/fields/src/index.tsx at the pinned .objectui-sha cf153eb2e (SelectCellRenderer), .changeset/24813-adopt-orphans-readonly-package.md and .changeset/24773-oauth-type-refusal.md (the BREAKING-minor shape), scripts/pm/clause2-line.mjs (readArmToken), the headers of scripts/check-empty-changeset.mjs and scripts/check-adr-0087-registration.mjs, and the Check Changeset job log on this head

Isolated contract review (narrowing face) of PR #25099 for card #25053, rendered 2026-10-21T10:19Z. Inputs: the card's body and both comments (the claim 6311507729 and the dev report 6313194098; no patch-round report is posted for a72b7acf8); card #24773's body and all 18 comments; PR #25038, its file list and its contract review 6308302124; #25013 as context; the PR body, its 10-path file list, the net diff against the merge base 4feccc268 (428 added, 41 removed; origin/main holds byte-identical copies of every touched path, the two changesets included, so the net diff against origin/main reads the same), and git diff 17458f50d a72b7acf8 (2 files, 2 added, 2 removed, both changesets); the check-runs on the head and the Check Changeset job log, read once. The provider dist was read from the dev worktree's node_modules. Nothing was built, run or re-run.

① Derived judgments

1. The diff. type: Field.select(['web', 'native', 'user-agent-based', 'public'], …) becomes Field.select(['web', 'native'], …) at :462 of the object file, with label, required: false, defaultValue: 'web' and group unchanged; the action comment (:159-:161) and a new field comment; the four bundles drop useragentbased and public under sys_oauth_application.fields.type.options and keep the translated web / native leaves; half 2b's four-option describe is removed; two new test files; one new changeset; one corrected pending changeset. public: Field.boolean(…) (:469) and apiMethods: ['get', 'list'] (:657) are byte-identical to the merge base. Field.select's string shorthand (selectWithOptions, field.zod.ts :3067-:3073) keeps the written string as label and derives value with toSnakeCase (:3054-:3059, which drops every character outside [a-z0-9_]), so the declared values are exactly web and native, each equal to its label (pinned), and #25013's stripping reaches neither.

(a) Rows that exist today: every door, read in the code and matched to the pins.

  • Reads, GET /api/v1/data/sys_oauth_application and /:id, with any filter: the read path judges no option membership. The only invalid_option sites outside tests in packages/objectql/src and packages/rest/src are the record validator's write arm (record-validator.ts:1502-:1521) and rule-validator.ts:3139, a visibleWhen gate on a write; no filter normaliser judges a select value, so a list filtered on any type value answers as before. Pinned at the engine for all six stored values (oauth-application-type-column.test.ts, first describe). A user-agent-based row was already outside the declared set before the change and read fine, by the same mechanism.
  • get-client: the provider's getOAuthClient returns application_type as stored; 1.7.3 branches on the value on no read (grep -c user-agent-based over its dist: 0). Pinned for all six.
  • The provider's update paths: /oauth2/update-client and /admin/oauth2/update-client declare application_type: z.enum(["web", "native"]).optional() (:2885, :2923) and re-run checkOAuthClient over the stored client merged with the update (:2420), which refuses any application_type other than web / native (:1770). So a row holding user-agent-based, useragentbased or public answers invalid_client_metadata to an update that leaves its type in place, before and after this diff, and the engine is never reached; with application_type: 'native' in the same update the provider writes native, which the validator accepts. Both pinned. A null row: applyOAuthClientRegistrationDefaults(client, "web") (:1757) fills the blank before the check, so a rename is accepted, as before.
  • toggle-disabled: the bridge (auth-plugin.ts:2952) writes { id, disabled, updated_at } through the engine. validateRecordInScope's update branch walks Object.entries(data) (record-validator.ts:1989), so a stored type is never judged by a write that leaves it out. Pinned for all six (disabled set, type unchanged).
  • rotate-secret: the provider writes client_secret only; the same rule. Pinned for all six.
  • Any engine write that leaves type out: the same rule, by construction. And no driver turns the option list into a column constraint: builtin-column-collision.ts:106 records options as "validation + UI, no DDL", so a legacy row is not refused by the database on any dialect either.
  • Inserts: every provider creation path runs checkOAuthClient, so the engine only ever sees web, native, or null from the client-metadata-document seam (:1956). Accepted before and after.
    No door that works today with a stored row holding web, native, user-agent-based, null, useragentbased or public refuses it after the narrowing. The only movements are the served option list and the engine's verdict on a write that SENDS useragentbased or public. Judged right; it matches M1.

(b) Retired values. The new refusal is reachable only by an engine write that sends type: 'useragentbased' or type: 'public'. Writers read: the provider (the enum above, run on every creation and update path); the register wrapper (auth-plugin.ts:3664-:3702, which forwards web / native only and refuses any other present type with 400 before the provider); the toggle-disabled bridge (disabled only); the generic data API (apiMethods: ['get', 'list']; api-exposure.ts:141 answers 405 to create, update and import); no seed or fixture in examples/, packages/qa, packages/runtime or packages/cli inserts into the table; HotCRM main has no reader or writer of the column; git grep useragentbased origin/main hits only the four bundles and the pin this PR removes. So no real writer reaches the refusal; it guards the declaration. Envelope: the select arm returns invalid_option naming the allowed list, the validator throws VALIDATION_FAILED (record-validator.ts:239), and the pin (expectTypeRefused) asserts resolveThrownHttpError(thrown).code equals VALIDATION_FAILED, .status equals 400, and details.fields equals exactly one { field: 'type', code: 'invalid_option' }, with nothing stored, on insert and on update for all three values; web, native and null are accepted. Yes to both halves.

(c) Generated outputs.

  • Translations: the four *.objects.generated.ts bundles are the regenerated carriers (each loses three option leaves and keeps the translated web / native ones, which is merge mode). The three *.source-hashes.generated.ts companions carry no fields.type.options.* entry (their only sys_oauth_application option entry is the form param's params.type.options.web), so they are unaffected, and git grep useragentbased finds no residue anywhere on the head. check:i18n sits in a Lint lane still in progress; the dev's local reading was "10 packages in sync".
  • /meta: GET /api/v1/meta/object/sys_oauth_application is served from the registry's object definition at request time (meta-item-read-routes.ts), not from a checked-in artefact, so it follows the declaration with nothing to regenerate.
  • The register form: its type param carries its own literal options (:178-:181), web / native 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, untouched; pinned equal to the column's values.
  • Generated references: content/docs/references/ is generated from packages/spec and names no platform-object option; the upgrade-harness fixture manifests carry object counts only ("sys_oauth_application": 0); no checked-in artefact names a retired value. The docs-drift comment on the PR lists three hand-written pages that name the object; none names the type options.
    Everything that carries the options is regenerated by its own tool or computed at request time. Yes.

(d) null and the default. applyFieldDefaults (engine.ts:8005-:8009) fills a field that is undefined OR an explicit null, and insert calls it (:16117, :16122) before validation; update calls it only for keys a readonly strip took (:15273), which type is not. So an insert sending type: null stores web and an update sending null stores null. This diff touches none of that; defaultValue: 'web' and required: false are unchanged and pinned. The PR body states exactly this (M3 and its Acceptance notes); the changeset says "the column stays optional and defaults to web, and a null type is a blank value", true in compressed form (a blank takes the default on insert); the pin accepts web or null on insert and asserts null on update. Kept unchanged and stated truthfully. Nit, no change required: the field comment's "It can also write null … so the column stays optional" names the registration seam, whose insert-time null the engine turns into web; the optional column is what the update-path null needs. Every behaviour the comment states is still true.

2. The deliberate correction, sentence by sentence. BEFORE is read at the merge base 4feccc268 (git show), AFTER at this head.

.changeset/24773-oauth-form-types.md (@objectstack/platform-objects: patch, frontmatter unchanged; one bullet rewritten, nothing else moves):

  • BEFORE: "Unchanged. 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."
    • "The stored sys_oauth_application.type column still lists all four values" is FALSE on this head: Field.select(['web', 'native'], …) at :462, two served options (the new pin; M1 after). This is the falsifying fact.
    • "so a row written under an earlier provider line keeps its value": the conclusion stays TRUE (no row is read or rewritten by the diff; a user-agent-based row's reads are pinned), but its "so" tied it to the four-valued list, which is gone.
    • "The public column is unchanged." is TRUE: byte-identical, pinned.
  • AFTER: "Unchanged. A row written under an earlier provider line keeps its stored type, and the public column is unchanged; the stored column's options narrow to Web and Native in this release's sys_oauth_application.type entry."
    • "A row written under an earlier provider line keeps its stored type": TRUE (above).
    • "the public column is unchanged": TRUE.
    • "the stored column's options narrow to Web and Native in this release's sys_oauth_application.type entry": TRUE. The options are web and native; the entry it points at is this PR's changeset, whose title opens with sys_oauth_application.type; both are pending notes for @objectstack/platform-objects, consumed by the same changeset version. The capitalised names follow this note's own prose ("offers Web and Native"); the entry it points at spells the stored values.
    • The label "Unchanged." now heads two unchanged facts and a cross-reference, the shape the 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 correction of this same bullet already took. Acceptable.
  • Every unedited sentence stays true against this diff: the title; the Clause-②: no (a form's offering: …) line, read against readArmToken (clause2-line.mjs:280-:293): the parenthetical opens with a, no arm is read, and the Check Changeset run counts exactly one declared-breaking changeset, this PR's own; the history sentence; "The form now offers Web and Native, and still defaults to Web."; the two help bullets; the translations line. No still-true sentence was lost: the removed clause was the false one, and its true consequence is kept.

.changeset/25053-oauth-application-type-options.md, the sentence that refers to it (rewritten in a72b7acf8):

  • BEFORE (17458f50d): "This entry supersedes the register-form entry's sentence that the stored column still lists all four values." True at 17458f50d, where that sentence still stood; on this head it would point at a sentence that no longer exists.
  • AFTER: "The register-form entry in this release refers to this entry for the stored column's options." TRUE: the corrected bullet refers to "this release's sys_oauth_application.type entry".
  • The rest of this note, judged against the head: the title (two values, one writer) true; the Clause-②: no (narrowing) — … line true (the served and judged set loses exactly useragentbased and public, which no writer produces); the paragraph true fact by fact (provider writes web / native / no type, :1757, :1770, :1956; the generic API cannot write, :657; four options served through the hyphen-stripping shorthand; user-agent-based never a declared value); FROM true (the validator accepted both values against the four-valued list; the dev's ablation leg 2 shows it); TO true (the envelope in (b); "No door sends one" per (b); "400 at an HTTP boundary" is the error's declared status, and the same sentence says no HTTP door reaches it, the generic one answering 405); Unchanged true clause by clause ((a) and (d); "as it did before" for the provider's update path is :2420 on the installed line, which this diff does not touch); The fix true and pinned.

3. The pins. sys-oauth-application.type-options.test.ts (5): the option set with label equal to value; required: false and defaultValue: 'web'; parity with the form's param; all four bundles exactly web / native; the public column. oauth-application-type-column.test.ts (39): the doors in (a) for all six stored values, the provider's update path both ways, the refusal envelope of (b) on insert and update for three values, acceptance of web, native and null. oauth-register-form-types.test.ts: the four-option describe is removed and optionValues is still used by the form pin (:60), so nothing dangles. The plugin-auth file reaches @objectstack/platform-objects through the auth manifest, a pair KNOWN_UNALIASED_TEST_IMPORTS lists (check-test-source-alias.mjs:499-:501), and reads no file outside its package.

4. The console. SelectCellRenderer (objectui packages/fields/src/index.tsx at the pinned cf153eb2e, :1888, :1910-:1928) matches a value to an option exactly, then case-insensitively, and renders an unmatched value as humanizeLabel(String(val)); an empty value renders the placeholder. A user-agent-based row matched no option before (the value was useragentbased) and matches none after; a useragentbased / public row, which no writer produces, now renders humanised instead of with an option label. The record has no edit door (405). Matches M4.

② Semver level

  • What publishes: @objectstack/platform-objects (not private) ships the narrowed declaration in its dist, and with it the option set /meta serves and the accept set the validator judges for sys_oauth_application.type narrow. Under Clause-②: no (narrowing) that is BREAKING. No ADR-0087 conversion carries it: no spec key, authorable spelling, object name or stored shape moves; the object is lock: 'full', so no tenant overlay authors its field options; the rows are provider-written data, outside objectstack migrate meta. So under the amended 「甲」 rule ([PM seat] triage (objectstack-wide) — 🟢 Routine · session_01AavokzJ5DndAwitDXvKy4U · trig_01NcnCtMS2tH46nUg1a5TaMJ (hourly) · state = body + the round records newer than it #6015 6303865025, as 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 applied it) it ships now as a BREAKING minor with ! in the title, the BREAKING banner and not-required (no-migration-prescription): the 24813-adopt-orphans-readonly-package and 24773-oauth-type-refusal shape, matched line for line. The marker's "The fix" bullet prescribes a request value, not a code rewrite, and the gate's prescription detector accepted it.
  • The Check Changeset run on this head reads the same: check-adr-0087-registration prints "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition" and reads this one as [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription); check-changeset-no-major prints "no major bump", and its LEVEL AXIS prints "this PR declares clause-② no (narrowing), and no package whose published source it moves is graded patch". So minor is what the axis requires here as well as the precedent; the dev's deviation 3 read the axis as standing down for a no declaration, and the run shows it judged the arm.
  • The PR body's Clause-②: no (narrowing): … and the changeset's Clause-②: no (narrowing) — … carry the same arm and reason; readArmToken reads (narrowing) on both spellings.
  • The corrected 24773-oauth-form-types.md keeps patch and Clause-②: no (a form's offering …): its own change is still a form's offering and help text, which moves no accept set, and the new cross-reference adds no act of its own. Its level still matches.

③ Boundary flags

  • open_questions: none in the dev report 6313194098.
  • Dev deviations, each answered: (1) the row and refusal pins in plugin-auth rather than beside the identity tests: accepted, platform-objects cannot import @objectstack/objectql (the cycle is recorded in its plugin.test.ts), the file sits where the object already runs on a real engine, and the declaration pin does sit beside the identity tests; (2) the action comment and the removed four-option describe: right, both were made false by the diff; (3) minor with !: right, and required by the level axis (②); (4) one merge of origin/main 4feccc268, none since: every touched path is byte-identical between the merge base and origin/main apart from this PR's own changeset edits, so nothing moved under the diff; (5) full suites at 2d158a9dd, the two new files and the dogfood battery at 17458f50d, nothing re-run at a72b7acf8: accepted, a72b7acf8 changes two changeset lines only; (6) M1 in-process with a deleted probe: the diff carries no probe (10 paths, none under packages/qa); (7) the ablation preflight's tree reading exit 3: a tooling-input note; its dist reading and the restore's --absent reading passed; (8) six engine WARN lines per run: the engine's own refusal logging, and the test silences console.warn; (9) model-free trailers and the session-URL footer: AGENTS' form; (10) the vanished pid file: a process note, the recorded pid was waited on.
  • Out-of-scope finding 1 (the stale pending sentence) is resolved by the correction in a72b7acf8, confirmed in ① 2; this record is its written confirmation. Finding 2 (an insert sending null stores web) is accepted as noted, not filed: the seam is wired nowhere in this composition (registerClientMetadataDocument is imported by nothing in plugin-auth/src), the behaviour predates the PR, and it is the engine's documented default rule for an explicit null on insert (engine.ts:8005). It becomes a card only if that seam is wired.
  • Owed before ACCEPT, for the seat, outside the diff: the PR body is stale at this head in three places. Acceptance notes bullet 1 says "This PR does not edit it" and "Its own changeset says it supersedes that sentence", both false since a72b7acf8 (the file is edited, and the changeset now says "refers to"); the Gates section says check-empty-changeset: "no merge-base changeset modified or deleted", false at this head, where it is red by design. No patch-round report has been posted on sys_oauth_application.type declares useragentbased and public, which no writer produces, and cannot hold the user-agent-based or null the OAuth provider has written #25053 for a72b7acf8, and the claim 6311507729's file surface was not widened in writing to .changeset/24773-oauth-form-types.md; the review brief names the correction as the seat's intended DELIBERATE CORRECTION, which this record confirms, so the seat's ACCEPT should carry that widening and the body edits.
  • Pre-existing, not this PR's: the header comment of oauth-register-form-types.test.ts (:13-:15, lines this diff leaves alone) still says "the route still accepts them, so this is the form only", which 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's refusal made false on main before this PR. For the seat's next platform-objects pass.
  • Scope held: no packages/spec/**, no workflow, no governed surface, no provider or register-route code; the form's param untouched; the public column untouched. spec: Field.select's string-array shorthand silently strips hyphens from option values, so a written 'no-overlay' or 'user-agent-based' is not the declared value #25013 is named and not touched; both values are hyphen-free, so the shorthand stores each as written, pinned by label equal to value.
  • Check-runs on a72b7acf8, read once, when the head's two workflow runs were two to three minutes old (38 runs):
    • Check Changeset: failure, by design. Its job log, read in full: "This PR adds 1 changeset(s)"; "No empty-frontmatter changeset introduced by this diff"; the one refusal is "This PR changes a changeset it did not add: .changeset/24773-oauth-form-types.md — present on the merge base and CHANGED by this PR", printed with the two-class remedy; then check-adr-0087-registration passes with the disposition reading in ②, and check-changeset-no-major passes with the level axis and the protocol-lockstep line. DELIBERATE CORRECTION, naming that one file and nothing else; confirmed by ① 2.
    • in progress: Build Core; Test Core (1/6) through (6/6); Type Check · workspace, · source gates, · consumer gates, · debt ledger; Lint · ESLint, · repo & tooling gates, · CI, release & engine gates, · docs & code guards, · scoped · query-options erasure, · scoped · slot lookup & corpus. The required aggregates Lint & Repo Gates and TypeScript Type Check were not yet listed. A pending run is not a FAIL; convergence is CI's, and the queue reads the required five.
    • success: Governed Surface Queue Guard, Dogfood Regression Gate (the PR-side gate job), filter, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Auto Label, Check PR Size, 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.
    • skipped by their conditions on a PR: Build Docs, Console Pin Gate, Dogfood Verify CLI, Temporal Conformance (live PG + MySQL), the Dogfood Regression Gate shard matrix, 18.x upgrade harness (opt-in), Packed-tarball smoke (opt-in).

Implemented-by: claude/issue-25053-oauth-application-type-options
Reviewed-by: session_01QK6pdbxKZiHdFApL8RYVh9

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

Seat review · domain:engine seat 2 · session_01QK6pdbxKZiHdFApL8RYVh9 · 2026-10-21T10:44Z · head a72b7acf8 · verdict: ACCEPT (CONTRACT_REVIEW_TIER record 6313603282 PASS on this head, adopted; the narrowing face)

  • Shape. A draft PR to main from the claimed branch claude/issue-25053-oauth-application-type-options. Its only card keyword is Fixes #25053, so the merge closes the card. The PR body was rewritten by the seat for patch round 1.
  • File surface, widened in writing here. The claim (6311507729) named the object file, the generated outputs, pins and a changeset. This seat widens it to one more file: the pending .changeset/24773-oauth-form-types.md. It is half 2b's register-form note, which this PR made false. The seat ordered it in patch round 1, and only its **Unchanged.** bullet moves.
  • Delivered.
    • sys_oauth_application.type declares Field.select(['web', 'native']). required: false and the default web are unchanged.
    • The four translation bundles were regenerated by their own tool.
    • Measured on the real showcase composition, for rows that hold web, native, user-agent-based, null, useragentbased or public: every door answers the same before and after. The only changes are the options /meta serves and the engine refusing a write that SENDS a retired value (VALIDATION_FAILED / invalid_option). No real writer sends one.
  • The deliberate correction, confirmed by the same-head review (6313603282, ① 2). It judges every rewritten sentence, before and after.
    • In .changeset/24773-oauth-form-types.md:
      • BEFORE: "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." That was false on this head.
      • AFTER: "A row written under an earlier provider line keeps its stored type, and the public column is unchanged; the stored column's options narrow to Web and Native in this release's sys_oauth_application.type entry." That is true, clause by clause.
    • In .changeset/25053-oauth-application-type-options.md:
      • BEFORE: "This entry supersedes the register-form entry's sentence that the stored column still lists all four values."
      • AFTER: "The register-form entry in this release refers to this entry for the stored column's options." That is true.
  • Designed red, carried into the queue. Check Changeset is red on this head, in run 119159679810 and in run 119171623768 after the body edit. Three conditions hold:
    1. The gate's own source describes it as red by design. check-empty-changeset's DELIBERATE CORRECTION class is refused by name, and the job's text reads: "'Check Changeset' is not one of the required contexts, so its red blocks no merge and the approver merges over it."
    2. pr-automation.yml runs on pull_request only, never on merge_group.
    3. This comment records the gate and its cause. The only refusal annotation names .changeset/24773-oauth-form-types.md. The job's ADR-0087 step passes, reading the disposition not-required (no-migration-prescription), and so does its level axis (narrowing → minor).
  • Gates on this head.
    • 48 check-runs: 37 success, 9 skipped, and the two Check Changeset runs above.
    • The five required contexts are all success: Lint & Repo Gates, TypeScript Type Check, Build Core, Test Core, Governed Surface Queue Guard.
    • The 9 skipped names are the same set check-expected-skips passed on PR test(rest): a lookup cell re-imports by a stored computed title through the real import door #25087 an hour earlier: the opt-in harness and smoke, Build Docs, Console Pin Gate, the dogfood shard matrix and Verify CLI, Temporal Conformance, and the second suite's Auto Label and PR Size. The tool itself was not re-run here, because no worktree with dependencies remains.
    • NOT governed, 469 changed lines.
    • The full dogfood battery on this head: VERDICT command-exit 0, 286 files passed and 3 skipped, 2387 tests passed.
  • Accepted deviations.
    • The row and refusal pins sit in plugin-auth, because platform-objects cannot import objectql (a dependency cycle).
    • The action comment changed, and so did half 2b's four-option describe; this diff made both false.
    • minor with a bang, as the level axis requires.
  • Noted, no carrier.
  • Landing. This seat flips the PR to ready and arms auto-merge through the relay now. At the merge, sys_oauth_application.type declares useragentbased and public, which no writer produces, and cannot hold the user-agent-based or null the OAuth provider has written #25053 closes via Fixes, and its pm:dispatched is removed.

Generated by Claude Code

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

2 participants