diff --git a/packages/plugins/plugin-auth/src/auth-manager.ts b/packages/plugins/plugin-auth/src/auth-manager.ts index 25737f9d09..ca2a27c9a2 100644 --- a/packages/plugins/plugin-auth/src/auth-manager.ts +++ b/packages/plugins/plugin-auth/src/auth-manager.ts @@ -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 @@ -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). diff --git a/packages/plugins/plugin-auth/src/auth-schema-config.ts b/packages/plugins/plugin-auth/src/auth-schema-config.ts index 651ff04b52..6f3b954b99 100644 --- a/packages/plugins/plugin-auth/src/auth-schema-config.ts +++ b/packages/plugins/plugin-auth/src/auth-schema-config.ts @@ -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) @@ -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) | * |:------------------------|:-------------------------| diff --git a/packages/plugins/plugin-auth/src/better-auth-schema-parity.test.ts b/packages/plugins/plugin-auth/src/better-auth-schema-parity.test.ts index dcaa0b52de..8c7371dced 100644 --- a/packages/plugins/plugin-auth/src/better-auth-schema-parity.test.ts +++ b/packages/plugins/plugin-auth/src/better-auth-schema-parity.test.ts @@ -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 @@ -244,8 +246,8 @@ const AUTH_MANAGER_PLUGINS: Record 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: diff --git a/packages/plugins/plugin-auth/src/managed-extension-fields.test.ts b/packages/plugins/plugin-auth/src/managed-extension-fields.test.ts index 9c57a17d7b..5f19b39053 100644 --- a/packages/plugins/plugin-auth/src/managed-extension-fields.test.ts +++ b/packages/plugins/plugin-auth/src/managed-extension-fields.test.ts @@ -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 = { sys_api_key: { @@ -210,21 +210,32 @@ const UNMAPPED_MANAGED_OBJECTS: Record = { // ── 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.', }, diff --git a/packages/plugins/plugin-auth/src/objectql-adapter.test.ts b/packages/plugins/plugin-auth/src/objectql-adapter.test.ts index b9187b0e85..e2743df93b 100644 --- a/packages/plugins/plugin-auth/src/objectql-adapter.test.ts +++ b/packages/plugins/plugin-auth/src/objectql-adapter.test.ts @@ -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' }) => { diff --git a/packages/plugins/plugin-auth/src/objectql-adapter.ts b/packages/plugins/plugin-auth/src/objectql-adapter.ts index 237753e1dc..877b93e6d8 100644 --- a/packages/plugins/plugin-auth/src/objectql-adapter.ts +++ b/packages/plugins/plugin-auth/src/objectql-adapter.ts @@ -32,12 +32,26 @@ export const AUTH_MODEL_TO_PROTOCOL: Record = { 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', @@ -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.