Skip to content

Commit 73a2b52

Browse files
fix(platform-objects): the OAuth register form offers only Web and Native, and its Redirect URLs help states the rule for each type (#25011)
Part of #24773: half 2b, the Console form's part of the maintainer's ruling D (`6293344532`). Half 1 landed as #24853. Half 2a (the `plugin-auth` register wrapper forwards the type as `application_type`) landed as #24992 (`fbb42d900`), which is merged into this branch. The 19.0 refusal of `user-agent-based` / `public` stays on the card. Clause-②: no (a form's offering: the register route still accepts every type value it accepted, and nothing on the server reads the action's param options, so no accept set or served shape moves) **Cross-lane:** `packages/platform-objects` is `domain:engine`. This PR comes from the `domain:services` seat 2 under the claim's widened half-2b block (`6302570331`). **The help describes the door as #24992 left it.** The new Redirect URLs help says a Native application may use `http://localhost`. Before #24992 that was false: the route sent `type`, which the provider drops, so a Native client was judged as Web. Measured both ways on a live showcase (`pnpm dev -- --fresh`, `OS_OIDC_PROVIDER_ENABLED=1`, seeded admin), through `POST /api/v1/auth/sys-oauth-application/register`: | Registration | At `2dc07452a` (without #24992) | At `fd686be8e` (with #24992) | |:--|:--|:--| | Native + `http://localhost:4001/callback` | 400 `OAUTH_REGISTER_FAILED`, "web clients require https redirect URIs on non-loopback hosts" | 200, stored `native` | | Native + `http://127.0.0.1:4001/callback` | not run | 200, stored `native` | | Native + `http://[::1]:4001/callback` | not run | 200, stored `native` | | Web + `http://localhost:4001/callback` | 400, same refusal | 400, same refusal | | Web + `https://app.example.com/callback` | 200, stored `web` | 200, stored `web` | | `user-agent-based` + https | 200, stored `web` | 200, stored `web`, one server warning | | `public` + https | 200, stored `web` | 200, stored `web`, one server warning | ## What changes `sys_oauth_application.create_oauth_application` (Setup › OAuth Applications › Register), in `sys-oauth-application.object.ts`: | | Before | After | |:--|:--|:--| | Application Type options | Web, Native, User-agent based, Public | Web, Native (default Web, still required) | | Redirect URLs help | "One URL per line. Must use https:// in production." | "One URL per line. Web applications need https:// on a host other than localhost or another loopback address. Native applications may also use http:// on exactly localhost, 127.0.0.1 or [::1], on any port." | | Application Type help | none | "Choose Native for a desktop or mobile app, or for an app in local development that redirects to http://localhost." | - The four `objects.generated.ts` bundles were regenerated with `pnpm i18n:extract`. The zh-CN, ja-JP and es-ES values of the two help texts were translated by hand, then extract was run again: the source-hash companions end unchanged, because the new leaves are translations, not copies. - Not changed: the stored `type` select (`:449` at this head, `:425` at `4b8f597ba`), the `public` boolean column, the register route, and every other line of the file. ## Stop condition: does anything on the server refuse a value the form no longer offers? No. Measured on the same live showcase at `2dc07452a`, and the register-route rows again at `fd686be8e` (table above): - `POST /api/v1/auth/sys-oauth-application/register` with `type: 'user-agent-based'` and with `type: 'public'` (https redirect): both `200`, `success: true`. - The generic action door, `POST /api/v1/actions/sys_oauth_application/create_oauth_application` with `type: 'user-agent-based'`: `400 VALIDATION_ERROR`, "is `type: 'api'` — it dispatches on `target`, not through the action registry". The same answer for `type: 'web'` (control). The door refuses an `api` action by its type before its param contract runs (`headlessActionTypeError` in `packages/runtime/src/domains/actions.ts`, ahead of `enforceActionParams`). The MCP `run_action` door refuses it the same way (`isHeadlessInvokableAction`, `action-execution.ts`). - Source reading: the register route reads `body.type` from the raw request and never reads the object's metadata. The middleware seam hands middleware no body. Nothing else matches an action's `target`. - So only the form changes. `GET /api/v1/meta/object/sys_oauth_application` now serves the `type` param with options `["web", "native"]` and the new help texts. With `?locale=zh-CN` it serves the zh-CN texts and options `Web 应用` / `原生应用`. ## The rule the help states, measured on the provider `@better-auth/oauth-provider` 1.7.3, `validateClientRedirectUri` (`dist/authorize-9whjxVLJ.mjs:1685`). #24992's pins cover Web + `http://localhost` refused, Web + https accepted, Native + `http://localhost:4001` and `http://127.0.0.1:4001` accepted, and Native + `http://127.0.0.2:4001` and `http://app.example.com` refused. The rest of what the help says was measured live through the provider's own `POST /api/v1/auth/oauth2/create-client`, at `2dc07452a`: | application_type | redirect URI | Answer | |:--|:--|:--| | native | `http://[::1]:4001/callback` | 201, stored `native` | | native | `http://localhost:59999/callback` | 201 (any port) | | native | `http://127.0.0.1/callback` (no port) | 201 | | native | `https://app.example.com/callback` | 201 (Native also takes what Web takes) | | native | `https://localhost:4001/callback` | 400 `invalid_redirect_uri`, "native clients must not use https loopback redirect URIs" | | native | `http://127.0.0.2:4001/callback` | 400, "may use http only on the exact loopback hosts localhost, 127.0.0.1, or [::1]" | | web | `https://localhost:4001/callback` | 400, "web clients require https redirect URIs on non-loopback hosts" | | web | `https://127.0.0.1:4001/callback` | 400, same | | web | `https://app.example.com/callback` | 201 | The help does not mention Native private-use URI schemes, which the rule also allows. Nothing here measured them through the route. ## The stored columns, measured (for the 19.0 card) - **What 1.7.3 writes to `type`.** Only `web` or `native`. `checkOAuthClient` refuses any other `application_type` (`:1770`). One more case: a client-metadata-document registration that names no `application_type` stores `null` (`:1956`). Live, at `2dc07452a`: the registrations above named `user-agent-based` and `public` were stored `type: 'web'`, `token_endpoint_auth_method: 'client_secret_basic'` (8 rows read through `GET /api/v1/data/sys_oauth_application`). Again at `fd686be8e`: 6 rows, the same columns. - **What 1.7.3 writes to `public`.** Never. The client record has no such field (`OAUTH_CLIENT_RECORD_FIELD_NAMES`, `:1034`). No row carries a `public` value (8 at `2dc07452a`, 6 at `fd686be8e`). A public client is `token_endpoint_auth_method: 'none'`. - **What the stored `type` select actually declares.** The values are `web`, `native`, `useragentbased`, `public`. The column uses `Field.select`'s string-array shorthand, which snake-cases each value and strips the hyphens. So the third declared value has never been `user-agent-based`: not the value the form's action param sent, and not the one the provider's earlier lines accepted (from the SDK and dynamic registration, for public clients). A row holding `user-agent-based` is already outside the declared set today. Keeping the column four-valued in 18.x protects `public`, not `user-agent-based`. The pin below holds the column exactly as declared. Fixing it, or narrowing it, is the 19.0 card's decision. ## Tests New `packages/platform-objects/src/identity/oauth-register-form-types.test.ts`, 7 cases. It reads the object and the four bundles from source. - The `type` param is a required select, defaults to `web`, and offers exactly `web` and `native`. Every shipped locale's options are exactly `web` and `native`. - The Redirect URLs help names `https://`, `http://`, `localhost`, `127.0.0.1` and `[::1]`. The Application Type help names Native and `http://localhost`. - The en bundle mirrors the source. Every locale's help names the same scheme and hosts, and each translated locale differs from the English source. - The stored `type` select still declares its four options (labels `web`, `native`, `user-agent-based`, `public`; values as measured above). The `public` column is still a declared boolean. Results at `2dc07452a`. `packages/platform-objects` and this PR's changeset are byte-identical at `fd686be8e`: the second merge brought only #24992's three `plugin-auth` / changeset files. - `pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2`: `Test Files 83 passed (83)`, `Tests 1239 passed (1239)`. - `pnpm --filter @objectstack/platform-objects typecheck`: exit 0. `check:test-typecheck: OK`, 1 file / 3 errors / 2 pinned signatures held. No debt added. The new file is in the `tsconfig.test.json` program (`--listFilesOnly`, 1 hit). ## Ablation (one-shot; no permanent file) Each leg went through `scripts/ablation-replace.mjs`, in a runner with its own EXIT/INT/TERM trap. Each anchor hit once and fell 1 → 0, with the blob changed on disk. Each restore was verified as blob == HEAD with `git diff HEAD` empty (object `240ded11b`, zh-CN bundle `5326f33a2`). The test reads source, so no `dist` is involved. | Leg | Mutation | Result | |:--|:--|:--| | A | `{ label: 'User-agent based', value: 'user-agent-based' }` put back on the action's `type` options | 1 failed: `expected [ 'web', 'native', 'user-agent-based' ] to deeply equal [ 'web', 'native' ]` | | B | Redirect URLs help back to "Must use https:// in production." | 2 failed: the source help no longer names `http://`; the en bundle no longer mirrors the source | | C | stored `type` select narrowed to `['web', 'native']` | 1 failed: the stored-column pin | | D | zh-CN help back to its old translation | 1 failed: `zh-CN redirectURLs help ... to contain 'http://'` | | E | `public: "公共客户端"` put back in zh-CN's action options | 1 failed: `zh-CN type options: expected [ 'web', 'native', 'public' ]` | | F | Application Type help deleted | 2 failed: the source help pin and the en-mirror pin | | G | `public` column renamed `is_public` | 1 failed: `expected undefined to be 'boolean'` | | control | unmutated | 7 passed | ## Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at the final head `fd686be8e` derived 66 commands for 7 paths (208 changed lines, +185 / -23, under the 3000 human-merge threshold). Each ran as `cmd > log 2>&1; rc=$?`, after a workspace build (`turbo run build --filter=!@objectstack/docs`). - **Result.** 66/66 exit 0, among them `check:adr-0087-registration --base origin/main` ("this PR adds no declared-breaking changeset"), `check:changeset-no-major`, `check:empty-changeset`, `check:i18n` ("all bundles in sync, no undeclared authoring keys"), `check:i18n-stale-fill` ("0 stale-fill leaf/leaves"), `check:nul-bytes`, `check:doc-authoring`, `check:closing-keyword-parity`, `check:issue-citations`, `check:type-check-debt` and `check:dual-build-cjs-loads`. - **Reconciliation.** `--ran` with every exit code recorded: `Run reconciliation — 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN`. - The same 66 also ran at `2dc07452a`, before #24992 was merged in: 66/66 exit 0, reconciled the same way. - **Not run locally.** `test:repo` is CI's `Test Core`. `check:skill-frame-freshness` does not apply: no frame file is touched. `check:dev-prereqs` passed as the first step of each `pnpm dev` boot. No exported symbol was renamed or removed, so `check-platform-checklist --changed-from` does not apply. The workspace type-check lanes and the CI jobs that `dispatch-gates` lists outside the 66 are CI's. ## Acceptance notes - **For the 19.0 card.** The stored-column readings above: 1.7.3 writes `web` / `native` (or `null` for a metadata-document client) to `type` and never writes `public`. The declared third value is `useragentbased`, not `user-agent-based`. - **`Field.select`'s string shorthand strips hyphens from values** (spaces become underscores; a hyphen is dropped). Measured from built `dist`: `sys_oauth_application.type` declares `useragentbased`. `sys_metadata_audit.lock_state` (`packages/metadata-core/src/objects/sys-metadata-audit.object.ts:190`) declares `nooverlay` / `nodelete`, while its writer (`recordMetadataAudit`, `packages/metadata-protocol/src/protocol.ts`) writes `no-overlay` / `no-delete`. That column is `readonly: true`, so the record validator judges only its shape and the audit writes are not refused. Source reading only; no wrong answer was measured through a door. Reported to the seat in the dev report, not filed here. --- _Generated by [Claude Code](https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent aaee084 commit 73a2b52

7 files changed

Lines changed: 185 additions & 23 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
fix(platform-objects): the OAuth application register form offers only Web and Native, and its Redirect URLs help states the rule for each (#24773)
6+
7+
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)
8+
9+
Setup › OAuth Applications › Register offered four application types: Web, Native, User-agent based and Public. The OAuth provider judges only `web` and `native`, so the register route registers the other two as `web`, with a client secret. The form now offers Web and Native, and still defaults to Web.
10+
11+
- **Redirect URLs help.** It said "Must use https:// in production", which did not tell an admin that a local `http://localhost` callback needs the Native type. It now states the rule for each type: a Web application needs `https://` on a host other than localhost or another loopback address; a Native application may also use `http://` on exactly `localhost`, `127.0.0.1` or `[::1]`, on any port.
12+
- **Application Type help.** New: choose Native for a desktop or mobile app, or for an app in local development that redirects to `http://localhost`.
13+
- **Unchanged.** The register route accepts the same values as before, including `user-agent-based` and `public`. 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.
14+
15+
The zh-CN, ja-JP and es-ES translations carry the new help texts.

‎packages/platform-objects/src/apps/translations/en.objects.generated.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1530,15 +1530,14 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
15301530
},
15311531
redirectURLs: {
15321532
label: "Redirect URLs",
1533-
helpText: "One URL per line. Must use https:// in production."
1533+
helpText: "One URL per line. Web applications need https:// on a host other than localhost or another loopback address. Native applications may also use http:// on exactly localhost, 127.0.0.1 or [::1], on any port."
15341534
},
15351535
type: {
15361536
label: "Application Type",
1537+
helpText: "Choose Native for a desktop or mobile app, or for an app in local development that redirects to http://localhost.",
15371538
options: {
15381539
web: "Web",
1539-
native: "Native",
1540-
"user-agent-based": "User-agent based",
1541-
public: "Public"
1540+
native: "Native"
15421541
}
15431542
}
15441543
},

