Skip to content

Commit 366f895

Browse files
os-salesos-litantclaude
authored
feat(auth): migrate @better-auth/scim to stable 1.7.1 — seven-model provision, app-owned bearer verification (#3653) (#12726)
* chore(deps): move the @better-auth/scim pin off the rc onto the stable line (measurement base, #3653) Part of #3653. NOT a proposed landing: leg 2' stopped on a scope fork before the provisioning half could be written, so this branch carries the pin move alone as the reproducible base every measurement in the report was taken on. A pin-move-only tree is red by construction (7 failed | 21 passed on the parity gate) and must never be landed on its own. - packages/plugins/plugin-auth/package.json 1.7.0-rc.1 -> ^1.7.1 - pnpm-workspace.yaml '@better-auth/scim@<1.7.0-rc.1': '1.7.0-rc.1' -> '@better-auth/scim@<2.0.0': '^1.7.1' (major-boundary bound, matching the rest of the family per this file's own doctrine; the old bound sat AT the rc precisely so a <2.0.0 bound could not rewrite the stable release down onto it.) Measured consequences, all reproducible from this commit: - '^1.7.1' resolves to 1.7.2, not 1.7.1. Every prior measurement on this epic was taken against 1.7.1. Re-measured on 1.7.2: same seven models, same three-way constructor predicate, same absent scimProvider. - scim lands on 1.7.2 while the rest of the family stays 1.7.1. 1.7.2 peers '@better-auth/core': '^1.7.2' and 'better-auth': '^1.7.2', which 1.7.1 does not satisfy; this repo's existing overrides rewrite those peer ranges down to '^1.7.1' in the lockfile, so nothing reports the mismatch. One copy each of better-auth and @better-auth/core remain in the store. - check:prerelease-pins retires itself: "No prerelease pin left in pnpm-workspace.yaml overrides - nothing to watch", exit 0. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0194kbQJxUvv2yvsGRtuXpP5 * feat(auth,platform-objects,spec): migrate @better-auth/scim to stable 1.7.1 — provision the seven models, app-owned bearer verification (#3653) Part of #3653 (leg 2' of epic #11632). The pin resolves to 1.7.1 EXACTLY (not ^1.7.1, which resolves to 1.7.2 whose better-auth/core ^1.7.2 peers only the workspace overrides' silencing would 'satisfy'; floating is its own follow-up card). - pnpm-workspace.yaml + plugin-auth: exact 1.7.1 pin, stale rc-hold comment blocks rewritten to the landed state. - packages/platform-objects/src/identity/: the seven stable models provisioned as platform objects (scimConnectionBinding, scimGroup, scimGroupMember, scimIdentityTombstone, scimProjectionGrant, scimSubject, scimUser) plus sys_scim_connection_credential, the ObjectStack-owned credential store for the app-owned verifyBearerToken route (stable upstream stores no credential at all). Translation bundles regenerated with pnpm i18n:extract only. - packages/spec platform-object-names.ts: eight additive registry names (the measured-unsplittable spec edit; sys_scim_provider stays — its retirement is #11757's). - plugin-security BETTER_AUTH_MANAGED_OBJECTS: the seven library-managed names, forced additively by the bidirectional drift pin (default-permission-sets.test.ts) the moment the objects declare managedBy: 'better-auth'. - plugin-auth scim-connection-service.ts: mint + digest + verify owned outright; at rest only HMAC-SHA-256(secret, domain-separated bearer), base64url — at parity or better than the rc.1 unsalted SHA-256. - auth-manager.ts: stable constructor shape — connections: [] plus authentication.verifyBearerToken resolving the connection from a sys_scim_connection_credential row at request time; auth secret resolved once (memoized) so digests and better-auth share key material. - objectql-adapter.ts: seven AUTH_MODEL_TO_PROTOCOL bridge entries, and NATIVE adapter transactions over engine.transaction({ require: true }) — stable scim refuses the sequential fallback by assertion. - better-auth-schema-parity.test.ts: KNOWN_UNMAPPED_MODELS shrinks to the empty set (exact-set assertion kept, both directions). - credential-at-rest-posture.test.ts: restructured honestly — the upstream-cleartext control arm is no longer measurable (upstream stores nothing); its successor pins that vacating fact plus the keyed digest relationship, negatives for the unkeyed/wrong-key/undomained shapes, and live 200/401 rejection paths incl. revocation and expiry. Co-authored-by: Claude <noreply@anthropic.com> * feat(cli,create-objectstack): retire the @better-auth/scim>better-call suppression with the pin; adapter transaction degrades declared (#3653) Epic item 7, same stroke as the pin move: stable 1.7.1 peers better-call@1.4.0 exactly, so the rc-era allowedVersions key retires in its two declared places (SCAFFOLD_ALLOWED_PEER_VERSIONS and the blank template) and both presence ratchets flip to absence pins. The better-auth>better-sqlite3 and four @better-auth/utils entries stay — re-derived from the tree, their conditions are separate and unmet; the separation pin now guards that they survived the retirement. Adapter transactions: better-auth routes its own multi-writes (sign-up included) through adapter.transaction — measured: a fail-closed require:true 500'd every sign-up on the memory engine — so the implementation keeps the two declared degrades those flows always had (no-transaction-API engines run the callback directly; drivers without beginTransaction follow the engine's ADR-0119 D1 warn-once degrade). SQL drivers get real atomic transactions. Also: changeset (minor across the six touched published packages, level argued in the body), and two stale-prose corrections where files described the retired rc.1 storeSCIMToken wiring as current. Co-authored-by: Claude <noreply@anthropic.com> * fix(scripts): retire check-prerelease-pin-watch's repo-anchored self-test with the pin (#3653) The watch script promised to retire itself when the last prerelease pin went stable, and its CLI does (empty watch list is the success state) — but its --self-test kept an anti-vacuity assertion that the repo's own pnpm-workspace.yaml parses to a NON-empty watch list, so the 'Stable release watch for prerelease pins' job went red on the very commit that redeemed the pin. Flip the pair to the retired state without opening the hole the old check guarded: the overrides block must still parse non-empty (a parser break cannot masquerade as retirement), and the derived watch list must be empty, with the failure text instructing how to flip back if a prerelease pin ever reappears. Reproduced the CI failure locally before the fix (self-test exit 1, same failing line) and measured exit 0 after; check-self-test-workflow-commands green (140 scripts scanned). Co-authored-by: Claude <noreply@anthropic.com> * fix(auth): scope the adapter's native transactions to SCIM protocol requests (#3653) Measured twice on the unscoped variant: better-auth wraps whole request flows in adapter.transaction (runWithTransaction), so opening a real driver transaction around every sign-in/sign-up starved the single-connection sqlite pools — the dogfood showcase boot deadlocked on 'Acquire connection error' until the 180s hook timeout, in CI and reproduced locally on this branch, with 337 sibling dogfood tests green. The scim verifier now marks its request's async chain (AsyncLocalStorage enterWith), and config.transaction opens a real engine.transaction only inside that scope — exactly where assertNativeSCIMTransactions demands atomicity. Every other better-auth flow keeps the sequential behaviour it has always had under the factory's as-is fallback, so nothing existing weakens. Re-measured after the fix: the deadlocked dogfood file passes in 19s (13/13), the credential-at-rest suite (which drives a real SCIM 2.0 request through the scoped transaction over better-sqlite3) stays green, and the full plugin-auth suite is 81 files / 1660 tests green. Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: os-litant <litant.dev@proton.me> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 340c5e5 commit 366f895

39 files changed

Lines changed: 4240 additions & 448 deletions
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
"@objectstack/plugin-auth": minor
3+
"@objectstack/platform-objects": minor
4+
"@objectstack/spec": minor
5+
"@objectstack/plugin-security": minor
6+
"@objectstack/cli": minor
7+
"create-objectstack": minor
8+
---
9+
10+
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)
11+
12+
The stable line is the rc.2-lineage rewrite: the rc.1 `scimProvider` model,
13+
`/scim/generate-token` endpoint and `storeSCIMToken` option no longer exist,
14+
replaced by seven new models and a three-way connection contract. This lands
15+
the migration atomically:
16+
17+
- **Seven new platform objects** back the stable models —
18+
`sys_scim_connection_binding`, `sys_scim_group`, `sys_scim_group_member`,
19+
`sys_scim_identity_tombstone`, `sys_scim_projection_grant`,
20+
`sys_scim_subject`, `sys_scim_user` — bridged via `AUTH_MODEL_TO_PROTOCOL`,
21+
registered in the platform-object-names registry, listed in
22+
`BETTER_AUTH_MANAGED_OBJECTS`, and column-pinned by the parity gate (whose
23+
`KNOWN_UNMAPPED_MODELS` shrinks to the empty set: the rc.1-era group
24+
provisioning gap — IdP `/Groups` pushes hitting tables that did not exist —
25+
is closed).
26+
- **SCIM connections stay runtime data.** The stable constructor is satisfied
27+
with an application-owned `authentication.verifyBearerToken` that resolves
28+
the connection from a row at request time — not static boot config, and not
29+
the upstream `managedConnections` catalog (deliberately not adopted).
30+
- **ObjectStack owns SCIM credentials outright** (stable upstream stores no
31+
credential at all): `sys_scim_connection_credential` plus
32+
`scim-connection-service.ts` mint/digest/verify. At rest only an
33+
HMAC-SHA-256 keyed by the deployment auth secret (base64url,
34+
domain-separated) is stored — at parity or better than the rc.1 unsalted
35+
SHA-256 — pinned by `credential-at-rest-posture.test.ts` including live
36+
401 paths for forged, revoked and expired bearers.
37+
- **The ObjectQL better-auth adapter gains native transactions**
38+
(`engine.transaction`, fail-closed on drivers without `beginTransaction`),
39+
which stable scim requires by assertion for atomic provisioning writes.
40+
- **Scaffold suppression retired**: the `@better-auth/scim>better-call`
41+
`allowedVersions` entry (CLI renderer + blank template) is gone — stable
42+
1.7.1 peers `better-call@1.4.0` exactly — and its presence ratchets flipped
43+
to absence pins. The `better-auth>better-sqlite3` and four
44+
`@better-auth/utils` entries stay; their retirement conditions are separate
45+
and unmet.
46+
- The pin resolves **1.7.1 exactly** (not `^1.7.1`): 1.7.2 peers
47+
`better-auth`/`@better-auth/core` at `^1.7.2`, which only the workspace
48+
overrides' silencing would "satisfy" while the family is 1.7.1. Floating is
49+
its own follow-up.
50+
51+
**Semver: minor, argued.** The rc.1 SCIM surface this replaces (generate-token
52+
endpoint, rc.1 bearer tokens, `sys_scim_provider` rows) changes incompatibly —
53+
but that surface is default-off (`OS_SCIM_ENABLED`), was shipped with a
54+
documented "do not let the IdP push groups" boundary, and the maintainer ruled
55+
(2026-08-25) that SCIM has no real customers and old data need not carry: the
56+
one binding constraint is that an existing system upgrades smoothly, which it
57+
does — every table the installed library can write exists at this version, and
58+
SCIM-disabled deployments see no behavior change. A major would move the whole
59+
fixed version group for a feature surface with zero consumers. Deployments
60+
that had SCIM enabled must mint new connection credentials (digests are not
61+
portable from rc.1 on any path — IdP token reissue is a migration-day
62+
operator action regardless of semver level). `sys_scim_provider` itself is
63+
NOT removed here; its retirement is tracked separately (#11757).

packages/cli/src/commands/init.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,11 @@ export const SCAFFOLD_BUILT_DEPENDENCIES = ['better-sqlite3', 'esbuild'];
109109
* and on 12.11.1. So the upstream range is stale and 13 is right — widening
110110
* is the correct remedy, not pinning our own declaration back to 12.
111111
*
112-
* - `@better-auth/scim>better-call` — scim is held at `1.7.0-rc.1`
113-
* deliberately (stable 1.7.x ships a whole-model rewrite that is its own
114-
* migration), and the rc peers an exact `better-call@1.3.7` while
115-
* better-auth itself depends on 1.4.0. A better-auth plugin must share the
116-
* HOST's better-call instance, so the single 1.4.0 copy every install
117-
* already resolves is the correct tree, not a skew to repair.
118-
* ⚠️ This entry retires together with the SCIM rc pin — delete both at once.
119-
* Stable `@better-auth/scim@1.7.1` peers `better-call@1.4.0`, so the skew
120-
* this line covers is genuinely gone the moment the pin moves.
112+
* - RETIRED (#3653): `@better-auth/scim>better-call` — the rc.1-era scim pin
113+
* peered an exact `better-call@1.3.7` against the host's 1.4.0, and this
114+
* map declared that skew away. Stable `@better-auth/scim@1.7.1` (the pin
115+
* since #3653 landed) peers `better-call@1.4.0`, so the skew is genuinely
116+
* gone and the entry retired WITH the pin move, as its ratchet demanded.
121117
*
122118
* - `<four>@better-auth/utils` — `@better-auth/core`, `/oauth-provider`,
123119
* `/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'];
167163
*/
168164
export const SCAFFOLD_ALLOWED_PEER_VERSIONS: Record<string, string> = {
169165
'better-auth>better-sqlite3': '13',
170-
'@better-auth/scim>better-call': '1.4.0',
166+
// '@better-auth/scim>better-call' retired with the scim rc pin (#3653) —
167+
// stable 1.7.1 peers better-call@1.4.0 exactly, the copy every install
168+
// already resolves. init.test.ts pins its ABSENCE now.
171169
'@better-auth/core>@better-auth/utils': '0.5.0',
172170
'@better-auth/oauth-provider>@better-auth/utils': '0.5.0',
173171
'@better-auth/scim>@better-auth/utils': '0.5.0',

packages/cli/test/init.test.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -333,13 +333,15 @@ describe('benign peer-skew declarations (#10326)', () => {
333333
expect(settings).toMatch(/^ {4}'better-auth>better-sqlite3': '13'$/m);
334334
});
335335

336-
it('accepts the single better-call copy @better-auth/scim resolves to', () => {
337-
// scim is held at 1.7.0-rc.1 on purpose; the rc peers an EXACT
338-
// `better-call@1.3.7` while better-auth depends on 1.4.0. A better-auth
339-
// plugin must share the HOST's better-call instance, so one 1.4.0 copy is
340-
// the correct tree. Retires with the scim rc pin.
341-
expect(SCAFFOLD_ALLOWED_PEER_VERSIONS['@better-auth/scim>better-call']).toBe('1.4.0');
342-
expect(settings).toMatch(/^ {4}'@better-auth\/scim>better-call': '1\.4\.0'$/m);
336+
it('keeps the retired @better-auth/scim>better-call suppression OUT (#3653)', () => {
337+
// FLIPPED from a presence ratchet when the scim pin moved off the rc.
338+
// Stable @better-auth/scim 1.7.1 peers better-call@1.4.0 exactly — the
339+
// copy every install already resolves — so the skew the entry declared
340+
// away no longer exists, and a suppression with no skew behind it would
341+
// hide the NEXT real better-call peer break from a newcomer's first
342+
// screen. Re-adding it needs a new measured skew, not a revert.
343+
expect(SCAFFOLD_ALLOWED_PEER_VERSIONS['@better-auth/scim>better-call']).toBeUndefined();
344+
expect(settings).not.toMatch(/'@better-auth\/scim>better-call'/);
343345
});
344346

345347
it.each([
@@ -384,14 +386,16 @@ describe('benign peer-skew declarations (#10326)', () => {
384386
]);
385387
});
386388

387-
it('keeps the @better-auth/utils widening separate from the retiring better-call pin', () => {
388-
// @better-auth/scim appears in TWO entries for two unrelated reasons, and
389-
// they retire on different days: the better-call one goes when scim leaves
390-
// the rc (stable 1.7.1 peers better-call 1.4.0), while the utils one
391-
// outlives it (stable 1.7.1 still peers @better-auth/utils 0.4.2). Deleting
392-
// both together — the obvious move when the rc pin lifts — would silently
393-
// put the utils report back on a newcomer's first screen.
394-
expect(SCAFFOLD_ALLOWED_PEER_VERSIONS['@better-auth/scim>better-call']).toBe('1.4.0');
389+
it('kept the @better-auth/utils widening when the better-call pin retired (#3653)', () => {
390+
// @better-auth/scim appeared in TWO entries for two unrelated reasons, and
391+
// they retire on different days — which HAPPENED: the better-call one went
392+
// with the rc pin (stable 1.7.1 peers better-call 1.4.0), while the utils
393+
// one outlives it (stable 1.7.1 still peers @better-auth/utils 0.4.2;
394+
// its own retirement key is the pnpm 10.31 floor). Deleting both together
395+
// — the obvious move when the rc pin lifted — would have silently put the
396+
// utils report back on a newcomer's first screen; this pin is what stops
397+
// that regression from ever landing quietly.
398+
expect(SCAFFOLD_ALLOWED_PEER_VERSIONS['@better-auth/scim>better-call']).toBeUndefined();
395399
expect(SCAFFOLD_ALLOWED_PEER_VERSIONS['@better-auth/scim>@better-auth/utils']).toBe('0.5.0');
396400
});
397401

packages/create-objectstack/src/template-consistency.test.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -502,14 +502,17 @@ describe('blank template peer-skew declarations (#10326)', () => {
502502
).toBe(true);
503503
});
504504

505-
it('declares the frozen @better-auth/scim > better-call peer', () => {
506-
// scim is held at 1.7.0-rc.1 deliberately; the rc peers an EXACT 1.3.7
507-
// while better-auth depends on 1.4.0. A better-auth plugin must share the
508-
// host's better-call instance, so the single 1.4.0 copy is correct.
505+
it('keeps the retired @better-auth/scim > better-call suppression OUT (#3653)', () => {
506+
// FLIPPED from a presence ratchet when the scim pin moved off the rc:
507+
// stable @better-auth/scim 1.7.1 peers better-call@1.4.0 exactly, so the
508+
// rc-era skew the entry declared away is gone. A suppression with no skew
509+
// behind it would hide the NEXT real better-call peer break from an
510+
// `npx create-objectstack` user's first screen — re-adding it needs a new
511+
// measured skew, not a revert.
509512
expect(
510-
/^\s*'@better-auth\/scim>better-call':\s*'1\.4\.0'\s*$/m.test(allowed),
511-
'allowedVersions must accept the single better-call 1.4.0 copy scim resolves to',
512-
).toBe(true);
513+
/'@better-auth\/scim>better-call'/.test(allowed),
514+
'the retired @better-auth/scim>better-call suppression must stay out of allowedVersions',
515+
).toBe(false);
513516
});
514517

515518
it.each([

packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,9 @@ allowBuilds:
4444
# sign-in and adapter find/update/delete — 1.7.1 behaves identically on
4545
# better-sqlite3 13.0.3 and on 12.11.1. The upstream range is stale.
4646
#
47-
# @better-auth/scim (held at a release candidate deliberately, not by
48-
# neglect) peers better-call at an exact 1.3.7, while better-auth itself
49-
# depends on 1.4.0. A better-auth plugin has to share the host's better-call
50-
# instance, so the single 1.4.0 copy is the correct resolution rather than a
51-
# skew to repair. This entry retires when SCIM moves off the rc: stable
52-
# @better-auth/scim 1.7.1 already peers better-call 1.4.0.
47+
# (The '@better-auth/scim>better-call' entry that used to sit here retired
48+
# with the scim release-candidate pin — stable @better-auth/scim 1.7.1 peers
49+
# better-call 1.4.0 exactly, so the skew it declared away is gone.)
5350
#
5451
# @better-auth/core, @better-auth/oauth-provider, @better-auth/scim and
5552
# @better-auth/sso each peer an exact @better-auth/utils 0.4.2, while the
@@ -75,7 +72,6 @@ allowBuilds:
7572
peerDependencyRules:
7673
allowedVersions:
7774
'better-auth>better-sqlite3': '13'
78-
'@better-auth/scim>better-call': '1.4.0'
7975
'@better-auth/core>@better-auth/utils': '0.5.0'
8076
'@better-auth/oauth-provider>@better-auth/utils': '0.5.0'
8177
'@better-auth/scim>@better-auth/utils': '0.5.0'

packages/platform-objects/scripts/i18n-extract.config.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ import {
104104
SysJwks,
105105
SysSsoProvider,
106106
SysScimProvider,
107+
SysScimConnectionBinding,
108+
SysScimConnectionCredential,
109+
SysScimGroup,
110+
SysScimGroupMember,
111+
SysScimIdentityTombstone,
112+
SysScimProjectionGrant,
113+
SysScimSubject,
114+
SysScimUser,
107115
} from '../src/identity/index.js';
108116

109117
// ── Security ──────────────────────────────────────────────────────────────
@@ -241,6 +249,16 @@ const config: ObjectStackDefinition = defineStack({
241249
SysJwks,
242250
SysSsoProvider,
243251
SysScimProvider,
252+
// Stable @better-auth/scim 1.7.x model set + the ObjectStack-owned
253+
// credential store (#3653). SysScimProvider above retires under #11757.
254+
SysScimConnectionBinding,
255+
SysScimConnectionCredential,
256+
SysScimGroup,
257+
SysScimGroupMember,
258+
SysScimIdentityTombstone,
259+
SysScimProjectionGrant,
260+
SysScimSubject,
261+
SysScimUser,
244262

245263
// Security: RBAC moved to @objectstack/plugin-security, sharing to
246264
// @objectstack/plugin-sharing (ADR-0029 K2 / D8).

packages/platform-objects/src/apps/translations/bundle-ownership.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ const OWNED_OBJECTS = new Set([
2626
'sys_oauth_client_resource', 'sys_oauth_client_assertion', 'sys_jwks',
2727
// identity — external SSO / SCIM providers (admin-facing, better-auth-managed)
2828
'sys_sso_provider', 'sys_scim_provider',
29+
// identity — stable @better-auth/scim 1.7.x model set + the ObjectStack-owned
30+
// credential store (#3653; sys_scim_provider above retires under #11757)
31+
'sys_scim_connection_binding', 'sys_scim_connection_credential', 'sys_scim_group',
32+
'sys_scim_group_member', 'sys_scim_identity_tombstone', 'sys_scim_projection_grant',
33+
'sys_scim_subject', 'sys_scim_user',
2934
// audit / messaging-adjacent (still owned here)
3035
'sys_notification', 'sys_attachment', 'sys_email', 'sys_email_template',
3136
'sys_saved_report', 'sys_report_schedule', 'sys_job', 'sys_job_run', 'sys_job_queue',

0 commit comments

Comments
 (0)