Skip to content

fix(plugin-auth): make the SCIM parity gate reach its model diff on stable @better-auth/scim - #11429

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-11380-scim-parity-gate-ctor
Aug 23, 2026
Merged

fix(plugin-auth): make the SCIM parity gate reach its model diff on stable @better-auth/scim#11429
os-sam merged 3 commits into
mainfrom
claude/issue-11380-scim-parity-gate-ctor

Conversation

@claude

@claudeclaudeBot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes#11380

The guard that did not open at the promised moment

The SCIM parity gate (better-auth-schema-parity.test.ts, the @better-auth/sso + @better-auth/scim block) was written down on #3653 as the safety net for the @better-auth/scim migration: "the gate will report all seven model differences the instant you upgrade." It does not. On the stable line it dies in collection with an uninformative vendor TypeError and prints no model list at all — a migrator gets a crash from the guard that exists to inform them. Declared, not enforced.

Reproduced first, so the repair has a before. Pristine gate, published @better-auth/scim@1.7.1:

 ❯ src/better-auth-schema-parity.test.ts (0 test)
FAIL src/better-auth-schema-parity.test.ts [ src/better-auth-schema-parity.test.ts ]
TypeError: Cannot read properties of undefined (reading 'length')
❯ validateConnections .../@better-auth/scim/dist/index.mjs:7052:26
❯ scim .../@better-auth/scim/dist/index.mjs:7579:2
❯ src/better-auth-schema-parity.test.ts:478:31
478| { label: 'scim', schema: (scim({} as never) as any).schema },
| ^
Test Files 1 failed (1)
Tests no tests

scim({}) is legal on the pinned 1.7.0-rc.1 — options are optional there and nothing is read at construction. The stable factory's first statement is validateConnections(options), which reaches options.connections.length before any existence check. The schema is read in the describe body and in a for header, so the failure lands at collection time and takes the whole file with it.

What this changes

A single test file. Three scim({} as never) call sites become one SCIM_GATE_OPTIONS constant — connections: [] plus a never-invoked authentication.verifyBearerToken — constructed through a small constructScim() wrapper that re-throws with the vendor error attached and says constructor mismatch, not parity failure, since a construction error here can only ever be a collection failure and should at least be legible.

Cheapest options, but not a narrower comparison — that is the half that would have made the repair worthless, so it is measured rather than assumed. Against the published 1.7.1, the gate's shape and a realistic static connection (connections: [{ id, credentials: [{ type: 'bearer', id, token }] }]) declare the same seven models with the same column sets. managedConnections is deliberately not configured: the auth manager does not pass it, and it is what adds the conditional scimManagedConnection / scimManagedCredential / scimManagedConnectionEvent trio (measured: 7 models without it, 10 with).

⛔ Out of scope by dispatch and untouched here: the @better-auth/scim pin (plugin-auth/package.json:29, still 1.7.0-rc.1), auth-manager.ts's scim({...}) call, both scaffold peerDependencyRules.allowedVersions suppressions, and packages/spec. This is gate-only: no product code, no accept/reject behaviour, no published surface — hence skip-changeset rather than a changeset (test files are excluded from every package's files whitelist, which check:published-files re-verified on this tree).

Answering the two questions the dispatch asked

  1. Constructor, not comparison. The repair belongs at the call site. The downstream diff logic is not written against the five-model world in a way that breaks: once collection succeeds it produces a correct, instructive diff (below). Nothing in the comparison logic needed changing.
  2. 1.7.1 was reached without moving the workspace pin. The published tarball was unpacked into the (gitignored) pnpm virtual store and the package's node_modules symlink pointed at it for the duration of each run, then restored by a trap. No tracked manifest was touched; git status was clean at every leg boundary, and the restore leg is printed in each harness log.

The diff the repaired gate prints (published 1.7.1)

The full run is posted on #3653, where the migration ruling resumes — #3653 stays open, nothing here decides it. Summary: 7 failed | 21 passed (28), where the pristine gate produced Tests no tests.

AssertionError: a model gained or lost a platform object. […]
- Expected
+ Received
[
+ "scimConnectionBinding",
"scimGroup",
"scimGroupMember",
- "scimGroupRole",
- "scimGroupRoleGrant",
+ "scimIdentityTombstone",
+ "scimProjectionGrant",
+ "scimSubject",
+ "scimUser",
]

plus expected [ 'user', 'session', 'account', …(9) ] to include 'scimProvider' (the model the bridge maps is gone on stable) and one named must map to a platform object via AUTH_MODEL_TO_PROTOCOL failure per new model.

Verification — all at 7544c02b5b

Gate union derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, no hand-supplied paths (1 path vs merge base 122ef3860). All 13 families green, each read from its own verdict line:

