Repository navigation
Commit ba244f1
fix(platform-objects)!: sys_oauth_application.type declares only web and native, the two application types its writer produces (#25099)
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 |
- The values are spelled so that the string shorthand's hyphen stripping
(#25013, still open, not touched here) cannot change them: neither has a
hyphen, and the new pin asserts each option's value equals its label.
- The register form's own `type` param already offers only `web` and
`native` (half 2b of #24773). It is not changed.
- The action's code comment said the stored column "is not narrowed". It
now says the column declares the same two values. The field gains a
comment stating the writer readings and the legacy-row behaviour below.
- The four translation bundles drop the two option keys, regenerated by
`node scripts/check-i18n-bundles.mjs --write --filter=platform-objects`
(translated values for `web` and `native` kept 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, 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.
- #25013 (the shorthand that strips hyphens) remains open; this PR only
spells its values so the shorthand cannot change them.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QK6pdbxKZiHdFApL8RYVh9)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 69712d5 commit ba244f1
10 files changed
Lines changed: 428 additions & 41 deletions
File tree
- .changeset
- packages
- platform-objects/src
- apps/translations
- identity
- plugins/plugin-auth/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | | - | |
| 13 | + | |
14 | 14 | | |
15 | 15 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1389 | 1389 | | |
1390 | 1390 | | |
1391 | 1391 | | |
1392 | | - | |
1393 | | - | |
1394 | | - | |
| 1392 | + | |
1395 | 1393 | | |
1396 | 1394 | | |
1397 | 1395 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1389 | 1389 | | |
1390 | 1390 | | |
1391 | 1391 | | |
1392 | | - | |
1393 | | - | |
1394 | | - | |
| 1392 | + | |
1395 | 1393 | | |
1396 | 1394 | | |
1397 | 1395 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1389 | 1389 | | |
1390 | 1390 | | |
1391 | 1391 | | |
1392 | | - | |
1393 | | - | |
1394 | | - | |
| 1392 | + | |
1395 | 1393 | | |
1396 | 1394 | | |
1397 | 1395 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1389 | 1389 | | |
1390 | 1390 | | |
1391 | 1391 | | |
1392 | | - | |
1393 | | - | |
1394 | | - | |
| 1392 | + | |
1395 | 1393 | | |
1396 | 1394 | | |
1397 | 1395 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
17 | | - | |
18 | | - | |
19 | | - | |
20 | | - | |
| 17 | + | |
| 18 | + | |
21 | 19 | | |
22 | 20 | | |
23 | 21 | | |
| |||
105 | 103 | | |
106 | 104 | | |
107 | 105 | | |
108 | | - | |
109 | | - | |
110 | | - | |
111 | | - | |
112 | | - | |
113 | | - | |
114 | | - | |
115 | | - | |
116 | | - | |
117 | | - | |
118 | | - | |
119 | | - | |
120 | | - | |
121 | | - | |
122 | | - | |
123 | | - | |
124 | | - | |
125 | | - | |
126 | | - | |
127 | | - | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
156 | 156 | | |
157 | 157 | | |
158 | 158 | | |
159 | | - | |
160 | | - | |
161 | | - | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
162 | 162 | | |
163 | 163 | | |
164 | 164 | | |
| |||
446 | 446 | | |
447 | 447 | | |
448 | 448 | | |
449 | | - | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
450 | 463 | | |
451 | 464 | | |
452 | 465 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
0 commit comments