diff --git a/.cspell/auth-terms.txt b/.cspell/auth-terms.txt index 2dbe8bbc5cf..b090e3954f4 100644 --- a/.cspell/auth-terms.txt +++ b/.cspell/auth-terms.txt @@ -1,4 +1,5 @@ totp +otps pkce unban siwe diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index ec455a40502..d8f1c5cda1d 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -15,3 +15,6 @@ myapp Neue CCPA CPRA +tmcp +ciba +CIBA \ No newline at end of file diff --git a/.cspell/names.txt b/.cspell/names.txt index 8396b05472f..850b60a990b 100644 --- a/.cspell/names.txt +++ b/.cspell/names.txt @@ -28,4 +28,4 @@ guilhermejansen iamjasonkendrick ejirocodes 0-Sandy -olliethedev +qamarq diff --git a/.cspell/tech-terms.txt b/.cspell/tech-terms.txt index 173a0a673e6..3efd48baca4 100644 --- a/.cspell/tech-terms.txt +++ b/.cspell/tech-terms.txt @@ -48,5 +48,5 @@ SIEM sess reactivations whsec -btst -orms +moonshotai +kimi diff --git a/.cspell/third-party.txt b/.cspell/third-party.txt index 50f31cd047e..b786d320dcb 100644 --- a/.cspell/third-party.txt +++ b/.cspell/third-party.txt @@ -40,3 +40,5 @@ vite electronjs wagmi tldts +headimgurl +encoredev diff --git a/.gitignore b/.gitignore index c194c15dc32..f4a5052209f 100644 --- a/.gitignore +++ b/.gitignore @@ -201,4 +201,4 @@ test-results/ state.txt .cursor/ -docs/.source/index.ts +.claude/worktrees diff --git a/biome.json b/biome.json index 458cde5b82f..bdad724c540 100644 --- a/biome.json +++ b/biome.json @@ -146,8 +146,7 @@ "!**/.output", "!**/.tmp", "!**/tmp-docs-fetch", - "!**/btst/**", - "!scripts/**" + "!**/.claude/worktrees/**" ] } } diff --git a/demo/electron/pnpm-lock.yaml b/demo/electron/pnpm-lock.yaml index d961ae8ac62..074c834c9b5 100644 --- a/demo/electron/pnpm-lock.yaml +++ b/demo/electron/pnpm-lock.yaml @@ -1466,6 +1466,10 @@ packages: resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} engines: {node: '>=14.14'} + fs-extra@11.3.4: + resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} + engines: {node: '>=14.14'} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -2640,7 +2644,7 @@ snapshots: dependencies: cross-dirname: 0.1.0 debug: 4.4.3 - fs-extra: 11.3.3 + fs-extra: 11.3.4 minimist: 1.2.8 postject: 1.0.0-alpha.6 transitivePeerDependencies: @@ -3809,6 +3813,13 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 + fs-extra@11.3.4: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + optional: true + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 diff --git a/demo/nextjs/package.json b/demo/nextjs/package.json index caf8870e8e8..40b289bfbb4 100644 --- a/demo/nextjs/package.json +++ b/demo/nextjs/package.json @@ -70,7 +70,7 @@ "lucide-react": "^0.575.0", "mcp-handler": "^1.0.7", "mysql2": "^3.18.2", - "next": "16.1.6", + "next": "16.2.0", "next-themes": "^0.4.6", "react": "^19.2.4", "react-day-picker": "9.14.0", diff --git a/demo/nextjs/pnpm-lock.yaml b/demo/nextjs/pnpm-lock.yaml index 1658d3d79f3..77b6b6ce48e 100644 --- a/demo/nextjs/pnpm-lock.yaml +++ b/demo/nextjs/pnpm-lock.yaml @@ -163,7 +163,7 @@ importers: version: 12.34.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) geist: specifier: ^1.7.0 - version: 1.7.0(next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + version: 1.7.0(next@16.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) input-otp: specifier: ^1.4.2 version: 1.4.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -175,13 +175,13 @@ importers: version: 0.575.0(react@19.2.4) mcp-handler: specifier: ^1.0.7 - version: 1.0.7(@modelcontextprotocol/sdk@1.27.1(zod@4.3.6))(next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + version: 1.0.7(@modelcontextprotocol/sdk@1.27.1(zod@4.3.6))(next@16.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) mysql2: specifier: ^3.18.2 version: 3.18.2(@types/node@25.3.3) next: - specifier: 16.1.6 - version: 16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 16.2.0 + version: 16.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -263,15 +263,19 @@ packages: resolution: {integrity: sha512-cTlrKttbrRHEw3W+0/I609A2Matj5JQaRvfLtEIGZvlN0RaPi+3ANsMeqAyCAVlH/lUIW2tmtBlSMni74lcXeg==} engines: {node: '>=12'} - '@better-auth/core@1.5.0-beta.20': - resolution: {integrity: sha512-6RmDZ6kU85u1BR/H6OPjV3Z9LrfnCXKVEXFCjOUy0lgNOLRD2QOL59fgu2rpGc85aDxjDi2ddRPrSfvySGnybg==} + '@better-auth/core@1.5.1-beta.3': + resolution: {integrity: sha512-up7xBj99ki9UlTLEsZRfkheYHBLaYIt9JGI9lyAdQcbowTvXnWrk/fkgVcc2YNqRoPvBWCSooEdKL+b5OETQdQ==} peerDependencies: '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 + '@cloudflare/workers-types': '>=4' better-call: 1.3.2 jose: ^6.1.0 kysely: ^0.28.5 nanostores: ^1.0.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true '@better-auth/dash@0.1.6': resolution: {integrity: sha512-5FTWeto6oZVhZRHvHiAEM0D6ifQKbjdVTBUidd9gUn5f+SSa1Zxn4qPtW+iWyJPDLWanXZNXoXGvggtj1TQn9w==} @@ -284,12 +288,12 @@ packages: better-auth: '>=1.4.15' zod: '>=4.1.12' - '@better-auth/sso@1.5.0-beta.20': - resolution: {integrity: sha512-seYOvqpqxYHLVa2gfEg2WtyHNjbNr6epk7dx/Qf/oTHYj4N0psKv5vGJ4KNiuCjh68qJDSe9tXo0nUHXIgToNw==} + '@better-auth/sso@1.5.1-beta.3': + resolution: {integrity: sha512-J59NWigp0uOVmG+gMEJyLSs1lgRYU4hqblrmenk5YlO4jFBn+/iOZfQfBeLOYZLGHrAhy3RiF5TTvLE2oD7lfA==} peerDependencies: - '@better-auth/core': 1.5.0-beta.20 + '@better-auth/core': 1.5.1-beta.3 '@better-auth/utils': 0.3.1 - better-auth: 1.5.0-beta.20 + better-auth: 1.5.1-beta.3 better-call: 1.3.2 '@better-auth/utils@0.3.1': @@ -571,57 +575,57 @@ packages: '@neon-rs/load@0.0.4': resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} - '@next/env@16.1.6': - resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==} + '@next/env@16.2.0': + resolution: {integrity: sha512-OZIbODWWAi0epQRCRjNe1VO45LOFBzgiyqmTLzIqWq6u1wrxKnAyz1HH6tgY/Mc81YzIjRPoYsPAEr4QV4l9TA==} - '@next/swc-darwin-arm64@16.1.6': - resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==} + '@next/swc-darwin-arm64@16.2.0': + resolution: {integrity: sha512-/JZsqKzKt01IFoiLLAzlNqys7qk2F3JkcUhj50zuRhKDQkZNOz9E5N6wAQWprXdsvjRP4lTFj+/+36NSv5AwhQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.1.6': - resolution: {integrity: sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==} + '@next/swc-darwin-x64@16.2.0': + resolution: {integrity: sha512-/hV8erWq4SNlVgglUiW5UmQ5Hwy5EW/AbbXlJCn6zkfKxTy/E/U3V8U1Ocm2YCTUoFgQdoMxRyRMOW5jYy4ygg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.1.6': - resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==} + '@next/swc-linux-arm64-gnu@16.2.0': + resolution: {integrity: sha512-GkjL/Q7MWOwqWR9zoxu1TIHzkOI2l2BHCf7FzeQG87zPgs+6WDh+oC9Sw9ARuuL/FUk6JNCgKRkA6rEQYadUaw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.1.6': - resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} + '@next/swc-linux-arm64-musl@16.2.0': + resolution: {integrity: sha512-1ffhC6KY5qWLg5miMlKJp3dZbXelEfjuXt1qcp5WzSCQy36CV3y+JT7OC1WSFKizGQCDOcQbfkH/IjZP3cdRNA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.1.6': - resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} + '@next/swc-linux-x64-gnu@16.2.0': + resolution: {integrity: sha512-FmbDcZQ8yJRq93EJSL6xaE0KK/Rslraf8fj1uViGxg7K4CKBCRYSubILJPEhjSgZurpcPQq12QNOJQ0DRJl6Hg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.1.6': - resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} + '@next/swc-linux-x64-musl@16.2.0': + resolution: {integrity: sha512-HzjIHVkmGAwRbh/vzvoBWWEbb8BBZPxBvVbDQDvzHSf3D8RP/4vjw7MNLDXFF9Q1WEzeQyEj2zdxBtVAHu5Oyw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.1.6': - resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} + '@next/swc-win32-arm64-msvc@16.2.0': + resolution: {integrity: sha512-UMiFNQf5H7+1ZsZPxEsA064WEuFbRNq/kEXyepbCnSErp4f5iut75dBA8UeerFIG3vDaQNOfCpevnERPp2V+nA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.1.6': - resolution: {integrity: sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==} + '@next/swc-win32-x64-msvc@16.2.0': + resolution: {integrity: sha512-DRrNJKW+/eimrZgdhVN1uvkN1OI4j6Lpefwr44jKQ0YQzztlmOBUUzHuV5GxOMPK3nmodAYElUVCY8ZXo/IWeA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1709,9 +1713,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - better-call@1.1.0-beta.2: - resolution: {integrity: sha512-H0kqGDNT1ixkzboxg3fAYkpJ4HyIhNUID/OyC3fBEfwmHFBLwHpNRwRZYGGOcgcRQNIZtXlJGXaGvnFUhCS44Q==} - better-call@1.3.3: resolution: {integrity: sha512-7IhuPAdH2m1z7ILNiH5DrIkLkA0vvMM8Q3cfqveiglA1p/Ad8yNDOSYMQ4h/rNounLcsJf2GoZemRKLHxE7E5A==} peerDependencies: @@ -1720,6 +1721,14 @@ packages: zod: optional: true + better-call@2.0.0-beta.4: + resolution: {integrity: sha512-C/b/XgTkTbblOtb6E3kuJPU341zRoKcjs+SFR/AVyrnH6VwKWUp2OpomkzKN+v6R4be+/W/NjX1vQbRVjwZHRw==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -1736,9 +1745,9 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} + camelcase@9.0.0: + resolution: {integrity: sha512-TO9xmyXTZ9HUHI8M1OnvExxYB0eYVS/1e5s7IDMTAoIcwUd+aNcFODs6Xk83mobk0velyHFQgA1yIrvYc6wclw==} + engines: {node: '>=20'} caniuse-lite@1.0.30001775: resolution: {integrity: sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==} @@ -1993,11 +2002,11 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fast-xml-builder@1.0.0: - resolution: {integrity: sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==} + fast-xml-builder@1.1.0: + resolution: {integrity: sha512-7mtITW/we2/wTUZqMyBOR2F8xP4CRxMiSEcQxPIqdRWdO2L/HZSOlzoNyghmyDwNB8BDxePooV1ZTJpkOUhdRg==} - fast-xml-parser@5.4.1: - resolution: {integrity: sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==} + fast-xml-parser@5.5.1: + resolution: {integrity: sha512-JTpMz8P5mDoNYzXTmTT/xzWjFiCWi0U+UQTJtrFH9muXsr2RqtXZPbnCW5h2mKsOd4u3XcPWCvDSrnaBPlUcMQ==} hasBin: true fetch-blob@3.2.0: @@ -2338,8 +2347,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@16.1.6: - resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==} + next@16.2.0: + resolution: {integrity: sha512-NLBVrJy1pbV1Yn00L5sU4vFyAHt5XuSjzrNyFnxo6Com0M0KrL6hHM5B99dbqXb2bE9pm4Ow3Zl1xp6HVY9edQ==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -2368,10 +2377,6 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-forge@1.3.3: - resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==} - engines: {node: '>= 6.13.0'} - node-rsa@1.1.1: resolution: {integrity: sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==} @@ -2393,9 +2398,6 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - pako@1.0.11: - resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} - parseley@0.12.1: resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} @@ -2403,6 +2405,10 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + path-expression-matcher@1.1.2: + resolution: {integrity: sha512-LXWqJmcpp2BKOEmgt4CyuESFmBfPuhJlAHKJsFzuJU6CxErWk75BrO+Ni77M9OxHN6dCYKM4vj+21Z6cOL96YQ==} + engines: {node: '>=14.0.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -2577,9 +2583,6 @@ packages: '@react-email/render': optional: true - rou3@0.5.1: - resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==} - rou3@0.7.12: resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} @@ -2590,8 +2593,8 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - samlify@2.10.2: - resolution: {integrity: sha512-y5s1cHwclqwP8h7K2Wj9SfP1q+1S9+jrs5OAegYTLAiuFi7nDvuKqbiXLmUTvYPMpzHcX94wTY2+D604jgTKvA==} + samlify@2.11.0: + resolution: {integrity: sha512-1C9ukjlf0rRsuyqdzztqikdItqa33j9NCCDZgeBiWk0etU6vxNB+SWJKW4Flk07ZlhXeev/twALEKrPhIAyfDg==} scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -2612,9 +2615,6 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} - set-cookie-parser@2.7.2: - resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} - set-cookie-parser@3.0.1: resolution: {integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==} @@ -2768,10 +2768,6 @@ packages: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true - uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - hasBin: true - vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -2827,6 +2823,10 @@ packages: resolution: {integrity: sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA==} engines: {node: '>=0.6.0'} + xpath@0.0.34: + resolution: {integrity: sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==} + engines: {node: '>=0.6.0'} + yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -2848,12 +2848,12 @@ snapshots: escape-html: 1.0.3 xpath: 0.0.32 - '@better-auth/core@1.5.0-beta.20(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.19-beta.1)(better-call@1.1.0-beta.2)(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)': + '@better-auth/core@1.5.1-beta.3(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.19-beta.1)(better-call@2.0.0-beta.4(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)': dependencies: '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.19-beta.1 '@standard-schema/spec': 1.1.0 - better-call: 1.1.0-beta.2 + better-call: 2.0.0-beta.4(zod@4.3.6) jose: 6.1.3 kysely: 0.28.11 nanostores: 1.1.1 @@ -2873,29 +2873,30 @@ snapshots: '@better-auth/infra@0.1.8(@better-auth/utils@0.3.1)(better-auth@..+packages+better-auth)(kysely@0.28.11)(nanostores@1.1.1)(zod@4.3.6)': dependencies: - '@better-auth/core': 1.5.0-beta.20(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.19-beta.1)(better-call@1.1.0-beta.2)(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) - '@better-auth/sso': 1.5.0-beta.20(@better-auth/core@1.5.0-beta.20(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.19-beta.1)(better-call@1.1.0-beta.2)(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@..+packages+better-auth)(better-call@1.1.0-beta.2) + '@better-auth/core': 1.5.1-beta.3(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.19-beta.1)(better-call@2.0.0-beta.4(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/sso': 1.5.1-beta.3(@better-auth/core@1.5.1-beta.3(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.19-beta.1)(better-call@2.0.0-beta.4(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@..+packages+better-auth)(better-call@2.0.0-beta.4(zod@4.3.6)) '@better-fetch/fetch': 1.1.19-beta.1 better-auth: link:../../packages/better-auth - better-call: 1.1.0-beta.2 + better-call: 2.0.0-beta.4(zod@4.3.6) jose: 6.1.3 libphonenumber-js: 1.12.38 zod: 4.3.6 transitivePeerDependencies: - '@better-auth/utils' + - '@cloudflare/workers-types' - kysely - nanostores - '@better-auth/sso@1.5.0-beta.20(@better-auth/core@1.5.0-beta.20(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.19-beta.1)(better-call@1.1.0-beta.2)(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@..+packages+better-auth)(better-call@1.1.0-beta.2)': + '@better-auth/sso@1.5.1-beta.3(@better-auth/core@1.5.1-beta.3(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.19-beta.1)(better-call@2.0.0-beta.4(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@..+packages+better-auth)(better-call@2.0.0-beta.4(zod@4.3.6))': dependencies: - '@better-auth/core': 1.5.0-beta.20(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.19-beta.1)(better-call@1.1.0-beta.2)(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.1-beta.3(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.19-beta.1)(better-call@2.0.0-beta.4(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 better-auth: link:../../packages/better-auth - better-call: 1.1.0-beta.2 - fast-xml-parser: 5.4.1 + better-call: 2.0.0-beta.4(zod@4.3.6) + fast-xml-parser: 5.5.1 jose: 6.1.3 - samlify: 2.10.2 + samlify: 2.11.0 zod: 4.3.6 '@better-auth/utils@0.3.1': {} @@ -3141,30 +3142,30 @@ snapshots: '@neon-rs/load@0.0.4': {} - '@next/env@16.1.6': {} + '@next/env@16.2.0': {} - '@next/swc-darwin-arm64@16.1.6': + '@next/swc-darwin-arm64@16.2.0': optional: true - '@next/swc-darwin-x64@16.1.6': + '@next/swc-darwin-x64@16.2.0': optional: true - '@next/swc-linux-arm64-gnu@16.1.6': + '@next/swc-linux-arm64-gnu@16.2.0': optional: true - '@next/swc-linux-arm64-musl@16.1.6': + '@next/swc-linux-arm64-musl@16.2.0': optional: true - '@next/swc-linux-x64-gnu@16.1.6': + '@next/swc-linux-x64-gnu@16.2.0': optional: true - '@next/swc-linux-x64-musl@16.1.6': + '@next/swc-linux-x64-musl@16.2.0': optional: true - '@next/swc-win32-arm64-msvc@16.1.6': + '@next/swc-win32-arm64-msvc@16.2.0': optional: true - '@next/swc-win32-x64-msvc@16.1.6': + '@next/swc-win32-x64-msvc@16.2.0': optional: true '@number-flow/react@0.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': @@ -4214,14 +4215,16 @@ snapshots: baseline-browser-mapping@2.10.0: {} - better-call@1.1.0-beta.2: + better-call@1.3.3(zod@4.3.6): dependencies: '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 - rou3: 0.5.1 - set-cookie-parser: 2.7.2 + rou3: 0.7.12 + set-cookie-parser: 3.0.1 + optionalDependencies: + zod: 4.3.6 - better-call@1.3.3(zod@4.3.6): + better-call@2.0.0-beta.4(zod@4.3.6): dependencies: '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 @@ -4256,7 +4259,7 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 - camelcase@6.3.0: {} + camelcase@9.0.0: {} caniuse-lite@1.0.30001775: {} @@ -4488,11 +4491,14 @@ snapshots: fast-uri@3.1.0: {} - fast-xml-builder@1.0.0: {} + fast-xml-builder@1.1.0: + dependencies: + path-expression-matcher: 1.1.2 - fast-xml-parser@5.4.1: + fast-xml-parser@5.5.1: dependencies: - fast-xml-builder: 1.0.0 + fast-xml-builder: 1.1.0 + path-expression-matcher: 1.1.2 strnum: 2.2.0 fetch-blob@3.2.0: @@ -4530,9 +4536,9 @@ snapshots: function-bind@1.1.2: {} - geist@1.7.0(next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): + geist@1.7.0(next@16.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): dependencies: - next: 16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) generate-function@2.3.1: dependencies: @@ -4726,14 +4732,14 @@ snapshots: math-intrinsics@1.1.0: {} - mcp-handler@1.0.7(@modelcontextprotocol/sdk@1.27.1(zod@4.3.6))(next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): + mcp-handler@1.0.7(@modelcontextprotocol/sdk@1.27.1(zod@4.3.6))(next@16.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): dependencies: '@modelcontextprotocol/sdk': 1.27.1(zod@4.3.6) chalk: 5.6.2 commander: 11.1.0 redis: 4.7.1 optionalDependencies: - next: 16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) media-typer@1.1.0: {} @@ -4780,9 +4786,9 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@16.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@next/env': 16.1.6 + '@next/env': 16.2.0 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.10.0 caniuse-lite: 1.0.30001775 @@ -4791,14 +4797,14 @@ snapshots: react-dom: 19.2.4(react@19.2.4) styled-jsx: 5.1.6(react@19.2.4) optionalDependencies: - '@next/swc-darwin-arm64': 16.1.6 - '@next/swc-darwin-x64': 16.1.6 - '@next/swc-linux-arm64-gnu': 16.1.6 - '@next/swc-linux-arm64-musl': 16.1.6 - '@next/swc-linux-x64-gnu': 16.1.6 - '@next/swc-linux-x64-musl': 16.1.6 - '@next/swc-win32-arm64-msvc': 16.1.6 - '@next/swc-win32-x64-msvc': 16.1.6 + '@next/swc-darwin-arm64': 16.2.0 + '@next/swc-darwin-x64': 16.2.0 + '@next/swc-linux-arm64-gnu': 16.2.0 + '@next/swc-linux-arm64-musl': 16.2.0 + '@next/swc-linux-x64-gnu': 16.2.0 + '@next/swc-linux-x64-musl': 16.2.0 + '@next/swc-win32-arm64-msvc': 16.2.0 + '@next/swc-win32-x64-msvc': 16.2.0 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -4812,8 +4818,6 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - node-forge@1.3.3: {} - node-rsa@1.1.1: dependencies: asn1: 0.2.6 @@ -4834,8 +4838,6 @@ snapshots: dependencies: wrappy: 1.0.2 - pako@1.0.11: {} - parseley@0.12.1: dependencies: leac: 0.6.0 @@ -4843,6 +4845,8 @@ snapshots: parseurl@1.3.3: {} + path-expression-matcher@1.1.2: {} + path-key@3.1.1: {} path-to-regexp@8.3.0: {} @@ -5013,8 +5017,6 @@ snapshots: optionalDependencies: '@react-email/render': 2.0.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - rou3@0.5.1: {} - rou3@0.7.12: {} router@2.2.0: @@ -5029,19 +5031,16 @@ snapshots: safer-buffer@2.1.2: {} - samlify@2.10.2: + samlify@2.11.0: dependencies: '@authenio/xml-encryption': 2.0.2 '@xmldom/xmldom': 0.8.11 - camelcase: 6.3.0 - node-forge: 1.3.3 + camelcase: 9.0.0 node-rsa: 1.1.1 - pako: 1.0.11 - uuid: 8.3.2 xml: 1.0.1 xml-crypto: 6.1.2 xml-escape: 1.1.0 - xpath: 0.0.32 + xpath: 0.0.34 scheduler@0.27.0: {} @@ -5077,8 +5076,6 @@ snapshots: transitivePeerDependencies: - supports-color - set-cookie-parser@2.7.2: {} - set-cookie-parser@3.0.1: {} setprototypeof@1.2.0: {} @@ -5234,8 +5231,6 @@ snapshots: uuid@10.0.0: {} - uuid@8.3.2: {} - vary@1.1.2: {} vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -5288,6 +5283,8 @@ snapshots: xpath@0.0.33: {} + xpath@0.0.34: {} + yallist@4.0.0: {} zod-to-json-schema@3.25.1(zod@4.3.6): diff --git a/demo/stateless/package.json b/demo/stateless/package.json index 5dadc00afa7..6a08d800ef9 100644 --- a/demo/stateless/package.json +++ b/demo/stateless/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "better-auth": "link:../../packages/better-auth", - "next": "16.1.6", + "next": "16.2.0", "react": "^19.2.4", "react-dom": "^19.2.4" }, diff --git a/demo/stateless/pnpm-lock.yaml b/demo/stateless/pnpm-lock.yaml index d0b630bb3c4..ce201a2ec3b 100644 --- a/demo/stateless/pnpm-lock.yaml +++ b/demo/stateless/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: link:../../packages/better-auth version: link:../../packages/better-auth next: - specifier: 16.1.6 - version: 16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 16.2.0 + version: 16.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: ^19.2.4 version: 19.2.4 @@ -218,57 +218,57 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@next/env@16.1.6': - resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==} + '@next/env@16.2.0': + resolution: {integrity: sha512-OZIbODWWAi0epQRCRjNe1VO45LOFBzgiyqmTLzIqWq6u1wrxKnAyz1HH6tgY/Mc81YzIjRPoYsPAEr4QV4l9TA==} - '@next/swc-darwin-arm64@16.1.6': - resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==} + '@next/swc-darwin-arm64@16.2.0': + resolution: {integrity: sha512-/JZsqKzKt01IFoiLLAzlNqys7qk2F3JkcUhj50zuRhKDQkZNOz9E5N6wAQWprXdsvjRP4lTFj+/+36NSv5AwhQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.1.6': - resolution: {integrity: sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==} + '@next/swc-darwin-x64@16.2.0': + resolution: {integrity: sha512-/hV8erWq4SNlVgglUiW5UmQ5Hwy5EW/AbbXlJCn6zkfKxTy/E/U3V8U1Ocm2YCTUoFgQdoMxRyRMOW5jYy4ygg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.1.6': - resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==} + '@next/swc-linux-arm64-gnu@16.2.0': + resolution: {integrity: sha512-GkjL/Q7MWOwqWR9zoxu1TIHzkOI2l2BHCf7FzeQG87zPgs+6WDh+oC9Sw9ARuuL/FUk6JNCgKRkA6rEQYadUaw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.1.6': - resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} + '@next/swc-linux-arm64-musl@16.2.0': + resolution: {integrity: sha512-1ffhC6KY5qWLg5miMlKJp3dZbXelEfjuXt1qcp5WzSCQy36CV3y+JT7OC1WSFKizGQCDOcQbfkH/IjZP3cdRNA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.1.6': - resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} + '@next/swc-linux-x64-gnu@16.2.0': + resolution: {integrity: sha512-FmbDcZQ8yJRq93EJSL6xaE0KK/Rslraf8fj1uViGxg7K4CKBCRYSubILJPEhjSgZurpcPQq12QNOJQ0DRJl6Hg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.1.6': - resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} + '@next/swc-linux-x64-musl@16.2.0': + resolution: {integrity: sha512-HzjIHVkmGAwRbh/vzvoBWWEbb8BBZPxBvVbDQDvzHSf3D8RP/4vjw7MNLDXFF9Q1WEzeQyEj2zdxBtVAHu5Oyw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.1.6': - resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} + '@next/swc-win32-arm64-msvc@16.2.0': + resolution: {integrity: sha512-UMiFNQf5H7+1ZsZPxEsA064WEuFbRNq/kEXyepbCnSErp4f5iut75dBA8UeerFIG3vDaQNOfCpevnERPp2V+nA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.1.6': - resolution: {integrity: sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==} + '@next/swc-win32-x64-msvc@16.2.0': + resolution: {integrity: sha512-DRrNJKW+/eimrZgdhVN1uvkN1OI4j6Lpefwr44jKQ0YQzztlmOBUUzHuV5GxOMPK3nmodAYElUVCY8ZXo/IWeA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -487,8 +487,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - next@16.1.6: - resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==} + next@16.2.0: + resolution: {integrity: sha512-NLBVrJy1pbV1Yn00L5sU4vFyAHt5XuSjzrNyFnxo6Com0M0KrL6hHM5B99dbqXb2bE9pm4Ow3Zl1xp6HVY9edQ==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -697,30 +697,30 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@next/env@16.1.6': {} + '@next/env@16.2.0': {} - '@next/swc-darwin-arm64@16.1.6': + '@next/swc-darwin-arm64@16.2.0': optional: true - '@next/swc-darwin-x64@16.1.6': + '@next/swc-darwin-x64@16.2.0': optional: true - '@next/swc-linux-arm64-gnu@16.1.6': + '@next/swc-linux-arm64-gnu@16.2.0': optional: true - '@next/swc-linux-arm64-musl@16.1.6': + '@next/swc-linux-arm64-musl@16.2.0': optional: true - '@next/swc-linux-x64-gnu@16.1.6': + '@next/swc-linux-x64-gnu@16.2.0': optional: true - '@next/swc-linux-x64-musl@16.1.6': + '@next/swc-linux-x64-musl@16.2.0': optional: true - '@next/swc-win32-arm64-msvc@16.1.6': + '@next/swc-win32-arm64-msvc@16.2.0': optional: true - '@next/swc-win32-x64-msvc@16.1.6': + '@next/swc-win32-x64-msvc@16.2.0': optional: true '@swc/helpers@0.5.15': @@ -878,9 +878,9 @@ snapshots: nanoid@3.3.11: {} - next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@16.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@next/env': 16.1.6 + '@next/env': 16.2.0 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.10.0 caniuse-lite: 1.0.30001775 @@ -889,14 +889,14 @@ snapshots: react-dom: 19.2.4(react@19.2.4) styled-jsx: 5.1.6(react@19.2.4) optionalDependencies: - '@next/swc-darwin-arm64': 16.1.6 - '@next/swc-darwin-x64': 16.1.6 - '@next/swc-linux-arm64-gnu': 16.1.6 - '@next/swc-linux-arm64-musl': 16.1.6 - '@next/swc-linux-x64-gnu': 16.1.6 - '@next/swc-linux-x64-musl': 16.1.6 - '@next/swc-win32-arm64-msvc': 16.1.6 - '@next/swc-win32-x64-msvc': 16.1.6 + '@next/swc-darwin-arm64': 16.2.0 + '@next/swc-darwin-x64': 16.2.0 + '@next/swc-linux-arm64-gnu': 16.2.0 + '@next/swc-linux-arm64-musl': 16.2.0 + '@next/swc-linux-x64-gnu': 16.2.0 + '@next/swc-linux-x64-musl': 16.2.0 + '@next/swc-win32-arm64-msvc': 16.2.0 + '@next/swc-win32-x64-msvc': 16.2.0 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' diff --git a/docs/content/blogs/1-5.mdx b/docs/content/blogs/1-5.mdx index 7cb9ccdf564..0c9af5af542 100644 --- a/docs/content/blogs/1-5.mdx +++ b/docs/content/blogs/1-5.mdx @@ -269,6 +269,87 @@ export const auth = betterAuth({ - **Provider CRUD endpoints**: List, get, update, and delete SSO providers via API. - **Shared OIDC redirect URI**: Single redirect URI for all OIDC providers. +--- + +### API Key Plugin + +The API Key plugin has received some major improvements. + +We've extracted the plugin into it's own package, install it via: + +```package-install +npm install @better-auth/api-key +``` + +And update your imports! + +```diff title="auth.ts" +- import { apiKey } from "better-auth/plugins"; ++ import { apiKey } from "@better-auth/api-key"; +``` + +```diff title="auth-client.ts" +- import { apiKeyClient } from "better-auth/client/plugins"; ++ import { apiKeyClient } from "@better-auth/api-key/client"; +``` + +#### Organization owned API Keys + +Long awaited support for organization-owned API keys has been added. +You can now create API keys that are owned by organizations instead of users. + +```ts +import { apiKey } from "@better-auth/api-key"; + +export const auth = betterAuth({ + plugins: [apiKey({ references: "organization" })], +}); +``` + +And creating a organization-owned API key: + +```ts +import { auth } from "@/lib/auth"; + +const apiKey = await auth.api.createApiKey({ + body: { organizationId: "org_123" }, +}); +``` + +#### Multiple configurations for API Keys + +You can now support multiple api-key use-cases within your app! + +```ts +export const auth = betterAuth({ + plugins: [ + apiKey([ + { + configId: "user-keys", + prefix: "usr_" + }, + { + configId: "org-keys", + prefix: "org_", + references: "organization" + } + ]) + ] +}); +``` + +To create an API Key for a given configuration, call: + +```ts +const key = auth.api.createApiKey({ + body: { + configId: "user-keys", + //... + } +}); +``` + + --- ### Unified Before & After Hooks @@ -631,7 +712,7 @@ The `/forget-password/email-otp` endpoint has been removed. Use the standard pas ### Adapter Imports -The `better-auth/adapters/test` export has been removed. Use the `testUtils` plugin instead. +The `better-auth/adapters/test` export has been removed. Use `testAdapter` and `createTestSuite` from `@better-auth/test-utils/adapter` instead. See the [Create a Database Adapter](/docs/guides/create-a-db-adapter#test-your-adapter) guide for details. ### API Key Plugin Moved to `@better-auth/api-key` @@ -682,6 +763,10 @@ export const auth = betterAuth({ + const configId = apiKey.configId; ``` +**Permission changes:** +The `updateApiKey` endpoint now requires at least a `userId` parameter or headers to be passed on the server side. +If you didn't provide this in previous version, you're now required to pass them. + --- ## 🛠 Developer Changes @@ -694,7 +779,6 @@ All previously deprecated APIs have been removed. This includes deprecated adapt | Removed | Replacement | | --- | --- | -| `createAdapter` | `createAdapterFactory` | | `Adapter` | `DBAdapter` | | `TransactionAdapter` | `DBTransactionAdapter` | | `Store` (client) | `ClientStore` | diff --git a/docs/content/docs/authentication/wechat.mdx b/docs/content/docs/authentication/wechat.mdx new file mode 100644 index 00000000000..8c607979db5 --- /dev/null +++ b/docs/content/docs/authentication/wechat.mdx @@ -0,0 +1,77 @@ +--- +title: WeChat +description: WeChat provider setup and usage. +--- + + + + ### Get your WeChat Credentials + To use WeChat sign in, you need to register a website application on the [WeChat Open Platform](https://open.weixin.qq.com/), set the `Authorization Callback Domain` to the better auth domain and get your App ID and App Secret. + + + + ### Configure the provider + To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. + + ```ts title="auth.ts" + import { betterAuth } from "better-auth" + + export const auth = betterAuth({ + socialProviders: { + wechat: { // [!code highlight] + clientId: process.env.WECHAT_CLIENT_ID, // [!code highlight] + clientSecret: process.env.WECHAT_CLIENT_SECRET, // [!code highlight] + }, // [!code highlight] + }, + }) + ``` + + #### Optional Configuration + + You can customize the WeChat provider with additional options: + + ```ts title="auth.ts" + export const auth = betterAuth({ + socialProviders: { + wechat: { + clientId: process.env.WECHAT_CLIENT_ID, + clientSecret: process.env.WECHAT_CLIENT_SECRET, + // Optional: Set UI language for the WeChat login page + lang: "cn", // or "en" for English + // Optional: Use custom scopes + scope: [], // "snsapi_login" for web QR code login. + }, + }, + }) + ``` + + + + ### Sign In with WeChat + To sign in with WeChat, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: + - `provider`: The provider to use. It should be set to `wechat`. + + ```ts title="auth-client.ts" + import { createAuthClient } from "better-auth/client" + const authClient = createAuthClient() + + const signIn = async () => { + const data = await authClient.signIn.social({ + provider: "wechat" + }) + } + ``` + + + + +## Usage + +### Platform Support + +WeChat provider currently supports the Website Application platform type, which enables WeChat QR code login for web applications. + +### Development Notes + +- The redirect URL domain must match the domain configured in the WeChat Open Platform + diff --git a/docs/content/docs/concepts/api.mdx b/docs/content/docs/concepts/api.mdx index e83d55c0ad4..fbaaaab5c9c 100644 --- a/docs/content/docs/concepts/api.mdx +++ b/docs/content/docs/concepts/api.mdx @@ -1,6 +1,6 @@ --- title: API -description: Better Auth API. +description: Learn how to call Better Auth API endpoints on the server, pass body, headers, and query parameters, retrieve response headers, and handle errors. --- When you create a new Better Auth instance, it provides you with an `api` object. This object exposes every endpoint that exists in your Better Auth instance. And you can use this to interact with Better Auth server side. @@ -81,7 +81,7 @@ const { headers, response } = await auth.api.signUpEmail({ The `headers` will be a `Headers` object, which you can use to get the cookies or the headers. ```ts -const cookies = headers.get("set-cookie"); +const cookies = headers.getSetCookie(); const headers = headers.get("x-custom-header"); ``` diff --git a/docs/content/docs/concepts/cli.mdx b/docs/content/docs/concepts/cli.mdx index 022eefba4dc..313aa409e61 100644 --- a/docs/content/docs/concepts/cli.mdx +++ b/docs/content/docs/concepts/cli.mdx @@ -1,6 +1,6 @@ --- title: CLI -description: Built-in CLI for managing your project. +description: Learn about the Better Auth CLI commands for generating and migrating database schemas, initializing projects, generating secret keys, and gathering diagnostic info. --- Better Auth comes with a built-in CLI to help you manage the database schemas, initialize your project, generate a secret key for your application, and gather diagnostic information about your setup. diff --git a/docs/content/docs/concepts/client.mdx b/docs/content/docs/concepts/client.mdx index 975f8746bae..d2967f3fb29 100644 --- a/docs/content/docs/concepts/client.mdx +++ b/docs/content/docs/concepts/client.mdx @@ -1,6 +1,6 @@ --- title: Client -description: Better Auth client library for authentication. +description: Learn how to set up the Better Auth client for React, Vue, Svelte, and other frameworks, use hooks, configure fetch options, handle errors, and extend with client plugins. --- Better Auth offers a client library compatible with popular frontend frameworks like React, Vue, Svelte, and more. This client library includes a set of functions for interacting with the Better Auth server. Each framework's client library is built on top of a core client library that is framework-agnostic, so that all methods and hooks are consistently available across all client libraries. diff --git a/docs/content/docs/concepts/cookies.mdx b/docs/content/docs/concepts/cookies.mdx index b183baede8d..617bc631c85 100644 --- a/docs/content/docs/concepts/cookies.mdx +++ b/docs/content/docs/concepts/cookies.mdx @@ -1,6 +1,6 @@ --- title: Cookies -description: Learn how cookies are used in Better Auth. +description: Learn how Better Auth uses cookies, including cookie prefixes, custom cookie attributes, cross-subdomain sharing, secure cookies, and handling Safari ITP with proxies. --- Cookies are used to store data such as session tokens, session data, OAuth state, and more. All cookies are signed using the `secret` key provided in the auth options or the `BETTER_AUTH_SECRET` environment variable. If you use [versioned secrets](/docs/reference/options#secrets) for rotation, encrypted cookie data (such as JWE session caches) will automatically use the current key and remain decryptable with previous keys. diff --git a/docs/content/docs/concepts/database.mdx b/docs/content/docs/concepts/database.mdx index 8c7e0ec9db6..e8a51173bc1 100644 --- a/docs/content/docs/concepts/database.mdx +++ b/docs/content/docs/concepts/database.mdx @@ -1,6 +1,6 @@ --- title: Database -description: Learn how to use a database with Better Auth. +description: Learn about database adapters, migrations, secondary storage with Redis, core schema (user, session, account, verification), custom tables, extending schemas, ID generation, database hooks, and plugin schemas. --- ## Adapters @@ -124,7 +124,7 @@ If you're using Cloudflare D1 with Drizzle or Prisma, use [`cloudflare:workers`] ## Secondary Storage -Secondary storage in Better Auth allows you to use key-value stores for managing session data, rate limiting counters, etc. This can be useful when you want to offload the storage of intensive records to a high performance storage or even RAM. +Secondary storage in Better Auth allows you to use key-value stores for managing session data, verification records, rate limiting counters, and other short-lived auth data. This can be useful when you want to offload the storage of intensive records to a high performance storage or even RAM. ### Implementation @@ -214,6 +214,104 @@ export const auth = betterAuth({ This implementation allows Better Auth to use Redis for storing session data and rate limiting counters. You can also add prefixes to the keys names. +**Example: Upstash Redis Implementation** + +Here's an example using Upstash Redis. First, install the Upstash Redis client: + +```bash +npm install @upstash/redis +``` + +Then, create a new Redis client: + +```typescript +// src/lib/redis/index.ts + +import { Redis } from "@upstash/redis"; + +export const redis = Redis.fromEnv(); +``` + +Don't forget to set the `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` environment variables. Also, [see the Upstash documentation](https://upstash.com/docs/redis/howto/connectwithupstashredis) for more information on how to use Upstash Redis with Node.js. + +After that, we can create a secondary storage implementation: + +```typescript +// src/lib/auth/adapters/redis-secondary-storage.ts + +import { SecondaryStorage } from "better-auth"; +import { redis } from "~/lib/redis"; + +export const redisSecondaryStorage: SecondaryStorage = { + async get(key: string) { + try { + const value = await redis.get(key); + + // Handle different return types from Redis + if (value === null || value === undefined) { + return null; + } + + // If it's already a string, return it + if (typeof value === "string") { + return value; + } + + // If it's an object, stringify it + if (typeof value === "object") { + return JSON.stringify(value); + } + + // Convert to string for any other type + return String(value); + } catch (error) { + console.error("Redis get error:", error); + return null; + } + }, + + async set(key: string, value: string, ttl?: number) { + try { + // Ensure value is a string + const stringValue = + typeof value === "string" ? value : JSON.stringify(value); + + if (ttl) { + // Set with TTL in seconds + await redis.setex(key, ttl, stringValue); + } else { + // Set without TTL + await redis.set(key, stringValue); + } + } catch (error) { + console.error("Redis set error:", error); + throw error; + } + }, + + async delete(key: string) { + try { + await redis.del(key); + } catch (error) { + console.error("Redis delete error:", error); + throw error; + } + }, +}; +``` + +Finally, we can pass the implementation to the `betterAuth` function. + +```typescript +import { betterAuth } from "better-auth"; +import { redisSecondaryStorage } from "~/lib/auth/adapters/redis-secondary-storage"; + +export const auth = betterAuth({ + // ... other options + secondaryStorage: redisSecondaryStorage, +}); +``` + ## Core Schema Better Auth requires the following tables to be present in the database. The types are in `typescript` format. You can use corresponding types in your database. @@ -239,6 +337,7 @@ Table Name: `user` name: "email", type: "string", description: "User's email address for communication and login", + isUnique: true, }, { name: "emailVerified", @@ -860,7 +959,10 @@ export const auth = betterAuth({ before: async (data, ctx) => { // You can access the session from the context object. if (ctx.context.session) { - console.log("User update initiated by:", ctx.context.session.userId); + console.log( + "User update initiated by:", + ctx.context.session.userId + ); } return { data }; }, diff --git a/docs/content/docs/concepts/email.mdx b/docs/content/docs/concepts/email.mdx index 854c623a3d7..58e45713026 100644 --- a/docs/content/docs/concepts/email.mdx +++ b/docs/content/docs/concepts/email.mdx @@ -1,6 +1,6 @@ --- title: Email -description: Learn how to use email with Better Auth. +description: Learn how to set up email verification, require verified emails for sign-in, auto sign-in after verification, handle post-verification callbacks, and implement password reset emails. --- Email is a key part of Better Auth, required for all users regardless of their authentication method. Better Auth provides email and password authentication out of the box, and a lot of utilities to help you manage email verification, password reset, and more. diff --git a/docs/content/docs/concepts/hooks.mdx b/docs/content/docs/concepts/hooks.mdx index c107f35d7ac..acde7edb808 100644 --- a/docs/content/docs/concepts/hooks.mdx +++ b/docs/content/docs/concepts/hooks.mdx @@ -1,6 +1,6 @@ --- title: Hooks -description: Better Auth Hooks let you customize BetterAuth's behavior +description: Learn how to use before and after hooks to customize endpoint behavior, modify requests and responses, handle cookies, throw errors, access auth context, and run background tasks. --- Hooks in Better Auth let you "hook into" the lifecycle and execute custom logic. They provide a way to customize Better Auth's behavior without writing a full plugin. diff --git a/docs/content/docs/concepts/oauth.mdx b/docs/content/docs/concepts/oauth.mdx index 70d9e2f25c6..d61e0aeacb9 100644 --- a/docs/content/docs/concepts/oauth.mdx +++ b/docs/content/docs/concepts/oauth.mdx @@ -1,6 +1,6 @@ --- title: OAuth -description: How Better Auth handles OAuth +description: Learn how to configure social OAuth providers, sign in and link accounts, request scopes, pass additional data, refresh access tokens, map profiles, and customize provider options. --- Better Auth comes with built-in support for OAuth 2.0 and OpenID Connect. This allows you to authenticate users via popular OAuth providers like Google, Facebook, GitHub, and more. diff --git a/docs/content/docs/concepts/plugins.mdx b/docs/content/docs/concepts/plugins.mdx index 948bf4ca630..abeed842178 100644 --- a/docs/content/docs/concepts/plugins.mdx +++ b/docs/content/docs/concepts/plugins.mdx @@ -1,6 +1,6 @@ --- title: Plugins -description: Learn how to use plugins with Better Auth. +description: Learn how to use and create Better Auth plugins, including defining endpoints, schemas, hooks, middleware, rate limits, trusted origins, and building client plugins with custom actions and atoms. --- Plugins are a key part of Better Auth, they let you extend the base functionalities. You can use them to add new authentication methods, features, or customize behaviors. diff --git a/docs/content/docs/concepts/rate-limit.mdx b/docs/content/docs/concepts/rate-limit.mdx index 9c29f32bb9f..663aacff98b 100644 --- a/docs/content/docs/concepts/rate-limit.mdx +++ b/docs/content/docs/concepts/rate-limit.mdx @@ -1,6 +1,6 @@ --- title: Rate Limit -description: How to limit the number of requests a user can make to the server in a given time period. +description: Learn how to configure rate limiting in Better Auth, including IP address detection, IPv6 support, custom rate limit windows, storage backends, error handling, and per-endpoint rules. --- Better Auth includes a built-in rate limiter to help manage traffic and prevent abuse. By default, in production mode, the rate limiter is set to: @@ -291,18 +291,19 @@ Table Name: `rateLimit` isPrimaryKey: true }, { - name: "key", - type: "string", + name: "key", + type: "string", description: "Unique identifier for each rate limit key", + isUnique: true, }, { - name: "count", - type: "integer", - description: "Time window in seconds" + name: "count", + type: "integer", + description: "Number of requests made in the current window" }, - { - name: "lastRequest", - type: "bigint", - description: "Max requests in the window" + { + name: "lastRequest", + type: "bigint", + description: "Timestamp of the last request (epoch ms)" }]} /> diff --git a/docs/content/docs/concepts/session-management.mdx b/docs/content/docs/concepts/session-management.mdx index 4115a53aa96..0cfdaee1eb9 100644 --- a/docs/content/docs/concepts/session-management.mdx +++ b/docs/content/docs/concepts/session-management.mdx @@ -1,6 +1,6 @@ --- title: Session Management -description: Better Auth session management. +description: Learn about session management in Better Auth, including session expiration, freshness, cookie caching strategies, secondary storage, stateless sessions, and customizing session responses. --- Better Auth manages session using a traditional cookie-based session management. The session is stored in a cookie and is sent to the server on every request. The server then verifies the session and returns the user data if the session is valid. diff --git a/docs/content/docs/concepts/typescript.mdx b/docs/content/docs/concepts/typescript.mdx index b464b113810..7ccc3135941 100644 --- a/docs/content/docs/concepts/typescript.mdx +++ b/docs/content/docs/concepts/typescript.mdx @@ -1,6 +1,6 @@ --- title: TypeScript -description: Better Auth TypeScript integration. +description: Learn about TypeScript configuration for Better Auth, including strict mode, inferring types for sessions and users, defining additional fields, and inferring additional fields on the client. --- Better Auth is designed to be type-safe. Both the client and server are built with TypeScript, allowing you to easily infer types. diff --git a/docs/content/docs/concepts/users-accounts.mdx b/docs/content/docs/concepts/users-accounts.mdx index 0851316bf9d..af699e3204c 100644 --- a/docs/content/docs/concepts/users-accounts.mdx +++ b/docs/content/docs/concepts/users-accounts.mdx @@ -1,6 +1,6 @@ --- title: User & Accounts -description: User and account management. +description: Learn how to manage users and accounts, including updating user info, changing emails and passwords, deleting users with verification, token encryption, and account linking and unlinking. --- Beyond authenticating users, Better Auth also provides a set of methods to manage users. This includes, updating user information, changing passwords, and more. diff --git a/docs/content/docs/guides/create-a-db-adapter.mdx b/docs/content/docs/guides/create-a-db-adapter.mdx index 924f9ef4c96..739d5eca471 100644 --- a/docs/content/docs/guides/create-a-db-adapter.mdx +++ b/docs/content/docs/guides/create-a-db-adapter.mdx @@ -359,60 +359,50 @@ createSchema: async ({ file, tables }) => { ## Test your adapter -We've provided a test suite that you can use to test your adapter. It requires you to use `vitest`. +We've provided a test suite via the `@better-auth/test-utils` package that you can use to test your adapter. It requires you to use `vitest`. -```ts title="my-adapter.test.ts" -import { expect, test, describe } from "vitest"; -import { runAdapterTest } from "better-auth/adapters/test"; -import { myAdapter } from "./my-adapter"; - -describe("My Adapter Tests", async () => { - afterAll(async () => { - // Run DB cleanup here... - }); - const adapter = myAdapter({ - debugLogs: { - // If your adapter config allows passing in debug logs, then pass this here. - isRunningAdapterTests: true, // This is our super secret flag to let us know to only log debug logs if a test fails. - }, - }); +First, install the test utilities: - runAdapterTest({ - getAdapter: async (betterAuthOptions = {}) => { - return adapter(betterAuthOptions); - }, - }); -}); +```bash +npm install -D @better-auth/test-utils ``` -### Numeric ID tests +Then create a test file using `testAdapter` and `createTestSuite`: -If your database supports numeric IDs, then you should run this test as well: - -```ts title="my-adapter.number-id.test.ts" -import { expect, test, describe } from "vitest"; -import { runNumberIdAdapterTest } from "better-auth/adapters/test"; +```ts title="my-adapter.test.ts" +import { testAdapter, createTestSuite } from "@better-auth/test-utils/adapter"; import { myAdapter } from "./my-adapter"; -describe("My Adapter Numeric ID Tests", async () => { - afterAll(async () => { - // Run DB cleanup here... - }); - const adapter = myAdapter({ - debugLogs: { - // If your adapter config allows passing in debug logs, then pass this here. - isRunningAdapterTests: true, // This is our super secret flag to let us know to only log debug logs if a test fails. - }, - }); +const normalTestSuite = createTestSuite("Normal", ({ test, adapter }) => [ + test("should create and find a user", async () => { + // Write your adapter tests here using the adapter instance + }), +]); - runNumberIdAdapterTest({ - getAdapter: async (betterAuthOptions = {}) => { - return adapter(betterAuthOptions); - }, - }); +const { execute } = await testAdapter({ + adapter: (options) => { + return myAdapter(/* your adapter config */); + }, + runMigrations: async (options) => { + // Run your database migrations here + }, + tests: [ + normalTestSuite(), + ], + async onFinish() { + // Optional: cleanup after all tests (e.g., delete a DB file) + }, }); + +execute(); ``` +The `testAdapter` function handles the test lifecycle for you, including running migrations before tests and cleaning up all tables after tests complete. + + + In v1.4, adapter tests used `runAdapterTest` and `runNumberIdAdapterTest` from `better-auth/adapters/test`. That export has been removed in v1.5. Use `@better-auth/test-utils/adapter` instead. + + ## Config The `config` object is used to provide information about the adapter to Better-Auth. diff --git a/docs/content/docs/guides/next-auth-migration-guide.mdx b/docs/content/docs/guides/next-auth-migration-guide.mdx index b03ac610d0d..2d0bfd43642 100644 --- a/docs/content/docs/guides/next-auth-migration-guide.mdx +++ b/docs/content/docs/guides/next-auth-migration-guide.mdx @@ -382,6 +382,7 @@ Just like Auth.js has database models, Better Auth also has a core schema. In th name: "email", type: "string", description: "User's email address for communication and login", + isUnique: true, }, { name: "emailVerified", diff --git a/docs/content/docs/guides/your-first-plugin.mdx b/docs/content/docs/guides/your-first-plugin.mdx index 9cc069e95c9..05bf6dd2505 100644 --- a/docs/content/docs/guides/your-first-plugin.mdx +++ b/docs/content/docs/guides/your-first-plugin.mdx @@ -118,8 +118,7 @@ In our case we want to match any requests going to the signup path: And for our logic, we’ll write the following code to check the if user’s birthday makes them above 5 years old. ```ts title="Imports" -import { APIError } from "better-auth/api"; -import { createAuthMiddleware } from "better-auth/plugins"; +import { createAuthMiddleware, APIError } from "better-auth/api"; ``` ```ts title="Before hook" { diff --git a/docs/content/docs/integrations/convex.mdx b/docs/content/docs/integrations/convex.mdx index 3e49f1edb7c..ff9ae4b6b39 100644 --- a/docs/content/docs/integrations/convex.mdx +++ b/docs/content/docs/integrations/convex.mdx @@ -33,11 +33,10 @@ npx convex dev ## Install packages -Install a pinned version of Better Auth and the Convex component for Better Auth, and make sure you are using the latest version of Convex. +Install Better Auth and the Convex component for Better Auth, and make sure you are using the latest version of Convex. ```package-install -npm install better-auth@1.4.9 --save-exact -npm install @convex-dev/better-auth +npm install better-auth @convex-dev/better-auth ``` diff --git a/docs/content/docs/integrations/encore.mdx b/docs/content/docs/integrations/encore.mdx new file mode 100644 index 00000000000..17ae8c557e1 --- /dev/null +++ b/docs/content/docs/integrations/encore.mdx @@ -0,0 +1,142 @@ +--- +title: Encore Integration +description: Integrate Better Auth with Encore. +--- + +Better Auth can be integrated with your [Encore](https://encore.dev) application (an open source TypeScript framework with automated infrastructure and observability). + +Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation). + +### Getting Started + +Install the Encore CLI and create a new application. This will scaffold a TypeScript project with the required structure: + +```bash title="Terminal" +brew install encoredev/tap/encore # if you don't have Encore installed +encore app create my-app --example=ts/hello-world +cd my-app +npm install better-auth +``` + +### Mount the handler + +To handle auth requests, mount Better Auth on a catch-all endpoint using Encore's `api.raw()`: + +```ts title="auth/handler.ts" +import { api } from "encore.dev/api"; +import { toNodeHandler } from "better-auth/node"; +import { auth } from "./auth"; // Your Better Auth instance + +export const authHandler = api.raw( + { expose: true, path: "/api/auth/*path", method: "*" }, + toNodeHandler(auth) +); +``` + + +Encore's `api.raw()` provides Node.js request/response types. We use `toNodeHandler` from `better-auth/node` to bridge these to Better Auth's Web API handler. + + +### CORS + +If your frontend runs on a different origin, configure CORS in your `encore.app` file to allow credentials (cookies) to be sent with requests: + +```json title="encore.app" +{ + "id": "your-app", + "global_cors": { + "allow_origins_with_credentials": ["http://localhost:3000"] + } +} +``` + +### Trusted Origins + +When requests come from a different origin, they are blocked by default. Add trusted origins to your Better Auth config: + +```ts title="auth/auth.ts" +export const auth = betterAuth({ + trustedOrigins: ["http://localhost:3000", "https://your-app.com"], + // ... rest of config +}); +``` + +### Local Development + +Start your app with the Encore CLI. Make sure Docker is running as Encore uses it to manage local infrastructure: + +```bash title="Terminal" +encore run +``` + + +Open the local dashboard at `localhost:9400` to see traces for all requests, including auth handler execution and session validation. Useful for debugging auth issues. + + +### Protecting Endpoints + +Encore has a built-in auth handler pattern for protecting endpoints. Create an auth handler that validates Better Auth sessions: + +```ts title="auth/gateway.ts" +import { APIError, Gateway, Header } from "encore.dev/api"; +import { authHandler } from "encore.dev/auth"; +import { auth } from "./auth"; + +interface AuthParams { + authorization: Header<"Authorization">; + cookie: Header<"Cookie">; +} + +interface AuthData { + userID: string; + email: string; + name: string; +} + +const handler = authHandler(async (params: AuthParams): Promise => { + const headers = new Headers(); + if (params.authorization) { + headers.set("Authorization", params.authorization); + } + if (params.cookie) { + headers.set("Cookie", params.cookie); + } + + const session = await auth.api.getSession({ headers }); + + if (!session?.user) { + throw APIError.unauthenticated("invalid session"); + } + + return { + userID: session.user.id, + email: session.user.email, + name: session.user.name, + }; +}); + +export const gateway = new Gateway({ authHandler: handler }); +``` + +Then protect any endpoint with `auth: true`: + +```ts +import { api } from "encore.dev/api"; +import { getAuthData } from "~encore/auth"; + +export const getProfile = api( + { expose: true, auth: true, method: "GET", path: "/profile" }, + async () => { + const authData = getAuthData()!; + return { id: authData.userID, email: authData.email }; + } +); +``` + + +If you want to manage session cookies directly in your Encore endpoints, check out Encore's [typed cookie support](https://encore.dev/docs/ts/primitives/cookies). + + +### Learn More + +For a complete walkthrough including database setup and deployment, see the [Better Auth with Encore tutorial](https://encore.dev/blog/betterauth-tutorial). diff --git a/docs/content/docs/integrations/solid-start.mdx b/docs/content/docs/integrations/solid-start.mdx index d833a33cebd..45963639d85 100644 --- a/docs/content/docs/integrations/solid-start.mdx +++ b/docs/content/docs/integrations/solid-start.mdx @@ -7,9 +7,9 @@ Before you start, make sure you have a Better Auth instance configured. If you h ### Mount the handler -We need to mount the handler to SolidStart server. Put the following code in your `*auth.ts` file inside `/routes/api/auth` folder. +We need to mount the handler to SolidStart server. Put the following code in your `[...auth].ts` file inside `/routes/api/auth` folder. -```ts title="*auth.ts" +```ts title="[...auth].ts" import { auth } from "~/lib/auth"; import { toSolidStartHandler } from "better-auth/solid-start"; diff --git a/docs/content/docs/integrations/tanstack.mdx b/docs/content/docs/integrations/tanstack.mdx index 5f207d08fa8..3950c6914e0 100644 --- a/docs/content/docs/integrations/tanstack.mdx +++ b/docs/content/docs/integrations/tanstack.mdx @@ -93,7 +93,7 @@ To protect resources that require authentication, use `beforeLoad` with a server First, create server-side helpers to check the session: -```ts title="src/lib/auth.server.ts" +```ts title="src/lib/auth.functions.ts" import { createServerFn } from "@tanstack/react-start"; import { getRequestHeaders } from "@tanstack/react-start/server"; import { auth } from "@/lib/auth"; @@ -123,7 +123,7 @@ Use `beforeLoad` in your route definitions: ```tsx title="src/routes/dashboard.tsx" import { createFileRoute, redirect } from '@tanstack/react-router' -import { getSession } from '@/lib/auth.server' +import { getSession } from '@/lib/auth.functions' export const Route = createFileRoute('/dashboard')({ beforeLoad: async () => { @@ -151,7 +151,7 @@ For protecting multiple routes, use a pathless layout route: ```tsx title="src/routes/_protected.tsx" import { createFileRoute, redirect, Outlet } from '@tanstack/react-router' -import { getSession } from '@/lib/auth.server' +import { getSession } from '@/lib/auth.functions' export const Route = createFileRoute('/_protected')({ beforeLoad: async ({ location }) => { @@ -189,9 +189,9 @@ Then nest protected routes under `_protected`: Use `ensureSession` helper to protect server functions: -```ts title="src/lib/posts.server.ts" +```ts title="src/lib/posts.functions.ts" import { createServerFn } from "@tanstack/react-start"; -import { ensureSession } from "./auth.server"; +import { ensureSession } from "./auth.functions"; export const createPost = createServerFn({ method: "POST" }) .inputValidator((data: { title: string }) => data) diff --git a/docs/content/docs/introduction.mdx b/docs/content/docs/introduction.mdx index 68ae7c44fb9..fe35d7f39ba 100644 --- a/docs/content/docs/introduction.mdx +++ b/docs/content/docs/introduction.mdx @@ -35,33 +35,6 @@ Better Auth provides an MCP server so you can use it with any AI model that supp -#### CLI Options - -Use the Better Auth CLI to easily add the MCP server to your preferred client: - - - - ```bash title="terminal" - npx auth mcp --cursor - ``` - - - ```bash title="terminal" - npx auth mcp --claude-code - ``` - - - ```bash title="terminal" - npx auth mcp --open-code - ``` - - - ```bash title="terminal" - npx auth mcp --manual - ``` - - - #### Manual Configuration Alternatively, you can manually configure the MCP server for each client: @@ -69,7 +42,7 @@ Alternatively, you can manually configure the MCP server for each client: ```bash title="terminal" - claude mcp add --transport http better-auth https://mcp.inkeep.com/better-auth/mcp + claude mcp add --transport http better-auth https://context7.com/better-auth/better-auth ``` @@ -79,7 +52,7 @@ Alternatively, you can manually configure the MCP server for each client: "mcp": { "better-auth": { "type": "remote", - "url": "https://mcp.inkeep.com/better-auth/mcp", + "url": "https://context7.com/better-auth/better-auth", "enabled": true, } } @@ -90,14 +63,9 @@ Alternatively, you can manually configure the MCP server for each client: ```json title="mcp.json" { "better-auth": { - "url": "https://mcp.inkeep.com/better-auth/mcp" + "url": "https://context7.com/better-auth/better-auth" } } ``` - - -We provide a first‑party MCP, powered by [Inkeep](https://inkeep.com). You can alternatively use [`context7`](https://context7.com/) and other MCP providers. - - diff --git a/docs/content/docs/plugins/2fa.mdx b/docs/content/docs/plugins/2fa.mdx index b03c6556448..207b57e4265 100644 --- a/docs/content/docs/plugins/2fa.mdx +++ b/docs/content/docs/plugins/2fa.mdx @@ -151,6 +151,24 @@ const authClient = createAuthClient({ }); ``` +Using the `twoFactorPage` config: + +```ts title="sign-in.ts" +import { createAuthClient } from "better-auth/client"; +import { twoFactorClient } from "better-auth/client/plugins"; + +const authClient = createAuthClient({ + plugins: [ + twoFactorClient({ + twoFactorPage: "/two-factor", // the page to redirect if a user needs to verify their 2nd factor + }), + ], +}); +``` + + +Using the `twoFactorPage` option will cause a full page reload when redirecting users to the two-factor authentication page. If you want to avoid page reloads, consider using the `onTwoFactorRedirect` callback instead to handle the redirect programmatically within your application. + @@ -482,8 +500,8 @@ Table: `twoFactor` fields={[ { name: "id", type: "string", description: "The ID of the two factor authentication.", isPrimaryKey: true }, { name: "userId", type: "string", description: "The ID of the user", isForeignKey: true }, - { name: "secret", type: "string", description: "The secret used to generate the TOTP code.", isOptional: true }, - { name: "backupCodes", type: "string", description: "The backup codes used to recover access to the account if the user loses access to their phone or email.", isOptional: true }, + { name: "secret", type: "string", description: "The secret used to generate the TOTP code." }, + { name: "backupCodes", type: "string", description: "The backup codes used to recover access to the account if the user loses access to their phone or email." }, ]} /> @@ -532,7 +550,7 @@ these are options for OTP. default: 3, }, storeOTP: { - description: "How to store the otp in the database. Whether to store it as plain text, encrypted or hashed. You can also provide a custom encryptor or hasher.", + description: "How to transform the stored OTP value, whether plain text, encrypted, or hashed. You can also provide a custom encryptor or hasher. The storage backend is controlled by the global verification config, so secondary storage can be used instead of the database.", type: "string", default: "plain", }, diff --git a/docs/content/docs/plugins/agent-auth.mdx b/docs/content/docs/plugins/agent-auth.mdx new file mode 100644 index 00000000000..0e471735bb3 --- /dev/null +++ b/docs/content/docs/plugins/agent-auth.mdx @@ -0,0 +1,531 @@ +--- +title: Agent Auth +description: Agent identity, registration, discovery, and capability-based authorization for AI agents. +--- + +`AI Agents` `MCP` `Capabilities` + +The Agent Auth plugin lets your Better Auth server act as an **Agent Auth provider**. It's a server implementation of the [Agent Auth Protocol](https://agentauthprotocol.com). + +It gives AI agents a standard way to discover your service, register themselves, request approval, and execute scoped capabilities using short-lived signed JWTs. It comes with adapters for **OpenAPI** and **MCP** — so you can turn an existing REST API or MCP server into an agent-auth-enabled service without writing capabilities by hand. + +## Features + +- **OpenAPI adapter** — derive capabilities, input/output schemas, and a proxy `onExecute` handler directly from an OpenAPI 3.x spec +- **MCP adapter** — expose agent auth as MCP tools so any MCP-compatible AI agent can discover and call your capabilities +- Discovery document at `/.well-known/agent-configuration` +- Capability listing, description, and execution (optional per-capability `location` URLs) +- Delegated and autonomous agent modes +- Device authorization and CIBA approval flows +- Short-lived signed JWTs with replay protection +- Audit/event hooks for approvals, grants, and execution + +## Installation + + + + ### Install the packages + + ```package-install + @better-auth/agent-auth + ``` + + Client and CLI packages (optional): + + ```bash + npm install @auth/agent @auth/agent-cli + ``` + + + + ### Add the plugin to your auth config + + Start by defining the capabilities your service exposes and an `onExecute` handler that performs the action for an authenticated agent. + + ```ts title="auth.ts" + import { betterAuth } from "better-auth"; + import { agentAuth } from "@better-auth/agent-auth"; // [!code highlight] + + export const auth = betterAuth({ + plugins: [ + agentAuth({ // [!code highlight] + providerName: "Acme", // [!code highlight] + providerDescription: "Acme project and deployment APIs for AI agents.", // [!code highlight] + modes: ["delegated", "autonomous"], // [!code highlight] + capabilities: [ // [!code highlight] + { // [!code highlight] + name: "deploy_project", // [!code highlight] + description: "Deploy a project to production.", // [!code highlight] + input: { // [!code highlight] + type: "object", // [!code highlight] + properties: { // [!code highlight] + projectId: { type: "string" }, // [!code highlight] + }, // [!code highlight] + required: ["projectId"], // [!code highlight] + }, // [!code highlight] + }, // [!code highlight] + { // [!code highlight] + name: "list_projects", // [!code highlight] + description: "List projects the current user can access.", // [!code highlight] + }, // [!code highlight] + ], // [!code highlight] + async onExecute({ capability, arguments: args, agentSession }) { // [!code highlight] + switch (capability) { // [!code highlight] + case "list_projects": // [!code highlight] + return [{ id: "proj_123", name: "marketing-site" }]; // [!code highlight] + case "deploy_project": // [!code highlight] + return { // [!code highlight] + ok: true, // [!code highlight] + projectId: args?.projectId, // [!code highlight] + requestedBy: agentSession.user.id, // [!code highlight] + }; // [!code highlight] + default: // [!code highlight] + throw new Error(`Unsupported capability: ${capability}`); // [!code highlight] + } // [!code highlight] + }, // [!code highlight] + }), // [!code highlight] + ], // [!code highlight] + }); // [!code highlight] + ``` + + + + ### Expose the discovery document + + The plugin provides `auth.api.getAgentConfiguration()`, but you should expose it from your app root at `/.well-known/agent-configuration`. + + ```ts title="app/.well-known/agent-configuration/route.ts" + import { auth } from "@/lib/auth"; + import { NextResponse } from "next/server"; + + export async function GET() { + const configuration = await auth.api.getAgentConfiguration(); + return NextResponse.json(configuration); + } + ``` + + + + ### Migrate the database + + Run the migration or generate the schema to add the agent, host, grant, and approval tables. + + + + ```package-install + npx auth migrate + ``` + + + ```package-install + npx auth generate + ``` + + + + + + ### Optional: add the Better Auth client plugin + + If you want type-safe access to the plugin endpoints from a Better Auth client, add the client plugin too. + + ```ts title="auth-client.ts" + import { createAuthClient } from "better-auth/client"; + import { agentAuthClient } from "@better-auth/agent-auth/client"; // [!code highlight] + + export const authClient = createAuthClient({ + plugins: [ + agentAuthClient(), // [!code highlight] + ], + }); + ``` + + + +## How It Works + +The Agent Auth flow usually looks like this: + +1. An agent discovers your provider from `/.well-known/agent-configuration` +2. The agent lists capabilities and decides what it needs +3. The agent registers with your server and requests capability grants +4. Your user approves the request through device authorization or CIBA +5. The agent signs short-lived JWTs (with an `aud` that matches the URL it calls) and invokes each granted capability at **`default_location`** or at that capability’s own **`location`**, if you set one + +## Discovery + +The discovery document tells agents how to interact with your server. The plugin includes provider metadata, supported modes, approval methods, absolute endpoint URLs, and a **`default_location`** field. + +Important fields for execution: + +- **`issuer`** — The provider’s base URL (Better Auth `baseURL`). +- **`endpoints`** — Absolute URLs for each route (for example `execute` points at `POST /capability/execute` on that base). +- **`default_location`** — The full URL of the default execute endpoint. It always matches `endpoints.execute`. Agents use this as the JWT **`aud`** when a capability does not define a custom URL, and as the request URL for those capabilities. + +Expose it from your app root: + +```ts title="app/.well-known/agent-configuration/route.ts" +import { auth } from "@/lib/auth"; +import { NextResponse } from "next/server"; + +export async function GET() { + const configuration = await auth.api.getAgentConfiguration(); + return NextResponse.json(configuration); +} +``` + + + The discovery route should live at `/.well-known/agent-configuration`, even if your Better Auth base path is `/api/auth`. + + +## OpenAPI Adapter + +If your service already has an OpenAPI spec, you can turn the entire API into an agent-auth provider in a few lines. **`createFromOpenAPI`** reads the spec and produces everything the plugin needs: capabilities (one per `operationId`), input/output JSON Schemas, a proxy **`onExecute`** handler, and optionally **`providerName`** / **`providerDescription`** from `info`. + +```ts title="auth.ts" +import { betterAuth } from "better-auth"; +import { agentAuth } from "@better-auth/agent-auth"; +import { createFromOpenAPI } from "@better-auth/agent-auth/openapi"; + +const spec = await fetch("https://api.example.com/openapi.json").then((r) => + r.json(), +); + +export const auth = betterAuth({ + plugins: [ + agentAuth({ + ...createFromOpenAPI(spec, { + baseUrl: "https://api.example.com", + }), + }), + ], +}); +``` + +That is all it takes. Every operation with an `operationId` in the spec becomes a capability whose name is that id. Path, query, and header parameters plus the JSON request body are merged into a single `input` schema, and the 200/201 response body becomes `output`. + +### Upstream authentication + +The proxy handler calls your upstream API on behalf of the agent. Use **`resolveHeaders`** to inject the credentials each request needs (for example an internal service token or a user-scoped access token looked up from `agentSession`). + +```ts title="auth.ts" +createFromOpenAPI(spec, { + baseUrl: "https://api.example.com", + async resolveHeaders({ agentSession }) { + const token = await getAccessToken(agentSession.user.id); + return { Authorization: `Bearer ${token}` }; + }, +}); +``` + +### Default host capabilities + +Control which capabilities are auto-granted to new hosts. You can pass `true` (all), a single HTTP method string, an array of methods, or a callback that receives the full runtime context. + +```ts title="auth.ts" +createFromOpenAPI(spec, { + baseUrl: "https://api.example.com", + defaultHostCapabilities: ["GET", "HEAD"], +}); +``` + +### Approval strength per method + +Map HTTP methods to **`approvalStrength`** so mutating operations require stronger user verification (for example WebAuthn) while reads use a normal session. + +```ts title="auth.ts" +createFromOpenAPI(spec, { + baseUrl: "https://api.example.com", + approvalStrength: { + GET: "session", + POST: "webauthn", + PUT: "webauthn", + DELETE: "webauthn", + }, +}); +``` + +### Per-capability `location` + +When you set **`location`**, every derived capability gets that URL. Agents call it directly (with the agent JWT) instead of going through the default execute endpoint—useful when you want the agent to hit the real API URL and handle the session in your own middleware rather than proxying through `onExecute`. + +```ts title="auth.ts" +createFromOpenAPI(spec, { + baseUrl: "https://api.example.com", + location: "https://api.example.com/agent/execute", +}); +``` + +### Using the pieces individually + +If you only need part of the pipeline, the adapter also exports the lower-level helpers: + +- **`fromOpenAPI(spec)`** — returns `Capability[]` only (no handler, no host caps). +- **`createOpenAPIHandler(spec, opts)`** — returns only the `onExecute` proxy handler so you can pair it with hand-written capabilities or filter the spec yourself. + +```ts title="auth.ts" +import { + fromOpenAPI, + createOpenAPIHandler, +} from "@better-auth/agent-auth/openapi"; + +const capabilities = fromOpenAPI(spec); +const onExecute = createOpenAPIHandler(spec, { + baseUrl: "https://api.example.com", +}); + +agentAuth({ capabilities, onExecute }); +``` + +## Capabilities + +Capabilities are the contract between your application and an agent. Each capability has a name, a description, and optionally a JSON Schema `input` definition. + +By default, agents call **`default_location`** from discovery (the execute URL) and the plugin runs **`onExecute`**. If you set **`location`** on a capability, agents call that absolute URL instead—for example an existing REST route—and **`onExecute` is not used** for those requests; you resolve the agent session with the helpers below and implement the handler yourself. + +Use capabilities to expose narrow, reviewable actions instead of broad API access. + +### Define capabilities + +```ts title="auth.ts" +agentAuth({ + capabilities: [ + { + name: "create_issue", + description: "Create an issue in the current workspace.", + input: { + type: "object", + properties: { + title: { type: "string" }, + body: { type: "string" }, + }, + required: ["title"], + }, + }, + ], +}); +``` + +Optional **`location`** — agents call this URL with the agent JWT instead of the default execute URL: + +```ts title="auth.ts" +{ + name: "create_issue", + description: "Create an issue in the current workspace.", + location: "https://api.example.com/v1/issues", +} +``` + +### Default execute vs custom `location` + +- **No `location`** — Agents `POST` to **`default_location`** (`endpoints.execute`) with `{ capability, arguments }`. After the plugin validates the JWT and grant, it runs **`onExecute`**. +- **With `location`** — Agents call that URL (your REST handler, another service, an OpenAPI operation URL, etc.). **`onExecute` does not run** for that call. Resolve **`agentSession`** in your handler using the helpers below, then enforce grants and your business logic. + +### Agent session outside `onExecute` + +For custom **`location`** routes (or any non-execute handler), the agent still sends an **`Authorization: Bearer`** header with the agent JWT. Whatever framework you use, take the incoming **`Headers`** (e.g. **`request.headers`**, or your runtime’s equivalent) and pass them through—the verification path is the same: signature, **`aud`**, replay (**`jti`**), expiry, and (when present) request-binding claims. + +**`auth.api.getAgentSession({ headers })`** runs that flow in-process and returns **`AgentSession`** or **`null`**. **`verifyAgentRequest(request, auth)`** does the same by forwarding the **`Request`**’s headers to **`GET /agent/session`** via **`auth.handler`**—pick whichever fits your code shape; there is no Hono-vs-Next split, only “headers in, session out.” + +```ts title="api/issues/route.ts" +import { auth } from "@/lib/auth"; + +export async function POST(request: Request) { + const agentSession = await auth.api.getAgentSession({ + headers: request.headers, + }); + if (!agentSession) { + return new Response("Unauthorized", { status: 401 }); + } + // Check grants, enforce constraints, run your handler… +} +``` + +```ts +// Equivalent when you already have `Request` + `auth` and prefer a helper: +import { verifyAgentRequest } from "@better-auth/agent-auth"; +const agentSession = await verifyAgentRequest(request, auth); +``` + +**Checking grants and inputs** + +After you have **`agentSession`**, inspect **`agentSession.agent.capabilityGrants`**. These are **active** DB grants **intersected** with the JWT’s **`capabilities`** claim (same as execute). For the capability this route implements, ensure there is a matching grant: + +```ts +const CAP = "create_issue"; +const allowed = agentSession.agent.capabilityGrants.some( + (g) => g.capability === CAP && g.status === "active", +); +if (!allowed) { + return new Response("Forbidden", { status: 403 }); +} +``` + +If that grant has **`constraints`**, validate the request body or query the same way **`POST /capability/execute`** would—otherwise a client could bypass constraints by calling your custom URL. The plugin does not re-run execute’s constraint helpers on arbitrary routes; that logic stays in your handler (or call into shared code you extract from your **`onExecute`** path). + +**What you get on the session** + +- **`agentSession.user`** — Resolved user for the agent (delegated host user or **`resolveAutonomousUser`**). +- **`agentSession.agent`** — Id, name, mode, **`capabilityGrants`**, host id, metadata. +- **`agentSession.host`** — Host record when the agent is linked to a host. + +Types are exported from **`@better-auth/agent-auth`** (for example **`AgentSession`**). + +### JWT audience (`aud`) + +The JWT **`aud`** must match what the server expects for the URL being called: + +- **No per-capability `location`** — Use **`default_location`** / **`endpoints.execute`**, or issuer / base URL values the plugin already allows. +- **With `location`** — **`aud`** should be that same absolute URL. `GET /capability/list` includes `location` when set. Invalid `location` values in config fail at startup. + +**Single capability in the JWT** — If `capabilities` lists exactly one id, **`aud`** may equal that capability’s **`location`** when set. + +**Multiple capabilities in the JWT** — Per-capability **`location`** values are not accepted as **`aud`**; use the issuer, base path, or default execute endpoint instead. + +Behind a reverse proxy, set **`trustProxy`** if you need **`Host`** / **`X-Forwarded-Proto`** to line up with **`aud`** validation. + +### Filter visible capabilities + +Use `resolveCapabilities` to show different capability sets to different callers, such as plan-gated, user-specific, or organization-specific capabilities. + +### `onExecute` + +Runs for capabilities that use the **default execute URL** (no per-capability **`location`**). The plugin verifies the JWT (including **`aud`**), attaches **`agentSession`**, checks the grant, then calls **`onExecute`**. Capabilities with a custom **`location`** never hit this path—you handle them in your own route using the session helpers above. + +```ts title="auth.ts" +agentAuth({ + capabilities: [ + { + name: "create_issue", + description: "Create an issue in the current workspace.", + }, + ], + async onExecute({ capability, arguments: args, agentSession }) { + if (capability !== "create_issue") { + throw new Error("Unsupported capability"); + } + + return { + ok: true, + title: args?.title, + createdBy: agentSession.user.id, + }; + }, +}); +``` + +## Approval Flows + +The plugin supports two approval methods: + +- `device_authorization` for browser-based approval with a user code +- `ciba` for backchannel approval flows + +By default, both are enabled. You can restrict or customize them with `approvalMethods` and `resolveApprovalMethod`. + +```ts title="auth.ts" +agentAuth({ + approvalMethods: ["ciba", "device_authorization"], + resolveApprovalMethod: ({ preferredMethod, supportedMethods }) => { + if (preferredMethod && supportedMethods.includes(preferredMethod)) { + return preferredMethod; + } + return "device_authorization"; + }, + deviceAuthorizationPage: "/device/capabilities", +}); +``` + + + The plugin does not render the device approval UI for you. Your app must provide the page referenced by `deviceAuthorizationPage`. + + + +## Events and Auditing + +Use `onEvent` to capture important lifecycle events such as: + +- agent creation and revocation +- host creation and enrollment +- capability requests and approvals +- capability execution + +This hook is a good place to write audit logs or feed analytics pipelines. + +## Configuration + +The Agent Auth plugin supports many options. These are the ones you will usually start with: + + + diff --git a/docs/content/docs/plugins/api-key/advanced.mdx b/docs/content/docs/plugins/api-key/advanced.mdx index 53c93cc7122..86c85664d8d 100644 --- a/docs/content/docs/plugins/api-key/advanced.mdx +++ b/docs/content/docs/plugins/api-key/advanced.mdx @@ -67,7 +67,7 @@ Or optionally, you can pass a `customAPIKeyGetter` function to the plugin option ```ts title="auth.ts" import { betterAuth } from "better-auth" -import { apiKey } from "better-auth/plugins" +import { apiKey } from "@better-auth/api-key" export const auth = betterAuth({ plugins: [ diff --git a/docs/content/docs/plugins/api-key/reference.mdx b/docs/content/docs/plugins/api-key/reference.mdx index c92012fa18f..bf22a9a8da4 100644 --- a/docs/content/docs/plugins/api-key/reference.mdx +++ b/docs/content/docs/plugins/api-key/reference.mdx @@ -171,7 +171,7 @@ Default is `false`. ```ts title="auth.ts" import { betterAuth } from "better-auth"; -import { apiKey } from "better-auth/plugins" +import { apiKey } from "@better-auth/api-key" export const auth = betterAuth({ secondaryStorage: { @@ -202,7 +202,7 @@ Useful when you want to use a different storage backend specifically for API key ```ts title="auth.ts" import { betterAuth } from "better-auth"; -import { apiKey } from "better-auth/plugins" +import { apiKey } from "@better-auth/api-key" export const auth = betterAuth({ plugins: [ @@ -307,7 +307,7 @@ You can configure default permissions that will be applied to all newly created ```ts title="auth.ts" import { betterAuth } from "better-auth" -import { apiKey } from "better-auth/plugins" +import { apiKey } from "@better-auth/api-key" export const auth = betterAuth({ plugins: [ @@ -327,7 +327,7 @@ You can also provide a function that returns permissions dynamically: ```ts title="auth.ts" import { betterAuth } from "better-auth" -import { apiKey } from "better-auth/plugins" +import { apiKey } from "@better-auth/api-key" export const auth = betterAuth({ plugins: [ @@ -497,11 +497,13 @@ Table: `apikey` name: "enabled", type: "boolean", description: "Whether the API key is enabled.", + isOptional: true, }, { name: "rateLimitEnabled", type: "boolean", description: "Whether the API key has rate limiting enabled.", + isOptional: true, }, { name: "rateLimitTimeWindow", @@ -521,6 +523,7 @@ Table: `apikey` type: "number", description: "The number of requests made within the rate limit time window.", + isOptional: true, }, { name: "remaining", @@ -558,7 +561,7 @@ Table: `apikey` }, { name: "metadata", - type: "Object", + type: "string", isOptional: true, description: "Any additional metadata you want to store with the key.", }, diff --git a/docs/content/docs/plugins/device-authorization.mdx b/docs/content/docs/plugins/device-authorization.mdx index f89d0a72be0..b1643430a5b 100644 --- a/docs/content/docs/plugins/device-authorization.mdx +++ b/docs/content/docs/plugins/device-authorization.mdx @@ -611,12 +611,11 @@ Table Name: `deviceCode` type: "string", description: "The user-friendly code for verification", }, - { - name: "userId", - type: "string", + { + name: "userId", + type: "string", description: "The ID of the user who approved/denied", - isOptional: true, - isForeignKey: true + isOptional: true }, { name: "clientId", @@ -652,15 +651,5 @@ Table Name: `deviceCode` description: "Minimum seconds between polls", isOptional: true }, - { - name: "createdAt", - type: "Date", - description: "When the request was created", - }, - { - name: "updatedAt", - type: "Date", - description: "When the request was last updated", - } ]} /> diff --git a/docs/content/docs/plugins/dodopayments.mdx b/docs/content/docs/plugins/dodopayments.mdx index d1acf94770a..126917454fc 100644 --- a/docs/content/docs/plugins/dodopayments.mdx +++ b/docs/content/docs/plugins/dodopayments.mdx @@ -99,6 +99,7 @@ export const auth = betterAuth({ Create or update `src/lib/auth-client.ts`: ```typescript +import { createAuthClient } from "better-auth/react"; import { dodopaymentsClient } from "@dodopayments/better-auth"; export const authClient = createAuthClient({ @@ -114,27 +115,21 @@ export const authClient = createAuthClient({ ### Creating a Checkout Session ```typescript -const { data: checkout, error } = await authClient.dodopayments.checkout({ +const { data: checkoutSession, error } = + await authClient.dodopayments.checkoutSession({ slug: "premium-plan", - customer: { - email: "customer@example.com", - name: "John Doe", - }, - billing: { - city: "San Francisco", - country: "US", - state: "CA", - street: "123 Market St", - zipcode: "94103", - }, - referenceId: "order_123", }); -if (checkout) { - window.location.href = checkout.url; +if (checkoutSession) { + window.location.href = checkoutSession.url; } ``` + + `authClient.dodopayments.checkout()` is deprecated. Use + `authClient.dodopayments.checkoutSession()` for new integrations. + + ### Accessing the Customer Portal ```typescript diff --git a/docs/content/docs/plugins/email-otp.mdx b/docs/content/docs/plugins/email-otp.mdx index 6a93e2f5e18..ac9b11b7140 100644 --- a/docs/content/docs/plugins/email-otp.mdx +++ b/docs/content/docs/plugins/email-otp.mdx @@ -411,13 +411,33 @@ export const auth = betterAuth({ When the maximum attempts are exceeded, the `verifyOTP`, `signIn.emailOtp`, `verifyEmail`, and `resetPassword` methods will return an error with code `TOO_MANY_ATTEMPTS`. -- `storeOTP`: The method to store the OTP in your database, whether `encrypted`, `hashed` or `plain` text. Default is `plain` text. +- `resendStrategy`: Controls what happens when a user requests a new OTP while an existing one is still valid. Defaults to `"rotate"`. + - `"rotate"`: Always generates a new OTP (default behavior). + - `"reuse"`: Resends the same OTP and extends its expiry. This prevents multiple valid codes from existing simultaneously when emails are delayed. Only works when the OTP is recoverable (`plain`, `encrypted`, or custom encrypt/decrypt). Falls back to `"rotate"` when the OTP is hashed. If the allowed attempts have been exhausted, a fresh OTP is generated instead of reusing the exhausted one. + +```ts title="auth.ts" +import { betterAuth } from "better-auth" +import { emailOTP } from "better-auth/plugins" + +export const auth = betterAuth({ + plugins: [ + emailOTP({ + resendStrategy: "reuse", // [!code highlight] + async sendVerificationOTP({ email, otp, type }) { + // send the OTP + }, + }) + ] +}) +``` + +- `storeOTP`: The method used to transform the OTP before it is stored by Better Auth's verification layer, whether `encrypted`, `hashed` or `plain` text. Default is `plain` text. -Note: This will not affect the OTP sent to the user, it will only affect the OTP stored in your database. +Note: This will not affect the OTP sent to the user. It only affects the stored OTP value. The storage backend itself is controlled by the global [`verification`](/docs/reference/options#verification) config, so if you configure `secondaryStorage`, these verification records can live there instead of the database. -Alternatively, you can pass a custom encryptor or hasher to store the OTP in your database. +Alternatively, you can pass a custom encryptor or hasher to control how the stored OTP value is persisted. **Custom encryptor** diff --git a/docs/content/docs/plugins/index.mdx b/docs/content/docs/plugins/index.mdx index 945d1e74155..0fb932603c1 100644 --- a/docs/content/docs/plugins/index.mdx +++ b/docs/content/docs/plugins/index.mdx @@ -35,6 +35,7 @@ Better Auth ships with 50+ plugins that extend the framework with additional aut | Plugin | Description | | --- | --- | +| [Agent Auth](/docs/plugins/agent-auth) New | Discovery, registration, and capability-based authorization for AI agents | | [API Key](/docs/plugins/api-key) | API key generation and management | | [JWT](/docs/plugins/jwt) | JSON Web Token authentication for services | | [Bearer](/docs/plugins/bearer) | Bearer token authentication for API requests | diff --git a/docs/content/docs/plugins/magic-link.mdx b/docs/content/docs/plugins/magic-link.mdx index 16b2146aa5c..dcfe64aeaee 100644 --- a/docs/content/docs/plugins/magic-link.mdx +++ b/docs/content/docs/plugins/magic-link.mdx @@ -20,7 +20,7 @@ Magic link or email link is a way to authenticate users without a password. When export const auth = betterAuth({ plugins: [ magicLink({ // [!code highlight] - sendMagicLink: async ({ email, token, url }, ctx) => { // [!code highlight] + sendMagicLink: async ({ email, token, url, metadata }, ctx) => { // [!code highlight] // send email to user // [!code highlight] } // [!code highlight] }) // [!code highlight] @@ -85,6 +85,10 @@ type signInMagicLink = { * redirected to the callbackURL with an `error` query parameter. */ errorCallbackURL?: string = "/error" + /** + * Additional metadata forwarded to the sendMagicLink callback. + */ + metadata?: Record = { inviteId: "123" } } ``` @@ -130,6 +134,7 @@ type magicLinkVerify = { - `email`: The email address of the user. - `url`: The URL to be sent to the user. This URL contains the token. - `token`: The token if you want to send the token with custom URL. +- `metadata`: Additional request metadata passed from `signIn.magicLink`. and a `ctx` context object as the second parameter. @@ -149,10 +154,12 @@ and a `ctx` context object as the second parameter. default, we return a long and cryptographically secure string. -**storeToken**: The `storeToken` function is called to store the magic link token in the database. The default value is `"plain"`. +**storeToken**: The `storeToken` function controls how the magic link token is transformed before it is stored by Better Auth's verification layer. The default value is `"plain"`. The `storeToken` function can be one of the following: - `"plain"`: The token is stored in plain text. - `"hashed"`: The token is hashed using the default hasher. - `{ type: "custom-hasher", hash: (token: string) => Promise }`: The token is hashed using a custom hasher. + +The storage backend itself is controlled by the global [`verification`](/docs/reference/options#verification) config. If you configure `secondaryStorage`, magic link verification records can be stored there instead of the database. diff --git a/docs/content/docs/plugins/meta.json b/docs/content/docs/plugins/meta.json new file mode 100644 index 00000000000..1e44f66ec99 --- /dev/null +++ b/docs/content/docs/plugins/meta.json @@ -0,0 +1,45 @@ +{ + "title": "Plugins", + "pages": [ + "index", + "2fa", + "passkey", + "magic-link", + "email-otp", + "phone-number", + "anonymous", + "username", + "one-tap", + "siwe", + "generic-oauth", + "multi-session", + "last-login-method", + "admin", + "organization", + "sso", + "scim", + "agent-auth", + "api-key", + "jwt", + "bearer", + "one-time-token", + "oauth-proxy", + "oauth-provider", + "oidc-provider", + "mcp", + "device-authorization", + "stripe", + "polar", + "autumn", + "creem", + "commet", + "dodopayments", + "captcha", + "have-i-been-pwned", + "i18n", + "open-api", + "test-utils", + "dub", + "community-plugins" + ] +} diff --git a/docs/content/docs/plugins/oauth-provider.mdx b/docs/content/docs/plugins/oauth-provider.mdx index b81fdf76fe8..44cd491d98b 100644 --- a/docs/content/docs/plugins/oauth-provider.mdx +++ b/docs/content/docs/plugins/oauth-provider.mdx @@ -195,6 +195,33 @@ type getOAuthClientPublic = { ``` +#### Get Public Client Prelogin + +To obtain a public client prior to login, you must first enable the endpoint in your configuration: + +```ts title="auth.ts" +oauthProvider({ + allowPublicClientPrelogin: true, +}) +``` + +Then, the following endpoint will obtain public client information. + + +```ts +type getOAuthClientPublicPrelogin = { + /** + * The OAuth client's client_id + */ + client_id: string, + /** + * Valid oauth query parameters (Sent automatically when using the provided client) + */ + oauth_query: string +} +``` + + #### List Clients To obtain a list of clients owned by a specific user or organization, use the following endpoint: @@ -1368,6 +1395,56 @@ oauthProvider({ ``` +### Pairwise Subject Identifiers + +By default, the `sub` (subject) claim in tokens uses the user's internal ID, which is the same across all clients. This is the **public** subject type per [OIDC Core Section 8](https://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes). + +You can enable **pairwise** subject identifiers so each client receives a unique, unlinkable `sub` for the same user. This prevents relying parties from correlating users across services. + +```ts title="auth.ts" +oauthProvider({ + pairwiseSecret: "your-256-bit-secret", // [!code highlight] +}) +``` + +When `pairwiseSecret` is configured, the server advertises both `"public"` and `"pairwise"` in the discovery endpoint's `subject_types_supported`. Clients opt in by setting `subject_type: "pairwise"` at registration. + +#### Per-Client Configuration + +```ts title="register-client.ts" +const response = await auth.api.createOAuthClient({ + headers, + body: { + client_name: 'Privacy-Sensitive App', + redirect_uris: ['https://app.example.com/callback'], + token_endpoint_auth_method: 'client_secret_post', + subject_type: 'pairwise', // Enable pairwise sub for this client + } +}); +``` + +#### How It Works + +Pairwise identifiers are computed using HMAC-SHA256 over the **sector identifier** (the host of the client's first redirect URI) and the user ID, keyed with `pairwiseSecret`. This means: + +- Two clients with different redirect URI hosts always receive different `sub` values for the same user +- Two clients sharing the same redirect URI host receive the **same** pairwise `sub` (per OIDC Core Section 8.1) +- The same client always receives the same `sub` for the same user (deterministic) + +Pairwise `sub` appears in: +- `id_token` +- `/oauth2/userinfo` response +- Token introspection (`/oauth2/introspect`) + +JWT access tokens always use the real user ID as `sub`, since resource servers may need to look up users directly. + + +**Limitations:** +- `sector_identifier_uri` is not yet supported. All `redirect_uris` for a pairwise client must share the same host. Clients with redirect URIs on different hosts will be rejected at registration. +- `pairwiseSecret` must be at least 32 characters long. +- Rotating `pairwiseSecret` will change all pairwise `sub` values, breaking existing RP sessions. Treat this secret as permanent once set. + + ### MCP You can easily make your APIs [MCP-compatible](https://modelcontextprotocol.io/specification/draft/basic/authorization) simply by adding a resource server which directs users to this OAuth 2.1 authorization server. @@ -1551,7 +1628,7 @@ Table Name: `oauthClient` name: "clientId", type: "string", description: "Unique identifier for each OAuth client", - isPrimaryKey: true, + isUnique: true, }, { name: "clientSecret", @@ -1577,6 +1654,12 @@ Table Name: `oauthClient` description: "Field that indicates if the application can logout via an id_token. You may choose to enable this for trusted applications.", isOptional: true, }, + { + name: "subjectType", + type: "string", + description: "Subject identifier type for this client. Set to \"pairwise\" to receive unique, unlinkable sub claims per user. Requires pairwiseSecret to be configured on the server.", + isOptional: true, + }, { name: "scopes", type: "string[]", @@ -1596,17 +1679,18 @@ Table Name: `oauthClient` type: "string", description: "ID of the reference of the client owner if not a user. (optional)", isOptional: true, - isForeignKey: true, }, { name: "createdAt", type: "Date", description: "Timestamp of when the OAuth client was created", + isOptional: true, }, { name: "updatedAt", type: "Date", description: "Timestamp of when the OAuth client was last updated", + isOptional: true, }, { name: "name", @@ -1666,7 +1750,12 @@ Table Name: `oauthClient` name: "redirectUris", type: "string[]", description: "Array of of redirect uris", - isRequired: true, + }, + { + name: "postLogoutRedirectUris", + type: "string[]", + description: "Array of post-logout redirect URIs", + isOptional: true, }, { name: "tokenEndpointAuthMethod", @@ -1698,6 +1787,12 @@ Table Name: `oauthClient` description: "Type of OAuth client. Supports: ['web', 'native', 'user-agent-based']", isOptional: true, }, + { + name: "requirePKCE", + type: "boolean", + description: "Whether PKCE is required for this client", + isOptional: true, + }, { name: "metadata", type: "json", @@ -1723,14 +1818,12 @@ Table Name: `oauthRefreshToken` name: "token", type: "string", description: "Hashed/encrypted refresh token", - isRequired: true, }, { name: "clientId", type: "string", description: "ID of the OAuth client", isForeignKey: true, - isRequired: true, references: { model: "oauthClient", field: "clientId" }, }, { @@ -1738,7 +1831,7 @@ Table Name: `oauthRefreshToken` type: "string", description: "ID of the session used at issuance of the token (and still active)", isForeignKey: true, - isRequired: false, + isOptional: true, references: { model: "session", field: "id" }, }, { @@ -1746,7 +1839,6 @@ Table Name: `oauthRefreshToken` type: "string", description: "ID of the user associated with the token", isForeignKey: true, - isRequired: true, references: { model: "user", field: "id" }, }, { @@ -1754,13 +1846,11 @@ Table Name: `oauthRefreshToken` type: "string", description: "ID of the consented reference", isOptional: true, - isForeignKey: true, }, { name: "scopes", type: "string[]", description: "Array of granted scopes", - isRequired: true, }, { name: "revoked", @@ -1803,14 +1893,13 @@ Table Name: `oauthAccessToken` name: "token", type: "string", description: "Hashed/encrypted access token", - isRequired: true, + isUnique: true, }, { name: "clientId", type: "string", description: "ID of the OAuth client", isForeignKey: true, - isRequired: true, references: { model: "oauthClient", field: "clientId" } }, { @@ -1842,18 +1931,16 @@ Table Name: `oauthAccessToken` type: "string", description: "ID of the consented reference", isOptional: true, - isForeignKey: true, }, { name: "scopes", type: "string[]", description: "Array of granted scopes", - isRequired: true, }, { name: "createdAt", type: "Date", - description: "Timestamp when the token was created" + description: "Timestamp when the token was created", }, { name: "expiresAt", @@ -1894,23 +1981,21 @@ Table Name: `oauthConsent` type: "string", description: "ID of the consented reference", isOptional: true, - isForeignKey: true, }, { name: "scopes", - type: "string", - description: "Comma-separated list of scopes consented to", - isRequired: true + type: "string[]", + description: "Array of scopes consented to", }, { name: "createdAt", type: "Date", - description: "Timestamp of when the consent was given" + description: "Timestamp of when the consent was given", }, { name: "updatedAt", type: "Date", - description: "Timestamp of when the consent was last updated" + description: "Timestamp of when the consent was last updated", }, ]} /> diff --git a/docs/content/docs/plugins/oauth-proxy.mdx b/docs/content/docs/plugins/oauth-proxy.mdx index b115f3f3555..c0e17f81513 100644 --- a/docs/content/docs/plugins/oauth-proxy.mdx +++ b/docs/content/docs/plugins/oauth-proxy.mdx @@ -3,49 +3,67 @@ title: OAuth Proxy description: OAuth Proxy plugin for Better Auth --- -A proxy plugin, that allows you to proxy OAuth requests. Useful for development and preview deployments where the redirect URL can't be known in advance to add to the OAuth provider. +A proxy plugin that allows you to proxy OAuth requests. Useful for development and preview deployments where the redirect URL can't be known in advance to add to the OAuth provider. ## Installation - ### Add the plugin to your **auth** config + ### Add the plugin to your auth config + ```ts title="auth.ts" import { betterAuth } from "better-auth" import { oAuthProxy } from "better-auth/plugins" // [!code highlight] export const auth = betterAuth({ - plugins: [ - oAuthProxy({ // [!code highlight] - productionURL: "https://my-main-app.com", // Optional - if the URL isn't inferred correctly // [!code highlight] - currentURL: "http://localhost:3000", // Optional - if the URL isn't inferred correctly // [!code highlight] - }), // [!code highlight] - ] - }) + plugins: [ + oAuthProxy({ // [!code highlight] + productionURL: "https://my-production-app.com", // [!code highlight] + }), // [!code highlight] + ], + socialProviders: { + github: { + clientId: process.env.GITHUB_CLIENT_ID || "", + clientSecret: process.env.GITHUB_CLIENT_SECRET || "", + }, + }, + }) ``` + + The plugin will automatically route OAuth requests through your production server. - ### Add redirect URL to your OAuth provider + ### Register the callback URL with your OAuth provider - For the proxy server to work properly, you’ll need to pass the redirect URL of your main production app registered with the OAuth provider in your social provider config. This needs to be done for each social provider you want to proxy requests for. + In your OAuth provider's developer console (e.g. GitHub, Google), register the callback URL using your **production** domain. For example: - ```ts + ``` + https://my-production-app.com/api/auth/callback/github + ``` + + Only the production callback URL needs to be registered. The plugin handles routing OAuth requests from preview and development environments through production automatically. + + + ### Add trusted origins + + Since preview and development servers redirect through production, you need to add them as `trustedOrigins` in your auth config: + + ```ts title="auth.ts" export const auth = betterAuth({ - plugins: [ - oAuthProxy(), - ], - socialProviders: { - github: { - clientId: "your-client-id", - clientSecret: "your-client-secret", - redirectURI: "https://my-main-app.com/api/auth/callback/github" // [!code highlight] - } - } - }) + // ...other config + trustedOrigins: [ // [!code highlight] + "http://localhost:3000", // [!code highlight] + "https://my-app-*-preview.example.com", // [!code highlight] + ], // [!code highlight] + }) ``` + +All environments (production, preview, development) must share the same `BETTER_AUTH_SECRET`. The plugin uses it to encrypt and decrypt data passed between servers during the OAuth flow. + + ## How it works The plugin allows you to use a single OAuth client (registered with your production URL) across multiple environments like preview deployments or local development. @@ -73,8 +91,8 @@ This plugin is intended for development and preview environments. If `baseURL` a ## Options -**productionURL**: If this value matches the `baseURL` in your auth config, requests will not be proxied. Defaults to the `BETTER_AUTH_URL` environment variable. +**productionURL**: The URL of your production server. If this value matches the `baseURL` in your auth config, requests will not be proxied. Defaults to the `BETTER_AUTH_URL` environment variable. -**currentURL**: The application's current URL is automatically determined by the plugin. It first checks for the request URL if invoked by a client, then it checks the base URL from popular hosting providers, and finally falls back to the `baseURL` in your auth config. If the URL isn't inferred correctly, you can specify it manually here. +**currentURL**: The application's current URL is automatically determined by the plugin. It first checks the request URL, then vendor-specific environment variables from popular hosting providers, and finally falls back to the `baseURL` in your auth config. You only need to set this if the URL isn't being inferred correctly in your environment. **maxAge**: Maximum age in seconds for encrypted profile payloads. Payloads older than this will be rejected to prevent replay attacks. Keep this value short (e.g., 30-60 seconds) to minimize the window for potential replay attacks while still allowing normal OAuth flows. Defaults to `60` seconds. diff --git a/docs/content/docs/plugins/oidc-provider.mdx b/docs/content/docs/plugins/oidc-provider.mdx index a0079b36223..019caae91a5 100644 --- a/docs/content/docs/plugins/oidc-provider.mdx +++ b/docs/content/docs/plugins/oidc-provider.mdx @@ -453,11 +453,11 @@ Table Name: `oauthApplication` description: "Database ID of the OAuth client", isPrimaryKey: true }, - { - name: "clientId", - type: "string", + { + name: "clientId", + type: "string", description: "Unique identifier for each OAuth client", - isPrimaryKey: true + isUnique: true }, { name: "clientSecret", @@ -465,17 +465,21 @@ Table Name: `oauthApplication` description: "Secret key for the OAuth client. Optional for public clients using PKCE.", isOptional: true }, - { - name: "name", - type: "string", - description: "Name of the OAuth client", - isRequired: true + { + name: "icon", + type: "string", + description: "Icon of the OAuth client", + isOptional: true + }, + { + name: "name", + type: "string", + description: "Name of the OAuth client" }, { name: "redirectUrls", type: "string", - description: "Comma-separated list of redirect URLs", - isRequired: true + description: "Comma-separated list of redirect URLs" }, { name: "metadata", @@ -486,26 +490,26 @@ Table Name: `oauthApplication` { name: "type", type: "string", - description: "Type of OAuth client (e.g., web, mobile)", - isRequired: true + description: "Type of OAuth client (e.g., web, mobile)" }, - { - name: "disabled", - type: "boolean", + { + name: "disabled", + type: "boolean", description: "Indicates if the client is disabled", - isRequired: true + isOptional: true }, { - name: "userId", - type: "string", + name: "userId", + type: "string", description: "ID of the user who owns the client. (optional)", isOptional: true, + isForeignKey: true, references: { model: "user", field: "id" } }, - { - name: "createdAt", - type: "Date", - description: "Timestamp of when the OAuth client was created" + { + name: "createdAt", + type: "Date", + description: "Timestamp of when the OAuth client was created" }, { name: "updatedAt", @@ -528,27 +532,26 @@ Table Name: `oauthAccessToken` isPrimaryKey: true }, { - name: "accessToken", - type: "string", + name: "accessToken", + type: "string", description: "Access token issued to the client", + isUnique: true }, { - name: "refreshToken", - type: "string", + name: "refreshToken", + type: "string", description: "Refresh token issued to the client", - isRequired: true + isUnique: true }, { name: "accessTokenExpiresAt", type: "Date", - description: "Expiration date of the access token", - isRequired: true + description: "Expiration date of the access token" }, { name: "refreshTokenExpiresAt", type: "Date", - description: "Expiration date of the refresh token", - isRequired: true + description: "Expiration date of the refresh token" }, { name: "clientId", @@ -558,22 +561,22 @@ Table Name: `oauthAccessToken` references: { model: "oauthApplication", field: "clientId" } }, { - name: "userId", - type: "string", + name: "userId", + type: "string", description: "ID of the user associated with the token", + isOptional: true, isForeignKey: true, references: { model: "user", field: "id" } }, { name: "scopes", type: "string", - description: "Comma-separated list of scopes granted", - isRequired: true + description: "Comma-separated list of scopes granted" }, - { - name: "createdAt", - type: "Date", - description: "Timestamp of when the access token was created" + { + name: "createdAt", + type: "Date", + description: "Timestamp of when the access token was created" }, { name: "updatedAt", @@ -612,19 +615,17 @@ Table Name: `oauthConsent` { name: "scopes", type: "string", - description: "Comma-separated list of scopes consented to", - isRequired: true + description: "Comma-separated list of scopes consented to" }, { name: "consentGiven", type: "boolean", - description: "Indicates if consent was given", - isRequired: true + description: "Indicates if consent was given" }, - { - name: "createdAt", - type: "Date", - description: "Timestamp of when the consent was given" + { + name: "createdAt", + type: "Date", + description: "Timestamp of when the consent was given" }, { name: "updatedAt", diff --git a/docs/content/docs/plugins/openfort.mdx b/docs/content/docs/plugins/openfort.mdx index 1341dffb4b0..66b7edfa90a 100644 --- a/docs/content/docs/plugins/openfort.mdx +++ b/docs/content/docs/plugins/openfort.mdx @@ -16,10 +16,11 @@ This plugin is maintained by the Openfort team. For bugs, issues or feature requ - Embedded wallet creation and management. - Seamless integration with Better Auth authentication. -- Multi-chain support (Ethereum, Polygon, and more). +- EVM support (Ethereum, Polygon, Base, etc.) and/or Solana — use one or both. - Gas sponsorship for user transactions. - Multiple recovery methods (passkey, password, automatic). - Sign messages and execute smart contract interactions. +- Optional Wagmi integration for EVM chains. ## Installation @@ -36,12 +37,24 @@ This plugin is maintained by the Openfort team. For bugs, issues or feature requ #### Client ```package-install - @openfort/react wagmi viem @tanstack/react-query + @openfort/react @tanstack/react-query + ``` + + Then install the peer dependencies for the chains you need: + + **For EVM (Ethereum, Polygon, Base, etc.):** + ```package-install + viem wagmi + ``` + + **For Solana:** + ```package-install + @solana/kit ``` - The `@openfort/react` package only supports **EVM chains** (Ethereum, Polygon, etc.). The React SDK integrates with Wagmi for blockchain interactions. - If you need support for non-EVM chains, use the Openfort JS SDK directly. + The `@openfort/react` package supports **EVM chains** (Ethereum, Polygon, Base, etc.) and **Solana**. You can build an EVM-only, Solana-only, or multi-chain app. The React SDK integrates with Wagmi for EVM blockchain interactions and `@solana/kit` for Solana. + If you need support for other chains, use the Openfort JS SDK directly. @@ -85,7 +98,7 @@ This plugin is maintained by the Openfort team. For bugs, issues or feature requ # Openfort VITE_OPENFORT_PUBLISHABLE_KEY=pk_test_... VITE_SHIELD_PUBLISHABLE_KEY=... - VITE_POLICY_ID=pol_... # Transaction policy ID from Openfort dashboard + VITE_FEE_SPONSORSHIP_ID=pol_... # Fee sponsorship ID from Openfort dashboard # Better Auth VITE_BETTERAUTH_URL=http://localhost:3000 @@ -165,10 +178,72 @@ This plugin is maintained by the Openfort team. For bugs, issues or feature requ ### Add OpenfortProvider to your app - Wrap your application with the OpenfortProvider, WagmiProvider, and QueryClientProvider: + The `OpenfortProvider` works standalone — Wagmi and React Query are only needed if you use Wagmi hooks. + + ```tsx title="app/providers.tsx" + import { OpenfortProvider, ThirdPartyOAuthProvider } from "@openfort/react"; + import { authClient } from "./auth-client"; + + export function OpenfortProviders({ children }: { children: React.ReactNode }) { + return ( + { + const response = await fetch( + import.meta.env.VITE_BETTERAUTH_URL + + import.meta.env.VITE_BETTERAUTH_BASE_PATH + + "/encryption-session", + { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) return null; + + const data = await response.json(); + return data.sessionId; + }, + connectOnLogin: false, + }} + thirdPartyAuth={{ + getAccessToken: async () => { + const session = await authClient.getSession(); + return session?.data?.session?.token ?? null; + }, + provider: ThirdPartyOAuthProvider.BETTER_AUTH, + }} + > + {children} + + ); + } + ``` + + + The `getEncryptionSession` callback receives `{ accessToken, otpCode, userId }` — the SDK provides these automatically. Set `connectOnLogin` to `false` if you want to handle wallet creation manually. Include only the chain configs (`ethereum`, `solana`) you need. + + + + + ### Add Wagmi for EVM interactions + + Wrap your provider tree with `OpenfortWagmiBridge` to enable Wagmi hooks (`useSignMessage`, `useWriteContract`, etc.): ```tsx title="app/providers.tsx" - import { OpenfortProvider, ThirdPartyOAuthProvider, getDefaultConfig } from "@openfort/react"; + import { OpenfortProvider, ThirdPartyOAuthProvider } from "@openfort/react"; + import { OpenfortWagmiBridge, getDefaultConfig } from "@openfort/react/wagmi"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { polygonAmoy, sepolia } from "viem/chains"; import { WagmiProvider, createConfig } from "wagmi"; @@ -187,47 +262,46 @@ This plugin is maintained by the Openfort team. For bugs, issues or feature requ return ( - { - const session = await authClient.getSession(); - const token = session?.data?.session?.token; - - if (!token) return null; - - const response = await fetch( - import.meta.env.VITE_BETTERAUTH_URL + - import.meta.env.VITE_BETTERAUTH_BASE_PATH + - "/encryption-session", - { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - } - ); - - if (!response.ok) return null; - - const data = await response.json(); - return data.sessionId; - }, - recoverWalletAutomaticallyAfterAuth: false, - }} - thirdPartyAuth={{ - getAccessToken: async () => { - const session = await authClient.getSession(); - return session?.data?.session?.token ?? null; - }, - provider: ThirdPartyOAuthProvider.BETTER_AUTH, - }} - > - {children} - + + { + const response = await fetch( + import.meta.env.VITE_BETTERAUTH_URL + + import.meta.env.VITE_BETTERAUTH_BASE_PATH + + "/encryption-session", + { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) return null; + + const data = await response.json(); + return data.sessionId; + }, + connectOnLogin: false, + }} + thirdPartyAuth={{ + getAccessToken: async () => { + const session = await authClient.getSession(); + return session?.data?.session?.token ?? null; + }, + provider: ThirdPartyOAuthProvider.BETTER_AUTH, + }} + > + {children} + + ); @@ -235,7 +309,7 @@ This plugin is maintained by the Openfort team. For bugs, issues or feature requ ``` - The OpenfortProvider requires Wagmi and React Query providers. Set `recoverWalletAutomaticallyAfterAuth` to `false` if you want to handle wallet creation manually. + `OpenfortWagmiBridge` and `getDefaultConfig` are imported from `@openfort/react/wagmi`. @@ -258,69 +332,84 @@ This plugin is maintained by the Openfort team. For bugs, issues or feature requ ## Usage -### Wallet Creation +### Wallet Creation (EVM) -Create an embedded wallet for authenticated users using the `useWallets` hook: +Create an embedded EVM wallet using the `useEthereumEmbeddedWallet` hook: ```tsx -import { RecoveryMethod, useWallets } from "@openfort/react"; +import { useEthereumEmbeddedWallet } from "@openfort/react/ethereum"; +import { RecoveryMethod } from "@openfort/react"; export default function CreateWallet() { - const { isCreating, createWallet, error } = useWallets({ - onSuccess: () => { - console.log("Wallet created successfully!"); - }, - }); + const { status, create, activeWallet } = useEthereumEmbeddedWallet(); - if (isCreating) { + if (status === "creating") { return
Creating wallet...
; } + if (status === "connected" && activeWallet) { + return
Wallet: {activeWallet.address}
; + } + return (
- - {error &&

Error: {error.message}

}
); } ``` +### Wallet Creation (Solana) + +Create an embedded Solana wallet using `useSolanaEmbeddedWallet`: + +```tsx +import { useSolanaEmbeddedWallet } from "@openfort/react/solana"; +import { RecoveryMethod } from "@openfort/react"; + +export default function CreateSolanaWallet() { + const { status, create, activeWallet } = useSolanaEmbeddedWallet(); + + if (status === "connected" && activeWallet) { + return
Solana Wallet: {activeWallet.address}
; + } + + return ( + + ); +} +``` + + + Solana requires the `@solana/kit` peer dependency and `solana` config in `walletConfig`. + + ### Sign Messages Sign messages using Wagmi hooks: @@ -394,24 +483,22 @@ export default function MintTokens() { ### Access User Information -Get user and wallet information: +Get user and wallet information using `useUser`: ```tsx import { useUser } from "@openfort/react"; -import { useAccount } from "wagmi"; export default function UserProfile() { - const { user } = useUser(); - const { address, isConnected } = useAccount(); + const { user, isAuthenticated, isConnected, linkedAccounts } = useUser(); - if (!isConnected) { - return

No wallet connected

; + if (!isAuthenticated) { + return

Not authenticated

; } return (
-

Welcome, {user?.player?.name || user?.linkedAccounts[0]?.email}

-

Wallet Address: {address}

+

Welcome, {user?.player?.name || linkedAccounts?.[0]?.email}

+

Connected: {isConnected ? "Yes" : "No"}

); } @@ -445,13 +532,22 @@ interface EncryptionSessionConfig { ```ts interface OpenfortProviderProps { publishableKey: string; // Openfort publishable key - walletConfig: { + walletConfig?: { shieldPublishableKey: string; // Shield publishable key - ethereumProviderPolicyId?: string; // Policy ID for gas sponsorship - getEncryptionSession: () => Promise; // Retrieve encryption session ID - recoverWalletAutomaticallyAfterAuth?: boolean; // Auto-recover wallet after auth (default: false) + connectOnLogin?: boolean; // Auto-recover/create wallet after auth (default: true) + passkeyDisplayName?: string; // Display name for passkey prompts + // Encryption session (pick one): + getEncryptionSession?: (params: { + accessToken: string; + otpCode?: string; + userId: string; + }) => Promise; + createEncryptedSessionEndpoint?: string; // Or provide an API endpoint + // Chain-specific config: + ethereum?: EthereumConfig; // EVM chain settings (chainId, rpcUrls, policyId) + solana?: SolanaConfig; // Solana cluster and RPC settings }; - thirdPartyAuth: { + thirdPartyAuth?: { getAccessToken: () => Promise; // Retrieve Better Auth token provider: ThirdPartyOAuthProvider; // Authentication provider type }; @@ -471,30 +567,21 @@ Openfort supports three wallet recovery methods: - **Password**: User-defined password for recovery ```tsx -import { RecoveryMethod, useWallets } from "@openfort/react"; +import { useEthereumEmbeddedWallet } from "@openfort/react/ethereum"; +import { RecoveryMethod } from "@openfort/react"; -const { createWallet } = useWallets(); +const { create } = useEthereumEmbeddedWallet(); // Passkey recovery -createWallet({ - recovery: { - recoveryMethod: RecoveryMethod.PASSKEY, - }, -}); +create({ recoveryMethod: RecoveryMethod.PASSKEY }); // Automatic recovery -createWallet({ - recovery: { - recoveryMethod: RecoveryMethod.AUTOMATIC, - }, -}); +create({ recoveryMethod: RecoveryMethod.AUTOMATIC }); // Password recovery -createWallet({ - recovery: { - recoveryMethod: RecoveryMethod.PASSWORD, - password: "your-secure-password", - }, +create({ + recoveryMethod: RecoveryMethod.PASSWORD, + password: "your-secure-password", }); ``` diff --git a/docs/content/docs/plugins/organization.mdx b/docs/content/docs/plugins/organization.mdx index b287d40e25e..de2ceb7b24d 100644 --- a/docs/content/docs/plugins/organization.mdx +++ b/docs/content/docs/plugins/organization.mdx @@ -647,9 +647,9 @@ type setActiveOrganization = { To automatically set an active organization when a session is created, you can use [database hooks](/docs/concepts/database#database-hooks). You'll need to implement logic to determine which organization to set as the initial active organization. ```ts title="auth.ts" -export const auth = betterAuth({ - import { betterAuth } from "better-auth"; +import { betterAuth } from "better-auth"; +export const auth = betterAuth({ databaseHooks: { session: { create: { @@ -2138,6 +2138,7 @@ Table Name: `teamMember` name: "createdAt", type: "Date", description: "Timestamp of when the team member was created", + isOptional: true, }, ]} /> @@ -2167,6 +2168,7 @@ Table Name: `organization` name: "slug", type: "string", description: "The slug of the organization", + isUnique: true, }, { name: "logo", @@ -2258,6 +2260,7 @@ Table Name: `invitation` name: "role", type: "string", description: "The role of the user in the organization", + isOptional: true, }, { name: "status", @@ -2349,6 +2352,7 @@ Table Name: `organizationRole` name: "updatedAt", type: "Date", description: "Timestamp of when the organization role was updated", + isOptional: true, }, ]} /> @@ -2416,6 +2420,7 @@ Table Name: `teamMember` name: "createdAt", type: "Date", description: "Timestamp of when the team member was created", + isOptional: true, }, ]} /> diff --git a/docs/content/docs/plugins/scim.mdx b/docs/content/docs/plugins/scim.mdx index cbcb51a635d..d19ae9f7e4b 100644 --- a/docs/content/docs/plugins/scim.mdx +++ b/docs/content/docs/plugins/scim.mdx @@ -455,11 +455,11 @@ The plugin requires additional fields in the `scimProvider` table to store the p @@ -469,7 +469,7 @@ The `scimProvider` schema is extended as follows: diff --git a/docs/content/docs/plugins/sso.mdx b/docs/content/docs/plugins/sso.mdx index dfa325e9fb2..b0446724212 100644 --- a/docs/content/docs/plugins/sso.mdx +++ b/docs/content/docs/plugins/sso.mdx @@ -1406,15 +1406,15 @@ The plugin requires additional fields in the `ssoProvider` table to store the pr @@ -1424,7 +1424,7 @@ The `ssoProvider` schema is extended as follows: diff --git a/docs/content/docs/plugins/stripe.mdx b/docs/content/docs/plugins/stripe.mdx index 93123e62609..5f4e70aea3c 100644 --- a/docs/content/docs/plugins/stripe.mdx +++ b/docs/content/docs/plugins/stripe.mdx @@ -739,9 +739,9 @@ Table Name: `subscription` description: "The Stripe subscription ID", isOptional: true }, - { - name: "status", - type: "string", + { + name: "status", + type: "string", description: "The status of the subscription (active, canceled, etc.)", defaultValue: "incomplete" }, @@ -874,6 +874,7 @@ stripe({ | `limits` | `object` | Limits for plan (e.g. `{ projects: 10, storage: 5 }`). | | `group` | `string` | A group name for categorizing plans. | | `seatPriceId` | `string` | Per-seat billing price ID. Requires the `organization` plugin. | +| `prorationBehavior` | `string` | Proration behavior on subscription updates: `"create_prorations"` (default), `"always_invoice"`, or `"none"`. | | `lineItems` | `LineItem[]` | Additional line items to include in the checkout session. | | `freeTrial` | `object` | Trial configuration. See [below](#free-trial-configuration). | diff --git a/docs/content/docs/plugins/username.mdx b/docs/content/docs/plugins/username.mdx index ae2c274ccc0..507ce823943 100644 --- a/docs/content/docs/plugins/username.mdx +++ b/docs/content/docs/plugins/username.mdx @@ -345,13 +345,14 @@ The plugin requires 2 fields to be added to the user table: name: "username", type: "string", description: "The username of the user", - isUnique: true + isUnique: true, + isOptional: true }, { name: "displayUsername", type: "string", description: "Non normalized username of the user", - isUnique: true + isOptional: true }, ]} /> diff --git a/docs/content/docs/reference/options.mdx b/docs/content/docs/reference/options.mdx index 75960d851d5..93b203d9115 100644 --- a/docs/content/docs/reference/options.mdx +++ b/docs/content/docs/reference/options.mdx @@ -192,7 +192,7 @@ Read more about databases [here](/docs/concepts/database). ## `secondaryStorage` -Secondary storage configuration used to store session and rate limit data. +Secondary storage configuration used to store session data, verification records, and rate limit data. ```ts import { betterAuth } from "better-auth"; @@ -491,12 +491,13 @@ Verification configuration options. ```ts import { betterAuth } from "better-auth"; export const auth = betterAuth({ + secondaryStorage: { + // your Redis or KV implementation + }, verification: { - modelName: "verifications", - fields: { - userId: "user_id" - }, - disableCleanup: false + disableCleanup: false, + storeIdentifier: "hashed", + storeInDatabase: false }, }) ``` @@ -504,6 +505,10 @@ export const auth = betterAuth({ - `modelName`: The model name for the verification table - `fields`: Map fields to different column names - `disableCleanup`: Disable cleaning up expired values when a verification value is fetched +- `storeIdentifier`: How to store verification identifiers such as tokens and OTP keys. Supports `"plain"`, `"hashed"`, or a custom hasher. You can also use `{ default, overrides }` to apply different strategies per identifier prefix. +- `storeInDatabase`: Store verification records in the database even when `secondaryStorage` is configured. Default is `false`. + +If `secondaryStorage` is configured, verification records are stored there by default. That behavior applies to flows that use Better Auth's shared verification layer, including OTP and magic-link style flows. ## `rateLimit` diff --git a/docs/content/docs/reference/security.mdx b/docs/content/docs/reference/security.mdx index f0901d63527..58444f979a7 100644 --- a/docs/content/docs/reference/security.mdx +++ b/docs/content/docs/reference/security.mdx @@ -200,7 +200,7 @@ Trusted origins prevent CSRF attacks and block open redirects. You can set a lis ### Basic Usage -The most basic usage is to specify exact origins: +The most basic usage is to specify exact origins, below is an example of a trusted origins configuration: ```typescript { @@ -211,6 +211,9 @@ The most basic usage is to specify exact origins: ] } ``` + + Do not leave the localhost origin in a trusted origins list of a production auth instance. + ### Wildcard Origins diff --git a/docs/package.json b/docs/package.json index ee67b916981..f9ae96913f1 100644 --- a/docs/package.json +++ b/docs/package.json @@ -66,7 +66,7 @@ "lucide-react": "^0.563.0", "mermaid": "^11.12.3", "motion": "^12.34.0", - "next": "16.1.6", + "next": "16.2.0", "next-themes": "^0.4.6", "prism-react-renderer": "^2.4.1", "react": "catalog:react19", diff --git a/e2e/adapter/test/adapter-factory/index.ts b/e2e/adapter/test/adapter-factory/index.ts index c60974851e9..5846aa74f81 100644 --- a/e2e/adapter/test/adapter-factory/index.ts +++ b/e2e/adapter/test/adapter-factory/index.ts @@ -1,6 +1,9 @@ -export * from "./auth-flow"; -export * from "./basic"; -export * from "./joins"; -export * from "./number-id"; -export * from "./transactions"; -export * from "./uuid"; +export { + authFlowTestSuite, + getNormalTestSuiteTests, + joinsTestSuite, + normalTestSuite, + numberIdTestSuite, + transactionsTestSuite, + uuidTestSuite, +} from "@better-auth/test-utils/adapter"; diff --git a/e2e/adapter/test/mongo-adapter/adapter.mongo-db.test.ts b/e2e/adapter/test/mongo-adapter/adapter.mongo-db.test.ts index 04e8cc3c6fb..63dc4cc053d 100644 --- a/e2e/adapter/test/mongo-adapter/adapter.mongo-db.test.ts +++ b/e2e/adapter/test/mongo-adapter/adapter.mongo-db.test.ts @@ -8,6 +8,7 @@ import { joinsTestSuite, normalTestSuite, transactionsTestSuite, + uuidTestSuite, } from "../adapter-factory"; const dbClient = async (connectionString: string, dbName: string) => { @@ -83,8 +84,8 @@ const { execute } = await testAdapter({ transactionsTestSuite(), joinsTestSuite(), updateObjectIdTestSuite(), + uuidTestSuite(), // numberIdTestSuite(), // no support - // uuidTestSuite() // no support ], customIdGenerator: () => new ObjectId().toHexString(), }); diff --git a/e2e/integration/vanilla-node/e2e/app.ts b/e2e/integration/vanilla-node/e2e/app.ts index 826c66f1583..b8410504c7d 100644 --- a/e2e/integration/vanilla-node/e2e/app.ts +++ b/e2e/integration/vanilla-node/e2e/app.ts @@ -1,11 +1,13 @@ import { createServer } from "node:http"; import { DatabaseSync } from "node:sqlite"; +import type { BetterAuthOptions } from "better-auth"; import { betterAuth } from "better-auth"; import { getMigrations } from "better-auth/db/migration"; import { toNodeHandler } from "better-auth/node"; export async function createAuthServer( baseURL: string = "http://localhost:3000", + overrides?: Partial, ) { const database = new DatabaseSync(":memory:"); @@ -20,6 +22,7 @@ export async function createAuthServer( "http://localhost:*", // Dynamic frontend port "http://test.com:*", // Cross-domain test ], + ...overrides, }); const { runMigrations } = await getMigrations(auth.options); diff --git a/e2e/integration/vanilla-node/e2e/cookie-cache-signout.spec.ts b/e2e/integration/vanilla-node/e2e/cookie-cache-signout.spec.ts new file mode 100644 index 00000000000..501cad70ee5 --- /dev/null +++ b/e2e/integration/vanilla-node/e2e/cookie-cache-signout.spec.ts @@ -0,0 +1,68 @@ +import { expect, test } from "@playwright/test"; +import { runClient, setup } from "./utils"; + +/** + * @see https://github.com/better-auth/better-auth/issues/8273 + */ +const { ref, start, clean } = setup({ + session: { + cookieCache: { + enabled: true, + maxAge: 60, + strategy: "compact", + }, + }, +}); + +test.describe("sign-out with cookieCache", () => { + test.beforeEach(async () => start()); + test.afterEach(async () => clean()); + + test("should clear both session_token and session_data cookies on sign-out", async ({ + page, + }) => { + await page.goto( + `http://localhost:${ref.clientPort}/?port=${ref.serverPort}`, + ); + await page.locator("text=Ready").waitFor(); + + // Sign in + await runClient(page, ({ client }) => + client.signIn.email({ + email: "test@test.com", + password: "password123", + }), + ); + + // Verify both cookies are set + let cookies = await page.context().cookies(); + expect( + cookies.find((c) => c.name === "better-auth.session_token"), + ).toBeDefined(); + expect( + cookies.find((c) => c.name === "better-auth.session_data"), + ).toBeDefined(); + + // Verify session is valid + const session = await runClient(page, ({ client }) => client.getSession()); + expect(session.data?.user.email).toBe("test@test.com"); + + // Sign out + await runClient(page, ({ client }) => client.signOut()); + + // Verify both cookies are cleared by the browser + cookies = await page.context().cookies(); + expect( + cookies.find((c) => c.name === "better-auth.session_token"), + ).toBeUndefined(); + expect( + cookies.find((c) => c.name === "better-auth.session_data"), + ).toBeUndefined(); + + // Verify session is null + const sessionAfter = await runClient(page, ({ client }) => + client.getSession(), + ); + expect(sessionAfter.data).toBeNull(); + }); +}); diff --git a/e2e/integration/vanilla-node/e2e/utils.ts b/e2e/integration/vanilla-node/e2e/utils.ts index 6b5b8c113bd..9e593dd9f99 100644 --- a/e2e/integration/vanilla-node/e2e/utils.ts +++ b/e2e/integration/vanilla-node/e2e/utils.ts @@ -3,6 +3,7 @@ import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; import { terminate } from "@better-auth-test/test-utils/playwright"; import type { Page } from "@playwright/test"; +import type { BetterAuthOptions } from "better-auth"; import { createAuthServer } from "./app"; const root = fileURLToPath(new URL("../", import.meta.url)); @@ -15,7 +16,7 @@ export async function runClient( return page.evaluate(fn, { client }); } -export function setup() { +export function setup(overrides?: Partial) { let server: Awaited>; let clientChild: ChildProcessWithoutNullStreams; const ref: { @@ -28,7 +29,7 @@ export function setup() { return { ref, start: async () => { - server = await createAuthServer(); + server = await createAuthServer(undefined, overrides); clientChild = spawn("pnpm", ["run", "start:client"], { cwd: root, stdio: "pipe", diff --git a/e2e/smoke/test/fixtures/cloudflare/package.json b/e2e/smoke/test/fixtures/cloudflare/package.json index 67a6b072243..ae1b986f6f9 100644 --- a/e2e/smoke/test/fixtures/cloudflare/package.json +++ b/e2e/smoke/test/fixtures/cloudflare/package.json @@ -5,7 +5,7 @@ "@better-auth/sso": "workspace:*", "better-auth": "workspace:*", "drizzle-orm": "^0.45.1", - "hono": "^4.12.3" + "hono": "^4.12.7" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.12.18", diff --git a/e2e/smoke/test/fixtures/ipv6/package.json b/e2e/smoke/test/fixtures/ipv6/package.json index 40928549873..76e80341ee1 100644 --- a/e2e/smoke/test/fixtures/ipv6/package.json +++ b/e2e/smoke/test/fixtures/ipv6/package.json @@ -3,8 +3,8 @@ "private": true, "type": "module", "dependencies": { - "@hono/node-server": "^1.19.9", + "@hono/node-server": "^1.19.10", "better-auth": "workspace:*", - "hono": "^4.12.3" + "hono": "^4.12.7" } } diff --git a/knip.jsonc b/knip.jsonc index 5a65fb8facf..e18ff9a0a98 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -86,6 +86,7 @@ "better-auth", "c12", "chalk", + "get-tsconfig", "open", "prettier", "prompts", @@ -116,6 +117,18 @@ ], "project": ["src/**/*.ts", "test/**/*.ts"] }, + "packages/kysely-adapter": { + "ignoreDependencies": [ + // optional peer dependency + "kysely" + ] + }, + "packages/mongo-adapter": { + "ignoreDependencies": [ + // optional peer dependency + "mongodb" + ] + }, "packages/stripe": { "ignoreDependencies": [ // implicit dependencies diff --git a/landing/.env.example b/landing/.env.example index 9abf53a3f25..dc3a99420ef 100644 --- a/landing/.env.example +++ b/landing/.env.example @@ -1,8 +1,12 @@ # GitHub API - community stats GITHUB_TOKEN= -# Inkeep API - AI chat -INKEEP_API_KEY= +# Openrouter API - docs AI chat +OPENROUTER_API_KEY= + +# Upstash Redis - AI chat rate limiting +UPSTASH_REDIS_REST_URL= +UPSTASH_REDIS_REST_TOKEN= # Typesense - Docs search NEXT_PUBLIC_TYPESENSE_SERVER_URL= @@ -11,3 +15,4 @@ TYPESENSE_ADMIN_API_KEY= # Next.js - public URL NEXT_PUBLIC_URL=http://localhost:3000 + diff --git a/landing/app/api/docs/chat/route.ts b/landing/app/api/docs/chat/route.ts index b3bcd631890..e0ea8b3f56c 100644 --- a/landing/app/api/docs/chat/route.ts +++ b/landing/app/api/docs/chat/route.ts @@ -1,21 +1 @@ -import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; -import { convertToModelMessages, streamText } from "ai"; - -const inkeep = createOpenAICompatible({ - name: "inkeep", - baseURL: "https://api.inkeep.com/v1", - apiKey: process.env.INKEEP_API_KEY, -}); - -export async function POST(req: Request) { - const { messages } = await req.json(); - - const result = streamText({ - model: inkeep.chatModel("inkeep-qa-expert"), - messages: await convertToModelMessages(messages, { - ignoreIncompleteToolCalls: true, - }), - }); - - return result.toUIMessageStreamResponse(); -} +export { POST } from "@/lib/ai-chat/route"; diff --git a/landing/app/blog/[[...slug]]/page.tsx b/landing/app/blog/[[...slug]]/page.tsx index e3d99cdb849..920c4d04cb8 100644 --- a/landing/app/blog/[[...slug]]/page.tsx +++ b/landing/app/blog/[[...slug]]/page.tsx @@ -7,6 +7,7 @@ import Link from "next/link"; import { notFound } from "next/navigation"; import { BlogLeftPanel } from "@/components/blog/blog-left-panel"; import { Callout } from "@/components/ui/callout"; +import { createMetadata } from "@/lib/metadata"; import { blogs } from "@/lib/source"; import { cn } from "@/lib/utils"; @@ -45,9 +46,9 @@ function BlogList() { href={`/blog/${post.slugs.join("/")}`} className="group block border-b border-dashed border-foreground/[0.06] px-5 sm:px-6 lg:px-8 py-5 transition-colors hover:bg-foreground/[0.02]" > -
+
{post.data?.image && ( -
+
{post.data.title} )}
-

+

{post.data.title}

{post.data.description && ( -

+

{post.data.description}

)} -
+
{post.data.author?.name && ( <> @@ -76,20 +77,14 @@ function BlogList() { )} {formatDate(post.data.date)} - {post.data.tags && post.data.tags.length > 0 && ( - <> - · - {post.data.tags.slice(0, 3).map((tag: string) => ( - - #{tag} - - ))} - - )}
+ {post.data.tags && post.data.tags.length > 0 && ( +
+ {post.data.tags.slice(0, 3).map((tag: string) => ( + #{tag} + ))} +
+ )}
→ @@ -137,52 +132,7 @@ export default async function Page({ {/* Right panel — blog content */}
-
- {/* Header */} -

- {title} -

- {description && ( -

- {description} -

- )} - - {/* Author & date */} -
- {page.data?.author?.name && ( - - {page.data.author.name} - - )} - {page.data?.author?.twitter && ( - <> - · - - @{page.data.author.twitter} - - - )} - {date && ( - <> - · - - - )} -
- -
- +
{/* Article body */}
; diff --git a/landing/app/changelog/page.tsx b/landing/app/changelog/page.tsx index 57090d91ef9..b9c9ebfcf61 100644 --- a/landing/app/changelog/page.tsx +++ b/landing/app/changelog/page.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import { HalftoneBackground } from "@/components/landing/halftone-bg"; +import { createMetadata } from "@/lib/metadata"; import { ChangelogContent } from "./changelog-content"; export const dynamic = "force-static"; @@ -148,7 +149,7 @@ export default async function ChangelogPage() { ); } -export const metadata = { - title: "Changelog - Better Auth", +export const metadata = createMetadata({ + title: "Changelog", description: "Latest changes, fixes, and updates to Better Auth", -}; +}); diff --git a/landing/app/community/community-client.tsx b/landing/app/community/community-client.tsx index a9847131efc..c2f01b9b2db 100644 --- a/landing/app/community/community-client.tsx +++ b/landing/app/community/community-client.tsx @@ -205,7 +205,7 @@ const platforms = [ function CommunityHero({ stats }: { stats: CommunityStats }) { return ( {formatNumber(stats.npmDownloads)} - /mo + /year

@@ -266,7 +266,7 @@ function CommunityHero({ stats }: { stats: CommunityStats }) { ].map((item, i) => ( ))}
- - {/* CTA */} -
- - - Star on GitHub - -
); @@ -317,7 +304,7 @@ function StatCard({ }) { return (
-
+
- +

{subtext} - +

@@ -353,7 +340,7 @@ function PlatformCard({ return ( -
+
{platform.cta} @@ -423,11 +410,11 @@ export function CommunityPageClient({ stats }: { stats: CommunityStats }) { {/* Section: Statistics */} -

+

# In Numbers

@@ -465,11 +452,11 @@ export function CommunityPageClient({ stats }: { stats: CommunityStats }) { {/* Section: Platforms */} -

+

# Join Us On

@@ -486,7 +473,7 @@ export function CommunityPageClient({ stats }: { stats: CommunityStats }) { {/* Merch */} @@ -494,7 +481,7 @@ export function CommunityPageClient({ stats }: { stats: CommunityStats }) { href="https://better-merch.dev" target="_blank" rel="noreferrer" - className="flex items-center justify-between w-full px-5 py-4 border border-dashed border-foreground/[0.08] hover:border-foreground/[0.14] hover:bg-foreground/[0.02] transition-all group" + className="flex items-center justify-between w-full px-5 py-4 border border-dashed border-foreground/20 hover:border-foreground/30 hover:bg-foreground/5 transition-all group" >
; diff --git a/landing/app/globals.css b/landing/app/globals.css index f57e0281904..80fad3129e5 100644 --- a/landing/app/globals.css +++ b/landing/app/globals.css @@ -386,6 +386,7 @@ --scrollbar-thumb-hover: var(--ring); --scrollbar-track: transparent; --radius: 0.2rem; + --landing-topbar-height: 48px; --fd-nav-height: 56px; --fd-banner-height: 0px; --fd-tocnav-height: 0px; @@ -409,6 +410,12 @@ --spacing: 0.25rem; } +@media (min-width: 64rem) { + :root { + --landing-topbar-height: 44px; + } +} + .dark { --background: hsl(0 0% 0%); --foreground: oklch(0.985 0.001 106.423); @@ -629,10 +636,10 @@ html:not([data-anchor-scrolling]) { --fd-layout-width: 100vw; --fd-layout-offset: 0px; --fd-page-width: 900px; - --fd-banner-height: 44px; + --fd-banner-height: var(--landing-topbar-height); --landing-left-pane-width: min(22vw, 300px); --staggered-nav-cta-width: 128px; - padding-top: 44px; + padding-top: var(--landing-topbar-height); } @media (min-width: 64rem) { @@ -641,7 +648,6 @@ html:not([data-anchor-scrolling]) { calc(100vw - var(--landing-left-pane-width)), 1200px ); - --fd-header-height: -1px; padding-inline-start: var(--landing-left-pane-width); } @@ -649,7 +655,7 @@ html:not([data-anchor-scrolling]) { content: ""; position: fixed; inset-block-start: 0; - height: 44px; + height: var(--landing-topbar-height); inset-inline-start: calc(var(--landing-left-pane-width) - 1px); width: 1px; background: var(--color-border); @@ -698,7 +704,7 @@ html:not([data-anchor-scrolling]) { /* Blog layout: matches docs pattern with narrow left pane + TOC sidebar */ .blog-layout { - padding-top: 44px; + padding-top: var(--landing-topbar-height); } @media (min-width: 64rem) { @@ -710,7 +716,7 @@ html:not([data-anchor-scrolling]) { content: ""; position: fixed; inset-block-start: 0; - height: 44px; + height: var(--landing-topbar-height); inset-inline-start: calc(var(--landing-left-pane-width) - 1px); width: 1px; background: var(--color-border); diff --git a/landing/app/layout.tsx b/landing/app/layout.tsx index 8d78b488b94..25b3466f9ab 100644 --- a/landing/app/layout.tsx +++ b/landing/app/layout.tsx @@ -7,6 +7,7 @@ import Script from "next/script"; import type { ReactNode } from "react"; import { StaggeredNavFiles } from "@/components/landing/staggered-nav-files"; import { Providers } from "@/components/providers"; +import { createMetadata } from "@/lib/metadata"; const fontSans = Geist({ subsets: ["latin"], @@ -18,43 +19,13 @@ const fontMono = Geist_Mono({ variable: "--font-mono", }); -export const metadata: Metadata = { - metadataBase: new URL( - process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : process.env.NODE_ENV === "production" - ? "https://better-auth.com" - : (process.env.NEXT_PUBLIC_URL ?? "http://localhost:3000"), - ), +export const metadata: Metadata = createMetadata({ title: { template: "%s | Better Auth", default: "Better Auth", }, description: "The Most Comprehensive Authentication Framework", - icons: { - icon: [ - { url: "/favicon/favicon.ico", sizes: "any" }, - { - url: "/favicon/favicon-32x32.png", - sizes: "32x32", - type: "image/png", - }, - { - url: "/favicon/favicon-16x16.png", - sizes: "16x16", - type: "image/png", - }, - ], - apple: "/favicon/apple-touch-icon.png", - }, - openGraph: { - images: ["/og.png"], - }, - twitter: { - card: "summary_large_image", - images: ["/og.png"], - }, -}; +}); export default function RootLayout({ children }: { children: ReactNode }) { return ( diff --git a/landing/app/page.tsx b/landing/app/page.tsx index 931fc1c5847..df935d109dc 100644 --- a/landing/app/page.tsx +++ b/landing/app/page.tsx @@ -10,7 +10,7 @@ export default async function HomePage() { return (
-
+
{/* Left side — Hero title */}
@@ -71,6 +71,7 @@ export default async function HomePage() { stats={{ npmDownloads: communityStats.npmDownloads, githubStars: communityStats.githubStars, + contributors: communityStats.contributors, }} />
diff --git a/landing/app/products/[tab]/page.tsx b/landing/app/products/[tab]/page.tsx index fbede66619f..8c75ed74819 100644 --- a/landing/app/products/[tab]/page.tsx +++ b/landing/app/products/[tab]/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { redirect } from "next/navigation"; +import { createMetadata } from "@/lib/metadata"; import { FrameworkContent } from "./_components/framework-content"; import { InfrastructureContent } from "./_components/infrastructure-content"; @@ -30,7 +31,7 @@ export async function generateMetadata({ const { tab } = await params; const meta = tabs[tab as Tab]; if (!meta) return {}; - return { title: meta.title, description: meta.description }; + return createMetadata({ title: meta.title, description: meta.description }); } export default async function TabPage({ diff --git a/landing/app/sitemap.ts b/landing/app/sitemap.ts new file mode 100644 index 00000000000..a6f94cf98c5 --- /dev/null +++ b/landing/app/sitemap.ts @@ -0,0 +1,72 @@ +import type { MetadataRoute } from "next"; +import { blogs, source } from "@/lib/source"; + +const BASE_URL = "https://better-auth.com"; + +export default async function sitemap(): Promise { + const basePages: MetadataRoute.Sitemap = [ + { + url: BASE_URL, + lastModified: new Date(), + changeFrequency: "daily", + priority: 1.0, + }, + { + url: `${BASE_URL}/blog`, + lastModified: new Date(), + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: `${BASE_URL}/changelog`, + lastModified: new Date(), + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: `${BASE_URL}/community`, + lastModified: new Date(), + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: `${BASE_URL}/enterprise`, + lastModified: new Date(), + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: `${BASE_URL}/products/framework`, + lastModified: new Date(), + changeFrequency: "weekly", + priority: 0.8, + }, + { + url: `${BASE_URL}/products/infrastructure`, + lastModified: new Date(), + changeFrequency: "weekly", + priority: 0.8, + }, + ]; + + const docPages: MetadataRoute.Sitemap = await Promise.all( + source.getPages().map(async (page) => { + const { lastModified } = await page.data.load(); + return { + url: `${BASE_URL}${page.url}`, + lastModified: lastModified ? new Date(lastModified) : new Date(), + changeFrequency: "weekly", + priority: 0.7, + }; + }), + ); + + const blogPages: MetadataRoute.Sitemap = blogs.getPages().map((page) => ({ + url: `${BASE_URL}${page.url.replace("/blogs/", "/blog/")}`, + lastModified: page.data.date ? new Date(page.data.date) : new Date(), + changeFrequency: "monthly", + priority: 0.6, + })); + + return [...basePages, ...docPages, ...blogPages]; +} diff --git a/landing/components/ai-chat.tsx b/landing/components/ai-chat.tsx index 6319ed7905e..4b9d086a3cf 100644 --- a/landing/components/ai-chat.tsx +++ b/landing/components/ai-chat.tsx @@ -183,15 +183,7 @@ function MobileDrawerPanel() {
AI Chat - Powered by{" "} - - Inkeep AI - + Better Auth docs assistant

AI Chat

- Powered by{" "} - - Inkeep AI - + Better Auth docs assistant

)}
@@ -558,6 +543,27 @@ function PanelInput({ autoFocus = false }: { autoFocus?: boolean }) { ); } +// ─── Thinking Indicator ────────────────────────────────────────────────────── + +function ThinkingIndicator() { + const { status, messages } = useChatContext(); + const lastMessage = messages.at(-1); + const hasNoText = + !lastMessage || + lastMessage.role !== "assistant" || + !lastMessage.parts?.some((p) => p.type === "text" && p.text.length > 0); + + if (status !== "submitted" && !(status === "streaming" && hasNoText)) + return null; + + return ( +
+ + Looking through docs... +
+ ); +} + // ─── Message ───────────────────────────────────────────────────────────────── const roleName: Record = { @@ -587,13 +593,6 @@ function Message({ } } - if (message.role === "assistant") { - // Remove Inkeep citation markers like (1), (2) etc. - markdown = markdown.replace(/\(\d+\)/g, ""); - // Remove stray backslashes used as line breaks by Inkeep - markdown = markdown.replace(/^\s*\\\s*$/gm, ""); - } - // Fix incomplete code blocks const codeBlockCount = (markdown.match(/```/g) || []).length; if (codeBlockCount % 2 !== 0) { diff --git a/landing/components/blog/blog-left-panel.tsx b/landing/components/blog/blog-left-panel.tsx index bfcdb6a2404..f3d8d138056 100644 --- a/landing/components/blog/blog-left-panel.tsx +++ b/landing/components/blog/blog-left-panel.tsx @@ -30,33 +30,7 @@ export function BlogLeftPanel({ postCount, post }: BlogLeftPanelProps) {
- {/* Mobile: just a back link */} -
- - - - - - All posts - - -
- {/* Desktop: full panel with title, meta, TOC */} -
+
-
-

+
+

{post.title}

{post.description && ( -

+

{post.description}

)}
-
+
{post.author?.name && ( {post.author.name} )} - {post.author?.name && post.date && ( - · + {post.author?.twitter && ( + <> + · + + @{post.author.twitter} + + + )} + {post.date && ( + <> + · + {formatDate(post.date)} + )} - {post.date && {formatDate(post.date)}}

diff --git a/landing/components/community-plugins-table.tsx b/landing/components/community-plugins-table.tsx index ca83e65ab29..4437bbb9929 100644 --- a/landing/components/community-plugins-table.tsx +++ b/landing/components/community-plugins-table.tsx @@ -301,6 +301,17 @@ export const communityPlugins: CommunityPlugin[] = [ avatar: "https://github.com/0-Sandy.png", }, }, + { + name: "better-auth-usos", + url: "https://github.com/qamarq/better-auth-usos", + description: + "USOS plugin for Better Auth - allows students to authenticate using their university credentials via the USOS API. Using oauth 1a.", + author: { + name: "qamarq", + github: "qamarq", + avatar: "https://github.com/qamarq.png", + }, + }, ]; export function CommunityPluginsTable() { const [sorting, setSorting] = useState([]); diff --git a/landing/components/docs/docs-sidebar.tsx b/landing/components/docs/docs-sidebar.tsx index 835f035f9de..90120a9f621 100644 --- a/landing/components/docs/docs-sidebar.tsx +++ b/landing/components/docs/docs-sidebar.tsx @@ -30,8 +30,10 @@ export function DocsSidebar() { item.list.some( (listItem) => listItem.href === pathname || - (listItem.hasSubpages && pathname.startsWith(`${listItem.href}/`)) || - listItem.subpages?.some((sp) => pathname === sp.href), + (listItem.subpages && + listItem.subpages.length > 0 && + pathname.startsWith(`${listItem.href}/`)) || + listItem.subpages?.some((sp) => sp.href && pathname === sp.href), ), ); return defaultValue === -1 ? 0 : defaultValue; @@ -69,7 +71,7 @@ export function DocsSidebar() { initial={{ x: -24, opacity: 0 }} animate={{ x: 0, opacity: 1 }} transition={{ duration: 0.28, ease: "easeOut" }} - className="fixed left-0 top-[42px] bottom-0 w-[22vw] max-w-[300px] hidden lg:flex flex-col z-30 bg-background border-r border-foreground/5 transition-[width] duration-300 ease-out" + className="fixed left-0 top-(--landing-topbar-height) bottom-0 w-[22vw] max-w-[300px] hidden lg:flex flex-col z-30 bg-background border-r border-foreground/5 transition-[width] duration-300 ease-out" > {/* Branch switcher */} @@ -115,7 +117,7 @@ export function DocsSidebar() { }} > - {section.title} + {section.title} +