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/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-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/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 2342f57894..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.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/packages/plugins/plugin-auth/src/auth-manager.ts b/packages/plugins/plugin-auth/src/auth-manager.ts index ef36b65545..77821cc9bc 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,44 @@ 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, 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); + }, + }, + }); }); } @@ -3268,6 +3296,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..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, @@ -36,7 +37,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 +56,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 +769,44 @@ export function createObjectQLAdapterFactory(rawDataEngine: IDataEngine) { const remapWhere = (where: CleanedWhere[]): CleanedWhere[] => where.map((c) => ({ ...c, field: camelToSnake(c.field) })); - return createAdapterFactory({ + // [#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. + // + // ⚠️ 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?( + 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 +817,24 @@ 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'); + } + // 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 (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({ create: async >( @@ -1012,6 +1084,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..525a4225dd --- /dev/null +++ b/packages/plugins/plugin-auth/src/scim-connection-service.ts @@ -0,0 +1,213 @@ +// 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'; +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'; + +/** 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-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`, 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@<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.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,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.1': + resolution: {integrity: sha512-bpOPVnYYTUROAJZU0ViL1jXHXoUulVn0Gl3HLN8Epj00Yqob4+rhf3XqVmc/2bi3TY40rmZIlsf1mXQhRdFd1w==} 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.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 + '@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..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@<1.7.0-rc.1': '1.7.0-rc.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' 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 });