From c87d731f6767ad3078bf322c18ca07e635870850 Mon Sep 17 00:00:00 2001 From: os-litant Date: Thu, 27 Aug 2026 09:53:45 +0000 Subject: [PATCH 1/5] chore(deps): move the @better-auth/scim pin off the rc onto the stable line (measurement base, #3653) Part of #3653. NOT a proposed landing: leg 2' stopped on a scope fork before the provisioning half could be written, so this branch carries the pin move alone as the reproducible base every measurement in the report was taken on. A pin-move-only tree is red by construction (7 failed | 21 passed on the parity gate) and must never be landed on its own. - packages/plugins/plugin-auth/package.json 1.7.0-rc.1 -> ^1.7.1 - pnpm-workspace.yaml '@better-auth/scim@<1.7.0-rc.1': '1.7.0-rc.1' -> '@better-auth/scim@<2.0.0': '^1.7.1' (major-boundary bound, matching the rest of the family per this file's own doctrine; the old bound sat AT the rc precisely so a <2.0.0 bound could not rewrite the stable release down onto it.) Measured consequences, all reproducible from this commit: - '^1.7.1' resolves to 1.7.2, not 1.7.1. Every prior measurement on this epic was taken against 1.7.1. Re-measured on 1.7.2: same seven models, same three-way constructor predicate, same absent scimProvider. - scim lands on 1.7.2 while the rest of the family stays 1.7.1. 1.7.2 peers '@better-auth/core': '^1.7.2' and 'better-auth': '^1.7.2', which 1.7.1 does not satisfy; this repo's existing overrides rewrite those peer ranges down to '^1.7.1' in the lockfile, so nothing reports the mismatch. One copy each of better-auth and @better-auth/core remain in the store. - check:prerelease-pins retires itself: "No prerelease pin left in pnpm-workspace.yaml overrides - nothing to watch", exit 0. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_0194kbQJxUvv2yvsGRtuXpP5 --- packages/plugins/plugin-auth/package.json | 2 +- pnpm-lock.yaml | 15 ++++++++------- pnpm-workspace.yaml | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/plugins/plugin-auth/package.json b/packages/plugins/plugin-auth/package.json index 2342f57894..d89451f9a3 100644 --- a/packages/plugins/plugin-auth/package.json +++ b/packages/plugins/plugin-auth/package.json @@ -26,7 +26,7 @@ "dependencies": { "@better-auth/core": "^1.7.1", "@better-auth/oauth-provider": "^1.7.1", - "@better-auth/scim": "1.7.0-rc.1", + "@better-auth/scim": "^1.7.1", "@better-auth/sso": "^1.7.1", "@noble/hashes": "^2.3.0", "@objectstack/core": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 793bc67e3a..ed89ca6619 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,7 +12,7 @@ overrides: undici@>=7.23.0 <8.0.0: ^7.29.0 better-auth@<2.0.0: ^1.7.1 '@better-auth/core@<2.0.0': ^1.7.1 - '@better-auth/scim@<1.7.0-rc.1': 1.7.0-rc.1 + '@better-auth/scim@<2.0.0': ^1.7.1 '@better-auth/oauth-provider@<2.0.0': ^1.7.1 '@better-auth/sso@<2.0.0': ^1.7.1 '@better-auth/drizzle-adapter@<2.0.0': ^1.7.1 @@ -1500,8 +1500,8 @@ importers: specifier: ^1.7.1 version: 1.7.1(@better-auth/core@1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.4)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.7.1(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.11.1)(mongodb@7.5.0(socks@2.8.9))(mysql2@3.23.3(@types/node@26.2.0))(next@16.3.1(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(svelte@5.56.9(@typescript-eslint/types@8.67.0))(vitest@4.1.10))(better-call@1.4.0(zod@4.4.3)) '@better-auth/scim': - specifier: 1.7.0-rc.1 - version: 1.7.0-rc.1(@better-auth/core@1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.4)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(better-auth@1.7.1(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.11.1)(mongodb@7.5.0(socks@2.8.9))(mysql2@3.23.3(@types/node@26.2.0))(next@16.3.1(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(svelte@5.56.9(@typescript-eslint/types@8.67.0))(vitest@4.1.10))(better-call@1.4.0(zod@4.4.3)) + specifier: ^1.7.1 + version: 1.7.2(@better-auth/core@1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.4)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(better-auth@1.7.1(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.11.1)(mongodb@7.5.0(socks@2.8.9))(mysql2@3.23.3(@types/node@26.2.0))(next@16.3.1(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(svelte@5.56.9(@typescript-eslint/types@8.67.0))(vitest@4.1.10))(better-call@1.4.0(zod@4.4.3)) '@better-auth/sso': specifier: ^1.7.1 version: 1.7.1(@better-auth/core@1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.4)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.7.1(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.11.1)(mongodb@7.5.0(socks@2.8.9))(mysql2@3.23.3(@types/node@26.2.0))(next@16.3.1(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(svelte@5.56.9(@typescript-eslint/types@8.67.0))(vitest@4.1.10))(better-call@1.4.0(zod@4.4.3)) @@ -3087,13 +3087,13 @@ packages: prisma: optional: true - '@better-auth/scim@1.7.0-rc.1': - resolution: {integrity: sha512-pcnliU2eewYq2SF4cRDn1XvQ2I7+WhufoDv5lx9yH7fmrfsU6mYQpZX3mu2Fj/AFCHauCI7Ld617pnz5+yTtOw==} + '@better-auth/scim@1.7.2': + resolution: {integrity: sha512-RBwVN189w37xwK0n+mRgARTmJFCriOsMVCbe7Bzeyak6esSg9kTGeCUmGtfgwOKJ3jJWQkeAQdWw7VRJ/HOG6w==} peerDependencies: '@better-auth/core': ^1.7.1 '@better-auth/utils': 0.4.2 better-auth: ^1.7.1 - better-call: 1.3.7 + better-call: 1.4.0 '@better-auth/sso@1.7.1': resolution: {integrity: sha512-fkGNMO8W5uNJSHAlvSe4Gxm1NVTQT4JFS7JpCNIxUAl5UyskE1yFXc2NH0KjcvSbR1RfgrqUgUevOjOjcSUKcg==} @@ -9592,10 +9592,11 @@ snapshots: '@better-auth/core': 1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.4)(nanostores@1.4.0) '@better-auth/utils': 0.4.2 - '@better-auth/scim@1.7.0-rc.1(@better-auth/core@1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.4)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(better-auth@1.7.1(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.11.1)(mongodb@7.5.0(socks@2.8.9))(mysql2@3.23.3(@types/node@26.2.0))(next@16.3.1(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(svelte@5.56.9(@typescript-eslint/types@8.67.0))(vitest@4.1.10))(better-call@1.4.0(zod@4.4.3))': + '@better-auth/scim@1.7.2(@better-auth/core@1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.4)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(better-auth@1.7.1(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.11.1)(mongodb@7.5.0(socks@2.8.9))(mysql2@3.23.3(@types/node@26.2.0))(next@16.3.1(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(svelte@5.56.9(@typescript-eslint/types@8.67.0))(vitest@4.1.10))(better-call@1.4.0(zod@4.4.3))': dependencies: '@better-auth/core': 1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.4)(nanostores@1.4.0) '@better-auth/utils': 0.4.2 + '@noble/hashes': 2.3.0 better-auth: 1.7.1(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.11.1)(mongodb@7.5.0(socks@2.8.9))(mysql2@3.23.3(@types/node@26.2.0))(next@16.3.1(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(svelte@5.56.9(@typescript-eslint/types@8.67.0))(vitest@4.1.10) better-call: 1.4.0(zod@4.4.3) zod: 4.4.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cdc713543f..126e35a476 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -184,7 +184,7 @@ overrides: # ships the rc.2 rewrite, so adopting it is the ADR-0071 migration (#3653), # not this bump. Its bound stays AT the rc on purpose: a major-boundary bound # here would rewrite the stable release down onto the rc. - '@better-auth/scim@<1.7.0-rc.1': '1.7.0-rc.1' + '@better-auth/scim@<2.0.0': '^1.7.1' '@better-auth/oauth-provider@<2.0.0': '^1.7.1' '@better-auth/sso@<2.0.0': '^1.7.1' '@better-auth/drizzle-adapter@<2.0.0': '^1.7.1' From 4533e02d9fca0e8ec560a829c405cbd99da9ffdd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 13:46:04 +0000 Subject: [PATCH 2/5] =?UTF-8?q?feat(auth,platform-objects,spec):=20migrate?= =?UTF-8?q?=20@better-auth/scim=20to=20stable=201.7.1=20=E2=80=94=20provis?= =?UTF-8?q?ion=20the=20seven=20models,=20app-owned=20bearer=20verification?= =?UTF-8?q?=20(#3653)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of #3653 (leg 2' of epic #11632). The pin resolves to 1.7.1 EXACTLY (not ^1.7.1, which resolves to 1.7.2 whose better-auth/core ^1.7.2 peers only the workspace overrides' silencing would 'satisfy'; floating is its own follow-up card). - pnpm-workspace.yaml + plugin-auth: exact 1.7.1 pin, stale rc-hold comment blocks rewritten to the landed state. - packages/platform-objects/src/identity/: the seven stable models provisioned as platform objects (scimConnectionBinding, scimGroup, scimGroupMember, scimIdentityTombstone, scimProjectionGrant, scimSubject, scimUser) plus sys_scim_connection_credential, the ObjectStack-owned credential store for the app-owned verifyBearerToken route (stable upstream stores no credential at all). Translation bundles regenerated with pnpm i18n:extract only. - packages/spec platform-object-names.ts: eight additive registry names (the measured-unsplittable spec edit; sys_scim_provider stays — its retirement is #11757's). - plugin-security BETTER_AUTH_MANAGED_OBJECTS: the seven library-managed names, forced additively by the bidirectional drift pin (default-permission-sets.test.ts) the moment the objects declare managedBy: 'better-auth'. - plugin-auth scim-connection-service.ts: mint + digest + verify owned outright; at rest only HMAC-SHA-256(secret, domain-separated bearer), base64url — at parity or better than the rc.1 unsalted SHA-256. - auth-manager.ts: stable constructor shape — connections: [] plus authentication.verifyBearerToken resolving the connection from a sys_scim_connection_credential row at request time; auth secret resolved once (memoized) so digests and better-auth share key material. - objectql-adapter.ts: seven AUTH_MODEL_TO_PROTOCOL bridge entries, and NATIVE adapter transactions over engine.transaction({ require: true }) — stable scim refuses the sequential fallback by assertion. - better-auth-schema-parity.test.ts: KNOWN_UNMAPPED_MODELS shrinks to the empty set (exact-set assertion kept, both directions). - credential-at-rest-posture.test.ts: restructured honestly — the upstream-cleartext control arm is no longer measurable (upstream stores nothing); its successor pins that vacating fact plus the keyed digest relationship, negatives for the unkeyed/wrong-key/undomained shapes, and live 200/401 rejection paths incl. revocation and expiry. Co-authored-by: Claude --- .../scripts/i18n-extract.config.ts | 18 + .../translations/bundle-ownership.test.ts | 5 + .../apps/translations/en.objects.generated.ts | 415 +++++++++++++++ .../translations/es-ES.objects.generated.ts | 415 +++++++++++++++ .../es-ES.source-hashes.generated.ts | 171 +++++++ .../translations/ja-JP.objects.generated.ts | 415 +++++++++++++++ .../ja-JP.source-hashes.generated.ts | 171 +++++++ .../translations/zh-CN.objects.generated.ts | 415 +++++++++++++++ .../zh-CN.source-hashes.generated.ts | 171 +++++++ .../platform-objects/src/identity/index.ts | 11 + .../sys-scim-connection-binding.object.ts | 177 +++++++ .../sys-scim-connection-credential.object.ts | 162 ++++++ .../identity/sys-scim-group-member.object.ts | 95 ++++ .../src/identity/sys-scim-group.object.ts | 142 ++++++ .../sys-scim-identity-tombstone.object.ts | 114 +++++ .../sys-scim-projection-grant.object.ts | 138 +++++ .../src/identity/sys-scim-subject.object.ts | 89 ++++ .../src/identity/sys-scim-user.object.ts | 233 +++++++++ packages/plugins/plugin-auth/package.json | 2 +- .../plugins/plugin-auth/src/auth-manager.ts | 61 ++- .../plugin-auth/src/auth-schema-config.ts | 13 +- .../src/better-auth-schema-parity.test.ts | 63 ++- .../src/credential-at-rest-posture.test.ts | 479 ++++++++---------- .../src/managed-extension-fields.test.ts | 63 ++- packages/plugins/plugin-auth/src/manifest.ts | 19 + .../plugin-auth/src/objectql-adapter.ts | 62 ++- .../src/scim-connection-service.ts | 185 +++++++ .../src/objects/default-permission-sets.ts | 18 +- .../system/constants/platform-object-names.ts | 8 + pnpm-lock.yaml | 12 +- pnpm-workspace.yaml | 96 ++-- 31 files changed, 4046 insertions(+), 392 deletions(-) create mode 100644 packages/platform-objects/src/identity/sys-scim-connection-binding.object.ts create mode 100644 packages/platform-objects/src/identity/sys-scim-connection-credential.object.ts create mode 100644 packages/platform-objects/src/identity/sys-scim-group-member.object.ts create mode 100644 packages/platform-objects/src/identity/sys-scim-group.object.ts create mode 100644 packages/platform-objects/src/identity/sys-scim-identity-tombstone.object.ts create mode 100644 packages/platform-objects/src/identity/sys-scim-projection-grant.object.ts create mode 100644 packages/platform-objects/src/identity/sys-scim-subject.object.ts create mode 100644 packages/platform-objects/src/identity/sys-scim-user.object.ts create mode 100644 packages/plugins/plugin-auth/src/scim-connection-service.ts diff --git a/packages/platform-objects/scripts/i18n-extract.config.ts b/packages/platform-objects/scripts/i18n-extract.config.ts index 73a8ff66a0..0e3f6ad3e0 100644 --- a/packages/platform-objects/scripts/i18n-extract.config.ts +++ b/packages/platform-objects/scripts/i18n-extract.config.ts @@ -104,6 +104,14 @@ import { SysJwks, SysSsoProvider, SysScimProvider, + SysScimConnectionBinding, + SysScimConnectionCredential, + SysScimGroup, + SysScimGroupMember, + SysScimIdentityTombstone, + SysScimProjectionGrant, + SysScimSubject, + SysScimUser, } from '../src/identity/index.js'; // ── Security ────────────────────────────────────────────────────────────── @@ -241,6 +249,16 @@ const config: ObjectStackDefinition = defineStack({ SysJwks, SysSsoProvider, SysScimProvider, + // Stable @better-auth/scim 1.7.x model set + the ObjectStack-owned + // credential store (#3653). SysScimProvider above retires under #11757. + SysScimConnectionBinding, + SysScimConnectionCredential, + SysScimGroup, + SysScimGroupMember, + SysScimIdentityTombstone, + SysScimProjectionGrant, + SysScimSubject, + SysScimUser, // Security: RBAC moved to @objectstack/plugin-security, sharing to // @objectstack/plugin-sharing (ADR-0029 K2 / D8). 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 638ee68bea..e13780e30c 100644 --- a/packages/platform-objects/src/apps/translations/bundle-ownership.test.ts +++ b/packages/platform-objects/src/apps/translations/bundle-ownership.test.ts @@ -26,6 +26,11 @@ const OWNED_OBJECTS = new Set([ '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 — stable @better-auth/scim 1.7.x model set + the ObjectStack-owned + // credential store (#3653; sys_scim_provider above retires 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', // audit / messaging-adjacent (still owned here) 'sys_notification', 'sys_attachment', 'sys_email', 'sys_email_template', 'sys_saved_report', 'sys_report_schedule', 'sys_job', 'sys_job_run', 'sys_job_queue', 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 44ba384370..e74fe0bb51 100644 --- a/packages/platform-objects/src/apps/translations/en.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/en.objects.generated.ts @@ -1935,6 +1935,421 @@ export const enObjects: NonNullable = { } } }, + sys_scim_connection_binding: { + label: "SCIM Connection Binding", + pluralLabel: "SCIM Connection Bindings", + description: "Lifecycle state for a SCIM provisioning connection, including its decommission state machine", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID", + help: "Immutable SCIM connection identifier that scopes every provisioned resource" + }, + connection_key: { + label: "Connection Key", + help: "Derived connection uniqueness key maintained by @better-auth/scim; do not write directly." + }, + provisioning_domain_id: { + label: "Provisioning Domain", + help: "Application-owned boundary that receives provisioned resources (defaults to the connection id)" + }, + decommission_status: { + label: "Decommission Status", + help: "Connection lifecycle state: active | reconciling | complete" + }, + decommissioned_at: { + label: "Decommissioned At" + }, + decommission_cursor_user_id: { + label: "Decommission Cursor User", + help: "Resumable reconciliation cursor maintained by the library" + }, + decommission_reconciled_user_count: { + label: "Reconciled Users" + }, + decommission_batch_count: { + label: "Decommission Batches" + }, + decommission_revision: { + label: "Decommission Revision", + help: "Optimistic-concurrency revision for the decommission state machine" + }, + decommission_completed_at: { + label: "Decommission Completed At" + }, + decommission_lease_id: { + label: "Decommission Lease", + help: "Single-worker reconciliation lease maintained by the library" + }, + decommission_lease_expires_at: { + label: "Decommission Lease Expires At" + }, + created_at: { + label: "Created At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_connection_credential: { + label: "SCIM Connection Credential", + pluralLabel: "SCIM Connection Credentials", + description: "Bearer credentials (one-way digests) that authenticate SCIM provisioning connections", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID", + help: "The SCIM connection this credential authenticates (e.g. \"okta-prod\"); scopes every resource the IdP provisions with it" + }, + provisioning_domain_id: { + label: "Provisioning Domain", + help: "Application-owned boundary receiving provisioned resources; defaults to the connection id when absent" + }, + organization_id: { + label: "Organization", + help: "Organization scope of this connection, when provisioning is org-scoped" + }, + label: { + label: "Label", + help: "Operator-facing name for this credential (e.g. \"rotation 2026-Q3\")" + }, + token_digest: { + label: "Token Digest", + help: "HMAC-SHA-256 (base64url) of the bearer, keyed by the deployment auth secret — one-way; the plaintext is shown once at mint and never stored." + }, + active: { + label: "Active", + help: "Revocation switch — an inactive credential is refused at verification" + }, + expires_at: { + label: "Expires At", + help: "Optional hard expiry for staged credential rotation; an expired credential is refused" + }, + user_id: { + label: "Minted By", + help: "User who minted this credential" + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_group: { + label: "SCIM Group", + pluralLabel: "SCIM Groups", + description: "IdP-pushed SCIM 2.0 groups, scoped per provisioning connection", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID" + }, + provisioning_domain_id: { + label: "Provisioning Domain" + }, + revision: { + label: "Revision", + help: "Optimistic-concurrency revision maintained by @better-auth/scim" + }, + display_name: { + label: "Display Name", + help: "SCIM displayName as sent by the IdP" + }, + display_name_key: { + label: "Display Name Key", + help: "Derived case-folded displayName uniqueness key maintained by @better-auth/scim; do not write directly." + }, + external_id: { + label: "External ID", + help: "IdP-assigned externalId, when the IdP sends one" + }, + external_id_key: { + label: "External ID Key", + help: "Derived externalId uniqueness key maintained by @better-auth/scim; do not write directly." + }, + order_key: { + label: "Order Key", + help: "Derived stable-pagination key maintained by @better-auth/scim; do not write directly." + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_group_member: { + label: "SCIM Group Member", + pluralLabel: "SCIM Group Members", + description: "SCIM group membership rows pushed by the IdP (group ↔ provisioned user)", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID" + }, + group_id: { + label: "Group" + }, + scim_user_id: { + label: "SCIM User" + }, + membership_key: { + label: "Membership Key", + help: "Derived membership uniqueness key maintained by @better-auth/scim; do not write directly." + }, + created_at: { + label: "Created At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_identity_tombstone: { + label: "SCIM Identity Tombstone", + pluralLabel: "SCIM Identity Tombstones", + description: "Tombstones of IdP-deleted SCIM identities, kept so a re-provision is recognized", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID" + }, + provisioning_domain_id: { + label: "Provisioning Domain" + }, + external_id: { + label: "External ID", + help: "The IdP-assigned externalId of the deleted identity" + }, + external_id_key: { + label: "External ID Key", + help: "Derived externalId uniqueness key maintained by @better-auth/scim; do not write directly." + }, + user_id: { + label: "User", + help: "The platform user the deleted identity was linked to" + }, + profile: { + label: "Final Profile", + help: "Serialized final SCIM profile at deletion time, maintained by @better-auth/scim" + }, + deleted_at: { + label: "Deleted At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_projection_grant: { + label: "SCIM Projection Grant", + pluralLabel: "SCIM Projection Grants", + description: "Role/entitlement grants projected onto platform users by SCIM provisioning", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID" + }, + provisioning_domain_id: { + label: "Provisioning Domain" + }, + scim_user_id: { + label: "SCIM User", + help: "The per-connection user projection this grant belongs to" + }, + user_id: { + label: "User", + help: "The platform user the grant is projected onto" + }, + source_kind: { + label: "Source Kind", + help: "What projected the grant (e.g. group, entitlement, attribute)" + }, + source_id: { + label: "Source ID", + help: "Identifier of the projecting source (e.g. the SCIM group id)" + }, + source_value: { + label: "Source Value", + help: "Source attribute value, when the source kind carries one" + }, + role: { + label: "Role", + help: "The role/entitlement projected onto the user" + }, + grant_key: { + label: "Grant Key", + help: "Derived grant uniqueness key maintained by @better-auth/scim; do not write directly." + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_subject: { + label: "SCIM Subject", + pluralLabel: "SCIM Subjects", + description: "Per-user SCIM provisioning link — one row per user any SCIM connection provisions", + fields: { + id: { + label: "ID" + }, + user_id: { + label: "User", + help: "The platform user this subject row tracks (unique — one row per user)" + }, + profile_source_id: { + label: "Profile Source", + help: "The sys_scim_user projection currently sourcing this user's profile" + }, + revision: { + label: "Revision", + help: "Optimistic-concurrency revision maintained by @better-auth/scim" + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_user: { + label: "SCIM User", + pluralLabel: "SCIM Users", + description: "Per-connection projection of an IdP-provisioned user (SCIM 2.0 /Users)", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID", + help: "SCIM connection that provisioned this user" + }, + provisioning_domain_id: { + label: "Provisioning Domain" + }, + user_id: { + label: "User", + help: "The platform user this projection materialized as" + }, + connection_user_key: { + label: "Connection User Key", + help: "Derived (connection, user) uniqueness key maintained by @better-auth/scim; do not write directly." + }, + user_name: { + label: "User Name", + help: "SCIM userName as sent by the IdP" + }, + user_name_key: { + label: "User Name Key", + help: "Derived case-folded userName uniqueness key maintained by @better-auth/scim; do not write directly." + }, + primary_email: { + label: "Primary Email" + }, + work_email_value_index: { + label: "Work Email Index", + help: "Derived lookup index maintained by @better-auth/scim; do not write directly." + }, + email_value_index: { + label: "Email Index", + help: "Derived lookup index maintained by @better-auth/scim; do not write directly." + }, + display_name: { + label: "Display Name" + }, + formatted_name: { + label: "Formatted Name" + }, + given_name: { + label: "Given Name" + }, + family_name: { + label: "Family Name" + }, + serialized_emails: { + label: "Emails (serialized)", + help: "Canonical serialized SCIM emails list maintained by @better-auth/scim; do not write directly." + }, + serialized_attributes: { + label: "Attributes (serialized)", + help: "Canonical serialized SCIM attributes maintained by @better-auth/scim; do not write directly." + }, + external_id: { + label: "External ID", + help: "IdP-assigned externalId, when the IdP sends one" + }, + external_id_key: { + label: "External ID Key", + help: "Derived externalId uniqueness key maintained by @better-auth/scim; do not write directly." + }, + active: { + label: "Active", + help: "SCIM active flag — false means the IdP deactivated this user" + }, + order_key: { + label: "Order Key", + help: "Derived stable-pagination key maintained by @better-auth/scim; do not write directly." + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, sys_notification: { label: "Notification Event", pluralLabel: "Notification Events", 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 39f7fb45b9..f72b25613d 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 @@ -1935,6 +1935,421 @@ export const esESObjects: NonNullable = { } } }, + sys_scim_connection_binding: { + label: "SCIM Connection Binding", + pluralLabel: "SCIM Connection Bindings", + description: "Lifecycle state for a SCIM provisioning connection, including its decommission state machine", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID", + help: "Immutable SCIM connection identifier that scopes every provisioned resource" + }, + connection_key: { + label: "Connection Key", + help: "Derived connection uniqueness key maintained by @better-auth/scim; do not write directly." + }, + provisioning_domain_id: { + label: "Provisioning Domain", + help: "Application-owned boundary that receives provisioned resources (defaults to the connection id)" + }, + decommission_status: { + label: "Decommission Status", + help: "Connection lifecycle state: active | reconciling | complete" + }, + decommissioned_at: { + label: "Decommissioned At" + }, + decommission_cursor_user_id: { + label: "Decommission Cursor User", + help: "Resumable reconciliation cursor maintained by the library" + }, + decommission_reconciled_user_count: { + label: "Reconciled Users" + }, + decommission_batch_count: { + label: "Decommission Batches" + }, + decommission_revision: { + label: "Decommission Revision", + help: "Optimistic-concurrency revision for the decommission state machine" + }, + decommission_completed_at: { + label: "Decommission Completed At" + }, + decommission_lease_id: { + label: "Decommission Lease", + help: "Single-worker reconciliation lease maintained by the library" + }, + decommission_lease_expires_at: { + label: "Decommission Lease Expires At" + }, + created_at: { + label: "Created At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_connection_credential: { + label: "SCIM Connection Credential", + pluralLabel: "SCIM Connection Credentials", + description: "Bearer credentials (one-way digests) that authenticate SCIM provisioning connections", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID", + help: "The SCIM connection this credential authenticates (e.g. \"okta-prod\"); scopes every resource the IdP provisions with it" + }, + provisioning_domain_id: { + label: "Provisioning Domain", + help: "Application-owned boundary receiving provisioned resources; defaults to the connection id when absent" + }, + organization_id: { + label: "Organization", + help: "Organization scope of this connection, when provisioning is org-scoped" + }, + label: { + label: "Label", + help: "Operator-facing name for this credential (e.g. \"rotation 2026-Q3\")" + }, + token_digest: { + label: "Token Digest", + help: "HMAC-SHA-256 (base64url) of the bearer, keyed by the deployment auth secret — one-way; the plaintext is shown once at mint and never stored." + }, + active: { + label: "Active", + help: "Revocation switch — an inactive credential is refused at verification" + }, + expires_at: { + label: "Expires At", + help: "Optional hard expiry for staged credential rotation; an expired credential is refused" + }, + user_id: { + label: "Minted By", + help: "User who minted this credential" + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_group: { + label: "SCIM Group", + pluralLabel: "SCIM Groups", + description: "IdP-pushed SCIM 2.0 groups, scoped per provisioning connection", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID" + }, + provisioning_domain_id: { + label: "Provisioning Domain" + }, + revision: { + label: "Revision", + help: "Optimistic-concurrency revision maintained by @better-auth/scim" + }, + display_name: { + label: "Display Name", + help: "SCIM displayName as sent by the IdP" + }, + display_name_key: { + label: "Display Name Key", + help: "Derived case-folded displayName uniqueness key maintained by @better-auth/scim; do not write directly." + }, + external_id: { + label: "External ID", + help: "IdP-assigned externalId, when the IdP sends one" + }, + external_id_key: { + label: "External ID Key", + help: "Derived externalId uniqueness key maintained by @better-auth/scim; do not write directly." + }, + order_key: { + label: "Order Key", + help: "Derived stable-pagination key maintained by @better-auth/scim; do not write directly." + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_group_member: { + label: "SCIM Group Member", + pluralLabel: "SCIM Group Members", + description: "SCIM group membership rows pushed by the IdP (group ↔ provisioned user)", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID" + }, + group_id: { + label: "Group" + }, + scim_user_id: { + label: "SCIM User" + }, + membership_key: { + label: "Membership Key", + help: "Derived membership uniqueness key maintained by @better-auth/scim; do not write directly." + }, + created_at: { + label: "Created At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_identity_tombstone: { + label: "SCIM Identity Tombstone", + pluralLabel: "SCIM Identity Tombstones", + description: "Tombstones of IdP-deleted SCIM identities, kept so a re-provision is recognized", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID" + }, + provisioning_domain_id: { + label: "Provisioning Domain" + }, + external_id: { + label: "External ID", + help: "The IdP-assigned externalId of the deleted identity" + }, + external_id_key: { + label: "External ID Key", + help: "Derived externalId uniqueness key maintained by @better-auth/scim; do not write directly." + }, + user_id: { + label: "User", + help: "The platform user the deleted identity was linked to" + }, + profile: { + label: "Final Profile", + help: "Serialized final SCIM profile at deletion time, maintained by @better-auth/scim" + }, + deleted_at: { + label: "Deleted At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_projection_grant: { + label: "SCIM Projection Grant", + pluralLabel: "SCIM Projection Grants", + description: "Role/entitlement grants projected onto platform users by SCIM provisioning", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID" + }, + provisioning_domain_id: { + label: "Provisioning Domain" + }, + scim_user_id: { + label: "SCIM User", + help: "The per-connection user projection this grant belongs to" + }, + user_id: { + label: "User", + help: "The platform user the grant is projected onto" + }, + source_kind: { + label: "Source Kind", + help: "What projected the grant (e.g. group, entitlement, attribute)" + }, + source_id: { + label: "Source ID", + help: "Identifier of the projecting source (e.g. the SCIM group id)" + }, + source_value: { + label: "Source Value", + help: "Source attribute value, when the source kind carries one" + }, + role: { + label: "Role", + help: "The role/entitlement projected onto the user" + }, + grant_key: { + label: "Grant Key", + help: "Derived grant uniqueness key maintained by @better-auth/scim; do not write directly." + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_subject: { + label: "SCIM Subject", + pluralLabel: "SCIM Subjects", + description: "Per-user SCIM provisioning link — one row per user any SCIM connection provisions", + fields: { + id: { + label: "ID" + }, + user_id: { + label: "User", + help: "The platform user this subject row tracks (unique — one row per user)" + }, + profile_source_id: { + label: "Profile Source", + help: "The sys_scim_user projection currently sourcing this user's profile" + }, + revision: { + label: "Revision", + help: "Optimistic-concurrency revision maintained by @better-auth/scim" + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_user: { + label: "SCIM User", + pluralLabel: "SCIM Users", + description: "Per-connection projection of an IdP-provisioned user (SCIM 2.0 /Users)", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID", + help: "SCIM connection that provisioned this user" + }, + provisioning_domain_id: { + label: "Provisioning Domain" + }, + user_id: { + label: "User", + help: "The platform user this projection materialized as" + }, + connection_user_key: { + label: "Connection User Key", + help: "Derived (connection, user) uniqueness key maintained by @better-auth/scim; do not write directly." + }, + user_name: { + label: "User Name", + help: "SCIM userName as sent by the IdP" + }, + user_name_key: { + label: "User Name Key", + help: "Derived case-folded userName uniqueness key maintained by @better-auth/scim; do not write directly." + }, + primary_email: { + label: "Primary Email" + }, + work_email_value_index: { + label: "Work Email Index", + help: "Derived lookup index maintained by @better-auth/scim; do not write directly." + }, + email_value_index: { + label: "Email Index", + help: "Derived lookup index maintained by @better-auth/scim; do not write directly." + }, + display_name: { + label: "Display Name" + }, + formatted_name: { + label: "Formatted Name" + }, + given_name: { + label: "Given Name" + }, + family_name: { + label: "Family Name" + }, + serialized_emails: { + label: "Emails (serialized)", + help: "Canonical serialized SCIM emails list maintained by @better-auth/scim; do not write directly." + }, + serialized_attributes: { + label: "Attributes (serialized)", + help: "Canonical serialized SCIM attributes maintained by @better-auth/scim; do not write directly." + }, + external_id: { + label: "External ID", + help: "IdP-assigned externalId, when the IdP sends one" + }, + external_id_key: { + label: "External ID Key", + help: "Derived externalId uniqueness key maintained by @better-auth/scim; do not write directly." + }, + active: { + label: "Active", + help: "SCIM active flag — false means the IdP deactivated this user" + }, + order_key: { + label: "Order Key", + help: "Derived stable-pagination key maintained by @better-auth/scim; do not write directly." + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, sys_notification: { label: "Notificación", pluralLabel: "Notificaciones", 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 8e6c78fbae..17343cb8d1 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 @@ -380,9 +380,180 @@ export const esESGeneratedSourceHashes: Readonly> = { "objects.sys_organization.fields.slug.label": "fe4132df05ec637b", "objects.sys_organization.fields.sort_order.help": "7355b98a96f85dfc", "objects.sys_organization.fields.sort_order.label": "5f6b33fdc89e9d9f", + "objects.sys_scim_connection_binding._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_connection_binding.description": "b895133352f6681c", + "objects.sys_scim_connection_binding.fields.connection_id.help": "034eb7d93a212da1", + "objects.sys_scim_connection_binding.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_connection_binding.fields.connection_key.help": "7df9975f32bdf0bc", + "objects.sys_scim_connection_binding.fields.connection_key.label": "d6f7536848750bc1", + "objects.sys_scim_connection_binding.fields.created_at.label": "1f02d416befb595b", + "objects.sys_scim_connection_binding.fields.decommission_batch_count.label": "69d0d27bdfad1158", + "objects.sys_scim_connection_binding.fields.decommission_completed_at.label": "4b4367514668f16b", + "objects.sys_scim_connection_binding.fields.decommission_cursor_user_id.help": "1d826764513040e4", + "objects.sys_scim_connection_binding.fields.decommission_cursor_user_id.label": "05a4591457a4e80f", + "objects.sys_scim_connection_binding.fields.decommission_lease_expires_at.label": "8593f0a433758002", + "objects.sys_scim_connection_binding.fields.decommission_lease_id.help": "4baba8767b4bd19d", + "objects.sys_scim_connection_binding.fields.decommission_lease_id.label": "3f5bbd3bde910338", + "objects.sys_scim_connection_binding.fields.decommission_reconciled_user_count.label": "bc1a74ac6ea6dc87", + "objects.sys_scim_connection_binding.fields.decommission_revision.help": "08fc0bb780421ed4", + "objects.sys_scim_connection_binding.fields.decommission_revision.label": "a017c828e4cc0b22", + "objects.sys_scim_connection_binding.fields.decommission_status.help": "f766f15ae7b4e26e", + "objects.sys_scim_connection_binding.fields.decommission_status.label": "7d49c647b4e1e843", + "objects.sys_scim_connection_binding.fields.decommissioned_at.label": "912fef87cf041169", + "objects.sys_scim_connection_binding.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_connection_binding.fields.provisioning_domain_id.help": "16baa93cc3b3ca9e", + "objects.sys_scim_connection_binding.fields.provisioning_domain_id.label": "d3da6908c7123b00", + "objects.sys_scim_connection_binding.label": "a86dce83ff9ae560", + "objects.sys_scim_connection_binding.pluralLabel": "074d8c7e8bd7e51b", + "objects.sys_scim_connection_credential._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_connection_credential.description": "e298fae44c1fb26b", + "objects.sys_scim_connection_credential.fields.active.help": "f1bb8af835e80434", + "objects.sys_scim_connection_credential.fields.active.label": "eee107cf4e466779", + "objects.sys_scim_connection_credential.fields.connection_id.help": "e12a9e64684333ba", + "objects.sys_scim_connection_credential.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_connection_credential.fields.created_at.label": "1f02d416befb595b", + "objects.sys_scim_connection_credential.fields.expires_at.help": "219771cb7e23e7f0", + "objects.sys_scim_connection_credential.fields.expires_at.label": "df0ef5fae02b2044", + "objects.sys_scim_connection_credential.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_connection_credential.fields.label.help": "4c9becbaf107e198", + "objects.sys_scim_connection_credential.fields.label.label": "c680364257b11569", + "objects.sys_scim_connection_credential.fields.organization_id.help": "6b3f4cd8ef844bc5", + "objects.sys_scim_connection_credential.fields.organization_id.label": "3e55836156e1c1de", + "objects.sys_scim_connection_credential.fields.provisioning_domain_id.help": "dbcc622262a32b57", + "objects.sys_scim_connection_credential.fields.provisioning_domain_id.label": "d3da6908c7123b00", + "objects.sys_scim_connection_credential.fields.token_digest.help": "657e4fa81f3972f5", + "objects.sys_scim_connection_credential.fields.token_digest.label": "4373e0d7f2686878", + "objects.sys_scim_connection_credential.fields.updated_at.label": "aba63dc2a9c79b8d", + "objects.sys_scim_connection_credential.fields.user_id.help": "d960168361e9bf6a", + "objects.sys_scim_connection_credential.fields.user_id.label": "31bf172b446396d0", + "objects.sys_scim_connection_credential.label": "115ec12ad3c5cd52", + "objects.sys_scim_connection_credential.pluralLabel": "dc5e36622fb23f62", + "objects.sys_scim_group._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_group.description": "2cefe59ab8f2d6a2", + "objects.sys_scim_group.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_group.fields.created_at.label": "1f02d416befb595b", + "objects.sys_scim_group.fields.display_name.help": "9dfb190daeafc7ca", + "objects.sys_scim_group.fields.display_name.label": "5512fceb5183fd1f", + "objects.sys_scim_group.fields.display_name_key.help": "44628861d6565776", + "objects.sys_scim_group.fields.display_name_key.label": "ce0b9207c2a0b3e2", + "objects.sys_scim_group.fields.external_id.help": "6f779f6f163b3028", + "objects.sys_scim_group.fields.external_id.label": "b81498d9908b2ed1", + "objects.sys_scim_group.fields.external_id_key.help": "d91a6810e74d9d74", + "objects.sys_scim_group.fields.external_id_key.label": "70076aa519914607", + "objects.sys_scim_group.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_group.fields.order_key.help": "d19cba034fc15454", + "objects.sys_scim_group.fields.order_key.label": "929e5f874fa283d5", + "objects.sys_scim_group.fields.provisioning_domain_id.label": "d3da6908c7123b00", + "objects.sys_scim_group.fields.revision.help": "9a4a009aaad3d9f7", + "objects.sys_scim_group.fields.revision.label": "9736a257b7ba380b", + "objects.sys_scim_group.fields.updated_at.label": "aba63dc2a9c79b8d", + "objects.sys_scim_group.label": "b1e8cffa120eaaf3", + "objects.sys_scim_group.pluralLabel": "041e7e49c26e6e23", + "objects.sys_scim_group_member._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_group_member.description": "5da1f5a9f6c74dce", + "objects.sys_scim_group_member.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_group_member.fields.created_at.label": "1f02d416befb595b", + "objects.sys_scim_group_member.fields.group_id.label": "588222bcabb4c16e", + "objects.sys_scim_group_member.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_group_member.fields.membership_key.help": "127024a3ec9d4924", + "objects.sys_scim_group_member.fields.membership_key.label": "8fcb9f17bcc8cae9", + "objects.sys_scim_group_member.fields.scim_user_id.label": "ff9e9e3696e17ae8", + "objects.sys_scim_group_member.label": "a15085ccdbba4e72", + "objects.sys_scim_group_member.pluralLabel": "3f643c5a50a46d0d", + "objects.sys_scim_identity_tombstone._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_identity_tombstone.description": "e0c9699fda63d907", + "objects.sys_scim_identity_tombstone.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_identity_tombstone.fields.deleted_at.label": "ecae63a743df29cd", + "objects.sys_scim_identity_tombstone.fields.external_id.help": "62b69d5bd951252c", + "objects.sys_scim_identity_tombstone.fields.external_id.label": "b81498d9908b2ed1", + "objects.sys_scim_identity_tombstone.fields.external_id_key.help": "d91a6810e74d9d74", + "objects.sys_scim_identity_tombstone.fields.external_id_key.label": "70076aa519914607", + "objects.sys_scim_identity_tombstone.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_identity_tombstone.fields.profile.help": "d11763773ca4c440", + "objects.sys_scim_identity_tombstone.fields.profile.label": "b174cad0a9c78e11", + "objects.sys_scim_identity_tombstone.fields.provisioning_domain_id.label": "d3da6908c7123b00", + "objects.sys_scim_identity_tombstone.fields.user_id.help": "b4c468629790014b", + "objects.sys_scim_identity_tombstone.fields.user_id.label": "6c0d22a931e235bd", + "objects.sys_scim_identity_tombstone.label": "1c70519325beeb22", + "objects.sys_scim_identity_tombstone.pluralLabel": "525ddcdf78b4ed15", + "objects.sys_scim_projection_grant._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_projection_grant.description": "8603e83095133dee", + "objects.sys_scim_projection_grant.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_projection_grant.fields.created_at.label": "1f02d416befb595b", + "objects.sys_scim_projection_grant.fields.grant_key.help": "d55c81e0237a4286", + "objects.sys_scim_projection_grant.fields.grant_key.label": "c5fbfa14170b0a49", + "objects.sys_scim_projection_grant.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_projection_grant.fields.provisioning_domain_id.label": "d3da6908c7123b00", + "objects.sys_scim_projection_grant.fields.role.help": "5c3718daca38e97e", + "objects.sys_scim_projection_grant.fields.role.label": "6e45203a573cd5ab", + "objects.sys_scim_projection_grant.fields.scim_user_id.help": "da657b19880f3536", + "objects.sys_scim_projection_grant.fields.scim_user_id.label": "ff9e9e3696e17ae8", + "objects.sys_scim_projection_grant.fields.source_id.help": "ba8fcdf2adba6295", + "objects.sys_scim_projection_grant.fields.source_id.label": "9764aa679981804c", + "objects.sys_scim_projection_grant.fields.source_kind.help": "fbf04bdf23967c33", + "objects.sys_scim_projection_grant.fields.source_kind.label": "44c76b7b2471dd7c", + "objects.sys_scim_projection_grant.fields.source_value.help": "eb7d8b2966e31fb1", + "objects.sys_scim_projection_grant.fields.source_value.label": "5699bf94c3a3d655", + "objects.sys_scim_projection_grant.fields.updated_at.label": "aba63dc2a9c79b8d", + "objects.sys_scim_projection_grant.fields.user_id.help": "e1e49423749a2d12", + "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", + "objects.sys_scim_subject.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_subject.fields.profile_source_id.help": "c109ad373576afe5", + "objects.sys_scim_subject.fields.profile_source_id.label": "6a263b010b51ecd1", + "objects.sys_scim_subject.fields.revision.help": "9a4a009aaad3d9f7", + "objects.sys_scim_subject.fields.revision.label": "9736a257b7ba380b", + "objects.sys_scim_subject.fields.updated_at.label": "aba63dc2a9c79b8d", + "objects.sys_scim_subject.fields.user_id.help": "6798a366e9db4bb8", + "objects.sys_scim_subject.fields.user_id.label": "6c0d22a931e235bd", + "objects.sys_scim_subject.label": "350900f1977ecb7f", + "objects.sys_scim_subject.pluralLabel": "6ae4d4963680cee7", + "objects.sys_scim_user._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_user.description": "efe9c1a95d6eeb8e", + "objects.sys_scim_user.fields.active.help": "d82a2ec5aebe530f", + "objects.sys_scim_user.fields.active.label": "eee107cf4e466779", + "objects.sys_scim_user.fields.connection_id.help": "e18a3d5a0d3d1ac8", + "objects.sys_scim_user.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_user.fields.connection_user_key.help": "efb51acc3fea954a", + "objects.sys_scim_user.fields.connection_user_key.label": "58622a8cc908c01e", + "objects.sys_scim_user.fields.created_at.label": "1f02d416befb595b", + "objects.sys_scim_user.fields.display_name.label": "5512fceb5183fd1f", + "objects.sys_scim_user.fields.email_value_index.help": "4f3c5b1f20c870be", + "objects.sys_scim_user.fields.email_value_index.label": "b31d7d6d561bcde3", + "objects.sys_scim_user.fields.external_id.help": "6f779f6f163b3028", + "objects.sys_scim_user.fields.external_id.label": "b81498d9908b2ed1", + "objects.sys_scim_user.fields.external_id_key.help": "d91a6810e74d9d74", + "objects.sys_scim_user.fields.external_id_key.label": "70076aa519914607", + "objects.sys_scim_user.fields.family_name.label": "9190adafe369fb71", + "objects.sys_scim_user.fields.formatted_name.label": "90c57aa3a7dfffd6", + "objects.sys_scim_user.fields.given_name.label": "055d37cebecaf97e", + "objects.sys_scim_user.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_user.fields.order_key.help": "d19cba034fc15454", + "objects.sys_scim_user.fields.order_key.label": "929e5f874fa283d5", + "objects.sys_scim_user.fields.primary_email.label": "15da02d3854d82c1", + "objects.sys_scim_user.fields.provisioning_domain_id.label": "d3da6908c7123b00", + "objects.sys_scim_user.fields.serialized_attributes.help": "e865a06565b3e303", + "objects.sys_scim_user.fields.serialized_attributes.label": "93c3785f905ff51e", + "objects.sys_scim_user.fields.serialized_emails.help": "5eea0721c9a26b9c", + "objects.sys_scim_user.fields.serialized_emails.label": "100eb0a58d7c9d5a", + "objects.sys_scim_user.fields.updated_at.label": "aba63dc2a9c79b8d", + "objects.sys_scim_user.fields.user_id.help": "8d35db5208c6bdf4", + "objects.sys_scim_user.fields.user_id.label": "6c0d22a931e235bd", + "objects.sys_scim_user.fields.user_name.help": "a72cc81d09cd66b6", + "objects.sys_scim_user.fields.user_name.label": "bf59c7fd3e98bc55", + "objects.sys_scim_user.fields.user_name_key.help": "72b94b4a5964572b", + "objects.sys_scim_user.fields.user_name_key.label": "632218ca52daf77b", + "objects.sys_scim_user.fields.work_email_value_index.help": "4f3c5b1f20c870be", + "objects.sys_scim_user.fields.work_email_value_index.label": "8d37193924db29c7", + "objects.sys_scim_user.label": "ff9e9e3696e17ae8", + "objects.sys_scim_user.pluralLabel": "b8538685c24f5084", "objects.sys_secret._views.all.label": "20d032bd60c81773", "objects.sys_secret.fields.id.label": "00b0385c9c152888", "objects.sys_session.fields.last_activity_at.help": "f7851e9373505e73", 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 2c2771ce8f..b63f354ae6 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 @@ -1935,6 +1935,421 @@ export const jaJPObjects: NonNullable = { } } }, + sys_scim_connection_binding: { + label: "SCIM Connection Binding", + pluralLabel: "SCIM Connection Bindings", + description: "Lifecycle state for a SCIM provisioning connection, including its decommission state machine", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID", + help: "Immutable SCIM connection identifier that scopes every provisioned resource" + }, + connection_key: { + label: "Connection Key", + help: "Derived connection uniqueness key maintained by @better-auth/scim; do not write directly." + }, + provisioning_domain_id: { + label: "Provisioning Domain", + help: "Application-owned boundary that receives provisioned resources (defaults to the connection id)" + }, + decommission_status: { + label: "Decommission Status", + help: "Connection lifecycle state: active | reconciling | complete" + }, + decommissioned_at: { + label: "Decommissioned At" + }, + decommission_cursor_user_id: { + label: "Decommission Cursor User", + help: "Resumable reconciliation cursor maintained by the library" + }, + decommission_reconciled_user_count: { + label: "Reconciled Users" + }, + decommission_batch_count: { + label: "Decommission Batches" + }, + decommission_revision: { + label: "Decommission Revision", + help: "Optimistic-concurrency revision for the decommission state machine" + }, + decommission_completed_at: { + label: "Decommission Completed At" + }, + decommission_lease_id: { + label: "Decommission Lease", + help: "Single-worker reconciliation lease maintained by the library" + }, + decommission_lease_expires_at: { + label: "Decommission Lease Expires At" + }, + created_at: { + label: "Created At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_connection_credential: { + label: "SCIM Connection Credential", + pluralLabel: "SCIM Connection Credentials", + description: "Bearer credentials (one-way digests) that authenticate SCIM provisioning connections", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID", + help: "The SCIM connection this credential authenticates (e.g. \"okta-prod\"); scopes every resource the IdP provisions with it" + }, + provisioning_domain_id: { + label: "Provisioning Domain", + help: "Application-owned boundary receiving provisioned resources; defaults to the connection id when absent" + }, + organization_id: { + label: "Organization", + help: "Organization scope of this connection, when provisioning is org-scoped" + }, + label: { + label: "Label", + help: "Operator-facing name for this credential (e.g. \"rotation 2026-Q3\")" + }, + token_digest: { + label: "Token Digest", + help: "HMAC-SHA-256 (base64url) of the bearer, keyed by the deployment auth secret — one-way; the plaintext is shown once at mint and never stored." + }, + active: { + label: "Active", + help: "Revocation switch — an inactive credential is refused at verification" + }, + expires_at: { + label: "Expires At", + help: "Optional hard expiry for staged credential rotation; an expired credential is refused" + }, + user_id: { + label: "Minted By", + help: "User who minted this credential" + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_group: { + label: "SCIM Group", + pluralLabel: "SCIM Groups", + description: "IdP-pushed SCIM 2.0 groups, scoped per provisioning connection", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID" + }, + provisioning_domain_id: { + label: "Provisioning Domain" + }, + revision: { + label: "Revision", + help: "Optimistic-concurrency revision maintained by @better-auth/scim" + }, + display_name: { + label: "Display Name", + help: "SCIM displayName as sent by the IdP" + }, + display_name_key: { + label: "Display Name Key", + help: "Derived case-folded displayName uniqueness key maintained by @better-auth/scim; do not write directly." + }, + external_id: { + label: "External ID", + help: "IdP-assigned externalId, when the IdP sends one" + }, + external_id_key: { + label: "External ID Key", + help: "Derived externalId uniqueness key maintained by @better-auth/scim; do not write directly." + }, + order_key: { + label: "Order Key", + help: "Derived stable-pagination key maintained by @better-auth/scim; do not write directly." + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_group_member: { + label: "SCIM Group Member", + pluralLabel: "SCIM Group Members", + description: "SCIM group membership rows pushed by the IdP (group ↔ provisioned user)", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID" + }, + group_id: { + label: "Group" + }, + scim_user_id: { + label: "SCIM User" + }, + membership_key: { + label: "Membership Key", + help: "Derived membership uniqueness key maintained by @better-auth/scim; do not write directly." + }, + created_at: { + label: "Created At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_identity_tombstone: { + label: "SCIM Identity Tombstone", + pluralLabel: "SCIM Identity Tombstones", + description: "Tombstones of IdP-deleted SCIM identities, kept so a re-provision is recognized", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID" + }, + provisioning_domain_id: { + label: "Provisioning Domain" + }, + external_id: { + label: "External ID", + help: "The IdP-assigned externalId of the deleted identity" + }, + external_id_key: { + label: "External ID Key", + help: "Derived externalId uniqueness key maintained by @better-auth/scim; do not write directly." + }, + user_id: { + label: "User", + help: "The platform user the deleted identity was linked to" + }, + profile: { + label: "Final Profile", + help: "Serialized final SCIM profile at deletion time, maintained by @better-auth/scim" + }, + deleted_at: { + label: "Deleted At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_projection_grant: { + label: "SCIM Projection Grant", + pluralLabel: "SCIM Projection Grants", + description: "Role/entitlement grants projected onto platform users by SCIM provisioning", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID" + }, + provisioning_domain_id: { + label: "Provisioning Domain" + }, + scim_user_id: { + label: "SCIM User", + help: "The per-connection user projection this grant belongs to" + }, + user_id: { + label: "User", + help: "The platform user the grant is projected onto" + }, + source_kind: { + label: "Source Kind", + help: "What projected the grant (e.g. group, entitlement, attribute)" + }, + source_id: { + label: "Source ID", + help: "Identifier of the projecting source (e.g. the SCIM group id)" + }, + source_value: { + label: "Source Value", + help: "Source attribute value, when the source kind carries one" + }, + role: { + label: "Role", + help: "The role/entitlement projected onto the user" + }, + grant_key: { + label: "Grant Key", + help: "Derived grant uniqueness key maintained by @better-auth/scim; do not write directly." + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_subject: { + label: "SCIM Subject", + pluralLabel: "SCIM Subjects", + description: "Per-user SCIM provisioning link — one row per user any SCIM connection provisions", + fields: { + id: { + label: "ID" + }, + user_id: { + label: "User", + help: "The platform user this subject row tracks (unique — one row per user)" + }, + profile_source_id: { + label: "Profile Source", + help: "The sys_scim_user projection currently sourcing this user's profile" + }, + revision: { + label: "Revision", + help: "Optimistic-concurrency revision maintained by @better-auth/scim" + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_user: { + label: "SCIM User", + pluralLabel: "SCIM Users", + description: "Per-connection projection of an IdP-provisioned user (SCIM 2.0 /Users)", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID", + help: "SCIM connection that provisioned this user" + }, + provisioning_domain_id: { + label: "Provisioning Domain" + }, + user_id: { + label: "User", + help: "The platform user this projection materialized as" + }, + connection_user_key: { + label: "Connection User Key", + help: "Derived (connection, user) uniqueness key maintained by @better-auth/scim; do not write directly." + }, + user_name: { + label: "User Name", + help: "SCIM userName as sent by the IdP" + }, + user_name_key: { + label: "User Name Key", + help: "Derived case-folded userName uniqueness key maintained by @better-auth/scim; do not write directly." + }, + primary_email: { + label: "Primary Email" + }, + work_email_value_index: { + label: "Work Email Index", + help: "Derived lookup index maintained by @better-auth/scim; do not write directly." + }, + email_value_index: { + label: "Email Index", + help: "Derived lookup index maintained by @better-auth/scim; do not write directly." + }, + display_name: { + label: "Display Name" + }, + formatted_name: { + label: "Formatted Name" + }, + given_name: { + label: "Given Name" + }, + family_name: { + label: "Family Name" + }, + serialized_emails: { + label: "Emails (serialized)", + help: "Canonical serialized SCIM emails list maintained by @better-auth/scim; do not write directly." + }, + serialized_attributes: { + label: "Attributes (serialized)", + help: "Canonical serialized SCIM attributes maintained by @better-auth/scim; do not write directly." + }, + external_id: { + label: "External ID", + help: "IdP-assigned externalId, when the IdP sends one" + }, + external_id_key: { + label: "External ID Key", + help: "Derived externalId uniqueness key maintained by @better-auth/scim; do not write directly." + }, + active: { + label: "Active", + help: "SCIM active flag — false means the IdP deactivated this user" + }, + order_key: { + label: "Order Key", + help: "Derived stable-pagination key maintained by @better-auth/scim; do not write directly." + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, sys_notification: { label: "通知", pluralLabel: "通知", 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 a23d7d08d4..bdc631344b 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 @@ -373,9 +373,180 @@ export const jaJPGeneratedSourceHashes: Readonly> = { "objects.sys_organization.fields.parent_organization_id.label": "24129608643b897f", "objects.sys_organization.fields.sort_order.help": "7355b98a96f85dfc", "objects.sys_organization.fields.sort_order.label": "5f6b33fdc89e9d9f", + "objects.sys_scim_connection_binding._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_connection_binding.description": "b895133352f6681c", + "objects.sys_scim_connection_binding.fields.connection_id.help": "034eb7d93a212da1", + "objects.sys_scim_connection_binding.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_connection_binding.fields.connection_key.help": "7df9975f32bdf0bc", + "objects.sys_scim_connection_binding.fields.connection_key.label": "d6f7536848750bc1", + "objects.sys_scim_connection_binding.fields.created_at.label": "1f02d416befb595b", + "objects.sys_scim_connection_binding.fields.decommission_batch_count.label": "69d0d27bdfad1158", + "objects.sys_scim_connection_binding.fields.decommission_completed_at.label": "4b4367514668f16b", + "objects.sys_scim_connection_binding.fields.decommission_cursor_user_id.help": "1d826764513040e4", + "objects.sys_scim_connection_binding.fields.decommission_cursor_user_id.label": "05a4591457a4e80f", + "objects.sys_scim_connection_binding.fields.decommission_lease_expires_at.label": "8593f0a433758002", + "objects.sys_scim_connection_binding.fields.decommission_lease_id.help": "4baba8767b4bd19d", + "objects.sys_scim_connection_binding.fields.decommission_lease_id.label": "3f5bbd3bde910338", + "objects.sys_scim_connection_binding.fields.decommission_reconciled_user_count.label": "bc1a74ac6ea6dc87", + "objects.sys_scim_connection_binding.fields.decommission_revision.help": "08fc0bb780421ed4", + "objects.sys_scim_connection_binding.fields.decommission_revision.label": "a017c828e4cc0b22", + "objects.sys_scim_connection_binding.fields.decommission_status.help": "f766f15ae7b4e26e", + "objects.sys_scim_connection_binding.fields.decommission_status.label": "7d49c647b4e1e843", + "objects.sys_scim_connection_binding.fields.decommissioned_at.label": "912fef87cf041169", + "objects.sys_scim_connection_binding.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_connection_binding.fields.provisioning_domain_id.help": "16baa93cc3b3ca9e", + "objects.sys_scim_connection_binding.fields.provisioning_domain_id.label": "d3da6908c7123b00", + "objects.sys_scim_connection_binding.label": "a86dce83ff9ae560", + "objects.sys_scim_connection_binding.pluralLabel": "074d8c7e8bd7e51b", + "objects.sys_scim_connection_credential._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_connection_credential.description": "e298fae44c1fb26b", + "objects.sys_scim_connection_credential.fields.active.help": "f1bb8af835e80434", + "objects.sys_scim_connection_credential.fields.active.label": "eee107cf4e466779", + "objects.sys_scim_connection_credential.fields.connection_id.help": "e12a9e64684333ba", + "objects.sys_scim_connection_credential.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_connection_credential.fields.created_at.label": "1f02d416befb595b", + "objects.sys_scim_connection_credential.fields.expires_at.help": "219771cb7e23e7f0", + "objects.sys_scim_connection_credential.fields.expires_at.label": "df0ef5fae02b2044", + "objects.sys_scim_connection_credential.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_connection_credential.fields.label.help": "4c9becbaf107e198", + "objects.sys_scim_connection_credential.fields.label.label": "c680364257b11569", + "objects.sys_scim_connection_credential.fields.organization_id.help": "6b3f4cd8ef844bc5", + "objects.sys_scim_connection_credential.fields.organization_id.label": "3e55836156e1c1de", + "objects.sys_scim_connection_credential.fields.provisioning_domain_id.help": "dbcc622262a32b57", + "objects.sys_scim_connection_credential.fields.provisioning_domain_id.label": "d3da6908c7123b00", + "objects.sys_scim_connection_credential.fields.token_digest.help": "657e4fa81f3972f5", + "objects.sys_scim_connection_credential.fields.token_digest.label": "4373e0d7f2686878", + "objects.sys_scim_connection_credential.fields.updated_at.label": "aba63dc2a9c79b8d", + "objects.sys_scim_connection_credential.fields.user_id.help": "d960168361e9bf6a", + "objects.sys_scim_connection_credential.fields.user_id.label": "31bf172b446396d0", + "objects.sys_scim_connection_credential.label": "115ec12ad3c5cd52", + "objects.sys_scim_connection_credential.pluralLabel": "dc5e36622fb23f62", + "objects.sys_scim_group._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_group.description": "2cefe59ab8f2d6a2", + "objects.sys_scim_group.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_group.fields.created_at.label": "1f02d416befb595b", + "objects.sys_scim_group.fields.display_name.help": "9dfb190daeafc7ca", + "objects.sys_scim_group.fields.display_name.label": "5512fceb5183fd1f", + "objects.sys_scim_group.fields.display_name_key.help": "44628861d6565776", + "objects.sys_scim_group.fields.display_name_key.label": "ce0b9207c2a0b3e2", + "objects.sys_scim_group.fields.external_id.help": "6f779f6f163b3028", + "objects.sys_scim_group.fields.external_id.label": "b81498d9908b2ed1", + "objects.sys_scim_group.fields.external_id_key.help": "d91a6810e74d9d74", + "objects.sys_scim_group.fields.external_id_key.label": "70076aa519914607", + "objects.sys_scim_group.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_group.fields.order_key.help": "d19cba034fc15454", + "objects.sys_scim_group.fields.order_key.label": "929e5f874fa283d5", + "objects.sys_scim_group.fields.provisioning_domain_id.label": "d3da6908c7123b00", + "objects.sys_scim_group.fields.revision.help": "9a4a009aaad3d9f7", + "objects.sys_scim_group.fields.revision.label": "9736a257b7ba380b", + "objects.sys_scim_group.fields.updated_at.label": "aba63dc2a9c79b8d", + "objects.sys_scim_group.label": "b1e8cffa120eaaf3", + "objects.sys_scim_group.pluralLabel": "041e7e49c26e6e23", + "objects.sys_scim_group_member._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_group_member.description": "5da1f5a9f6c74dce", + "objects.sys_scim_group_member.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_group_member.fields.created_at.label": "1f02d416befb595b", + "objects.sys_scim_group_member.fields.group_id.label": "588222bcabb4c16e", + "objects.sys_scim_group_member.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_group_member.fields.membership_key.help": "127024a3ec9d4924", + "objects.sys_scim_group_member.fields.membership_key.label": "8fcb9f17bcc8cae9", + "objects.sys_scim_group_member.fields.scim_user_id.label": "ff9e9e3696e17ae8", + "objects.sys_scim_group_member.label": "a15085ccdbba4e72", + "objects.sys_scim_group_member.pluralLabel": "3f643c5a50a46d0d", + "objects.sys_scim_identity_tombstone._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_identity_tombstone.description": "e0c9699fda63d907", + "objects.sys_scim_identity_tombstone.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_identity_tombstone.fields.deleted_at.label": "ecae63a743df29cd", + "objects.sys_scim_identity_tombstone.fields.external_id.help": "62b69d5bd951252c", + "objects.sys_scim_identity_tombstone.fields.external_id.label": "b81498d9908b2ed1", + "objects.sys_scim_identity_tombstone.fields.external_id_key.help": "d91a6810e74d9d74", + "objects.sys_scim_identity_tombstone.fields.external_id_key.label": "70076aa519914607", + "objects.sys_scim_identity_tombstone.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_identity_tombstone.fields.profile.help": "d11763773ca4c440", + "objects.sys_scim_identity_tombstone.fields.profile.label": "b174cad0a9c78e11", + "objects.sys_scim_identity_tombstone.fields.provisioning_domain_id.label": "d3da6908c7123b00", + "objects.sys_scim_identity_tombstone.fields.user_id.help": "b4c468629790014b", + "objects.sys_scim_identity_tombstone.fields.user_id.label": "6c0d22a931e235bd", + "objects.sys_scim_identity_tombstone.label": "1c70519325beeb22", + "objects.sys_scim_identity_tombstone.pluralLabel": "525ddcdf78b4ed15", + "objects.sys_scim_projection_grant._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_projection_grant.description": "8603e83095133dee", + "objects.sys_scim_projection_grant.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_projection_grant.fields.created_at.label": "1f02d416befb595b", + "objects.sys_scim_projection_grant.fields.grant_key.help": "d55c81e0237a4286", + "objects.sys_scim_projection_grant.fields.grant_key.label": "c5fbfa14170b0a49", + "objects.sys_scim_projection_grant.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_projection_grant.fields.provisioning_domain_id.label": "d3da6908c7123b00", + "objects.sys_scim_projection_grant.fields.role.help": "5c3718daca38e97e", + "objects.sys_scim_projection_grant.fields.role.label": "6e45203a573cd5ab", + "objects.sys_scim_projection_grant.fields.scim_user_id.help": "da657b19880f3536", + "objects.sys_scim_projection_grant.fields.scim_user_id.label": "ff9e9e3696e17ae8", + "objects.sys_scim_projection_grant.fields.source_id.help": "ba8fcdf2adba6295", + "objects.sys_scim_projection_grant.fields.source_id.label": "9764aa679981804c", + "objects.sys_scim_projection_grant.fields.source_kind.help": "fbf04bdf23967c33", + "objects.sys_scim_projection_grant.fields.source_kind.label": "44c76b7b2471dd7c", + "objects.sys_scim_projection_grant.fields.source_value.help": "eb7d8b2966e31fb1", + "objects.sys_scim_projection_grant.fields.source_value.label": "5699bf94c3a3d655", + "objects.sys_scim_projection_grant.fields.updated_at.label": "aba63dc2a9c79b8d", + "objects.sys_scim_projection_grant.fields.user_id.help": "e1e49423749a2d12", + "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", + "objects.sys_scim_subject.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_subject.fields.profile_source_id.help": "c109ad373576afe5", + "objects.sys_scim_subject.fields.profile_source_id.label": "6a263b010b51ecd1", + "objects.sys_scim_subject.fields.revision.help": "9a4a009aaad3d9f7", + "objects.sys_scim_subject.fields.revision.label": "9736a257b7ba380b", + "objects.sys_scim_subject.fields.updated_at.label": "aba63dc2a9c79b8d", + "objects.sys_scim_subject.fields.user_id.help": "6798a366e9db4bb8", + "objects.sys_scim_subject.fields.user_id.label": "6c0d22a931e235bd", + "objects.sys_scim_subject.label": "350900f1977ecb7f", + "objects.sys_scim_subject.pluralLabel": "6ae4d4963680cee7", + "objects.sys_scim_user._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_user.description": "efe9c1a95d6eeb8e", + "objects.sys_scim_user.fields.active.help": "d82a2ec5aebe530f", + "objects.sys_scim_user.fields.active.label": "eee107cf4e466779", + "objects.sys_scim_user.fields.connection_id.help": "e18a3d5a0d3d1ac8", + "objects.sys_scim_user.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_user.fields.connection_user_key.help": "efb51acc3fea954a", + "objects.sys_scim_user.fields.connection_user_key.label": "58622a8cc908c01e", + "objects.sys_scim_user.fields.created_at.label": "1f02d416befb595b", + "objects.sys_scim_user.fields.display_name.label": "5512fceb5183fd1f", + "objects.sys_scim_user.fields.email_value_index.help": "4f3c5b1f20c870be", + "objects.sys_scim_user.fields.email_value_index.label": "b31d7d6d561bcde3", + "objects.sys_scim_user.fields.external_id.help": "6f779f6f163b3028", + "objects.sys_scim_user.fields.external_id.label": "b81498d9908b2ed1", + "objects.sys_scim_user.fields.external_id_key.help": "d91a6810e74d9d74", + "objects.sys_scim_user.fields.external_id_key.label": "70076aa519914607", + "objects.sys_scim_user.fields.family_name.label": "9190adafe369fb71", + "objects.sys_scim_user.fields.formatted_name.label": "90c57aa3a7dfffd6", + "objects.sys_scim_user.fields.given_name.label": "055d37cebecaf97e", + "objects.sys_scim_user.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_user.fields.order_key.help": "d19cba034fc15454", + "objects.sys_scim_user.fields.order_key.label": "929e5f874fa283d5", + "objects.sys_scim_user.fields.primary_email.label": "15da02d3854d82c1", + "objects.sys_scim_user.fields.provisioning_domain_id.label": "d3da6908c7123b00", + "objects.sys_scim_user.fields.serialized_attributes.help": "e865a06565b3e303", + "objects.sys_scim_user.fields.serialized_attributes.label": "93c3785f905ff51e", + "objects.sys_scim_user.fields.serialized_emails.help": "5eea0721c9a26b9c", + "objects.sys_scim_user.fields.serialized_emails.label": "100eb0a58d7c9d5a", + "objects.sys_scim_user.fields.updated_at.label": "aba63dc2a9c79b8d", + "objects.sys_scim_user.fields.user_id.help": "8d35db5208c6bdf4", + "objects.sys_scim_user.fields.user_id.label": "6c0d22a931e235bd", + "objects.sys_scim_user.fields.user_name.help": "a72cc81d09cd66b6", + "objects.sys_scim_user.fields.user_name.label": "bf59c7fd3e98bc55", + "objects.sys_scim_user.fields.user_name_key.help": "72b94b4a5964572b", + "objects.sys_scim_user.fields.user_name_key.label": "632218ca52daf77b", + "objects.sys_scim_user.fields.work_email_value_index.help": "4f3c5b1f20c870be", + "objects.sys_scim_user.fields.work_email_value_index.label": "8d37193924db29c7", + "objects.sys_scim_user.label": "ff9e9e3696e17ae8", + "objects.sys_scim_user.pluralLabel": "b8538685c24f5084", "objects.sys_secret._views.all.label": "20d032bd60c81773", "objects.sys_secret.fields.id.label": "00b0385c9c152888", "objects.sys_session.fields.last_activity_at.help": "f7851e9373505e73", 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 4d8893d96d..507f2cf523 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 @@ -1935,6 +1935,421 @@ export const zhCNObjects: NonNullable = { } } }, + sys_scim_connection_binding: { + label: "SCIM Connection Binding", + pluralLabel: "SCIM Connection Bindings", + description: "Lifecycle state for a SCIM provisioning connection, including its decommission state machine", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID", + help: "Immutable SCIM connection identifier that scopes every provisioned resource" + }, + connection_key: { + label: "Connection Key", + help: "Derived connection uniqueness key maintained by @better-auth/scim; do not write directly." + }, + provisioning_domain_id: { + label: "Provisioning Domain", + help: "Application-owned boundary that receives provisioned resources (defaults to the connection id)" + }, + decommission_status: { + label: "Decommission Status", + help: "Connection lifecycle state: active | reconciling | complete" + }, + decommissioned_at: { + label: "Decommissioned At" + }, + decommission_cursor_user_id: { + label: "Decommission Cursor User", + help: "Resumable reconciliation cursor maintained by the library" + }, + decommission_reconciled_user_count: { + label: "Reconciled Users" + }, + decommission_batch_count: { + label: "Decommission Batches" + }, + decommission_revision: { + label: "Decommission Revision", + help: "Optimistic-concurrency revision for the decommission state machine" + }, + decommission_completed_at: { + label: "Decommission Completed At" + }, + decommission_lease_id: { + label: "Decommission Lease", + help: "Single-worker reconciliation lease maintained by the library" + }, + decommission_lease_expires_at: { + label: "Decommission Lease Expires At" + }, + created_at: { + label: "Created At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_connection_credential: { + label: "SCIM Connection Credential", + pluralLabel: "SCIM Connection Credentials", + description: "Bearer credentials (one-way digests) that authenticate SCIM provisioning connections", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID", + help: "The SCIM connection this credential authenticates (e.g. \"okta-prod\"); scopes every resource the IdP provisions with it" + }, + provisioning_domain_id: { + label: "Provisioning Domain", + help: "Application-owned boundary receiving provisioned resources; defaults to the connection id when absent" + }, + organization_id: { + label: "Organization", + help: "Organization scope of this connection, when provisioning is org-scoped" + }, + label: { + label: "Label", + help: "Operator-facing name for this credential (e.g. \"rotation 2026-Q3\")" + }, + token_digest: { + label: "Token Digest", + help: "HMAC-SHA-256 (base64url) of the bearer, keyed by the deployment auth secret — one-way; the plaintext is shown once at mint and never stored." + }, + active: { + label: "Active", + help: "Revocation switch — an inactive credential is refused at verification" + }, + expires_at: { + label: "Expires At", + help: "Optional hard expiry for staged credential rotation; an expired credential is refused" + }, + user_id: { + label: "Minted By", + help: "User who minted this credential" + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_group: { + label: "SCIM Group", + pluralLabel: "SCIM Groups", + description: "IdP-pushed SCIM 2.0 groups, scoped per provisioning connection", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID" + }, + provisioning_domain_id: { + label: "Provisioning Domain" + }, + revision: { + label: "Revision", + help: "Optimistic-concurrency revision maintained by @better-auth/scim" + }, + display_name: { + label: "Display Name", + help: "SCIM displayName as sent by the IdP" + }, + display_name_key: { + label: "Display Name Key", + help: "Derived case-folded displayName uniqueness key maintained by @better-auth/scim; do not write directly." + }, + external_id: { + label: "External ID", + help: "IdP-assigned externalId, when the IdP sends one" + }, + external_id_key: { + label: "External ID Key", + help: "Derived externalId uniqueness key maintained by @better-auth/scim; do not write directly." + }, + order_key: { + label: "Order Key", + help: "Derived stable-pagination key maintained by @better-auth/scim; do not write directly." + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_group_member: { + label: "SCIM Group Member", + pluralLabel: "SCIM Group Members", + description: "SCIM group membership rows pushed by the IdP (group ↔ provisioned user)", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID" + }, + group_id: { + label: "Group" + }, + scim_user_id: { + label: "SCIM User" + }, + membership_key: { + label: "Membership Key", + help: "Derived membership uniqueness key maintained by @better-auth/scim; do not write directly." + }, + created_at: { + label: "Created At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_identity_tombstone: { + label: "SCIM Identity Tombstone", + pluralLabel: "SCIM Identity Tombstones", + description: "Tombstones of IdP-deleted SCIM identities, kept so a re-provision is recognized", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID" + }, + provisioning_domain_id: { + label: "Provisioning Domain" + }, + external_id: { + label: "External ID", + help: "The IdP-assigned externalId of the deleted identity" + }, + external_id_key: { + label: "External ID Key", + help: "Derived externalId uniqueness key maintained by @better-auth/scim; do not write directly." + }, + user_id: { + label: "User", + help: "The platform user the deleted identity was linked to" + }, + profile: { + label: "Final Profile", + help: "Serialized final SCIM profile at deletion time, maintained by @better-auth/scim" + }, + deleted_at: { + label: "Deleted At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_projection_grant: { + label: "SCIM Projection Grant", + pluralLabel: "SCIM Projection Grants", + description: "Role/entitlement grants projected onto platform users by SCIM provisioning", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID" + }, + provisioning_domain_id: { + label: "Provisioning Domain" + }, + scim_user_id: { + label: "SCIM User", + help: "The per-connection user projection this grant belongs to" + }, + user_id: { + label: "User", + help: "The platform user the grant is projected onto" + }, + source_kind: { + label: "Source Kind", + help: "What projected the grant (e.g. group, entitlement, attribute)" + }, + source_id: { + label: "Source ID", + help: "Identifier of the projecting source (e.g. the SCIM group id)" + }, + source_value: { + label: "Source Value", + help: "Source attribute value, when the source kind carries one" + }, + role: { + label: "Role", + help: "The role/entitlement projected onto the user" + }, + grant_key: { + label: "Grant Key", + help: "Derived grant uniqueness key maintained by @better-auth/scim; do not write directly." + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_subject: { + label: "SCIM Subject", + pluralLabel: "SCIM Subjects", + description: "Per-user SCIM provisioning link — one row per user any SCIM connection provisions", + fields: { + id: { + label: "ID" + }, + user_id: { + label: "User", + help: "The platform user this subject row tracks (unique — one row per user)" + }, + profile_source_id: { + label: "Profile Source", + help: "The sys_scim_user projection currently sourcing this user's profile" + }, + revision: { + label: "Revision", + help: "Optimistic-concurrency revision maintained by @better-auth/scim" + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, + sys_scim_user: { + label: "SCIM User", + pluralLabel: "SCIM Users", + description: "Per-connection projection of an IdP-provisioned user (SCIM 2.0 /Users)", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "Connection ID", + help: "SCIM connection that provisioned this user" + }, + provisioning_domain_id: { + label: "Provisioning Domain" + }, + user_id: { + label: "User", + help: "The platform user this projection materialized as" + }, + connection_user_key: { + label: "Connection User Key", + help: "Derived (connection, user) uniqueness key maintained by @better-auth/scim; do not write directly." + }, + user_name: { + label: "User Name", + help: "SCIM userName as sent by the IdP" + }, + user_name_key: { + label: "User Name Key", + help: "Derived case-folded userName uniqueness key maintained by @better-auth/scim; do not write directly." + }, + primary_email: { + label: "Primary Email" + }, + work_email_value_index: { + label: "Work Email Index", + help: "Derived lookup index maintained by @better-auth/scim; do not write directly." + }, + email_value_index: { + label: "Email Index", + help: "Derived lookup index maintained by @better-auth/scim; do not write directly." + }, + display_name: { + label: "Display Name" + }, + formatted_name: { + label: "Formatted Name" + }, + given_name: { + label: "Given Name" + }, + family_name: { + label: "Family Name" + }, + serialized_emails: { + label: "Emails (serialized)", + help: "Canonical serialized SCIM emails list maintained by @better-auth/scim; do not write directly." + }, + serialized_attributes: { + label: "Attributes (serialized)", + help: "Canonical serialized SCIM attributes maintained by @better-auth/scim; do not write directly." + }, + external_id: { + label: "External ID", + help: "IdP-assigned externalId, when the IdP sends one" + }, + external_id_key: { + label: "External ID Key", + help: "Derived externalId uniqueness key maintained by @better-auth/scim; do not write directly." + }, + active: { + label: "Active", + help: "SCIM active flag — false means the IdP deactivated this user" + }, + order_key: { + label: "Order Key", + help: "Derived stable-pagination key maintained by @better-auth/scim; do not write directly." + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, sys_notification: { label: "通知", pluralLabel: "通知", 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 750fb397ba..d13d2cad75 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 @@ -320,9 +320,180 @@ export const zhCNGeneratedSourceHashes: Readonly> = { "objects.sys_organization.fields.parent_organization_id.label": "24129608643b897f", "objects.sys_organization.fields.sort_order.help": "7355b98a96f85dfc", "objects.sys_organization.fields.sort_order.label": "5f6b33fdc89e9d9f", + "objects.sys_scim_connection_binding._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_connection_binding.description": "b895133352f6681c", + "objects.sys_scim_connection_binding.fields.connection_id.help": "034eb7d93a212da1", + "objects.sys_scim_connection_binding.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_connection_binding.fields.connection_key.help": "7df9975f32bdf0bc", + "objects.sys_scim_connection_binding.fields.connection_key.label": "d6f7536848750bc1", + "objects.sys_scim_connection_binding.fields.created_at.label": "1f02d416befb595b", + "objects.sys_scim_connection_binding.fields.decommission_batch_count.label": "69d0d27bdfad1158", + "objects.sys_scim_connection_binding.fields.decommission_completed_at.label": "4b4367514668f16b", + "objects.sys_scim_connection_binding.fields.decommission_cursor_user_id.help": "1d826764513040e4", + "objects.sys_scim_connection_binding.fields.decommission_cursor_user_id.label": "05a4591457a4e80f", + "objects.sys_scim_connection_binding.fields.decommission_lease_expires_at.label": "8593f0a433758002", + "objects.sys_scim_connection_binding.fields.decommission_lease_id.help": "4baba8767b4bd19d", + "objects.sys_scim_connection_binding.fields.decommission_lease_id.label": "3f5bbd3bde910338", + "objects.sys_scim_connection_binding.fields.decommission_reconciled_user_count.label": "bc1a74ac6ea6dc87", + "objects.sys_scim_connection_binding.fields.decommission_revision.help": "08fc0bb780421ed4", + "objects.sys_scim_connection_binding.fields.decommission_revision.label": "a017c828e4cc0b22", + "objects.sys_scim_connection_binding.fields.decommission_status.help": "f766f15ae7b4e26e", + "objects.sys_scim_connection_binding.fields.decommission_status.label": "7d49c647b4e1e843", + "objects.sys_scim_connection_binding.fields.decommissioned_at.label": "912fef87cf041169", + "objects.sys_scim_connection_binding.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_connection_binding.fields.provisioning_domain_id.help": "16baa93cc3b3ca9e", + "objects.sys_scim_connection_binding.fields.provisioning_domain_id.label": "d3da6908c7123b00", + "objects.sys_scim_connection_binding.label": "a86dce83ff9ae560", + "objects.sys_scim_connection_binding.pluralLabel": "074d8c7e8bd7e51b", + "objects.sys_scim_connection_credential._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_connection_credential.description": "e298fae44c1fb26b", + "objects.sys_scim_connection_credential.fields.active.help": "f1bb8af835e80434", + "objects.sys_scim_connection_credential.fields.active.label": "eee107cf4e466779", + "objects.sys_scim_connection_credential.fields.connection_id.help": "e12a9e64684333ba", + "objects.sys_scim_connection_credential.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_connection_credential.fields.created_at.label": "1f02d416befb595b", + "objects.sys_scim_connection_credential.fields.expires_at.help": "219771cb7e23e7f0", + "objects.sys_scim_connection_credential.fields.expires_at.label": "df0ef5fae02b2044", + "objects.sys_scim_connection_credential.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_connection_credential.fields.label.help": "4c9becbaf107e198", + "objects.sys_scim_connection_credential.fields.label.label": "c680364257b11569", + "objects.sys_scim_connection_credential.fields.organization_id.help": "6b3f4cd8ef844bc5", + "objects.sys_scim_connection_credential.fields.organization_id.label": "3e55836156e1c1de", + "objects.sys_scim_connection_credential.fields.provisioning_domain_id.help": "dbcc622262a32b57", + "objects.sys_scim_connection_credential.fields.provisioning_domain_id.label": "d3da6908c7123b00", + "objects.sys_scim_connection_credential.fields.token_digest.help": "657e4fa81f3972f5", + "objects.sys_scim_connection_credential.fields.token_digest.label": "4373e0d7f2686878", + "objects.sys_scim_connection_credential.fields.updated_at.label": "aba63dc2a9c79b8d", + "objects.sys_scim_connection_credential.fields.user_id.help": "d960168361e9bf6a", + "objects.sys_scim_connection_credential.fields.user_id.label": "31bf172b446396d0", + "objects.sys_scim_connection_credential.label": "115ec12ad3c5cd52", + "objects.sys_scim_connection_credential.pluralLabel": "dc5e36622fb23f62", + "objects.sys_scim_group._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_group.description": "2cefe59ab8f2d6a2", + "objects.sys_scim_group.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_group.fields.created_at.label": "1f02d416befb595b", + "objects.sys_scim_group.fields.display_name.help": "9dfb190daeafc7ca", + "objects.sys_scim_group.fields.display_name.label": "5512fceb5183fd1f", + "objects.sys_scim_group.fields.display_name_key.help": "44628861d6565776", + "objects.sys_scim_group.fields.display_name_key.label": "ce0b9207c2a0b3e2", + "objects.sys_scim_group.fields.external_id.help": "6f779f6f163b3028", + "objects.sys_scim_group.fields.external_id.label": "b81498d9908b2ed1", + "objects.sys_scim_group.fields.external_id_key.help": "d91a6810e74d9d74", + "objects.sys_scim_group.fields.external_id_key.label": "70076aa519914607", + "objects.sys_scim_group.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_group.fields.order_key.help": "d19cba034fc15454", + "objects.sys_scim_group.fields.order_key.label": "929e5f874fa283d5", + "objects.sys_scim_group.fields.provisioning_domain_id.label": "d3da6908c7123b00", + "objects.sys_scim_group.fields.revision.help": "9a4a009aaad3d9f7", + "objects.sys_scim_group.fields.revision.label": "9736a257b7ba380b", + "objects.sys_scim_group.fields.updated_at.label": "aba63dc2a9c79b8d", + "objects.sys_scim_group.label": "b1e8cffa120eaaf3", + "objects.sys_scim_group.pluralLabel": "041e7e49c26e6e23", + "objects.sys_scim_group_member._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_group_member.description": "5da1f5a9f6c74dce", + "objects.sys_scim_group_member.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_group_member.fields.created_at.label": "1f02d416befb595b", + "objects.sys_scim_group_member.fields.group_id.label": "588222bcabb4c16e", + "objects.sys_scim_group_member.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_group_member.fields.membership_key.help": "127024a3ec9d4924", + "objects.sys_scim_group_member.fields.membership_key.label": "8fcb9f17bcc8cae9", + "objects.sys_scim_group_member.fields.scim_user_id.label": "ff9e9e3696e17ae8", + "objects.sys_scim_group_member.label": "a15085ccdbba4e72", + "objects.sys_scim_group_member.pluralLabel": "3f643c5a50a46d0d", + "objects.sys_scim_identity_tombstone._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_identity_tombstone.description": "e0c9699fda63d907", + "objects.sys_scim_identity_tombstone.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_identity_tombstone.fields.deleted_at.label": "ecae63a743df29cd", + "objects.sys_scim_identity_tombstone.fields.external_id.help": "62b69d5bd951252c", + "objects.sys_scim_identity_tombstone.fields.external_id.label": "b81498d9908b2ed1", + "objects.sys_scim_identity_tombstone.fields.external_id_key.help": "d91a6810e74d9d74", + "objects.sys_scim_identity_tombstone.fields.external_id_key.label": "70076aa519914607", + "objects.sys_scim_identity_tombstone.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_identity_tombstone.fields.profile.help": "d11763773ca4c440", + "objects.sys_scim_identity_tombstone.fields.profile.label": "b174cad0a9c78e11", + "objects.sys_scim_identity_tombstone.fields.provisioning_domain_id.label": "d3da6908c7123b00", + "objects.sys_scim_identity_tombstone.fields.user_id.help": "b4c468629790014b", + "objects.sys_scim_identity_tombstone.fields.user_id.label": "6c0d22a931e235bd", + "objects.sys_scim_identity_tombstone.label": "1c70519325beeb22", + "objects.sys_scim_identity_tombstone.pluralLabel": "525ddcdf78b4ed15", + "objects.sys_scim_projection_grant._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_projection_grant.description": "8603e83095133dee", + "objects.sys_scim_projection_grant.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_projection_grant.fields.created_at.label": "1f02d416befb595b", + "objects.sys_scim_projection_grant.fields.grant_key.help": "d55c81e0237a4286", + "objects.sys_scim_projection_grant.fields.grant_key.label": "c5fbfa14170b0a49", + "objects.sys_scim_projection_grant.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_projection_grant.fields.provisioning_domain_id.label": "d3da6908c7123b00", + "objects.sys_scim_projection_grant.fields.role.help": "5c3718daca38e97e", + "objects.sys_scim_projection_grant.fields.role.label": "6e45203a573cd5ab", + "objects.sys_scim_projection_grant.fields.scim_user_id.help": "da657b19880f3536", + "objects.sys_scim_projection_grant.fields.scim_user_id.label": "ff9e9e3696e17ae8", + "objects.sys_scim_projection_grant.fields.source_id.help": "ba8fcdf2adba6295", + "objects.sys_scim_projection_grant.fields.source_id.label": "9764aa679981804c", + "objects.sys_scim_projection_grant.fields.source_kind.help": "fbf04bdf23967c33", + "objects.sys_scim_projection_grant.fields.source_kind.label": "44c76b7b2471dd7c", + "objects.sys_scim_projection_grant.fields.source_value.help": "eb7d8b2966e31fb1", + "objects.sys_scim_projection_grant.fields.source_value.label": "5699bf94c3a3d655", + "objects.sys_scim_projection_grant.fields.updated_at.label": "aba63dc2a9c79b8d", + "objects.sys_scim_projection_grant.fields.user_id.help": "e1e49423749a2d12", + "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", + "objects.sys_scim_subject.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_subject.fields.profile_source_id.help": "c109ad373576afe5", + "objects.sys_scim_subject.fields.profile_source_id.label": "6a263b010b51ecd1", + "objects.sys_scim_subject.fields.revision.help": "9a4a009aaad3d9f7", + "objects.sys_scim_subject.fields.revision.label": "9736a257b7ba380b", + "objects.sys_scim_subject.fields.updated_at.label": "aba63dc2a9c79b8d", + "objects.sys_scim_subject.fields.user_id.help": "6798a366e9db4bb8", + "objects.sys_scim_subject.fields.user_id.label": "6c0d22a931e235bd", + "objects.sys_scim_subject.label": "350900f1977ecb7f", + "objects.sys_scim_subject.pluralLabel": "6ae4d4963680cee7", + "objects.sys_scim_user._views.all.label": "6d6f653f8264ab17", + "objects.sys_scim_user.description": "efe9c1a95d6eeb8e", + "objects.sys_scim_user.fields.active.help": "d82a2ec5aebe530f", + "objects.sys_scim_user.fields.active.label": "eee107cf4e466779", + "objects.sys_scim_user.fields.connection_id.help": "e18a3d5a0d3d1ac8", + "objects.sys_scim_user.fields.connection_id.label": "0208b246b41cd035", + "objects.sys_scim_user.fields.connection_user_key.help": "efb51acc3fea954a", + "objects.sys_scim_user.fields.connection_user_key.label": "58622a8cc908c01e", + "objects.sys_scim_user.fields.created_at.label": "1f02d416befb595b", + "objects.sys_scim_user.fields.display_name.label": "5512fceb5183fd1f", + "objects.sys_scim_user.fields.email_value_index.help": "4f3c5b1f20c870be", + "objects.sys_scim_user.fields.email_value_index.label": "b31d7d6d561bcde3", + "objects.sys_scim_user.fields.external_id.help": "6f779f6f163b3028", + "objects.sys_scim_user.fields.external_id.label": "b81498d9908b2ed1", + "objects.sys_scim_user.fields.external_id_key.help": "d91a6810e74d9d74", + "objects.sys_scim_user.fields.external_id_key.label": "70076aa519914607", + "objects.sys_scim_user.fields.family_name.label": "9190adafe369fb71", + "objects.sys_scim_user.fields.formatted_name.label": "90c57aa3a7dfffd6", + "objects.sys_scim_user.fields.given_name.label": "055d37cebecaf97e", + "objects.sys_scim_user.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_user.fields.order_key.help": "d19cba034fc15454", + "objects.sys_scim_user.fields.order_key.label": "929e5f874fa283d5", + "objects.sys_scim_user.fields.primary_email.label": "15da02d3854d82c1", + "objects.sys_scim_user.fields.provisioning_domain_id.label": "d3da6908c7123b00", + "objects.sys_scim_user.fields.serialized_attributes.help": "e865a06565b3e303", + "objects.sys_scim_user.fields.serialized_attributes.label": "93c3785f905ff51e", + "objects.sys_scim_user.fields.serialized_emails.help": "5eea0721c9a26b9c", + "objects.sys_scim_user.fields.serialized_emails.label": "100eb0a58d7c9d5a", + "objects.sys_scim_user.fields.updated_at.label": "aba63dc2a9c79b8d", + "objects.sys_scim_user.fields.user_id.help": "8d35db5208c6bdf4", + "objects.sys_scim_user.fields.user_id.label": "6c0d22a931e235bd", + "objects.sys_scim_user.fields.user_name.help": "a72cc81d09cd66b6", + "objects.sys_scim_user.fields.user_name.label": "bf59c7fd3e98bc55", + "objects.sys_scim_user.fields.user_name_key.help": "72b94b4a5964572b", + "objects.sys_scim_user.fields.user_name_key.label": "632218ca52daf77b", + "objects.sys_scim_user.fields.work_email_value_index.help": "4f3c5b1f20c870be", + "objects.sys_scim_user.fields.work_email_value_index.label": "8d37193924db29c7", + "objects.sys_scim_user.label": "ff9e9e3696e17ae8", + "objects.sys_scim_user.pluralLabel": "b8538685c24f5084", "objects.sys_secret._views.all.label": "20d032bd60c81773", "objects.sys_secret.fields.id.label": "00b0385c9c152888", "objects.sys_session.fields.last_activity_at.help": "f7851e9373505e73", diff --git a/packages/platform-objects/src/identity/index.ts b/packages/platform-objects/src/identity/index.ts index 7738089bbb..3a6cd2f81c 100644 --- a/packages/platform-objects/src/identity/index.ts +++ b/packages/platform-objects/src/identity/index.ts @@ -43,4 +43,15 @@ 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. +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'; +export { SysScimGroupMember } from './sys-scim-group-member.object.js'; +export { SysScimIdentityTombstone } from './sys-scim-identity-tombstone.object.js'; +export { SysScimProjectionGrant } from './sys-scim-projection-grant.object.js'; +export { SysScimSubject } from './sys-scim-subject.object.js'; +export { SysScimUser } from './sys-scim-user.object.js'; diff --git a/packages/platform-objects/src/identity/sys-scim-connection-binding.object.ts b/packages/platform-objects/src/identity/sys-scim-connection-binding.object.ts new file mode 100644 index 0000000000..e03924a276 --- /dev/null +++ b/packages/platform-objects/src/identity/sys-scim-connection-binding.object.ts @@ -0,0 +1,177 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_scim_connection_binding — Durable lifecycle state for one SCIM connection + * (`@better-auth/scim` stable `scimConnectionBinding`). + * + * One row per provisioning connection the plugin has seen. The library creates + * the row the first time a connection provisions and drives the decommission + * state machine (`active` → `reconciling` → `complete`) through it when a + * connection is retired. Columns mirror the installed stable schema, bridged + * mechanically camelCase → snake_case by `objectql-adapter.ts` (see + * `better-auth-schema-parity.test.ts`, #3653). + * + * This is NOT a credential store — `connection_key` is a uniqueness key the + * library derives, not a secret. The bearer credentials that authenticate a + * connection live in `sys_scim_connection_credential`, which ObjectStack owns + * outright (stable `@better-auth/scim` stores no credential at all). + * + * @namespace sys + */ +export const SysScimConnectionBinding = ObjectSchema.create({ + name: 'sys_scim_connection_binding', + label: 'SCIM Connection Binding', + pluralLabel: 'SCIM Connection Bindings', + icon: 'users', + isSystem: true, + managedBy: 'better-auth', + // 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: 'Lifecycle state for a SCIM provisioning connection, including its decommission state machine', + displayNameField: 'connection_id', + nameField: 'connection_id', // [ADR-0079] canonical primary-title pointer + titleFormat: '{connection_id}', + highlightFields: ['connection_id', 'provisioning_domain_id', 'decommission_status'], + + listViews: { + all: { + type: 'grid', + name: 'all', + label: 'All', + data: { provider: 'object', object: 'sys_scim_connection_binding' }, + columns: ['connection_id', 'provisioning_domain_id', 'decommission_status', 'created_at'], + sort: [{ field: 'connection_id', order: 'asc' }], + pagination: { pageSize: 50 }, + }, + }, + + fields: { + id: Field.text({ label: 'ID', required: true, readonly: true, group: 'System' }), + + connection_id: Field.text({ + label: 'Connection ID', + required: true, + searchable: true, + maxLength: 255, + description: 'Immutable SCIM connection identifier that scopes every provisioned resource', + group: 'Connection', + }), + + // Derived uniqueness key, declared `required: true, unique: true, + // returned: false` upstream — same class as `sys_team_member.membership_key`: + // owned end to end by the library, never authored from the ObjectStack side. + connection_key: Field.text({ + label: 'Connection Key', + required: true, + readonly: true, + maxLength: 512, + description: 'Derived connection uniqueness key maintained by @better-auth/scim; do not write directly.', + group: 'System', + }), + + provisioning_domain_id: Field.text({ + label: 'Provisioning Domain', + required: true, + maxLength: 255, + description: 'Application-owned boundary that receives provisioned resources (defaults to the connection id)', + group: 'Connection', + }), + + decommission_status: Field.text({ + label: 'Decommission Status', + required: true, + defaultValue: 'active', + maxLength: 32, + description: 'Connection lifecycle state: active | reconciling | complete', + group: 'Decommission', + }), + + decommissioned_at: Field.datetime({ + label: 'Decommissioned At', + required: false, + readonly: true, + group: 'Decommission', + }), + + decommission_cursor_user_id: Field.text({ + label: 'Decommission Cursor User', + required: false, + readonly: true, + maxLength: 255, + description: 'Resumable reconciliation cursor maintained by the library', + group: 'Decommission', + }), + + decommission_reconciled_user_count: Field.number({ + label: 'Reconciled Users', + required: false, + defaultValue: 0, + readonly: true, + group: 'Decommission', + }), + + decommission_batch_count: Field.number({ + label: 'Decommission Batches', + required: false, + defaultValue: 0, + readonly: true, + group: 'Decommission', + }), + + decommission_revision: Field.number({ + label: 'Decommission Revision', + required: false, + defaultValue: 0, + readonly: true, + description: 'Optimistic-concurrency revision for the decommission state machine', + group: 'Decommission', + }), + + decommission_completed_at: Field.datetime({ + label: 'Decommission Completed At', + required: false, + readonly: true, + group: 'Decommission', + }), + + decommission_lease_id: Field.text({ + label: 'Decommission Lease', + required: false, + readonly: true, + maxLength: 255, + description: 'Single-worker reconciliation lease maintained by the library', + group: 'Decommission', + }), + + decommission_lease_expires_at: Field.datetime({ + label: 'Decommission Lease Expires At', + required: false, + readonly: true, + group: 'Decommission', + }), + + created_at: Field.datetime({ label: 'Created At', readonly: true, group: 'System' }), + }, + + indexes: [ + // UNIQUE mirrors @better-auth/scim's own declaration on connectionKey. + { fields: ['connection_key'], unique: true }, + { fields: ['connection_id'] }, + { fields: ['provisioning_domain_id'] }, + ], + + enable: { + trackHistory: true, + searchable: false, + apiEnabled: true, + // Reads only: all mutations flow through @better-auth/scim's endpoints + // under /api/v1/auth/scim/v2/*; the generic data layer never writes. + apiMethods: ['get', 'list'], + }, +}); diff --git a/packages/platform-objects/src/identity/sys-scim-connection-credential.object.ts b/packages/platform-objects/src/identity/sys-scim-connection-credential.object.ts new file mode 100644 index 0000000000..8ad441a7b8 --- /dev/null +++ b/packages/platform-objects/src/identity/sys-scim-connection-credential.object.ts @@ -0,0 +1,162 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_scim_connection_credential — ObjectStack-owned bearer credentials for + * SCIM provisioning connections (#3653, ADR-0071). + * + * Stable `@better-auth/scim` stores NO credential of its own: the rc.1 + * `/scim/generate-token` endpoint and `scimProvider.scim_token` column are + * gone, and none of the stable models declares a token/secret column + * (measured — see `credential-at-rest-posture.test.ts`). Instead the plugin is + * mounted with an application-owned `authentication.verifyBearerToken`, so + * ObjectStack owns the whole credential lifecycle — mint, store, verify — + * outright. This table is that store; the service half lives in + * `plugin-auth/src/scim-connection-service.ts`. + * + * Deliberately NOT a resurrection of `sys_scim_provider` (which retires under + * #11757): a row here is one bearer credential FOR a connection, not the + * connection itself. Several rows may authenticate the same `connection_id` + * (staged rotation); the connection's durable lifecycle state lives in + * `sys_scim_connection_binding`, written by the library. + * + * Credential-at-rest posture (pinned by `credential-at-rest-posture.test.ts`, + * never relax it): `token_digest` holds an HMAC-SHA-256 (base64url, unpadded) + * of the bearer, keyed by the deployment's auth secret — one-way, never + * cleartext, and stronger than the unsalted SHA-256 the rc.1 line stored. The + * plaintext bearer is returned exactly once at mint time and is not + * recoverable from this row. + * + * @namespace sys + */ +export const SysScimConnectionCredential = ObjectSchema.create({ + name: 'sys_scim_connection_credential', + label: 'SCIM Connection Credential', + pluralLabel: 'SCIM Connection Credentials', + icon: 'users', + isSystem: true, + // ObjectStack owns this lifecycle end to end — plugin-auth's SCIM connection + // service is the only writer; no user CRUD, no better-auth involvement. + managedBy: 'engine-owned', + // [ADR-0066 D3/④] Admin-only identity config carrying a live credential + // digest — same object-level capability AND-gate as `sys_sso_provider` / + // `sys_scim_provider`: ordinary members are denied entirely, regardless of + // how permissive their CRUD grants are. + requiredPermissions: ['manage_platform_settings'], + // ADR-0010 §3.7 — platform-managed identity table; tenants may not edit schema. + protection: { + lock: 'full', + reason: 'ObjectStack-owned SCIM credential store (#3653) — see ADR-0071.', + docsUrl: 'https://objectstack.ai/docs/references/shared/protection', + }, + description: 'Bearer credentials (one-way digests) that authenticate SCIM provisioning connections', + displayNameField: 'connection_id', + nameField: 'connection_id', // [ADR-0079] canonical primary-title pointer + titleFormat: '{connection_id}', + highlightFields: ['connection_id', 'label', 'active', 'expires_at'], + + listViews: { + all: { + type: 'grid', + name: 'all', + label: 'All', + data: { provider: 'object', object: 'sys_scim_connection_credential' }, + // token_digest is intentionally excluded — never surface the credential + // column, even though it is one-way. + columns: ['connection_id', 'label', 'organization_id', 'active', 'expires_at', 'created_at'], + sort: [{ field: 'connection_id', order: 'asc' }], + pagination: { pageSize: 50 }, + }, + }, + + fields: { + id: Field.text({ label: 'ID', required: true, readonly: true, group: 'System' }), + + connection_id: Field.text({ + label: 'Connection ID', + required: true, + searchable: true, + maxLength: 255, + description: 'The SCIM connection this credential authenticates (e.g. "okta-prod"); scopes every resource the IdP provisions with it', + group: 'Connection', + }), + + provisioning_domain_id: Field.text({ + label: 'Provisioning Domain', + required: false, + maxLength: 255, + description: 'Application-owned boundary receiving provisioned resources; defaults to the connection id when absent', + group: 'Connection', + }), + + organization_id: Field.text({ + label: 'Organization', + required: false, + maxLength: 255, + description: 'Organization scope of this connection, when provisioning is org-scoped', + group: 'Connection', + }), + + label: Field.text({ + label: 'Label', + required: false, + maxLength: 255, + description: 'Operator-facing name for this credential (e.g. "rotation 2026-Q3")', + group: 'Identity', + }), + + token_digest: Field.text({ + label: 'Token Digest', + required: true, + readonly: true, + maxLength: 255, + description: 'HMAC-SHA-256 (base64url) of the bearer, keyed by the deployment auth secret — one-way; the plaintext is shown once at mint and never stored.', + group: 'Secret', + }), + + active: Field.boolean({ + label: 'Active', + required: true, + defaultValue: true, + description: 'Revocation switch — an inactive credential is refused at verification', + group: 'Lifecycle', + }), + + expires_at: Field.datetime({ + label: 'Expires At', + required: false, + description: 'Optional hard expiry for staged credential rotation; an expired credential is refused', + group: 'Lifecycle', + }), + + user_id: Field.lookup('sys_user', { + label: 'Minted By', + required: false, + description: 'User who minted this credential', + 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: [ + // The digest is the verification lookup key — deterministic keyed HMAC, so + // an indexed equality probe answers "which credential is this bearer". + { fields: ['token_digest'], unique: true }, + { fields: ['connection_id'] }, + { fields: ['organization_id'] }, + { fields: ['user_id'] }, + ], + + enable: { + trackHistory: true, + searchable: false, + apiEnabled: true, + // Mint / revoke go through plugin-auth's SCIM connection service; the + // generic data layer is read-only so the credential row cannot be written + // or bypassed through it. + apiMethods: ['list'], + }, +}); diff --git a/packages/platform-objects/src/identity/sys-scim-group-member.object.ts b/packages/platform-objects/src/identity/sys-scim-group-member.object.ts new file mode 100644 index 0000000000..b0ee343b09 --- /dev/null +++ b/packages/platform-objects/src/identity/sys-scim-group-member.object.ts @@ -0,0 +1,95 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_scim_group_member — SCIM group membership junction + * (`@better-auth/scim` stable `scimGroupMember`). + * + * One row per (group, member) pair the IdP pushes. The name survives from + * rc.1 but the anchoring is the stable rewrite's: the member references the + * per-connection `sys_scim_user` projection (`scim_user_id`), not the core + * user table. Columns mirror the installed stable schema, bridged mechanically + * camelCase → snake_case by `objectql-adapter.ts` (see + * `better-auth-schema-parity.test.ts`, #3653). + * + * @namespace sys + */ +export const SysScimGroupMember = ObjectSchema.create({ + name: 'sys_scim_group_member', + label: 'SCIM Group Member', + pluralLabel: 'SCIM Group Members', + icon: 'users', + isSystem: true, + managedBy: 'better-auth', + // 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 group membership rows pushed by the IdP (group ↔ provisioned user)', + titleFormat: '{scim_user_id} in {group_id}', + highlightFields: ['group_id', 'scim_user_id', 'created_at'], + + listViews: { + all: { + type: 'grid', + name: 'all', + label: 'All', + data: { provider: 'object', object: 'sys_scim_group_member' }, + columns: ['group_id', 'scim_user_id', 'connection_id', 'created_at'], + sort: [{ field: 'created_at', order: 'desc' }], + pagination: { pageSize: 50 }, + }, + }, + + fields: { + id: Field.text({ label: 'ID', required: true, readonly: true, group: 'System' }), + + connection_id: Field.text({ + label: 'Connection ID', + required: true, + maxLength: 255, + group: 'Connection', + }), + + group_id: Field.lookup('sys_scim_group', { + label: 'Group', + required: true, + group: 'Membership', + }), + + scim_user_id: Field.lookup('sys_scim_user', { + label: 'SCIM User', + required: true, + group: 'Membership', + }), + + membership_key: Field.text({ + label: 'Membership Key', + required: true, + readonly: true, + maxLength: 512, + description: 'Derived membership uniqueness key maintained by @better-auth/scim; do not write directly.', + group: 'System', + }), + + created_at: Field.datetime({ label: 'Created At', readonly: true, group: 'System' }), + }, + + indexes: [ + // UNIQUE mirrors @better-auth/scim's own declaration — what makes its + // concurrent-add recovery work (same shape as sys_team_member). + { fields: ['membership_key'], unique: true }, + { fields: ['group_id'] }, + { fields: ['scim_user_id'] }, + ], + + enable: { + trackHistory: true, + searchable: false, + apiEnabled: true, + apiMethods: ['get', 'list'], + }, +}); diff --git a/packages/platform-objects/src/identity/sys-scim-group.object.ts b/packages/platform-objects/src/identity/sys-scim-group.object.ts new file mode 100644 index 0000000000..5eaf6b28a4 --- /dev/null +++ b/packages/platform-objects/src/identity/sys-scim-group.object.ts @@ -0,0 +1,142 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_scim_group — IdP-pushed SCIM group for one connection + * (`@better-auth/scim` stable `scimGroup`). + * + * One row per group the IdP pushes over SCIM 2.0 `/Groups`. The name survives + * from rc.1 but the columns are the stable rewrite's: groups are scoped by + * `connection_id` / `provisioning_domain_id` rather than rc.1's + * provider/organization pair, and carry the library's derived uniqueness and + * pagination keys. Columns mirror the installed stable schema, bridged + * mechanically camelCase → snake_case by `objectql-adapter.ts` (see + * `better-auth-schema-parity.test.ts`, #3653). + * + * Provisioning this table (with `sys_scim_group_member`) is what closes the + * "IdP pushes groups into tables that do not exist" gap this card documented. + * + * @namespace sys + */ +export const SysScimGroup = ObjectSchema.create({ + name: 'sys_scim_group', + label: 'SCIM Group', + pluralLabel: 'SCIM Groups', + icon: 'users', + isSystem: true, + managedBy: 'better-auth', + // 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: 'IdP-pushed SCIM 2.0 groups, scoped per provisioning connection', + displayNameField: 'display_name', + nameField: 'display_name', // [ADR-0079] canonical primary-title pointer + titleFormat: '{display_name}', + highlightFields: ['display_name', 'connection_id', 'updated_at'], + + listViews: { + all: { + type: 'grid', + name: 'all', + label: 'All', + data: { provider: 'object', object: 'sys_scim_group' }, + columns: ['display_name', 'connection_id', 'external_id', 'updated_at'], + sort: [{ field: 'display_name', order: 'asc' }], + pagination: { pageSize: 50 }, + }, + }, + + fields: { + id: Field.text({ label: 'ID', required: true, readonly: true, group: 'System' }), + + connection_id: Field.text({ + label: 'Connection ID', + required: true, + maxLength: 255, + group: 'Connection', + }), + + provisioning_domain_id: Field.text({ + label: 'Provisioning Domain', + required: true, + maxLength: 255, + group: 'Connection', + }), + + revision: Field.number({ + label: 'Revision', + required: false, + defaultValue: 0, + readonly: true, + description: 'Optimistic-concurrency revision maintained by @better-auth/scim', + group: 'System', + }), + + display_name: Field.text({ + label: 'Display Name', + required: true, + searchable: true, + maxLength: 255, + description: 'SCIM displayName as sent by the IdP', + group: 'Identity', + }), + + display_name_key: Field.text({ + label: 'Display Name Key', + required: true, + readonly: true, + maxLength: 512, + description: 'Derived case-folded displayName uniqueness key maintained by @better-auth/scim; do not write directly.', + group: 'System', + }), + + external_id: Field.text({ + label: 'External ID', + required: false, + maxLength: 255, + description: 'IdP-assigned externalId, when the IdP sends one', + group: 'Identity', + }), + + external_id_key: Field.text({ + label: 'External ID Key', + required: false, + readonly: true, + maxLength: 512, + description: 'Derived externalId uniqueness key maintained by @better-auth/scim; do not write directly.', + group: 'System', + }), + + order_key: Field.text({ + label: 'Order Key', + required: true, + readonly: true, + maxLength: 512, + description: 'Derived stable-pagination key maintained by @better-auth/scim; do not write directly.', + group: 'System', + }), + + created_at: Field.datetime({ label: 'Created At', readonly: true, group: 'System' }), + updated_at: Field.datetime({ label: 'Updated At', readonly: true, group: 'System' }), + }, + + indexes: [ + // UNIQUE mirrors @better-auth/scim's own declarations. + { fields: ['display_name_key'], unique: true }, + // Nullable — repeated NULLs are admitted when the IdP sends no externalId. + { fields: ['external_id_key'], unique: true }, + { fields: ['order_key'], unique: true }, + { fields: ['connection_id'] }, + ], + + enable: { + trackHistory: true, + searchable: false, + apiEnabled: true, + apiMethods: ['get', 'list'], + }, +}); diff --git a/packages/platform-objects/src/identity/sys-scim-identity-tombstone.object.ts b/packages/platform-objects/src/identity/sys-scim-identity-tombstone.object.ts new file mode 100644 index 0000000000..8334af5c09 --- /dev/null +++ b/packages/platform-objects/src/identity/sys-scim-identity-tombstone.object.ts @@ -0,0 +1,114 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_scim_identity_tombstone — Deleted-identity tombstones + * (`@better-auth/scim` stable `scimIdentityTombstone`). + * + * When an IdP deletes a provisioned user over SCIM, the library keeps a + * tombstone of the identity it removed — connection scope, the IdP's + * externalId and the final serialized profile — so a re-provision of the same + * external identity is recognized rather than double-created. Columns mirror + * the installed stable schema, bridged mechanically camelCase → snake_case by + * `objectql-adapter.ts` (see `better-auth-schema-parity.test.ts`, #3653). + * + * @namespace sys + */ +export const SysScimIdentityTombstone = ObjectSchema.create({ + name: 'sys_scim_identity_tombstone', + label: 'SCIM Identity Tombstone', + pluralLabel: 'SCIM Identity Tombstones', + icon: 'users', + isSystem: true, + managedBy: 'better-auth', + // 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: 'Tombstones of IdP-deleted SCIM identities, kept so a re-provision is recognized', + displayNameField: 'external_id', + nameField: 'external_id', // [ADR-0079] canonical primary-title pointer + titleFormat: '{external_id}', + highlightFields: ['external_id', 'connection_id', 'deleted_at'], + + listViews: { + all: { + type: 'grid', + name: 'all', + label: 'All', + data: { provider: 'object', object: 'sys_scim_identity_tombstone' }, + columns: ['external_id', 'connection_id', 'user_id', 'deleted_at'], + sort: [{ field: 'deleted_at', order: 'desc' }], + pagination: { pageSize: 50 }, + }, + }, + + fields: { + id: Field.text({ label: 'ID', required: true, readonly: true, group: 'System' }), + + connection_id: Field.text({ + label: 'Connection ID', + required: true, + maxLength: 255, + group: 'Connection', + }), + + provisioning_domain_id: Field.text({ + label: 'Provisioning Domain', + required: true, + maxLength: 255, + group: 'Connection', + }), + + external_id: Field.text({ + label: 'External ID', + required: true, + maxLength: 255, + description: 'The IdP-assigned externalId of the deleted identity', + group: 'Identity', + }), + + external_id_key: Field.text({ + label: 'External ID Key', + required: true, + readonly: true, + maxLength: 512, + description: 'Derived externalId uniqueness key maintained by @better-auth/scim; do not write directly.', + group: 'System', + }), + + user_id: Field.lookup('sys_user', { + label: 'User', + required: true, + description: 'The platform user the deleted identity was linked to', + group: 'Identity', + }), + + profile: Field.textarea({ + label: 'Final Profile', + required: true, + readonly: true, + description: 'Serialized final SCIM profile at deletion time, maintained by @better-auth/scim', + group: 'System', + }), + + deleted_at: Field.datetime({ label: 'Deleted At', required: true, readonly: true, group: 'System' }), + }, + + indexes: [ + // UNIQUE mirrors @better-auth/scim's own declaration. + { fields: ['external_id_key'], unique: true }, + { fields: ['connection_id'] }, + { fields: ['user_id'] }, + ], + + enable: { + trackHistory: true, + searchable: false, + apiEnabled: true, + apiMethods: ['get', 'list'], + }, +}); diff --git a/packages/platform-objects/src/identity/sys-scim-projection-grant.object.ts b/packages/platform-objects/src/identity/sys-scim-projection-grant.object.ts new file mode 100644 index 0000000000..cc5f66d82b --- /dev/null +++ b/packages/platform-objects/src/identity/sys-scim-projection-grant.object.ts @@ -0,0 +1,138 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_scim_projection_grant — Role/entitlement grants projected from SCIM + * (`@better-auth/scim` stable `scimProjectionGrant`). + * + * The stable line's generalization of rc.1's `scimGroupRole` + + * `scimGroupRoleGrant` pair: one row per (source, role, user) grant a SCIM + * push projected onto a platform user, where the source can be a group, an + * entitlement, or a direct attribute (`source_kind` / `source_id` / + * `source_value`). Columns mirror the installed stable schema, bridged + * mechanically camelCase → snake_case by `objectql-adapter.ts` (see + * `better-auth-schema-parity.test.ts`, #3653). + * + * @namespace sys + */ +export const SysScimProjectionGrant = ObjectSchema.create({ + name: 'sys_scim_projection_grant', + label: 'SCIM Projection Grant', + pluralLabel: 'SCIM Projection Grants', + icon: 'users', + isSystem: true, + managedBy: 'better-auth', + // 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: 'Role/entitlement grants projected onto platform users by SCIM provisioning', + titleFormat: '{role} → {user_id}', + highlightFields: ['role', 'source_kind', 'user_id', 'connection_id'], + + listViews: { + all: { + type: 'grid', + name: 'all', + label: 'All', + data: { provider: 'object', object: 'sys_scim_projection_grant' }, + columns: ['role', 'source_kind', 'source_id', 'user_id', 'connection_id', 'updated_at'], + sort: [{ field: 'updated_at', order: 'desc' }], + pagination: { pageSize: 50 }, + }, + }, + + fields: { + id: Field.text({ label: 'ID', required: true, readonly: true, group: 'System' }), + + connection_id: Field.text({ + label: 'Connection ID', + required: true, + maxLength: 255, + group: 'Connection', + }), + + provisioning_domain_id: Field.text({ + label: 'Provisioning Domain', + required: true, + maxLength: 255, + group: 'Connection', + }), + + scim_user_id: Field.lookup('sys_scim_user', { + label: 'SCIM User', + required: true, + description: 'The per-connection user projection this grant belongs to', + group: 'Identity', + }), + + user_id: Field.lookup('sys_user', { + label: 'User', + required: true, + description: 'The platform user the grant is projected onto', + group: 'Identity', + }), + + source_kind: Field.text({ + label: 'Source Kind', + required: true, + maxLength: 64, + description: 'What projected the grant (e.g. group, entitlement, attribute)', + group: 'Grant', + }), + + source_id: Field.text({ + label: 'Source ID', + required: true, + maxLength: 255, + description: 'Identifier of the projecting source (e.g. the SCIM group id)', + group: 'Grant', + }), + + source_value: Field.text({ + label: 'Source Value', + required: false, + maxLength: 512, + description: 'Source attribute value, when the source kind carries one', + group: 'Grant', + }), + + role: Field.text({ + label: 'Role', + required: true, + maxLength: 255, + description: 'The role/entitlement projected onto the user', + group: 'Grant', + }), + + grant_key: Field.text({ + label: 'Grant Key', + required: true, + readonly: true, + maxLength: 512, + description: 'Derived grant uniqueness key maintained by @better-auth/scim; do not write directly.', + group: 'System', + }), + + created_at: Field.datetime({ label: 'Created At', readonly: true, group: 'System' }), + updated_at: Field.datetime({ label: 'Updated At', readonly: true, group: 'System' }), + }, + + indexes: [ + // UNIQUE mirrors @better-auth/scim's own declaration. + { fields: ['grant_key'], unique: true }, + { fields: ['scim_user_id'] }, + { fields: ['user_id'] }, + { fields: ['connection_id'] }, + ], + + enable: { + trackHistory: true, + searchable: false, + apiEnabled: true, + apiMethods: ['get', 'list'], + }, +}); diff --git a/packages/platform-objects/src/identity/sys-scim-subject.object.ts b/packages/platform-objects/src/identity/sys-scim-subject.object.ts new file mode 100644 index 0000000000..00a934e678 --- /dev/null +++ b/packages/platform-objects/src/identity/sys-scim-subject.object.ts @@ -0,0 +1,89 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_scim_subject — Per-user SCIM link registry + * (`@better-auth/scim` stable `scimSubject`). + * + * Exactly one row per platform user that any SCIM connection has provisioned. + * It records which connection's projection currently sources the user's + * profile (`profile_source_id` → a `sys_scim_user` row) and carries the + * revision counter the library uses to serialize concurrent provisioning + * writes across connections. Columns mirror the installed stable schema, + * bridged mechanically camelCase → snake_case by `objectql-adapter.ts` (see + * `better-auth-schema-parity.test.ts`, #3653). + * + * @namespace sys + */ +export const SysScimSubject = ObjectSchema.create({ + name: 'sys_scim_subject', + label: 'SCIM Subject', + pluralLabel: 'SCIM Subjects', + icon: 'users', + isSystem: true, + managedBy: 'better-auth', + // 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: 'Per-user SCIM provisioning link — one row per user any SCIM connection provisions', + titleFormat: '{user_id}', + highlightFields: ['user_id', 'profile_source_id', 'updated_at'], + + listViews: { + all: { + type: 'grid', + name: 'all', + label: 'All', + data: { provider: 'object', object: 'sys_scim_subject' }, + columns: ['user_id', 'profile_source_id', 'revision', 'updated_at'], + sort: [{ field: 'updated_at', order: 'desc' }], + pagination: { pageSize: 50 }, + }, + }, + + fields: { + id: Field.text({ label: 'ID', required: true, readonly: true, group: 'System' }), + + user_id: Field.lookup('sys_user', { + label: 'User', + required: true, + description: 'The platform user this subject row tracks (unique — one row per user)', + group: 'Identity', + }), + + profile_source_id: Field.text({ + label: 'Profile Source', + required: false, + maxLength: 255, + description: 'The sys_scim_user projection currently sourcing this user\'s profile', + group: 'Identity', + }), + + revision: Field.number({ + label: 'Revision', + required: true, + readonly: true, + description: 'Optimistic-concurrency revision maintained by @better-auth/scim', + group: 'System', + }), + + created_at: Field.datetime({ label: 'Created At', readonly: true, group: 'System' }), + updated_at: Field.datetime({ label: 'Updated At', readonly: true, group: 'System' }), + }, + + indexes: [ + // UNIQUE mirrors @better-auth/scim's own declaration — one row per user. + { fields: ['user_id'], unique: true }, + ], + + enable: { + trackHistory: true, + searchable: false, + apiEnabled: true, + apiMethods: ['get', 'list'], + }, +}); diff --git a/packages/platform-objects/src/identity/sys-scim-user.object.ts b/packages/platform-objects/src/identity/sys-scim-user.object.ts new file mode 100644 index 0000000000..e631aa6eb3 --- /dev/null +++ b/packages/platform-objects/src/identity/sys-scim-user.object.ts @@ -0,0 +1,233 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * sys_scim_user — Provisioned-user projection for one SCIM connection + * (`@better-auth/scim` stable `scimUser`). + * + * The stable line no longer writes IdP-provisioned attributes onto the core + * `user` table the way rc.1 did — each connection keeps its own projection of + * the user it provisioned here, linked to the real `sys_user` row through + * `user_id` (and de-duplicated across connections through `sys_scim_subject`). + * Columns mirror the installed stable schema, bridged mechanically + * camelCase → snake_case by `objectql-adapter.ts` (see + * `better-auth-schema-parity.test.ts`, #3653). + * + * The `*_key` / `*_index` columns are derived lookup/uniqueness keys the + * library owns end to end (`returned: false` upstream) — never authored or + * interpreted from the ObjectStack side. + * + * @namespace sys + */ +export const SysScimUser = ObjectSchema.create({ + name: 'sys_scim_user', + label: 'SCIM User', + pluralLabel: 'SCIM Users', + icon: 'users', + isSystem: true, + managedBy: 'better-auth', + // 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: 'Per-connection projection of an IdP-provisioned user (SCIM 2.0 /Users)', + displayNameField: 'user_name', + nameField: 'user_name', // [ADR-0079] canonical primary-title pointer + titleFormat: '{user_name}', + highlightFields: ['user_name', 'display_name', 'primary_email', 'active'], + + listViews: { + all: { + type: 'grid', + name: 'all', + label: 'All', + data: { provider: 'object', object: 'sys_scim_user' }, + columns: ['user_name', 'display_name', 'primary_email', 'connection_id', 'active', 'updated_at'], + sort: [{ field: 'user_name', order: 'asc' }], + pagination: { pageSize: 50 }, + }, + }, + + fields: { + id: Field.text({ label: 'ID', required: true, readonly: true, group: 'System' }), + + connection_id: Field.text({ + label: 'Connection ID', + required: true, + maxLength: 255, + description: 'SCIM connection that provisioned this user', + group: 'Connection', + }), + + provisioning_domain_id: Field.text({ + label: 'Provisioning Domain', + required: true, + maxLength: 255, + group: 'Connection', + }), + + user_id: Field.lookup('sys_user', { + label: 'User', + required: true, + description: 'The platform user this projection materialized as', + group: 'Identity', + }), + + connection_user_key: Field.text({ + label: 'Connection User Key', + required: true, + readonly: true, + maxLength: 512, + description: 'Derived (connection, user) uniqueness key maintained by @better-auth/scim; do not write directly.', + group: 'System', + }), + + user_name: Field.text({ + label: 'User Name', + required: true, + searchable: true, + maxLength: 255, + description: 'SCIM userName as sent by the IdP', + group: 'Identity', + }), + + user_name_key: Field.text({ + label: 'User Name Key', + required: true, + readonly: true, + maxLength: 512, + description: 'Derived case-folded userName uniqueness key maintained by @better-auth/scim; do not write directly.', + group: 'System', + }), + + primary_email: Field.text({ + label: 'Primary Email', + required: true, + searchable: true, + maxLength: 255, + group: 'Identity', + }), + + work_email_value_index: Field.text({ + label: 'Work Email Index', + required: true, + readonly: true, + maxLength: 512, + description: 'Derived lookup index maintained by @better-auth/scim; do not write directly.', + group: 'System', + }), + + email_value_index: Field.text({ + label: 'Email Index', + required: true, + readonly: true, + maxLength: 512, + description: 'Derived lookup index maintained by @better-auth/scim; do not write directly.', + group: 'System', + }), + + display_name: Field.text({ + label: 'Display Name', + required: true, + searchable: true, + maxLength: 255, + group: 'Identity', + }), + + formatted_name: Field.text({ + label: 'Formatted Name', + required: true, + maxLength: 255, + group: 'Identity', + }), + + given_name: Field.text({ + label: 'Given Name', + required: false, + maxLength: 255, + group: 'Identity', + }), + + family_name: Field.text({ + label: 'Family Name', + required: false, + maxLength: 255, + group: 'Identity', + }), + + serialized_emails: Field.textarea({ + label: 'Emails (serialized)', + required: true, + readonly: true, + description: 'Canonical serialized SCIM emails list maintained by @better-auth/scim; do not write directly.', + group: 'System', + }), + + serialized_attributes: Field.textarea({ + label: 'Attributes (serialized)', + required: false, + readonly: true, + description: 'Canonical serialized SCIM attributes maintained by @better-auth/scim; do not write directly.', + group: 'System', + }), + + external_id: Field.text({ + label: 'External ID', + required: false, + maxLength: 255, + description: 'IdP-assigned externalId, when the IdP sends one', + group: 'Identity', + }), + + external_id_key: Field.text({ + label: 'External ID Key', + required: false, + readonly: true, + maxLength: 512, + description: 'Derived externalId uniqueness key maintained by @better-auth/scim; do not write directly.', + group: 'System', + }), + + active: Field.boolean({ + label: 'Active', + required: true, + description: 'SCIM active flag — false means the IdP deactivated this user', + group: 'Identity', + }), + + order_key: Field.text({ + label: 'Order Key', + required: true, + readonly: true, + maxLength: 512, + description: 'Derived stable-pagination key maintained by @better-auth/scim; do not write directly.', + group: 'System', + }), + + created_at: Field.datetime({ label: 'Created At', readonly: true, group: 'System' }), + updated_at: Field.datetime({ label: 'Updated At', readonly: true, group: 'System' }), + }, + + indexes: [ + // UNIQUE mirrors @better-auth/scim's own declarations. + { fields: ['connection_user_key'], unique: true }, + { fields: ['user_name_key'], unique: true }, + // Nullable — repeated NULLs are admitted on sqlite / postgres / mysql when + // the IdP sends no externalId. + { fields: ['external_id_key'], unique: true }, + { fields: ['order_key'], unique: true }, + { fields: ['connection_id'] }, + { fields: ['user_id'] }, + ], + + enable: { + trackHistory: true, + searchable: false, + apiEnabled: true, + // Reads only: all mutations flow through the SCIM 2.0 protocol endpoints. + apiMethods: ['get', 'list'], + }, +}); diff --git a/packages/plugins/plugin-auth/package.json b/packages/plugins/plugin-auth/package.json index d89451f9a3..007f070c2d 100644 --- a/packages/plugins/plugin-auth/package.json +++ b/packages/plugins/plugin-auth/package.json @@ -26,7 +26,7 @@ "dependencies": { "@better-auth/core": "^1.7.1", "@better-auth/oauth-provider": "^1.7.1", - "@better-auth/scim": "^1.7.1", + "@better-auth/scim": "1.7.1", "@better-auth/sso": "^1.7.1", "@noble/hashes": "^2.3.0", "@objectstack/core": "workspace:*", diff --git a/packages/plugins/plugin-auth/src/auth-manager.ts b/packages/plugins/plugin-auth/src/auth-manager.ts index ef36b65545..dd6510fb0a 100644 --- a/packages/plugins/plugin-auth/src/auth-manager.ts +++ b/packages/plugins/plugin-auth/src/auth-manager.ts @@ -961,6 +961,14 @@ async function smsQuotaExceededApiError(message: string): Promise { export class AuthManager { private auth: Auth | null = null; private config: AuthManagerOptions; + /** + * [#3653] The auth secret, resolved ONCE per manager. `generateSecret()`'s + * dev fallback is `'dev-secret-' + Date.now()` — a fresh value per call — so + * every consumer that needs the same key material (better-auth's own + * `secret` and the SCIM credential digests, which are keyed HMACs over it) + * must read this memo rather than re-resolving. + */ + private resolvedAuthSecret?: string; /** * [#8289] The org-role ac map handed to the `organization` plugin as `roles` * (`undefined` → the plugin runs on better-auth's `defaultRoles`). Stashed at @@ -1061,7 +1069,7 @@ export class AuthManager { const passwordHasher = await this.resolvePasswordHasher(); const betterAuthConfig: BetterAuthOptions = { // Base configuration - secret: this.config.secret || this.generateSecret(), + secret: this.resolveAuthSecret(), // Absolute origin (getCanonicalOrigin prepends https:// when baseUrl is a // bare host) so the reset-password / verify-email / magic-link URLs // better-auth derives from baseURL are always clickable links. @@ -3091,24 +3099,39 @@ export class AuthManager { // External SCIM 2.0 Service Provider (@better-auth/scim, MIT) — lets an // external IdP (Okta / Entra) auto-provision / deprovision THIS env's users // (the paid Identity lifecycle, ADR-0071). The env is the SCIM Service - // 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. 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`. + // Provider; SCIM 2.0 endpoints mount under /api/v1/auth/scim/v2/{Users,…}. + // `active:false` → ban + session revoke (needs the admin plugin, forced on + // above). This plugin still accepts no `schema` option (`SCIMOptions` + // declares no `schema` / `modelName` / `fields` member on the installed + // 1.7.1), so its seven models are bridged to the `sys_scim_*` platform + // objects 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). + // Connections are RUNTIME DATA, not boot config (#3653): the stable + // constructor's three-way requirement (static `connections` | bearer + // verifier | managed catalog) is satisfied with an application-owned + // `verifyBearerToken` that resolves the connection from a + // `sys_scim_connection_credential` row at request time. ObjectStack owns + // the whole credential lifecycle — mint / store / verify — in + // `scim-connection-service.ts`; only a keyed one-way digest is ever + // persisted (pinned by `credential-at-rest-posture.test.ts`). The rc.1 + // `storeSCIMToken` / `/scim/generate-token` surface no longer exists on + // stable, and the upstream `managedConnections` catalog is deliberately + // NOT adopted (maintainer ruling 2026-08-25). if (enabled.scim) { await this.addOptionalPlugin(plugins, 'scim', async () => { const { scim } = await import('@better-auth/scim'); - return scim({ storeSCIMToken: 'hashed' }); + const { verifyScimBearerToken } = await import('./scim-connection-service.js'); + const secret = this.resolveAuthSecret(); + return scim({ + connections: [], + authentication: { + verifyBearerToken: async (input) => { + const engine = this.config.dataEngine; + if (!engine) return null; // no store to verify against — fail closed + return verifyScimBearerToken(engine as never, secret, input.token); + }, + }, + }); }); } @@ -3268,6 +3291,14 @@ export class AuthManager { /** * Generate a secure secret if not provided */ + /** The auth secret, resolved once — see {@link resolvedAuthSecret}. */ + private resolveAuthSecret(): string { + if (!this.resolvedAuthSecret) { + this.resolvedAuthSecret = this.config.secret || this.generateSecret(); + } + return this.resolvedAuthSecret; + } + private generateSecret(): string { const envSecret = readEnvWithDeprecation('OS_AUTH_SECRET', ['AUTH_SECRET', 'BETTER_AUTH_SECRET'], { silent: true }); if (envSecret) return envSecret; diff --git a/packages/plugins/plugin-auth/src/auth-schema-config.ts b/packages/plugins/plugin-auth/src/auth-schema-config.ts index e37a685898..4f3ffc1c96 100644 --- a/packages/plugins/plugin-auth/src/auth-schema-config.ts +++ b/packages/plugins/plugin-auth/src/auth-schema-config.ts @@ -977,18 +977,19 @@ export const AUTH_SSO_PROVIDER_SCHEMA = { // revisiting it is the open architecture question on #8224. See ADR-0024. // --------------------------------------------------------------------------- -// SCIM plugin – scimProvider table (@better-auth/scim) +// SCIM plugin – scim* tables (@better-auth/scim) // --------------------------------------------------------------------------- -// NOTE: there is intentionally no `scimProvider` mapping constant here, and no -// `buildScimPluginSchema()`. `@better-auth/scim` hardcodes its model and exposes -// NO `schema` option — still true of the installed `@better-auth/scim@1.7.0-rc.1` +// NOTE: there is intentionally no scim mapping constant here, and no +// `buildScimPluginSchema()`. `@better-auth/scim` hardcodes its models and exposes +// NO `schema` option — still true of the installed stable `@better-auth/scim@1.7.1` // (`SCIMOptions` declares no `schema` / `modelName` / `fields` member at all; -// measured 2026-08-19), so there is nowhere to hand one. This is no longer true +// measured 2026-08-19 on the rc, re-measured 2026-08-27 on stable, #3653), so +// there is nowhere to hand one. This is no longer true // of `@better-auth/sso@1.7.1`, which now accepts one (#8224) — for scim, and for // scim alone, the ADAPTER layer is the only available route. // -// `sys_scim_provider`'s column names are therefore owned by that adapter layer +// The `sys_scim_*` column names are therefore owned by that adapter layer // alone: AUTH_MODEL_TO_PROTOCOL + the camelCase → snake_case field resolution in // objectql-adapter.ts, pinned by the sso/scim block in // better-auth-schema-parity.test.ts. A copy of those names here would be a 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 d60d80b040..dc71d07437 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 @@ -47,20 +47,20 @@ * 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-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): + * the installed `@better-auth/sso@1.7.1` / `@better-auth/scim@1.7.0-rc.1`, and + * again 2026-08-27 against the stable `@better-auth/scim@1.7.1` after the + * #3653 migration moved the pin (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 - * `scimGroup*` models. The library is not hiding them. + * `ssoProvider` model; the stable scim (constructed with the gate options + * below) yields the seven `scim*` models. The library is not hiding them. * - `@better-auth/sso` also ACCEPTS a `schema` option now: * `SSOOptions.schema.ssoProvider.{modelName,fields,additionalFields}`, honoured * at runtime (`modelName: options?.modelName ?? options?.schema?.ssoProvider * ?.modelName ?? 'ssoProvider'`, plus a per-field `fieldName` fallback each). * `@better-auth/scim` still accepts none — `SCIMOptions` declares no - * `schema` / `modelName` / `fields` — so that half of the old sentence - * survives for scim alone. + * `schema` / `modelName` / `fields`, on stable 1.7.1 as on the rc — so + * that half of the old sentence survives for scim alone. * - What holds for both, and is the actual reason: **the auth manager passes * them no `schema` option.** Their models are bridged at the ADAPTER layer * instead (`AUTH_MODEL_TO_PROTOCOL` + a mechanical camelCase → snake_case in @@ -114,7 +114,14 @@ import { SysSession, SysTeam, SysTeamMember, + SysScimConnectionBinding, + SysScimGroup, + SysScimGroupMember, + SysScimIdentityTombstone, + SysScimProjectionGrant, SysScimProvider, + SysScimSubject, + SysScimUser, SysSsoProvider, SysTwoFactor, SysUser, @@ -145,7 +152,12 @@ const PLATFORM_OBJECTS: Record = Object.fromEntries( 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, + SysScimConnectionBinding, SysScimGroup, SysScimGroupMember, + SysScimIdentityTombstone, SysScimProjectionGrant, SysScimSubject, + SysScimUser, ] as unknown as PlatformObject[]).map((o) => [o.name, o]), ); @@ -252,8 +264,9 @@ const AUTH_MANAGER_PLUGINS: Record unknown } | { skip scim: { skip: 'same adapter bridge as sso and the same dedicated block below. `SCIMOptions` additionally ' - + 'still declares no `schema` / `modelName` / `fields` option at all on 1.7.0-rc.1, so there ' - + 'is nothing to pass it even if the bridge moved onto the plugin option.', + + 'still declares no `schema` / `modelName` / `fields` option at all on the installed stable ' + + '1.7.1 (re-measured 2026-08-27, #3653), so there is nothing to pass it even if the bridge ' + + 'moved onto the plugin option.', }, oauthProvider: { skip: @@ -457,9 +470,9 @@ function adapterColumn(field: string): string { } /** - * Construct `@better-auth/scim` with options BOTH the pinned prerelease and the - * stable line accept, so this gate reaches its diff-printing stage instead of - * dying in collection (#11380). + * Construct `@better-auth/scim` with options BOTH the rc.1 prerelease (the pin + * until #3653 landed) and the stable line accept, so this gate reaches its + * diff-printing stage instead of dying in collection (#11380). * * The gate was written down as the safety net for the `@better-auth/scim` * migration (#3653): the day the pin moves off `1.7.0-rc.1`, it is supposed to @@ -548,19 +561,19 @@ function scimSchema(): Record }> { /** * SCIM models with no platform object, acknowledged rather than silently - * skipped. These four are SCIM **group** provisioning (`/Groups` push from the - * IdP); ObjectStack ships only the provider row today, so an IdP pushing - * groups would write tables that do not exist — filed as its own feature gap. + * skipped. EMPTY as of the stable-1.7.x migration (#3653): every model the + * installed plugin declares is provisioned and column-checked below — the + * rc.1-era group-provisioning gap (four acknowledged `scimGroup*` models with + * no tables) is closed. * - * Pinned as an exact set on purpose: a NEW unmapped model is a build failure, - * so this list can never quietly grow the way the original hole did. + * Still pinned as an exact set on purpose, both directions: a NEW name here + * is a model the plugin added that nothing provisions (a build failure, so + * the list can never quietly grow the way the original hole did), and a name + * that DISAPPEARS from the plugin side is caught by the same assertion. + * ⛔ Widening this set to get a version bump green is the gate-weakening the + * #3653 maintainer ruling forbids — declare the object instead. */ -const KNOWN_UNMAPPED_MODELS = new Set([ - 'scimGroup', - 'scimGroupMember', - 'scimGroupRole', - 'scimGroupRoleGrant', -]); +const KNOWN_UNMAPPED_MODELS = new Set([]); describe('@better-auth/sso + @better-auth/scim schema ↔ platform-objects parity (#3653)', () => { const plugins: Array<{ label: string; schema: Record }> }> = [ @@ -579,8 +592,10 @@ describe('@better-auth/sso + @better-auth/scim schema ↔ platform-objects parit >; // 1. getAuthTables() DOES see them — "cannot see them" was never the reason. + // (`scimUser`, not rc.1's `scimProvider`: stable 1.7.x no longer derives + // that model at all — #3653.) expect(Object.keys(derived)).toContain('ssoProvider'); - expect(Object.keys(derived)).toContain('scimProvider'); + expect(Object.keys(derived)).toContain('scimUser'); // 2. …but under better-auth's own model name, which is not the platform // object name, because the auth manager passes no `schema.modelName`. 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 bdcf0379d9..eb16e1dbf2 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 @@ -1,130 +1,86 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. /** - * [#8192] The credential-at-rest posture for `sys_scim_provider.scim_token` and + * [#8192 → #3653] The credential-at-rest posture for + * `sys_scim_connection_credential.token_digest` and * `sys_oauth_application.client_secret` is PINNED here. * - * ## Why this file exists (nothing is broken — that is the point) + * ## History — why the SCIM half of this file changed shape (#3653) * - * #8011 measured, by real round trip, that both columns already store a one-way - * SHA-256 digest. This file is not a defect fix; it is the tripwire that holds - * that posture in place. The two columns get there by two mechanisms with very - * different fragilities: + * This file originally pinned `sys_scim_provider.scim_token`, whose hashing + * hung on a single option literal: `scim({ storeSCIMToken: 'hashed' })` in + * `auth-manager.ts`, against an upstream default of **`'plain'` — cleartext**. + * The stable `@better-auth/scim` 1.7.x line VACATED that posture rather than + * weakening it: `storeSCIMToken`, `/scim/generate-token` and the + * `scimProvider` model are all gone (0 occurrences), and **no stable model + * declares any token/secret/credential column at all** — upstream stores + * nothing. Under the application-owned `verifyBearerToken` route ObjectStack + * now owns the whole credential lifecycle — mint, store, verify — in + * `scim-connection-service.ts` + `sys_scim_connection_credential`. * - * | column | how hashing is obtained | upstream default if the wiring is dropped | - * |:--------------------------------|:---------------------------------------|:------------------------------------------| - * | `sys_scim_provider.scim_token` | EXPLICIT `scim({ storeSCIMToken: 'hashed' })` in `auth-manager.ts` | **`'plain'` — cleartext** | - * | `sys_oauth_application.client_secret` | IMPLICIT — nothing is passed; `@better-auth/oauth-provider` resolves `storeClientSecret: disableJwtPlugin ? 'encrypted' : 'hashed'` | `'hashed'` | + * So the pin moved WITH the credential. The posture it holds is deliberately + * at parity or better than what the old literal bought: * - * The SCIM row is the sharp one, and the asymmetry is structural rather than a - * matter of which default happens to be safer. Upstream's two helpers END - * differently on an unrecognised storage method: + * | column | posture | how it is obtained | + * |:---|:---|:---| + * | `sys_scim_connection_credential.token_digest` | HMAC-SHA-256(secret, "scim-credential-v1:" + token), base64url | `mintScimConnectionCredential()` — ObjectStack code, no upstream default to fall back to | + * | `sys_oauth_application.client_secret` | SHA-256(secret), base64url | IMPLICIT — `@better-auth/oauth-provider` resolves `storeClientSecret: 'hashed'` | * - * - `@better-auth/scim`'s `storeSCIMToken()` falls through to - * `return scimToken` — cleartext, silently. - * - `@better-auth/oauth-provider`'s `storeClientSecret()` ends in - * `throw new BetterAuthError("Unsupported storeClientSecret type …")`. + * "Better": rc.1 stored an UNSALTED SHA-256 of the token; the keyed digest + * additionally resists offline table matching if the table leaks without the + * deployment secret. Case ②-b pins the unkeyed shape as a NEGATIVE so a + * future "simplification" back to bare SHA-256 goes red. * - * So `client_secret` cannot reach cleartext by a dropped literal at all: the - * only non-hashed value the plugin will accept is `'encrypted'` (and only with - * `disableJwtPlugin`, which its own validation cross-checks). `scim_token` can, - * and a single option literal is the only thing between a live IdP bearer and a - * cleartext value in a column that is readable over the generic data API - * (`sys_scim_provider` declares `apiMethods: ['list']`). - * Before this file, `grep -rn 'storeSCIMToken|storeClientSecret' --include=*.test.ts` - * returned **zero hits** — so dropping that literal (a plausible edit during the - * `@better-auth/scim` rc.2 migration tracked in #3653, which changes this exact - * call site's construction signature) would persist the bearer in cleartext with - * the entire gate farm green. + * ## The control arm, restated honestly (#3653) * - * ## What makes this pin non-vacuous — five deliberate choices + * Case ③ used to construct `scim()` with NO option and MEASURE upstream's + * cleartext default — proof the repo literal was load-bearing. That arm is no + * longer measurable, because upstream now stores nothing at all: there is no + * upstream write path to compare against. Its honest successor asserts the + * fact that vacated it, from the installed artifact itself: no stable scim + * model declares a credential-shaped column, and the rc.1 mint endpoint + * answers 404 through the repo's own manager. If ③ ever goes RED, upstream + * has grown a credential store again — re-read whether ObjectStack's own + * mint/verify is still the right ownership boundary before touching anything + * here (do NOT delete the service to get green). * - * 1. **It drives `AuthManager`, not `scim()`.** This is the load-bearing choice. - * A test that calls `scim({ storeSCIMToken: 'hashed' })` itself pins its own - * literal and would stay green forever after the repo's literal was deleted — - * it would certify exactly the regression it was written to catch. Every - * assertion below runs against a plugin list built by the REPO's own - * `AuthManager.buildPluginList()`, so the option literal in `auth-manager.ts` - * is inside the system under test. - * 2. **It reads the row at DRIVER level**, below the ObjectQL read mask and below - * the plugin's own accessor. Asking better-auth what it stored proves nothing - * about what is on disk. - * 3. **It asserts the hash RELATIONSHIP, recomputed independently.** The expected - * digest is computed here with `node:crypto`, not by calling better-auth's - * hasher. "Differs from the plaintext" would be satisfied by base64 of the - * plaintext, which is not a credential-at-rest posture at all. - * 4. **It pins the inner-token trap as an explicit NEGATIVE.** For SCIM the value - * handed to the caller is `base64url("BASE:providerId:organizationId")` while - * the stored digest is over the inner `BASE` alone. Hashing the full bearer - * does NOT reproduce the stored value. #8011 measured both; case ②-b below - * asserts the full-bearer digest does not match, so a future rewrite cannot - * "fix" this file into the naive shape that fails on correct code. - * 5. **The control arm measures the upstream default rather than citing it.** - * Case ③ constructs `scim()` with NO option and shows the bearer lands in - * cleartext. That is what makes the literal demonstrably load-bearing: without - * it a reader cannot tell whether `storeSCIMToken: 'hashed'` is doing work or - * is redundant belt-and-braces. It is also the permanent form of this card's - * ablation — the "flip it to plain and watch it go red" experiment, kept. + * ## What makes this pin non-vacuous — the same five choices, restated * - * ## Ablation measured when this file landed (predicted first, then run) - * - * - `storeSCIMToken: 'hashed'` → `'plain'` in `auth-manager.ts`: case ②-a red, - * `stored` = the raw 24-char inner base token, everything else green. - * - `storeClientSecret: { hash: async (s) => s }` added to the same file's - * `oauthProvider(...)` call: case ① red, `stored` = the 32-char plaintext - * secret, everything else green. Note the shape — `'plain'` is NOT usable as - * an ablation here, because `storeClientSecret()` throws on it rather than - * storing cleartext. - * - * Predicted and measured agreed in both directions, including the deliberate - * prediction that the AUTHENTICATES case stays GREEN under the SCIM ablation: - * `verifySCIMToken` mirrors whatever storage method is configured, so with - * `'plain'` it compares plaintext to plaintext and still succeeds. That case is - * a liveness arm, not a posture detector, and the ablation is what establishes - * the difference. - * - * And each positive case asserts the credential still AUTHENTICATES. Hashing is - * trivially satisfiable by storing garbage; a posture pin that only checked "not - * cleartext" would pass on a build where SCIM provisioning is entirely broken — - * the same trap `sso-client-secret-at-rest.test.ts` (#8009) names in its header. - * - * ## One honest deviation from the usual "the test chooses the plaintext" bar - * - * For `sso-client-secret-at-rest.test.ts` the secret is caller-supplied, so that - * file feeds in a literal of its own and nothing in the implementation can - * supply it. Neither credential here works that way: `/scim/generate-token` - * mints `generateRandomString(24)` and `/oauth2/register` mints - * `generateRandomString(32)` server-side by design — RFC 7591 registration does - * not accept a client-chosen secret. So the plaintext is necessarily observed - * rather than chosen. The independence that bar protects is preserved by the - * ORACLE instead: the expected digest is recomputed here from that observed - * plaintext by a different implementation (`node:crypto`), and the negative - * cases below pin the two ways the relationship could be wrong. Stated plainly - * rather than papered over. + * 1. **Verification drives `AuthManager`.** The AUTHENTICATES cases run + * against `manager.handleRequest()`, so the `verifyBearerToken` wiring in + * `auth-manager.ts` — not the service called directly — is inside the + * system under test. (Mint has no HTTP surface yet by design — the Setup + * admin surface is its own epic leg — so ② mints through the service the + * manager's verifier reads.) + * 2. **It reads the row at DRIVER level**, below the ObjectQL read mask and + * below the service's own accessor. + * 3. **It asserts the digest RELATIONSHIP, recomputed independently** with + * `node:crypto` here — not by importing the service's digest function, + * which would certify whatever the implementation does. + * 4. **It pins the wrong-digest shapes as explicit NEGATIVES**: the unkeyed + * SHA-256 (the rc.1 posture), a wrong-key HMAC, and a missing + * domain-separation prefix must all NOT match the stored value. + * 5. **Each positive case asserts the credential still AUTHENTICATES** a real + * SCIM 2.0 request — hashing is trivially satisfiable by storing garbage — + * and the rejection paths (forged, revoked, expired) pin status and error + * envelope, so the 200 is evidence of verification, not an open door. * * ## Backend note * - * A real `ObjectQL` over `@objectstack/driver-sql` + better-sqlite3 `:memory:`, - * the backend the sibling at-rest pin (#8009) and the SCIM adapter tests already - * use. + * A real `ObjectQL` over `@objectstack/driver-sql` + better-sqlite3 + * `:memory:`, the backend the sibling at-rest pin (#8009) already uses. */ import { describe, it, expect, afterEach } from 'vitest'; -import { createHash } from 'node:crypto'; +import { createHash, createHmac } from 'node:crypto'; import { ObjectQL } from '@objectstack/objectql'; import { SqlDriver } from '@objectstack/driver-sql'; -import { betterAuth } from 'better-auth'; import { AuthManager } from './auth-manager.js'; -import { createObjectQLAdapterFactory } from './objectql-adapter.js'; -import { - buildOrganizationPluginSchema, - buildAdminPluginSchema, - AUTH_USER_CONFIG, - AUTH_SESSION_CONFIG, - AUTH_ACCOUNT_CONFIG, - AUTH_VERIFICATION_CONFIG, -} from './auth-schema-config.js'; import { createTenancyService } from './tenancy-service.js'; +import { + mintScimConnectionCredential, + SCIM_BEARER_PREFIX, +} from './scim-connection-service.js'; import { SysUser, SysSession, @@ -136,38 +92,47 @@ import { SysTeam, SysTeamMember, SysScimProvider, + SysScimConnectionBinding, + SysScimConnectionCredential, + SysScimGroup, + SysScimGroupMember, + SysScimIdentityTombstone, + SysScimProjectionGrant, + SysScimSubject, + SysScimUser, SysOauthApplication, SysOauthAccessToken, SysOauthRefreshToken, SysOauthConsent, SysJwks, } from '@objectstack/platform-objects'; -import { inviteForAudienceGate } from './audience-gate-test-support'; const BASE = 'http://localhost:3000'; const AUTH = `${BASE}/api/v1/auth`; const SECRET = 'test-secret-at-least-32-chars-long-8192'; -const SCIM_PROVIDER_OBJECT = 'sys_scim_provider'; +const SCIM_CREDENTIAL_OBJECT = 'sys_scim_connection_credential'; const OAUTH_APPLICATION_OBJECT = 'sys_oauth_application'; /** - * SHA-256 → base64url, unpadded. Recomputed HERE rather than imported from - * better-auth: an expectation produced by the implementation under test cannot - * fail. This mirrors `@better-auth/utils`' `defaultKeyHasher`, and #8011 - * measured the two agree byte for byte. + * SHA-256 → base64url, unpadded. Recomputed HERE rather than imported: an + * expectation produced by the implementation under test cannot fail. Positive + * oracle for the oauth case (①), and the UNKEYED negative for the scim case + * (②-b) — the rc.1-era posture the keyed digest deliberately exceeds. */ function sha256b64url(value: string): string { return createHash('sha256').update(value, 'utf8').digest('base64url'); } /** - * The SCIM bearer handed to the IdP admin is - * `base64url("BASE:providerId:organizationId")`. The stored digest covers the - * inner `BASE` only — see `generateSCIMToken` in `@better-auth/scim`. + * The scim digest relationship, recomputed independently with `node:crypto`: + * HMAC-SHA-256 keyed by the deployment auth secret over the domain-separated + * bearer. Mirrors `scim-connection-service.ts`'s documented construction — + * the negatives in ②-b are what keep this from being a tautology if the two + * ever drift. */ -function decodeInnerBaseToken(bearer: string): string { - return Buffer.from(bearer, 'base64url').toString('utf8').split(':')[0]; +function hmacScimDigest(secret: string, token: string): string { + return createHmac('sha256', secret).update(`scim-credential-v1:${token}`, 'utf8').digest('base64url'); } const engines: ObjectQL[] = []; @@ -194,6 +159,15 @@ const AUTH_OBJECTS = [ SysTeam, SysTeamMember, SysScimProvider, + // The stable scim model set + the ObjectStack-owned credential store (#3653). + SysScimConnectionBinding, + SysScimConnectionCredential, + SysScimGroup, + SysScimGroupMember, + SysScimIdentityTombstone, + SysScimProjectionGrant, + SysScimSubject, + SysScimUser, SysOauthApplication, SysOauthAccessToken, SysOauthRefreshToken, @@ -225,7 +199,7 @@ async function bootEngine(): Promise { /** * The stored row read at DRIVER level — below every engine read mask, and below - * the plugin accessor that would happily tell us what it *meant* to store. + * the service accessor that would happily tell us what it *meant* to store. */ async function readRowsAtRest( engine: ObjectQL, @@ -240,77 +214,11 @@ async function readRowsAtRest( return (Array.isArray(found) ? found : [found]).filter(Boolean) as Record[]; } -function cookiesFrom(response: Response): string { - return (response.headers.get('set-cookie') ?? '') - .split(',') - .map((c) => c.split(';')[0].trim()) - .filter(Boolean) - .join('; '); -} - -/** Sign up through the real pipeline and return the session cookie header. */ -async function signUpAdmin( - send: (request: Request) => Promise, - email = 'admin@example.com', - engine?: unknown, -): Promise { - // [#11739] default posture invite_only: fixture users beyond the first - // enter through the invitation carve-out (see audience-gate-test-support). - if (engine) await inviteForAudienceGate(engine, email); - const res = await send( - new Request(`${AUTH}/sign-up/email`, { - method: 'POST', - headers: { 'content-type': 'application/json', origin: BASE }, - body: JSON.stringify({ email, password: 'S3cure!Passw0rd-8192', name: 'Admin' }), - }), - ); - expect(res.status, `sign-up/email failed: ${await res.clone().text()}`).toBeLessThan(400); - return cookiesFrom(res); -} - -async function createOrganization( - send: (request: Request) => Promise, - cookie: string, - slug: string, -): Promise { - const res = await send( - new Request(`${AUTH}/organization/create`, { - method: 'POST', - headers: { 'content-type': 'application/json', origin: BASE, cookie }, - body: JSON.stringify({ name: 'Probe Org', slug }), - }), - ); - expect(res.status, `organization/create failed: ${await res.clone().text()}`).toBeLessThan(400); - const body = (await res.json()) as Record; - const id = (body?.id ?? (body?.organization as Record)?.id) as string; - expect(id, 'organization/create must return an organization id').toBeTruthy(); - return id; -} - -async function generateScimToken( - send: (request: Request) => Promise, - cookie: string, - organizationId: string, - providerId = 'okta-probe', -): Promise { - const res = await send( - new Request(`${AUTH}/scim/generate-token`, { - method: 'POST', - headers: { 'content-type': 'application/json', origin: BASE, cookie }, - body: JSON.stringify({ providerId, organizationId }), - }), - ); - expect(res.status, `scim/generate-token failed: ${await res.clone().text()}`).toBeLessThan(400); - const body = (await res.json()) as { scimToken?: string }; - expect(body.scimToken, 'generate-token must return the plaintext bearer exactly once').toBeTruthy(); - return body.scimToken!; -} - /** * The manager under test — built exactly the way a deployment with SCIM and the - * OIDC provider turned on builds it. Nothing here names `storeSCIMToken` or - * `storeClientSecret`; those come from `AuthManager.buildPluginList()`, which is - * the whole point. + * OIDC provider turned on builds it. Nothing here names a digest or a storage + * method; the verifier wiring comes from `AuthManager.buildPluginList()`, which + * is the whole point. */ function makeManager(engine: ObjectQL): AuthManager { return new AuthManager({ @@ -318,8 +226,7 @@ function makeManager(engine: ObjectQL): AuthManager { baseUrl: BASE, dataEngine: engine as never, // ADR-0093 D5 / #5233 — `organization/create` is gated by the EFFECTIVE - // tenancy posture. SCIM tokens are org-scoped by construction, so a wall - // that is actually in force is part of the fixture, not part of the claim. + // tenancy posture; a wall that is actually in force is part of the fixture. getTenancy: () => createTenancyService({ requested: 'isolated', probeIsolation: () => true }), plugins: { scim: true, @@ -330,68 +237,63 @@ function makeManager(engine: ObjectQL): AuthManager { } as never); } -describe('[#8192] sys_scim_provider.scim_token is hashed at rest — via the repo’s own wiring', () => { - it('② the persisted bearer is the SHA-256 digest of the INNER base token, never cleartext', async () => { +describe('[#8192/#3653] sys_scim_connection_credential.token_digest is a keyed one-way digest — via the repo’s own wiring', () => { + it('② the persisted value is the keyed HMAC of the bearer, never cleartext and never the unkeyed rc.1 shape', async () => { const engine = await bootEngine(); - const manager = makeManager(engine); - const send = (request: Request) => manager.handleRequest(request); - const cookie = await signUpAdmin(send, undefined, engine); - const organizationId = await createOrganization(send, cookie, 'probe-org-8192'); - const bearer = await generateScimToken(send, cookie, organizationId); + const { token, credentialId } = await mintScimConnectionCredential(engine as never, SECRET, { + connectionId: 'okta-probe', + organizationId: 'probe-org-8192', + }); + expect(token.startsWith(SCIM_BEARER_PREFIX)).toBe(true); + expect(credentialId).toBeTruthy(); - const rows = await readRowsAtRest(engine, SCIM_PROVIDER_OBJECT); + const rows = await readRowsAtRest(engine, SCIM_CREDENTIAL_OBJECT); expect(rows).toHaveLength(1); - const stored = String(rows[0].scim_token); - const inner = decodeInnerBaseToken(bearer); - - // Sanity on the decode itself, so a change in the bearer's SHAPE surfaces - // here rather than silently turning the cases below into tautologies. - expect(inner).toBeTruthy(); - expect(inner).not.toBe(bearer); - expect(Buffer.from(bearer, 'base64url').toString('utf8')).toContain(`:${organizationId}`); + const stored = String(rows[0].token_digest); // ②-a THE POSITIVE. Recomputed by node:crypto from the observed plaintext. - expect(stored).toBe(sha256b64url(inner)); - - // ②-b THE NEGATIVE THAT CATCHES THE NAIVE PIN. Hashing the full bearer — - // the string the caller actually received — must NOT match. #8011 measured - // this; without the case, a future rewrite could "simplify" ②-a into the - // shape that fails while the code is correct. - expect(stored).not.toBe(sha256b64url(bearer)); - - // ②-c not cleartext, in either spelling, anywhere in the row. - expect(stored).not.toBe(bearer); - expect(stored).not.toBe(inner); - expect(JSON.stringify(rows[0])).not.toContain(inner); - expect(JSON.stringify(rows[0])).not.toContain(bearer); + expect(stored).toBe(hmacScimDigest(SECRET, token)); + + // ②-b THE NEGATIVES THAT CATCH THE WRONG SHAPES. The unkeyed SHA-256 is + // the rc.1 posture this store deliberately exceeds; the wrong-key HMAC + // pins that the digest is bound to THIS deployment's secret; the + // no-domain-separation HMAC pins the input framing. Any of these matching + // means the relationship in ②-a stopped being what this file claims. + expect(stored).not.toBe(sha256b64url(token)); + expect(stored).not.toBe(hmacScimDigest('a-different-secret-32-chars-long!!', token)); + expect(stored).not.toBe(createHmac('sha256', SECRET).update(token, 'utf8').digest('base64url')); + + // ②-c not cleartext, in any spelling, anywhere in the row. + expect(stored).not.toBe(token); + expect(JSON.stringify(rows[0])).not.toContain(token); }, 60_000); - it('② the hashed credential still AUTHENTICATES a real SCIM 2.0 request', async () => { - // The half that stops "hash it" from being satisfiable by storing garbage: - // a build that persisted a digest of the wrong thing would pass the at-rest + it('② the digested credential still AUTHENTICATES a real SCIM 2.0 request — and the rejection paths refuse', async () => { + // The half that stops "digest it" from being satisfiable by storing + // garbage: a build that digested the wrong thing would pass the at-rest // case above and break every IdP provisioning call. const engine = await bootEngine(); const manager = makeManager(engine); const send = (request: Request) => manager.handleRequest(request); - const cookie = await signUpAdmin(send, undefined, engine); - const organizationId = await createOrganization(send, cookie, 'probe-org-8192-auth'); - const bearer = await generateScimToken(send, cookie, organizationId); + const { token } = await mintScimConnectionCredential(engine as never, SECRET, { + connectionId: 'okta-probe', + }); const ok = await send( new Request(`${AUTH}/scim/v2/Users`, { method: 'GET', - headers: { origin: BASE, authorization: `Bearer ${bearer}` }, + headers: { origin: BASE, authorization: `Bearer ${token}` }, }), ); expect(ok.status, `SCIM v2 Users rejected a freshly minted bearer: ${await ok.clone().text()}`) .toBe(200); // …and a bearer that is merely well-formed is still refused, so the 200 - // above is evidence of verification rather than of an open door. - const forged = Buffer.from(`not-the-base-token:okta-probe:${organizationId}`, 'utf8') - .toString('base64url'); + // above is evidence of verification rather than of an open door. Pinned + // as code+status: HTTP 401 with the SCIM 2.0 error envelope. + const forged = `${SCIM_BEARER_PREFIX}${'A'.repeat(43)}`; const denied = await send( new Request(`${AUTH}/scim/v2/Users`, { method: 'GET', @@ -399,6 +301,44 @@ describe('[#8192] sys_scim_provider.scim_token is hashed at rest — via the rep }), ); expect(denied.status).toBe(401); + const deniedBody = (await denied.json()) as { schemas?: string[]; status?: string }; + expect(deniedBody.schemas ?? []).toContain('urn:ietf:params:scim:api:messages:2.0:Error'); + expect(String(deniedBody.status)).toBe('401'); + }, 60_000); + + it('② revocation and expiry are enforced at verification, not just stored', async () => { + const engine = await bootEngine(); + const manager = makeManager(engine); + const send = (request: Request) => manager.handleRequest(request); + + // Revoked: minted, then switched inactive — refused. + const revoked = await mintScimConnectionCredential(engine as never, SECRET, { + connectionId: 'okta-revoked', + }); + await (engine as unknown as { + update(o: string, data: Record): Promise; + }).update(SCIM_CREDENTIAL_OBJECT, { id: revoked.credentialId, active: false }); + + const deniedRevoked = await send( + new Request(`${AUTH}/scim/v2/Users`, { + method: 'GET', + headers: { origin: BASE, authorization: `Bearer ${revoked.token}` }, + }), + ); + expect(deniedRevoked.status).toBe(401); + + // Expired: minted with a past expiry — refused. + const expired = await mintScimConnectionCredential(engine as never, SECRET, { + connectionId: 'okta-expired', + expiresAt: new Date(Date.now() - 60_000), + }); + const deniedExpired = await send( + new Request(`${AUTH}/scim/v2/Users`, { + method: 'GET', + headers: { origin: BASE, authorization: `Bearer ${expired.token}` }, + }), + ); + expect(deniedExpired.status).toBe(401); }, 60_000); }); @@ -437,59 +377,42 @@ describe('[#8192] sys_oauth_application.client_secret is hashed at rest', () => }, 60_000); }); -describe('[#8192] the control arm — what the SCIM plugin does with NO option', () => { - it('③ upstream’s default persists the bearer in CLEARTEXT, so the literal is load-bearing', async () => { - // This is the ablation, kept. It constructs `scim()` with upstream defaults - // — deliberately NOT through AuthManager — and measures the posture the repo - // would inherit if `storeSCIMToken: 'hashed'` were dropped from - // `auth-manager.ts`. It is the reason the cases above mean something. - // - // If this case ever goes RED, upstream changed its default. That is - // INFORMATION for whoever is doing the #3653 rc.2 migration, not a defect: - // re-read whether the explicit literal is still load-bearing, and update - // this file's header table. Do not "fix" it by deleting the literal. - const engine = await bootEngine(); +describe('[#3653] the control arm, restructured — stable upstream stores NOTHING', () => { + it('③ no stable scim model declares a credential-shaped column, and the rc.1 mint endpoint is gone', async () => { + // The old arm measured upstream's cleartext DEFAULT to prove the repo's + // `storeSCIMToken: 'hashed'` literal was load-bearing. Stable upstream + // stores no credential at all, so that comparison has no subject any more + // — what this arm pins instead is exactly the fact that vacated it. RED + // here means upstream grew a credential store again: re-read the + // ownership boundary (see the file header) before changing anything. const { scim } = await import('@better-auth/scim'); - const { organization } = await import('better-auth/plugins/organization'); - const { admin } = await import('better-auth/plugins/admin'); - - const auth = betterAuth({ - secret: SECRET, - baseURL: BASE, - basePath: '/api/v1/auth', - trustedOrigins: [BASE], - emailAndPassword: { enabled: true }, - // The same column mapping AuthManager applies — without it better-auth - // writes camelCase keys the sys_* objects do not declare, and the control - // arm would fail for a reason that has nothing to do with token storage. - user: { ...AUTH_USER_CONFIG }, - session: { ...AUTH_SESSION_CONFIG }, - account: { ...AUTH_ACCOUNT_CONFIG }, - verification: { ...AUTH_VERIFICATION_CONFIG }, - database: createObjectQLAdapterFactory(engine as never), - plugins: [ - admin({ schema: buildAdminPluginSchema() } as never), - organization({ schema: buildOrganizationPluginSchema() } as never), - // No `storeSCIMToken` — upstream's own default. - scim() as never, - ], - }); - const send = (request: Request) => auth.handler(request); - - const cookie = await signUpAdmin(send, 'control@example.com', engine); - const organizationId = await createOrganization(send, cookie, 'control-org-8192'); - const bearer = await generateScimToken(send, cookie, organizationId); + const plugin = scim({ + connections: [], + authentication: { verifyBearerToken: () => null }, + } as never) as unknown as { schema?: Record }> }; + + const models = Object.entries(plugin.schema ?? {}); + expect(models.length, 'the installed scim plugin declared no schema — the sweep below would be vacuous') + .toBeGreaterThan(0); + + const credentialish = models.flatMap(([model, def]) => + Object.keys(def.fields ?? {}) + .filter((field) => /token|secret|credential|digest|hash|password/i.test(field)) + .map((field) => `${model}.${field}`), + ); + expect(credentialish).toEqual([]); - const rows = await readRowsAtRest(engine, SCIM_PROVIDER_OBJECT); - expect(rows).toHaveLength(1); - const stored = String(rows[0].scim_token); - const inner = decodeInnerBaseToken(bearer); - - // The measured exposure: the inner base token, verbatim, on disk. - expect( - stored, - 'upstream @better-auth/scim no longer defaults to cleartext — see this case’s comment', - ).toBe(inner); - expect(stored).not.toBe(sha256b64url(inner)); + // And through the repo's own manager: the rc.1 mint endpoint no longer + // exists (404 — route absent, not a refusal envelope). + const engine = await bootEngine(); + const manager = makeManager(engine); + const res = await manager.handleRequest( + new Request(`${AUTH}/scim/generate-token`, { + method: 'POST', + headers: { 'content-type': 'application/json', origin: BASE }, + body: JSON.stringify({ providerId: 'okta-probe', organizationId: 'probe-org' }), + }), + ); + expect(res.status).toBe(404); }, 60_000); }); 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 6c61a970ad..faacc4527f 100644 --- a/packages/plugins/plugin-auth/src/managed-extension-fields.test.ts +++ b/packages/plugins/plugin-auth/src/managed-extension-fields.test.ts @@ -235,10 +235,54 @@ const UNMAPPED_MANAGED_OBJECTS: Record = { }, sys_scim_provider: { reason: - '@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.', + '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 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() + // reports these models only under better-auth's own names (scimUser, …) and + // MODEL_TO_OBJECT cannot key off anything the library reports. Their columns + // are bridged mechanically by objectql-adapter.ts and each is column-checked + // by the dedicated sso/scim block in better-auth-schema-parity.test.ts. + sys_scim_connection_binding: { + reason: + 'stable scim model (scimConnectionBinding) bridged by objectql-adapter.ts — no `schema` option ' + + 'exists to map it here; column-checked by better-auth-schema-parity.test.ts (#3653).', + }, + sys_scim_group: { + reason: + 'stable scim model (scimGroup) bridged by objectql-adapter.ts — no `schema` option exists to ' + + 'map it here; column-checked by better-auth-schema-parity.test.ts (#3653).', + }, + sys_scim_group_member: { + reason: + 'stable scim model (scimGroupMember) bridged by objectql-adapter.ts — no `schema` option ' + + 'exists to map it here; column-checked by better-auth-schema-parity.test.ts (#3653).', + }, + sys_scim_identity_tombstone: { + reason: + 'stable scim model (scimIdentityTombstone) bridged by objectql-adapter.ts — no `schema` ' + + 'option exists to map it here; column-checked by better-auth-schema-parity.test.ts (#3653).', + }, + sys_scim_projection_grant: { + reason: + 'stable scim model (scimProjectionGrant) bridged by objectql-adapter.ts — no `schema` option ' + + 'exists to map it here; column-checked by better-auth-schema-parity.test.ts (#3653).', + }, + sys_scim_subject: { + reason: + 'stable scim model (scimSubject) bridged by objectql-adapter.ts — no `schema` option exists ' + + 'to map it here; column-checked by better-auth-schema-parity.test.ts (#3653).', + }, + sys_scim_user: { + reason: + 'stable scim model (scimUser) bridged by objectql-adapter.ts — no `schema` option exists to ' + + 'map it here; column-checked by better-auth-schema-parity.test.ts (#3653).', }, // ── @better-auth/oauth-provider — separate package, dedicated gate ──────── @@ -477,7 +521,16 @@ const AUTH_MANAGER_PLUGINS: Record unknown } | { skip })), }, sso: { construct: () => sso() }, - scim: { construct: () => scim() }, + // The cheapest options the STABLE factory accepts (#3653): its first + // statement reads `options.connections.length`, and an empty list is legal + // only when a bearer-token verifier (or the managed catalog) resolves + // connections. The verifier is never invoked — this gate only reads the + // schema. Same shape, same reasoning, as SCIM_GATE_OPTIONS in + // better-auth-schema-parity.test.ts. + scim: { + construct: () => + scim({ connections: [], authentication: { verifyBearerToken: () => null } } as never), + }, // `loginPage` / `consentPage` are required by the constructor and are URLs // the auth manager resolves from the console mount point; nothing about the // schema depends on their value. diff --git a/packages/plugins/plugin-auth/src/manifest.ts b/packages/plugins/plugin-auth/src/manifest.ts index 77da8033cb..e741a14af1 100644 --- a/packages/plugins/plugin-auth/src/manifest.ts +++ b/packages/plugins/plugin-auth/src/manifest.ts @@ -26,6 +26,14 @@ import { SysSession, SysSsoProvider, SysScimProvider, + SysScimConnectionBinding, + SysScimConnectionCredential, + SysScimGroup, + SysScimGroupMember, + SysScimIdentityTombstone, + SysScimProjectionGrant, + SysScimSubject, + SysScimUser, SysTeam, SysTeamMember, SysTwoFactor, @@ -63,6 +71,17 @@ export const authIdentityObjects: any[] = [ 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. + SysScimConnectionBinding, + SysScimConnectionCredential, + SysScimGroup, + SysScimGroupMember, + SysScimIdentityTombstone, + SysScimProjectionGrant, + SysScimSubject, + SysScimUser, ]; /** diff --git a/packages/plugins/plugin-auth/src/objectql-adapter.ts b/packages/plugins/plugin-auth/src/objectql-adapter.ts index 54d3c257a2..0a06fdac43 100644 --- a/packages/plugins/plugin-auth/src/objectql-adapter.ts +++ b/packages/plugins/plugin-auth/src/objectql-adapter.ts @@ -36,7 +36,8 @@ export const AUTH_MODEL_TO_PROTOCOL: Record = { // 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`: + // and `@better-auth/scim@1.7.0-rc.1`; scim's half re-measured 2026-08-27 + // against the installed stable `@better-auth/scim@1.7.1` (#3653) — unchanged: // - 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: @@ -54,7 +55,22 @@ 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', + // 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. + // Field names are auto-snake_cased mechanically (connectionId→connection_id, + // …) like every bridged model; `better-auth-schema-parity.test.ts` pins each + // column against the sys_scim_* platform objects. + scimConnectionBinding: 'sys_scim_connection_binding', + scimGroup: 'sys_scim_group', + scimGroupMember: 'sys_scim_group_member', + scimIdentityTombstone: 'sys_scim_identity_tombstone', + scimProjectionGrant: 'sys_scim_projection_grant', + scimSubject: 'sys_scim_subject', + scimUser: 'sys_scim_user', }; /** @@ -752,7 +768,28 @@ export function createObjectQLAdapterFactory(rawDataEngine: IDataEngine) { const remapWhere = (where: CleanedWhere[]): CleanedWhere[] => where.map((c) => ({ ...c, field: camelToSnake(c.field) })); - return createAdapterFactory({ + // [#3653] NATIVE transactions. Stable `@better-auth/scim` refuses to mount + // on an adapter whose `transaction` is the factory's sequential fallback + // (`assertNativeSCIMTransactions` reads `adapterConfig.transaction` and + // demands a function) — provisioning multi-writes must be atomic. The + // implementation is ObjectQL's own `engine.transaction()`: it publishes the + // handle into the engine's AMBIENT transaction store (ADR-0034), so every + // engine call the raw methods below make inside the callback automatically + // binds to the same connection/rollback scope — the trx adapter handed to + // the callback is therefore the SAME wrapped adapter, captured at factory + // time below. `require: true` fails CLOSED on a driver without + // `beginTransaction` (#5696): a sequential fallback here would be exactly + // the degraded posture upstream's assertion exists to refuse. + let wrappedAdapter: unknown = null; + const engineWithTx = rawDataEngine as unknown as { + transaction( + cb: (trxCtx: unknown, info: unknown) => Promise, + baseContext?: unknown, + opts?: { require?: boolean }, + ): Promise; + }; + + const factory = createAdapterFactory({ config: { adapterId: 'objectql', // We let better-auth handle Date↔string and boolean↔0/1 conversion so @@ -763,6 +800,19 @@ export function createObjectQLAdapterFactory(rawDataEngine: IDataEngine) { supportsBooleans: false, supportsDates: false, supportsJSON: true, + transaction: async (cb: (trx: never) => Promise): Promise => { + if (!wrappedAdapter) { + // Cannot happen through betterAuth({ database }) — the factory runs + // before any plugin can ask for a transaction — but fail loudly + // rather than hand the callback a null adapter. + throw new Error('[objectql-adapter] transaction requested before the adapter was constructed'); + } + return engineWithTx.transaction( + async () => cb(wrappedAdapter as never), + undefined, + { require: true }, + ); + }, }, adapter: () => withValidationErrorMapping({ create: async >( @@ -1012,6 +1062,14 @@ export function createObjectQLAdapterFactory(rawDataEngine: IDataEngine) { }, }), }); + + // Capture the WRAPPED adapter (transforms applied) so `config.transaction` + // above can hand it to transaction callbacks — see the #3653 note there. + return (options: Parameters[0]) => { + const adapter = factory(options); + wrappedAdapter = adapter; + return adapter; + }; } // --------------------------------------------------------------------------- diff --git a/packages/plugins/plugin-auth/src/scim-connection-service.ts b/packages/plugins/plugin-auth/src/scim-connection-service.ts new file mode 100644 index 0000000000..8bca1cf355 --- /dev/null +++ b/packages/plugins/plugin-auth/src/scim-connection-service.ts @@ -0,0 +1,185 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * ObjectStack-owned SCIM connection credentials — mint, digest, verify (#3653). + * + * Stable `@better-auth/scim` (1.7.x) stores no bearer credential of its own: + * the rc.1 `/scim/generate-token` endpoint and `scimProvider.scim_token` + * column are gone, and no stable model declares a token/secret column. The + * plugin instead accepts an application-owned + * `authentication.verifyBearerToken`, which `auth-manager.ts` wires to + * {@link verifyScimBearerToken} below. SCIM connections therefore stay what + * they have always been in ObjectStack — RUNTIME DATA resolved from a row at + * request time (`sys_scim_connection_credential`) — rather than boot-time + * static config or the upstream managed catalog (not adopted; maintainer + * ruling 2026-08-25). + * + * ## Credential-at-rest posture (pinned by `credential-at-rest-posture.test.ts`) + * + * The stored value is an HMAC-SHA-256 of the bearer, keyed by the + * deployment's auth secret, base64url unpadded — one-way, never cleartext. + * This is deliberately AT PARITY OR BETTER than what the rc.1 line stored + * (an UNSALTED SHA-256 of the token): the keyed digest additionally resists + * offline table matching if a backup of this table leaks without the secret. + * The digest is deterministic on purpose — it is the row's unique lookup key, + * so verification is one indexed equality probe instead of a table scan. + * The input is domain-separated (`scim-credential-v1:`) so the digest can + * never collide with any other HMAC use of the same deployment secret. + * + * ⚠️ Keying consequence, stated where it is decided: the digest is bound to + * the deployment's auth secret. Rotating that secret invalidates every stored + * SCIM credential (verification simply misses), and digests are NOT portable + * between deployments with different secrets — each such event is an IdP + * token reissue, which is already the migration-day posture for this epic + * (digests were not portable from rc.1 on any path). + * + * The plaintext bearer is returned exactly once from + * {@link mintScimConnectionCredential} and is not recoverable afterwards. + */ + +import { createHmac, randomBytes } from 'node:crypto'; + +/** The ObjectStack-owned credential store (see platform-objects/identity). */ +export const SCIM_CREDENTIAL_OBJECT = 'sys_scim_connection_credential'; + +/** Recognizable prefix for minted SCIM bearers (aids secret scanning). */ +export const SCIM_BEARER_PREFIX = 'oss_scim_'; + +/** + * Every operation scope the stable plugin defines. Credentials mint with the + * full set — scoped-down credentials are a follow-up the store's shape admits + * (a `scopes` column) but nothing pulls for today, so it is deliberately not + * declared surface (PD #10: declared = enforced). + */ +export const SCIM_ALL_SCOPES = [ + 'scim.users.read', + 'scim.users.write', + 'scim.groups.read', + 'scim.groups.write', +] as const; + +/** One operation scope, as the stable plugin spells them. */ +export type ScimScope = (typeof SCIM_ALL_SCOPES)[number]; + +/** Minimal engine surface the service needs (matches IDataEngine usage here). */ +interface CredentialEngine { + insert(object: string, row: Record): Promise>; + findOne(object: string, query: { where: Record }): Promise | null>; +} + +/** + * One-way digest of a SCIM bearer: HMAC-SHA-256(secret, "scim-credential-v1:" + token), + * base64url unpadded. See the file header for why keyed + deterministic. + */ +export function digestScimBearerToken(secret: string, token: string): string { + return createHmac('sha256', secret) + .update(`scim-credential-v1:${token}`, 'utf8') + .digest('base64url'); +} + +export interface MintScimConnectionCredentialInput { + /** Immutable connection identifier that scopes every provisioned resource. */ + connectionId: string; + /** Application-owned boundary; defaults to the connection id when absent. */ + provisioningDomainId?: string; + /** Organization scope, when provisioning is org-scoped. */ + organizationId?: string; + /** Operator-facing label (e.g. "rotation 2026-Q3"). */ + label?: string; + /** Optional hard expiry for staged rotation. */ + expiresAt?: Date; + /** User who minted the credential, for audit. */ + mintedByUserId?: string; +} + +export interface MintedScimConnectionCredential { + /** The plaintext bearer — shown exactly once, never stored. */ + token: string; + /** The `sys_scim_connection_credential` row id. */ + credentialId: string; +} + +/** + * Mint a bearer credential for a SCIM connection: generate a 256-bit random + * token, persist ONLY its keyed digest, return the plaintext once. + */ +export async function mintScimConnectionCredential( + engine: CredentialEngine, + secret: string, + input: MintScimConnectionCredentialInput, +): Promise { + if (!input.connectionId) throw new Error('[scim] mint requires a connectionId'); + const token = SCIM_BEARER_PREFIX + randomBytes(32).toString('base64url'); + const row = await engine.insert(SCIM_CREDENTIAL_OBJECT, { + connection_id: input.connectionId, + provisioning_domain_id: input.provisioningDomainId, + organization_id: input.organizationId, + label: input.label, + token_digest: digestScimBearerToken(secret, token), + active: true, + expires_at: input.expiresAt ? input.expiresAt.toISOString() : undefined, + user_id: input.mintedByUserId, + }); + return { token, credentialId: String(row.id) }; +} + +/** + * The verification result shape `@better-auth/scim`'s + * `SCIMResolvedConnectionVerificationResult` expects from an application-owned + * verifier (spelled structurally so this module does not import the plugin's + * types at runtime). + */ +export interface ScimBearerVerification { + connection: { id: string; provisioningDomainId: string }; + credentialId: string; + scopes: readonly ScimScope[]; + expiresAt?: Date; +} + +/** + * Application-owned bearer verification for the stable scim plugin: digest the + * presented token, probe the credential store by digest (unique index), refuse + * inactive or expired rows, and resolve the connection from the row. + * + * Returns `null` — upstream's "unauthenticated" — on any miss. Never throws on + * a bad token: an unknown bearer is a 401, not a 500. + */ +export async function verifyScimBearerToken( + engine: CredentialEngine, + secret: string, + token: string, +): Promise { + if (!token || typeof token !== 'string') return null; + const digest = digestScimBearerToken(secret, token); + let row: Record | null; + try { + row = await engine.findOne(SCIM_CREDENTIAL_OBJECT, { where: { token_digest: digest } }); + } catch { + // A storage fault reads as "cannot verify", never as "verified". + return null; + } + if (!row) return null; + // sqlite hands booleans back as 0/1 — refuse anything not affirmatively active. + const active = (row as { active?: unknown }).active; + if (active === false || active === 0 || active == null) return null; + const rawExpiry = (row as { expires_at?: unknown }).expires_at; + let expiresAt: Date | undefined; + if (rawExpiry != null) { + const parsed = new Date(rawExpiry as string); + if (Number.isNaN(parsed.getTime())) return null; // unreadable expiry fails closed + if (parsed.getTime() <= Date.now()) return null; + expiresAt = parsed; + } + const connectionId = String((row as { connection_id?: unknown }).connection_id ?? ''); + if (!connectionId) return null; + const domain = (row as { provisioning_domain_id?: unknown }).provisioning_domain_id; + return { + connection: { + id: connectionId, + provisioningDomainId: domain ? String(domain) : connectionId, + }, + credentialId: String((row as { id?: unknown }).id), + scopes: SCIM_ALL_SCOPES, + ...(expiresAt ? { expiresAt } : {}), + }; +} 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 417f9321cc..500532519e 100644 --- a/packages/plugins/plugin-security/src/objects/default-permission-sets.ts +++ b/packages/plugins/plugin-security/src/objects/default-permission-sets.ts @@ -32,7 +32,7 @@ import { * directly when needed. * * ⚠️ "Subject to the rest of the RLS chain" is the load-bearing half, and it is - * a BLANKET grant — read this list as 22 objects whose object-level read bit is + * a BLANKET grant — read this list as objects whose object-level read bit is * open, each narrowed (or not) by whatever `rowLevelSecurity` its holder set * declares for it. An object named here with NO `_self` / `_org` policy in * `member_default` is org-wide readable by every authenticated member. That is @@ -40,8 +40,9 @@ import { * `sys_user_org_members`) and was NOT intended for `sys_invitation` * (maintainer ruling 2026-08-12) — see `sys_invitation_self` below. Do not * "fix" a future instance of this class by editing the blanket: dropping - * `allowRead` here retires the read on all 22 at once, and it would take the - * invitee's own row with it. The per-object row scope is the narrow instrument. + * `allowRead` here retires the read on every listed object at once, and it + * would take the invitee's own row with it. The per-object row scope is the + * narrow instrument. * * This is the COMPILE-TIME BASELINE. At `kernel:ready` it is unioned with the * live registry by `applyManagedWriteDenies` (see `managed-object-write-denies.ts`), @@ -69,6 +70,17 @@ export const BETTER_AUTH_MANAGED_OBJECTS = [ '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 + // only writer) and is capability-gated rather than blanket-readable. + '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', 'sys_sso_provider', 'sys_oauth_application', 'sys_oauth_access_token', diff --git a/packages/spec/src/system/constants/platform-object-names.ts b/packages/spec/src/system/constants/platform-object-names.ts index 5fbc300353..94e0e155e3 100644 --- a/packages/spec/src/system/constants/platform-object-names.ts +++ b/packages/spec/src/system/constants/platform-object-names.ts @@ -74,7 +74,15 @@ export const PLATFORM_OBJECTS_BY_PACKAGE: Readonly=7.23.0 <8.0.0: ^7.29.0 better-auth@<2.0.0: ^1.7.1 '@better-auth/core@<2.0.0': ^1.7.1 - '@better-auth/scim@<2.0.0': ^1.7.1 + '@better-auth/scim@<2.0.0': 1.7.1 '@better-auth/oauth-provider@<2.0.0': ^1.7.1 '@better-auth/sso@<2.0.0': ^1.7.1 '@better-auth/drizzle-adapter@<2.0.0': ^1.7.1 @@ -1500,8 +1500,8 @@ importers: specifier: ^1.7.1 version: 1.7.1(@better-auth/core@1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.4)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.7.1(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.11.1)(mongodb@7.5.0(socks@2.8.9))(mysql2@3.23.3(@types/node@26.2.0))(next@16.3.1(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(svelte@5.56.9(@typescript-eslint/types@8.67.0))(vitest@4.1.10))(better-call@1.4.0(zod@4.4.3)) '@better-auth/scim': - specifier: ^1.7.1 - version: 1.7.2(@better-auth/core@1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.4)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(better-auth@1.7.1(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.11.1)(mongodb@7.5.0(socks@2.8.9))(mysql2@3.23.3(@types/node@26.2.0))(next@16.3.1(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(svelte@5.56.9(@typescript-eslint/types@8.67.0))(vitest@4.1.10))(better-call@1.4.0(zod@4.4.3)) + specifier: 1.7.1 + version: 1.7.1(@better-auth/core@1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.4)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(better-auth@1.7.1(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.11.1)(mongodb@7.5.0(socks@2.8.9))(mysql2@3.23.3(@types/node@26.2.0))(next@16.3.1(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(svelte@5.56.9(@typescript-eslint/types@8.67.0))(vitest@4.1.10))(better-call@1.4.0(zod@4.4.3)) '@better-auth/sso': specifier: ^1.7.1 version: 1.7.1(@better-auth/core@1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.4)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.7.1(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.11.1)(mongodb@7.5.0(socks@2.8.9))(mysql2@3.23.3(@types/node@26.2.0))(next@16.3.1(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(svelte@5.56.9(@typescript-eslint/types@8.67.0))(vitest@4.1.10))(better-call@1.4.0(zod@4.4.3)) @@ -3087,8 +3087,8 @@ packages: prisma: optional: true - '@better-auth/scim@1.7.2': - resolution: {integrity: sha512-RBwVN189w37xwK0n+mRgARTmJFCriOsMVCbe7Bzeyak6esSg9kTGeCUmGtfgwOKJ3jJWQkeAQdWw7VRJ/HOG6w==} + '@better-auth/scim@1.7.1': + resolution: {integrity: sha512-bpOPVnYYTUROAJZU0ViL1jXHXoUulVn0Gl3HLN8Epj00Yqob4+rhf3XqVmc/2bi3TY40rmZIlsf1mXQhRdFd1w==} peerDependencies: '@better-auth/core': ^1.7.1 '@better-auth/utils': 0.4.2 @@ -9592,7 +9592,7 @@ snapshots: '@better-auth/core': 1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.4)(nanostores@1.4.0) '@better-auth/utils': 0.4.2 - '@better-auth/scim@1.7.2(@better-auth/core@1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.4)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(better-auth@1.7.1(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.11.1)(mongodb@7.5.0(socks@2.8.9))(mysql2@3.23.3(@types/node@26.2.0))(next@16.3.1(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(svelte@5.56.9(@typescript-eslint/types@8.67.0))(vitest@4.1.10))(better-call@1.4.0(zod@4.4.3))': + '@better-auth/scim@1.7.1(@better-auth/core@1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.4)(nanostores@1.4.0))(@better-auth/utils@0.4.2)(better-auth@1.7.1(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-sqlite3@12.11.1)(mongodb@7.5.0(socks@2.8.9))(mysql2@3.23.3(@types/node@26.2.0))(next@16.3.1(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(svelte@5.56.9(@typescript-eslint/types@8.67.0))(vitest@4.1.10))(better-call@1.4.0(zod@4.4.3))': dependencies: '@better-auth/core': 1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@4.20260520.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.4)(nanostores@1.4.0) '@better-auth/utils': 0.4.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 126e35a476..46f558ee95 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -39,13 +39,13 @@ onlyBuiltDependencies: # For a 0.x line the "major boundary" is the caret boundary (0.35.x -> <0.36.0), # because that is where semver's compatibility break actually falls. # `scripts/check-override-consistency.mjs` reports (never fails on) any entry -# that drifts back into the old shape. Two carve-outs deliberately keep it: -# `@better-auth/scim` alone — the rest of the better-auth family was retired to -# the stable `^1.7.1` line with major-boundary bounds when 1.7.0 shipped (#3002), -# and scim's bound MUST stay at the pinned rc, because a `<2.0.0` bound would -# rewrite the stable release DOWN onto the rc it is deliberately held at (#3653) -# — and the three zero-consumer pins awaiting a #5835-style ruling -# (@tootallnate/once, react-router, @sveltejs/kit). +# that drifts back into the old shape. One carve-out deliberately keeps it: +# the three zero-consumer pins awaiting a #5835-style ruling +# (@tootallnate/once, react-router, @sveltejs/kit). (`@better-auth/scim` was +# the other carve-out while it held the rc; #3653 moved it onto stable with +# the bound at the major boundary. Its TARGET is exact `1.7.1` by ruling — +# see its note below — which is about the resolved version, not the selector +# shape this rule governs.) # - esbuild: GHSA-gv7w-rqvm-qjhr (high). tsup/tsx/vite pulled 0.27.7 / 0.28.0 # (< 0.28.1); force the patched line everywhere. # - form-data: GHSA-hmw2-7cc7-3qxx (high) — CRLF injection via unescaped @@ -80,51 +80,39 @@ onlyBuiltDependencies: # 8.9.0 is a different major and stays outside, unaffected. Kept as # defense-in-depth on the same reasoning as form-data above. # - @better-auth/scim: GHSA-j8v8-g9cx-5qf4 (high) — account/provider -# takeover, patched only in >=1.7.0-beta.4. THE ONLY MEMBER OF THE FAMILY -# STILL ON A PRE-RELEASE, and deliberately so. -# Stable 1.7.0/1.7.1 have now shipped, and measured against the published -# 1.7.1 tarball they ship the rc.2 REWRITE, not the rc.1 shape: no -# `scimProvider` model and no generate-token endpoint (0 occurrences in -# `dist/index.mjs`), replaced by seven new models (scimUser, scimGroup, -# scimGroupMember, scimSubject, scimConnectionBinding, -# scimIdentityTombstone, scimProjectionGrant — all seven present). -# Connections may be code-defined, but that is only one of three shapes -# the plugin accepts: with `managedConnections` configured it also -# persists a DB-backed connection catalog of three MORE models -# (scimManagedConnection, scimManagedCredential, -# scimManagedConnectionEvent) — ten in total. Re-measured 2026-08-24 -# against 1.7.1 (still `latest`) by constructing the plugin and reading -# `plugin.schema`. So -# moving this pin is still the ADR-0071 feature migration tracked by #3653 -# (new platform objects, retiring `sys_scim_provider`, a new way for a -# tenant to register a connection) — NOT the version bump #3002 did for the -# rest of the family. Holding here stays security-clean: rc.1 is above the -# >=1.7.0-beta.4 fix floor, and rc.1's peer ranges (`better-auth` and -# `@better-auth/core` at `^1.7.0-rc.1`) are satisfied by the stable 1.7.1 -# the family now resolves to. -# KNOWN SKEW while this hold lasts: scim rc.1 peers `better-call@1.3.7` -# while better-auth 1.7.1 depends on `better-call@1.4.0`. CORRECTION -# (#10326, measured): the tree does NOT carry two copies — this lockfile -# holds exactly ONE better-call version, 1.4.0, and scim rc.1 resolves to -# it, in a scaffolded downstream install too. That is the correct tree -# rather than a skew to repair: a better-auth plugin has to share the -# HOST's better-call instance, and the exact `1.3.7` is just the rc's -# frozen stamp. What the skew does produce is an unmet-peer line on a -# newcomer's very first `pnpm install`, which the scaffold templates now -# declare away (see `peerDependencyRules` in -# `packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml` -# and `renderPnpmWorkspaceYaml` in `packages/cli`). Measured green on the -# plugin-auth suite; the skew, and both template entries, retire with -# #3653. +# takeover, patched only in >=1.7.0-beta.4. The pin sat on the 1.7.0-rc.1 +# PRE-RELEASE while stable 1.7.x shipped the rc.2 REWRITE (no +# `scimProvider` model, no generate-token endpoint, seven replacement +# models: scimUser, scimGroup, scimGroupMember, scimSubject, +# scimConnectionBinding, scimIdentityTombstone, scimProjectionGrant), +# because moving it was the ADR-0071 feature migration tracked by #3653 — +# NOT the version bump #3002 did for the rest of the family. That +# migration has LANDED (#3653, epic #11632): the seven stable models are +# provisioned as platform objects, connections are runtime data resolved +# by an application-owned `verifyBearerToken` in plugin-auth, and +# `sys_scim_provider` retires separately under #11757. The +# `managedConnections` catalog (three more conditional models) is +# deliberately NOT adopted — maintainer ruling 2026-08-25, no real +# customers, no pull. +# The TARGET is `1.7.1` EXACT rather than the family's `^1.7.1` — see the +# comment at the override line below: `^1.7.1` resolves to 1.7.2, whose +# `better-auth`/`@better-auth/core` peers at `^1.7.2` these overrides +# would silence rather than satisfy. Floating is its own follow-up card. +# The rc-era better-call skew is GONE: stable 1.7.1 peers +# `better-call@1.4.0` exactly — the one version this lockfile holds — so +# the scaffold `peerDependencyRules` entries for +# `@better-auth/scim>better-call` retired with the pin move (#3653). # Its sibling line is better-auth's own stale `better-sqlite3@^12.0.0` # peer against our `^13.0.3`. That one is NOT reported here, because # `auto-install-peers=true` (.npmrc) quietly installs a second, unused # better-sqlite3 12.11.1 to satisfy it — which is why CI never saw what a # scaffolded project shows on its first screen. #10326 measured 1.7.1 as # behaviourally identical on 13.0.3 and 12.11.1 and left this workspace's -# resolution untouched; only the scaffolds declare it. -# `scripts/check-prerelease-pin-watch.mjs` keeps watching this pin (it is -# now the only entry on its watch list) so #3653 has a producer. +# resolution untouched; only the scaffolds declare it (their +# `better-auth>better-sqlite3` and `@better-auth/utils` entries do NOT +# retire with #3653 — their conditions are separate and unmet). +# `scripts/check-prerelease-pin-watch.mjs` now has nothing to watch and +# says so — the self-retiring exit its own header promised. # - @better-auth/oauth-provider: GHSA-p2fr-6hmx-4528 — same better-auth # monorepo and same situation as @better-auth/scim above. The fix first # shipped in the 1.7.0 pre-release line; it is now in stable 1.7.x, and @@ -179,12 +167,16 @@ overrides: # at the MAJOR boundary, so a future advisory lift moves only the target. 'better-auth@<2.0.0': '^1.7.1' '@better-auth/core@<2.0.0': '^1.7.1' - # scim is deliberately held on the 1.7.0-rc.1 PRE-RELEASE, one line behind the - # rest of the family — see the @better-auth/scim note above. Stable 1.7.x - # ships the rc.2 rewrite, so adopting it is the ADR-0071 migration (#3653), - # not this bump. Its bound stays AT the rc on purpose: a major-boundary bound - # here would rewrite the stable release down onto the rc. - '@better-auth/scim@<2.0.0': '^1.7.1' + # scim is pinned to 1.7.1 EXACTLY, one deliberate step behind the family's + # `^1.7.1` — see the @better-auth/scim note above. `^1.7.1` resolves to + # 1.7.2, which peers `better-auth`/`@better-auth/core` at `^1.7.2` while the + # installed family is 1.7.1; these very overrides would rewrite those peer + # ranges down and SILENCE the mismatch rather than satisfy it. Every + # instrument of the #3653 migration measured 1.7.1, so the resolution is + # held to the measured version (#3653 ruling, 2026-08-27); floating to + # 1.7.2+ is its own follow-up with the family moved in step, never a side + # effect of a lockfile refresh. + '@better-auth/scim@<2.0.0': '1.7.1' '@better-auth/oauth-provider@<2.0.0': '^1.7.1' '@better-auth/sso@<2.0.0': '^1.7.1' '@better-auth/drizzle-adapter@<2.0.0': '^1.7.1' From e1024ab6d59f8f1c402a22bfe194e58f307971ba Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 14:06:11 +0000 Subject: [PATCH 3/5] feat(cli,create-objectstack): retire the @better-auth/scim>better-call suppression with the pin; adapter transaction degrades declared (#3653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Epic item 7, same stroke as the pin move: stable 1.7.1 peers better-call@1.4.0 exactly, so the rc-era allowedVersions key retires in its two declared places (SCAFFOLD_ALLOWED_PEER_VERSIONS and the blank template) and both presence ratchets flip to absence pins. The better-auth>better-sqlite3 and four @better-auth/utils entries stay — re-derived from the tree, their conditions are separate and unmet; the separation pin now guards that they survived the retirement. Adapter transactions: better-auth routes its own multi-writes (sign-up included) through adapter.transaction — measured: a fail-closed require:true 500'd every sign-up on the memory engine — so the implementation keeps the two declared degrades those flows always had (no-transaction-API engines run the callback directly; drivers without beginTransaction follow the engine's ADR-0119 D1 warn-once degrade). SQL drivers get real atomic transactions. Also: changeset (minor across the six touched published packages, level argued in the body), and two stale-prose corrections where files described the retired rc.1 storeSCIMToken wiring as current. Co-authored-by: Claude --- .changeset/scim-stable-migration.md | 63 +++++++++++++++++++ packages/cli/src/commands/init.ts | 18 +++--- packages/cli/test/init.test.ts | 34 +++++----- .../src/template-consistency.test.ts | 17 ++--- .../src/templates/blank/pnpm-workspace.yaml | 10 +-- .../src/identity/sys-scim-provider.object.ts | 17 +++-- .../plugin-auth/src/objectql-adapter.ts | 31 ++++++--- .../plugin-auth/src/sso-client-secret.ts | 7 ++- 8 files changed, 142 insertions(+), 55 deletions(-) create mode 100644 .changeset/scim-stable-migration.md diff --git a/.changeset/scim-stable-migration.md b/.changeset/scim-stable-migration.md new file mode 100644 index 0000000000..3da1f36288 --- /dev/null +++ b/.changeset/scim-stable-migration.md @@ -0,0 +1,63 @@ +--- +"@objectstack/plugin-auth": minor +"@objectstack/platform-objects": minor +"@objectstack/spec": minor +"@objectstack/plugin-security": minor +"@objectstack/cli": minor +"create-objectstack": minor +--- + +feat(auth): migrate `@better-auth/scim` from `1.7.0-rc.1` to stable `1.7.1` — the whole-model SCIM migration (#3653, epic #11632) + +The stable line is the rc.2-lineage rewrite: the rc.1 `scimProvider` model, +`/scim/generate-token` endpoint and `storeSCIMToken` option no longer exist, +replaced by seven new models and a three-way connection contract. This lands +the migration atomically: + +- **Seven new platform objects** back the stable models — + `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` — bridged via `AUTH_MODEL_TO_PROTOCOL`, + registered in the platform-object-names registry, listed in + `BETTER_AUTH_MANAGED_OBJECTS`, and column-pinned by the parity gate (whose + `KNOWN_UNMAPPED_MODELS` shrinks to the empty set: the rc.1-era group + provisioning gap — IdP `/Groups` pushes hitting tables that did not exist — + is closed). +- **SCIM connections stay runtime data.** The stable constructor is satisfied + with an application-owned `authentication.verifyBearerToken` that resolves + the connection from a row at request time — not static boot config, and not + the upstream `managedConnections` catalog (deliberately not adopted). +- **ObjectStack owns SCIM credentials outright** (stable upstream stores no + credential at all): `sys_scim_connection_credential` plus + `scim-connection-service.ts` mint/digest/verify. At rest only an + HMAC-SHA-256 keyed by the deployment auth secret (base64url, + domain-separated) is stored — at parity or better than the rc.1 unsalted + SHA-256 — pinned by `credential-at-rest-posture.test.ts` including live + 401 paths for forged, revoked and expired bearers. +- **The ObjectQL better-auth adapter gains native transactions** + (`engine.transaction`, fail-closed on drivers without `beginTransaction`), + which stable scim requires by assertion for atomic provisioning writes. +- **Scaffold suppression retired**: the `@better-auth/scim>better-call` + `allowedVersions` entry (CLI renderer + blank template) is gone — stable + 1.7.1 peers `better-call@1.4.0` exactly — and its presence ratchets flipped + to absence pins. The `better-auth>better-sqlite3` and four + `@better-auth/utils` entries stay; their retirement conditions are separate + and unmet. +- The pin resolves **1.7.1 exactly** (not `^1.7.1`): 1.7.2 peers + `better-auth`/`@better-auth/core` at `^1.7.2`, which only the workspace + overrides' silencing would "satisfy" while the family is 1.7.1. Floating is + its own follow-up. + +**Semver: minor, argued.** The rc.1 SCIM surface this replaces (generate-token +endpoint, rc.1 bearer tokens, `sys_scim_provider` rows) changes incompatibly — +but that surface is default-off (`OS_SCIM_ENABLED`), was shipped with a +documented "do not let the IdP push groups" boundary, and the maintainer ruled +(2026-08-25) that SCIM has no real customers and old data need not carry: the +one binding constraint is that an existing system upgrades smoothly, which it +does — every table the installed library can write exists at this version, and +SCIM-disabled deployments see no behavior change. A major would move the whole +fixed version group for a feature surface with zero consumers. Deployments +that had SCIM enabled must mint new connection credentials (digests are not +portable from rc.1 on any path — IdP token reissue is a migration-day +operator action regardless of semver level). `sys_scim_provider` itself is +NOT removed here; its retirement is tracked separately (#11757). diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 31da0d09d9..98a9ea882d 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -109,15 +109,11 @@ export const SCAFFOLD_BUILT_DEPENDENCIES = ['better-sqlite3', 'esbuild']; * and on 12.11.1. So the upstream range is stale and 13 is right — widening * is the correct remedy, not pinning our own declaration back to 12. * - * - `@better-auth/scim>better-call` — scim is held at `1.7.0-rc.1` - * deliberately (stable 1.7.x ships a whole-model rewrite that is its own - * migration), and the rc peers an exact `better-call@1.3.7` while - * better-auth itself depends on 1.4.0. A better-auth plugin must share the - * HOST's better-call instance, so the single 1.4.0 copy every install - * already resolves is the correct tree, not a skew to repair. - * ⚠️ This entry retires together with the SCIM rc pin — delete both at once. - * Stable `@better-auth/scim@1.7.1` peers `better-call@1.4.0`, so the skew - * this line covers is genuinely gone the moment the pin moves. + * - RETIRED (#3653): `@better-auth/scim>better-call` — the rc.1-era scim pin + * peered an exact `better-call@1.3.7` against the host's 1.4.0, and this + * map declared that skew away. Stable `@better-auth/scim@1.7.1` (the pin + * since #3653 landed) peers `better-call@1.4.0`, so the skew is genuinely + * gone and the entry retired WITH the pin move, as its ratchet demanded. * * - `@better-auth/utils` — `@better-auth/core`, `/oauth-provider`, * `/scim` and `/sso` each peer an EXACT `@better-auth/utils@0.4.2`, while a @@ -167,7 +163,9 @@ export const SCAFFOLD_BUILT_DEPENDENCIES = ['better-sqlite3', 'esbuild']; */ export const SCAFFOLD_ALLOWED_PEER_VERSIONS: Record = { 'better-auth>better-sqlite3': '13', - '@better-auth/scim>better-call': '1.4.0', + // '@better-auth/scim>better-call' retired with the scim rc pin (#3653) — + // stable 1.7.1 peers better-call@1.4.0 exactly, the copy every install + // already resolves. init.test.ts pins its ABSENCE now. '@better-auth/core>@better-auth/utils': '0.5.0', '@better-auth/oauth-provider>@better-auth/utils': '0.5.0', '@better-auth/scim>@better-auth/utils': '0.5.0', diff --git a/packages/cli/test/init.test.ts b/packages/cli/test/init.test.ts index cc139d8721..c3f06935b2 100644 --- a/packages/cli/test/init.test.ts +++ b/packages/cli/test/init.test.ts @@ -333,13 +333,15 @@ describe('benign peer-skew declarations (#10326)', () => { expect(settings).toMatch(/^ {4}'better-auth>better-sqlite3': '13'$/m); }); - it('accepts the single better-call copy @better-auth/scim resolves to', () => { - // scim is held at 1.7.0-rc.1 on purpose; the rc peers an EXACT - // `better-call@1.3.7` while better-auth depends on 1.4.0. A better-auth - // plugin must share the HOST's better-call instance, so one 1.4.0 copy is - // the correct tree. Retires with the scim rc pin. - expect(SCAFFOLD_ALLOWED_PEER_VERSIONS['@better-auth/scim>better-call']).toBe('1.4.0'); - expect(settings).toMatch(/^ {4}'@better-auth\/scim>better-call': '1\.4\.0'$/m); + it('keeps the retired @better-auth/scim>better-call suppression OUT (#3653)', () => { + // FLIPPED from a presence ratchet when the scim pin moved off the rc. + // Stable @better-auth/scim 1.7.1 peers better-call@1.4.0 exactly — the + // copy every install already resolves — so the skew the entry declared + // away no longer exists, and a suppression with no skew behind it would + // hide the NEXT real better-call peer break from a newcomer's first + // screen. Re-adding it needs a new measured skew, not a revert. + expect(SCAFFOLD_ALLOWED_PEER_VERSIONS['@better-auth/scim>better-call']).toBeUndefined(); + expect(settings).not.toMatch(/'@better-auth\/scim>better-call'/); }); it.each([ @@ -384,14 +386,16 @@ describe('benign peer-skew declarations (#10326)', () => { ]); }); - it('keeps the @better-auth/utils widening separate from the retiring better-call pin', () => { - // @better-auth/scim appears in TWO entries for two unrelated reasons, and - // they retire on different days: the better-call one goes when scim leaves - // the rc (stable 1.7.1 peers better-call 1.4.0), while the utils one - // outlives it (stable 1.7.1 still peers @better-auth/utils 0.4.2). Deleting - // both together — the obvious move when the rc pin lifts — would silently - // put the utils report back on a newcomer's first screen. - expect(SCAFFOLD_ALLOWED_PEER_VERSIONS['@better-auth/scim>better-call']).toBe('1.4.0'); + it('kept the @better-auth/utils widening when the better-call pin retired (#3653)', () => { + // @better-auth/scim appeared in TWO entries for two unrelated reasons, and + // they retire on different days — which HAPPENED: the better-call one went + // with the rc pin (stable 1.7.1 peers better-call 1.4.0), while the utils + // one outlives it (stable 1.7.1 still peers @better-auth/utils 0.4.2; + // its own retirement key is the pnpm 10.31 floor). Deleting both together + // — the obvious move when the rc pin lifted — would have silently put the + // utils report back on a newcomer's first screen; this pin is what stops + // that regression from ever landing quietly. + expect(SCAFFOLD_ALLOWED_PEER_VERSIONS['@better-auth/scim>better-call']).toBeUndefined(); expect(SCAFFOLD_ALLOWED_PEER_VERSIONS['@better-auth/scim>@better-auth/utils']).toBe('0.5.0'); }); diff --git a/packages/create-objectstack/src/template-consistency.test.ts b/packages/create-objectstack/src/template-consistency.test.ts index 39b85af67d..ee44ed17ab 100644 --- a/packages/create-objectstack/src/template-consistency.test.ts +++ b/packages/create-objectstack/src/template-consistency.test.ts @@ -502,14 +502,17 @@ describe('blank template peer-skew declarations (#10326)', () => { ).toBe(true); }); - it('declares the frozen @better-auth/scim > better-call peer', () => { - // scim is held at 1.7.0-rc.1 deliberately; the rc peers an EXACT 1.3.7 - // while better-auth depends on 1.4.0. A better-auth plugin must share the - // host's better-call instance, so the single 1.4.0 copy is correct. + it('keeps the retired @better-auth/scim > better-call suppression OUT (#3653)', () => { + // FLIPPED from a presence ratchet when the scim pin moved off the rc: + // stable @better-auth/scim 1.7.1 peers better-call@1.4.0 exactly, so the + // rc-era skew the entry declared away is gone. A suppression with no skew + // behind it would hide the NEXT real better-call peer break from an + // `npx create-objectstack` user's first screen — re-adding it needs a new + // measured skew, not a revert. expect( - /^\s*'@better-auth\/scim>better-call':\s*'1\.4\.0'\s*$/m.test(allowed), - 'allowedVersions must accept the single better-call 1.4.0 copy scim resolves to', - ).toBe(true); + /'@better-auth\/scim>better-call'/.test(allowed), + 'the retired @better-auth/scim>better-call suppression must stay out of allowedVersions', + ).toBe(false); }); it.each([ diff --git a/packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml b/packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml index 34ef0a3e6a..2bb641d63f 100644 --- a/packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml +++ b/packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml @@ -44,12 +44,9 @@ allowBuilds: # sign-in and adapter find/update/delete — 1.7.1 behaves identically on # better-sqlite3 13.0.3 and on 12.11.1. The upstream range is stale. # -# @better-auth/scim (held at a release candidate deliberately, not by -# neglect) peers better-call at an exact 1.3.7, while better-auth itself -# depends on 1.4.0. A better-auth plugin has to share the host's better-call -# instance, so the single 1.4.0 copy is the correct resolution rather than a -# skew to repair. This entry retires when SCIM moves off the rc: stable -# @better-auth/scim 1.7.1 already peers better-call 1.4.0. +# (The '@better-auth/scim>better-call' entry that used to sit here retired +# with the scim release-candidate pin — stable @better-auth/scim 1.7.1 peers +# better-call 1.4.0 exactly, so the skew it declared away is gone.) # # @better-auth/core, @better-auth/oauth-provider, @better-auth/scim and # @better-auth/sso each peer an exact @better-auth/utils 0.4.2, while the @@ -75,7 +72,6 @@ allowBuilds: peerDependencyRules: allowedVersions: 'better-auth>better-sqlite3': '13' - '@better-auth/scim>better-call': '1.4.0' '@better-auth/core>@better-auth/utils': '0.5.0' '@better-auth/oauth-provider>@better-auth/utils': '0.5.0' '@better-auth/scim>@better-auth/utils': '0.5.0' diff --git a/packages/platform-objects/src/identity/sys-scim-provider.object.ts b/packages/platform-objects/src/identity/sys-scim-provider.object.ts index 949ae30f78..0252ad4b4e 100644 --- a/packages/platform-objects/src/identity/sys-scim-provider.object.ts +++ b/packages/platform-objects/src/identity/sys-scim-provider.object.ts @@ -13,11 +13,18 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; * is OPEN (here, framework `plugin-auth`); enablement is entitlement-gated by * the cloud / EE license. * - * `scim_token` holds the connection's bearer credential. With the plugin's - * `storeSCIMToken: 'hashed'` (the default this env wires) it stores only a - * HASH — the plaintext is 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. + * ⚠️ 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 diff --git a/packages/plugins/plugin-auth/src/objectql-adapter.ts b/packages/plugins/plugin-auth/src/objectql-adapter.ts index 0a06fdac43..5d8c3ec3a3 100644 --- a/packages/plugins/plugin-auth/src/objectql-adapter.ts +++ b/packages/plugins/plugin-auth/src/objectql-adapter.ts @@ -777,12 +777,24 @@ export function createObjectQLAdapterFactory(rawDataEngine: IDataEngine) { // engine call the raw methods below make inside the callback automatically // binds to the same connection/rollback scope — the trx adapter handed to // the callback is therefore the SAME wrapped adapter, captured at factory - // time below. `require: true` fails CLOSED on a driver without - // `beginTransaction` (#5696): a sequential fallback here would be exactly - // the degraded posture upstream's assertion exists to refuse. + // time below. + // + // ⚠️ Scope this honestly: better-auth routes its OWN multi-writes through + // `adapter.transaction` too — sign-up (user + account) included, measured — + // so this is the transaction path for EVERY better-auth flow, not a + // scim-only seam, and it must keep the same degrade contract those flows + // had under the factory's sequential fallback. Two declared degrades: + // - an engine with no `transaction` API at all (test doubles, minimal + // IDataEngine implementations) runs the callback directly — exactly the + // factory's own `createAsIsTransaction` behaviour; + // - a driver without `beginTransaction` follows the engine's OWN declared + // contract (ADR-0119 D1): run directly, warn once (#4619). Every SQL + // production driver has `beginTransaction`, so a real deployment's scim + // provisioning is genuinely atomic; fail-closed here (`require: true`) + // was measured to 500 every sign-up on the memory engine. let wrappedAdapter: unknown = null; const engineWithTx = rawDataEngine as unknown as { - transaction( + transaction?( cb: (trxCtx: unknown, info: unknown) => Promise, baseContext?: unknown, opts?: { require?: boolean }, @@ -807,11 +819,12 @@ export function createObjectQLAdapterFactory(rawDataEngine: IDataEngine) { // rather than hand the callback a null adapter. throw new Error('[objectql-adapter] transaction requested before the adapter was constructed'); } - return engineWithTx.transaction( - async () => cb(wrappedAdapter as never), - undefined, - { require: true }, - ); + if (typeof engineWithTx.transaction !== 'function') { + // Declared degrade #1 (see the #3653 note above): no transaction API + // on this engine — run directly, as the factory fallback would. + return cb(wrappedAdapter as never); + } + return engineWithTx.transaction(async () => cb(wrappedAdapter as never)); }, }, adapter: () => withValidationErrorMapping({ diff --git a/packages/plugins/plugin-auth/src/sso-client-secret.ts b/packages/plugins/plugin-auth/src/sso-client-secret.ts index 90bd85d940..a7e22bccde 100644 --- a/packages/plugins/plugin-auth/src/sso-client-secret.ts +++ b/packages/plugins/plugin-auth/src/sso-client-secret.ts @@ -22,8 +22,11 @@ * * ## The seam * `@better-auth/sso` has NO secret-at-rest option, so there is no upstream - * switch to flip: `SSOOptions` has no equivalent of - * `scim({ storeSCIMToken: 'hashed' })`. Measured 2026-08-20 against the + * switch to flip: `SSOOptions` has no equivalent of the rc.1-era + * `scim({ storeSCIMToken: 'hashed' })` (an option stable scim has since + * dropped entirely — ObjectStack owns SCIM credentials outright now, #3653, + * which only sharpens the point that at-rest posture is ours to establish). + * Measured 2026-08-20 against the * installed `@better-auth/sso@1.7.1` by enumerating the top-level members of * `SSOOptions` in the shipped `dist/index-CZytzKv6.d.mts` — `resolveUser`, * `guardProviderMutation`, `provisionUser`, `provisionUserOnEveryLogin`, From da00d48a8b9ad7738850adf826c6547315011e7a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 14:10:16 +0000 Subject: [PATCH 4/5] fix(scripts): retire check-prerelease-pin-watch's repo-anchored self-test with the pin (#3653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The watch script promised to retire itself when the last prerelease pin went stable, and its CLI does (empty watch list is the success state) — but its --self-test kept an anti-vacuity assertion that the repo's own pnpm-workspace.yaml parses to a NON-empty watch list, so the 'Stable release watch for prerelease pins' job went red on the very commit that redeemed the pin. Flip the pair to the retired state without opening the hole the old check guarded: the overrides block must still parse non-empty (a parser break cannot masquerade as retirement), and the derived watch list must be empty, with the failure text instructing how to flip back if a prerelease pin ever reappears. Reproduced the CI failure locally before the fix (self-test exit 1, same failing line) and measured exit 0 after; check-self-test-workflow-commands green (140 scripts scanned). Co-authored-by: Claude --- scripts/check-prerelease-pin-watch.mjs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/scripts/check-prerelease-pin-watch.mjs b/scripts/check-prerelease-pin-watch.mjs index e77eaa9b38..23428da531 100644 --- a/scripts/check-prerelease-pin-watch.mjs +++ b/scripts/check-prerelease-pin-watch.mjs @@ -960,18 +960,26 @@ function selfTest() { jsonRun.stdout, ); - // The REAL pins must parse — a self-test that only ever reads its own - // fixture would pass while the file it polices had moved out from under it. - const realWatch = buildWatchList(readOverrides(readFileSync(DEFAULT_WORKSPACE, 'utf8'))); + // The REAL file must still parse — a self-test that only ever reads its + // own fixture would pass while the file it polices had moved out from + // under it. Since #3653 redeemed the last prerelease pin (the scim + // override now targets stable 1.7.1 exactly), the probe's RETIRED state — + // an empty watch list — is the expected reading. Assert it over a + // successfully parsed, non-empty overrides block, so a parser break + // cannot masquerade as retirement. + const realOverrides = readOverrides(readFileSync(DEFAULT_WORKSPACE, 'utf8')); check( - `the repo's own pnpm-workspace.yaml parses to a non-empty watch list (${realWatch.length} pin(s))`, - realWatch.length > 0, - 'no prerelease pin found — if that is genuinely true, this probe has retired itself', + `the repo's own pnpm-workspace.yaml overrides still parse (${Object.keys(realOverrides).length} entr(y/ies))`, + Object.keys(realOverrides).length > 0, + 'readOverrides returned nothing — the parser broke or the file moved; this is NOT retirement', ); + const realWatch = buildWatchList(realOverrides); check( - 'every watched pin from the real file has a base version and a line', - realWatch.every((w) => /^\d+\.\d+\.\d+$/.test(w.base) && /^\d+\.\d+$/.test(w.line)), - JSON.stringify(realWatch.map((w) => [w.name, w.base])), + `the repo's own watch list is EMPTY — the probe's retired state (#3653; found ${realWatch.length} pin(s))`, + realWatch.length === 0, + 'a prerelease pin re-appeared in pnpm-workspace.yaml — the probe is live again: flip this ' + + 'check back to the non-empty expectations it replaced (git log this file), and restore ' + + 'the per-pin base/line shape assertion beside it', ); } finally { rmSync(tmp, { recursive: true, force: true }); From dba7c1d21892ef3d87f9fe25ccbec125804d32c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 14:38:38 +0000 Subject: [PATCH 5/5] fix(auth): scope the adapter's native transactions to SCIM protocol requests (#3653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured twice on the unscoped variant: better-auth wraps whole request flows in adapter.transaction (runWithTransaction), so opening a real driver transaction around every sign-in/sign-up starved the single-connection sqlite pools — the dogfood showcase boot deadlocked on 'Acquire connection error' until the 180s hook timeout, in CI and reproduced locally on this branch, with 337 sibling dogfood tests green. The scim verifier now marks its request's async chain (AsyncLocalStorage enterWith), and config.transaction opens a real engine.transaction only inside that scope — exactly where assertNativeSCIMTransactions demands atomicity. Every other better-auth flow keeps the sequential behaviour it has always had under the factory's as-is fallback, so nothing existing weakens. Re-measured after the fix: the deadlocked dogfood file passes in 19s (13/13), the credential-at-rest suite (which drives a real SCIM 2.0 request through the scoped transaction over better-sqlite3) stays green, and the full plugin-auth suite is 81 files / 1660 tests green. Co-authored-by: Claude --- .../plugins/plugin-auth/src/auth-manager.ts | 7 ++- .../plugin-auth/src/objectql-adapter.ts | 57 +++++++++++-------- .../src/scim-connection-service.ts | 28 +++++++++ 3 files changed, 67 insertions(+), 25 deletions(-) diff --git a/packages/plugins/plugin-auth/src/auth-manager.ts b/packages/plugins/plugin-auth/src/auth-manager.ts index dd6510fb0a..77821cc9bc 100644 --- a/packages/plugins/plugin-auth/src/auth-manager.ts +++ b/packages/plugins/plugin-auth/src/auth-manager.ts @@ -3120,12 +3120,17 @@ export class AuthManager { if (enabled.scim) { await this.addOptionalPlugin(plugins, 'scim', async () => { const { scim } = await import('@better-auth/scim'); - const { verifyScimBearerToken } = await import('./scim-connection-service.js'); + const { verifyScimBearerToken, scimRequestScope } = await import('./scim-connection-service.js'); const secret = this.resolveAuthSecret(); return scim({ connections: [], authentication: { verifyBearerToken: async (input) => { + // Mark the remainder of this request's async chain as a SCIM + // protocol request, so the adapter runs its provisioning writes + // inside a REAL engine transaction (see scimRequestScope's + // rationale in scim-connection-service.ts). + scimRequestScope.enterWith({ scim: true }); const engine = this.config.dataEngine; if (!engine) return null; // no store to verify against — fail closed return verifyScimBearerToken(engine as never, secret, input.token); diff --git a/packages/plugins/plugin-auth/src/objectql-adapter.ts b/packages/plugins/plugin-auth/src/objectql-adapter.ts index 5d8c3ec3a3..82e37b00b9 100644 --- a/packages/plugins/plugin-auth/src/objectql-adapter.ts +++ b/packages/plugins/plugin-auth/src/objectql-adapter.ts @@ -5,6 +5,7 @@ import { createAdapterFactory } from 'better-auth/adapters'; import type { CleanedWhere, WhereOperator } from 'better-auth/adapters'; import { SystemObjectName } from '@objectstack/spec/system'; import { resolveAttributedUserId } from './auth-actor-attribution.js'; +import { inScimRequestScope } from './scim-connection-service.js'; import { adoptExistingMembership } from './adopt-membership.js'; import { filterRevokedSessionRows, @@ -768,30 +769,34 @@ export function createObjectQLAdapterFactory(rawDataEngine: IDataEngine) { const remapWhere = (where: CleanedWhere[]): CleanedWhere[] => where.map((c) => ({ ...c, field: camelToSnake(c.field) })); - // [#3653] NATIVE transactions. Stable `@better-auth/scim` refuses to mount - // on an adapter whose `transaction` is the factory's sequential fallback - // (`assertNativeSCIMTransactions` reads `adapterConfig.transaction` and - // demands a function) — provisioning multi-writes must be atomic. The - // implementation is ObjectQL's own `engine.transaction()`: it publishes the - // handle into the engine's AMBIENT transaction store (ADR-0034), so every - // engine call the raw methods below make inside the callback automatically - // binds to the same connection/rollback scope — the trx adapter handed to - // the callback is therefore the SAME wrapped adapter, captured at factory - // time below. + // [#3653] NATIVE transactions, SCOPED to SCIM protocol requests. Stable + // `@better-auth/scim` refuses to mount on an adapter whose `transaction` is + // the factory's sequential fallback (`assertNativeSCIMTransactions` reads + // `adapterConfig.transaction` and demands a function) — provisioning + // multi-writes must be atomic. The implementation is ObjectQL's own + // `engine.transaction()`: it publishes the handle into the engine's AMBIENT + // transaction store (ADR-0034), so every engine call the raw methods below + // make inside the callback automatically binds to the same + // connection/rollback scope — the trx adapter handed to the callback is + // therefore the SAME wrapped adapter, captured at factory time below. // - // ⚠️ Scope this honestly: better-auth routes its OWN multi-writes through - // `adapter.transaction` too — sign-up (user + account) included, measured — - // so this is the transaction path for EVERY better-auth flow, not a - // scim-only seam, and it must keep the same degrade contract those flows - // had under the factory's sequential fallback. Two declared degrades: - // - an engine with no `transaction` API at all (test doubles, minimal - // IDataEngine implementations) runs the callback directly — exactly the - // factory's own `createAsIsTransaction` behaviour; - // - a driver without `beginTransaction` follows the engine's OWN declared - // contract (ADR-0119 D1): run directly, warn once (#4619). Every SQL - // production driver has `beginTransaction`, so a real deployment's scim - // provisioning is genuinely atomic; fail-closed here (`require: true`) - // was measured to 500 every sign-up on the memory engine. + // ⚠️ The scoping is load-bearing, measured twice, not a hedge. better-auth + // wraps its OWN whole request flows in `adapter.transaction` too + // (`runWithTransaction` — sign-in/sign-up included), and two unscoped + // variants each broke a measured surface: + // - `require: true` (fail closed on non-transactional drivers) 500'd every + // sign-up on the memory engine — 275 plugin-auth tests red; + // - unconditional real transactions starved the single-connection sqlite + // pools: the dogfood showcase boot deadlocked on `Acquire connection + // error` until the 180s hook timeout, in CI and reproduced locally. + // Core better-auth flows never had native DB transactions here (the factory + // default is the sequential as-is fallback), so they KEEP that historical + // posture; the real transaction opens exactly where upstream's assertion + // demands it — inside an authenticated SCIM protocol request, marked by the + // auth manager's `verifyBearerToken` via `scimRequestScope`. Remaining + // declared degrades on that path: an engine with no `transaction` API runs + // the callback directly, and a driver without `beginTransaction` follows + // the engine's ADR-0119 D1 warn-once degrade. let wrappedAdapter: unknown = null; const engineWithTx = rawDataEngine as unknown as { transaction?( @@ -819,8 +824,12 @@ export function createObjectQLAdapterFactory(rawDataEngine: IDataEngine) { // rather than hand the callback a null adapter. throw new Error('[objectql-adapter] transaction requested before the adapter was constructed'); } + // Non-SCIM better-auth flows keep their historical sequential + // behaviour — see the #3653 scoping note above for the two measured + // breakages that make this conditional load-bearing. + if (!inScimRequestScope()) return cb(wrappedAdapter as never); if (typeof engineWithTx.transaction !== 'function') { - // Declared degrade #1 (see the #3653 note above): no transaction API + // Declared degrade (see the #3653 note above): no transaction API // on this engine — run directly, as the factory fallback would. return cb(wrappedAdapter as never); } diff --git a/packages/plugins/plugin-auth/src/scim-connection-service.ts b/packages/plugins/plugin-auth/src/scim-connection-service.ts index 8bca1cf355..525a4225dd 100644 --- a/packages/plugins/plugin-auth/src/scim-connection-service.ts +++ b/packages/plugins/plugin-auth/src/scim-connection-service.ts @@ -38,6 +38,34 @@ */ import { createHmac, randomBytes } from 'node:crypto'; +import { AsyncLocalStorage } from 'node:async_hooks'; + +/** + * Request-scoped marker: "the current async chain is a SCIM protocol + * request". Entered by the auth manager's `verifyBearerToken` wrapper (the + * first application code every authenticated SCIM request runs) via + * `enterWith`, so it holds for the remainder of that request's async chain — + * including the provisioning writes the plugin performs afterwards. + * + * Read by `objectql-adapter.ts`'s `config.transaction`: SCIM requests get a + * REAL engine transaction (the atomicity upstream's + * `assertNativeSCIMTransactions` exists to demand), while every other + * better-auth flow keeps the sequential behaviour it has always had. The + * scoping is load-bearing, measured, not a convenience: better-auth wraps + * WHOLE request flows in `adapter.transaction` (`runWithTransaction`), and + * opening a real driver transaction around every sign-in/sign-up starved the + * single-connection sqlite pools — the dogfood showcase boot deadlocked on + * `Acquire connection error` until the hook timeout, reproduced in CI and + * locally (#3653). Core flows never had native DB transactions before (the + * adapter factory's default is the sequential as-is fallback), so this keeps + * them at their historical posture rather than weakening anything. + */ +export const scimRequestScope = new AsyncLocalStorage<{ scim: true }>(); + +/** Is the current async chain inside an authenticated SCIM protocol request? */ +export function inScimRequestScope(): boolean { + return scimRequestScope.getStore()?.scim === true; +} /** The ObjectStack-owned credential store (see platform-objects/identity). */ export const SCIM_CREDENTIAL_OBJECT = 'sys_scim_connection_credential';