‎packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1530,15 +1530,14 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
15301530
},
15311531
redirectURLs: {
15321532
label: "URLs de redirección",
1533-
helpText: "Una URL por línea. Debe usar https:// en producción."
1533+
helpText: "Una URL por línea. Las aplicaciones web necesitan https:// en un host que no sea localhost ni otra dirección de loopback. Las aplicaciones nativas también pueden usar http:// exactamente en localhost, 127.0.0.1 o [::1], en cualquier puerto."
15341534
},
15351535
type: {
15361536
label: "Tipo de aplicación",
1537+
helpText: "Elija Nativa para una aplicación de escritorio o móvil, o para una aplicación en desarrollo local que redirige a http://localhost.",
15371538
options: {
15381539
web: "Web",
1539-
native: "Nativa",
1540-
"user-agent-based": "Basada en user-agent",
1541-
public: "Pública"
1540+
native: "Nativa"
15421541
}
15431542
}
15441543
},

‎packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1530,15 +1530,14 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
15301530
},
15311531
redirectURLs: {
15321532
label: "リダイレクト URL",
1533-
helpText: "1 行につき 1 つの URL。本番環境では https:// が必須です。"
1533+
helpText: "1 行につき 1 つの URL。Web アプリケーションでは、localhost やその他のループバックアドレス以外のホストで https:// を使用する必要があります。ネイティブアプリケーションでは、ホストが厳密に localhost、127.0.0.1、[::1] のいずれかであれば http:// も使用できます(ポートは任意)。"
15341534
},
15351535
type: {
15361536
label: "アプリケーションタイプ",
1537+
helpText: "デスクトップアプリやモバイルアプリ、または http://localhost にリダイレクトするローカル開発中のアプリには「ネイティブ」を選択してください。",
15371538
options: {
15381539
web: "Web",
1539-
native: "ネイティブ",
1540-
"user-agent-based": "ユーザーエージェント型",
1541-
public: "パブリック"
1540+
native: "ネイティブ"
15421541
}
15431542
}
15441543
},

