fix(plugin-auth): isolate optional better-auth plugin init failures — one broken federation feature must not 500 all of auth#3087
Merged
Conversation
…rom core auth Follow-up to #3085. In the 15.1.0 incident, @better-auth/oauth-provider threw "Cannot set properties of undefined (setting 'modelName')" during plugin construction, and because the better-auth instance is built lazily per request, that single optional federation feature turned EVERY auth endpoint (sign-up / sign-in / get-session) into a 500 — a blast radius wildly out of proportion to the feature. AuthManager.buildPluginList now constructs plugins in two explicit tiers: - OPTIONAL (organization, admin, phoneNumber, magicLink, genericOAuth, jwt+oauthProvider as one atomic unit, sso, scim, deviceAuthorization): built through addOptionalPlugin(), which on failure skips the feature with one loud actionable console.error, records it in getDegradedAuthFeatures(), and leaves core email/password + session auth up. The feature's endpoints 404 until the operator fixes the underlying error and restarts. - CORE (bearer, twoFactor, haveIBeenPwned, customSession): still fail hard. Skipping these would silently weaken auth — 2FA-enrolled accounts signing in on password alone, opted-in breached-password rejection vanishing, the ADR-0069 authGate (password expiry, enforced MFA) and positions[] disappearing from sessions. Better a hard 500 than fail-open security. The OIDC discovery mount in auth-plugin.ts consults the degraded set and skips advertising /.well-known documents for an IdP whose endpoints did not come up (previously the "Failed to register OIDC discovery routes" path), logging why instead of sending external clients into 404s. Regression tests run the REAL better-auth pipeline (no betterAuth mock, a minimal in-memory IDataEngine behind the genuine ObjectQL adapter) with the exact incident TypeError injected into oauthProvider: sign-up and get-session return 200, the degraded feature is reported, the console.error fires; a throwing CORE plugin (bearer) still rejects; the degraded set resets on a healthy rebuild. Full plugin-auth suite: 457 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 9 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
added a commit
that referenced
this pull request
Jul 17, 2026
…r would (#3100) The 15.1.0 incident (#3091): workspace pnpm overrides kept every in-repo CI job on better-auth 1.7.0-rc.1, but overrides do not ship with published packages, so every fresh `npx create-objectstack` project resolved an untested dependency mix that 500'd all auth endpoints. #3085 added the static gate (override ↔ declaration consistency); this adds the dynamic gate — install the exact bits a user would get and drive the first-run flow. scripts/publish-smoke.sh, two modes sharing one probe set: - pack (default): pnpm-pack all 67 publishable packages (pack applies the same manifest rewrites as publish), scaffold a fresh project outside the workspace with the repo-built create-objectstack, pin @objectstack/* to the tarballs via the project's OWN pnpm-workspace.yaml overrides (nothing inherited from the repo), install, and smoke. A lockfile assertion proves no @objectstack/* leaked to the registry. - registry: npx create-objectstack@latest + npm install against the real registry — catches ^-range ecosystem drift breaking published versions. Probes: anonymous get-session 200 → sign-up 200 → sign-in 200 → session carries the user → REST CRUD round (201/200/200/200) on the scaffolded object as the seeded dev admin → zero error/fatal log lines (ANSI-stripped; catches the #3091 'Failed to register OIDC discovery routes' signature). .github/workflows/publish-smoke.yml: - workflow_run after each Release run — the changesets release PR is pushed with GITHUB_TOKEN, whose events never trigger on:push/pull_request (the release PR has no Actions checks today), so the smoke runs after Release completes, checks out the release-branch head SHA iff an open release PR exists, and reports the verdict back as a commit status on that SHA so it is visible on the release PR. Deliberately not in normal PR CI (too slow). - weekly registry canary (Mon 04:47 UTC) that opens/refreshes an issue on failure. Verified locally: pack mode green on the current tree; a negative run with plugin-auth's declarations reverted to the 15.1.0 mix (^1.6.23 + scim rc.1) goes red via the log scan — core sign-in stays up thanks to the #3087 isolation, and the smoke still catches the broken optional-plugin init. Registry mode green against published 15.1.1. better-sqlite3 stays an optionalDependency, so a failed native build falls back to WASM sqlite (#2229) instead of blocking the smoke. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.
Blast-radius follow-up to #3085
In the 15.1.0 incident,
@better-auth/oauth-providerthrewCannot set properties of undefined (setting 'modelName')during plugin construction. Because the better-auth instance is lazily (re)built per request, that one optional IdP feature failed the whole instance and turned every auth endpoint — sign-up, sign-in, get-session — into a 500. #3085 fixed that specific version mix; this PR fixes the disproportionate failure mode itself.Two explicit tiers in
AuthManager.buildPluginListOPTIONAL — degrade gracefully via a new
addOptionalPlugin()wrapper:organization,admin,phoneNumber,magicLink,genericOAuth,jwt+oauthProvider(one atomic unit — on failure neither lands),sso,scim,deviceAuthorization. On construction failure the feature is skipped with one loud, actionableconsole.error, recorded in a newgetDegradedAuthFeatures(), and its endpoints 404 — while core email/password + session auth keeps serving. The degraded set resets on every rebuild (applyConfigPatch→ healthy restart clears it).CORE — still fail hard (better a hard 500 than fail-open security):
bearer— token auth for every non-cookie API client;twoFactor— skipping it would let 2FA-enrolled accounts sign in on password alone;haveIBeenPwned— operator explicitly opted into breached-password rejection; dropping it silently relaxes policy;customSession— carriespositions[]/isPlatformAdminand the ADR-0069 authGate (password expiry, enforced MFA).The rationale table lives in the
buildPluginListdocstring so the tier of any future plugin is a deliberate decision.OIDC discovery mount is degrade-aware
registerOidcDiscoveryRoutes(the "Failed to register OIDC discovery routes" path from the incident log) now checks the degraded set aftergetAuthInstance(): ifoidcProvideris degraded it logs why and skips mounting/.well-known/{oauth-authorization-server,openid-configuration}, instead of advertising an IdP whose endpoints would all 404.Regression tests (real pipeline, not mocks)
New
auth-manager.optional-plugin-isolation.test.ts— unlike the existing suite it does not mockbetter-auth: the real handler runs against a minimal in-memoryIDataEnginebehind the genuine ObjectQL adapter, and only the two plugin factories get controllable failure switches (viaimportOriginalpassthrough):oauthProvider→ sign-up 200,get-session 200, degraded set[{feature: 'oidcProvider', error: …modelName…}], loudconsole.errorasserted;oauthProviderconstructed, degraded set empty (proves the harness doesn't bypass the plugin);bearer) → request rejects, nothing recorded as degraded (fail-hard semantics pinned);Full plugin-auth suite: 457/457 pass (20 files). Repo-wide
pnpm lint(CI-identical--no-inline-config) clean.Refs: #3085 · GHSA-j8v8-g9cx-5qf4 (why the family pin exists at all)
🤖 Generated with Claude Code