From 5370b6b2b15f48d316a576ec62529adbac98ca06 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 10:40:41 +0000 Subject: [PATCH 1/2] feat(platform-objects)!: retire the sys_scim_provider platform object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the rc.1-era @better-auth/scim connection-row object and every in-repo reader, plus its registry line in packages/spec/src/system/constants/platform-object-names.ts (one PR, both halves — the registry conformance test asserts the set bidirectionally). Stable @better-auth/scim 1.7.x derives no scimProvider model (#3653 landed in PR #12726), so the object backed nothing. No data migration by ruling. The ADR-0066 D3 capability-gate pin moves from the retired object to the surviving sibling SysSsoProvider so the gate cannot be dropped silently. Translation bundles regenerated (pnpm i18n:extract). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4 --- content/docs/permissions/authorization.mdx | 4 +- ...registry-tenant-index-follows-wall.test.ts | 3 +- packages/objectql/src/registry.ts | 2 +- .../scripts/i18n-extract.config.ts | 5 +- .../translations/bundle-ownership.test.ts | 6 +- .../apps/translations/en.objects.generated.ts | 41 ----- .../translations/es-ES.objects.generated.ts | 41 ----- .../es-ES.source-hashes.generated.ts | 3 - .../translations/ja-JP.objects.generated.ts | 41 ----- .../ja-JP.source-hashes.generated.ts | 3 - .../translations/zh-CN.objects.generated.ts | 41 ----- .../zh-CN.source-hashes.generated.ts | 3 - .../action-predicate-sparse-face.test.ts | 2 - .../platform-objects/src/identity/index.ts | 3 +- .../src/identity/sys-scim-provider.object.ts | 169 ------------------ .../src/platform-objects.test.ts | 12 +- .../src/better-auth-schema-parity.test.ts | 8 +- .../src/credential-at-rest-posture.test.ts | 2 - .../src/managed-extension-fields.test.ts | 11 +- packages/plugins/plugin-auth/src/manifest.ts | 5 +- .../plugin-auth/src/objectql-adapter.ts | 5 +- .../src/objects/default-permission-sets.ts | 1 - .../dogfood/test/authz-conformance.matrix.ts | 2 +- .../system/constants/platform-object-names.ts | 1 - 24 files changed, 27 insertions(+), 387 deletions(-) delete mode 100644 packages/platform-objects/src/identity/sys-scim-provider.object.ts diff --git a/content/docs/permissions/authorization.mdx b/content/docs/permissions/authorization.mdx index 80a8e78711..9202b02b11 100644 --- a/content/docs/permissions/authorization.mdx +++ b/content/docs/permissions/authorization.mdx @@ -528,8 +528,8 @@ The complete, prioritized gap map lives in issue **#2561** (the production `sys_verification`, `sys_oauth_access_token`, `sys_oauth_refresh_token`, `sys_device_code`) declare `access: { default: 'private' }` — no wildcard grant reaches them; platform admins retain access via the posture-gated - superuser bypass. `sys_scim_provider` is capability-gated like - `sys_sso_provider`. Member self-service objects (`sys_session`, + superuser bypass. `sys_sso_provider` is capability-gated + (`manage_platform_settings`). Member self-service objects (`sys_session`, `sys_api_key`, `sys_oauth_application`, `sys_two_factor`) deliberately stay public-posture (the Account app reads them as the member; row scoping is their guard). Still open: Studio posture surfacing (objectui). diff --git a/packages/objectql/src/registry-tenant-index-follows-wall.test.ts b/packages/objectql/src/registry-tenant-index-follows-wall.test.ts index 96cd51af33..3a23b63583 100644 --- a/packages/objectql/src/registry-tenant-index-follows-wall.test.ts +++ b/packages/objectql/src/registry-tenant-index-follows-wall.test.ts @@ -195,8 +195,7 @@ describe('[#8608] the tenant index follows the wall’s derivation', () => { // being dialect-dependent). It is the ONE shipped platform object whose // answer this card changes; every other table that would qualify already // declares its own single-column tenant index (`sys_invitation`, - // `sys_team`, `sys_scim_provider`) or is tenancy-disabled - // (`sys_sso_provider`). + // `sys_team`) or is tenancy-disabled (`sys_sso_provider`). const out: any = applySystemFields(leadWith({ managedBy: 'better-auth' }), { multiTenant: true, }); diff --git a/packages/objectql/src/registry.ts b/packages/objectql/src/registry.ts index c04df1f860..83b4bb5ee6 100644 --- a/packages/objectql/src/registry.ts +++ b/packages/objectql/src/registry.ts @@ -846,7 +846,7 @@ function provisionTenantScopeIndex( * (`sys_member`) is walled on that column and is now indexed on it. Re-adding * an exclusion the wall does not have is precisely the drift this rebinding * closes — and `declaresTenantIndex` already covers the tables that declare - * their own tenant index (`sys_invitation`, `sys_team`, `sys_scim_provider`). + * their own tenant index (`sys_invitation`, `sys_team`). * * ⚠️ The clause-1 rows are NOT a leftover of the condition #8459 lifted: an * object that declares itself non-tenant-scoped is one the wall composes NO diff --git a/packages/platform-objects/scripts/i18n-extract.config.ts b/packages/platform-objects/scripts/i18n-extract.config.ts index 0e3f6ad3e0..3308711e1e 100644 --- a/packages/platform-objects/scripts/i18n-extract.config.ts +++ b/packages/platform-objects/scripts/i18n-extract.config.ts @@ -103,7 +103,6 @@ import { SysOauthClientAssertion, SysJwks, SysSsoProvider, - SysScimProvider, SysScimConnectionBinding, SysScimConnectionCredential, SysScimGroup, @@ -248,9 +247,9 @@ const config: ObjectStackDefinition = defineStack({ SysOauthClientAssertion, SysJwks, SysSsoProvider, - SysScimProvider, // Stable @better-auth/scim 1.7.x model set + the ObjectStack-owned - // credential store (#3653). SysScimProvider above retires under #11757. + // credential store (#3653). The rc.1-era SysScimProvider retired under + // #11757. SysScimConnectionBinding, SysScimConnectionCredential, SysScimGroup, diff --git a/packages/platform-objects/src/apps/translations/bundle-ownership.test.ts b/packages/platform-objects/src/apps/translations/bundle-ownership.test.ts index e13780e30c..12de4a2861 100644 --- a/packages/platform-objects/src/apps/translations/bundle-ownership.test.ts +++ b/packages/platform-objects/src/apps/translations/bundle-ownership.test.ts @@ -24,10 +24,10 @@ const OWNED_OBJECTS = new Set([ 'sys_user_preference', 'sys_oauth_application', 'sys_oauth_access_token', 'sys_oauth_refresh_token', 'sys_oauth_consent', 'sys_oauth_resource', 'sys_oauth_client_resource', 'sys_oauth_client_assertion', 'sys_jwks', - // identity — external SSO / SCIM providers (admin-facing, better-auth-managed) - 'sys_sso_provider', 'sys_scim_provider', + // identity — external SSO provider (admin-facing, better-auth-managed) + 'sys_sso_provider', // identity — stable @better-auth/scim 1.7.x model set + the ObjectStack-owned - // credential store (#3653; sys_scim_provider above retires under #11757) + // credential store (#3653; the rc.1-era sys_scim_provider retired under #11757) 'sys_scim_connection_binding', 'sys_scim_connection_credential', 'sys_scim_group', 'sys_scim_group_member', 'sys_scim_identity_tombstone', 'sys_scim_projection_grant', 'sys_scim_subject', 'sys_scim_user', diff --git a/packages/platform-objects/src/apps/translations/en.objects.generated.ts b/packages/platform-objects/src/apps/translations/en.objects.generated.ts index e74fe0bb51..71116a6ba8 100644 --- a/packages/platform-objects/src/apps/translations/en.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/en.objects.generated.ts @@ -1894,47 +1894,6 @@ export const enObjects: NonNullable = { } } }, - sys_scim_provider: { - label: "SCIM Provider", - pluralLabel: "SCIM Providers", - description: "SCIM 2.0 connections (bearer tokens) external IdPs use to provision/deprovision this environment's users", - fields: { - id: { - label: "ID" - }, - provider_id: { - label: "Provider ID", - help: "Stable SCIM provider identifier (e.g. \"okta-scim\")" - }, - provider_key: { - label: "Provider Key", - help: "Derived : uniqueness key maintained by @better-auth/scim; do not write directly." - }, - scim_token: { - label: "SCIM Token (hash)", - help: "Hashed bearer credential for this SCIM connection — the plaintext is shown once at generate-token. Sensitive; do not expose." - }, - organization_id: { - label: "Organization", - help: "Organization scope of this token (org-scoped tokens restrict provisioning to that org)" - }, - user_id: { - label: "Owned By", - help: "User who generated this token (when provider-ownership is enabled)" - }, - created_at: { - label: "Created At" - }, - updated_at: { - label: "Updated At" - } - }, - _views: { - all: { - label: "All" - } - } - }, sys_scim_connection_binding: { label: "SCIM Connection Binding", pluralLabel: "SCIM Connection Bindings", diff --git a/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts index f72b25613d..a02d8c361e 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts @@ -1894,47 +1894,6 @@ export const esESObjects: NonNullable = { } } }, - sys_scim_provider: { - label: "Proveedor SCIM", - pluralLabel: "Proveedores SCIM", - description: "Conexiones SCIM 2.0 (tokens bearer) que los IdP externos usan para aprovisionar/desaprovisionar los usuarios de este entorno", - fields: { - id: { - label: "ID" - }, - provider_id: { - label: "ID de proveedor", - help: "Identificador de proveedor SCIM estable (p. ej. «okta-scim»)" - }, - provider_key: { - label: "Provider Key", - help: "Derived : uniqueness key maintained by @better-auth/scim; do not write directly." - }, - scim_token: { - label: "Token SCIM (hash)", - help: "Credencial bearer con hash de esta conexión SCIM: el texto plano se muestra una sola vez al generar el token. Sensible; no lo exponga." - }, - organization_id: { - label: "Organización", - help: "Ámbito de organización de este token (los tokens por organización limitan el aprovisionamiento a esa organización)" - }, - user_id: { - label: "Propiedad de", - help: "Usuario que generó este token (cuando la propiedad de proveedor está habilitada)" - }, - created_at: { - label: "Creado el" - }, - updated_at: { - label: "Actualizado el" - } - }, - _views: { - all: { - label: "Todos" - } - } - }, sys_scim_connection_binding: { label: "SCIM Connection Binding", pluralLabel: "SCIM Connection Bindings", diff --git a/packages/platform-objects/src/apps/translations/es-ES.source-hashes.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.source-hashes.generated.ts index 17343cb8d1..b1ab1d66e8 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.source-hashes.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.source-hashes.generated.ts @@ -499,9 +499,6 @@ export const esESGeneratedSourceHashes: Readonly> = { "objects.sys_scim_projection_grant.fields.user_id.label": "6c0d22a931e235bd", "objects.sys_scim_projection_grant.label": "fbf273320dc0e8cb", "objects.sys_scim_projection_grant.pluralLabel": "b8bb5769deedfbe3", - "objects.sys_scim_provider.fields.id.label": "00b0385c9c152888", - "objects.sys_scim_provider.fields.provider_key.help": "6eba9e41bfb954ab", - "objects.sys_scim_provider.fields.provider_key.label": "fbc96a8b3ed4709d", "objects.sys_scim_subject._views.all.label": "6d6f653f8264ab17", "objects.sys_scim_subject.description": "8fc839e493ef3e75", "objects.sys_scim_subject.fields.created_at.label": "1f02d416befb595b", diff --git a/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts index b63f354ae6..26b1458d04 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts @@ -1894,47 +1894,6 @@ export const jaJPObjects: NonNullable = { } } }, - sys_scim_provider: { - label: "SCIM プロバイダー", - pluralLabel: "SCIM プロバイダー", - description: "外部 IdP がこの環境のユーザーをプロビジョニング/デプロビジョニングするために使用する SCIM 2.0 接続(ベアラートークン)", - fields: { - id: { - label: "ID" - }, - provider_id: { - label: "プロバイダー ID", - help: "安定した SCIM プロバイダー識別子(例: 「okta-scim」)" - }, - provider_key: { - label: "Provider Key", - help: "Derived : uniqueness key maintained by @better-auth/scim; do not write directly." - }, - scim_token: { - label: "SCIM トークン(ハッシュ)", - help: "この SCIM 接続のハッシュ化されたベアラー資格情報——平文はトークン生成時に一度だけ表示されます。機密情報のため、公開しないでください。" - }, - organization_id: { - label: "組織", - help: "このトークンの組織スコープ(組織スコープのトークンはプロビジョニングをその組織に限定します)" - }, - user_id: { - label: "所有者", - help: "このトークンを生成したユーザー(プロバイダー所有権が有効な場合)" - }, - created_at: { - label: "作成日時" - }, - updated_at: { - label: "更新日時" - } - }, - _views: { - all: { - label: "すべて" - } - } - }, sys_scim_connection_binding: { label: "SCIM Connection Binding", pluralLabel: "SCIM Connection Bindings", diff --git a/packages/platform-objects/src/apps/translations/ja-JP.source-hashes.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.source-hashes.generated.ts index bdc631344b..63eb5d4a9b 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.source-hashes.generated.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.source-hashes.generated.ts @@ -492,9 +492,6 @@ export const jaJPGeneratedSourceHashes: Readonly> = { "objects.sys_scim_projection_grant.fields.user_id.label": "6c0d22a931e235bd", "objects.sys_scim_projection_grant.label": "fbf273320dc0e8cb", "objects.sys_scim_projection_grant.pluralLabel": "b8bb5769deedfbe3", - "objects.sys_scim_provider.fields.id.label": "00b0385c9c152888", - "objects.sys_scim_provider.fields.provider_key.help": "6eba9e41bfb954ab", - "objects.sys_scim_provider.fields.provider_key.label": "fbc96a8b3ed4709d", "objects.sys_scim_subject._views.all.label": "6d6f653f8264ab17", "objects.sys_scim_subject.description": "8fc839e493ef3e75", "objects.sys_scim_subject.fields.created_at.label": "1f02d416befb595b", diff --git a/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts index 507f2cf523..050e1ece56 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts @@ -1894,47 +1894,6 @@ export const zhCNObjects: NonNullable = { } } }, - sys_scim_provider: { - label: "SCIM 提供方", - pluralLabel: "SCIM 提供方", - description: "外部 IdP 用于开通/停用本环境用户的 SCIM 2.0 连接(Bearer 令牌)", - fields: { - id: { - label: "ID" - }, - provider_id: { - label: "提供方 ID", - help: "稳定的 SCIM 提供方标识符(如 “okta-scim”)" - }, - provider_key: { - label: "Provider Key", - help: "Derived : uniqueness key maintained by @better-auth/scim; do not write directly." - }, - scim_token: { - label: "SCIM 令牌(哈希)", - help: "该 SCIM 连接的哈希 Bearer 凭据——明文仅在生成令牌时显示一次。敏感信息,请勿泄露。" - }, - organization_id: { - label: "组织", - help: "该令牌的组织范围(组织级令牌将开通限制在该组织内)" - }, - user_id: { - label: "所有者", - help: "生成该令牌的用户(启用提供方归属时)" - }, - created_at: { - label: "创建时间" - }, - updated_at: { - label: "更新时间" - } - }, - _views: { - all: { - label: "全部" - } - } - }, sys_scim_connection_binding: { label: "SCIM Connection Binding", pluralLabel: "SCIM Connection Bindings", diff --git a/packages/platform-objects/src/apps/translations/zh-CN.source-hashes.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.source-hashes.generated.ts index d13d2cad75..9f218a2fcb 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.source-hashes.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.source-hashes.generated.ts @@ -439,9 +439,6 @@ export const zhCNGeneratedSourceHashes: Readonly> = { "objects.sys_scim_projection_grant.fields.user_id.label": "6c0d22a931e235bd", "objects.sys_scim_projection_grant.label": "fbf273320dc0e8cb", "objects.sys_scim_projection_grant.pluralLabel": "b8bb5769deedfbe3", - "objects.sys_scim_provider.fields.id.label": "00b0385c9c152888", - "objects.sys_scim_provider.fields.provider_key.help": "6eba9e41bfb954ab", - "objects.sys_scim_provider.fields.provider_key.label": "fbc96a8b3ed4709d", "objects.sys_scim_subject._views.all.label": "6d6f653f8264ab17", "objects.sys_scim_subject.description": "8fc839e493ef3e75", "objects.sys_scim_subject.fields.created_at.label": "1f02d416befb595b", diff --git a/packages/platform-objects/src/identity/action-predicate-sparse-face.test.ts b/packages/platform-objects/src/identity/action-predicate-sparse-face.test.ts index b7a7e8536c..8b6f1b3903 100644 --- a/packages/platform-objects/src/identity/action-predicate-sparse-face.test.ts +++ b/packages/platform-objects/src/identity/action-predicate-sparse-face.test.ts @@ -50,7 +50,6 @@ import { SysTeam } from './sys-team.object.js'; import { SysTeamMember } from './sys-team-member.object.js'; import { SysApiKey } from './sys-api-key.object.js'; import { SysSsoProvider } from './sys-sso-provider.object.js'; -import { SysScimProvider } from './sys-scim-provider.object.js'; import { SysTwoFactor } from './sys-two-factor.object.js'; import { SysAccount } from './sys-account.object.js'; import { SysSession } from './sys-session.object.js'; @@ -142,7 +141,6 @@ const OBJECTS: Array<[string, { actions?: unknown }]> = [ ['sys_team_member', SysTeamMember], ['sys_api_key', SysApiKey], ['sys_sso_provider', SysSsoProvider], - ['sys_scim_provider', SysScimProvider], ['sys_two_factor', SysTwoFactor], ['sys_account', SysAccount], ['sys_session', SysSession], diff --git a/packages/platform-objects/src/identity/index.ts b/packages/platform-objects/src/identity/index.ts index 3a6cd2f81c..d83b789196 100644 --- a/packages/platform-objects/src/identity/index.ts +++ b/packages/platform-objects/src/identity/index.ts @@ -43,10 +43,9 @@ export { SysJwks } from './sys-jwks.object.js'; export { SysSsoProvider } from './sys-sso-provider.object.js'; // ── SCIM 2.0 provisioning (@better-auth/scim) ────────────────────── -// `SysScimProvider` is the rc.1-era connection row; it retires under #11757. -export { SysScimProvider } from './sys-scim-provider.object.js'; // The stable 1.7.x model set (#3653) — seven library-managed tables plus the // ObjectStack-owned credential store the app-owned verifyBearerToken uses. +// (The rc.1-era `SysScimProvider` connection row retired under #11757.) export { SysScimConnectionBinding } from './sys-scim-connection-binding.object.js'; export { SysScimConnectionCredential } from './sys-scim-connection-credential.object.js'; export { SysScimGroup } from './sys-scim-group.object.js'; diff --git a/packages/platform-objects/src/identity/sys-scim-provider.object.ts b/packages/platform-objects/src/identity/sys-scim-provider.object.ts deleted file mode 100644 index 0252ad4b4e..0000000000 --- a/packages/platform-objects/src/identity/sys-scim-provider.object.ts +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import { ObjectSchema, Field } from '@objectstack/spec/data'; - -/** - * sys_scim_provider — Registered SCIM 2.0 connection (@better-auth/scim) - * - * Backed by `@better-auth/scim`'s `scimProvider` model. Each row is a SCIM - * connection: a bearer token an external IdP (Okta / Entra / OneLogin) uses to - * **auto-provision / deprovision** THIS environment's users. The environment is - * the SCIM **Service Provider** (the receiver); the IdP is the SCIM **client** - * (the sender). This is the paid Identity lifecycle (ADR-0071) — the mechanism - * is OPEN (here, framework `plugin-auth`); enablement is entitlement-gated by - * the cloud / EE license. - * - * ⚠️ RC.1-ERA OBJECT — retirement tracked by #11757. The stable - * `@better-auth/scim` line (#3653) derives no `scimProvider` model, exposes no - * `/scim/generate-token`, and has no `storeSCIMToken` option, so nothing - * writes rows here any more; SCIM connection credentials live in - * `sys_scim_connection_credential` (ObjectStack-owned). The paragraphs below - * describe the rc.1 behaviour this object was built for. - * - * `scim_token` holds the connection's bearer credential. With the rc.1 - * plugin's `storeSCIMToken: 'hashed'` (which this env wired) it stored only a - * HASH — the plaintext was returned exactly once at `/scim/generate-token`. - * Even so, treat this object as sensitive: it is read-only over the generic - * data API and the token is excluded from list views. - * - * All mutations route through @better-auth/scim's endpoints under - * `/api/v1/auth/scim/*` (generate-token / delete-provider-connection) and the - * SCIM 2.0 protocol under `/api/v1/auth/scim/v2/*`; the generic data layer is - * read-only (see `enable.apiMethods`). - * - * @namespace sys - */ -export const SysScimProvider = ObjectSchema.create({ - name: 'sys_scim_provider', - label: 'SCIM Provider', - pluralLabel: 'SCIM Providers', - icon: 'users', - isSystem: true, - managedBy: 'better-auth', - // [ADR-0066 D3/④] Admin-only identity config carrying a live credential - // (`scim_token` — the bearer external IdPs authenticate provisioning calls - // with). Object-level capability gate, mirroring the sibling - // `sys_sso_provider`: ordinary members are denied entirely. - // - // [#6964] The exposure this used to name — `member_default`'s - // `'*': allowRead` — no longer exists: #5491 removed that wildcard and the - // platform baseline is explicit-allow. The gate is not thereby redundant, and - // its live reason is the stronger one: `requiredPermissions` is a capability - // AND-gate evaluated BEFORE the CRUD grant (`security-plugin.ts` step 1.5), so - // a caller missing the capability is denied "regardless of how permissive - // their grants are" — including a grant an app-declared default profile or a - // customer-authored permission set names on this object. Without it the - // table's only protection would be that no set happens to grant it today. - // - // better-auth's own endpoints read via a system context, so SCIM provisioning - // is unaffected. - requiredPermissions: ['manage_platform_settings'], - // ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema. - protection: { - lock: 'full', - reason: 'Identity table managed by better-auth (@better-auth/scim) — see ADR-0071.', - docsUrl: 'https://objectstack.ai/docs/references/shared/protection', - }, - description: 'SCIM 2.0 connections (bearer tokens) external IdPs use to provision/deprovision this environment\'s users', - displayNameField: 'provider_id', - nameField: 'provider_id', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) - titleFormat: '{provider_id}', - highlightFields: ['provider_id', 'organization_id'], - - listViews: { - all: { - type: 'grid', - name: 'all', - label: 'All', - data: { provider: 'object', object: 'sys_scim_provider' }, - // scim_token is intentionally excluded — never surface the credential. - columns: ['provider_id', 'organization_id', 'created_at'], - sort: [{ field: 'provider_id', order: 'asc' }], - pagination: { pageSize: 50 }, - }, - }, - - fields: { - id: Field.text({ label: 'ID', required: true, readonly: true, group: 'System' }), - - provider_id: Field.text({ - label: 'Provider ID', - required: true, - searchable: true, - maxLength: 255, - description: 'Stable SCIM provider identifier (e.g. "okta-scim")', - group: 'Identity', - }), - - // `@better-auth/scim`'s uniqueness boundary for a connection: - // `:`, declared `required: true, unique: true, - // returned: false` upstream and written on every provider insert. Same - // shape as `sys_team_member.membership_key` — a derived key the library - // owns end to end, never authored or read from the ObjectStack side. - // Provisioned here because the adapter writes the column; without it SCIM - // provider creation fails the moment SCIM is switched on. See #3653. - provider_key: Field.text({ - label: 'Provider Key', - required: false, - readonly: true, - maxLength: 512, - description: 'Derived : uniqueness key maintained by @better-auth/scim; do not write directly.', - group: 'System', - }), - - scim_token: Field.text({ - label: 'SCIM Token (hash)', - required: false, - readonly: true, - maxLength: 1024, - description: 'Hashed bearer credential for this SCIM connection — the plaintext is shown once at generate-token. Sensitive; do not expose.', - group: 'Secret', - }), - - organization_id: Field.text({ - label: 'Organization', - required: false, - maxLength: 255, - description: 'Organization scope of this token (org-scoped tokens restrict provisioning to that org)', - group: 'System', - }), - - user_id: Field.lookup('sys_user', { - label: 'Owned By', - required: false, - description: 'User who generated this token (when provider-ownership is enabled)', - group: 'System', - }), - - created_at: Field.datetime({ label: 'Created At', defaultValue: 'NOW()', readonly: true, group: 'System' }), - updated_at: Field.datetime({ label: 'Updated At', defaultValue: 'NOW()', readonly: true, group: 'System' }), - }, - - indexes: [ - { fields: ['provider_id'], unique: true }, - { fields: ['organization_id'] }, - { fields: ['user_id'] }, - // UNIQUE mirrors @better-auth/scim's own declaration. Nullable, so rows - // provisioned before the column existed admit repeated NULLs on sqlite / - // postgres / mysql. - // - // NOTE: upstream's boundary is `:`, i.e. the - // SAME provider_id in two different organizations is legal there, while - // the `provider_id` unique index above forbids it. That is a stricter - // constraint than the library assumes, not something this column changes — - // left as-is deliberately and raised separately (#3653) rather than - // relaxed here, since loosening a live uniqueness constraint is its own - // decision. - { fields: ['provider_key'], unique: true }, - ], - - enable: { - trackHistory: true, - searchable: false, - apiEnabled: true, - // Mutations + token issuance go through @better-auth/scim's endpoints - // under /api/v1/auth/scim/*; the generic data layer is read-only so the - // credential cannot be written/bypassed through it. - apiMethods: ['list'], - }, -}); diff --git a/packages/platform-objects/src/platform-objects.test.ts b/packages/platform-objects/src/platform-objects.test.ts index cb85306fc9..52787974f0 100644 --- a/packages/platform-objects/src/platform-objects.test.ts +++ b/packages/platform-objects/src/platform-objects.test.ts @@ -14,7 +14,6 @@ import { SysOauthApplication, SysOauthRefreshToken, SysOrganization, - SysScimProvider, SysSession, SysSsoProvider, SysTeam, @@ -114,10 +113,13 @@ describe('@objectstack/platform-objects', () => { expect((object as any).access?.default).toBe('private'); }); - it('SysScimProvider is capability-gated like its sibling sys_sso_provider', () => { - // Admin config with an embedded live credential (scim_token) — the D3 - // capability gate (not the private posture) is the sso/scim pattern. - expect((SysScimProvider as any).requiredPermissions).toEqual(['manage_platform_settings']); + it('SysSsoProvider is capability-gated (ADR-0066 D3)', () => { + // Admin config with an embedded live credential (oidc_client_secret) — + // the D3 capability gate (not the private posture) is the sso pattern. + // This pin lived on SysScimProvider until that object retired (#11757); + // it moves to the surviving sibling so the gate cannot be dropped + // silently. + expect((SysSsoProvider as any).requiredPermissions).toEqual(['manage_platform_settings']); }); it('member self-service objects deliberately stay on the public posture', () => { 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 dc71d07437..931ce64398 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 @@ -119,7 +119,6 @@ import { SysScimGroupMember, SysScimIdentityTombstone, SysScimProjectionGrant, - SysScimProvider, SysScimSubject, SysScimUser, SysSsoProvider, @@ -151,10 +150,9 @@ const PLATFORM_OBJECTS: Record = Object.fromEntries( SysOrganization, SysMember, SysInvitation, SysTeam, SysTeamMember, SysTwoFactor, SysDeviceCode, SysJwks, // Bridged at the adapter layer rather than via a plugin `schema` option — - // see the sso/scim block at the bottom of this file (#3653). - // `SysScimProvider` backs no stable model any more; it retires under - // #11757 and stays here only until that lands. - SysSsoProvider, SysScimProvider, + // see the sso/scim block at the bottom of this file (#3653). (The rc.1-era + // `SysScimProvider` backed no stable model; it retired under #11757.) + SysSsoProvider, SysScimConnectionBinding, SysScimGroup, SysScimGroupMember, SysScimIdentityTombstone, SysScimProjectionGrant, SysScimSubject, SysScimUser, diff --git a/packages/plugins/plugin-auth/src/credential-at-rest-posture.test.ts b/packages/plugins/plugin-auth/src/credential-at-rest-posture.test.ts index eb16e1dbf2..81465dd3c6 100644 --- a/packages/plugins/plugin-auth/src/credential-at-rest-posture.test.ts +++ b/packages/plugins/plugin-auth/src/credential-at-rest-posture.test.ts @@ -91,7 +91,6 @@ import { SysInvitation, SysTeam, SysTeamMember, - SysScimProvider, SysScimConnectionBinding, SysScimConnectionCredential, SysScimGroup, @@ -158,7 +157,6 @@ const AUTH_OBJECTS = [ SysInvitation, SysTeam, SysTeamMember, - SysScimProvider, // The stable scim model set + the ObjectStack-owned credential store (#3653). SysScimConnectionBinding, SysScimConnectionCredential, 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 faacc4527f..dd2f28421b 100644 --- a/packages/plugins/plugin-auth/src/managed-extension-fields.test.ts +++ b/packages/plugins/plugin-auth/src/managed-extension-fields.test.ts @@ -233,15 +233,8 @@ const UNMAPPED_MANAGED_OBJECTS: Record = { + '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: - 'rc.1-era SCIM connection row. The installed stable @better-auth/scim@1.7.1 no longer derives ' - + 'a scimProvider model at all (re-measured 2026-08-27, #3653), so no better-auth column can ' - + 'change hands on this table any more; it retires under #11757 and stays only until that ' - + 'lands. (No noBetterAuthColumns flag: this object declares no extension fields, so the ' - + 'flag would assert a licence nothing uses; the parity gate\'s exact-set assertion is the ' - + 'tripwire that fires if a scimProvider model ever reappears.)', - }, + // (The rc.1-era `sys_scim_provider` exemption retired with its object under + // #11757 — the stale-entry assertion below is what forced it out.) // The stable @better-auth/scim 1.7.x model set (#3653). Same bridge shape as // sys_sso_provider: SCIMOptions still accepts no `schema`/`modelName`/`fields` // option on the installed 1.7.1 (re-measured 2026-08-27), so getAuthTables() diff --git a/packages/plugins/plugin-auth/src/manifest.ts b/packages/plugins/plugin-auth/src/manifest.ts index e741a14af1..626f093657 100644 --- a/packages/plugins/plugin-auth/src/manifest.ts +++ b/packages/plugins/plugin-auth/src/manifest.ts @@ -25,7 +25,6 @@ import { SysOrganization, SysSession, SysSsoProvider, - SysScimProvider, SysScimConnectionBinding, SysScimConnectionCredential, SysScimGroup, @@ -70,10 +69,10 @@ export const authIdentityObjects: any[] = [ SysJwks, SysDeviceCode, SysSsoProvider, - SysScimProvider, // Stable @better-auth/scim 1.7.x model set (#3653): seven library-managed // tables plus the ObjectStack-owned credential store for the app-owned - // verifyBearerToken route. SysScimProvider above retires under #11757. + // verifyBearerToken route. The rc.1-era SysScimProvider retired under + // #11757. SysScimConnectionBinding, SysScimConnectionCredential, SysScimGroup, diff --git a/packages/plugins/plugin-auth/src/objectql-adapter.ts b/packages/plugins/plugin-auth/src/objectql-adapter.ts index 82e37b00b9..4b268e9968 100644 --- a/packages/plugins/plugin-auth/src/objectql-adapter.ts +++ b/packages/plugins/plugin-auth/src/objectql-adapter.ts @@ -56,9 +56,8 @@ export const AUTH_MODEL_TO_PROTOCOL: Record = { // 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', - // rc.1's one scim model. Stable 1.7.x no longer derives it; the entry (and - // sys_scim_provider itself) retires under #11757, not here. - scimProvider: 'sys_scim_provider', + // (rc.1's one scim model, `scimProvider` → `sys_scim_provider`, retired + // under #11757: stable 1.7.x no longer derives that model.) // The stable @better-auth/scim 1.7.x model set (#3653). Verified against the // installed 1.7.1: `SCIMOptions` still declares no `schema` / `modelName` / // `fields` member, so the adapter bridge remains scim's ONLY naming route. diff --git a/packages/plugins/plugin-security/src/objects/default-permission-sets.ts b/packages/plugins/plugin-security/src/objects/default-permission-sets.ts index 500532519e..bca07f3b04 100644 --- a/packages/plugins/plugin-security/src/objects/default-permission-sets.ts +++ b/packages/plugins/plugin-security/src/objects/default-permission-sets.ts @@ -69,7 +69,6 @@ export const BETTER_AUTH_MANAGED_OBJECTS = [ 'sys_verification', 'sys_jwks', 'sys_device_code', - 'sys_scim_provider', // Stable @better-auth/scim 1.7.x model set (#3653). The ObjectStack-owned // `sys_scim_connection_credential` is deliberately NOT here — it declares // `managedBy: 'engine-owned'` (plugin-auth's SCIM connection service is its diff --git a/packages/qa/dogfood/test/authz-conformance.matrix.ts b/packages/qa/dogfood/test/authz-conformance.matrix.ts index dbe91fde12..5d7eff5355 100644 --- a/packages/qa/dogfood/test/authz-conformance.matrix.ts +++ b/packages/qa/dogfood/test/authz-conformance.matrix.ts @@ -245,7 +245,7 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ { id: 'system-permissions', summary: 'systemPermissions / tab-app gating', state: 'enforced', enforcement: 'rest/rest-server.ts filterAppForUser' }, { id: 'secure-by-default-posture', summary: 'ADR-0066 ④ — sensitive system objects opt out of the wildcard grant (access.default: private)', state: 'enforced', - enforcement: 'plugin-security/permission-evaluator.ts resolveObjectPermission (plain wildcard does not cover a private object) + posture-gated superuser bypass; declarations in platform-objects (sys_secret, sys_jwks, sys_verification, sys_oauth_access_token, sys_oauth_refresh_token, sys_device_code) + sys_scim_provider D3 capability gate', + enforcement: 'plugin-security/permission-evaluator.ts resolveObjectPermission (plain wildcard does not cover a private object) + posture-gated superuser bypass; declarations in platform-objects (sys_secret, sys_jwks, sys_verification, sys_oauth_access_token, sys_oauth_refresh_token, sys_device_code) + sys_sso_provider D3 capability gate (the gate\'s other carrier, sys_scim_provider, retired under #11757)', note: 'Primitive enforcement unit-proven in plugin-security/security-plugin.test.ts (ADR-0066 posture suite); the per-object declarations are pinned by platform-objects.test.ts "secure-by-default posture" so dropping the flag from a secret store fails CI, not review. Member self-service objects (sys_session, sys_api_key, sys_oauth_application, sys_two_factor) deliberately stay public-posture — the Account app reads them with a member context; row scoping (owner/tenant RLS + _self carve-outs) is their guard.' }, { id: 'flow-run-as', summary: 'flow runAs — data nodes execute under the run\'s effective identity (#1888)', state: 'enforced', enforcement: 'service-automation/engine.ts runAs authorization envelope → runtime-identity.ts → builtin/crud-nodes.ts (runAs:\'system\' → RLS-bypassing; runAs:\'user\' → trigger identity, RLS enforced as that user)', diff --git a/packages/spec/src/system/constants/platform-object-names.ts b/packages/spec/src/system/constants/platform-object-names.ts index 94e0e155e3..2d9b29d2e2 100644 --- a/packages/spec/src/system/constants/platform-object-names.ts +++ b/packages/spec/src/system/constants/platform-object-names.ts @@ -80,7 +80,6 @@ export const PLATFORM_OBJECTS_BY_PACKAGE: Readonly Date: Fri, 28 Aug 2026 10:44:43 +0000 Subject: [PATCH 2/2] chore(spec): register the sys_scim_provider retirement in the ADR-0087 ledger + changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Semantic entry scim-provider-object-retired (major 18), registry regions regenerated (major-18 entries project into spec-changes.json/upgrade guide only when that major ships — registry.ts is the only artifact that moves). Breaking changeset ships as minor per the launch-window convention, with the adr-0087 registered marker. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4 --- .changeset/retire-sys-scim-provider-object.md | 23 +++++++++ .../18.scim-provider-object-retired.ts | 51 +++++++++++++++++++ packages/spec/src/migrations/registry.ts | 47 +++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 .changeset/retire-sys-scim-provider-object.md create mode 100644 packages/spec/src/migrations/entries/semantic/18.scim-provider-object-retired.ts diff --git a/.changeset/retire-sys-scim-provider-object.md b/.changeset/retire-sys-scim-provider-object.md new file mode 100644 index 0000000000..37979f5c30 --- /dev/null +++ b/.changeset/retire-sys-scim-provider-object.md @@ -0,0 +1,23 @@ +--- +'@objectstack/platform-objects': minor +'@objectstack/spec': minor +'@objectstack/plugin-auth': minor +'@objectstack/plugin-security': minor +--- + +**BREAKING (platform object removed):** the `sys_scim_provider` platform object is retired (#11757, ruled on #11693 — leg 1a of the #11632 SCIM epic). + +FROM → TO, per surface: + +- `SysScimProvider` (export of `@objectstack/platform-objects` / `.../identity`) → removed, no replacement export. Fix: delete the import. Stable SCIM state lives on the seven `sys_scim_*` stable-model objects (#3653), and connection credentials on `sys_scim_connection_credential`. +- `sys_scim_provider` in `PLATFORM_PROVIDED_OBJECT_NAMES` (`@objectstack/spec/system`) → removed. `isPlatformProvidedObjectName('sys_scim_provider')` is now `false`, so a stack referencing the name is flagged as a probable typo instead of resolving. +- plugin-auth: the object is no longer provisioned, and `AUTH_MODEL_TO_PROTOCOL` carries no `scimProvider` entry — the installed stable `@better-auth/scim@1.7.1` derives no such model, so the entry bridged nothing. +- plugin-security: the `BETTER_AUTH_MANAGED_OBJECTS` write-deny entry for it is gone with the object (the list is pinned bidirectionally against `managedBy: 'better-auth'` declarations). + +The rc.1-era row was written only by the retired `/scim/generate-token` endpoint; after the stable-1.7.1 migration (PR #12726) nothing could write to it. Per the maintainer's ruling (2026-08-24, 「不需要考虑历史数据」; reaffirmed 2026-08-25 — SCIM has no real customers), **no data migration ships**: existing `sys_scim_provider` tables in deployed databases are left untouched — no backfill, no reaper, no migrate command. SCIM-enabled deployments re-register connections on the stable surface; the IdP token reissue is a migration-day operator action regardless of this change. + +The ADR-0066 D3 capability-gate pin moves from the retired object to its surviving sibling `sys_sso_provider`, so the gate posture stays test-pinned. + +Breaking ships as `minor` per the launch-window convention (`scripts/check-changeset-no-major.mjs`) and the #12726 precedent on the same ruling. + + diff --git a/packages/spec/src/migrations/entries/semantic/18.scim-provider-object-retired.ts b/packages/spec/src/migrations/entries/semantic/18.scim-provider-object-retired.ts new file mode 100644 index 0000000000..bf89a5bedd --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.scim-provider-object-retired.ts @@ -0,0 +1,51 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +export const entry: SemanticMigration = { + id: 'scim-provider-object-retired', + surface: + 'the `sys_scim_provider` platform object (`SysScimProvider` in ' + + '`@objectstack/platform-objects/identity`, re-exported from the package ' + + 'root) and its name in `PLATFORM_PROVIDED_OBJECT_NAMES` ' + + '(`@objectstack/spec/system` constants). The rc.1-era `@better-auth/scim` ' + + 'connection row: one row per SCIM bearer connection, written only by the ' + + 'retired `/scim/generate-token` endpoint.', + replacement: + '(removed — no direct replacement row. The stable `@better-auth/scim` ' + + '1.7.x line (#3653, PR #12726) derives no `scimProvider` model: SCIM ' + + 'state lives in the seven stable platform objects ' + + '(`sys_scim_connection_binding`, `sys_scim_group`, ' + + '`sys_scim_group_member`, `sys_scim_identity_tombstone`, ' + + '`sys_scim_projection_grant`, `sys_scim_subject`, `sys_scim_user`) and ' + + 'connection credentials in the ObjectStack-owned ' + + '`sys_scim_connection_credential`, minted/verified by ' + + '`scim-connection-service.ts` behind the application-owned ' + + '`verifyBearerToken`. A SCIM-enabled deployment re-registers its ' + + 'connections on the stable surface; rc.1 token digests are not portable ' + + 'on any path, so the IdP reissues its token — a migration-day operator ' + + 'action, not a code rewrite.)', + reason: + 'Maintainer ruling 2026-08-24 on #11693 (verbatim: 「11700 11693 不需要考虑' + + '历史数据,其他按照你的建议继续」) — disposition A: retire, with no ' + + 'data-migration path owed for existing rows (reaffirmed 2026-08-25: SCIM ' + + 'has no real customers; the binding constraint is a smooth upgrade). ' + + 'Executed as #11757 after the stable-1.7.1 migration landed (#3653 / ' + + 'PR #12726): the installed library derives no `scimProvider` model, so ' + + 'the object backed nothing — nothing could write a row to it any more. ' + + 'Retiring it also removes its `provider_id` unique index, whose ' + + 'stricter-than-upstream uniqueness was flagged on #3653 and parked ' + + 'pending exactly this retirement.', + acceptanceCriteria: + 'No code imports `SysScimProvider` from `@objectstack/platform-objects` ' + + '(TS2305 after upgrade); `isPlatformProvidedObjectName(\'sys_scim_provider\')` ' + + 'returns false, so a stack referencing the name is flagged as a probable ' + + 'typo rather than resolved; plugin-auth provisions no `sys_scim_provider` ' + + 'object and `AUTH_MODEL_TO_PROTOCOL` carries no `scimProvider` entry; the ' + + 'spec registry conformance test (`platform-object-names.test.ts`) pins ' + + 'the absence bidirectionally — re-adding either the object file or the ' + + 'registry name alone reds `registry group "platform-objects" is out of ' + + 'date` (measured both ways on #11757). Existing `sys_scim_provider` ' + + 'tables in deployed databases are left in place untouched, by ruling — ' + + 'no backfill, no reaper, no migrate command.', +}; diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 71b1b31ea3..c2350410a9 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -6987,6 +6987,53 @@ const step18: MigrationStep = { + '`collapsible: true` explicitly — an unset key now defers to the renderer, which does ' + 'not collapse.', }, + { + id: 'scim-provider-object-retired', + surface: + 'the `sys_scim_provider` platform object (`SysScimProvider` in ' + + '`@objectstack/platform-objects/identity`, re-exported from the package ' + + 'root) and its name in `PLATFORM_PROVIDED_OBJECT_NAMES` ' + + '(`@objectstack/spec/system` constants). The rc.1-era `@better-auth/scim` ' + + 'connection row: one row per SCIM bearer connection, written only by the ' + + 'retired `/scim/generate-token` endpoint.', + replacement: + '(removed — no direct replacement row. The stable `@better-auth/scim` ' + + '1.7.x line (#3653, PR #12726) derives no `scimProvider` model: SCIM ' + + 'state lives in the seven stable platform objects ' + + '(`sys_scim_connection_binding`, `sys_scim_group`, ' + + '`sys_scim_group_member`, `sys_scim_identity_tombstone`, ' + + '`sys_scim_projection_grant`, `sys_scim_subject`, `sys_scim_user`) and ' + + 'connection credentials in the ObjectStack-owned ' + + '`sys_scim_connection_credential`, minted/verified by ' + + '`scim-connection-service.ts` behind the application-owned ' + + '`verifyBearerToken`. A SCIM-enabled deployment re-registers its ' + + 'connections on the stable surface; rc.1 token digests are not portable ' + + 'on any path, so the IdP reissues its token — a migration-day operator ' + + 'action, not a code rewrite.)', + reason: + 'Maintainer ruling 2026-08-24 on #11693 (verbatim: 「11700 11693 不需要考虑' + + '历史数据,其他按照你的建议继续」) — disposition A: retire, with no ' + + 'data-migration path owed for existing rows (reaffirmed 2026-08-25: SCIM ' + + 'has no real customers; the binding constraint is a smooth upgrade). ' + + 'Executed as #11757 after the stable-1.7.1 migration landed (#3653 / ' + + 'PR #12726): the installed library derives no `scimProvider` model, so ' + + 'the object backed nothing — nothing could write a row to it any more. ' + + 'Retiring it also removes its `provider_id` unique index, whose ' + + 'stricter-than-upstream uniqueness was flagged on #3653 and parked ' + + 'pending exactly this retirement.', + acceptanceCriteria: + 'No code imports `SysScimProvider` from `@objectstack/platform-objects` ' + + '(TS2305 after upgrade); `isPlatformProvidedObjectName(\'sys_scim_provider\')` ' + + 'returns false, so a stack referencing the name is flagged as a probable ' + + 'typo rather than resolved; plugin-auth provisions no `sys_scim_provider` ' + + 'object and `AUTH_MODEL_TO_PROTOCOL` carries no `scimProvider` entry; the ' + + 'spec registry conformance test (`platform-object-names.test.ts`) pins ' + + 'the absence bidirectionally — re-adding either the object file or the ' + + 'registry name alone reds `registry group "platform-objects" is out of ' + + 'date` (measured both ways on #11757). Existing `sys_scim_provider` ' + + 'tables in deployed databases are left in place untouched, by ruling — ' + + 'no backfill, no reaper, no migrate command.', + }, { id: 'send-template-input-org-retired', surface: 'contracts.emailService.sendTemplate input.org',