‎packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1530,15 +1530,14 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
15301530
},
15311531
redirectURLs: {
15321532
label: "回调 URL",
1533-
helpText: "每行一个 URL。生产环境必须使用 https://。"
1533+
helpText: "每行一个 URL。Web 应用必须使用 https://,且主机不能是 localhost 或其他回环地址。原生应用还可以使用 http://,但主机必须恰好是 localhost、127.0.0.1 或 [::1],端口不限。"
15341534
},
15351535
type: {
15361536
label: "应用类型",
1537+
helpText: "桌面或移动应用,以及在本地开发中回调到 http://localhost 的应用,请选择“原生应用”。",
15371538
options: {
15381539
web: "Web 应用",
1539-
native: "原生应用",
1540-
"user-agent-based": "基于 User-Agent",
1541-
public: "公共客户端"
1540+
native: "原生应用"
15421541
}
15431542
}
15441543
},
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#24773] `sys_oauth_application.create_oauth_application` — the register
5+
* form offers the two application types the OAuth provider judges, and its
6+
* redirect-URL help states the provider's rule per type.
7+
*
8+
* The form posts to plugin-auth's `POST /api/v1/auth/sys-oauth-application/register`,
9+
* which forwards the chosen type as the provider's `application_type`.
10+
* `@better-auth/oauth-provider` 1.7.3 declares `web` | `native` there and
11+
* judges redirect URIs by it: web needs https on a host that is not loopback;
12+
* native may also use http on exactly `localhost`, `127.0.0.1` or `[::1]`.
13+
* The form used to offer `user-agent-based` and `public` as well. The provider
14+
* has neither, so the route registers both as `web`. The form stops offering
15+
* them; the route still accepts them, so this is the form only.
16+
*
17+
* The stored `type` column is a different surface and keeps all four options:
18+
* its narrowing waits for the major version that refuses the two values at the
19+
* route, so the pin below holds it as it is on purpose (including a declared
20+
* value that differs from the one earlier provider lines wrote — see there).
21+
*
22+
* The help text is not pinned word for word. What is pinned is what it names:
23+
* the scheme each type needs and the three native loopback hosts, in the source
24+
* and in all four shipped locales. The translated leaves are hand-written, so
25+
* `pnpm i18n:extract` keeps them in merge mode and no other gate notices a
26+
* translation of the old wording.
27+
*/
28+
29+
import { describe, expect, it } from 'vitest';
30+
import { SysOauthApplication } from './index.js';
31+
import { enObjects } from '../apps/translations/en.objects.generated.js';
32+
import { zhCNObjects } from '../apps/translations/zh-CN.objects.generated.js';
33+
import { jaJPObjects } from '../apps/translations/ja-JP.objects.generated.js';
34+
import { esESObjects } from '../apps/translations/es-ES.objects.generated.js';
35+
36+
type Param = { name?: string; type?: string; required?: boolean; defaultValue?: unknown; helpText?: string; options?: unknown[] };
37+
38+
const register = (SysOauthApplication.actions ?? []).find((a) => a.name === 'create_oauth_application');
39+
const param = (name: string): Param | undefined =>
40+
((register?.params ?? []) as Param[]).find((p) => p.name === name);
41+
const optionValues = (options: unknown[] | undefined): unknown[] =>
42+
(options ?? []).map((o) => (typeof o === 'string' ? o : (o as { value?: unknown })?.value));
43+
44+
/** The hosts a native client may use with http://, as the provider spells them. */
45+
const NATIVE_LOOPBACK_HOSTS = ['localhost', '127.0.0.1', '[::1]'] as const;
46+
47+
const LOCALES: readonly (readonly [string, any])[] = [
48+
['en', enObjects],
49+
['zh-CN', zhCNObjects],
50+
['ja-JP', jaJPObjects],
51+
['es-ES', esESObjects],
52+
];
53+
const registerNode = (bundle: any) => bundle?.sys_oauth_application?._actions?.create_oauth_application;
54+
55+
describe('[#24773] create_oauth_application — the type param offers the provider\'s two application types', () => {
56+
it('offers exactly web and native, defaulting to web', () => {
57+
expect(register, 'create_oauth_application is declared').toBeDefined();
58+
const type = param('type');
59+
expect(type?.type).toBe('select');
60+
expect(type?.required).toBe(true);
61+
expect(type?.defaultValue).toBe('web');
62+
expect(optionValues(type?.options)).toEqual(['web', 'native']);
63+
});
64+
65+
it('every shipped locale translates exactly those two options', () => {
66+
for (const [locale, bundle] of LOCALES) {
67+
const options = registerNode(bundle)?.params?.type?.options;
68+
expect(Object.keys(options ?? {}), `${locale} type options`).toEqual(['web', 'native']);
69+
}
70+
});
71+
});
72+
73+
describe('[#24773] create_oauth_application — the redirect-URL help states the rule per type', () => {
74+
it('names the scheme each type needs and the three native loopback hosts', () => {
75+
const help = param('redirectURLs')?.helpText ?? '';
76+
expect(help).toContain('https://');
77+
expect(help).toContain('http://');
78+
for (const host of NATIVE_LOOPBACK_HOSTS) {
79+
expect(help, `the help names the native loopback host ${host}`).toContain(host);
80+
}
81+
});
82+
83+
it('the type help names Native and the loopback redirect it is for', () => {
84+
const help = param('type')?.helpText ?? '';
85+
expect(help).toContain('Native');
86+
expect(help).toContain('http://localhost');
87+
});
88+
89+
it('the English bundle mirrors the source, and every locale names the hosts in its own translation', () => {
90+
expect(registerNode(enObjects)?.params?.redirectURLs?.helpText).toBe(param('redirectURLs')?.helpText);
91+
expect(registerNode(enObjects)?.params?.type?.helpText).toBe(param('type')?.helpText);
92+
for (const [locale, bundle] of LOCALES) {
93+
const node = registerNode(bundle);
94+
const help: string = node?.params?.redirectURLs?.helpText ?? '';
95+
expect(help, `${locale} redirectURLs help`).toContain('https://');
96+
expect(help, `${locale} redirectURLs help`).toContain('http://');
97+
for (const host of NATIVE_LOOPBACK_HOSTS) {
98+
expect(help, `${locale} redirectURLs help names ${host}`).toContain(host);
99+
}
100+
expect(node?.params?.type?.helpText ?? '', `${locale} type help`).toContain('http://localhost');
101+
if (locale !== 'en') {
102+
expect(help, `${locale} redirectURLs help is the English source`).not.toBe(param('redirectURLs')?.helpText);
103+
expect(node?.params?.type?.helpText, `${locale} type help is the English source`).not.toBe(param('type')?.helpText);
104+
}
105+
}
106+
});
107+
});
108+
109+
describe('[#24773] the stored columns are not narrowed in this major', () => {
110+
it('the stored `type` select still declares all four options, exactly as it is served today', () => {
111+
const stored = (SysOauthApplication.fields as Record<string, { type?: string; options?: unknown[] }>).type;
112+
expect(stored?.type).toBe('select');
113+
const options = (stored?.options ?? []) as { label?: unknown }[];
114+
expect(options.map((o) => o?.label)).toEqual(['web', 'native', 'user-agent-based', 'public']);
115+
// Measured, not intended: the column is declared through `Field.select`'s
116+
// string-array shorthand, which snake-cases each value and drops the
117+
// hyphens, so the third option's VALUE is `useragentbased`, not the
118+
// `user-agent-based` an earlier provider line wrote. Recorded for the
119+
// major version that narrows this column; this PR changes neither.
120+
expect(optionValues(stored?.options)).toEqual(['web', 'native', 'useragentbased', 'public']);
121+
});
122+
123+
it('the stored `public` column is still declared', () => {
124+
const column = (SysOauthApplication.fields as Record<string, { type?: string }>).public;
125+
expect(column?.type).toBe('boolean');
126+
});
127+
});

