Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions packages/plugins/plugin-auth/src/auth-manager.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2761,10 +2761,11 @@ export class AuthManager {
// AUTH_SSO_PROVIDER_SCHEMA).
//
// That bridge dates from 1.6.20, where @better-auth/sso hardcoded the
// model and read no `schema` option. Re-checked against the pinned
// 1.7.0-rc.2 (`node_modules/@better-auth/sso/dist`) on 2026-08-12: that is
// no longer true — `SSOOptions.schema.ssoProvider` now exists
// (index-D1yk91me.d.mts) and the runtime honours `modelName` plus a
// model and read no `schema` option. Re-measured against the installed
// 1.7.1 (`node_modules/@better-auth/sso/dist`) on 2026-08-19 — the pin
// moved off 1.7.0-rc.2, so the previous stamp here had itself expired
// (#8224): still no longer true — `SSOOptions.schema.ssoProvider` exists
// (index-CZytzKv6.d.mts) and the runtime honours `modelName` plus a
// per-field `fieldName` map (index.mjs, the plugin's `schema:` block). The
// adapter-level bridge is kept as-is here because it is what the rest of
// the auth stack is wired to; whether to move it onto the plugin option is
Expand DownExpand Up@@ -2797,9 +2798,14 @@ export class AuthManager {
// Provider; endpoints mount under /api/v1/auth/scim/v2/{Users,…} (SCIM 2.0)
// and /api/v1/auth/scim/{generate-token,…} (management). `active:false` →
// ban + session revoke (needs the admin plugin, forced on above); org-scoped
// tokens need the organization plugin. Like @better-auth/sso it hardcodes
// its `scimProvider` model (no schema option) — bridged to `sys_scim_provider`
// via AUTH_MODEL_TO_PROTOCOL. Toggle with `OS_SCIM_ENABLED`.
// tokens need the organization plugin. This plugin hardcodes its
// `scimProvider` model and accepts no `schema` option — still true of the
// installed 1.7.0-rc.1 (`SCIMOptions` declares no `schema` / `modelName` /
// `fields` member; measured 2026-08-19). NOT "like @better-auth/sso", as
// this line used to say: sso accepts one as of 1.7.1 (#8224), so scim is now
// the only one of the pair for which the adapter bridge is forced. Bridged
// to `sys_scim_provider` via AUTH_MODEL_TO_PROTOCOL. Toggle with
// `OS_SCIM_ENABLED`.
//
// storeSCIMToken: 'hashed' — never persist the bearer in cleartext; the
// plaintext is returned exactly once from generate-token (for the IdP admin).
Expand Down
27 changes: 18 additions & 9 deletions packages/plugins/plugin-auth/src/auth-schema-config.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -942,11 +942,15 @@ export const AUTH_SSO_PROVIDER_SCHEMA = {
},
} as const;

// NOTE: there is intentionally no `buildSsoPluginSchema()`. Unlike
// `oauthProvider`, the @better-auth/sso plugin exposes NO `schema` option
// (verified vs 1.6.20), so the mapping above cannot be handed to the plugin —
// it must be consumed at the ADAPTER layer (AUTH_MODEL_TO_PROTOCOL + field
// resolution in objectql-adapter.ts). See ADR-0024.
// NOTE: there is intentionally no `buildSsoPluginSchema()`. The original reason
// was that the plugin exposed NO `schema` option (true of 1.6.20) — that is no
// longer why. Measured 2026-08-19 against the installed `@better-auth/sso@1.7.1`:
// `SSOOptions.schema.ssoProvider.{modelName,fields,additionalFields}` exists and
// the runtime honours it, so the mapping above COULD be handed to the plugin.
// It is still consumed at the ADAPTER layer instead (AUTH_MODEL_TO_PROTOCOL +
// field resolution in objectql-adapter.ts), which is now a deliberate choice
// about where the bridge lives rather than a limitation of the dependency;
// revisiting it is the open architecture question on #8224. See ADR-0024.

// ---------------------------------------------------------------------------
// SCIM plugin – scimProvider table (@better-auth/scim)
Expand All@@ -957,10 +961,15 @@ export const AUTH_SSO_PROVIDER_SCHEMA = {
*
* Each row is a SCIM connection: a bearer token an external IdP (Okta / Entra)
* uses to auto-provision / deprovision THIS environment's users — the env is
* the SCIM Service Provider (ADR-0071). Like `@better-auth/sso`, the plugin
* hardcodes its model and exposes NO `schema` option, so the mapping is
* consumed at the ADAPTER layer (AUTH_MODEL_TO_PROTOCOL + field resolution in
* objectql-adapter.ts), NOT handed to the plugin.
* the SCIM Service Provider (ADR-0071). This plugin hardcodes its model and
* exposes NO `schema` option — still true of the installed
* `@better-auth/scim@1.7.0-rc.1` (`SCIMOptions` declares no `schema` /
* `modelName` / `fields` member at all; measured 2026-08-19). It is no longer
* true of `@better-auth/sso@1.7.1`, which this doc used to lean on for the
* comparison and which now accepts one (#8224). So for scim — and for scim
* alone — the ADAPTER layer (AUTH_MODEL_TO_PROTOCOL + field resolution in
* objectql-adapter.ts) is the only available route: the mapping cannot be
* handed to the plugin.
*
* | camelCase (better-auth) | snake_case (ObjectStack) |
* |:------------------------|:-------------------------|
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,8 +46,10 @@
* `@better-auth/sso` / `@better-auth/scim` are excluded from the call above
* for a narrower reason than this header used to give. The previous wording —
* "accept no `schema` option, so `getAuthTables()` cannot see them" — was
* measured false and is corrected here (#8224). Re-measured 2026-08-18 against
* the pinned `@better-auth/sso@1.7.0-rc.2` / `@better-auth/scim@1.7.0-rc.1`:
* measured false and is corrected here (#8224). Re-measured 2026-08-19 against
* the installed `@better-auth/sso@1.7.1` / `@better-auth/scim@1.7.0-rc.1` (the
* 2026-08-18 stamp this block carried named `sso@1.7.0-rc.2`, a pin that has
* since moved — the very drift this card is about):
*
* - Both DECLARE a schema `getAuthTables()` reads. Passing `sso()` yields the
* `ssoProvider` model; `scim({})` yields `scimProvider` plus the four
Expand DownExpand Up@@ -244,8 +246,8 @@ const AUTH_MANAGER_PLUGINS: Record<string, { construct: () => unknown } | { skip
'the auth manager passes it no `schema` option, so getAuthTables() would report its model as '
+ '`ssoProvider` with camelCase columns while the adapter bridge writes `sys_sso_provider` in '
+ 'snake_case — covered by the dedicated sso/scim block below, which reproduces the adapter '
+ 'rule that governs its writes. NOT because it accepts no schema option: it does, on '
+ '1.7.0-rc.2 (#8224). See the file header.',
+ 'rule that governs its writes. NOT because it accepts no schema option: it does, on the '
+ 'installed 1.7.1 (measured 2026-08-19, #8224). See the file header.',
},
scim: {
skip:
Expand Down
31 changes: 21 additions & 10 deletions packages/plugins/plugin-auth/src/managed-extension-fields.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -173,8 +173,8 @@ interface UnmappedManagedObject {
* coverage at all, so the reason has to say why that is the right answer for
* this table rather than an oversight. `managedBy` alone does not mean
* better-auth's core `getAuthTables()` surface owns the columns — a plugin may
* be opt-in, ship in its own package, or (for sso/scim) expose no `schema`
* option this call can read.
* be opt-in, ship in its own package, or (for sso/scim) be passed no `schema`
* option by the auth manager, leaving this call no mapping to key off.
*/
const UNMAPPED_MANAGED_OBJECTS: Record<string, UnmappedManagedObject> = {
sys_api_key: {
Expand DownExpand Up@@ -210,21 +210,32 @@ const UNMAPPED_MANAGED_OBJECTS: Record<string, UnmappedManagedObject> = {
// ── Plugins getAuthTables() cannot ADDRESS as an ObjectStack object (#3653) ─
// Both plugins are now in the call (#7820), so their models do appear in the
// derived tables — under better-auth's own names (`ssoProvider`,
// `scimProvider`, …). What they accept no `schema` option for is the mapping:
// there is no way to tell getAuthTables() that `ssoProvider` materializes as
// `sys_sso_provider`, so MODEL_TO_OBJECT cannot key off anything the library
// reports and the derivation has nothing to compare against the object.
// `scimProvider`, …). The MAPPING is what this call cannot reach: the auth
// manager passes neither plugin a `schema` option, so nothing tells
// getAuthTables() that `ssoProvider` materializes as `sys_sso_provider`,
// MODEL_TO_OBJECT cannot key off anything the library reports, and the
// derivation has nothing to compare against the object.
//
// Note the reason above is about what the auth manager PASSES, not about what
// the plugins ACCEPT — the two were conflated here until #8224. Measured
// 2026-08-19: `@better-auth/sso@1.7.1` does accept a `schema` option
// (`SSOOptions.schema.ssoProvider`), so passing one is a live option rather
// than something the dependency forbids; `@better-auth/scim@1.7.0-rc.1` still
// accepts none.
sys_sso_provider: {
reason:
'@better-auth/sso accepts no `schema` option, so getAuthTables() reports its models only under '
+ "better-auth's own names and they cannot be mapped onto this object (#3653). Its columns are "
'The auth manager passes @better-auth/sso no `schema` option, so getAuthTables() reports its '
+ "models only under better-auth's own names and they cannot be mapped onto this object (#3653). "
+ '(The plugin DOES accept one as of 1.7.1 — measured 2026-08-19, #8224; it is simply not passed.) '
+ 'Its columns are '
+ 'bridged mechanically by objectql-adapter.ts and gated by the dedicated sso/scim block in '
+ 'better-auth-schema-parity.test.ts.',
},
sys_scim_provider: {
reason:
'@better-auth/scim accepts no `schema` option, so getAuthTables() reports its models only under '
+ "better-auth's own names and they cannot be mapped onto this object (#3653). Same bridge and "
'@better-auth/scim accepts no `schema` option at all (measured 2026-08-19 against the installed '
+ '1.7.0-rc.1, and the auth manager passes none either), so getAuthTables() reports its models '
+ "only under better-auth's own names and they cannot be mapped onto this object (#3653). Same bridge and "
+ 'same dedicated gate as sys_sso_provider.',
},

Expand Down
10 changes: 7 additions & 3 deletions packages/plugins/plugin-auth/src/objectql-adapter.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -302,9 +302,13 @@ describe('createObjectQLAdapter – legacy model name mapping', () => {
});
});

describe('createObjectQLAdapterFactory – schema-less plugin bridging (@better-auth/sso)', () => {
// The sso plugin exposes no `schema` option, so its `ssoProvider` table +
// camelCase fields are bridged at the adapter layer. Pass the plugin so
describe('createObjectQLAdapterFactory – adapter-layer plugin bridging (@better-auth/sso)', () => {
// The auth manager passes the sso plugin no `schema` option, so its
// `ssoProvider` table + camelCase fields are bridged at the adapter layer
// instead. (Not because the plugin "exposes no `schema` option" — that was
// true of 1.6.20 and expired with the pin: measured 2026-08-19, the installed
// `@better-auth/sso@1.7.1` accepts `schema.ssoProvider.{modelName,fields,
// additionalFields}`. #8224.) Pass the plugin so
// better-auth's wrapper recognises the model (it validates against the
// merged schema before delegating to our adapter methods).
const makeAdapter = (findOneRow: any = { id: '1', provider_id: 'okta', oidc_config: '{"clientId":"x"}', domain: 'acme.com' }) => {
Expand Down
32 changes: 24 additions & 8 deletions packages/plugins/plugin-auth/src/objectql-adapter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,12 +32,26 @@ export const AUTH_MODEL_TO_PROTOCOL: Record<string, string> = {
session: SystemObjectName.SESSION,
account: SystemObjectName.ACCOUNT,
verification: SystemObjectName.VERIFICATION,
// Plugin models. `@better-auth/sso` and `@better-auth/scim` both hardcode
// their model name and accept NO `schema` option (verified vs 1.6.2x — no
// mergeSchema, runtime never reads options.schema), so the table name is
// bridged here and `createObjectQLAdapterFactory` (below) auto-maps their
// camelCase fields to snake_case (oidcConfig→oidc_config, scimToken→
// scim_token, …) on every CRUD op via resolveProtocolName. Off by default
// Plugin models, bridged HERE rather than through a plugin `schema` option.
// This comment used to justify that with "both hardcode their model name and
// accept NO `schema` option (verified vs 1.6.2x)". That expired with the pin
// (#8224). Measured 2026-08-19 against the installed `@better-auth/sso@1.7.1`
// and `@better-auth/scim@1.7.0-rc.1`:
// - sso DOES accept one now — `SSOOptions.schema.ssoProvider.{modelName,
// fields,additionalFields}` (dist/index-CZytzKv6.d.mts), honoured at
// runtime (dist/index.mjs, the plugin's own `schema:` block: `modelName:
// options?.modelName ?? options?.schema?.ssoProvider?.modelName ??
// 'ssoProvider'`, plus a per-field `fieldName` fallback each).
// - scim still accepts none — `SCIMOptions` declares no `schema` /
// `modelName` / `fields` member at all.
// What holds for both is narrower and is the actual reason: the auth manager
// passes neither plugin a `schema` option, so their models arrive under
// better-auth's own names. The table name is bridged here and
// `createObjectQLAdapterFactory` (below) auto-maps their camelCase fields to
// snake_case (oidcConfig→oidc_config, scimToken→scim_token, …) on every CRUD
// op via resolveProtocolName. For scim that is the only available route; for
// sso it is now a CHOICE (see #8224 — moving it onto the plugin option is an
// open architecture question, deliberately not decided here). Off by default
// (OS_SSO_ENABLED / OS_SCIM_ENABLED). See ADR-0024 / ADR-0071.
ssoProvider: 'sys_sso_provider',
scimProvider: 'sys_scim_provider',
Expand DownExpand Up@@ -713,8 +727,10 @@ export function createObjectQLAdapterFactory(rawDataEngine: IDataEngine) {
// /refresh-token) see the row whole while the generic data API does not.
// See `internal-field-readback.ts`.
const internalFieldEngine = rawDataEngine as unknown as InternalFieldResolvingEngine;
// Field-name bridging for better-auth plugins that expose NO `schema` option
// (e.g. @better-auth/sso): when a model is remapped via AUTH_MODEL_TO_PROTOCOL,
// Field-name bridging for better-auth plugins the auth manager passes no
// `schema` option (@better-auth/sso, @better-auth/scim — NOT "plugins that
// expose no `schema` option": sso accepts one as of 1.7.1, #8224): when a
// model is remapped via AUTH_MODEL_TO_PROTOCOL,
// its camelCase model fields are also converted to snake_case columns on the
// way in and back to camelCase on the way out. SCOPED by `objectName !== model`
// so core / schema-declared models are byte-for-byte untouched.
Expand Down
Loading