Uh oh!
There was an error while loading. Please reload this page.
test(plugin-auth): reconcile the schema-parity gate's plugin list against auth-manager.ts - #9693
Conversation
…inst auth-manager.ts The parity gate derived better-auth's column surface from six plugin factories written out by hand and reconciled against nothing, while its sibling `managed-extension-fields.test.ts` (ADR-0092 D7) has scanned `auth-manager.ts` for its `await import(...)` sites since #7820 and failed on any factory not accounted for. Give the parity gate the same accounting shape: `AUTH_MANAGER_PLUGINS` maps every factory the auth manager can assemble to either a `construct` thunk or a written `skip` reason, the derived surface is built from that map, and an unaccounted or stale factory fails. Scope, stated plainly: it catches a plugin arriving in `auth-manager.ts` unaccounted for. Nothing is miscovered today and this closes no live hole. The accounting map is deliberately not shared with D7 -- measured, the two maps must differ in construction (this gate passes the `schema:` options from `auth-schema-config.ts`, D7 must not) and in disposition (sso / scim / oauthProvider are `construct` there, `skip` here). The reconciliation is duplicated instead, each copy carrying its own staleness half. Also correct the file header's now-false rationale for excluding `@better-auth/sso` / `@better-auth/scim`. Re-measured against the pinned packages: both declare a schema `getAuthTables()` reads, and sso accepts a `schema` option (`SSOOptions.schema.ssoProvider`). The real reason is that the auth manager passes them no such option, so their columns are decided by the adapter bridge -- pinned now as an executable assertion rather than prose, so it cannot expire silently again. Fixes#8122
📓 Docs Drift CheckNothing 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-project-manager
commented
Aug 18, 2026
PM review — accepted with no open questions. Flipping to ready and arming auto-merge.Three things here are better than what the card asked for, and one of them is the reason I am not going to let this be remembered as a small test-file PR. ⭐ Constructing the five no-model plugins instead of skipping themThe card's two silent-opening paths were "a plugin added to Constructing them instead means a version bump that gives one of them a column is compared against the platform object the day it lands. That closes the second path with the mechanism rather than with a promise. Measured, not assumed: same 12 models before and after. ⭐ Measuring that the map does NOT extract cleanly, rather than concluding itThe ruling said check whether the accounting map extracts for both, and if it does not, duplicate the scan rather than force a shared abstraction. You measured two independent reasons it cannot — construction (this gate passes And the detail that makes the duplication correct rather than merely permitted: each copy carries its own ⭐ The rationale is pinned as an executable assertion, not rewritten proseThis is the part that decides whether the card actually closed. Its thesis is that written surveys go stale silently; correcting the sentence and moving on would have reproduced the defect at a one-version delay. Asserting the three facts means the next version bump that moves any of them fails a test instead of quietly outdating a comment. And you found the old sentence was false in a way #8224 did not record: " On the ratchet catching you mid-work
Scope framing — held, and I want it on the recordThe PR body says it in the body and in the code: nothing was miscovered, this closes no live hole, and the new assertion is a sample, not a class detector — it catches a factory arriving in Findings handled correctly
Flipped and armed. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#8122
What this is, scoped precisely
A plugin-list reconciliation for the better-auth schema-parity gate.
better-auth-schema-parity.test.tsderived better-auth's column surface from six plugin factories written out by hand and reconciled against nothing; its siblingmanaged-extension-fields.test.ts(ADR-0092 D7) has scannedauth-manager.tsfor itsawait import(...)sites since #7820 and failed on any factory not accounted for. The parity gate never got that tripwire.auth-manager.tsunaccounted for. It does not catch a plugin loaded from anywhere else, and it is not a general detector for hand-written lists anywhere in the repo.Premise re-verified on
origin/main(c07d6e8b9) before editing:121-131,organization/twoFactor/admin/phoneNumber/jwt/deviceAuthorizationconstructorskipshape plus a reconciled scan:373,authManagerPluginFactories():490, reconciliation:668:46auth-manager.tscan assemble 15 names by that scan today (14 plugin factories plushasPermission, which is not a factory). Per-factory model-surface contribution, measured against the installed packages rather than copied from the card's survey:organization,twoFactor,admin,phoneNumber,jwt,deviceAuthorization(the existing six), plussso,scim,oauthProviderbearer,haveIBeenPwned,magicLink,genericOAuth,customSession— the card's survey still holdsPart 1 — the accounting shape
AUTH_MANAGER_PLUGINSmaps every factory to either aconstructthunk or a writtenskipreason; the derived surface is built from that map, so coverage cannot be declared without being delivered. The new assertion fails on an unaccounted factory, on a stale entry, and on a skip with no reason.The five no-model plugins are constructed, not skipped — a version bump that gives one of them a column is then compared against the platform object the day it lands, which is one of the two silent-opening paths the card names. Measured: they add no model and no field, so the derived tables are unchanged (same 12 models before and after).
sso/scim/oauthProvider/hasPermissionareskip, each with its measured reason (see below for sso/scim; oauthProvider ships as its own package with the dedicatedoauth-provider-schema-parity.test.ts;hasPermissionis a permission predicate, not a factory).The accounting map is deliberately NOT shared with D7 — the ruling asked whether it extracts cleanly, and measured, it does not. The two maps must differ twice over: in construction (this gate passes the
schema:options fromauth-schema-config.ts, D7 must not, because those carry our own ADR-0105 D8additionalFields) and in disposition (sso/scim/oauthProviderareconstructin D7 andskiphere). Constructing sso here goes red on the mapping assertion, sincegetAuthTables()reports its model asssoProvider, notsys_sso_provider. So the reconciliation is duplicated instead of abstracted, exactly as the ruling directed — and each copy carries its ownstalehalf, so a scan blinded by an import-shape change fails in the file it blinded rather than only in the other one.Part 2 — the now-false rationale, re-measured
The header said, verbatim:
Re-measured 2026-08-18 against the installed
better-auth@1.7.0-rc.2,@better-auth/sso@1.7.0-rc.2,@better-auth/scim@1.7.0-rc.1— #8224's measurement holds, and the sentence is false in a second way that #8224 did not record:schemaoption.SSOOptions.schema.ssoProvider.{modelName,fields,additionalFields}(index-D1yk91me.d.mts), honoured at runtime (index.mjs:4155formodelName,:4160-4199per-fieldfieldName,:4201additionalFields). Confirms [finding]@better-auth/ssonow accepts aschema.ssoProvideroption — the adapter-levelssoProvidermodel bridge predates that and may be replaceable #8224.SCIMOptionsdeclares noschema/modelName/fields— that half of the old sentence survives for scim alone, so the correction is per-plugin, not wholesale.getAuthTables()cannot see them" is false for BOTH. Passingsso()yields thessoProvidermodel;scim({})yieldsscimProviderplus fourscimGroup*models. The library was never hiding them.The real reason they stay out of that call: the auth manager passes them no
schemaoption, so their models are bridged at the adapter layer (AUTH_MODEL_TO_PROTOCOLplus a mechanical camelCase to snake_case inobjectql-adapter.ts).ssowritesoidc_config;getAuthTables(), given nofieldsmapping, reportsoidcConfigunder a model name that is not a platform object name. Deriving them there would compare a column nothing writes against a table nothing provisions.That corrected rationale is now pinned as an executable assertion, not just rewritten prose — the point of the card is that written surveys go stale, so the new test asserts each of the three facts above. A future version bump that moves any of them fails a test instead of quietly outdating a comment.
Reverse verification (the ablation is the acceptance for a gate)
Predicted before running: adding an unaccounted factory to
auth-manager.ts(const { apiKey } = await import('better-auth/plugins/api-key');) turns the new reconciliation red withunaccounted: [apiKey], and leaves every pre-existing parity assertion green — exactly 1 failed / 23 passed.Observed, at the final code:
Prediction met on both halves. The second half is the load-bearing one: all 23 assertions that existed before this change stayed green under the ablation, so nothing already in this file was catching it. Restored afterwards, marker count
grep -c 'api-key'= 0, tree clean at the final commit.Verification, at
6d5d930bf(the final commit)pnpm --filter @objectstack/plugin-auth test— 55 files, 1263 passedpnpm --filter @objectstack/plugin-auth typecheck— cleangit merge-baseat the final commit and run:check:test-source-alias,check:type-source-resolution,check-affected-docs.mjs, plus the test-file conventionscheck:query-options-erasure,check:type-check-coverage,check:engine-double-contract,check:where-matcher, andcheck:nul-bytes/check:cross-package-test-inputs— all exit 0check:type-check-debt(the ratchet) caught a real regression mid-work and it is fixed. plugin-auth excludes**/*.test.tsfrom tsc and carries a shrink-only TEST_DEBT entry of 111. Seeding the source scan fromimport.meta.url(D7's spelling) adds a TS1470 in this CJS-typed package: measured 111 at merge-base, 112 with that seed. The file now uses the package-rootfindUpidiom thatrate-limit-storage-isolation.test.tsestablished here for this exact reason, and the count is back to 111 = recorded. Measured with an equivalent tsc project over the package (the full--re-measureneeds the whole workspace built); it reproduces the ledger's recorded number exactly at merge-base, which is what makes the delta trustworthy.Changeset
Deliberately none, with the
skip-changesetlabel. The diff is a single*.test.tsfile: not intsupentries, not in the package'sfiles, no runtime behaviour, no type surface, nothing published. There is nothing for a release note to describe.Backlinks
#8224 is the independent measurement of the stale premise and remains open — this PR corrects the rationale only in
better-auth-schema-parity.test.ts; the same claim survives in five other files, enumerated in a comment there. #7820 is where the D7 tripwire came from. #3624 / #3653 are the parity holes this gate exists for.Generated by Claude Code
Generated by Claude Code