Repository navigation
fix(platform-objects)!: sys_oauth_application.type declares only web and native, the two application types its writer produces - #25099
Conversation
…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>
…uth-application-type-options
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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
|
…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>
Contract reviewServed-tier: 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 ① Derived judgments1. The diff. (a) Rows that exist today: every door, read in the code and matched to the pins.
(b) Retired values. The new refusal is reachable only by an engine write that sends (c) Generated outputs.
(d) 2. The deliberate correction, sentence by sentence. BEFORE is read at the merge base
3. The pins. 4. The console. ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对Seat review ·
Generated by Claude Code |
Fixes #25053
Clause-②: no (narrowing): the declared option set that
/metaserves and the write path judges losesuseragentbasedandpublic, which no writer produces. It owes aCONTRACT_REVIEW_TIERreview before the queue.sys_oauth_application.typenow declares exactly the two application types its one writer produces,webandnative, 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 BREAKINGminorchangeset for@objectstack/platform-objects.What changes
packages/platform-objects/src/identity/sys-oauth-application.object.ts, the storedtypecolumn's declaration and two comments:a39389eb5)Field.select(['web', 'native', 'user-agent-based', 'public'], ...)Field.select(['web', 'native'], ...)GET /api/v1/meta/object/sys_oauth_application)web,native,useragentbased(labeluser-agent-based),publicweb,nativerequired/defaultValuefalse/webtypeparam already offers onlywebandnative(half 2b of OAuth app registration rejects http://localhost and http://127.0.0.1 redirect URIs as "non-loopback", with HTTP 500 #24773). It is not changed.node scripts/check-i18n-bundles.mjs --write --filter=platform-objects(translated values forwebandnativekept by merge mode).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, holdingtypeweb,native,user-agent-based,null,useragentbasedandpublic. The probe was a throwaway file inpackages/qa/dogfood/test, run once before the change (closure built ata39389eb5) and once after (platform-objectsrebuilt), and is not in this PR.GET /api/v1/meta/object/sys_oauth_application, thetypeoptionsweb,nativeGET /api/v1/data/sys_oauth_application(list)GET /api/v1/data/sys_oauth_application/:idGET /api/v1/auth/oauth2/get-clientapplication_typeis the stored value (absent fornull)POST /api/v1/data/sys_oauth_application,PATCH .../:id,POST .../importPOST /api/v1/auth/oauth2/update-client(rename only)web,native,null: 200.user-agent-based,useragentbased,public: 400invalid_client_metadatafrom the provider, row unchangedPATCH /api/v1/auth/admin/oauth2/update-clientPOST /api/v1/auth/admin/oauth2/toggle-disableddisabledset, type unchangedPOST /api/v1/auth/oauth2/client/rotate-secrettype(system context, as plugin-auth's adapter writes)web,native,null,useragentbased,publicaccepted;user-agent-basedrefusedVALIDATION_FAILED/invalid_optionweb,native,nullaccepted;user-agent-based,useragentbased,publicrefusedVALIDATION_FAILED/invalid_optionSo the only movements are the served option list and the engine's verdict on a write that sends
useragentbasedorpublic. No door sends either value: the provider sendsweb,nativeor no type, and the generic data API answers 405. H4 holds, so this isdone, notneeds_decision: no door that works today with such a row refuses it after, and no mapping ofuser-agent-basedis needed.M2: the writers (H1)
Re-measured on
origin/mainfrom source and the installed dist:@better-auth/oauth-provider1.7.3 (packages/plugins/plugin-auth/package.json, exact pin). Every creation path goes throughpersistOAuthClientRegistrationandcheckOAuthClient, which refuses anyapplication_typeother thanweb/native(dist/authorize-9whjxVLJ.mjs:1770). The default isweb, ornullfor a client-metadata-document registration that names none (:1956). Both update paths declareapplication_typeasz.enum(["web", "native"])(:2885,:2923) and re-runcheckOAuthClientover the stored client merged with the update (:2420), which is the 400 in M1.applicationType: 'type'inauth-schema-config.ts:489. The generic data API:apiMethods: ['get', 'list'](sys-oauth-application.object.ts:657), measured as 405 above.registerClientMetadataDocument) is exported only from@better-auth/oauth-provider/internal, and nothing inplugin-auth/srcimports it (grep -rfor the function and the subpath: 0 hits), so thenullwriter is not reachable in this composition (see Acceptance notes).M3:
nullandrequired(H3)typeisrequired: falsetoday (M1's/metareading) and stays so. Anullrow reads and lists (M1), and an engine update that sendsnullis accepted and storesnull, before and after. The new pin assertsrequired: false.M4: the console
Read from objectui at the pinned
.objectui-shacf153eb2e: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. Auser-agent-basedrow matched no option before (the option value wasuseragentbased) and matches none after, so it renders the same. Only auseragentbasedorpublicrow, 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 exactlywebandnative, each value equal to its label;required: falseand defaultweb; the register form offers the same values; all four bundles translate exactly those two; thepubliccolumn 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:web,native,null,user-agent-based,useragentbased,publicreads and lists with its stored value, and the provider'sgetOAuthClientreturns it;disabledwrite and the provider'srotateClientSecretare accepted on every such row, the type unchanged;updateOAuthClientrefuses a rename that leaves an undeclared type in place (invalid_client_metadata, row unchanged), and accepts one that also setsapplication_type: 'native', storingnative(the remedy the changeset names);user-agent-based,useragentbasedorpublicis refused:resolveThrownHttpErrorgivesstatus400 andcodeVALIDATION_FAILED,details.fieldsholds oneinvalid_optionontype, nothing is written;web,nativeandnullare accepted.plugin-authrather than beside theplatform-objectsidentity tests becauseplatform-objectscannot import@objectstack/objectql, which depends on it (plugin.test.tsrecords the cycle). The new file importsplatform-objectsthrough itsdist, a pair already inKNOWN_UNALIASED_TEST_IMPORTS.Results (os-verify-lock
VERDICT command-exit 0on each):platform-objectsat2d158a9dd: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 thetsconfig.test.jsonprogram (--listFilesOnly: 1 hit).plugin-authat2d158a9dd: the full suiteTest 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).origin/main4feccc268(head17458f50d): the ablation's control legs below re-ran both new files green (5/5, 39/39), andcheck:i18nstayed in sync.Ablation (one-shot, no permanent file)
One leg restored the retired options:
scripts/ablation-replace.mjs --holdreplaced the declaration withField.select(['web', 'native', 'user-agent-based', 'public'], ...)(anchor 1 to 0, blob2f185ac16441to1af41cbfc3ae), 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.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), thepubliccolumn.platform-objectsrebuilt;ablation-dist-preflight.mjsfound the marker in 4 built files (its dist reading passed; its tree reading refused because the dist spelling uses double quotes and no--source-markerwas passed, exit 3). Thenoauth-application-type-column.test.ts:Tests 4 failed | 35 passed (39). Red: insert and update withuseragentbasedand withpublic, accepted again.user-agent-basedstayed refused, because the shorthand still strips it.2f185ac16441equalsHEAD,git diff HEADempty; rebuilt; the preflight--absentfound the marker in none of 66 built files and the tree clean; both files green (5/5, 39/39);git status --porcelainempty.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat the final head17458f50dderived 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), aseval cmd > log 2>&1; status=$?, recorded byte for byte with:: exit N.--ran:Run reconciliation — 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN.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: nomajorbump.check-empty-changeset: no merge-base changeset modified or deleted at17458f50d; ata72b7acf8it 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.check-changeset-fixed,check:auth-mount-ledger,check:authz-resolver,check:error-code-casing,check:filter-alias-parity, all exit 0.test:repo(CI'sTest Core); the type-check lanes and CI-only jobs the derivation lists outside the 69;check:dev-prereqsandcheck:skill-frame-freshness(nopackages/specbuild input or frame file touched);check-platform-checklist --changed-from(no exported symbol renamed or removed).Patch round 1 (
a72b7acf8, two.changesetlines only): the same 69 commands re-derived and re-run after a workspace build; 68 exit 0 andcheck-empty-changeset --base origin/mainexits 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 head17458f50d, as CI spells it: the closure built (turbo run build --filter=!@objectstack/docs, 76 tasks, a superset of@objectstack/dogfood^...), thenpnpm --filter @objectstack/dogfood testunder the lock, with noOS_TEST_SHARDand 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.Dogfood Regression Gatedoes not run on a PR or in the merge queue, so this is the only pre-merge reading of it.The head merges
origin/main4feccc268.mainhas since moved to1862d8d13(two commits inpackages/spec,driver-tursoandservice-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 mergesorigin/main4feccc268;mainis at8f59304dd, with no path of this diff moved.Acceptance notes
a72b7acf8)..changeset/24773-oauth-form-types.md(half 2b, still pending) said: "The storedsys_oauth_application.typecolumn still lists all four values, so a row written under an earlier provider line keeps its value. Thepubliccolumn is unchanged." That bullet now reads: "A row written under an earlier provider line keeps its stored type, and thepubliccolumn is unchanged; the stored column's options narrow to Web and Native in this release'ssys_oauth_application.typeentry." 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 Changesetis 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.nullthe provider sends on insert is stored asweb. Measured in M1: an engine insert withtype: nullstoresweb(the declared default fills the blank), while an update that sendsnullstoresnull. The provider sendsnullonly for a client-metadata-document registration, whose seam this composition does not wire. Unchanged by this PR; noted, not filed.user-agent-based(or any type other thanweb/native) is refused by the provider's own update path unless the update also setsapplication_type, before and after this change (M1, and pinned). No deployment's data is in reach to count such rows.Generated by Claude Code