Skip to content

feat!: migrate to better-auth 1.6#323

Merged
erquhart merged 30 commits into
get-convex:mainfrom
ramonclaudio:feat/better-auth-1.6
Apr 24, 2026
Merged

feat!: migrate to better-auth 1.6#323
erquhart merged 30 commits into
get-convex:mainfrom
ramonclaudio:feat/better-auth-1.6

Conversation

@ramonclaudio

@ramonclaudio ramonclaudio commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Bump peer to >=1.6.7 <1.7.0 and fix five runtime breaks that ship with better-auth 1.6.

Where.mode (v1.6.0). The Where type gained mode?: "sensitive" | "insensitive" and CleanedWhere = Required<Where> forces it onto every where clause the adapter receives. Both adapterWhereValidator and per-table whereValidator reject unknown fields, so every adapter call throws ArgumentValidationError the moment a user bumps the peer.

shouldReturnResponse (v1.6.0, commit 8304f65). to-auth-endpoints.ts now defaults to returning a Response when the context carries a Request (const shouldReturnResponse = context?.asResponse ?? hasRequest). In 1.5.x this was just context?.asResponse. The convex and cross-domain plugins call internal endpoints from hook handlers where ctx.request is real. Without asResponse: false the destructured { token } is undefined, JWT cookies get the literal string "undefined", and cross-domain setSessionCookie crashes on response.session.token. Test suite stays green because test contexts lack a real Request. Production auth silently breaks.

