Skip to content

Commit 2847684

Browse files
committed
Merge origin/main into claude/issue-25096-better-auth-1-7-12
Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
2 parents 89b9aff + ba244f1 commit 2847684

15 files changed

Lines changed: 482 additions & 56 deletions

‎.changeset/24773-oauth-form-types.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ Setup › OAuth Applications › Register offered four application types: Web, N
1010

1111
- **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.
1212
- **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 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.
13+
- **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.
1414

1515
The zh-CN, ja-JP and es-ES translations carry the new help texts.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): `ApprovalRequestRow`'s display labels are documented as the reader's-locale label
6+
7+
Clause-②: no (ledger and docblock text only: no shape, key, default, accepted value or export moves)
8+
9+
The `ApprovalRequestRow` docblocks for `process_label`, `step_label`, `current_step_label` and `object_label` named the authored or schema label. Since plugin-approvals serves those members in the reader's locale, they now say so: the served value is the translation picked from `flows.<flow>.label`, `flows.<flow>.approvals.<node_id>.label` or `objects.<object>.label` in the read's `ExecutionContext.locale`, falling back to the stored snapshot (or the object's schema label). The stored `sys_approval_request` columns are unchanged. The `flows.approvals` translation liveness rows read `live`. No type, key or export changes.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/platform-objects': minor
3+
---
4+
5+
fix(platform-objects)!: `sys_oauth_application.type` declares only `web` and `native`, the two application types its writer produces
6+
7+
Clause-②: no (narrowing) — the stored column's declared options, which `GET /api/v1/meta/object/sys_oauth_application` serves and the record validator judges a write against, lose `useragentbased` and `public`, which no writer produces.
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling or stored shape is added, removed, renamed or re-shaped, and no exported declaration is removed or narrowed, so there is nothing for `objectstack migrate meta` to rewrite. The one definition that changes is the option list of one field on `sys_oauth_application`, a platform object this package ships and owns, and what narrows is the set of values a write to that object's rows may send. Those rows are OAuth client registrations the OAuth provider writes, not metadata, and no ADR-0087 conversion reaches a stored data row. No writer sends either retired value, no stored row is rewritten, and a row holding any value reads and takes every write that leaves `type` out. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
10+
11+
**BREAKING** (a platform object's declared option set narrows), shipped as `minor` under the launch-window convention for breaking changes.
12+
13+
The column's only writer is the OAuth provider (`@better-auth/oauth-provider`), whose `application_type` plugin-auth maps onto `type`. The provider writes `web` or `native` on every creation and update path, or no type for a client-metadata-document client that names none. The generic data API cannot write the table. The column declared four options, and the string shorthand that declared them strips hyphens, so it served `web`, `native`, `useragentbased` and `public`. No writer produces the last two, and the `user-agent-based` an earlier provider line stored was never one of them.
14+
15+
- **FROM:** the `type` field of `sys_oauth_application` declared and served the options `web`, `native`, `useragentbased` and `public`. A write that sent `useragentbased` or `public` was accepted and stored.
16+
- **TO:** it declares and serves `web` and `native`. A write that sends any other value, including `useragentbased` and `public`, is refused with `VALIDATION_FAILED` (`400` at an HTTP boundary) and an `invalid_option` finding on `type`. No door sends one: the provider sends only `web`, `native` or no type, and the generic data API answers `405` for every write to this table.
17+
- **Unchanged:** the column stays optional and defaults to `web`, and a `null` type is a blank value. No stored row is rewritten. A row holding any type, `user-agent-based` included, still reads and lists through the data API and the provider. It takes every write that leaves `type` out, such as a secret rotation or enabling and disabling the application. The provider's own update path still refuses a row whose type is not `web` or `native`, as it did before. The register-form entry in this release refers to this entry for the stored column's options.
18+
- **The fix:** nothing to change for a client the provider registered. A row an earlier provider line stored as `user-agent-based` keeps that value. To edit it through the provider's update path, set `application_type` (`web` or `native`) in the same update.

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,9 +1389,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
13891389
label: "Client Type",
13901390
options: {
13911391
web: "web",
1392-
native: "native",
1393-
useragentbased: "user-agent-based",
1394-
public: "public"
1392+
native: "native"
13951393
}
13961394
},
13971395
public: {

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,9 +1389,7 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
13891389
label: "Tipo de cliente",
13901390
options: {
13911391
web: "Web",
1392-
native: "Nativa",
1393-
useragentbased: "Basada en agente de usuario",
1394-
public: "Público"
1392+
native: "Nativa"
13951393
}
13961394
},
13971395
public: {

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,9 +1389,7 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
13891389
label: "クライアントタイプ",
13901390
options: {
13911391
web: "Web",
1392-
native: "ネイティブ",
1393-
useragentbased: "ユーザーエージェントベース",
1394-
public: "公開"
1392+
native: "ネイティブ"
13951393
}
13961394
},
13971395
public: {

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,9 +1389,7 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
13891389
label: "客户端类型",
13901390
options: {
13911391
web: "Web 应用",
1392-
native: "原生应用",
1393-
useragentbased: "基于用户代理",
1394-
public: "公开"
1392+
native: "原生应用"
13951393
}
13961394
},
13971395
public: {

‎packages/platform-objects/src/identity/oauth-register-form-types.test.ts‎

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,8 @@
1414
* has neither, so the route registers both as `web`. The form stops offering
1515
* them; the route still accepts them, so this is the form only.
1616
*
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).
17+
* The stored `type` column is a different surface. It declares the same two
18+
* values since #25053, and `sys-oauth-application.type-options.test.ts` pins it.
2119
*
2220
* The help text is not pinned word for word. What is pinned is what it names:
2321
* the scheme each type needs and the three native loopback hosts, in the source
@@ -105,23 +103,3 @@ describe('[#24773] create_oauth_application — the redirect-URL help states the
105103
}
106104
});
107105
});
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: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,9 @@ export const SysOauthApplication = ObjectSchema.create({
156156
// 400 before the provider runs; no type (absent or null) registers `web`.
157157
// Nothing on the server reads these options: the generic action door
158158
// refuses a `type: 'api'` action before its param contract runs. The
159-
// stored `type` column below is not narrowed: 1.7.3 writes only `web` or
160-
// `native` (null for a client-metadata-document client), an earlier
161-
// provider line could write `user-agent-based`, and no in-tree reader branches on it.
159+
// stored `type` column below declares the same two values [#25053]:
160+
// 1.7.3 writes only `web` or `native` (null for a client-metadata-document
161+
// client), and a row an earlier provider line stored as `user-agent-based` still reads.
162162
params: [
163163
{ name: 'name', label: 'Application Name', type: 'text', required: true },
164164
{
@@ -446,7 +446,20 @@ export const SysOauthApplication = ObjectSchema.create({
446446
// (#3002). The column keeps its name and its data; the rename is absorbed
447447
// by the `applicationType: 'type'` mapping in plugin-auth's
448448
// `auth-schema-config.ts`.
449-
type: Field.select(['web', 'native', 'user-agent-based', 'public'], {
449+
//
450+
// [#25053, ADR-0049 enforce-or-remove] The options are exactly the values
451+
// the column's one writer produces. `@better-auth/oauth-provider` 1.7.3
452+
// writes `application_type` `web` or `native` on every creation and
453+
// update path, and the generic data API cannot write this table
454+
// (`apiMethods` is `get` / `list`). It can also write `null` (a
455+
// client-metadata-document client that names no type), so the column
456+
// stays optional. The former `useragentbased` and `public` options had no
457+
// writer. Rows are not rewritten: a row an earlier provider line stored as
458+
// `user-agent-based` (never a declared value, because the string shorthand
459+
// strips hyphens) still reads and lists, and takes every write that leaves
460+
// `type` out, because the record validator judges only the keys a write
461+
// sends. Neither value has a hyphen, so the shorthand stores each as written.
462+
type: Field.select(['web', 'native'], {
450463
label: 'Client Type',
451464
required: false,
452465
defaultValue: 'web',
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25053] `sys_oauth_application.type` declares exactly the application types
5+
* its one writer produces: `web` and `native`.
6+
*
7+
* The column's only writer is `@better-auth/oauth-provider`, whose
8+
* `applicationType` plugin-auth maps onto `type`. 1.7.3 writes
9+
* `application_type` `web` or `native` on every creation and update path, and
10+
* the generic data API cannot write this table (`apiMethods` is `get` /
11+
* `list`). The column used to declare four options through the string
12+
* shorthand, which strips hyphens, so it served `web`, `native`,
13+
* `useragentbased` and `public`. No writer produces the last two (ADR-0049
14+
* enforce-or-remove), and the hyphenated `user-agent-based` an earlier
15+
* provider line wrote was never one of them.
16+
*
17+
* What the narrowed declaration does to stored rows is pinned where a real
18+
* engine runs, in plugin-auth's `oauth-application-type-column.test.ts`. This
19+
* package cannot import `@objectstack/objectql`, which depends on it.
20+
*/
21+
22+
import { describe, expect, it } from 'vitest';
23+
import { SysOauthApplication } from './index.js';
24+
import { enObjects } from '../apps/translations/en.objects.generated.js';
25+
import { zhCNObjects } from '../apps/translations/zh-CN.objects.generated.js';
26+
import { jaJPObjects } from '../apps/translations/ja-JP.objects.generated.js';
27+
import { esESObjects } from '../apps/translations/es-ES.objects.generated.js';
28+
29+
type Option = { label?: unknown; value?: unknown };
30+
type Column = { type?: string; required?: boolean; defaultValue?: unknown; options?: Option[] };
31+
32+
const fields = SysOauthApplication.fields as Record<string, Column>;
33+
const column = fields.type;
34+
const values = (options: unknown[] | undefined): unknown[] =>
35+
(options ?? []).map((o) => (typeof o === 'string' ? o : (o as Option)?.value));
36+
37+
const LOCALES: readonly (readonly [string, any])[] = [
38+
['en', enObjects],
39+
['zh-CN', zhCNObjects],
40+
['ja-JP', jaJPObjects],
41+
['es-ES', esESObjects],
42+
];
43+
44+
describe('[#25053] sys_oauth_application.type — the declared options are what the provider writes', () => {
45+
it('declares exactly web and native, each value as written', () => {
46+
expect(column?.type).toBe('select');
47+
expect(values(column?.options)).toEqual(['web', 'native']);
48+
// The string shorthand keeps the written string as the label and derives
49+
// the value from it; equal pairs say the derivation changed nothing.
50+
for (const option of column?.options ?? []) {
51+
expect(option.value, `option ${String(option.label)}`).toBe(option.label);
52+
}
53+
});
54+
55+
it('stays optional, with web as its default', () => {
56+
// The provider can write null (a client-metadata-document client that
57+
// names no type): a required column would make that a missing value.
58+
expect(column?.required).toBe(false);
59+
expect(column?.defaultValue).toBe('web');
60+
});
61+
62+
it('declares the same values the register form offers', () => {
63+
const register = (SysOauthApplication.actions ?? []).find((a) => a.name === 'create_oauth_application');
64+
const param = ((register?.params ?? []) as { name?: string; options?: unknown[] }[]).find((p) => p.name === 'type');
65+
expect(values(param?.options)).toEqual(values(column?.options));
66+
});
67+
68+
it('every shipped locale translates exactly those two options', () => {
69+
for (const [locale, bundle] of LOCALES) {
70+
const options = bundle?.sys_oauth_application?.fields?.type?.options;
71+
expect(Object.keys(options ?? {}), `${locale} type options`).toEqual(['web', 'native']);
72+
}
73+
});
74+
75+
it('keeps the `public` column', () => {
76+
expect(fields.public?.type).toBe('boolean');
77+
});
78+
});

0 commit comments

Comments
 (0)