‎packages/platform-objects/src/identity/sys-oauth-application.object.ts‎

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,39 @@ export const SysOauthApplication = ObjectSchema.create({
147147
target: '/api/v1/auth/sys-oauth-application/register',
148148
requiresFeature: 'oidcProvider',
149149
refreshAfter: true,
150+
// [#24773] The form offers exactly the two application types the OAuth
151+
// provider judges (`@better-auth/oauth-provider` 1.7.3 declares
152+
// `application_type` as `web` | `native`), and the redirect-URL help
153+
// states that provider's redirect-URI rule per type, which the register
154+
// route forwards the type to. `user-agent-based` and `public` are no
155+
// longer offered: the provider has no such application type, so the
156+
// route registers either as `web`. The route itself still accepts both
157+
// (refusing them narrows the door, which waits for a major version), and
158+
// nothing on the server reads these options: the generic action door
159+
// refuses a `type: 'api'` action before its param contract runs, so the
160+
// route is the only judge. The stored `type` column below is a separate
161+
// surface and is not narrowed here: that waits for the same major version.
150162
params: [
151163
{ name: 'name', label: 'Application Name', type: 'text', required: true },
152-
{ name: 'redirectURLs', label: 'Redirect URLs', type: 'textarea', required: true, helpText: 'One URL per line. Must use https:// in production.' },
153-
{ name: 'type', label: 'Application Type', type: 'select', required: true, defaultValue: 'web', options: [
154-
{ label: 'Web', value: 'web' },
155-
{ label: 'Native', value: 'native' },
156-
{ label: 'User-agent based', value: 'user-agent-based' },
157-
{ label: 'Public', value: 'public' },
158-
] },
164+
{
165+
name: 'redirectURLs',
166+
label: 'Redirect URLs',
167+
type: 'textarea',
168+
required: true,
169+
helpText: 'One URL per line. Web applications need https:// on a host other than localhost or another loopback address. Native applications may also use http:// on exactly localhost, 127.0.0.1 or [::1], on any port.',
170+
},
171+
{
172+
name: 'type',
173+
label: 'Application Type',
174+
type: 'select',
175+
required: true,
176+
defaultValue: 'web',
177+
helpText: 'Choose Native for a desktop or mobile app, or for an app in local development that redirects to http://localhost.',
178+
options: [
179+
{ label: 'Web', value: 'web' },
180+
{ label: 'Native', value: 'native' },
181+
],
182+
},
159183
],
160184
resultDialog: {
161185
title: 'OAuth application registered',

0 commit comments

Comments
 (0)