mirror of
https://github.com/better-auth/better-auth.git
synced 2026-08-31 12:32:52 -05:00
Merge branch 'canary' into fix/expo-oauth-state-cookie
This commit is contained in:
@@ -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: |
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ export const APIMethod = ({
|
||||
|
||||
const serverTabContent = (
|
||||
<>
|
||||
{isClientOnly ? null : (
|
||||
{isClientOnly || isServerOnly ? null : (
|
||||
<Endpoint
|
||||
method={method || "GET"}
|
||||
path={path}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`);
|
||||
+163
-12
@@ -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
@@ -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
@@ -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],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -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
@@ -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
@@ -8,6 +8,6 @@
|
||||
"better-auth": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^7.2.4"
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,3 +1,2 @@
|
||||
export { generateId } from "@better-auth/core/utils";
|
||||
export * from "../oauth2/state";
|
||||
export * from "./hide-metadata";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,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,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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -13,6 +13,8 @@ export type {
|
||||
BetterAuthAdvancedOptions,
|
||||
BetterAuthOptions,
|
||||
BetterAuthRateLimitOptions,
|
||||
BetterAuthRateLimitRule,
|
||||
BetterAuthRateLimitStorage,
|
||||
GenerateIdFn,
|
||||
} from "./init-options";
|
||||
export type { BetterAuthPlugin, HookEndpointContext } from "./plugin";
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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";
|
||||
@@ -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
Reference in New Issue
Block a user