Merge branch 'canary' into fix/expo-oauth-state-cookie

This commit is contained in:
Taesu
2026-01-14 09:27:06 +09:00
committed by GitHub
131 changed files with 5533 additions and 1608 deletions
+12 -9
View File
@@ -13,28 +13,31 @@ on:
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
(
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') && contains(fromJSON('["MEMBER", "OWNER", "COLLABORATOR"]'), github.event.comment.author_association)) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && contains(fromJSON('["MEMBER", "OWNER", "COLLABORATOR"]'), github.event.comment.author_association)) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && contains(fromJSON('["MEMBER", "OWNER", "COLLABORATOR"]'), github.event.review.author_association)) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) && contains(fromJSON('["MEMBER", "OWNER", "COLLABORATOR"]'), github.event.issue.author_association))
)
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
contents: write
pull-requests: write
issues: write
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
uses: anthropics/claude-code-action@1b8ee3b94104046d71fde52ec3557651ad8c0d71 # v1.0.29
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
+1 -1
View File
@@ -55,7 +55,7 @@ jobs:
- uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3
with:
deno-version: 2.6.3
deno-version: 2.6.4
- name: Smoke
env:
+7 -1
View File
@@ -12,7 +12,10 @@ function addCorsHeaders(url: URL, headers: Headers) {
"/api/auth/oauth2/register",
].includes(url.pathname)
) {
headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
headers.set(
"Access-Control-Allow-Methods",
"GET, POST, PUT, PATCH, DELETE, OPTIONS",
);
headers.set("Access-Control-Allow-Origin", "*");
headers.set("Access-Control-Allow-Headers", "authorization, content-type");
headers.set(
@@ -35,6 +38,9 @@ const handler = toNextJsHandler(auth);
export const GET = withCors(handler.GET);
export const POST = withCors(handler.POST);
export const PUT = withCors(handler.PUT);
export const PATCH = withCors(handler.PATCH);
export const DELETE = withCors(handler.DELETE);
export async function OPTIONS(req: NextRequest): Promise<NextResponse> {
const headers = new Headers();
+118
View File
@@ -1,5 +1,7 @@
import { oauthProvider } from "@better-auth/oauth-provider";
import { passkey } from "@better-auth/passkey";
import { scim } from "@better-auth/scim";
import { sso } from "@better-auth/sso";
import { stripe } from "@better-auth/stripe";
import { LibsqlDialect } from "@libsql/kysely-libsql";
import type { BetterAuthOptions } from "better-auth";
@@ -221,6 +223,122 @@ const authOptions = {
},
},
}),
sso({
defaultSSO: [
{
domain: "http://localhost:3000",
providerId: "sso",
samlConfig: {
issuer: "http://localhost:3000/api/auth/sso/saml2/sp/metadata",
entryPoint:
"https://dummyidp.com/apps/app_01k16v4vb5yytywqjjvv2b3435",
cert: `-----BEGIN CERTIFICATE-----
MIIDBzCCAe+gAwIBAgIUCLBK4f75EXEe4gyroYnVaqLoSp4wDQYJKoZIhvcNAQEL
BQAwEzERMA8GA1UEAwwIZHVtbXlpZHAwHhcNMjQwNTEzMjE1NDE2WhcNMzQwNTEx
MjE1NDE2WjATMREwDwYDVQQDDAhkdW1teWlkcDCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAKhmgQmWb8NvGhz952XY4SlJlpWIK72RilhOZS9frDYhqWVJ
HsGH9Z7sSzrM/0+YvCyEWuZV9gpMeIaHZxEPDqW3RJ7KG51fn/s/qFvwctf+CZDj
yfGDzYs+XIgf7p56U48EmYeWpB/aUW64gSbnPqrtWmVFBisOfIx5aY3NubtTsn+g
0XbdX0L57+NgSvPQHXh/GPXA7xCIWm54G5kqjozxbKEFA0DS3yb6oHRQWHqIAM/7
mJMdUVZNIV1q7c2JIgAl23uDWq+2KTE2R5liP/KjvjwKonVKtTqGqX6ei25rsTHO
aDpBH/LdQK2txgsm7R7+IThWNvUI0TttrmwBqyMCAwEAAaNTMFEwHQYDVR0OBBYE
FD142gxIAJMhpgMkgpzmRNoW9XbEMB8GA1UdIwQYMBaAFD142gxIAJMhpgMkgpzm
RNoW9XbEMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADQd6k6z
FIc20GfGHY5C2MFwyGOmP5/UG/JiTq7Zky28G6D0NA0je+GztzXx7VYDfCfHxLcm
2k5t9nYhb9kVawiLUUDVF6s+yZUXA4gUA3KoTWh1/oRxR3ggW7dKYm9fsNOdQAbx
UUkzp7HLZ45ZlpKUS0hO7es+fPyF5KVw0g0SrtQWwWucnQMAQE9m+B0aOf+92y7J
QkdgdR8Gd/XZ4NZfoOnKV7A1utT4rWxYCgICeRTHx9tly5OhPW4hQr5qOpngcsJ9
vhr86IjznQXhfj3hql5lA3VbHW04ro37ROIkh2bShDq5dwJJHpYCGrF3MQv8S3m+
jzGhYL6m9gFTm/8=
-----END CERTIFICATE-----`,
spMetadata: {
metadata: `
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://localhost:3000/api/auth/sso/saml2/sp/metadata">
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIIE3jCCAsYCCQDE5FzoAkixzzANBgkqhkiG9w0BAQsFADAxMQswCQYDVQQGEwJVUzEQMA4GA1UECAwHRmxvcmlkYTEQMA4GA1UEBwwHT3JsYW5kbzAeFw0yMzExMTkxMjUyMTVaFw0zMzExMTYxMjUyMTVaMDExCzAJBgNVBAYTAlVTMRAwDgYDVQQIDAdGbG9yaWRhMRAwDgYDVQQHDAdPcmxhbmRvMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2ELJsLZs4yBH7a2U5pA7xw+Oiut7b/ROKh2BqSTKRbEG4xy7WwljT02Mh7GTjLvswtZSUObWFO5v14HNORa3+J9JT2DH+9F+FJ770HX8a3cKYBNQt3xP4IeUyjI3QWzrGtkYPwSZ74tDpAUtuqPAxtoCaZXFDtX6lvCJDqiPnfxRZrKkepYWINSwu4DRpg6KoiPWRCYTsEcCzImInzlACdM97jpG1gLGA6a4dmjalQbRtvC56N0Z56gIhYq2F5JdzB2a10pqoIY8ggXZGIJS9I++8mmdTj6So5pPxLwnCYUhwDew1/DMbi9xIwYozs9pEtHCTn1l34jldDwTziVAxGQZO7QUuoMl997zqcPS7pVWRnfz5odKuytLvQDA0lRVfzOxtqbM3qVhoLT2iDmnuEtlZzgfbt4WEuT2538qxZJkFRpZQIrTj3ybqmWAv36Cp49dfeMwaqjhfX7/mVfbsPMSC653DSZBB+n+Uz0FC3QhH+vIdNhXNAQ5tBseHUR6pXiMnLtI/WVbMvpvFwK2faFTcx1oaP/Qk6yCq66tJvPbnatT9qGF8rdBJmAk9aBdQTI+hAh5mDtDweCrgVL+Tm/+Q85hSl4HGzH/LhLVS478tZVX+o+0yorZ35LCW3e4v8iX+1VEGSdg2ooOWtbSSXK2cYZr8ilyUQp0KueenR0CAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAsonAahruWuHlYbDNQVD0ryhL/b+ttKKqVeT87XYDkvVhlSSSVAKcCwK/UU6z8Ty9dODUkd93Qsbof8fGMlXeYCtDHMRanvWLtk4wVkAMyNkDYHzJ1FbO7v44ZBbqNzSLy2kosbRELlcz+P3/42xumlDqAw/k13tWUdlLDxb0pd8R5yBev6HkIdJBIWtKmUuI+e8F/yTNf5kY7HO1p0NeKdVeZw4Ydw33+BwVxVNmhIxzdP5ZFQv0XRFWhCMo/6RLEepCvWUp/T1WRFqgwAdURaQrvvfpjO/Ls+neht1SWDeP8RRgsDrXIc3gZfaD8q4liIDTZ6HsFi7FmLbZatU8jJ4pCstxQLCvmix+1zF6Fwa9V5OApSTbVqBOsDZbJxeAoSzy5Wx28wufAZT4Kc/OaViXPV5o/ordPs4EYKgd/eNFCgIsZYXe75rYXqnieAIfJEGddsLBpqlgLkwvf5KVS4QNqqX+2YubP63y+3sICq2ScdhO3LZs3nlqQ/SgMiJnCBbDUDZ9GGgJNJVVytcSz5IDQHeflrq/zTt1c4q1DO3CS7mimAnTCjetERRQ3mgY/2hRiuCDFj3Cy7QMjFs3vBsbWrjNWlqyveFmHDRkq34Om7eA2jl3LZ5u7vSm0/ylp/vtoysMjwEmw/0NA3hZPTG3OJxcvFcXBsz0SiFcd1U=</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:KeyDescriptor use="encryption">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIIE3jCCAsYCCQDE5FzoAkixzzANBgkqhkiG9w0BAQsFADAxMQswCQYDVQQGEwJVUzEQMA4GA1UECAwHRmxvcmlkYTEQMA4GA1UEBwwHT3JsYW5kbzAeFw0yMzExMTkxMjUyMTVaFw0zMzExMTYxMjUyMTVaMDExCzAJBgNVBAYTAlVTMRAwDgYDVQQIDAdGbG9yaWRhMRAwDgYDVQQHDAdPcmxhbmRvMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2ELJsLZs4yBH7a2U5pA7xw+Oiut7b/ROKh2BqSTKRbEG4xy7WwljT02Mh7GTjLvswtZSUObWFO5v14HNORa3+J9JT2DH+9F+FJ770HX8a3cKYBNQt3xP4IeUyjI3QWzrGtkYPwSZ74tDpAUtuqPAxtoCaZXFDtX6lvCJDqiPnfxRZrKkepYWINSwu4DRpg6KoiPWRCYTsEcCzImInzlACdM97jpG1gLGA6a4dmjalQbRtvC56N0Z56gIhYq2F5JdzB2a10pqoIY8ggXZGIJS9I++8mmdTj6So5pPxLwnCYUhwDew1/DMbi9xIwYozs9pEtHCTn1l34jldDwTziVAxGQZO7QUuoMl997zqcPS7pVWRnfz5odKuytLvQDA0lRVfzOxtqbM3qVhoLT2iDmnuEtlZzgfbt4WEuT2538qxZJkFRpZQIrTj3ybqmWAv36Cp49dfeMwaqjhfX7/mVfbsPMSC653DSZBB+n+Uz0FC3QhH+vIdNhXNAQ5tBseHUR6pXiMnLtI/WVbMvpvFwK2faFTcx1oaP/Qk6yCq66tJvPbnatT9qGF8rdBJmAk9aBdQTI+hAh5mDtDweCrgVL+Tm/+Q85hSl4HGzH/LhLVS478tZVX+o+0yorZ35LCW3e4v8iX+1VEGSdg2ooOWtbSSXK2cYZr8ilyUQp0KueenR0CAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAsonAahruWuHlYbDNQVD0ryhL/b+ttKKqVeT87XYDkvVhlSSSVAKcCwK/UU6z8Ty9dODUkd93Qsbof8fGMlXeYCtDHMRanvWLtk4wVkAMyNkDYHzJ1FbO7v44ZBbqNzSLy2kosbRELlcz+P3/42xumlDqAw/k13tWUdlLDxb0pd8R5yBev6HkIdJBIWtKmUuI+e8F/yTNf5kY7HO1p0NeKdVeZw4Ydw33+BwVxVNmhIxzdP5ZFQv0XRFWhCMo/6RLEepCvWUp/T1WRFqgwAdURaQrvvfpjO/Ls+neht1SWDeP8RRgsDrXIc3gZfaD8q4liIDTZ6HsFi7FmLbZatU8jJ4pCstxQLCvmix+1zF6Fwa9V5OApSTbVqBOsDZbJxeAoSzy5Wx28wufAZT4Kc/OaViXPV5o/ordPs4EYKgd/eNFCgIsZYXe75rYXqnieAIfJEGddsLBpqlgLkwvf5KVS4QNqqX+2YubP63y+3sICq2ScdhO3LZs3nlqQ/SgMiJnCBbDUDZ9GGgJNJVVytcSz5IDQHeflrq/zTt1c4q1DO3CS7mimAnTCjetERRQ3mgY/2hRiuCDFj3Cy7QMjFs3vBsbWrjNWlqyveFmHDRkq34Om7eA2jl3LZ5u7vSm0/ylp/vtoysMjwEmw/0NA3hZPTG3OJxcvFcXBsz0SiFcd1U=</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:3000/api/auth/sso/saml2/sp/sls"/>
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:3000/api/auth/sso/saml2/sp/acs/sso" index="1"/>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:3000/api/auth/sso/saml2/sp/acs/sso" index="1"/>
</md:SPSSODescriptor>
<md:Organization>
<md:OrganizationName xml:lang="en-US">Organization Name</md:OrganizationName>
<md:OrganizationDisplayName xml:lang="en-US">Organization DisplayName</md:OrganizationDisplayName>
<md:OrganizationURL xml:lang="en-US">http://localhost:3000/</md:OrganizationURL>
</md:Organization>
<md:ContactPerson contactType="technical">
<md:GivenName>Technical Contact Name</md:GivenName>
<md:EmailAddress>technical_contact@gmail.com</md:EmailAddress>
</md:ContactPerson>
<md:ContactPerson contactType="support">
<md:GivenName>Support Contact Name</md:GivenName>
<md:EmailAddress>support_contact@gmail.com</md:EmailAddress>
</md:ContactPerson>
</md:EntityDescriptor>
`,
},
idpMetadata: {
entityURL:
"https://dummyidp.com/apps/app_01k16v4vb5yytywqjjvv2b3435/metadata",
entityID:
"https://dummyidp.com/apps/app_01k16v4vb5yytywqjjvv2b3435",
redirectURL:
"https://dummyidp.com/apps/app_01k16v4vb5yytywqjjvv2b3435/sso",
singleSignOnService: [
{
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
Location:
"https://dummyidp.com/apps/app_01k16v4vb5yytywqjjvv2b3435/sso",
},
],
cert: `-----BEGIN CERTIFICATE-----
MIIDBzCCAe+gAwIBAgIUCLBK4f75EXEe4gyroYnVaqLoSp4wDQYJKoZIhvcNAQEL
BQAwEzERMA8GA1UEAwwIZHVtbXlpZHAwHhcNMjQwNTEzMjE1NDE2WhcNMzQwNTEx
MjE1NDE2WjATMREwDwYDVQQDDAhkdW1teWlkcDCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAKhmgQmWb8NvGhz952XY4SlJlpWIK72RilhOZS9frDYhqWVJ
HsGH9Z7sSzrM/0+YvCyEWuZV9gpMeIaHZxEPDqW3RJ7KG51fn/s/qFvwctf+CZDj
yfGDzYs+XIgf7p56U48EmYeWpB/aUW64gSbnPqrtWmVFBisOfIx5aY3NubtTsn+g
0XbdX0L57+NgSvPQHXh/GPXA7xCIWm54G5kqjozxbKEFA0DS3yb6oHRQWHqIAM/7
mJMdUVZNIV1q7c2JIgAl23uDWq+2KTE2R5liP/KjvjwKonVKtTqGqX6ei25rsTHO
aDpBH/LdQK2txgsm7R7+IThWNvUI0TttrmwBqyMCAwEAAaNTMFEwHQYDVR0OBBYE
FD142gxIAJMhpgMkgpzmRNoW9XbEMB8GA1UdIwQYMBaAFD142gxIAJMhpgMkgpzm
RNoW9XbEMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADQd6k6z
FIc20GfGHY5C2MFwyGOmP5/UG/JiTq7Zky28G6D0NA0je+GztzXx7VYDfCfHxLcm
2k5t9nYhb9kVawiLUUDVF6s+yZUXA4gUA3KoTWh1/oRxR3ggW7dKYm9fsNOdQAbx
UUkzp7HLZ45ZlpKUS0hO7es+fPyF5KVw0g0SrtQWwWucnQMAQE9m+B0aOf+92y7J
QkdgdR8Gd/XZ4NZfoOnKV7A1utT4rWxYCgICeRTHx9tly5OhPW4hQr5qOpngcsJ9
vhr86IjznQXhfj3hql5lA3VbHW04ro37ROIkh2bShDq5dwJJHpYCGrF3MQv8S3m+
jzGhYL6m9gFTm/8=
-----END CERTIFICATE-----`,
},
callbackUrl: "/dashboard",
},
},
],
}),
scim({
defaultSCIM: [
{
providerId: "sso",
/* cspell:disable-next-line */
// encoded token = ZGVmYXVsdC1zY2ltLXRva2VuOnNzbw==
scimToken: "default-scim-token",
},
],
}),
deviceAuthorization({
expiresIn: "3min",
interval: "5s",
+1
View File
@@ -15,6 +15,7 @@
"@better-auth/oauth-provider": "workspace:*",
"@better-auth/passkey": "workspace:*",
"@better-auth/sso": "workspace:*",
"@better-auth/scim": "workspace:^",
"@better-auth/stripe": "workspace:*",
"@hookform/resolvers": "^5.2.2",
"@libsql/client": "^0.8.1",
+1 -1
View File
@@ -33,6 +33,6 @@
"@vitejs/plugin-react-swc": "^4.0.0",
"tailwindcss": "catalog:tailwind",
"typescript": "^5.9.3",
"vite": "^7.2.4"
"vite": "^7.3.1"
}
}
+1 -1
View File
@@ -178,7 +178,7 @@ export const APIMethod = ({
const serverTabContent = (
<>
{isClientOnly ? null : (
{isClientOnly || isServerOnly ? null : (
<Endpoint
method={method || "GET"}
path={path}
+2 -2
View File
@@ -252,8 +252,8 @@ export const communityPlugins: CommunityPlugin[] = [
},
},
{
name: "better-auth-sequenzy",
url: "https://www.npmjs.com/package/better-auth-sequenzy",
name: "@sequenzy/better-auth",
url: "https://github.com/Sequenzy/sequenzy-better-auth",
description:
"Automatically add users to Sequenzy mailing lists on signup for seamless email marketing integration.",
author: {
+4 -4
View File
@@ -137,8 +137,8 @@ In addition to the standard methods, the client provides hooks to easily access
<Tab value="Svelte">
```svelte title="user.svelte"
<script lang="ts">
import { client } from "$lib/client";
const session = client.useSession();
import { authClient } from "$lib/auth-client";
const session = authClient.useSession();
</script>
<div
@@ -179,11 +179,11 @@ In addition to the standard methods, the client provides hooks to easily access
<Tab value="Solid">
```tsx title="user.tsx"
import { client } from "~/lib/client";
import { authClient } from "~/lib/auth-client";
import { Show } from 'solid-js';
export default function Home() {
const session = client.useSession()
const session = authClient.useSession()
return (
<Show
when={session()}
+148
View File
@@ -45,6 +45,154 @@ See the [CLI](/docs/concepts/cli) documentation for more information on the CLI.
required by plugins in the plugin documentation.
</Callout>
### Programmatic Migrations
In some environments (like Cloudflare Workers, serverless functions, or custom deployment setups), running the CLI may not be possible or practical. In these cases, you can run migrations programmatically from within your application code.
Better Auth provides a `getMigrations` function that you can use to generate and run migrations programmatically. This is particularly useful when:
- You're deploying to edge environments (Cloudflare Workers, Deno Deploy, etc.)
- Your environment variables are only available at runtime (not at build time)
- You need to run migrations as part of your application startup or through a custom endpoint
#### Using `getMigrations`
The `getMigrations` function is available from `better-auth/db` and works with the built-in Kysely adapter (SQLite, PostgreSQL, MySQL, MSSQL).
```typescript
import { getMigrations } from "better-auth/db";
const { toBeCreated, toBeAdded, runMigrations, compileMigrations } = await getMigrations(authConfig);
// Check what migrations are needed
console.log("Tables to create:", toBeCreated);
console.log("Fields to add:", toBeAdded);
// Run migrations
await runMigrations();
// Or get the SQL to run manually
const sql = await compileMigrations();
console.log(sql);
```
#### Example: Cloudflare Workers Migration Endpoint
For Cloudflare Workers using D1 (SQLite), you can create a migration endpoint that runs when your worker starts or through a manual trigger:
```typescript title="src/index.ts"
import { Hono } from "hono";
import { auth } from "./auth"; // your auth instance
import { getMigrations } from "better-auth/db";
const app = new Hono<{ Bindings: Env }>();
// Migration endpoint - call this once to set up your database
app.post("/migrate", async (c) => {
const env = c.env;
// Create auth config with runtime environment
const authConfig = {
database: env.DB, // D1 database binding
// ... rest of your auth config
};
try {
const { toBeCreated, toBeAdded, runMigrations } = await getMigrations(authConfig);
if (toBeCreated.length === 0 && toBeAdded.length === 0) {
return c.json({ message: "No migrations needed" });
}
await runMigrations();
return c.json({
message: "Migrations completed successfully",
created: toBeCreated.map(t => t.table),
added: toBeAdded.map(t => t.table)
});
} catch (error) {
return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
}
});
// Your normal auth endpoints
app.on(["POST", "GET"], "/api/auth/*", (c) => {
return auth.handler(c.req.raw);
});
export default app;
```
<Callout type="warn">
**Important Limitations**
Programmatic migrations currently work **only** with:
- Built-in database adapters (SQLite/D1, PostgreSQL, MySQL, MSSQL) using the Kysely adapter
- Does **not** work with Prisma or Drizzle ORM adapters
For Prisma or Drizzle in Cloudflare Workers, see the [Cloudflare Workers](#cloudflare-workers-with-prisma-drizzle) section below.
</Callout>
#### Cloudflare Workers with Prisma/Drizzle
If you're using Prisma or Drizzle with Cloudflare Workers, you have a few options:
1. **Use `cloudflare:workers` import** (Recommended for newer Cloudflare projects):
Cloudflare now supports importing environment variables from `cloudflare:workers`, which allows you to access `env` at the top level:
```typescript title="auth.ts"
import { env } from "cloudflare:workers";
import { drizzle } from "drizzle-orm/d1";
export const auth = betterAuth({
database: drizzle(env.DB),
// ... rest of config
});
```
With this approach, you can run the standard CLI commands:
```bash
npx @better-auth/cli migrate
# or
npx @better-auth/cli generate
```
2. **Use `process.env` with compatibility flag**:
Add the `nodejs_compat_populate_process_env` compatibility flag to your `wrangler.toml`:
```toml title="wrangler.toml"
compatibility_flags = ["nodejs_compat_populate_process_env"]
```
Then use `process.env` in your auth config:
```typescript title="auth.ts"
import { drizzle } from "drizzle-orm/d1";
export const auth = betterAuth({
database: drizzle(process.env.DB as any),
// ... rest of config
});
```
After setting this up, run the CLI commands as normal:
```bash
npx @better-auth/cli generate
```
3. **Generate schema locally and push manually**:
Run the generate command locally with a mock configuration, then use your ORM's push/migrate commands to apply the schema.
<Callout type="info">
See the [Hono integration documentation](/docs/integrations/hono#cloudflare-workers) for a complete example of using Better Auth with Cloudflare Workers.
</Callout>
## 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 this intensive records to a high performance storage or even RAM.
+4 -1
View File
@@ -41,4 +41,7 @@ See [Demo](https://demo.better-auth.com)
### SSO Login Example
For this example, we utilize DummyIDP. Initiate the login from the [DummyIDP login](https://dummyidp.com/apps/app_01k16v4vb5yytywqjjvv2b3435/login), click "Proceed", and from here it will direct you to user's dashboard.
### SCIM Sync Example
For this example, we utilize DummyIDP. Head out to the [IDP dashboard](https://dummyidp.com/apps/app_01k16v4vb5yytywqjjvv2b3435) and try to add, update or remove users, then go to the admin page or directly to your database and watch the synchronization work.
+134
View File
@@ -184,3 +184,137 @@ This configuration is necessary when:
The `credentials: "include"` option tells the fetch client to send cookies even for cross-origin requests. This works in conjunction with the CORS configuration on your server that has `credentials: true`.
> **Note:** Make sure your CORS configuration on the server matches your client's domain, and that `credentials: true` is set in both the server's CORS config and the client's fetch config.
### Cloudflare Workers
When deploying Better Auth with Hono on Cloudflare Workers, you may encounter issues with the CLI if your auth configuration depends on runtime environment variables (like `env.DB` for D1 databases).
#### The Problem
In Cloudflare Workers, environment variables are passed at runtime through request handlers, not available via `process.env` at build time. This makes it challenging to use the Better Auth CLI for migrations since the CLI needs to import your auth configuration file.
```typescript title="auth.ts"
// This won't work with the CLI because env is not available
export const createAuth = (env: Env) => betterAuth({
database: env.DB, // env.DB is only available at runtime
// ... rest of config
});
```
#### Solution 1: Programmatic Migrations (Built-in Kysely Adapter)
If you're using the built-in Kysely adapter (SQLite/D1, PostgreSQL, MySQL), you can run migrations programmatically through a custom endpoint:
```typescript title="src/index.ts"
import { Hono } from "hono";
import { getMigrations } from "better-auth/db";
import { betterAuth } from "better-auth";
type Env = {
DB: D1Database;
// ... other bindings
};
const app = new Hono<{ Bindings: Env }>();
// Migration endpoint - call this to set up your database
app.post("/migrate", async (c) => {
const authConfig = {
database: c.env.DB,
// Add all your other auth config options here
// (socialProviders, plugins, etc.)
};
try {
const { toBeCreated, toBeAdded, runMigrations } = await getMigrations(authConfig);
if (toBeCreated.length === 0 && toBeAdded.length === 0) {
return c.json({ message: "No migrations needed" });
}
await runMigrations();
return c.json({
message: "Migrations completed successfully",
tablesCreated: toBeCreated.map(t => t.table),
tablesUpdated: toBeAdded.map(t => t.table)
});
} catch (error) {
return c.json({
error: error instanceof Error ? error.message : "Migration failed"
}, 500);
}
});
// Your auth handler
app.on(["POST", "GET"], "/api/auth/*", async (c) => {
const auth = betterAuth({
database: c.env.DB,
// ... your config
});
return auth.handler(c.req.raw);
});
export default app;
```
**Usage:**
1. Deploy your worker
2. Call the migration endpoint once: `curl -X POST https://your-worker.workers.dev/migrate`
3. Remove or protect the migration endpoint after running
<Callout type="warn">
**Security Note**: Protect your migration endpoint in production! Consider:
- Adding authentication/authorization
- Using environment-based checks to only allow migrations in development
- Removing the endpoint entirely after initial setup
</Callout>
#### Solution 2: Use `cloudflare:workers` Import (Prisma/Drizzle)
For Prisma or Drizzle users, Cloudflare now supports importing environment variables from `cloudflare:workers`:
```typescript title="auth.ts"
import { env } from "cloudflare:workers";
import { drizzle } from "drizzle-orm/d1";
import { betterAuth } from "better-auth";
export const auth = betterAuth({
database: drizzle(env.DB),
// ... rest of your config
});
```
This allows you to use the standard CLI commands:
```bash
npx @better-auth/cli generate
```
See the [Cloudflare changelog](https://developers.cloudflare.com/changelog/2025-03-17-importable-env/) for more details on this feature.
#### Solution 3: Use `process.env` with Compatibility Flag
Add the `nodejs_compat_populate_process_env` flag to your `wrangler.toml`:
```toml title="wrangler.toml"
compatibility_flags = ["nodejs_compat_populate_process_env"]
```
Then use `process.env` in your auth config:
```typescript title="auth.ts"
import { drizzle } from "drizzle-orm/d1";
import { betterAuth } from "better-auth";
export const auth = betterAuth({
database: drizzle(process.env.DB as any),
// ... rest of config
});
```
For compatibility dates on or after `2025-04-01`, this is the default behavior when `nodejs_compat` is enabled. See the [Cloudflare documentation](https://developers.cloudflare.com/workers/runtime-apis/nodejs/process/) for more details.
<Callout type="info">
For more information on programmatic migrations, see the [Database documentation](/docs/concepts/database#programmatic-migrations).
</Callout>
+27
View File
@@ -134,6 +134,32 @@ scim({
See the [hooks](#hooks) documentation for more details about supported hooks.
#### Default SCIM token
We also provide a way for you to specify a `SCIM` token to use by default. This allows you to test a SCIM connection without setting up providers in the database:
```ts title="auth.ts"
const auth = betterAuth({
plugins: [
scim({
defaultSCIM: [
{
providerId: "default-scim", // ID of the existing provider you want to provision
scimToken: "some-scim-token", // SCIM plain token
organizationId: "the-org" // Optional organization id
}
]
})
]
});
```
<Callout type="info">
**Important**: Please note that you must base64 encode your `scimToken` before you try to use as follows: `base64(scimToken:providerId[:organizationId])`.
In our example above, you would need to encode the `some-scim-token:default-scim:the-org` text to base64, resulting in the following scimToken: `c29tZS1zY2ltLXRva2VuOmRlZmF1bHQtc2NpbTp0aGUtb3Jn`
</Callout>
### SCIM endpoints
The following subset of the specification is currently supported:
@@ -356,6 +382,7 @@ The plugin requires additional fields in the `scimProvider` table to store the p
### Server
- `defaultSCIM`: Default list of SCIM tokens for testing.
- `storeSCIMToken`: The method to store the SCIM token in your database, whether `encrypted`, `hashed` or `plain` text. Default is `plain` text.
Alternatively, you can pass a custom encryptor or hasher to store the SCIM token in your database.
+8 -8
View File
@@ -76,7 +76,7 @@ The Stripe plugin integrates Stripe's payment and subscription functionality wit
import { createAuthClient } from "better-auth/client"
import { stripeClient } from "@better-auth/stripe/client"
export const client = createAuthClient({
export const authClient = createAuthClient({
// ... your existing config
plugins: [
stripeClient({
@@ -265,7 +265,7 @@ type upgradeSubscription = {
**Simple Example:**
```ts title="client.ts"
await client.subscription.upgrade({
await authClient.subscription.upgrade({
plan: "pro",
successUrl: "/dashboard",
cancelUrl: "/pricing",
@@ -284,7 +284,7 @@ If the user already has an active subscription, you *must* provide the `subscrip
> **Important:** The `successUrl` parameter will be internally modified to handle race conditions between checkout completion and webhook processing. The plugin creates an intermediate redirect that ensures subscription status is properly updated before redirecting to your success page.
```ts
const { error } = await client.subscription.upgrade({
const { error } = await authClient.subscription.upgrade({
plan: "pro",
successUrl: "/dashboard",
cancelUrl: "/pricing",
@@ -302,7 +302,7 @@ For each reference ID (user or organization), only one active or trialing subscr
To switch a subscription to a different plan, use the `subscription.upgrade` method:
```ts title="client.ts"
await client.subscription.upgrade({
await authClient.subscription.upgrade({
plan: "pro",
successUrl: "/dashboard",
cancelUrl: "/pricing",
@@ -501,7 +501,7 @@ By default, subscriptions are associated with the user ID. However, you can use
```ts title="client.ts"
// Create a subscription for an organization
await client.subscription.upgrade({
await authClient.subscription.upgrade({
plan: "pro",
referenceId: "org_123456",
successUrl: "/dashboard",
@@ -510,7 +510,7 @@ await client.subscription.upgrade({
});
// List subscriptions for an organization
const { data: subscriptions } = await client.subscription.list({
const { data: subscriptions } = await authClient.subscription.list({
query: {
referenceId: "org_123456"
}
@@ -522,7 +522,7 @@ const { data: subscriptions } = await client.subscription.list({
For team or organization plans, you can specify the number of seats:
```ts
await client.subscription.upgrade({
await authClient.subscription.upgrade({
plan: "team",
referenceId: "org_123456",
seats: 10, // 10 team members
@@ -888,7 +888,7 @@ plugins: [
Even with Organization Customer enabled, user subscriptions remain available and are the default. To use the organization as the billing entity, pass `customerType: "organization"`:
```ts title="client.ts"
await client.subscription.upgrade({
await authClient.subscription.upgrade({
plan: "team",
referenceId: activeOrg.id,
customerType: "organization", // [!code highlight]
+6 -3
View File
@@ -74,11 +74,14 @@ Once you have an idea of what you want to contribute, you can start making chang
### 1. Create a new branch
```bash
# Make sure you're on main
git checkout main
# Add upstream remote (if not already added)
git remote add upstream https://github.com/better-auth/better-auth.git
# Make sure you're on canary
git checkout canary
# Pull latest changes
git pull upstream main
git pull upstream canary
# Create and switch to a new branch
git checkout -b feature/your-feature-name
+133 -6
View File
@@ -6,7 +6,7 @@ description: Frequently asked questions about Better Auth.
This page contains frequently asked questions, common issues, and other helpful information about Better Auth.
<Accordions>
<Accordion title="Auth client not working">
<Accordion id="auth-client-not-working" title="Auth client not working">
When encountering `createAuthClient` related errors, make sure to have the correct import path as it varies based on environment.
If you're using the auth client on react front-end, you'll need to import it from `/react`:
@@ -23,7 +23,7 @@ import { createAuthClient } from "better-auth/client";
</Accordion>
<Accordion title="getSession not working">
<Accordion id="getsession-not-working" title="getSession not working">
If you try to call `authClient.getSession` on a server environment (e.g, a Next.js server component), it doesn't work since it can't access the cookies. You can use the `auth.api.getSession` instead and pass the request headers to it.
```tsx title="server.tsx"
@@ -49,13 +49,13 @@ const session = await authClient.getSession({
```
</Accordion>
<Accordion title="Adding custom fields to the users table">
<Accordion id="adding-custom-fields-to-the-users-table" title="Adding custom fields to the users table">
Better Auth provides a type-safe way to extend the user and session schemas, take a look at our docs on <Link href="/docs/concepts/database#extending-core-schema">extending core schema</Link>.
</Accordion>
<Accordion title="Difference between getSession and useSession">
<Accordion id="difference-between-getsession-and-usesession" title="Difference between getSession and useSession">
Both `useSession` and `getSession` instances are used fundamentally different based on the situation.
`useSession` is a hook, meaning it can trigger re-renders whenever session data changes.
@@ -78,7 +78,7 @@ For all other situations where you shouldn't use `useSession`, is when you shoul
</Callout>
</Accordion>
<Accordion title="Common TypeScript Errors">
<Accordion id="common-typescript-errors" title="Common TypeScript Errors">
If you're facing typescript errors, make sure your tsconfig has `strict` set to `true`:
```json title="tsconfig.json"
{
@@ -99,9 +99,136 @@ if you can't set strict to true, you can enable strictNullChecks:
You can learn more in our <Link href="/docs/concepts/typescript#typescript-config">TypeScript docs</Link>.
</Accordion>
<Accordion title="Can I remove `name`, `image`, or `email` fields from the user table?">
<Accordion id="can-i-remove-name-image-or-email-fields-from-the-user-table" title="Can I remove `name`, `image`, or `email` fields from the user table?">
At this time, you can't remove the `name`, `image`, or `email` fields from the user table.
We do plan to have more customizability in the future in this regard, but for now, you can't remove these fields.
</Accordion>
<Accordion id="dual-module-hazard-issue" title="Dual Module Hazard Issue">
If you're encountering errors like:
```
No request state found. Please make sure you are calling this function within a `runWithRequestState` callback.
```
This is typically caused by a **dual module hazard** - when multiple versions of `better-auth` or `@better-auth/core` exist in your dependency tree. This commonly occurs after upgrading to v1.4+, especially in Cloudflare Workers, Nuxt, or when using plugins like `oauthProvider`.
### How to Diagnose
Check if you have duplicate versions of Better Auth packages:
```bash
# For pnpm users
pnpm why @better-auth/core
pnpm why better-auth
# For npm users
npm ls @better-auth/core
npm ls better-auth
# For yarn users
yarn why @better-auth/core
yarn why better-auth
```
If you see multiple versions listed, you have a dual module hazard.
### How to Fix
#### Basic Steps
Start by ensuring your dependencies are clean and all Better Auth packages use compatible versions.
<Callout type="tip">
**Clean reinstall:**
```bash
# Remove node_modules and lockfile
rm -rf node_modules
rm pnpm-lock.yaml # or package-lock.json or yarn.lock
# Reinstall dependencies
pnpm install # or npm install or yarn install
```
Check your `package.json` and make sure all Better Auth related packages (`better-auth`, `@better-auth/core`, `@better-auth/oauth-provider`, etc.) are using compatible versions.
</Callout>
#### Package Manager Resolution Issues
If you're using Yarn v1 (Classic) or potentially other package managers like pnpm v9, you may need to force `better-call` to resolve to a single instance.
<Callout type="warn">
**For Yarn v1 / pnpm v9 users:**
Manually install `better-call` and add it to both `dependencies` and `resolutions` to force it to be hoisted to the top level:
```json title="package.json"
{
"dependencies": {
"better-call": "^1.1.8",
// ... other dependencies
},
"resolutions": {
"better-call": "^1.1.8"
}
}
```
Then reinstall your dependencies:
```bash
yarn install # or pnpm install
```
Note: Yarn v3 and pnpm v10 don't typically have this issue.
</Callout>
<Callout type="error">
**Important:** Make sure to install `better-auth` and related packages in `dependencies`, not `devDependencies`.
</Callout>
#### Bundler Resolution Issues
This is a bundler-level issue where the same module gets resolved to different instances. The goal is to make your bundler resolve the module to the same instance.
<Callout type="info">
**For Next.js users:**
Add `better-auth` to `serverExternalPackages` in your `next.config.js`:
```ts title="next.config.js"
const config = {
serverExternalPackages: ['better-auth']
};
```
</Callout>
<Callout type="info">
**For Cloudflare Workers users:**
Make sure you have the `nodejs_compat` compatibility flag enabled in your `wrangler.toml`:
```toml title="wrangler.toml"
compatibility_flags = ["nodejs_compat"]
```
</Callout>
#### Verify the Fix
Run the diagnostic commands again to confirm only one version of each package exists:
```bash
pnpm why @better-auth/core
pnpm why better-auth
pnpm why better-call
```
You should see only one version listed for each package.
<Callout type="info">
Related issues: [#6613](https://github.com/better-auth/better-auth/issues/6613), [yjs#438](https://github.com/yjs/yjs/issues/438)
</Callout>
</Accordion>
</Accordions>
+18
View File
@@ -269,6 +269,24 @@ export const auth = betterAuth({
})
```
- `clientId`: OAuth client ID from the provider
- `clientSecret`: OAuth client secret from the provider
- `clientKey`: Client key (used by some providers like TikTok instead of clientId) (optional)
- `redirectURI`: Custom redirect URI for the OAuth callback (optional)
- `scope`: Additional OAuth scopes to request (optional)
- `mapProfileToUser`: Custom function to map the provider profile to a user (optional)
- `disableSignUp`: Disable sign up for new users (optional)
- `disableImplicitSignUp`: Disable implicit sign up for new users (optional)
- `overrideUserInfoOnSignIn`: Override user info with provider user info on sign in (optional)
- `prompt`: The prompt to use for the authorization code request (`"select_account"`, `"consent"`, `"login"`, `"none"`, `"select_account consent"`) (optional)
- `responseMode`: The response mode to use (`"query"`, `"form_post"`) (optional)
- `getUserInfo`: Custom function to get user info from the provider (optional)
- `refreshAccessToken`: Custom function to refresh a token (optional)
- `verifyIdToken`: Custom function to verify the ID token (optional)
- `disableIdTokenSignIn`: Disable sign in with ID token sent from the client (optional)
- `disableDefaultScope`: Disable the provider's default scopes (optional)
- `authorizationEndpoint`: Custom authorization endpoint URL (optional)
## `plugins`
List of Better Auth plugins.
+1 -1
View File
@@ -7,7 +7,7 @@
},
"devDependencies": {
"@better-auth/test-utils": "workspace:*",
"vite": "^7.2.4"
"vite": "^7.3.1"
},
"dependencies": {
"better-auth": "workspace:*",
@@ -1,49 +0,0 @@
CREATE TABLE `account` (
`id` text PRIMARY KEY NOT NULL,
`account_id` text NOT NULL,
`provider_id` text NOT NULL,
`user_id` text NOT NULL,
`access_token` text,
`refresh_token` text,
`id_token` text,
`access_token_expires_at` integer,
`refresh_token_expires_at` integer,
`scope` text,
`password` text,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `session` (
`id` text PRIMARY KEY NOT NULL,
`expires_at` integer NOT NULL,
`token` text NOT NULL,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL,
`ip_address` text,
`user_agent` text,
`user_id` text NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint
CREATE TABLE `user` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`email` text NOT NULL,
`email_verified` integer NOT NULL,
`image` text,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint
CREATE TABLE `verification` (
`id` text PRIMARY KEY NOT NULL,
`identifier` text NOT NULL,
`value` text NOT NULL,
`expires_at` integer NOT NULL,
`created_at` integer,
`updated_at` integer
);
@@ -0,0 +1,74 @@
CREATE TABLE `account` (
`id` text PRIMARY KEY NOT NULL,
`account_id` text NOT NULL,
`provider_id` text NOT NULL,
`user_id` text NOT NULL,
`access_token` text,
`refresh_token` text,
`id_token` text,
`access_token_expires_at` integer,
`refresh_token_expires_at` integer,
`scope` text,
`password` text,
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
`updated_at` integer NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE INDEX `account_userId_idx` ON `account` (`user_id`);--> statement-breakpoint
CREATE TABLE `jwks` (
`id` text PRIMARY KEY NOT NULL,
`public_key` text NOT NULL,
`private_key` text NOT NULL,
`created_at` integer NOT NULL,
`expires_at` integer
);
--> statement-breakpoint
CREATE TABLE `session` (
`id` text PRIMARY KEY NOT NULL,
`expires_at` integer NOT NULL,
`token` text NOT NULL,
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
`updated_at` integer NOT NULL,
`ip_address` text,
`user_agent` text,
`user_id` text NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint
CREATE INDEX `session_userId_idx` ON `session` (`user_id`);--> statement-breakpoint
CREATE TABLE `sso_provider` (
`id` text PRIMARY KEY NOT NULL,
`issuer` text NOT NULL,
`oidc_config` text,
`saml_config` text,
`user_id` text,
`provider_id` text NOT NULL,
`organization_id` text,
`domain` text NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `sso_provider_provider_id_unique` ON `sso_provider` (`provider_id`);--> statement-breakpoint
CREATE TABLE `user` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`email` text NOT NULL,
`email_verified` integer DEFAULT false NOT NULL,
`image` text,
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint
CREATE TABLE `verification` (
`id` text PRIMARY KEY NOT NULL,
`identifier` text NOT NULL,
`value` text NOT NULL,
`expires_at` integer NOT NULL,
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
);
--> statement-breakpoint
CREATE INDEX `verification_identifier_idx` ON `verification` (`identifier`);
@@ -1,7 +1,7 @@
{
"version": "6",
"dialect": "sqlite",
"id": "dde09aa0-ff07-4e38-a49a-742a3c2b7af4",
"id": "cd9bf58a-5fe9-4865-8507-e1fe41e35967",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"account": {
@@ -89,7 +89,8 @@
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
},
"updated_at": {
"name": "updated_at",
@@ -99,7 +100,13 @@
"autoincrement": false
}
},
"indexes": {},
"indexes": {
"account_userId_idx": {
"name": "account_userId_idx",
"columns": ["user_id"],
"isUnique": false
}
},
"foreignKeys": {
"account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk",
@@ -115,6 +122,51 @@
"uniqueConstraints": {},
"checkConstraints": {}
},
"jwks": {
"name": "jwks",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"public_key": {
"name": "public_key",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"private_key": {
"name": "private_key",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"session": {
"name": "session",
"columns": {
@@ -144,7 +196,8 @@
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
},
"updated_at": {
"name": "updated_at",
@@ -180,6 +233,11 @@
"name": "session_token_unique",
"columns": ["token"],
"isUnique": true
},
"session_userId_idx": {
"name": "session_userId_idx",
"columns": ["user_id"],
"isUnique": false
}
},
"foreignKeys": {
@@ -197,6 +255,88 @@
"uniqueConstraints": {},
"checkConstraints": {}
},
"sso_provider": {
"name": "sso_provider",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"issuer": {
"name": "issuer",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"oidc_config": {
"name": "oidc_config",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"saml_config": {
"name": "saml_config",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"organization_id": {
"name": "organization_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"domain": {
"name": "domain",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"sso_provider_provider_id_unique": {
"name": "sso_provider_provider_id_unique",
"columns": ["provider_id"],
"isUnique": true
}
},
"foreignKeys": {
"sso_provider_user_id_user_id_fk": {
"name": "sso_provider_user_id_user_id_fk",
"tableFrom": "sso_provider",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user": {
"name": "user",
"columns": {
@@ -226,7 +366,8 @@
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
"autoincrement": false,
"default": false
},
"image": {
"name": "image",
@@ -240,14 +381,16 @@
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
}
},
"indexes": {
@@ -297,18 +440,26 @@
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
"notNull": true,
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
"notNull": true,
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
}
},
"indexes": {
"verification_identifier_idx": {
"name": "verification_identifier_idx",
"columns": ["identifier"],
"isUnique": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
@@ -5,8 +5,8 @@
{
"idx": 0,
"version": "6",
"when": 1740122602683,
"tag": "0000_clean_vector",
"when": 1768234754154,
"tag": "0000_lively_paladin",
"breakpoints": true
}
]
+2 -1
View File
@@ -2,9 +2,10 @@
"name": "cloudflare",
"private": true,
"dependencies": {
"@better-auth/sso": "workspace:*",
"better-auth": "workspace:*",
"drizzle-orm": "^0.44.5",
"hono": "^4.9.7"
"hono": "^4.11.4"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.8.69",
+120 -41
View File
@@ -1,55 +1,134 @@
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
import { relations, sql } from "drizzle-orm";
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const user = sqliteTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: integer("email_verified", { mode: "boolean" }).notNull(),
emailVerified: integer("email_verified", { mode: "boolean" })
.default(false)
.notNull(),
image: text("image"),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
});
export const session = sqliteTable("session", {
export const session = sqliteTable(
"session",
{
id: text("id").primaryKey(),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
token: text("token").notNull().unique(),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
},
(table) => [index("session_userId_idx").on(table.userId)],
);
export const account = sqliteTable(
"account",
{
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: integer("access_token_expires_at", {
mode: "timestamp_ms",
}),
refreshTokenExpiresAt: integer("refresh_token_expires_at", {
mode: "timestamp_ms",
}),
scope: text("scope"),
password: text("password"),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
},
(table) => [index("account_userId_idx").on(table.userId)],
);
export const verification = sqliteTable(
"verification",
{
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
},
(table) => [index("verification_identifier_idx").on(table.identifier)],
);
export const jwks = sqliteTable("jwks", {
id: text("id").primaryKey(),
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
token: text("token").notNull().unique(),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
publicKey: text("public_key").notNull(),
privateKey: text("private_key").notNull(),
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }),
});
export const account = sqliteTable("account", {
export const ssoProvider = sqliteTable("sso_provider", {
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: integer("access_token_expires_at", {
mode: "timestamp",
issuer: text("issuer").notNull(),
oidcConfig: text("oidc_config"),
samlConfig: text("saml_config"),
userId: text("user_id").references(() => user.id, { onDelete: "cascade" }),
providerId: text("provider_id").notNull().unique(),
organizationId: text("organization_id"),
domain: text("domain").notNull(),
});
export const userRelations = relations(user, ({ many }) => ({
sessions: many(session),
accounts: many(account),
ssoProviders: many(ssoProvider),
}));
export const sessionRelations = relations(session, ({ one }) => ({
user: one(user, {
fields: [session.userId],
references: [user.id],
}),
refreshTokenExpiresAt: integer("refresh_token_expires_at", {
mode: "timestamp",
}),
scope: text("scope"),
password: text("password"),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
});
}));
export const verification = sqliteTable("verification", {
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
createdAt: integer("created_at", { mode: "timestamp" }),
updatedAt: integer("updated_at", { mode: "timestamp" }),
});
export const accountRelations = relations(account, ({ one }) => ({
user: one(user, {
fields: [account.userId],
references: [user.id],
}),
}));
export const ssoProviderRelations = relations(ssoProvider, ({ one }) => ({
user: one(user, {
fields: [ssoProvider.userId],
references: [user.id],
}),
}));
+3
View File
@@ -1,5 +1,7 @@
import { sso } from "@better-auth/sso";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { jwt } from "better-auth/plugins/jwt";
import { Hono } from "hono";
import { createDrizzle } from "./db";
@@ -17,6 +19,7 @@ const createAuth = (env: CloudflareBindings) =>
logger: {
level: "debug",
},
plugins: [jwt(), sso()],
});
type Auth = ReturnType<typeof createAuth>;
+1 -1
View File
@@ -8,7 +8,7 @@
"d1_databases": [
{
"binding": "DB",
"database_id": "d1-db-id",
"database_id": "REPLACE_ME",
"database_name": "db",
"migrations_dir": "./drizzle"
}
+1 -1
View File
@@ -8,6 +8,6 @@
"better-auth": "workspace:*"
},
"devDependencies": {
"vite": "^7.2.4"
"vite": "^7.3.1"
}
}
+4 -2
View File
@@ -35,7 +35,6 @@
// implicit integrations dependencies
"@lynx-js/react",
"@sveltejs/kit",
"@tanstack/react-start",
"@tanstack/start-server-core",
"next",
"react",
@@ -49,7 +48,10 @@
"ignoreDependencies": [
// type-only dependencies
"kysely!",
"nanostores!"
"nanostores!",
// implicit test dependencies
"vitest"
]
},
"packages/cli": {
+6 -6
View File
@@ -14,7 +14,7 @@
"format": "biome format . --write",
"format:md": "remark . --output --quiet",
"format:check": "remark . --quiet --frail",
"lint": "biome check .",
"lint": "biome check . --error-on-warnings",
"lint:dependencies": "knip --strict",
"lint:fix": "biome check . --fix --unsafe",
"lint:spell": "cspell . --color",
@@ -37,20 +37,20 @@
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.18.2",
"@biomejs/biome": "2.3.8",
"@types/bun": "^1.3.3",
"@types/node": "^24.10.1",
"@biomejs/biome": "2.3.11",
"@types/bun": "^1.3.5",
"@types/node": "^25.0.6",
"@vitest/coverage-istanbul": "catalog:vitest",
"bumpp": "^10.3.2",
"cspell": "^9.4.0",
"knip": "^5.71.0",
"knip": "^5.80.2",
"nyc": "^17.1.0",
"publint": "^0.3.15",
"remark-cli": "^12.0.1",
"remark-mdx": "^3.1.1",
"remark-preset-wooorm": "^11.0.0",
"tinyglobby": "^0.2.15",
"turbo": "^2.6.3",
"turbo": "^2.7.4",
"typescript": "catalog:",
"vitest": "catalog:vitest"
}
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "better-auth",
"version": "1.5.0-beta.3",
"version": "1.5.0-beta.6",
"description": "The most comprehensive authentication framework for TypeScript.",
"type": "module",
"license": "MIT",
@@ -463,7 +463,7 @@
"@prisma/client": "^5.22.0",
"@sveltejs/kit": "^2.37.1",
"@types/better-sqlite3": "^7.6.13",
"@types/bun": "^1.3.3",
"@types/bun": "^1.3.5",
"@types/pg": "^8.15.5",
"@types/react": "catalog:react19",
"better-sqlite3": "^12.4.1",
@@ -494,7 +494,7 @@
"@lynx-js/react": "*",
"@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
"@sveltejs/kit": "^2.0.0",
"@tanstack/react-start": "^1.0.0",
"@tanstack/start-server-core": "^1.0.0",
"better-sqlite3": "^12.0.0",
"drizzle-kit": ">=0.31.4",
"drizzle-orm": ">=0.41.0",
@@ -520,7 +520,7 @@
"@sveltejs/kit": {
"optional": true
},
"@tanstack/react-start": {
"@tanstack/start-server-core": {
"optional": true
},
"next": {
@@ -7,7 +7,7 @@ import {
initGetDefaultModelName,
} from "@better-auth/core/db/adapter";
import { TTY_COLORS } from "@better-auth/core/env";
import { generateId } from "@better-auth/core/utils";
import { generateId } from "@better-auth/core/utils/id";
import { test } from "vitest";
import { betterAuth } from "../auth";
import type { Account, Session, User, Verification } from "../types";
@@ -147,7 +147,7 @@ class ConnectionMutex {
#resolve?: () => void;
async lock(): Promise<void> {
while (this.#promise) {
while (this.#promise !== undefined) {
await this.#promise;
}
@@ -149,7 +149,7 @@ class ConnectionMutex {
#resolve?: () => void;
async lock(): Promise<void> {
while (this.#promise) {
while (this.#promise !== undefined) {
await this.#promise;
}
+1 -1
View File
@@ -1,8 +1,8 @@
import type { Awaitable, BetterAuthOptions } from "@better-auth/core";
import type { DBAdapter } from "@better-auth/core/db/adapter";
import { generateId } from "@better-auth/core/utils/id";
import { beforeAll, describe, expect, test } from "vitest";
import type { User } from "../types";
import { generateId } from "../utils";
interface AdapterTestOptions {
getAdapter: (
@@ -1,7 +1,7 @@
import type { GenericEndpointContext } from "@better-auth/core";
import { createAuthMiddleware } from "@better-auth/core/api";
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import { deprecate } from "@better-auth/core/utils";
import { deprecate } from "@better-auth/core/utils/deprecate";
import { matchesOriginPattern } from "../../auth/trusted-origins";
/**
@@ -1,9 +1,19 @@
import type { AuthContext } from "@better-auth/core";
import { safeJSONParse } from "@better-auth/core/utils";
import type {
AuthContext,
BetterAuthRateLimitStorage,
} from "@better-auth/core";
import { safeJSONParse } from "@better-auth/core/utils/json";
import type { RateLimit } from "../../types";
import { getIp } from "../../utils/get-request-ip";
import { wildcardMatch } from "../../utils/wildcard";
interface MemoryRateLimitEntry {
data: RateLimit;
expiresAt: number;
}
const memory = new Map<string, MemoryRateLimitEntry>();
function shouldRateLimit(
max: number,
window: number,
@@ -36,7 +46,9 @@ function getRetryAfter(lastRequest: number, window: number) {
return Math.ceil((lastRequest + windowInMs - now) / 1000);
}
function createDBStorage(ctx: AuthContext) {
function createDatabaseStorageWrapper(
ctx: AuthContext,
): BetterAuthRateLimitStorage {
const model = "rateLimit";
const db = ctx.adapter;
return {
@@ -85,20 +97,12 @@ function createDBStorage(ctx: AuthContext) {
};
}
interface MemoryRateLimitEntry {
data: RateLimit;
expiresAt: number;
}
const memory = new Map<string, MemoryRateLimitEntry>();
function getRateLimitStorage(
ctx: AuthContext,
rateLimitSettings?:
| {
window?: number;
}
| undefined,
) {
rateLimitSettings: {
window: number;
},
): BetterAuthRateLimitStorage {
if (ctx.options.rateLimit?.customStorage) {
return ctx.options.rateLimit.customStorage;
}
@@ -107,7 +111,7 @@ function getRateLimitStorage(
return {
get: async (key: string) => {
const data = await ctx.options.secondaryStorage?.get(key);
return data ? safeJSONParse<RateLimit>(data) : undefined;
return data ? safeJSONParse<RateLimit>(data) : null;
},
set: async (
key: string,
@@ -128,12 +132,12 @@ function getRateLimitStorage(
async get(key: string) {
const entry = memory.get(key);
if (!entry) {
return undefined;
return null;
}
// Check if entry has expired
if (Date.now() >= entry.expiresAt) {
memory.delete(key);
return undefined;
return null;
}
return entry.data;
},
@@ -148,7 +152,7 @@ function getRateLimitStorage(
},
};
}
return createDBStorage(ctx);
return createDatabaseStorageWrapper(ctx);
}
export async function onRequestRateLimit(req: Request, ctx: AuthContext) {
@@ -158,8 +162,8 @@ export async function onRequestRateLimit(req: Request, ctx: AuthContext) {
const path = new URL(req.url).pathname
.replace(ctx.options.basePath || "/api/auth", "")
.replace(/\/+$/, "");
let window = ctx.rateLimit.window;
let max = ctx.rateLimit.max;
let currentWindow = ctx.rateLimit.window;
let currentMax = ctx.rateLimit.max;
const ip = getIp(req, ctx.options);
if (!ip) {
return;
@@ -169,8 +173,8 @@ export async function onRequestRateLimit(req: Request, ctx: AuthContext) {
const specialRule = specialRules.find((rule) => rule.pathMatcher(path));
if (specialRule) {
window = specialRule.window;
max = specialRule.max;
currentWindow = specialRule.window;
currentMax = specialRule.max;
}
for (const plugin of ctx.options.plugins || []) {
@@ -179,8 +183,8 @@ export async function onRequestRateLimit(req: Request, ctx: AuthContext) {
rule.pathMatcher(path),
);
if (matchedRule) {
window = matchedRule.window;
max = matchedRule.max;
currentWindow = matchedRule.window;
currentMax = matchedRule.max;
break;
}
}
@@ -197,10 +201,15 @@ export async function onRequestRateLimit(req: Request, ctx: AuthContext) {
if (_path) {
const customRule = ctx.rateLimit.customRules[_path];
const resolved =
typeof customRule === "function" ? await customRule(req) : customRule;
typeof customRule === "function"
? await customRule(req, {
window: currentWindow,
max: currentMax,
})
: customRule;
if (resolved) {
window = resolved.window;
max = resolved.max;
currentWindow = resolved.window;
currentMax = resolved.max;
}
if (resolved === false) {
@@ -210,7 +219,7 @@ export async function onRequestRateLimit(req: Request, ctx: AuthContext) {
}
const storage = getRateLimitStorage(ctx, {
window,
window: currentWindow,
});
const data = await storage.get(key);
const now = Date.now();
@@ -224,10 +233,10 @@ export async function onRequestRateLimit(req: Request, ctx: AuthContext) {
} else {
const timeSinceLastRequest = now - data.lastRequest;
if (shouldRateLimit(max, window, data)) {
const retryAfter = getRetryAfter(data.lastRequest, window);
if (shouldRateLimit(currentMax, currentWindow, data)) {
const retryAfter = getRetryAfter(data.lastRequest, currentWindow);
return rateLimitResponse(retryAfter);
} else if (timeSinceLastRequest > window * 1000) {
} else if (timeSinceLastRequest > currentWindow * 1000) {
// Reset the count if the window has passed since the last request
await storage.set(
key,
@@ -1,6 +1,6 @@
import { createAuthEndpoint } from "@better-auth/core/api";
import type { OAuth2Tokens } from "@better-auth/core/oauth2";
import { safeJSONParse } from "@better-auth/core/utils";
import { safeJSONParse } from "@better-auth/core/utils/json";
import * as z from "zod";
import { setSessionCookie } from "../../cookies";
import { handleOAuthUserInfo } from "../../oauth2/link-account";
@@ -1,8 +1,8 @@
import type { AuthContext } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import { generateId } from "@better-auth/core/utils/id";
import * as z from "zod";
import { generateId } from "../../utils";
import { getDate } from "../../utils/date";
import { validatePassword } from "../../utils/password";
import { originCheck } from "../middlewares";
@@ -7,7 +7,7 @@ import {
createAuthMiddleware,
} from "@better-auth/core/api";
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import { safeJSONParse } from "@better-auth/core/utils";
import { safeJSONParse } from "@better-auth/core/utils/json";
import { base64Url } from "@better-auth/utils/base64";
import { binary } from "@better-auth/utils/binary";
import { createHMAC } from "@better-auth/utils/hmac";
@@ -407,6 +407,160 @@ describe("type", () => {
}>();
});
it("should support refetch with query parameters - solid", () => {
const client = createSolidClient({
plugins: [testClientPlugin()],
baseURL: "http://localhost:3000",
fetchOptions: {
customFetchImpl: async (url, init) => {
return new Response();
},
},
});
type UseSessionReturn = ReturnType<ReturnType<typeof client.useSession>>;
expectTypeOf<UseSessionReturn>().toMatchTypeOf<{
data: {
user: {
id: string;
email: string;
emailVerified: boolean;
name: string;
createdAt: Date;
updatedAt: Date;
image?: string | undefined | null;
testField4: string;
testField?: string | undefined | null;
testField2?: number | undefined | null;
};
session: Session;
} | null;
isPending: boolean;
isRefetching: boolean;
error: BetterFetchError | null;
refetch: (
queryParams?: { query?: SessionQueryParams } | undefined,
) => Promise<void>;
}>();
});
it("should support refetch with query parameters - svelte", () => {
const client = createSvelteClient({
plugins: [testClientPlugin()],
baseURL: "http://localhost:3000",
fetchOptions: {
customFetchImpl: async (url, init) => {
return new Response();
},
},
});
type UseSessionAtom = ReturnType<typeof client.useSession>;
type UseSessionReturn = ReturnType<UseSessionAtom["get"]>;
expectTypeOf<UseSessionReturn>().toMatchTypeOf<{
data: {
user: {
id: string;
email: string;
emailVerified: boolean;
name: string;
createdAt: Date;
updatedAt: Date;
image?: string | undefined | null;
testField4: string;
testField?: string | undefined | null;
testField2?: number | undefined | null;
};
session: Session;
} | null;
isPending: boolean;
isRefetching: boolean;
error: BetterFetchError | null;
refetch: (
queryParams?: { query?: SessionQueryParams } | undefined,
) => Promise<void>;
}>();
});
it("should support refetch with query parameters - vue", () => {
const client = createVueClient({
plugins: [testClientPlugin()],
baseURL: "http://localhost:3000",
fetchOptions: {
customFetchImpl: async (url, init) => {
return new Response();
},
},
});
// Test the function signature directly to avoid overload resolution issues
expectTypeOf(client.useSession).toMatchTypeOf<
() => Readonly<
Ref<{
data: {
user: {
id: string;
email: string;
emailVerified: boolean;
name: string;
createdAt: Date;
updatedAt: Date;
image?: string | undefined | null;
testField4: string;
testField?: string | undefined | null;
testField2?: number | undefined | null;
};
session: Session;
} | null;
isPending: boolean;
isRefetching: boolean;
error: BetterFetchError | null;
refetch: (
queryParams?: { query?: SessionQueryParams } | undefined,
) => Promise<void>;
}>
>
>();
});
it("should support refetch with query parameters - vanilla", () => {
const client = createVanillaClient({
plugins: [testClientPlugin()],
baseURL: "http://localhost:3000",
fetchOptions: {
customFetchImpl: async (url, init) => {
return new Response();
},
},
});
type UseSessionAtom = typeof client.useSession;
type UseSessionReturn = ReturnType<UseSessionAtom["get"]>;
expectTypeOf<UseSessionReturn>().toMatchTypeOf<{
data: {
user: {
id: string;
email: string;
emailVerified: boolean;
name: string;
createdAt: Date;
updatedAt: Date;
image?: string | undefined | null;
testField4: string;
testField?: string | undefined | null;
testField2?: number | undefined | null;
};
session: Session;
} | null;
isPending: boolean;
isRefetching: boolean;
error: BetterFetchError | null;
refetch: (
queryParams?: { query?: SessionQueryParams } | undefined,
) => Promise<void>;
}>();
});
it("should infer $ERROR_CODES with multiple plugins", () => {
const client = createReactClient({
plugins: [
@@ -3,7 +3,7 @@ import type {
BetterAuthClientPlugin,
} from "@better-auth/core";
import type { BASE_ERROR_CODES } from "@better-auth/core/error";
import { capitalizeFirstLetter } from "@better-auth/core/utils";
import { capitalizeFirstLetter } from "@better-auth/core/utils/string";
import type {
BetterFetchError,
BetterFetchResponse,
@@ -3,7 +3,7 @@ import type {
BetterAuthClientPlugin,
} from "@better-auth/core";
import type { BASE_ERROR_CODES } from "@better-auth/core/error";
import { capitalizeFirstLetter } from "@better-auth/core/utils";
import { capitalizeFirstLetter } from "@better-auth/core/utils/string";
import type {
BetterFetchError,
BetterFetchResponse,
@@ -3,7 +3,7 @@ import type {
BetterAuthClientPlugin,
} from "@better-auth/core";
import type { BASE_ERROR_CODES } from "@better-auth/core/error";
import { capitalizeFirstLetter } from "@better-auth/core/utils";
import { capitalizeFirstLetter } from "@better-auth/core/utils/string";
import type {
BetterFetchError,
BetterFetchResponse,
@@ -17,6 +17,7 @@ import type {
InferClientAPI,
InferErrorCodes,
IsSignal,
SessionQueryParams,
} from "../types";
import { useStore } from "./solid-store";
@@ -87,6 +88,9 @@ export function createAuthClient<Option extends BetterAuthClientOptions>(
isPending: boolean;
isRefetching: boolean;
error: BetterFetchError | null;
refetch: (
queryParams?: { query?: SessionQueryParams } | undefined,
) => Promise<void>;
}>;
$Infer: {
Session: NonNullable<Session>;
@@ -3,7 +3,7 @@ import type {
BetterAuthClientPlugin,
} from "@better-auth/core";
import type { BASE_ERROR_CODES } from "@better-auth/core/error";
import { capitalizeFirstLetter } from "@better-auth/core/utils";
import { capitalizeFirstLetter } from "@better-auth/core/utils/string";
import type {
BetterFetchError,
BetterFetchResponse,
@@ -17,6 +17,7 @@ import type {
InferClientAPI,
InferErrorCodes,
IsSignal,
SessionQueryParams,
} from "../types";
type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends {
@@ -85,6 +86,9 @@ export function createAuthClient<Option extends BetterAuthClientOptions>(
error: BetterFetchError | null;
isPending: boolean;
isRefetching: boolean;
refetch: (
queryParams?: { query?: SessionQueryParams } | undefined,
) => Promise<void>;
}>;
$fetch: typeof $fetch;
$store: typeof $store;
+6 -1
View File
@@ -3,7 +3,7 @@ import type {
BetterAuthClientPlugin,
} from "@better-auth/core";
import type { BASE_ERROR_CODES } from "@better-auth/core/error";
import { capitalizeFirstLetter } from "@better-auth/core/utils";
import { capitalizeFirstLetter } from "@better-auth/core/utils/string";
import type {
BetterFetchError,
BetterFetchResponse,
@@ -17,6 +17,7 @@ import type {
InferClientAPI,
InferErrorCodes,
IsSignal,
SessionQueryParams,
} from "./types";
type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends {
@@ -84,6 +85,10 @@ export function createAuthClient<Option extends BetterAuthClientOptions>(
data: Session;
error: BetterFetchError | null;
isPending: boolean;
isRefetching: boolean;
refetch: (
queryParams?: { query?: SessionQueryParams } | undefined,
) => Promise<void>;
}>;
$fetch: typeof $fetch;
$store: typeof $store;
+5 -1
View File
@@ -3,7 +3,7 @@ import type {
BetterAuthClientPlugin,
} from "@better-auth/core";
import type { BASE_ERROR_CODES } from "@better-auth/core/error";
import { capitalizeFirstLetter } from "@better-auth/core/utils";
import { capitalizeFirstLetter } from "@better-auth/core/utils/string";
import type {
BetterFetchError,
BetterFetchResponse,
@@ -17,6 +17,7 @@ import type {
InferClientAPI,
InferErrorCodes,
IsSignal,
SessionQueryParams,
} from "../types";
import { useStore } from "./vue-store";
@@ -80,6 +81,9 @@ export function createAuthClient<Option extends BetterAuthClientOptions>(
isPending: boolean;
isRefetching: boolean;
error: BetterFetchError | null;
refetch: (
queryParams?: { query?: SessionQueryParams } | undefined,
) => Promise<void>;
}>
>;
function useSession<F extends (...args: any) => any>(
@@ -6,7 +6,8 @@ import { BetterAuthError } from "@better-auth/core/error";
import type { OAuthProvider } from "@better-auth/core/oauth2";
import type { SocialProviders } from "@better-auth/core/social-providers";
import { socialProviders } from "@better-auth/core/social-providers";
import { deprecate } from "@better-auth/core/utils";
import { deprecate } from "@better-auth/core/utils/deprecate";
import { generateId } from "@better-auth/core/utils/id";
import { createTelemetry } from "@better-auth/telemetry";
import defu from "defu";
import type { Entries } from "type-fest";
@@ -15,7 +16,6 @@ import { matchesOriginPattern } from "../auth/trusted-origins";
import { createCookieGetter, getCookies } from "../cookies";
import { hashPassword, verifyPassword } from "../crypto/password";
import { createInternalAdapter } from "../db/internal-adapter";
import { generateId } from "../utils";
import { DEFAULT_SECRET } from "../utils/constants";
import { isPromise } from "../utils/is-promise";
import { checkPassword } from "../utils/password";
+1 -1
View File
@@ -5,7 +5,7 @@ import type {
} from "@better-auth/core";
import { env, isProduction } from "@better-auth/core/env";
import { BetterAuthError } from "@better-auth/core/error";
import { safeJSONParse } from "@better-auth/core/utils";
import { safeJSONParse } from "@better-auth/core/utils/json";
import { base64Url } from "@better-auth/utils/base64";
import { binary } from "@better-auth/utils/binary";
import { createHMAC } from "@better-auth/utils/hmac";
@@ -1,7 +1,7 @@
import type { GenericEndpointContext } from "@better-auth/core";
import type { Account } from "@better-auth/core/db";
import type { InternalLogger } from "@better-auth/core/env";
import { safeJSONParse } from "@better-auth/core/utils";
import { safeJSONParse } from "@better-auth/core/utils/json";
import type { CookieOptions } from "better-call";
import * as z from "zod";
import { symmetricDecodeJWT, symmetricEncodeJWT } from "../crypto";
@@ -1,5 +1,5 @@
import type { GenericEndpointContext } from "@better-auth/core";
import { safeJSONParse } from "@better-auth/core/utils";
import { safeJSONParse } from "@better-auth/core/utils/json";
import Database from "better-sqlite3";
import { Kysely, SqliteDialect } from "kysely";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
@@ -751,4 +751,75 @@ describe("internal adapter test", async () => {
// Clean up DB
testDb.close();
});
it("should deduplicate sessions when active-sessions list contains duplicates", async () => {
const testDb = new Database(":memory:");
const testDialect = new SqliteDialect({ database: testDb });
const testMap = new Map<string, string>();
const testExpirationMap = new Map<string, number>();
const testOpts = {
database: {
dialect: testDialect,
type: "sqlite",
},
secondaryStorage: {
set(key: string, value: string, ttl?: number) {
testMap.set(key, value);
if (ttl !== undefined) {
testExpirationMap.set(key, ttl);
}
},
get(key: string) {
return testMap.get(key) || null;
},
delete(key: string) {
testMap.delete(key);
testExpirationMap.delete(key);
},
},
} satisfies BetterAuthOptions;
(await getMigrations(testOpts)).runMigrations();
const testAuthContext = await init(testOpts);
const testInternalAdapter = testAuthContext.internalAdapter;
// Create a user
const user = await testInternalAdapter.createUser({
name: "corrupt-sessions-test-user",
email: "corrupt-sessions-test@example.com",
});
// Create a session
const session = await testInternalAdapter.createSession(user.id);
// Manually corrupt the active-sessions list by adding duplicate tokens
const listStr = testMap.get(`active-sessions-${user.id}`);
const list = safeJSONParse<{ token: string; expiresAt: number }[]>(
listStr!,
);
// Add duplicates of the same token
const corruptedList = [
...list!,
{ token: session.token, expiresAt: session.expiresAt.getTime() },
{ token: session.token, expiresAt: session.expiresAt.getTime() },
];
testMap.set(`active-sessions-${user.id}`, JSON.stringify(corruptedList));
// Verify corruption
const corruptedListStr = testMap.get(`active-sessions-${user.id}`);
const parsed = safeJSONParse<{ token: string; expiresAt: number }[]>(
corruptedListStr!,
);
expect(parsed!.length).toBe(3); // 1 original + 2 duplicates
// listSessions should deduplicate and return only unique sessions
const sessions = await testInternalAdapter.listSessions(user.id);
expect(sessions.length).toBe(1);
// Clean up DB
testDb.close();
});
});
+33 -31
View File
@@ -10,7 +10,8 @@ import {
} from "@better-auth/core/context";
import type { DBAdapter, Where } from "@better-auth/core/db/adapter";
import type { InternalLogger } from "@better-auth/core/env";
import { generateId, safeJSONParse } from "@better-auth/core/utils";
import { generateId } from "@better-auth/core/utils/id";
import { safeJSONParse } from "@better-auth/core/utils/json";
import type { Account, Session, User, Verification } from "../types";
import { getDate } from "../utils/date";
import { getIp } from "../utils/get-request-ip";
@@ -158,23 +159,28 @@ export const createInternalAdapter = (
safeJSONParse(currentList) || [];
const now = Date.now();
const validSessions = list.filter((s) => s.expiresAt > now);
const sessions = [];
const seenTokens = new Set<string>();
const sessions: Session[] = [];
for (const session of validSessions) {
const sessionStringified = await secondaryStorage.get(session.token);
if (sessionStringified) {
const s = safeJSONParse<{
session: Session;
user: User;
}>(sessionStringified);
if (!s) return [];
const parsedSession = parseSessionOutput(ctx.options, {
...s.session,
expiresAt: new Date(s.session.expiresAt),
});
sessions.push(parsedSession);
}
for (const { token, expiresAt } of list) {
if (expiresAt <= now || seenTokens.has(token)) continue;
seenTokens.add(token);
const data = await secondaryStorage.get(token);
if (!data) continue;
const parsed = safeJSONParse<{
session: Session;
user: User;
}>(data);
if (!parsed) continue;
sessions.push(
parseSessionOutput(ctx.options, {
...parsed.session,
expiresAt: new Date(parsed.session.expiresAt),
}),
);
}
return sessions;
}
@@ -313,22 +319,18 @@ export const createInternalAdapter = (
if (currentList) {
list = safeJSONParse(currentList) || [];
list = list.filter((session) => session.expiresAt > now);
list = list.filter(
(session) =>
session.expiresAt > now && session.token !== data.token,
);
}
const sorted = list.sort((a, b) => a.expiresAt - b.expiresAt);
let furthestSessionExp = sorted.at(-1)?.expiresAt;
sorted.push({
token: data.token,
expiresAt: data.expiresAt.getTime(),
});
if (
!furthestSessionExp ||
furthestSessionExp < data.expiresAt.getTime()
) {
furthestSessionExp = data.expiresAt.getTime();
}
const sorted = [
...list,
{ token: data.token, expiresAt: data.expiresAt.getTime() },
].sort((a, b) => a.expiresAt - b.expiresAt);
const furthestSessionExp =
sorted.at(-1)?.expiresAt ?? data.expiresAt.getTime();
const furthestSessionTTL = Math.max(
Math.floor((furthestSessionExp - now) / 1000),
0,
@@ -1,4 +1,4 @@
import { safeJSONParse } from "@better-auth/core/utils";
import { safeJSONParse } from "@better-auth/core/utils/json";
import { beforeEach, describe, expect, it } from "vitest";
import { getTestInstance } from "../test-utils/test-instance";
+3 -1
View File
@@ -6,7 +6,9 @@ export * from "@better-auth/core/db";
export * from "@better-auth/core/env";
export * from "@better-auth/core/error";
export * from "@better-auth/core/oauth2";
export * from "@better-auth/core/utils";
export * from "@better-auth/core/utils/error-codes";
export * from "@better-auth/core/utils/id";
export * from "@better-auth/core/utils/json";
//#endregion
export { betterAuth } from "./auth";
// @ts-expect-error
@@ -20,9 +20,7 @@ export const tanstackStartCookies = () => {
const setCookies = returned?.get("set-cookie");
if (!setCookies) return;
const parsed = parseSetCookieHeader(setCookies);
const { setCookie } = await import(
"@tanstack/react-start/server"
);
const { setCookie } = await import("@tanstack/start-server-core");
parsed.forEach((value, key) => {
if (!key) return;
const opts = {
@@ -1,4 +1,4 @@
import { defineErrorCodes } from "@better-auth/core/utils";
import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
export const ADMIN_ERROR_CODES = defineErrorCodes({
FAILED_TO_CREATE_USER: "Failed to create user",
@@ -8,6 +8,7 @@ export const anonymousClient = () => {
$InferServerPlugin: {} as ReturnType<typeof anonymous>,
pathMethods: {
"/sign-in/anonymous": "POST",
"/delete-anonymous-user": "POST",
},
atomListeners: [
{
@@ -1,4 +1,4 @@
import { defineErrorCodes } from "@better-auth/core/utils";
import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
export const ANONYMOUS_ERROR_CODES = defineErrorCodes({
INVALID_EMAIL_FORMAT: "Email was not generated in a valid format",
@@ -3,7 +3,7 @@ import {
createAuthEndpoint,
createAuthMiddleware,
} from "@better-auth/core/api";
import { generateId } from "@better-auth/core/utils";
import { generateId } from "@better-auth/core/utils/id";
import * as z from "zod";
import {
APIError,
@@ -1,6 +1,6 @@
import type { GenericEndpointContext } from "@better-auth/core";
import type { SecondaryStorage } from "@better-auth/core/db";
import { safeJSONParse } from "@better-auth/core/utils";
import { safeJSONParse } from "@better-auth/core/utils/json";
import type { PredefinedApiKeyOptions } from "./routes";
import type { ApiKey } from "./types";
@@ -1,4 +1,4 @@
import { defineErrorCodes } from "@better-auth/core/utils";
import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
export const API_KEY_ERROR_CODES = defineErrorCodes({
INVALID_METADATA_TYPE: "metadata must be an object or undefined",
@@ -1,10 +1,10 @@
import type { AuthContext, Awaitable } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
import { APIError } from "@better-auth/core/error";
import { safeJSONParse } from "@better-auth/core/utils";
import { generateId } from "@better-auth/core/utils/id";
import { safeJSONParse } from "@better-auth/core/utils/json";
import * as z from "zod";
import { getSessionFromCtx } from "../../../api";
import { generateId } from "../../../utils";
import { getDate } from "../../../utils/date";
import { API_KEY_TABLE_NAME, API_KEY_ERROR_CODES as ERROR_CODES } from "..";
import { defaultKeyHasher } from "../";
@@ -1,7 +1,7 @@
import type { AuthContext } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
import { APIError } from "@better-auth/core/error";
import { safeJSONParse } from "@better-auth/core/utils";
import { safeJSONParse } from "@better-auth/core/utils/json";
import * as z from "zod";
import { sessionMiddleware } from "../../../api";
import { API_KEY_ERROR_CODES as ERROR_CODES } from "..";
@@ -1,6 +1,6 @@
import type { AuthContext } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
import { safeJSONParse } from "@better-auth/core/utils";
import { safeJSONParse } from "@better-auth/core/utils/json";
import { sessionMiddleware } from "../../../api";
import {
batchMigrateLegacyMetadata,
@@ -1,7 +1,7 @@
import type { AuthContext } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
import { APIError } from "@better-auth/core/error";
import { safeJSONParse } from "@better-auth/core/utils";
import { safeJSONParse } from "@better-auth/core/utils/json";
import * as z from "zod";
import { getSessionFromCtx } from "../../../api";
import { getDate } from "../../../utils/date";
@@ -1,7 +1,7 @@
import type { AuthContext, GenericEndpointContext } from "@better-auth/core";
import { createAuthEndpoint } from "@better-auth/core/api";
import { APIError } from "@better-auth/core/error";
import { safeJSONParse } from "@better-auth/core/utils";
import { safeJSONParse } from "@better-auth/core/utils/json";
import * as z from "zod";
import { isAPIError } from "../../../utils/is-api-error";
import { role } from "../../access";
@@ -1,5 +1,5 @@
// These error codes are returned by the API
import { defineErrorCodes } from "@better-auth/core/utils";
import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
export const EXTERNAL_ERROR_CODES = defineErrorCodes({
VERIFICATION_FAILED: "Captcha verification failed",
@@ -1,4 +1,4 @@
import { defineErrorCodes } from "@better-auth/core/utils";
import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
export const DEVICE_AUTHORIZATION_ERROR_CODES = defineErrorCodes({
INVALID_DEVICE_CODE: "Invalid device code",
@@ -1,4 +1,4 @@
import { defineErrorCodes } from "@better-auth/core/utils";
import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
export const EMAIL_OTP_ERROR_CODES = defineErrorCodes({
OTP_EXPIRED: "OTP expired",
@@ -1,4 +1,4 @@
import { defineErrorCodes } from "@better-auth/core/utils";
import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
export const GENERIC_OAUTH_ERROR_CODES = defineErrorCodes({
INVALID_OAUTH_CONFIGURATION: "Invalid OAuth configuration",
@@ -1,6 +1,6 @@
import type { BetterAuthPlugin } from "@better-auth/core";
import { getCurrentAuthContext } from "@better-auth/core/context";
import { defineErrorCodes } from "@better-auth/core/utils";
import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
import { createHash } from "@better-auth/utils/hash";
import { betterFetch } from "@better-fetch/fetch";
import { APIError } from "../../api";
@@ -8,7 +8,7 @@ import {
createAuthMiddleware,
} from "@better-auth/core/api";
import { isProduction, logger } from "@better-auth/core/env";
import { safeJSONParse } from "@better-auth/core/utils";
import { safeJSONParse } from "@better-auth/core/utils/json";
import { getWebcryptoSubtle } from "@better-auth/utils";
import { base64 } from "@better-auth/utils/base64";
import { createHash } from "@better-auth/utils/hash";
@@ -1,4 +1,4 @@
import { defineErrorCodes } from "@better-auth/core/utils";
import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
export const MULTI_SESSION_ERROR_CODES = defineErrorCodes({
INVALID_SESSION_TOKEN: "Invalid session token",
@@ -1,4 +1,4 @@
import { defineErrorCodes } from "@better-auth/core/utils";
import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
export const ORGANIZATION_ERROR_CODES = defineErrorCodes({
YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION:
@@ -1,8 +1,8 @@
import type { BetterAuthPluginDBSchema } from "@better-auth/core/db";
import { generateId } from "@better-auth/core/utils/id";
import type { Prettify } from "better-call";
import * as z from "zod";
import type { InferAdditionalFieldsFromPluginOptions } from "../../db";
import { generateId } from "../../utils";
import type { OrganizationOptions } from "./types";
type InferSchema<
@@ -1,4 +1,4 @@
import { defineErrorCodes } from "@better-auth/core/utils";
import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
export const PHONE_NUMBER_ERROR_CODES = defineErrorCodes({
INVALID_PHONE_NUMBER: "Invalid phone number",
@@ -1,6 +1,6 @@
import { createAuthEndpoint } from "@better-auth/core/api";
import { APIError } from "@better-auth/core/error";
import { safeJSONParse } from "@better-auth/core/utils";
import { safeJSONParse } from "@better-auth/core/utils/json";
import * as z from "zod";
import { sessionMiddleware } from "../../../api";
import { symmetricDecrypt, symmetricEncrypt } from "../../../crypto";
@@ -29,6 +29,10 @@ export const twoFactorClient = (
"/two-factor/enable": "POST",
"/two-factor/send-otp": "POST",
"/two-factor/generate-backup-codes": "POST",
"/two-factor/get-totp-uri": "POST",
"/two-factor/verify-totp": "POST",
"/two-factor/verify-otp": "POST",
"/two-factor/verify-backup-code": "POST",
},
fetchPlugins: [
{
@@ -1,4 +1,4 @@
import { defineErrorCodes } from "@better-auth/core/utils";
import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
export const TWO_FACTOR_ERROR_CODES = defineErrorCodes({
OTP_NOT_ENABLED: "OTP not enabled",
@@ -1,4 +1,4 @@
import { defineErrorCodes } from "@better-auth/core/utils";
import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
export const USERNAME_ERROR_CODES = defineErrorCodes({
INVALID_USERNAME_OR_PASSWORD: "Invalid username or password",
+5 -14
View File
@@ -6,25 +6,16 @@ export type FilteredAPI<API> = Omit<
API extends { [key in infer K]: Endpoint }
? K extends string
? K extends "getSession"
? K
: API[K]["options"]["metadata"] extends { isAction: false }
? never
: API[K]["options"]["metadata"] extends
| { isAction: false }
| { scope: "http" }
? K
: never
: never
: never
>;
export type FilterActions<API> = Omit<
API,
API extends { [key in infer K]: Endpoint }
? K extends string
? API[K]["options"]["metadata"] extends { isAction: false }
? K
: never
: never
: never
>;
export type InferSessionAPI<API> = API extends {
[key: string]: infer E;
}
@@ -59,4 +50,4 @@ export type InferSessionAPI<API> = API extends {
>
: never;
export type InferAPI<API> = InferSessionAPI<API> & API;
export type InferAPI<API> = InferSessionAPI<API> & FilteredAPI<API>;
+12 -2
View File
@@ -76,14 +76,24 @@ describe("general types", async (it) => {
},
async () => "ok",
),
testNonAction: createAuthEndpoint(
"/test-non-action",
{
method: "GET",
metadata: {
isAction: false,
},
},
async () => "ok",
),
},
},
],
});
expectTypeOf<typeof auth.api>().toHaveProperty("testServerScoped");
expectTypeOf<typeof auth.api>().toHaveProperty("testVirtual");
expectTypeOf<typeof auth.api>().not.toHaveProperty("testHttpScoped");
expectTypeOf<typeof auth.api>().not.toHaveProperty("testHTTPScoped");
expectTypeOf<typeof auth.api>().not.toHaveProperty("testNonAction");
});
it("should infer additional fields from plugins", async () => {
-1
View File
@@ -1,3 +1,2 @@
export { generateId } from "@better-auth/core/utils";
export * from "../oauth2/state";
export * from "./hide-metadata";
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@better-auth/cli",
"version": "1.5.0-beta.3",
"version": "1.5.0-beta.6",
"type": "module",
"description": "The CLI for Better Auth",
"module": "dist/index.mjs",
+207 -80
View File
@@ -11,101 +11,178 @@ interface MCPOptions {
claudeCode?: boolean;
openCode?: boolean;
manual?: boolean;
localOnly?: boolean;
remoteOnly?: boolean;
}
const REMOTE_MCP_URL =
"https://mcp.chonkie.ai/better-auth/better-auth-builder/mcp";
const LOCAL_MCP_COMMAND = "npx @better-auth/mcp";
async function mcpAction(options: MCPOptions) {
const mcpUrl = "https://mcp.chonkie.ai/better-auth/better-auth-builder/mcp";
const mcpName = "better-auth";
const installLocal = !options.remoteOnly;
const installRemote = !options.localOnly;
if (options.cursor) {
await handleCursorAction(mcpUrl, mcpName);
await handleCursorAction(installLocal, installRemote);
} else if (options.claudeCode) {
handleClaudeCodeAction(mcpUrl);
handleClaudeCodeAction(installLocal, installRemote);
} else if (options.openCode) {
handleOpenCodeAction(mcpUrl);
handleOpenCodeAction(installLocal, installRemote);
} else if (options.manual) {
handleManualAction(mcpUrl, mcpName);
handleManualAction(installLocal, installRemote);
} else {
showAllOptions(mcpUrl, mcpName);
showAllOptions();
}
}
async function handleCursorAction(mcpUrl: string, mcpName: string) {
const mcpConfig = {
url: mcpUrl,
};
const encodedConfig = base64.encode(
new TextEncoder().encode(JSON.stringify(mcpConfig)),
);
const deeplinkUrl = `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(mcpName)}&config=${encodedConfig}`;
async function _installMcpServers(
client: "cursor" | "claude-code" | "open-code" | "manual" | string,
installLocal: boolean = true,
installRemote: boolean = true,
) {
switch (client) {
case "cursor":
await handleCursorAction(installLocal, installRemote);
break;
case "claude-code":
handleClaudeCodeAction(installLocal, installRemote);
break;
case "open-code":
handleOpenCodeAction(installLocal, installRemote);
break;
case "manual":
handleManualAction(installLocal, installRemote);
break;
}
}
async function handleCursorAction(
installLocal: boolean,
installRemote: boolean,
) {
console.log(chalk.bold.blue("🚀 Adding Better Auth MCP to Cursor..."));
try {
const platform = os.platform();
let command: string;
const platform = os.platform();
let openCommand: string;
switch (platform) {
case "darwin":
command = `open "${deeplinkUrl}"`;
break;
case "win32":
command = `start "" "${deeplinkUrl}"`;
break;
case "linux":
command = `xdg-open "${deeplinkUrl}"`;
break;
default:
throw new Error(`Unsupported platform: ${platform}`);
switch (platform) {
case "darwin":
openCommand = "open";
break;
case "win32":
openCommand = "start";
break;
case "linux":
openCommand = "xdg-open";
break;
default:
throw new Error(`Unsupported platform: ${platform}`);
}
const installed: string[] = [];
if (installRemote) {
const remoteConfig = { url: REMOTE_MCP_URL };
const encodedRemote = base64.encode(
new TextEncoder().encode(JSON.stringify(remoteConfig)),
);
const remoteDeeplink = `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent("better-auth-docs")}&config=${encodedRemote}`;
try {
const cmd =
platform === "win32"
? `start "" "${remoteDeeplink}"`
: `${openCommand} "${remoteDeeplink}"`;
execSync(cmd, { stdio: "inherit" });
installed.push("better-auth-docs (remote - documentation & search)");
} catch {
console.log(
chalk.yellow("\n⚠ Could not automatically open Cursor for remote MCP."),
);
}
}
execSync(command, { stdio: "inherit" });
console.log(chalk.green("\n✓ Cursor MCP installed successfully!"));
} catch {
console.log(
chalk.yellow(
"\n⚠ Could not automatically open Cursor. Please copy the deeplink URL above and open it manually.",
),
if (installLocal) {
await new Promise((resolve) => setTimeout(resolve, 1000));
const localConfig = { command: LOCAL_MCP_COMMAND };
const encodedLocal = base64.encode(
new TextEncoder().encode(JSON.stringify(localConfig)),
);
console.log(
chalk.gray(
"\nYou can also manually add this configuration to your Cursor MCP settings:",
),
);
console.log(chalk.gray(JSON.stringify(mcpConfig, null, 2)));
const localDeeplink = `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent("better-auth")}&config=${encodedLocal}`;
try {
const cmd =
platform === "win32"
? `start "" "${localDeeplink}"`
: `${openCommand} "${localDeeplink}"`;
execSync(cmd, { stdio: "inherit" });
installed.push("better-auth (local - setup & diagnostics)");
} catch {
console.log(
chalk.yellow("\n⚠ Could not automatically open Cursor for local MCP."),
);
}
}
if (installed.length > 0) {
console.log(chalk.green("\n✓ Cursor MCP servers installed:"));
for (const name of installed) {
console.log(chalk.green(`${name}`));
}
}
console.log(chalk.bold.white("\n✨ Next Steps:"));
console.log(
chalk.gray("• The MCP server will be added to your Cursor configuration"),
chalk.gray("• The MCP servers will be added to your Cursor configuration"),
);
console.log(
chalk.gray("• You can now use Better Auth features directly in Cursor"),
);
console.log(
chalk.gray(
'• Try: "Set up Better Auth with Google login" or "Help me debug my auth"',
),
);
}
function handleClaudeCodeAction(mcpUrl: string) {
function handleClaudeCodeAction(installLocal: boolean, installRemote: boolean) {
console.log(chalk.bold.blue("🤖 Adding Better Auth MCP to Claude Code..."));
const command = `claude mcp add --transport http better-auth ${mcpUrl}`;
const commands: string[] = [];
try {
execSync(command, { stdio: "inherit" });
console.log(chalk.green("\n✓ Claude Code MCP installed successfully!"));
} catch {
console.log(
chalk.yellow(
"\n⚠ Could not automatically add to Claude Code. Please run this command manually:",
),
if (installRemote) {
commands.push(
`claude mcp add --transport http better-auth-docs ${REMOTE_MCP_URL}`,
);
console.log(chalk.cyan(command));
}
if (installLocal) {
commands.push(`claude mcp add better-auth -- ${LOCAL_MCP_COMMAND}`);
}
let anySucceeded = false;
for (const command of commands) {
try {
execSync(command, { stdio: "inherit" });
anySucceeded = true;
} catch {
console.log(
chalk.yellow(
"\n⚠ Could not automatically add to Claude Code. Please run this command manually:",
),
);
console.log(chalk.cyan(command));
}
}
if (anySucceeded) {
console.log(chalk.green("\n✓ Claude Code MCP configured!"));
}
console.log(chalk.bold.white("\n✨ Next Steps:"));
console.log(
chalk.gray(
"• The MCP server will be added to your Claude Code configuration",
"• The MCP servers will be added to your Claude Code configuration",
),
);
console.log(
@@ -115,18 +192,30 @@ function handleClaudeCodeAction(mcpUrl: string) {
);
}
function handleOpenCodeAction(mcpUrl: string) {
function handleOpenCodeAction(installLocal: boolean, installRemote: boolean) {
console.log(chalk.bold.blue("🔧 Adding Better Auth MCP to Open Code..."));
const mcpConfig: Record<string, unknown> = {};
if (installRemote) {
mcpConfig["better-auth-docs"] = {
type: "remote",
url: REMOTE_MCP_URL,
enabled: true,
};
}
if (installLocal) {
mcpConfig["better-auth"] = {
type: "stdio",
command: LOCAL_MCP_COMMAND,
enabled: true,
};
}
const openCodeConfig = {
$schema: "https://opencode.ai/config.json",
mcp: {
"better-auth": {
type: "remote",
url: mcpUrl,
enabled: true,
},
},
mcp: mcpConfig,
};
const configPath = path.join(process.cwd(), "opencode.json");
@@ -154,7 +243,7 @@ function handleOpenCodeAction(mcpUrl: string) {
console.log(
chalk.green(`\n✓ Open Code configuration written to ${configPath}`),
);
console.log(chalk.green("✓ Better Auth MCP added successfully!"));
console.log(chalk.green("✓ Better Auth MCP servers added successfully!"));
} catch {
console.log(
chalk.yellow(
@@ -165,20 +254,28 @@ function handleOpenCodeAction(mcpUrl: string) {
}
console.log(chalk.bold.white("\n✨ Next Steps:"));
console.log(chalk.gray("• Restart Open Code to load the new MCP server"));
console.log(chalk.gray("• Restart Open Code to load the new MCP servers"));
console.log(
chalk.gray("• You can now use Better Auth features directly in Open Code"),
);
}
function handleManualAction(mcpUrl: string, mcpName: string) {
console.log(chalk.bold.blue("📝 Adding Better Auth MCP Configuration..."));
function handleManualAction(installLocal: boolean, installRemote: boolean) {
console.log(chalk.bold.blue("📝 Better Auth MCP Configuration..."));
const manualConfig = {
[mcpName]: {
url: mcpUrl,
},
};
const manualConfig: Record<string, unknown> = {};
if (installRemote) {
manualConfig["better-auth-docs"] = {
url: REMOTE_MCP_URL,
};
}
if (installLocal) {
manualConfig["better-auth"] = {
command: LOCAL_MCP_COMMAND,
};
}
const configPath = path.join(process.cwd(), "mcp.json");
@@ -196,7 +293,7 @@ function handleManualAction(mcpUrl: string, mcpName: string) {
fs.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2));
console.log(chalk.green(`\n✓ MCP configuration written to ${configPath}`));
console.log(chalk.green("✓ Better Auth MCP added successfully!"));
console.log(chalk.green("✓ Better Auth MCP servers added successfully!"));
} catch {
console.log(
chalk.yellow(
@@ -207,7 +304,7 @@ function handleManualAction(mcpUrl: string, mcpName: string) {
}
console.log(chalk.bold.white("\n✨ Next Steps:"));
console.log(chalk.gray("• Restart your MCP client to load the new server"));
console.log(chalk.gray("• Restart your MCP client to load the new servers"));
console.log(
chalk.gray(
"• You can now use Better Auth features directly in your MCP client",
@@ -215,12 +312,12 @@ function handleManualAction(mcpUrl: string, mcpName: string) {
);
}
function showAllOptions(mcpUrl: string, mcpName: string) {
console.log(chalk.bold.blue("🔌 Better Auth MCP Server"));
function showAllOptions() {
console.log(chalk.bold.blue("🔌 Better Auth MCP Servers"));
console.log(chalk.gray("Choose your MCP client to get started:"));
console.log();
console.log(chalk.bold.white("Available Commands:"));
console.log(chalk.bold.white("MCP Clients:"));
console.log(chalk.cyan(" --cursor ") + chalk.gray("Add to Cursor"));
console.log(
chalk.cyan(" --claude-code ") + chalk.gray("Add to Claude Code"),
@@ -230,12 +327,42 @@ function showAllOptions(mcpUrl: string, mcpName: string) {
chalk.cyan(" --manual ") + chalk.gray("Manual configuration"),
);
console.log();
console.log(chalk.bold.white("Server Selection:"));
console.log(
chalk.cyan(" --local-only ") +
chalk.gray("Install only local MCP (setup & diagnostics)"),
);
console.log(
chalk.cyan(" --remote-only ") +
chalk.gray("Install only remote MCP (documentation & search)"),
);
console.log(chalk.gray(" (default: install both servers)"));
console.log();
console.log(chalk.bold.white("Servers:"));
console.log(
chalk.gray(" • ") +
chalk.white("better-auth") +
chalk.gray(" (local) - Setup auth, diagnose issues, validate config"),
);
console.log(
chalk.gray(" • ") +
chalk.white("better-auth-docs") +
chalk.gray(" (remote) - Search documentation, code examples"),
);
console.log();
}
export const mcp = new Command("mcp")
.description("Add Better Auth MCP server to MCP Clients")
.description("Add Better Auth MCP servers to MCP Clients")
.option("--cursor", "Automatically open Cursor with the MCP configuration")
.option("--claude-code", "Show Claude Code MCP configuration command")
.option("--open-code", "Show Open Code MCP configuration")
.option("--manual", "Show manual MCP configuration for mcp.json")
.option("--local-only", "Install only local MCP server (setup & diagnostics)")
.option(
"--remote-only",
"Install only remote MCP server (documentation & search)",
)
.action(mcpAction);
+1 -1
View File
@@ -1,7 +1,7 @@
import { existsSync } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { capitalizeFirstLetter } from "@better-auth/core/utils";
import { capitalizeFirstLetter } from "@better-auth/core/utils/string";
import { produceSchema } from "@mrleebo/prisma-ast";
import { initGetFieldName, initGetModelName } from "better-auth/adapters";
import type { DBFieldType } from "better-auth/db";
+8 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@better-auth/core",
"version": "1.5.0-beta.3",
"version": "1.5.0-beta.6",
"description": "The most comprehensive authentication framework for TypeScript.",
"type": "module",
"repository": {
@@ -48,10 +48,10 @@
"types": "./dist/error/index.d.mts",
"default": "./dist/error/index.mjs"
},
"./utils": {
"dev-source": "./src/utils/index.ts",
"types": "./dist/utils/index.d.mts",
"default": "./dist/utils/index.mjs"
"./utils/*": {
"dev-source": "./src/utils/*.ts",
"types": "./dist/utils/*.d.mts",
"default": "./dist/utils/*.mjs"
},
"./social-providers": {
"dev-source": "./src/social-providers/index.ts",
@@ -97,6 +97,9 @@
"utils": [
"dist/utils/index.d.mts"
],
"utils/*": [
"dist/utils/*.d.mts"
],
"social-providers": [
"dist/social-providers/index.d.mts"
],
@@ -2,6 +2,7 @@ import type { AsyncLocalStorage } from "@better-auth/core/async_hooks";
import { getAsyncLocalStorage } from "@better-auth/core/async_hooks";
import type { EndpointContext, InputContext } from "better-call";
import type { AuthContext } from "../types";
import { getBetterAuthGlobal } from "./global";
export type AuthEndpointContext = Partial<
InputContext<string, any> & EndpointContext<string, any>
@@ -9,15 +10,15 @@ export type AuthEndpointContext = Partial<
context: AuthContext;
};
let currentContextAsyncStorage: AsyncLocalStorage<AuthEndpointContext> | null =
null;
const ensureAsyncStorage = async () => {
if (!currentContextAsyncStorage) {
const betterAuthGlobal = getBetterAuthGlobal();
if (!betterAuthGlobal.context.endpointContextAsyncStorage) {
const AsyncLocalStorage = await getAsyncLocalStorage();
currentContextAsyncStorage = new AsyncLocalStorage();
betterAuthGlobal.context.endpointContextAsyncStorage =
new AsyncLocalStorage<AuthEndpointContext>();
}
return currentContextAsyncStorage;
return betterAuthGlobal.context
.endpointContextAsyncStorage as AsyncLocalStorage<AuthEndpointContext>;
};
/**
+43
View File
@@ -0,0 +1,43 @@
import type { AsyncLocalStorage } from "@better-auth/core/async_hooks";
interface BetterAuthGlobal {
/**
* Used to track the number of BetterAuth instances in the same process.
*
* Debugging purposes only.
*/
epoch: number;
/**
* Stores the AsyncLocalStorage instances for each context.
*/
context: Record<string, AsyncLocalStorage<unknown>>;
}
const symbol = Symbol.for("better-auth:global");
let bind: BetterAuthGlobal | null = null;
const context: Record<string, AsyncLocalStorage<unknown>> = {};
/**
* We store context instance in the globalThis.
*
* The reason we do this is that some bundlers, web framework, or package managers might
* create multiple copies of BetterAuth in the same process intentionally or unintentionally.
*
* For example, yarn v1, Next.js, SSR, Vite...
*/
export function getBetterAuthGlobal(): BetterAuthGlobal {
if (!(globalThis as any)[symbol]) {
(globalThis as any)[symbol] = {
epoch: 1,
context,
};
bind = (globalThis as any)[symbol] as BetterAuthGlobal;
} else {
if (!bind) {
bind = (globalThis as any)[symbol] as BetterAuthGlobal;
bind.epoch++;
}
}
return (globalThis as any)[symbol] as BetterAuthGlobal;
}
-29
View File
@@ -19,32 +19,3 @@ export {
runWithAdapter,
runWithTransaction,
} from "./transaction";
const glo: any =
typeof globalThis !== "undefined"
? globalThis
: typeof window !== "undefined"
? window
: typeof global !== "undefined"
? global
: {};
const importIdentifier = "__ $BETTER_AUTH$ __";
if (glo[importIdentifier] === true) {
/**
* Dear reader of this message. Please take this seriously.
*
* If you see this message, make sure that you only import one version of Better Auth. In many cases,
* your package manager installs two versions of Better Auth that are used by different packages within your project.
*
* This often leads to issues that are hard to debug. We often need to ensure async local storage instance,
* If you imported different versions of Better Auth, it is impossible for us to
* do status synchronization per request anymore - which might break the states.
*
*/
console.error(
"Better Auth was already imported. This breaks async local storage instance and will lead to issues!",
);
}
glo[importIdentifier] = true;
+7 -6
View File
@@ -1,17 +1,18 @@
import type { AsyncLocalStorage } from "@better-auth/core/async_hooks";
import { getAsyncLocalStorage } from "@better-auth/core/async_hooks";
import { getBetterAuthGlobal } from "./global";
export type RequestStateWeakMap = WeakMap<object, any>;
let requestStateAsyncStorage: AsyncLocalStorage<RequestStateWeakMap> | null =
null;
const ensureAsyncStorage = async () => {
if (!requestStateAsyncStorage) {
const betterAuthGlobal = getBetterAuthGlobal();
if (!betterAuthGlobal.context.requestStateAsyncStorage) {
const AsyncLocalStorage = await getAsyncLocalStorage();
requestStateAsyncStorage = new AsyncLocalStorage();
betterAuthGlobal.context.requestStateAsyncStorage =
new AsyncLocalStorage<RequestStateWeakMap>();
}
return requestStateAsyncStorage;
return betterAuthGlobal.context
.requestStateAsyncStorage as AsyncLocalStorage<RequestStateWeakMap>;
};
export async function getRequestStateAsyncLocalStorage() {
+7 -7
View File
@@ -1,16 +1,16 @@
import type { AsyncLocalStorage } from "@better-auth/core/async_hooks";
import type { AsyncLocalStorage } from "node:async_hooks";
import { getAsyncLocalStorage } from "@better-auth/core/async_hooks";
import type { DBAdapter, DBTransactionAdapter } from "../db/adapter";
let currentAdapterAsyncStorage: AsyncLocalStorage<DBTransactionAdapter> | null =
null;
import { getBetterAuthGlobal } from "./global";
const ensureAsyncStorage = async () => {
if (!currentAdapterAsyncStorage) {
const betterAuthGlobal = getBetterAuthGlobal();
if (!betterAuthGlobal.context.adapterAsyncStorage) {
const AsyncLocalStorage = await getAsyncLocalStorage();
currentAdapterAsyncStorage = new AsyncLocalStorage();
betterAuthGlobal.context.adapterAsyncStorage = new AsyncLocalStorage();
}
return currentAdapterAsyncStorage;
return betterAuthGlobal.context
.adapterAsyncStorage as AsyncLocalStorage<DBTransactionAdapter>;
};
/**
+1 -1
View File
@@ -1,6 +1,6 @@
import { logger } from "../../env";
import type { BetterAuthOptions } from "../../types";
import { generateId as defaultGenerateId } from "../../utils";
import { generateId as defaultGenerateId } from "../../utils/id";
import type { BetterAuthDBSchema, DBFieldAttribute } from "../type";
import { initGetDefaultModelName } from "./get-default-model-name";
+1 -1
View File
@@ -1,4 +1,4 @@
import { defineErrorCodes } from "../utils";
import { defineErrorCodes } from "../utils/error-codes";
export const BASE_ERROR_CODES = defineErrorCodes({
USER_NOT_FOUND: "User not found",
@@ -40,7 +40,7 @@ export async function createAuthorizationURL({
additionalParams?: Record<string, string> | undefined;
scopeJoiner?: string | undefined;
}) {
const url = new URL(authorizationEndpoint);
const url = new URL(options.authorizationEndpoint || authorizationEndpoint);
url.searchParams.set("response_type", responseType || "code");
const primaryClientId = Array.isArray(options.clientId)
? options.clientId[0]
@@ -119,6 +119,12 @@ export type ProviderOptions<Profile extends Record<string, any> = any> = {
* whitelisted in the provider's dashboard.
*/
redirectURI?: string | undefined;
/**
* Custom authorization endpoint URL.
* Use this to override the default authorization endpoint of the provider.
* Useful for testing with local OAuth servers or using sandbox environments.
*/
authorizationEndpoint?: string | undefined;
/**
* The client key of your application
* Tiktok Social Provider uses this field instead of clientId
+4 -1
View File
@@ -267,7 +267,10 @@ export type AuthContext<Options extends BetterAuthOptions = BetterAuthOptions> =
window: number;
max: number;
storage: "memory" | "database" | "secondary-storage";
} & BetterAuthRateLimitOptions;
} & Omit<
BetterAuthRateLimitOptions,
"enabled" | "window" | "max" | "storage"
>;
adapter: DBAdapter<Options>;
internalAdapter: InternalAdapter<Options>;
createAuthCookie: CreateCookieGetterFn;
+2
View File
@@ -13,6 +13,8 @@ export type {
BetterAuthAdvancedOptions,
BetterAuthOptions,
BetterAuthRateLimitOptions,
BetterAuthRateLimitRule,
BetterAuthRateLimitStorage,
GenerateIdFn,
} from "./init-options";
export type { BetterAuthPlugin, HookEndpointContext } from "./plugin";
+26 -32
View File
@@ -37,25 +37,37 @@ export type GenerateIdFn = (options: {
size?: number | undefined;
}) => string | false;
export type BetterAuthRateLimitOptions = {
/**
* By default, rate limiting is only
* enabled on production.
*/
enabled?: boolean | undefined;
export interface BetterAuthRateLimitStorage {
get: (key: string) => Promise<RateLimit | null | undefined>;
set: (
key: string,
value: RateLimit,
update?: boolean | undefined,
) => Promise<void>;
}
export type BetterAuthRateLimitRule = {
/**
* Default window to use for rate limiting. The value
* should be in seconds.
*
* @default 10 seconds
*/
window?: number | undefined;
window: number;
/**
* The default maximum number of requests allowed within the window.
*
* @default 100 requests
*/
max?: number | undefined;
max: number;
};
export type BetterAuthRateLimitOptions = Optional<BetterAuthRateLimitRule> & {
/**
* By default, rate limiting is only
* enabled on production.
*/
enabled?: boolean | undefined;
/**
* Custom rate limit rules to apply to
* specific paths.
@@ -63,27 +75,12 @@ export type BetterAuthRateLimitOptions = {
customRules?:
| {
[key: string]:
| {
/**
* The window to use for the custom rule.
*/
window: number;
/**
* The maximum number of requests allowed within the window.
*/
max: number;
}
| BetterAuthRateLimitRule
| false
| ((request: Request) =>
| { window: number; max: number }
| false
| Promise<
| {
window: number;
max: number;
}
| false
>);
| ((
request: Request,
currentRule: BetterAuthRateLimitRule,
) => Awaitable<false | BetterAuthRateLimitRule>);
}
| undefined;
/**
@@ -113,10 +110,7 @@ export type BetterAuthRateLimitOptions = {
* NOTE: If custom storage is used storage
* is ignored
*/
customStorage?: {
get: (key: string) => Promise<RateLimit | undefined>;
set: (key: string, value: RateLimit) => Promise<void>;
};
customStorage?: BetterAuthRateLimitStorage;
};
export type BetterAuthAdvancedOptions = {
-5
View File
@@ -1,5 +0,0 @@
export { deprecate } from "./deprecate";
export { defineErrorCodes } from "./error-codes";
export { generateId } from "./id";
export { safeJSONParse } from "./json";
export { capitalizeFirstLetter } from "./string";
+3 -1
View File
@@ -14,9 +14,11 @@ export default defineConfig({
"./src/oauth2/index.ts",
"./src/api/index.ts",
"./src/social-providers/index.ts",
"./src/utils/index.ts",
"./src/utils/*.ts",
"!./src/utils/*.test.ts",
"./src/error/index.ts",
],
external: ["@better-auth/core/async_hooks"],
unbundle: true,
clean: true,
});

Some files were not shown because too many files have changed in this diff Show More