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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .changeset/scim-stable-migration.md
Original file line number Diff line number Diff line change
@@ -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).
18 changes: 8 additions & 10 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* - `<four>@better-auth/utils` — `@better-auth/core`, `/oauth-provider`,
* `/scim` and `/sso` each peer an EXACT `@better-auth/utils@0.4.2`, while a
Expand Down Expand Up @@ -167,7 +163,9 @@ export const SCAFFOLD_BUILT_DEPENDENCIES = ['better-sqlite3', 'esbuild'];
*/
export const SCAFFOLD_ALLOWED_PEER_VERSIONS: Record<string, string> = {
'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',
Expand Down
34 changes: 19 additions & 15 deletions packages/cli/test/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -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');
});

Expand Down
17 changes: 10 additions & 7 deletions packages/create-objectstack/src/template-consistency.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Expand Down
18 changes: 18 additions & 0 deletions packages/platform-objects/scripts/i18n-extract.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ import {
SysJwks,
SysSsoProvider,
SysScimProvider,
SysScimConnectionBinding,
SysScimConnectionCredential,
SysScimGroup,
SysScimGroupMember,
SysScimIdentityTombstone,
SysScimProjectionGrant,
SysScimSubject,
SysScimUser,
} from '../src/identity/index.js';

// ── Security ──────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading
Loading