twoFactor.verified (v1.6.2, PR #8711). New verified boolean column on the twoFactor table. enableTwoFactor writes { verified: false }, Convex validator rejects unknown fields, 2FA enrollment crashes.

parseSetCookieHeader (pre-existing, caught in the 1.6.5 audit). src/plugins/cross-domain/client.ts shipped a local copy that split on ", " and shattered Expires=Wed, 21 Oct 2015 07:28:00 GMT into four garbage cookies. better-auth fixed its own splitter in 1.6.0 (#8301) with a lookahead heuristic. Delegate to better-auth/cookies, drop the 34-line local copy. Every cross-domain user with a session cookie carrying Expires hits this (effectively everyone once session.expiresIn kicks in).

./instrumentation (v1.6.6, #9111). packages/core/src/instrumentation/api.ts wires a dynamic import("@opentelemetry/api") into every withSpan call. Convex's V8 isolate rejects bare specifiers synchronously from import() instead of rejecting the returned promise, so the inline .catch() never runs. Every /api/auth/* request 500s with [Better Auth]: Error: Relative import path "@opentelemetry/api" not prefixed with / or ./ or ../. 1.6.5 was the last safe release, 1.6.7 the first with the fix. Not patched in this component: this PR raises the peer floor to 1.6.7 where #9281 routes ./instrumentation to a noop on browser/edge export conditions, which Convex's esbuild picks up via platform: "browser".

Verified against a live Convex deployment running tanstack-convex-starter. Self e2e (4 Playwright), upstream caseInsensitiveTestSuite (12 tests), and three new hook-level regression tests (asResponse convex, asResponse cross-domain, Set-Cookie Expires). Build / typecheck / vitest (183 passed) / lint all clean.

Breaking: ^0.11.x users must bump better-auth to ^1.6.7 in their own package.json.

  • chore(deps)!: peer >=1.6.7 <1.7.0, dev deps ^1.6.7, version 0.12.0. Pulls in GHSA-xr8f-h2gw-9xh6 (authz bypass in @better-auth/oauth-provider, 1.6.5), 1.6.2 OAuth state CSRF fix (#8949), 1.6.3 baseURL hardening for direct auth.api.* calls (#9113, #9131), 1.6.5 $sessionSignal fix for /change-password and /revoke-other-sessions (#9087), 1.6.6 SSRF hardening in @better-auth/oauth-provider (#9226), and 1.6.7 ./instrumentation noop for Convex (#9281)
  • feat(adapter): validators accept mode: "sensitive" | "insensitive". findIndex excludes insensitive clauses from the indexable set (Convex indexes are byte-compared). paginate unique/in fast-paths skip them. filterByWhere case-folds for eq/ne/in/not_in/contains/starts_with/ends_with and normalizes undefined as null for null comparisons
  • fix(plugins): pass asResponse: false, returnHeaders: false, returnStatus: false at all 7 internal .endpoints.* call sites. Regression tests mock the inner plugin to return a Response unless asResponse === false. Pre-set outer flags to true so dropping the override fails the assertion
  • fix(cross-domain): delete local parseSetCookieHeader, re-export from better-auth/cookies. Two regression tests pin behavior on Expires=Wed, 21 Oct 2015 07:28:00 GMT alone and joined with a sibling cookie
  • fix(schema): add verified: v.optional(v.union(v.null(), v.boolean())) to twoFactor table for 1.6.2 compat
  • feat(plugins): expose version on convex, convexClient, crossDomain, crossDomainClient
  • chore: __skipDeprecationWarning: true on both internal oidcProvider instantiations
  • test: wire upstream caseInsensitiveTestSuite (12 tests) into base adapter profile
  • docs(changelog): 0.12.0 entry with migration notes. Migration folds in the checkPasswordINVALID_PASSWORD switch (1.6.1 #8902) and the twoFactor schema regen. Full inherited behavior delta lives in better-auth's own release notes, linked out

Closes #330, closes #324.
Supersedes #328, #326.
Rebased onto main at v0.11.5.

See also #329 (React token cache invalidation on session rotation). Orthogonal, no dependency.

Summary by CodeRabbit

Release Notes

  • Breaking Changes

    • Minimum required better-auth version increased to 1.6.7 or higher. Update your dependency constraints accordingly.
  • New Features

    • Database queries now support case-insensitive mode for flexible string matching.
    • Version information is now accessible and queryable from core plugins.
  • Schema Updates

    • Two-factor authentication schema extended with a new optional verified field to track verification status.
  • Bug Fixes

    • Cross-domain parseSetCookieHeader now handles Set-Cookie headers containing Expires dates with commas.

@vercel

vercel Bot commented Apr 8, 2026

Copy link
Copy Markdown

@ramonclaudio is attempting to deploy a commit to the Convex Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Add support for where.mode ("sensitive" | "insensitive") with case-insensitive filtering and null normalization; bump package and better-auth peer/dev deps to >=1.6.2 <1.7.0; expose VERSION on plugins; pass asResponse: false to internal endpoint calls and suppress oidc-provider deprecation warnings; add related tests and a twoFactor verified field.

Changes

Cohort / File(s) Summary
Release & Versioning
CHANGELOG.md, package.json, src/version.ts
Bump package to 0.12.0; update peer/dev deps to better-auth >=1.6.2 <1.7.0; add exported VERSION constant.
Adapter Where & Filtering
src/client/adapter-utils.ts, src/client/create-api.ts
Accept optional `mode?: "sensitive"
Adapter Tests
src/component/adapterTest.ts
Dynamically import and wire caseInsensitiveTestSuite into base adapter profile.
Plugin Version Exports
src/plugins/convex/client.ts, src/plugins/convex/index.ts, src/plugins/cross-domain/client.ts, src/plugins/cross-domain/index.ts
Expose version: VERSION on returned plugin objects (client and server).
Endpoint Call Behavior & Deprecation Suppression
src/auth-options.ts, src/plugins/convex/index.ts, src/plugins/cross-domain/index.ts
Pass asResponse: false to internal .endpoints.* calls while preserving returnHeaders:false/returnStatus:false; add __skipDeprecationWarning: true for oidc-provider instantiation.
Tests / Regression Coverage
src/plugins/convex/index.test.ts, src/plugins/cross-domain/index.test.ts
Add Vitest mocks and regression tests verifying asResponse: false propagation, JWT cookie after-hook behavior, and unwrapped session handling for cross-domain verifyOneTimeToken.
Schema
src/component/schema.ts
Add optional `verified: null

Sequence Diagram(s)

sequenceDiagram
  rect rgba(200,200,255,0.5)
    participant Client
    participant CrossDomainPlugin
    participant OneTimeTokenPlugin
    participant CookieUtil
  end
  Client->>CrossDomainPlugin: POST /verifyOneTimeToken (outer flags: asResponse=true,...)
  CrossDomainPlugin->>OneTimeTokenPlugin: verifyOneTimeToken(..., asResponse=false, returnHeaders=false, returnStatus=false)
  OneTimeTokenPlugin-->>CrossDomainPlugin: session (raw object)
  CrossDomainPlugin->>CookieUtil: setSessionCookie(session)
  CrossDomainPlugin-->>Client: 200 with session cookie
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped a version up with nimble feet,
Folded cases gently so matches meet.
Plugins wear VERSION like a shiny pin,
Cookies tucked safe after each sign-in.
A little rabbit cheer for changes neat 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat!: migrate to better-auth 1.6' is clear and concise, directly summarizing the primary change: upgrading to better-auth v1.6.
Linked Issues check ✅ Passed The PR addresses all linked objectives: supports where.mode ('sensitive'|'insensitive') [#324, #330], updates peer dependency to >=1.6.2 [#330], adds twoFactor.verified field [#330], implements asResponse: false pattern for endpoints [implied by v1.6 changes], exports version field [additional enhancement], and includes necessary test coverage.
Out of Scope Changes check ✅ Passed The PR includes version export field on plugins (convex/convexClient/crossDomain/crossDomainClient) and __skipDeprecationWarning flag on oidcProvider, which are not explicitly required by linked issues but are reasonable supporting changes aligned with the v1.6 migration.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
package.json (1)

106-107: Align dev and peer better-auth ranges to avoid compatibility drift.

peerDependencies.better-auth is pinned to >=1.6.0 <1.7.0, but devDependencies uses ^1.6.0 for better-auth, @better-auth/core, and @better-auth/test-utils. The caret range can expand beyond 1.6.x and diverge from the peer constraint, creating a mismatch between tested and published compatibility windows.

♻️ Proposed alignment
-    "@better-auth/core": "^1.6.0",
-    "@better-auth/test-utils": "^1.6.0",
+    "@better-auth/core": ">=1.6.0 <1.7.0",
+    "@better-auth/test-utils": ">=1.6.0 <1.7.0",
...
-    "better-auth": "^1.6.0",
+    "better-auth": ">=1.6.0 <1.7.0",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 106 - 107, Dev and peer ranges for better-auth
currently diverge; update the devDependency entries "better-auth",
"@better-auth/core", and "@better-auth/test-utils" to use the exact same range
as peerDependencies (>=1.6.0 <1.7.0) so tests run against the same compatibility
window you publish; locate those package names in package.json and replace their
"^1.6.0" caret ranges with ">=1.6.0 <1.7.0".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@package.json`:
- Around line 106-107: Dev and peer ranges for better-auth currently diverge;
update the devDependency entries "better-auth", "@better-auth/core", and
"@better-auth/test-utils" to use the exact same range as peerDependencies
(>=1.6.0 <1.7.0) so tests run against the same compatibility window you publish;
locate those package names in package.json and replace their "^1.6.0" caret
ranges with ">=1.6.0 <1.7.0".

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4b3cbc19-4e4f-4f4e-b44e-523c9edb8446

📥 Commits

Reviewing files that changed from the base of the PR and between 8587abc and 5fa91dc.

⛔ Files ignored due to path filters (3)
  • examples/next/package-lock.json is excluded by !**/package-lock.json
  • examples/react/package-lock.json is excluded by !**/package-lock.json
  • examples/tanstack/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • CHANGELOG.md
  • package.json
  • src/auth-options.ts
  • src/client/adapter-utils.ts
  • src/client/create-api.ts
  • src/component/adapterTest.ts
  • src/plugins/convex/client.ts
  • src/plugins/convex/index.test.ts
  • src/plugins/convex/index.ts
  • src/plugins/cross-domain/client.ts
  • src/plugins/cross-domain/index.ts
  • src/version.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/plugins/cross-domain/index.test.ts`:
- Around line 121-133: The test's inputCtx currently pre-sets flags (asResponse,
returnHeaders, returnStatus) to false so a server-side override could be masked;
update the test to flip or omit those flags so it actually verifies the endpoint
forces overrides. Specifically, in the inputCtx object used in the test
(properties asResponse, returnHeaders, returnStatus) set them to true (or remove
them entirely) instead of false, and make the same change for the other inputCtx
instance later in the file to ensure the test fails if flag-forwarding/override
logic breaks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1259a0a6-7ead-4085-ac34-80d7d81d51e2

📥 Commits

Reviewing files that changed from the base of the PR and between 5fa91dc and 6c07067.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/client/adapter-utils.ts
  • src/plugins/cross-domain/index.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • CHANGELOG.md
  • src/client/adapter-utils.ts

Comment thread src/plugins/cross-domain/index.test.ts Outdated
@ramonclaudio ramonclaudio force-pushed the feat/better-auth-1.6 branch 3 times, most recently from 4c84ef9 to 0dcaf2e Compare April 21, 2026 15:16
ramonclaudio added a commit to ramonclaudio/tanvex that referenced this pull request Apr 21, 2026
- better-auth peer ^1.6.1 → ^1.6.5 (bun resolves to 1.6.6)
- patches/@Convex-Dev%2Fbetter-auth@0.11.4.patch refreshed from
  get-convex/better-auth#323 HEAD (a7d5b33):
  - peer >=1.6.0 → >=1.6.5 <1.7.0
  - cross-domain parseSetCookieHeader delegated to better-auth/cookies
    (local copy pre-dated better-auth's 1.6.0 lookahead fix, #8301)
  - asResponse: false at 7 internal plugin endpoint sites
  - twoFactor.verified schema (1.6.2 compat)
  - Where.mode case-folding in adapter validators
  - version field on all 4 plugins
  - picks up @better-auth/oauth-provider GHSA-xr8f-h2gw-9xh6 via peer

Verified: bun install clean, tsc --noEmit clean. Lint errors in
sidebar.tsx/profile.tsx are pre-existing and unrelated.
@erquhart

Copy link
Copy Markdown
Member

Thanks for your work on this @ramonclaudio! Digging in soon

@ramonclaudio

Copy link
Copy Markdown
Contributor Author

Thanks @erquhart! Ping me if anything needs to be reworked.

Bumps the `better-auth` peer to `>=1.6.0 <1.7.0` and dev deps to
`^1.6.0`. Bumps package version to `0.12.0`.

Users on `^0.11.x` must also bump `better-auth` to `^1.6.0` in their
own `package.json`. Inherited semantic changes from upstream (freshAge
from `createdAt`, oidc-provider deprecation, account cookie cache
miss, non-blocking scrypt) are documented in `CHANGELOG.md`.
v1.6.0 changed `to-auth-endpoints.ts:108-109` so `shouldReturnResponse`
defaults to true whenever `ctx.request instanceof Request`. The convex
and cross-domain plugins call `jwt.endpoints.getToken`,
`jwt.endpoints.getJwks`, `oidcProvider.endpoints.getOpenIdConfig`, and
`oneTimeToken.endpoints.verifyOneTimeToken` from inside hook handlers
where `ctx.request` is a real `Request`. Without `asResponse: false`
those calls now return `Response` objects instead of the expected
payload. The destructured `{ token }` becomes undefined, JWT cookies
get set to the literal string `"undefined"`, and the existing
`try/catch` at `convex/index.ts:347-351` swallows the failure. Tests
stay green; production auth silently breaks.

Pass `asResponse: false` at all 7 sites (6 in `convex/index.ts`, 1 in
`cross-domain/index.ts`). Add a hook-level regression test that mocks
`jwt.endpoints.getToken` with v1.6.0 wrap-on-Request semantics and
asserts the resulting `set-cookie` header contains a real token, not
`"undefined"`.
…oken

Mirrors the convex jwt cookie regression test. Mocks better-auth/plugins/one-time-token to return a Response unless asResponse === false (matching v1.6.0 wrap-on-Request semantics) and asserts the cross-domain endpoint forwards asResponse: false to the inner call. Without the fix, setSessionCookie would crash on response.session.token being undefined and the OTT verification would fail loudly mid-flow.
Convex indexes are byte-compared so checkUniqueFields cannot enforce case-insensitive uniqueness. Better Auth doesn't currently mark unique fields insensitive, but if it ever does, uniqueness must be enforced via a separate normalized field. One-line guard against a silent future regression.
Adds 4 verified inherited changes downstream Convex users will observe:

- requestPasswordReset now runs originCheck on redirectTo (#8392). Reset URLs not in trustedOrigins return 403.

- checkPassword throws INVALID_PASSWORD instead of CREDENTIAL_ACCOUNT_NOT_FOUND for missing-credential cases (#8902, in 1.6.1). UI catching the old code in deleteUser, changePassword, and 2FA flows must update.

- genericOAuth with verification.storeIdentifier: hashed no longer double-hashes state (#8980).

- auth.$Infer and auth.$ERROR_CODES no longer collapse to any when generic options bleed through (#8981). Strict TS users may see new compile errors.

Also adds a Migration section pointing at npx auth upgrade and notes the ~46% smaller better-auth install. Wires the new cross-domain regression test entry into the bullet list.
Pre-set inputCtx flags to true so the spread inside the handler carries true forward unless production explicitly overrides. Verified by temporarily dropping the override in cross-domain/index.ts:188 and watching the test fail with 'expected true to be false'.

Catches CodeRabbit's gap: with the original false values, a refactor that drops asResponse: false would still produce asResponse: false via the spread and the assertion would pass spuriously.
better-auth 1.6.2 (#8711) adds a verified boolean to the twoFactor
table. Without this field in the component schema, enableTwoFactor
writes fail with ArgumentValidationError.

Also bumps peer and dev deps to >=1.6.2, aligns dev dep ranges with
the peer range, documents the twoFactor migration in the changelog,
and adds returnHeaders/returnStatus assertions to the convex plugin
regression test for parity with the cross-domain test.
Our local parseSetCookieHeader used a naive header.split(", ") which
mis-split Set-Cookie values containing RFC-1123 Expires dates
("Expires=Wed, 21 Oct 2015 07:28:00 GMT") into four garbage entries.
This is exactly the bug better-auth fixed in 1.6.0 (#8301) by adding a
lookahead-aware splitSetCookieHeader in better-auth/cookies.

Delete the local implementation, re-export parseSetCookieHeader from
better-auth/cookies so downstream callers (and the existing test
import) keep working. The CookieAttributes type moves to the
better-auth-shipped shape (value, max-age, expires, domain, path,
secure, httponly, samesite) which aligns with what we already consume
in getSetCookie and crossDomainClient.onSuccess.

Add two regression tests for Set-Cookie headers containing
Expires=... GMT both alone and joined with a sibling cookie.
Prior entries in this CHANGELOG are 3-19 lines. 0.11.0 (the 1.5 migration,
direct analog to this one) is 4 lines. The 0.12.0 section had grown to 98
lines with the version-grouped inherited subsections.

Collapse the inherited section: keep the GHSA + CSRF + baseURL fixes as a
single trailing bullet in the main list, drop the per-version subsections,
point readers to better-auth's release notes for the full delta, and fold
the critical migration items (schema regen for twoFactor.verified and
checkPassword error code change) into the Migration prose.

Result: 23 lines, in range with 0.10.0's historical maximum.
@erquhart

Copy link
Copy Markdown
Member

Blocked on better-auth/better-auth#9340

@gustavovalverde

Copy link
Copy Markdown

@erquhart we made a specific release for this https://github.com/better-auth/better-auth/releases/tag/v1.6.9

1.6.7 and 1.6.8 crash the Convex V8 isolate on every /api/auth/*
request via a bare @opentelemetry/api import from @better-auth/core.
1.6.9 (better-auth/better-auth#9340) routes the import through a
package self-reference so browser/edge export conditions resolve to
a noop under Convex's esbuild.

- bump peer better-auth to >=1.6.9 <1.7.0
- bump dev deps better-auth, @better-auth/core, @better-auth/test-utils
  to ~1.6.9
- refresh root and examples/{next,react,tanstack} lockfiles
- update 0.12 migration guide and framework install snippets
@erquhart

Copy link
Copy Markdown
Member

@gustavovalverde y'all are amazing, thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for better-auth v1.6 Bug: where.mode from better-auth >=1.6 is rejected (ArgumentValidationError)

3 participants