familyverdict
check:published-files✓ … 69 publishable package(s) … admits no test, test-harness config or build script
check:slot-lookup✓ slot-lookup ratchet holds … none new
check:test-source-aliascheck-test-source-alias OK — 72 packages with tests scanned
check:type-source-resolutioncheck-type-source-resolution OK — 77 packages … scanned
check-ci-filter-parity.mjsOK: all 89 declared cross-package glob(s) … covered
check-plugin-teardown-shape.mjs✓ … 63 Plugin implementation(s) across 4547 source(s)
docs-audit/check-affected-docs.mjsexit 0
check:query-options-erasure✓ query-options-erasure ratchet holds … none new
check:type-check-coverageexit 0 (structural half)
check:engine-double-contractOK — 388 pinned, 133 in the DEBT ledger, 2 exempt
check:cross-package-test-inputsOK: 14 package(s) read outside themselves, all declared
check:where-matcher✓ where-matcher conformance holds … 0 silently-wrong
check:nul-bytescheck-nul-bytes: OK (scanned 6411 text file(s) … no raw ASCII control bytes)
  • pnpm --filter @objectstack/plugin-auth test70 files / 1455 tests passed, VERDICT command-exit 0.
  • pnpm --filter @objectstack/plugin-auth typecheck — green (VERDICT command-exit 0) once the package's own dist is built; the first attempt's examples/basic-usage.ts … TS2307 was an unbuilt-package artefact, not this change.
  • pnpm lint — repo-wide eslint . --no-inline-config, VERDICT command-exit 0. No narrowing claimed.
  • Parity gate on the pinned rc.1, pristine vs repaired: 24 passed (24) both ways — the repair moves nothing on the version CI actually runs.

Declared narrowing, one family:check:type-check-debt (the --re-measure ratchet) needs the whole ledgered dependency closure built and was not run locally; CI runs it. Substituted, since this change adds test code to a TEST_DEBT package: the same TEST_DEBT-shaped tsc project (package tsconfig + explicit files list putting the hidden *.test.ts back, 116 files) measured 97 errors before (origin/main) and 97 after, identical error sets. Reverse control: injecting one deliberate type error into the edited file moves it to 99 and names the file and line — the instrument can see this file, so the zero delta is a measurement rather than a blind spot.


Generated by Claude Code

os-samand others added 3 commits August 23, 2026 15:53
… accepts
The gate is the declared safety net for the @better-auth/scim migration
(#3653): the day the pin moves off 1.7.0-rc.1 it is supposed to name every
model whose platform object is missing. It did not open at that moment.
`scim({})` is legal on the pinned rc.1 (options optional, nothing read at
construction), but the stable factory's first statement is
`validateConnections(options)`, which reaches `options.connections.length`
before any existence check. The schema is read in the describe body and in a
`for` header, so on stable the file dies during COLLECTION with
TypeError: Cannot read properties of undefined (reading 'length')
and prints that instead of the model diff it promises.
Construct with `connections: []` plus a never-invoked bearer-token verifier --
the cheapest shape stable accepts (an empty connection list is only legal when
a verifier or the managed catalog resolves connections), and inert on the
pinned rc.1, whose factory spreads its options and reads only the keys it
knows. `managedConnections` is deliberately not configured: the auth manager
does not pass it, and it is what adds the conditional managed trio.
A constructor mismatch can only ever be a collection failure here -- the `for`
header has to read the schema -- but it does not have to be illegible, so the
call is wrapped and re-thrown with the vendor error as `cause` and an
instruction naming the constructor as the broken half.
Gate-only change: no product code, no published surface.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
…pares the real surface
The repair picked the cheapest options stable accepts. Cheapest is only safe if
it does not narrow WHAT the gate compares, so that half is now measured rather
than assumed: against the published 1.7.1, `connections: [] +
authentication.verifyBearerToken` and a realistic static connection
(`connections: [{ id, credentials: [{ type: 'bearer', id, token }] }]`) declare
the SAME seven models with the SAME column sets.
Also records why a connection entry is not the smaller input it looks like:
validateConnections goes on to read `connection.credentials.length`, so a
half-specified entry throws the same TypeError one line further down -- measured
while probing this.
Comment-only; no assertion or construction changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

@os-sam
os-sam marked this pull request as ready for review August 23, 2026 18:06
@os-sam
os-sam added this pull request to the merge queueAug 23, 2026
Merged via the queue into main with commit 4ed3053Aug 23, 2026
35 checks passed
@os-sam
os-sam deleted the claude/issue-11380-scim-parity-gate-ctor branch August 23, 2026 18:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix the SCIM parity gate's constructor call so it actually prints the seven-model diff against @better-auth/scim 1.7.1

2 participants

@os-sam@claude