fix(plugin-auth): pin the better-auth family to an exact 1.7.2, and gate the declared range against our own import surface - #16634
Draft
hotlong wants to merge 4 commits into
Draft
Conversation
`@better-auth/core@1.7.3` deleted `createLocalAccountIssuer` and `createOAuthAccountIssuer` from `@better-auth/core/db` — and the whole `account.issuer` column with them — in a PATCH release. `plugin-auth` imports both names statically, so every consumer of published 17.1.0 / 17.2.0 / 17.3.0 resolved 1.7.3 through the declared `^1.7.2` range and could not load the plugin at all. This repo's lockfile held 1.7.2, so every CI job here was green while the artifact it produced was broken on install. The declared range and the workspace override target both become exact 1.7.2, so the version this repo tests is the version a consumer resolves. Resolutions are unchanged: the lockfile already held 1.7.2 for all eleven family members, and `pnpm install --lockfile-only` moves only the echoed specifier strings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pinning the version fixes today's break and leaves the repository exactly as blind as it was: `pnpm-lock.yaml` held `@better-auth/core@1.7.2`, so every job here imported the version that still had `createLocalAccountIssuer` while every consumer resolved `^1.7.2` to 1.7.3 and could not load the plugin at all. `scripts/check-vendor-export-contract.mjs` closes that class. For every governed vendor a publishable package statically imports runtime values from, it asserts the declared range is an EXACT version, that the installed copy IS that version, and that importing each specifier really yields every symbol our shipped source takes from it — so "what CI imported" and "what a consumer resolves" are the same string. `--resolve` measures the same property instead of proving it: it enumerates every registry version the DECLARED range admits, installs each outside this workspace, and fails if any is missing a symbol we import. Wired into validate-deps.yml, which already has the network and runs daily; the offline leg runs in the lint farm on every PR. `check-override-consistency.mjs` structurally cannot cover this — it asks whether the override target is reachable from the declared range, and `^1.7.2` agreed with `^1.7.2` all the way onto the broken version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…16186) The offline leg joins the lint farm (no `paths:` filter — a manifest edit, a lockfile refresh and a new vendor import can each move it). The network leg joins validate-deps.yml, which already installs from the registry and runs daily, so "a vendor published something today" becomes a red here instead of in a customer's install. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…16186) `check:entry-guard` caught a hand-typed `process.argv[1]` comparison: it goes inert through a symlinked checkout, silently, at exit 0 — which for a gate means a green that never ran. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
📓 Docs Drift Check
What this run could not see
Coarse fallback — 12 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #16186
@objectstack/plugin-authpins thebetter-authfamily to an exact1.7.2, and a new gate makes the class stay closed.The defect, restated from the artifact's side
plugin-auth's shippeddist/index.mjsline 825 carries a static named import:and the manifest declared
"@better-auth/core": "^1.7.2".@better-auth/core@1.7.3— a patch — deleted both names. A static ESM named import of a missing export is a link-timeSyntaxError, so the plugin did not load at all. Everything reported on the card follows from that one failure, and every symptom was quiet: the scaffolded project's CLI printed theSyntaxErroras a scrollable oclif warning and carried on, the server printed the ready banner on the broken boot, the four system objects were never created, the seeded admin sign-in never answered, the Console's sign-in form answeredAuth request failed with status 404.Measured here, not restated (all commands below were run):
npm install --no-package-lockofmain's five declared ranges resolves the whole family to 1.7.3 —better-auth,core,oauth-provider,scim,sso,kysely-adapter,telemetryplugin-auth's own builtdist/index.mjsagainst@better-auth/core@1.7.3reproduces the reported error verbatim, and against 1.7.2 it loads:Which of the two candidates this is, plainly
This is the stopgap — the exact pin — and it is labelled as one. It is not the durable fix. The durable fix is #16629, filed with the measurements below.
Reading the installed 1.7.3 first, as asked:
createLocalAccountIssuerwas not renamed and has no replacement. Upstream removed the issuer-scoped account identity outright (better-auth/better-auth#10909). Diffing the two publishedsrctrees:db/schema/account.ts—issueris gone fromaccountSchema;AccountKeyreverts toPick(BaseAccount, "providerId" | "accountId"); both helper functions are deleteddb/get-tables.ts— theaccount.issuercolumn and theunique (issuer, accountId)index are goneoauth2/oauth-provider.ts— theaccountIssueroption is gone;accountIssueroccurs zero times in the whole 1.7.3 packageSo adopting 1.7.3 means dropping
sys_account.issuer— a required platform-object column under a unique index — retiringbackfill-account-issuer.tsand its 44-reference test, retiringaccount-issuer-parity.test.ts, and migrating every existing deployment. That is its own change with its own decision; #16629 states the three options and the costs. Shipping it inside a p0 hotfix would be the wrong trade.All five declared members move, not one
@better-auth/core@1.7.2and@better-auth/kysely-adapter@1.7.3are mutually incompatible in both directions (1.7.2 hascreateLocalAccountIssuerand lackschecksSchema, which 1.7.3'sdb/internaladded; 1.7.3 is the reverse).better-auth@1.7.2declares its own siblings exactly, so pinning the five thatplugin-authdeclares drags all twelve — measured on a cleannpm install --no-package-lock, twelve members at 1.7.2, one copy each.The
pnpm-workspace.yamloverride targets move to the same exact1.7.2in step, so the version this repository tests is the version a consumer resolves. Selectors keep their<2.0.0major boundary, so a future lift stays a target-only edit. In-repo resolutions are unchanged: the lockfile already held 1.7.2 for all eleven overridden members, andpnpm install --lockfile-onlymoved only the echoed specifier strings.This does not change the resolution #16530 is about — that card is already measured against oauth-provider 1.7.2, the version this pin holds.
The regression control —
scripts/check-vendor-export-contract.mjsA version pin alone leaves this repository exactly as blind as it was:
pnpm-lock.yamlheld 1.7.2, so every job here imported the version that still had the symbol, while the artifact it produced was broken on install. The lockfile protected the producer from the defect it was shipping.scripts/check-override-consistency.mjsstructurally cannot cover this. It asks whether the override target is reachable from the declared range — and^1.7.2agreed with^1.7.2perfectly, all the way onto the broken version.The new gate takes the question the other way round. For every governed vendor a publishable package statically imports runtime values from, it asserts:
(2) and (3) are what make (4) a statement about consumers rather than about our lockfile: an exact range resolves to one version everywhere, so the surface just verified is the surface a fresh install gets.
--resolvemeasures the same property instead of proving it, and is the literal reading of the ask: it enumerates every registry version the declared range admits — no lockfile involved — installs each outside this workspace, and fails if any is missing a symbol we import.The offline leg joins the lint farm (no
paths:filter — a manifest edit, a lockfile refresh and a new vendor import can each move it).--resolvejoinsvalidate-deps.yml, which already installs from the registry and runs daily, so "a vendor published something today" becomes a red here rather than in a customer's install. An unreachable registry exits 3 (PREREQUISITE NOT MET), never a green.Scope is a ledger of vendor names carrying their evidence, not a blanket "pin everything": a repo-wide exactness rule is a different decision with a real cost. Type-only imports are out of scope (erased before runtime); test files are out of scope (they fail in
pnpm test, loudly, on the same install).The control was watched failing — twice, for two different reasons
Ablation: mutate
packages/plugins/plugin-auth/package.jsonback tomain's^1.7.2(mutation confirmed on disk before reading anything:exact=5 caret=0before,exact=0 caret=5after; blob7ced877e…to8ab88d1d…), run both legs, restore againstHEAD.Restore leg proved, not assumed: restored blob
7ced877e…equals theHEADblob,git diff HEADempty,git status --porcelainclean. No build/diststep is involved on either leg — the gate reads manifests, source text and installed packages — so there is no stale-artifact direction to rule out.Acceptance, each item watched running
1. A fresh install of the published-shape artifact seeds.
bash scripts/publish-smoke.sh(pack mode) passed end to end: every publishable package packed, a project scaffolded outside this workspace, installed from the tarballs, built, and booted. The scaffolded project carries nobetter-authoverride — only thepeerDependencyRules.allowedVersionsreport suppression, which moves no resolution — so its family came fromplugin-auth's own declared ranges, and all seven checked members resolved 1.7.2.Then, read directly rather than inferred, against that same packed scaffold booted with
--fresh --seed-admin:zero
no such table, zeroSyntaxError, zerodegraded capabilities, zeroAuthPlugin failedlines in the server log.2. The scaffolded project's CLI prints no
SyntaxErrorwarning (carried over from #16373). Run in the packed scaffold:objectstack --version,objectstack validate,objectstack build— all exit 0, noSyntaxError, nodoes not provide an export, noWarning:line at all.3. The regression control fails when pointed at the broken resolution — shown above, both legs.
Is
mainfixed, not merely the pinned buildYes — and the answer is about the declared ranges, which is where the defect lived. Before this branch,
main's five declared ranges resolved to 1.7.3 on a fresh no-lockfile install (measured today); after it they resolve to 1.7.2, and the packed scaffold above boots and seeds. A release cut from this branch does not ship the defect. A release cut frommainbefore it does.One thing this PR deliberately does not do: it makes no claim about already-published 17.1.0 / 17.2.0 / 17.3.0, whose manifests are immutable on npm. Those remain broken for anyone installing them; the consumer-side workaround on the card (a family-wide
pnpm.overridesblock) is still the only remedy for an existing install until a release carrying this ships.Verification
Union run at
b09fe9d8f, on a clean tree (git status --porcelainempty).pnpm --filter '@objectstack/plugin-auth^...' build— exit 0pnpm --filter @objectstack/plugin-auth build— exit 0pnpm --filter @objectstack/plugin-auth test— 102 files, 2158 tests, all passedpnpm --filter @objectstack/plugin-auth typecheck— exit 0 (tsc --noEmit, the examples project, andcheck:test-typecheck: 10 files / 94 errors / 23 pinned signatures held, unchanged)pnpm build --concurrency=2— 73/73 tasks successfulbash scripts/publish-smoke.sh(SMOKE_MODE=pack) — exit 0pnpm lint— the full repo scan, not a narrowing: 6295 files linted, 0 errors, 0 warningsnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackand run at that sha, all exit 0:nul-bytes,agent-test-spelling,entry-guard,parse-guard,watch-hint-literal,doc-authoring,cli-command-ids,cross-package-test-inputs,pnpm-filter-targets,pnpm-acquisition,override-consistency,vendor-export-contract,changeset-gate-self-tests,objectui-changeset,required-contexts,workflow-status-functions,single-claim-paths,pm-dispatch-gates,published-files,manifest-repository-directory,workspace-manifest-cycles,turbo-task-graph,type-check-coverage,type-check-debt,scripts-symbol-anchorspnpm check:vendor-export-contract --resolve— exit 0 against the pinned rangescheck:entry-guardfound a real defect in the first draft of the new gate (a hand-typedprocess.argv[1]comparison, which goes inert through a symlinked checkout at exit 0) and it is fixed inb09fe9d8f.One gate NOT MEASURED here, and it is not a green:
pnpm check:bash32-floorexits 1 on this macOS host, in its own--self-test, because the host'sbashIS 3.2 and has nomapfile, so the harness cannot prove its simulated-3.2 shell removed a builtin the real shell never had. It fails identically (8 of 153 cases) on the untouched sharedmaincheckout, and this diff contains no shell file at all. CI's ubuntu runner is where that gate is measured.Verification-lock disclosure: every heavy command was routed through
scripts/pm/os-verify-lock.sh, which reportedUNLOCKED (declared)on every run —flockis util-linux and this macOS host does not ship it, so the shared lock was never taken and nothing was serialized. Wall-clock numbers from these runs are shared-box readings, not quiet-machine claims.Out of scope, filed rather than fixed here
✓ Server is readyon a degraded boot — the ready signal is independent of the degraded-capabilities warning, and the louder one is wrong #16630 — carried out of bug(plugin-auth): published 17.1.0/17.2.0/17.3.0 float@better-auth/coreto 1.7.3, which droppedcreateLocalAccountIssuer— a freshobjectstack dev --seed-adminnever creates the system tables and never seeds #16186 so it is not buried when that card closes: the server prints✓ Server is readyon a degraded boot, independently of thedegraded capabilitieswarning printed a few lines earlier. Fixing the range does not touch that seam. Recorded on bug(plugin-auth): published 17.1.0/17.2.0/17.3.0 float@better-auth/coreto 1.7.3, which droppedcreateLocalAccountIssuer— a freshobjectstack dev --seed-adminnever creates the system tables and never seeds #16186 by objectui's PM seat, which deliberately left the filing decision to triage; bug(plugin-auth): published 17.1.0/17.2.0/17.3.0 float@better-auth/coreto 1.7.3, which droppedcreateLocalAccountIssuer— a freshobjectstack dev --seed-adminnever creates the system tables and never seeds #16186 is about to close, so it is filed as afinding, unassigned, with no repair proposed. Neither#16629nor#16630is addressed by this PR.🤖 Generated with Claude Code
Generated by Claude Code