Skip to content

Commit 03520eb

Browse files
authored
deps(auth): move the better-auth family off the 1.7.0-rc.2 prerelease onto stable ^1.7.1 (#9869)
* deps(auth): move the better-auth family off 1.7.0-rc.2 onto the stable ^1.7.1 line Stable 1.7.x has shipped: `npm view <pkg> dist-tags` reports latest 1.7.1 for better-auth, @better-auth/core, oauth-provider, sso, the five adapters and telemetry. The prerelease pin was the remediation for GHSA-p2fr-6hmx-4528 and GHSA-j8v8-g9cx-5qf4 (patched only in >=1.7.0-beta.4), so it could not be dropped until a stable line above that floor existed. It now does. - pnpm-workspace.yaml overrides move to `^1.7.1`, with the selector bounds reshaped to the MAJOR boundary (`pkg@<2.0.0`) so a future advisory lift moves only the target — the shape the file's own header mandates. This retires the better-auth carve-out in the drift note. - plugin-auth's direct declarations move to `^1.7.1` in lockstep (overrides do not ship with published packages; check:override-consistency holds them). - @better-auth/scim stays at 1.7.0-rc.1. Measured against the published 1.7.1 tarball, stable ships the rc.2 REWRITE — no scimProvider model, no generate-token endpoint, and all six new models present — so moving it is the ADR-0071 migration tracked by #3653, not this bump. rc.1 is above the advisory fix floor and its peer ranges accept the stable 1.7.1 core. Refs #3002 * fix(auth): absorb the rc.2 → stable 1.7 vendor drift on the auth surface Moving to stable 1.7.1 surfaced three pieces of upstream drift that the pinned rc.2 did not have. All three were caught by the repo's own drift gates, and all three are mechanical parity — no behaviour is authored here. 1. THE ACCOUNT ID FIELD FLIP-FLOPPED. `1.7.0-rc.2` renamed `account.accountId` → `providerAccountId`; stable 1.7.0/1.7.1 renamed it BACK to `accountId`, keeping the new required `issuer`. Measured on the installed 1.7.1: `getAuthTables({}).account.fields` = `issuer, accountId, providerId, userId, …` with no `providerAccountId` at all. Carrying the rc.2 spelling left `accountId` unmapped, so the adapter asked for a column of that name and EVERY sign-up answered 500 `Unknown field 'accountId' on object 'sys_account'`. The `account_id` column is unchanged and no data moves. Fixed in the schema mapping, in the `createAccount` call in admin-user-endpoints, in the two mapping pin tests, and in the client's `/list-accounts` response type. 2. `@better-auth/oauth-provider` 1.7.1's `oauthClient` model writes three fields the platform object did not answer for: `applicationType` (the OIDC spelling of what rc.2 called `type` — mapped onto the EXISTING `type` column, so no data moves), plus genuinely new `clientDiscoveryId` and `clientCredentialsScopes`, now declared on `sys_oauth_application`. Without these, `POST /oauth2/register` 500s at the driver. 3. Two new endpoints are mounted publicly by the catch-all: `POST /oauth2/end-session` and `POST /oauth2/end-session/confirm` — the POST form of OIDC RP-initiated logout, whose GET counterpart was already ledgered. Added to BETTER_AUTH_MOUNTED_SURFACE with that rationale. Also re-points the #5024 prerelease-pin probe at the state this leaves behind: `@better-auth/scim` is the only prerelease pin left, so its follow-up ledger names #3653 (the ADR-0071 migration) alone rather than a card this PR closes. plugin-auth: 55 files, 1263 tests, all passing. tsc --noEmit clean. Refs #3002 * chore(i18n): regenerate platform-objects bundles for the two new sys_oauth_application columns Generated output, not hand-edited: node scripts/check-i18n-bundles.mjs --write. `pnpm check:i18n` is green again (9 packages, all bundles in sync). Refs #3002 * fix(showcase): the demo-persona seeder writes the stable 1.7 account id key Second `internalAdapter.createAccount` call site carrying the rc.2 `providerAccountId` spelling — found by the dogfood suite, not by grep: `showcase-demo-personas-loginable.dogfood.test.ts` failed with phone.demo@example.com holds a credential account: expected undefined to be truthy verify signIn failed: 401 {"code":"INVALID_EMAIL_OR_PASSWORD"} The account row was written with no account id, so better-auth's `findAccountByKey({ issuer, accountId })` could not see it and both demo personas were silently un-loginable — exactly the failure mode #9308 fixture 1 exists to prevent, and exactly the shape its own comment warns about (a misleading "User not found" pointing at the sys_user row rather than the account). The admin persona was unaffected, which is why only the persona test caught it. Also adds the changeset for the family bump, the drift it absorbs, and the one consumer-visible rename (`@objectstack/client`'s `/list-accounts` response type), and lowers plugin-auth's TEST_DEBT ceiling 111 → 110 to the number this branch now measures, as `check:type-check-debt` prescribes. Verified: showcase-demo-personas-loginable dogfood 4/4 pass; the auth-path dogfood set 57/57; app-showcase 337 tests + tsc clean. Refs #3002 * docs(auth): the account-mapping section names the stable 1.7 field, with the flip-flop stated content/docs/permissions/authentication.mdx documented the mapping as `providerAccountId: 'account_id'` and told the reader that is "the field formerly called accountId". On stable 1.7 that is backwards, and a reader following it writes exactly the mapping that answers 500 on every sign-up. The field name moved twice inside the 1.7 line — rc.2 renamed `accountId` → `providerAccountId`, stable 1.7.0 renamed it back — so the correction carries a callout saying so rather than silently swapping the word: the next reader arriving from an rc-era note needs to know which way it went. content/docs/releases/ is left alone by rule; its v15/v17 entries are accurate records of what those releases did. Refs #3002
1 parent b40fe54 commit 03520eb

25 files changed

Lines changed: 437 additions & 218 deletions
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
"@objectstack/plugin-auth": patch
3+
"@objectstack/platform-objects": patch
4+
"@objectstack/client": patch
5+
---
6+
7+
deps(auth): the better-auth family moves off the `1.7.0-rc.2` prerelease onto stable `^1.7.1` (#3002)
8+
9+
`@objectstack/plugin-auth` shipped with **exact pins on a release candidate**
10+
`better-auth`, `@better-auth/core`, `@better-auth/oauth-provider` and
11+
`@better-auth/sso` all at `1.7.0-rc.2`. That pin was never housekeeping debt: it was
12+
the remediation for **GHSA-p2fr-6hmx-4528** (`@better-auth/oauth-provider`) and
13+
**GHSA-j8v8-g9cx-5qf4** (`@better-auth/scim`, high — account/provider takeover), both
14+
patched only in `>=1.7.0-beta.4`, so there was no stable line to move to. Upstream has
15+
now shipped one: `npm view <pkg> dist-tags` reports `latest: 1.7.1` for every family
16+
member. The declarations become `^1.7.1`, which is what a downstream
17+
`npx create-objectstack` install now resolves.
18+
19+
**`@better-auth/scim` deliberately stays at `1.7.0-rc.1`.** Measured against the
20+
published stable tarball rather than assumed: `@better-auth/scim@1.7.1` ships the rc.2
21+
**rewrite** — no `scimProvider` model, no generate-token endpoint, and six replacement
22+
models (`scimUser`, `scimGroup`, `scimGroupMember`, `scimSubject`,
23+
`scimConnectionBinding`, `scimIdentityTombstone`). Adopting it is a feature migration
24+
(ADR-0071, tracked separately), not a version bump. The hold stays security-clean: rc.1
25+
is above the advisory's fix floor, `pnpm audit --audit-level=high` is green, and rc.1's
26+
peer ranges accept the stable 1.7.1 core the rest of the family resolves to.
27+
28+
**Three pieces of upstream drift are absorbed here, and one of them was a live
29+
sign-in outage waiting to happen.**
30+
31+
`1.7.0-rc.2` renamed the account model's `accountId` field to `providerAccountId`;
32+
**stable 1.7.0/1.7.1 renamed it back to `accountId`**, keeping the new required
33+
`issuer`. Carrying the rc.2 spelling into the stable line left the field unmapped, so
34+
better-auth's adapter asked for a column named `accountId` and **every sign-up answered
35+
500**`Unknown field 'accountId' on object 'sys_account'`. The `account_id` column
36+
itself never changed and no data moves; only the camelCase key does. The same rename
37+
reaches `@objectstack/client`: `auth.accounts.list()` (better-auth's `/list-accounts`)
38+
returns `accountId`, and its declared response type said `providerAccountId`. If you
39+
read that field off the client's typed response, rename it.
40+
41+
`@better-auth/oauth-provider` 1.7.1's client model writes three fields the platform
42+
object did not answer for. `applicationType` is the OIDC spelling of what rc.2 called
43+
`type`, so it maps onto the **existing** `type` column and no data moves;
44+
`clientDiscoveryId` and `clientCredentialsScopes` are genuinely new and are now
45+
declared on `sys_oauth_application` as `client_discovery_id` and
46+
`client_credentials_scopes`. Without them, dynamic client registration
47+
(`POST /oauth2/register`) fails at the driver.
48+
49+
Two endpoints are newly mounted by the auth catch-all and are now ledgered:
50+
`POST /oauth2/end-session` and `POST /oauth2/end-session/confirm` — the POST form of
51+
OIDC RP-initiated logout, whose `GET` counterpart was already published.
52+
53+
**Nothing here needs an action on upgrade.** The new columns are additive and optional,
54+
and the field rename is internal to how the plugin talks to better-auth — with the one
55+
exception of the `@objectstack/client` response type named above.

content/docs/permissions/authentication.mdx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,17 +1048,26 @@ The plugin bridges this gap using better-auth's official **`modelName` / `fields
10481048
// Declared in the betterAuth() config via AUTH_*_CONFIG constants:
10491049
user: { modelName: 'sys_user', fields: { emailVerified: 'email_verified', … } },
10501050
session: { modelName: 'sys_session', fields: { userId: 'user_id', expiresAt: 'expires_at', … } },
1051-
account: { modelName: 'sys_account', fields: { providerId: 'provider_id', issuer: 'issuer', providerAccountId: 'account_id', … } },
1051+
account: { modelName: 'sys_account', fields: { providerId: 'provider_id', issuer: 'issuer', accountId: 'account_id', … } },
10521052
verification: { modelName: 'sys_verification', fields: { expiresAt: 'expires_at', … } },
10531053
```
10541054

1055-
better-auth 1.7 identifies an account by `(issuer, providerAccountId)``providerAccountId` is
1056-
the field formerly called `accountId` (same `account_id` column) and `issuer` names the authority
1057-
that vouched for it: an OIDC `iss` for federated logins, or a synthetic `local:credential` /
1055+
better-auth 1.7 identifies an account by `(issuer, accountId)`. `issuer` names the authority
1056+
that vouched for the id: an OIDC `iss` for federated logins, or a synthetic `local:credential` /
10581057
`local:oauth:<providerId>` for providers that carry none. Rows written before 1.7 have no issuer,
10591058
so the auth plugin stamps them once at boot; accounts from a federated IdP that is no longer
10601059
registered cannot be derived and are reported in the boot log instead of guessed.
10611060

1061+
<Callout type="warn">
1062+
The account id field's NAME changed twice inside the 1.7 line, so read it off the version you
1063+
run rather than off an older note. The `1.7.0-rc.2` pre-release renamed `accountId`
1064+
`providerAccountId`; the stable `1.7.0` / `1.7.1` releases renamed it **back to `accountId`**,
1065+
keeping the new `issuer`. The `account_id` column is the same throughout — only the camelCase
1066+
key moved. On stable 1.7 the mapping above, and any `internalAdapter.createAccount({ …,
1067+
accountId })` call, must use `accountId`; the rc.2 spelling leaves the field unmapped and every
1068+
sign-up answers 500 `Unknown field 'accountId' on object 'sys_account'`.
1069+
</Callout>
1070+
10621071
The ObjectQL adapter factory (`createObjectQLAdapterFactory`) then uses better-auth's `createAdapterFactory`
10631072
which automatically transforms all data and where-clauses using these mappings — no manual
10641073
camelCase ↔ snake_case conversion is needed in the adapter.

examples/app-showcase/src/security/demo-personas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
* distinct people, a submitter who is not an approver, an out-of-office
4141
* delegation decided under the delegate's own identity — was stuck on it, and
4242
* each rediscovered the same non-obvious cause: a password hash is not enough.
43-
* better-auth 1.7 keys accounts on `(issuer, providerAccountId)`, so a
43+
* better-auth 1.7 keys accounts on `(issuer, accountId)`, so a
4444
* credential row whose `issuer` is not the local credential issuer is INVISIBLE
4545
* to sign-in, which then fails `INVALID_EMAIL_OR_PASSWORD` behind a misleading
4646
* "User not found" — pointing at the row, which is fine, instead of at the

examples/app-showcase/src/security/seed-approval-demo.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,13 @@ interface AuthContextLike {
9292
userId: string;
9393
providerId: string;
9494
issuer: string;
95-
providerAccountId: string;
95+
/**
96+
* The STABLE better-auth 1.7 spelling. `1.7.0-rc.2` briefly called this
97+
* `providerAccountId` and stable 1.7.0 renamed it back (#3002) — under
98+
* the rc.2 spelling the account row is written with no account id and
99+
* the persona silently stays un-loginable.
100+
*/
101+
accountId: string;
96102
password: string;
97103
}) => Promise<unknown>;
98104
};
@@ -176,7 +182,7 @@ async function assignPositions(
176182
*
177183
* ## Why this is read and not written
178184
*
179-
* better-auth 1.7 keys account identity on `(issuer, providerAccountId)`:
185+
* better-auth 1.7 keys account identity on `(issuer, accountId)`:
180186
* `findAccountByKey` looks a credential up under the issuer better-auth mints
181187
* for itself, so a row carrying any other value — or none — is INVISIBLE and
182188
* sign-in fails `INVALID_EMAIL_OR_PASSWORD` behind a "User not found" warn that
@@ -245,7 +251,7 @@ async function ensureCredentialAccount(
245251
userId,
246252
providerId: 'credential',
247253
issuer,
248-
providerAccountId: userId,
254+
accountId: userId,
249255
password: hashed,
250256
});
251257
ctx.logger?.info?.('[showcase] approval-demo persona is now loginable', { userId });

packages/client/src/index.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2788,10 +2788,14 @@ export class ObjectStackClient {
27882788
return { accounts: accounts as Array<{
27892789
id: string;
27902790
providerId: string;
2791-
/** Authority that vouched for `providerAccountId` — an OIDC issuer, or `local:…`. */
2791+
/** Authority that vouched for `accountId` — an OIDC issuer, or `local:…`. */
27922792
issuer: string;
2793-
/** The user's id at the provider — better-auth 1.7 renamed this from `accountId`. */
2794-
providerAccountId: string;
2793+
/**
2794+
* The user's id at the provider. `1.7.0-rc.2` briefly published this
2795+
* as `providerAccountId`; stable 1.7 answers with `accountId` again
2796+
* (#3002), which is what this route returns today.
2797+
*/
2798+
accountId: string;
27952799
createdAt?: string;
27962800
updatedAt?: string;
27972801
}> };

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,6 +1228,10 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
12281228
label: "Client ID",
12291229
help: "Public OAuth client identifier"
12301230
},
1231+
client_discovery_id: {
1232+
label: "Client Discovery ID",
1233+
help: "Opaque identifier the provider uses to look this client up on the discovery path, kept apart from the public `client_id`"
1234+
},
12311235
client_secret: {
12321236
label: "Client Secret",
12331237
help: "OAuth client secret — stored as a SHA-256 digest, never plaintext (`@better-auth/oauth-provider`'s `storeClientSecret`, which defaults to hashed whenever the jwt plugin is enabled; wired in plugin-auth's `AuthManager.buildPluginList()`, oidcProvider branch). Shown once at registration."
@@ -1272,6 +1276,10 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
12721276
label: "Allowed Scopes",
12731277
help: "JSON-serialized list of scopes the client may request"
12741278
},
1279+
client_credentials_scopes: {
1280+
label: "Client-Credentials Scopes",
1281+
help: "JSON-serialized list of scopes the client may request on the client_credentials grant, where there is no user to consent — kept apart from `scopes`, which governs user-delegated grants"
1282+
},
12751283
subject_type: {
12761284
label: "Subject Type",
12771285
help: "OIDC subject type (e.g. public, pairwise)"

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,6 +1228,10 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
12281228
label: "ID de cliente",
12291229
help: "Identificador público del cliente OAuth."
12301230
},
1231+
client_discovery_id: {
1232+
label: "Client Discovery ID",
1233+
help: "Opaque identifier the provider uses to look this client up on the discovery path, kept apart from the public `client_id`"
1234+
},
12311235
client_secret: {
12321236
label: "Secreto de cliente",
12331237
help: "Secreto del cliente OAuth (con hash/cifrado en reposo)."
@@ -1272,6 +1276,10 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
12721276
label: "Ámbitos permitidos",
12731277
help: "Lista serializada en JSON de ámbitos que el cliente puede solicitar."
12741278
},
1279+
client_credentials_scopes: {
1280+
label: "Client-Credentials Scopes",
1281+
help: "JSON-serialized list of scopes the client may request on the client_credentials grant, where there is no user to consent — kept apart from `scopes`, which governs user-delegated grants"
1282+
},
12751283
subject_type: {
12761284
label: "Tipo de sujeto",
12771285
help: "Tipo de sujeto OIDC (p. ej. public, pairwise)."

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,6 +1228,10 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
12281228
label: "クライアント ID",
12291229
help: "公開 OAuth クライアント識別子"
12301230
},
1231+
client_discovery_id: {
1232+
label: "Client Discovery ID",
1233+
help: "Opaque identifier the provider uses to look this client up on the discovery path, kept apart from the public `client_id`"
1234+
},
12311235
client_secret: {
12321236
label: "クライアントシークレット",
12331237
help: "OAuth クライアントシークレット(保存時にハッシュ/暗号化済み)"
@@ -1272,6 +1276,10 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
12721276
label: "許可スコープ",
12731277
help: "クライアントがリクエスト可能なスコープの JSON シリアライズリスト"
12741278
},
1279+
client_credentials_scopes: {
1280+
label: "Client-Credentials Scopes",
1281+
help: "JSON-serialized list of scopes the client may request on the client_credentials grant, where there is no user to consent — kept apart from `scopes`, which governs user-delegated grants"
1282+
},
12751283
subject_type: {
12761284
label: "サブジェクトタイプ",
12771285
help: "OIDC サブジェクトタイプ(例: public、pairwise)"

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,6 +1228,10 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
12281228
label: "客户端 ID",
12291229
help: "公开的 OAuth 客户端标识"
12301230
},
1231+
client_discovery_id: {
1232+
label: "Client Discovery ID",
1233+
help: "Opaque identifier the provider uses to look this client up on the discovery path, kept apart from the public `client_id`"
1234+
},
12311235
client_secret: {
12321236
label: "客户端密钥",
12331237
help: "OAuth 客户端密钥(静态存储时会哈希/加密)"
@@ -1272,6 +1276,10 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
12721276
label: "允许的范围",
12731277
help: "客户端可申请的 scopes 的 JSON 序列化列表"
12741278
},
1279+
client_credentials_scopes: {
1280+
label: "Client-Credentials Scopes",
1281+
help: "JSON-serialized list of scopes the client may request on the client_credentials grant, where there is no user to consent — kept apart from `scopes`, which governs user-delegated grants"
1282+
},
12751283
subject_type: {
12761284
label: "主体类型",
12771285
help: "OIDC 主体类型(例如 public、pairwise)"

packages/platform-objects/src/identity/sys-account.object.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const SysAccount = ObjectSchema.create({
3535
// this, where `accountId` is the account ROW id (better-auth 1.7 narrowed
3636
// the body from the old `{ providerId, accountId }` pair, and `accountId`
3737
// no longer means the provider's id for the user — that field is now
38-
// `providerAccountId`). The form is locked to the row's values so it acts
38+
// `accountId`). The form is locked to the row's values so it acts
3939
// as a one-click confirmation rather than a free-form edit.
4040
//
4141
// `link_social` is the self-service counterpart — a toolbar action
@@ -306,7 +306,7 @@ export const SysAccount = ObjectSchema.create({
306306
indexes: [
307307
{ fields: ['user_id'], unique: false },
308308
{ fields: ['provider_id', 'account_id'], unique: true },
309-
// better-auth 1.7 resolves accounts by (issuer, providerAccountId) and
309+
// better-auth 1.7 resolves accounts by (issuer, accountId) and
310310
// declares that pair unique on its own `account` table — mirror it here so
311311
// the physical table enforces the same identity key the auth code assumes.
312312
{ fields: ['issuer', 'account_id'], unique: true },

0 commit comments

Comments
 (0)