This is page 28 of 71. Use http://codebase.md/better-auth/better-auth?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ ├── nextjs │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app │ │ │ ├── (auth) │ │ │ │ ├── forget-password │ │ │ │ │ └── page.tsx │ │ │ │ ├── reset-password │ │ │ │ │ └── page.tsx │ │ │ │ ├── sign-in │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── two-factor │ │ │ │ ├── otp │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── accept-invitation │ │ │ │ └── [id] │ │ │ │ ├── invitation-error.tsx │ │ │ │ └── page.tsx │ │ │ ├── admin │ │ │ │ └── page.tsx │ │ │ ├── api │ │ │ │ └── auth │ │ │ │ └── [...all] │ │ │ │ └── route.ts │ │ │ ├── apps │ │ │ │ └── register │ │ │ │ └── page.tsx │ │ │ ├── client-test │ │ │ │ └── page.tsx │ │ │ ├── dashboard │ │ │ │ ├── change-plan.tsx │ │ │ │ ├── client.tsx │ │ │ │ ├── organization-card.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── upgrade-button.tsx │ │ │ │ └── user-card.tsx │ │ │ ├── device │ │ │ │ ├── approve │ │ │ │ │ └── page.tsx │ │ │ │ ├── denied │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── success │ │ │ │ └── page.tsx │ │ │ ├── favicon.ico │ │ │ ├── features.tsx │ │ │ ├── fonts │ │ │ │ ├── GeistMonoVF.woff │ │ │ │ └── GeistVF.woff │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ ├── oauth │ │ │ │ └── authorize │ │ │ │ ├── concet-buttons.tsx │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ └── pricing │ │ │ └── page.tsx │ │ ├── components │ │ │ ├── account-switch.tsx │ │ │ ├── blocks │ │ │ │ └── pricing.tsx │ │ │ ├── logo.tsx │ │ │ ├── one-tap.tsx │ │ │ ├── sign-in-btn.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── theme-provider.tsx │ │ │ ├── theme-toggle.tsx │ │ │ ├── tier-labels.tsx │ │ │ ├── ui │ │ │ │ ├── accordion.tsx │ │ │ │ ├── alert-dialog.tsx │ │ │ │ ├── alert.tsx │ │ │ │ ├── aspect-ratio.tsx │ │ │ │ ├── avatar.tsx │ │ │ │ ├── badge.tsx │ │ │ │ ├── breadcrumb.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── calendar.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── carousel.tsx │ │ │ │ ├── chart.tsx │ │ │ │ ├── checkbox.tsx │ │ │ │ ├── collapsible.tsx │ │ │ │ ├── command.tsx │ │ │ │ ├── context-menu.tsx │ │ │ │ ├── copy-button.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── drawer.tsx │ │ │ │ ├── dropdown-menu.tsx │ │ │ │ ├── form.tsx │ │ │ │ ├── hover-card.tsx │ │ │ │ ├── input-otp.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── menubar.tsx │ │ │ │ ├── navigation-menu.tsx │ │ │ │ ├── pagination.tsx │ │ │ │ ├── password-input.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── progress.tsx │ │ │ │ ├── radio-group.tsx │ │ │ │ ├── resizable.tsx │ │ │ │ ├── scroll-area.tsx │ │ │ │ ├── select.tsx │ │ │ │ ├── separator.tsx │ │ │ │ ├── sheet.tsx │ │ │ │ ├── skeleton.tsx │ │ │ │ ├── slider.tsx │ │ │ │ ├── sonner.tsx │ │ │ │ ├── switch.tsx │ │ │ │ ├── table.tsx │ │ │ │ ├── tabs.tsx │ │ │ │ ├── tabs2.tsx │ │ │ │ ├── textarea.tsx │ │ │ │ ├── toast.tsx │ │ │ │ ├── toaster.tsx │ │ │ │ ├── toggle-group.tsx │ │ │ │ ├── toggle.tsx │ │ │ │ └── tooltip.tsx │ │ │ └── wrapper.tsx │ │ ├── components.json │ │ ├── hooks │ │ │ └── use-toast.ts │ │ ├── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth-types.ts │ │ │ ├── auth.ts │ │ │ ├── email │ │ │ │ ├── invitation.tsx │ │ │ │ ├── resend.ts │ │ │ │ └── reset-password.tsx │ │ │ ├── metadata.ts │ │ │ ├── shared.ts │ │ │ └── utils.ts │ │ ├── next.config.ts │ │ ├── package.json │ │ ├── postcss.config.mjs │ │ ├── proxy.ts │ │ ├── public │ │ │ ├── __og.png │ │ │ ├── _og.png │ │ │ ├── favicon │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ ├── light │ │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ │ ├── apple-touch-icon.png │ │ │ │ │ ├── favicon-16x16.png │ │ │ │ │ ├── favicon-32x32.png │ │ │ │ │ ├── favicon.ico │ │ │ │ │ └── site.webmanifest │ │ │ │ └── site.webmanifest │ │ │ ├── logo.svg │ │ │ └── og.png │ │ ├── README.md │ │ ├── tailwind.config.ts │ │ ├── tsconfig.json │ │ └── turbo.json │ └── stateless │ ├── .env.example │ ├── .gitignore │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── src │ │ ├── app │ │ │ ├── api │ │ │ │ ├── auth │ │ │ │ │ └── [...all] │ │ │ │ │ └── route.ts │ │ │ │ └── user │ │ │ │ └── route.ts │ │ │ ├── dashboard │ │ │ │ └── page.tsx │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ └── lib │ │ ├── auth-client.ts │ │ └── auth.ts │ ├── tailwind.config.ts │ └── tsconfig.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── polar.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-declaration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── demo.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── username.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-exact-optional-property-types │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── user-additional-fields.ts │ │ │ │ │ └── username.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-isolated-module-bundler │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-verbatim-module-syntax-node10 │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── vite │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── client.ts │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr.ts │ │ ├── typecheck.spec.ts │ │ └── vite.spec.ts │ └── tsconfig.json ├── LICENSE.md ├── package.json ├── packages │ ├── better-auth │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── init.test.ts.snap │ │ │ ├── adapters │ │ │ │ ├── adapter-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── adapter-factory.test.ts.snap │ │ │ │ │ │ └── adapter-factory.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── create-test-suite.ts │ │ │ │ ├── drizzle-adapter │ │ │ │ │ ├── drizzle-adapter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── adapter.drizzle.mysql.test.ts │ │ │ │ │ ├── adapter.drizzle.pg.test.ts │ │ │ │ │ ├── adapter.drizzle.sqlite.test.ts │ │ │ │ │ └── generate-schema.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely-adapter │ │ │ │ │ ├── bun-sqlite-dialect.ts │ │ │ │ │ ├── dialect.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kysely-adapter.ts │ │ │ │ │ ├── node-sqlite-dialect.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── adapter.kysely.mssql.test.ts │ │ │ │ │ │ ├── adapter.kysely.mysql.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg-custom-schema.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg.test.ts │ │ │ │ │ │ ├── adapter.kysely.sqlite.test.ts │ │ │ │ │ │ └── node-sqlite-dialect.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── memory-adapter │ │ │ │ │ ├── adapter.memory.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── memory-adapter.ts │ │ │ │ ├── mongodb-adapter │ │ │ │ │ ├── adapter.mongo-db.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mongodb-adapter.ts │ │ │ │ ├── prisma-adapter │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── prisma-adapter.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── base.prisma │ │ │ │ │ ├── generate-auth-config.ts │ │ │ │ │ ├── generate-prisma-schema.ts │ │ │ │ │ ├── get-prisma-client.ts │ │ │ │ │ ├── prisma.mysql.test.ts │ │ │ │ │ ├── prisma.pg.test.ts │ │ │ │ │ ├── prisma.sqlite.test.ts │ │ │ │ │ └── push-prisma-schema.ts │ │ │ │ ├── test-adapter.ts │ │ │ │ ├── test.ts │ │ │ │ ├── tests │ │ │ │ │ ├── auth-flow.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── normal.ts │ │ │ │ │ ├── number-id.ts │ │ │ │ │ ├── performance.ts │ │ │ │ │ └── transactions.ts │ │ │ │ └── utils.ts │ │ │ ├── api │ │ │ │ ├── check-endpoint-conflicts.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── middlewares │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── origin-check.test.ts │ │ │ │ │ └── origin-check.ts │ │ │ │ ├── rate-limiter │ │ │ │ │ ├── index.ts │ │ │ │ │ └── rate-limiter.test.ts │ │ │ │ ├── routes │ │ │ │ │ ├── account.test.ts │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── callback.ts │ │ │ │ │ ├── email-verification.test.ts │ │ │ │ │ ├── email-verification.ts │ │ │ │ │ ├── error.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ok.ts │ │ │ │ │ ├── reset-password.test.ts │ │ │ │ │ ├── reset-password.ts │ │ │ │ │ ├── session-api.test.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── sign-in.test.ts │ │ │ │ │ ├── sign-in.ts │ │ │ │ │ ├── sign-out.test.ts │ │ │ │ │ ├── sign-out.ts │ │ │ │ │ ├── sign-up.test.ts │ │ │ │ │ ├── sign-up.ts │ │ │ │ │ ├── update-user.test.ts │ │ │ │ │ └── update-user.ts │ │ │ │ ├── to-auth-endpoints.test.ts │ │ │ │ └── to-auth-endpoints.ts │ │ │ ├── auth.test.ts │ │ │ ├── auth.ts │ │ │ ├── call.test.ts │ │ │ ├── client │ │ │ │ ├── client-ssr.test.ts │ │ │ │ ├── client.test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── fetch-plugins.ts │ │ │ │ ├── index.ts │ │ │ │ ├── lynx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── lynx-store.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── path-to-object.ts │ │ │ │ ├── plugins │ │ │ │ │ ├── index.ts │ │ │ │ │ └── infer-plugin.ts │ │ │ │ ├── proxy.ts │ │ │ │ ├── query.ts │ │ │ │ ├── react │ │ │ │ │ ├── index.ts │ │ │ │ │ └── react-store.ts │ │ │ │ ├── session-atom.ts │ │ │ │ ├── solid │ │ │ │ │ ├── index.ts │ │ │ │ │ └── solid-store.ts │ │ │ │ ├── svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── test-plugin.ts │ │ │ │ ├── types.ts │ │ │ │ ├── url.test.ts │ │ │ │ ├── vanilla.ts │ │ │ │ └── vue │ │ │ │ ├── index.ts │ │ │ │ └── vue-store.ts │ │ │ ├── cookies │ │ │ │ ├── check-cookies.ts │ │ │ │ ├── cookie-utils.ts │ │ │ │ ├── cookies.test.ts │ │ │ │ └── index.ts │ │ │ ├── crypto │ │ │ │ ├── buffer.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt.ts │ │ │ │ ├── password.test.ts │ │ │ │ ├── password.ts │ │ │ │ └── random.ts │ │ │ ├── db │ │ │ │ ├── db.test.ts │ │ │ │ ├── field.ts │ │ │ │ ├── get-migration-schema.test.ts │ │ │ │ ├── get-migration.ts │ │ │ │ ├── get-schema.ts │ │ │ │ ├── get-tables.test.ts │ │ │ │ ├── get-tables.ts │ │ │ │ ├── index.ts │ │ │ │ ├── internal-adapter.test.ts │ │ │ │ ├── internal-adapter.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── secondary-storage.test.ts │ │ │ │ ├── to-zod.ts │ │ │ │ ├── utils.ts │ │ │ │ └── with-hooks.ts │ │ │ ├── index.ts │ │ │ ├── init.test.ts │ │ │ ├── init.ts │ │ │ ├── integrations │ │ │ │ ├── next-js.ts │ │ │ │ ├── node.ts │ │ │ │ ├── react-start.ts │ │ │ │ ├── solid-start.ts │ │ │ │ └── svelte-kit.ts │ │ │ ├── oauth2 │ │ │ │ ├── index.ts │ │ │ │ ├── link-account.test.ts │ │ │ │ ├── link-account.ts │ │ │ │ ├── state.ts │ │ │ │ └── utils.ts │ │ │ ├── plugins │ │ │ │ ├── access │ │ │ │ │ ├── access.test.ts │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── additional-fields │ │ │ │ │ ├── additional-fields.test.ts │ │ │ │ │ └── client.ts │ │ │ │ ├── admin │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── admin.test.ts │ │ │ │ │ ├── admin.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── anonymous │ │ │ │ │ ├── anon.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── api-key │ │ │ │ │ ├── api-key.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── create-api-key.ts │ │ │ │ │ │ ├── delete-all-expired-api-keys.ts │ │ │ │ │ │ ├── delete-api-key.ts │ │ │ │ │ │ ├── get-api-key.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list-api-keys.ts │ │ │ │ │ │ ├── update-api-key.ts │ │ │ │ │ │ └── verify-api-key.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── bearer │ │ │ │ │ ├── bearer.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── captcha │ │ │ │ │ ├── captcha.test.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-handlers │ │ │ │ │ ├── captchafox.ts │ │ │ │ │ ├── cloudflare-turnstile.ts │ │ │ │ │ ├── google-recaptcha.ts │ │ │ │ │ ├── h-captcha.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── custom-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-session.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── device-authorization │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── device-authorization.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schema.ts │ │ │ │ ├── email-otp │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── email-otp.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── generic-oauth │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── generic-oauth.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── haveibeenpwned │ │ │ │ │ ├── haveibeenpwned.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── jwt.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── sign.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── last-login-method │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-prefix.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── last-login-method.test.ts │ │ │ │ ├── magic-link │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── magic-link.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── mcp │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mcp.test.ts │ │ │ │ ├── multi-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── multi-session.test.ts │ │ │ │ ├── oauth-proxy │ │ │ │ │ ├── index.ts │ │ │ │ │ └── oauth-proxy.test.ts │ │ │ │ ├── oidc-provider │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── oidc.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── ui.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── one-tap │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── one-time-token │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── one-time-token.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── open-api │ │ │ │ │ ├── generator.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logo.ts │ │ │ │ │ └── open-api.test.ts │ │ │ │ ├── organization │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── call.ts │ │ │ │ │ ├── client.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization-hook.test.ts │ │ │ │ │ ├── organization.test.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── permission.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── crud-access-control.test.ts │ │ │ │ │ │ ├── crud-access-control.ts │ │ │ │ │ │ ├── crud-invites.ts │ │ │ │ │ │ ├── crud-members.test.ts │ │ │ │ │ │ ├── crud-members.ts │ │ │ │ │ │ ├── crud-org.test.ts │ │ │ │ │ │ ├── crud-org.ts │ │ │ │ │ │ └── crud-team.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── team.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── passkey │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── passkey.test.ts │ │ │ │ ├── phone-number │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── phone-number-error.ts │ │ │ │ │ └── phone-number.test.ts │ │ │ │ ├── siwe │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── siwe.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── two-factor │ │ │ │ │ ├── backup-codes │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── constant.ts │ │ │ │ │ ├── error-code.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── otp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── totp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── two-factor.test.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-two-factor.ts │ │ │ │ └── username │ │ │ │ ├── client.ts │ │ │ │ ├── error-codes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── username.test.ts │ │ │ ├── social-providers │ │ │ │ └── index.ts │ │ │ ├── social.test.ts │ │ │ ├── test-utils │ │ │ │ ├── headers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── test-instance.ts │ │ │ ├── types │ │ │ │ ├── adapter.ts │ │ │ │ ├── api.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── models.ts │ │ │ │ ├── plugins.ts │ │ │ │ └── types.test.ts │ │ │ └── utils │ │ │ ├── await-object.ts │ │ │ ├── boolean.ts │ │ │ ├── clone.ts │ │ │ ├── constants.ts │ │ │ ├── date.ts │ │ │ ├── ensure-utc.ts │ │ │ ├── get-request-ip.ts │ │ │ ├── hashing.ts │ │ │ ├── hide-metadata.ts │ │ │ ├── id.ts │ │ │ ├── import-util.ts │ │ │ ├── index.ts │ │ │ ├── is-atom.ts │ │ │ ├── is-promise.ts │ │ │ ├── json.ts │ │ │ ├── merger.ts │ │ │ ├── middleware-response.ts │ │ │ ├── misc.ts │ │ │ ├── password.ts │ │ │ ├── plugin-helper.ts │ │ │ ├── shim.ts │ │ │ ├── time.ts │ │ │ ├── url.ts │ │ │ └── wildcard.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ ├── vitest.config.ts │ │ └── vitest.setup.ts │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── core │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ └── index.ts │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── endpoint-context.ts │ │ │ │ ├── index.ts │ │ │ │ └── transaction.ts │ │ │ ├── db │ │ │ │ ├── adapter │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── schema │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── verification.ts │ │ │ │ └── type.ts │ │ │ ├── env │ │ │ │ ├── color-depth.ts │ │ │ │ ├── env-impl.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.test.ts │ │ │ │ └── logger.ts │ │ │ ├── error │ │ │ │ ├── codes.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── oauth2 │ │ │ │ ├── client-credentials-token.ts │ │ │ │ ├── create-authorization-url.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-provider.ts │ │ │ │ ├── refresh-access-token.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate-authorization-code.ts │ │ │ ├── social-providers │ │ │ │ ├── apple.ts │ │ │ │ ├── atlassian.ts │ │ │ │ ├── cognito.ts │ │ │ │ ├── discord.ts │ │ │ │ ├── dropbox.ts │ │ │ │ ├── facebook.ts │ │ │ │ ├── figma.ts │ │ │ │ ├── github.ts │ │ │ │ ├── gitlab.ts │ │ │ │ ├── google.ts │ │ │ │ ├── huggingface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kakao.ts │ │ │ │ ├── kick.ts │ │ │ │ ├── line.ts │ │ │ │ ├── linear.ts │ │ │ │ ├── linkedin.ts │ │ │ │ ├── microsoft-entra-id.ts │ │ │ │ ├── naver.ts │ │ │ │ ├── notion.ts │ │ │ │ ├── paypal.ts │ │ │ │ ├── polar.ts │ │ │ │ ├── reddit.ts │ │ │ │ ├── roblox.ts │ │ │ │ ├── salesforce.ts │ │ │ │ ├── slack.ts │ │ │ │ ├── spotify.ts │ │ │ │ ├── tiktok.ts │ │ │ │ ├── twitch.ts │ │ │ │ ├── twitter.ts │ │ │ │ ├── vk.ts │ │ │ │ └── zoom.ts │ │ │ ├── types │ │ │ │ ├── context.ts │ │ │ │ ├── cookie.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init-options.ts │ │ │ │ ├── plugin-client.ts │ │ │ │ └── plugin.ts │ │ │ └── utils │ │ │ ├── error-codes.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── expo │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── client.ts │ │ │ └── index.ts │ │ ├── test │ │ │ └── expo.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── tsdown.config.ts │ ├── sso │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── oidc.test.ts │ │ │ └── saml.test.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── stripe │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── stripe.test.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── telemetry │ ├── package.json │ ├── src │ │ ├── detectors │ │ │ ├── detect-auth-config.ts │ │ │ ├── detect-database.ts │ │ │ ├── detect-framework.ts │ │ │ ├── detect-project-info.ts │ │ │ ├── detect-runtime.ts │ │ │ └── detect-system-info.ts │ │ ├── index.ts │ │ ├── project-id.ts │ │ ├── telemetry.test.ts │ │ ├── types.ts │ │ └── utils │ │ ├── hash.ts │ │ ├── id.ts │ │ ├── import-util.ts │ │ └── package-json.ts │ ├── tsconfig.json │ └── tsdown.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── SECURITY.md ├── tsconfig.base.json ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /docs/app/changelogs/_components/default-changelog.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import Link from "next/link"; 2 | import { useId } from "react"; 3 | import { cn } from "@/lib/utils"; 4 | import { IconLink } from "./changelog-layout"; 5 | import { BookIcon, GitHubIcon, XIcon } from "./icons"; 6 | import { DiscordLogoIcon } from "@radix-ui/react-icons"; 7 | import { StarField } from "./stat-field"; 8 | import { betterFetch } from "@better-fetch/fetch"; 9 | import Markdown from "react-markdown"; 10 | import defaultMdxComponents from "fumadocs-ui/mdx"; 11 | import rehypeHighlight from "rehype-highlight"; 12 | import "highlight.js/styles/dark.css"; 13 | 14 | export const dynamic = "force-static"; 15 | const ChangelogPage = async () => { 16 | const { data: releases } = await betterFetch< 17 | { 18 | id: number; 19 | tag_name: string; 20 | name: string; 21 | body: string; 22 | html_url: string; 23 | prerelease: boolean; 24 | published_at: string; 25 | }[] 26 | >("https://api.github.com/repos/better-auth/better-auth/releases"); 27 | 28 | const messages = releases 29 | ?.filter((release) => !release.prerelease) 30 | .map((release) => ({ 31 | tag: release.tag_name, 32 | title: release.name, 33 | content: getContent(release.body), 34 | date: new Date(release.published_at).toLocaleDateString("en-US", { 35 | year: "numeric", 36 | month: "short", 37 | day: "numeric", 38 | }), 39 | url: release.html_url, 40 | })); 41 | 42 | function getContent(content: string) { 43 | const lines = content.split("\n"); 44 | const newContext = lines.map((line) => { 45 | if (line.trim().startsWith("- ")) { 46 | const mainContent = line.split(";")[0]; 47 | const context = line.split(";")[2]; 48 | const mentionMatches = 49 | (context ?? line)?.match(/@([A-Za-z0-9-]+)/g) ?? []; 50 | if (mentionMatches.length === 0) { 51 | return (mainContent || line).replace(/ /g, ""); 52 | } 53 | const mentions = mentionMatches.map((match) => { 54 | const username = match.slice(1); 55 | const avatarUrl = `https://github.com/${username}.png`; 56 | return `[](https://github.com/${username})`; 57 | }); 58 | // Remove   59 | return ( 60 | (mainContent || line).replace(/ /g, "") + 61 | " – " + 62 | mentions.join(" ") 63 | ); 64 | } 65 | return line; 66 | }); 67 | return newContext.join("\n"); 68 | } 69 | 70 | return ( 71 | <div className="grid items-start md:grid-cols-2"> 72 | <div className="bg-gradient-to-tr overflow-hidden px-12 py-24 md:py-0 -mt-[100px] md:h-dvh relative md:sticky top-0 from-transparent dark:via-stone-950/5 via-stone-100/30 to-stone-200/20 dark:to-transparent/10"> 73 | <StarField className="top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" /> 74 | <Glow /> 75 | 76 | <div className="flex flex-col mx-auto max-w-xl h-full md:justify-center"> 77 | <h1 className="mt-14 font-sans text-5xl font-semibold tracking-tighter"> 78 | All of the changes made will be{" "} 79 | <span className="">available here.</span> 80 | </h1> 81 | <p className="mt-4 text-sm text-gray-600 dark:text-gray-300"> 82 | Better Auth is comprehensive authentication library for TypeScript 83 | that provides a wide range of features to make authentication easier 84 | and more secure. 85 | </p> 86 | <hr className="mt-5 h-px bg-gray-300" /> 87 | <div className="flex flex-wrap gap-x-1 gap-y-3 mt-8 text-gray-600 dark:text-gray-300 sm:gap-x-2"> 88 | <IconLink 89 | href="/docs" 90 | icon={BookIcon} 91 | className="flex-none text-gray-600 dark:text-gray-300" 92 | > 93 | Documentation 94 | </IconLink> 95 | <IconLink 96 | href="https://github.com/better-auth/better-auth" 97 | icon={GitHubIcon} 98 | className="flex-none text-gray-600 dark:text-gray-300" 99 | > 100 | GitHub 101 | </IconLink> 102 | <IconLink 103 | href="https://discord.gg/better-auth" 104 | icon={DiscordLogoIcon} 105 | className="flex-none text-gray-600 dark:text-gray-300" 106 | > 107 | Community 108 | </IconLink> 109 | </div> 110 | <p className="flex items-baseline absolute bottom-4 max-md:left-1/2 max-md:-translate-x-1/2 gap-x-2 text-[0.8125rem]/6 text-gray-500"> 111 | <IconLink href="https://x.com/better_auth" icon={XIcon} compact> 112 | BETTER-AUTH. 113 | </IconLink> 114 | </p> 115 | </div> 116 | </div> 117 | <div className="relative px-4 pb-12 md:px-8 md:py-12"> 118 | <div className="absolute top-0 left-0 mb-2 w-2 h-full -translate-x-full bg-gradient-to-b from-black/10 dark:from-white/20 from-50% to-50% to-transparent bg-[length:100%_5px] bg-repeat-y"></div> 119 | 120 | <div className="relative max-w-2xl"> 121 | <Markdown 122 | rehypePlugins={[[rehypeHighlight]]} 123 | components={{ 124 | pre: (props) => ( 125 | <defaultMdxComponents.pre 126 | {...props} 127 | className={cn(props.className, " ml-10 my-2")} 128 | /> 129 | ), 130 | h2: (props) => ( 131 | <h2 132 | id={props.children?.toString().split("date=")[0].trim()} // Extract ID dynamically 133 | className="text-2xl relative mb-6 font-bold flex-col flex justify-center tracking-tighter before:content-[''] before:block before:h-[65px] before:-mt-[10px]" 134 | {...props} 135 | > 136 | <div className="sticky top-0 left-[-9.9rem] hidden md:block"> 137 | <time className="flex gap-2 items-center text-gray-500 dark:text-white/80 text-sm md:absolute md:left-[-9.8rem] font-normal tracking-normal"> 138 | {props.children?.toString().includes("date=") && 139 | props.children?.toString().split("date=")[1]} 140 | 141 | <div className="w-4 h-[1px] dark:bg-white/60 bg-black" /> 142 | </time> 143 | </div> 144 | <Link 145 | href={ 146 | props.children 147 | ?.toString() 148 | .split("date=")[0] 149 | .trim() 150 | .endsWith(".00") 151 | ? `/changelogs/${props.children 152 | ?.toString() 153 | .split("date=")[0] 154 | .trim()}` 155 | : `#${props.children 156 | ?.toString() 157 | .split("date=")[0] 158 | .trim()}` 159 | } 160 | > 161 | {props.children?.toString().split("date=")[0].trim()} 162 | </Link> 163 | <p className="hidden text-xs font-normal opacity-60"> 164 | {props.children?.toString().includes("date=") && 165 | props.children?.toString().split("date=")[1]} 166 | </p> 167 | </h2> 168 | ), 169 | h3: (props) => ( 170 | <h3 className="py-1 text-xl tracking-tighter" {...props}> 171 | {props.children?.toString()?.trim()} 172 | <hr className="h-[1px] my-1 mb-2 bg-input" /> 173 | </h3> 174 | ), 175 | p: (props) => <p className="my-0 ml-10 text-sm" {...props} />, 176 | ul: (props) => ( 177 | <ul 178 | className="list-disc ml-10 text-[0.855rem] text-gray-600 dark:text-gray-300" 179 | {...props} 180 | /> 181 | ), 182 | li: (props) => <li className="my-1" {...props} />, 183 | a: ({ className, ...props }: any) => ( 184 | <Link 185 | target="_blank" 186 | className={cn("font-medium underline", className)} 187 | {...props} 188 | /> 189 | ), 190 | strong: (props) => ( 191 | <strong className="font-semibold" {...props} /> 192 | ), 193 | img: (props) => ( 194 | <img 195 | className="inline-block w-6 h-6 rounded-full border opacity-70" 196 | {...props} 197 | style={{ maxWidth: "100%" }} 198 | /> 199 | ), 200 | }} 201 | > 202 | {messages 203 | ?.map((message) => { 204 | return ` 205 | ## ${message.title} date=${message.date} 206 | 207 | ${message.content} 208 | `; 209 | }) 210 | .join("\n")} 211 | </Markdown> 212 | </div> 213 | </div> 214 | </div> 215 | ); 216 | }; 217 | 218 | export default ChangelogPage; 219 | 220 | export function Glow() { 221 | let id = useId(); 222 | 223 | return ( 224 | <div className="overflow-hidden absolute inset-0 bg-gradient-to-tr from-transparent -z-10 dark:via-stone-950/5 via-stone-100/30 to-stone-200/20 dark:to-transparent/10"> 225 | <svg 226 | className="absolute -bottom-48 left-[-40%] h-[80rem] w-[180%] lg:-right-40 lg:bottom-auto lg:left-auto lg:top-[-40%] lg:h-[180%] lg:w-[80rem]" 227 | aria-hidden="true" 228 | > 229 | <defs> 230 | <radialGradient id={`${id}-desktop`} cx="100%"> 231 | <stop offset="0%" stopColor="rgba(41, 37, 36, 0.4)" /> 232 | <stop offset="53.95%" stopColor="rgba(28, 25, 23, 0.09)" /> 233 | <stop offset="100%" stopColor="rgba(0, 0, 0, 0)" /> 234 | </radialGradient> 235 | <radialGradient id={`${id}-mobile`} cy="100%"> 236 | <stop offset="0%" stopColor="rgba(41, 37, 36, 0.3)" /> 237 | <stop offset="53.95%" stopColor="rgba(28, 25, 23, 0.09)" /> 238 | <stop offset="100%" stopColor="rgba(0, 0, 0, 0)" /> 239 | </radialGradient> 240 | </defs> 241 | <rect 242 | width="100%" 243 | height="100%" 244 | fill={`url(#${id}-desktop)`} 245 | className="hidden lg:block" 246 | /> 247 | <rect 248 | width="100%" 249 | height="100%" 250 | fill={`url(#${id}-mobile)`} 251 | className="lg:hidden" 252 | /> 253 | </svg> 254 | <div className="absolute inset-x-0 right-0 bottom-0 h-px mix-blend-overlay dark:bg-white/5 lg:left-auto lg:top-0 lg:h-auto lg:w-px" /> 255 | </div> 256 | ); 257 | } 258 | ``` -------------------------------------------------------------------------------- /docs/components/ui/chart.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as RechartsPrimitive from "recharts"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | // Format: { THEME_NAME: CSS_SELECTOR } 9 | const THEMES = { light: "", dark: ".dark" } as const; 10 | 11 | export type ChartConfig = { 12 | [k in string]: { 13 | label?: React.ReactNode; 14 | icon?: React.ComponentType; 15 | } & ( 16 | | { color?: string; theme?: never } 17 | | { color?: never; theme: Record<keyof typeof THEMES, string> } 18 | ); 19 | }; 20 | 21 | type ChartContextProps = { 22 | config: ChartConfig; 23 | }; 24 | 25 | const ChartContext = React.createContext<ChartContextProps | null>(null); 26 | 27 | function useChart() { 28 | const context = React.useContext(ChartContext); 29 | 30 | if (!context) { 31 | throw new Error("useChart must be used within a <ChartContainer />"); 32 | } 33 | 34 | return context; 35 | } 36 | 37 | function ChartContainer({ 38 | id, 39 | className, 40 | children, 41 | config, 42 | ...props 43 | }: React.ComponentProps<"div"> & { 44 | config: ChartConfig; 45 | children: React.ComponentProps< 46 | typeof RechartsPrimitive.ResponsiveContainer 47 | >["children"]; 48 | }) { 49 | const uniqueId = React.useId(); 50 | const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; 51 | 52 | return ( 53 | <ChartContext.Provider value={{ config }}> 54 | <div 55 | data-slot="chart" 56 | data-chart={chartId} 57 | className={cn( 58 | "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden", 59 | className, 60 | )} 61 | {...props} 62 | > 63 | <ChartStyle id={chartId} config={config} /> 64 | <RechartsPrimitive.ResponsiveContainer> 65 | {children} 66 | </RechartsPrimitive.ResponsiveContainer> 67 | </div> 68 | </ChartContext.Provider> 69 | ); 70 | } 71 | 72 | const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { 73 | const colorConfig = Object.entries(config).filter( 74 | ([, config]) => config.theme || config.color, 75 | ); 76 | 77 | if (!colorConfig.length) { 78 | return null; 79 | } 80 | 81 | return ( 82 | <style 83 | dangerouslySetInnerHTML={{ 84 | __html: Object.entries(THEMES) 85 | .map( 86 | ([theme, prefix]) => ` 87 | ${prefix} [data-chart=${id}] { 88 | ${colorConfig 89 | .map(([key, itemConfig]) => { 90 | const color = 91 | itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || 92 | itemConfig.color; 93 | return color ? ` --color-${key}: ${color};` : null; 94 | }) 95 | .join("\n")} 96 | } 97 | `, 98 | ) 99 | .join("\n"), 100 | }} 101 | /> 102 | ); 103 | }; 104 | 105 | const ChartTooltip = RechartsPrimitive.Tooltip; 106 | 107 | function ChartTooltipContent({ 108 | active, 109 | payload, 110 | className, 111 | indicator = "dot", 112 | hideLabel = false, 113 | hideIndicator = false, 114 | label, 115 | labelFormatter, 116 | labelClassName, 117 | formatter, 118 | color, 119 | nameKey, 120 | labelKey, 121 | }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> & 122 | React.ComponentProps<"div"> & { 123 | hideLabel?: boolean; 124 | hideIndicator?: boolean; 125 | indicator?: "line" | "dot" | "dashed"; 126 | nameKey?: string; 127 | labelKey?: string; 128 | }) { 129 | const { config } = useChart(); 130 | 131 | const tooltipLabel = React.useMemo(() => { 132 | if (hideLabel || !payload?.length) { 133 | return null; 134 | } 135 | 136 | const [item] = payload; 137 | const key = `${labelKey || item?.dataKey || item?.name || "value"}`; 138 | const itemConfig = getPayloadConfigFromPayload(config, item, key); 139 | const value = 140 | !labelKey && typeof label === "string" 141 | ? config[label as keyof typeof config]?.label || label 142 | : itemConfig?.label; 143 | 144 | if (labelFormatter) { 145 | return ( 146 | <div className={cn("font-medium", labelClassName)}> 147 | {labelFormatter(value, payload)} 148 | </div> 149 | ); 150 | } 151 | 152 | if (!value) { 153 | return null; 154 | } 155 | 156 | return <div className={cn("font-medium", labelClassName)}>{value}</div>; 157 | }, [ 158 | label, 159 | labelFormatter, 160 | payload, 161 | hideLabel, 162 | labelClassName, 163 | config, 164 | labelKey, 165 | ]); 166 | 167 | if (!active || !payload?.length) { 168 | return null; 169 | } 170 | 171 | const nestLabel = payload.length === 1 && indicator !== "dot"; 172 | 173 | return ( 174 | <div 175 | className={cn( 176 | "border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl", 177 | className, 178 | )} 179 | > 180 | {!nestLabel ? tooltipLabel : null} 181 | <div className="grid gap-1.5"> 182 | {payload.map((item, index) => { 183 | const key = `${nameKey || item.name || item.dataKey || "value"}`; 184 | const itemConfig = getPayloadConfigFromPayload(config, item, key); 185 | const indicatorColor = color || item.payload.fill || item.color; 186 | 187 | return ( 188 | <div 189 | key={item.dataKey} 190 | className={cn( 191 | "[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5", 192 | indicator === "dot" && "items-center", 193 | )} 194 | > 195 | {formatter && item?.value !== undefined && item.name ? ( 196 | formatter(item.value, item.name, item, index, item.payload) 197 | ) : ( 198 | <> 199 | {itemConfig?.icon ? ( 200 | <itemConfig.icon /> 201 | ) : ( 202 | !hideIndicator && ( 203 | <div 204 | className={cn( 205 | "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)", 206 | { 207 | "h-2.5 w-2.5": indicator === "dot", 208 | "w-1": indicator === "line", 209 | "w-0 border-[1.5px] border-dashed bg-transparent": 210 | indicator === "dashed", 211 | "my-0.5": nestLabel && indicator === "dashed", 212 | }, 213 | )} 214 | style={ 215 | { 216 | "--color-bg": indicatorColor, 217 | "--color-border": indicatorColor, 218 | } as React.CSSProperties 219 | } 220 | /> 221 | ) 222 | )} 223 | <div 224 | className={cn( 225 | "flex flex-1 justify-between leading-none", 226 | nestLabel ? "items-end" : "items-center", 227 | )} 228 | > 229 | <div className="grid gap-1.5"> 230 | {nestLabel ? tooltipLabel : null} 231 | <span className="text-muted-foreground"> 232 | {itemConfig?.label || item.name} 233 | </span> 234 | </div> 235 | {item.value && ( 236 | <span className="text-foreground font-mono font-medium tabular-nums"> 237 | {item.value.toLocaleString()} 238 | </span> 239 | )} 240 | </div> 241 | </> 242 | )} 243 | </div> 244 | ); 245 | })} 246 | </div> 247 | </div> 248 | ); 249 | } 250 | 251 | const ChartLegend = RechartsPrimitive.Legend; 252 | 253 | function ChartLegendContent({ 254 | className, 255 | hideIcon = false, 256 | payload, 257 | verticalAlign = "bottom", 258 | nameKey, 259 | }: React.ComponentProps<"div"> & 260 | Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { 261 | hideIcon?: boolean; 262 | nameKey?: string; 263 | }) { 264 | const { config } = useChart(); 265 | 266 | if (!payload?.length) { 267 | return null; 268 | } 269 | 270 | return ( 271 | <div 272 | className={cn( 273 | "flex items-center justify-center gap-4", 274 | verticalAlign === "top" ? "pb-3" : "pt-3", 275 | className, 276 | )} 277 | > 278 | {payload.map((item) => { 279 | const key = `${nameKey || item.dataKey || "value"}`; 280 | const itemConfig = getPayloadConfigFromPayload(config, item, key); 281 | 282 | return ( 283 | <div 284 | key={item.value} 285 | className={cn( 286 | "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3", 287 | )} 288 | > 289 | {itemConfig?.icon && !hideIcon ? ( 290 | <itemConfig.icon /> 291 | ) : ( 292 | <div 293 | className="h-2 w-2 shrink-0 rounded-[2px]" 294 | style={{ 295 | backgroundColor: item.color, 296 | }} 297 | /> 298 | )} 299 | {itemConfig?.label} 300 | </div> 301 | ); 302 | })} 303 | </div> 304 | ); 305 | } 306 | 307 | // Helper to extract item config from a payload. 308 | function getPayloadConfigFromPayload( 309 | config: ChartConfig, 310 | payload: unknown, 311 | key: string, 312 | ) { 313 | if (typeof payload !== "object" || payload === null) { 314 | return undefined; 315 | } 316 | 317 | const payloadPayload = 318 | "payload" in payload && 319 | typeof payload.payload === "object" && 320 | payload.payload !== null 321 | ? payload.payload 322 | : undefined; 323 | 324 | let configLabelKey: string = key; 325 | 326 | if ( 327 | key in payload && 328 | typeof payload[key as keyof typeof payload] === "string" 329 | ) { 330 | configLabelKey = payload[key as keyof typeof payload] as string; 331 | } else if ( 332 | payloadPayload && 333 | key in payloadPayload && 334 | typeof payloadPayload[key as keyof typeof payloadPayload] === "string" 335 | ) { 336 | configLabelKey = payloadPayload[ 337 | key as keyof typeof payloadPayload 338 | ] as string; 339 | } 340 | 341 | return configLabelKey in config 342 | ? config[configLabelKey] 343 | : config[key as keyof typeof config]; 344 | } 345 | 346 | export { 347 | ChartContainer, 348 | ChartTooltip, 349 | ChartTooltipContent, 350 | ChartLegend, 351 | ChartLegendContent, 352 | ChartStyle, 353 | }; 354 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/siwe/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { APIError } from "../../api"; 2 | import { createAuthEndpoint } from "@better-auth/core/api"; 3 | import { setSessionCookie } from "../../cookies"; 4 | import * as z from "zod"; 5 | import type { InferOptionSchema } from "../../types"; 6 | import type { BetterAuthPlugin } from "@better-auth/core"; 7 | import type { 8 | ENSLookupArgs, 9 | ENSLookupResult, 10 | SIWEVerifyMessageArgs, 11 | WalletAddress, 12 | } from "./types"; 13 | import type { User } from "../../types"; 14 | import { schema } from "./schema"; 15 | import { getOrigin } from "../../utils/url"; 16 | import { toChecksumAddress } from "../../utils/hashing"; 17 | import { mergeSchema } from "../../db/schema"; 18 | 19 | export interface SIWEPluginOptions { 20 | domain: string; 21 | emailDomainName?: string; 22 | anonymous?: boolean; 23 | getNonce: () => Promise<string>; 24 | verifyMessage: (args: SIWEVerifyMessageArgs) => Promise<boolean>; 25 | ensLookup?: (args: ENSLookupArgs) => Promise<ENSLookupResult>; 26 | schema?: InferOptionSchema<typeof schema>; 27 | } 28 | 29 | export const siwe = (options: SIWEPluginOptions) => 30 | ({ 31 | id: "siwe", 32 | schema: mergeSchema(schema, options?.schema), 33 | endpoints: { 34 | getSiweNonce: createAuthEndpoint( 35 | "/siwe/nonce", 36 | { 37 | method: "POST", 38 | body: z.object({ 39 | walletAddress: z 40 | .string() 41 | .regex(/^0[xX][a-fA-F0-9]{40}$/i) 42 | .length(42), 43 | chainId: z 44 | .number() 45 | .int() 46 | .positive() 47 | .max(2147483647) 48 | .optional() 49 | .default(1), // Default to Ethereum mainnet 50 | }), 51 | }, 52 | async (ctx) => { 53 | const { walletAddress: rawWalletAddress, chainId } = ctx.body; 54 | const walletAddress = toChecksumAddress(rawWalletAddress); 55 | const nonce = await options.getNonce(); 56 | 57 | // Store nonce with wallet address and chain ID context 58 | await ctx.context.internalAdapter.createVerificationValue({ 59 | identifier: `siwe:${walletAddress}:${chainId}`, 60 | value: nonce, 61 | expiresAt: new Date(Date.now() + 15 * 60 * 1000), 62 | }); 63 | 64 | return ctx.json({ nonce }); 65 | }, 66 | ), 67 | verifySiweMessage: createAuthEndpoint( 68 | "/siwe/verify", 69 | { 70 | method: "POST", 71 | body: z 72 | .object({ 73 | message: z.string().min(1), 74 | signature: z.string().min(1), 75 | walletAddress: z 76 | .string() 77 | .regex(/^0[xX][a-fA-F0-9]{40}$/i) 78 | .length(42), 79 | chainId: z 80 | .number() 81 | .int() 82 | .positive() 83 | .max(2147483647) 84 | .optional() 85 | .default(1), 86 | email: z.string().email().optional(), 87 | }) 88 | .refine((data) => options.anonymous !== false || !!data.email, { 89 | message: 90 | "Email is required when the anonymous plugin option is disabled.", 91 | path: ["email"], 92 | }), 93 | requireRequest: true, 94 | }, 95 | async (ctx) => { 96 | const { 97 | message, 98 | signature, 99 | walletAddress: rawWalletAddress, 100 | chainId, 101 | email, 102 | } = ctx.body; 103 | const walletAddress = toChecksumAddress(rawWalletAddress); 104 | const isAnon = options.anonymous ?? true; 105 | 106 | if (!isAnon && !email) { 107 | throw new APIError("BAD_REQUEST", { 108 | message: "Email is required when anonymous is disabled.", 109 | status: 400, 110 | }); 111 | } 112 | 113 | try { 114 | // Find stored nonce with wallet address and chain ID context 115 | const verification = 116 | await ctx.context.internalAdapter.findVerificationValue( 117 | `siwe:${walletAddress}:${chainId}`, 118 | ); 119 | 120 | // Ensure nonce is valid and not expired 121 | if (!verification || new Date() > verification.expiresAt) { 122 | throw new APIError("UNAUTHORIZED", { 123 | message: "Unauthorized: Invalid or expired nonce", 124 | status: 401, 125 | code: "UNAUTHORIZED_INVALID_OR_EXPIRED_NONCE", 126 | }); 127 | } 128 | 129 | // Verify SIWE message with enhanced parameters 130 | const { value: nonce } = verification; 131 | const verified = await options.verifyMessage({ 132 | message, 133 | signature, 134 | address: walletAddress, 135 | chainId, 136 | cacao: { 137 | h: { t: "caip122" }, 138 | p: { 139 | domain: options.domain, 140 | aud: options.domain, 141 | nonce, 142 | iss: options.domain, 143 | version: "1", 144 | }, 145 | s: { t: "eip191", s: signature }, 146 | }, 147 | }); 148 | 149 | if (!verified) { 150 | throw new APIError("UNAUTHORIZED", { 151 | message: "Unauthorized: Invalid SIWE signature", 152 | status: 401, 153 | }); 154 | } 155 | 156 | // Clean up used nonce 157 | await ctx.context.internalAdapter.deleteVerificationValue( 158 | verification.id, 159 | ); 160 | 161 | // Look for existing user by their wallet addresses 162 | let user: User | null = null; 163 | 164 | // Check if there's a wallet address record for this exact address+chainId combination 165 | const existingWalletAddress: WalletAddress | null = 166 | await ctx.context.adapter.findOne({ 167 | model: "walletAddress", 168 | where: [ 169 | { field: "address", operator: "eq", value: walletAddress }, 170 | { field: "chainId", operator: "eq", value: chainId }, 171 | ], 172 | }); 173 | 174 | if (existingWalletAddress) { 175 | // Get the user associated with this wallet address 176 | user = await ctx.context.adapter.findOne({ 177 | model: "user", 178 | where: [ 179 | { 180 | field: "id", 181 | operator: "eq", 182 | value: existingWalletAddress.userId, 183 | }, 184 | ], 185 | }); 186 | } else { 187 | // No exact match found, check if this address exists on any other chain 188 | const anyWalletAddress: WalletAddress | null = 189 | await ctx.context.adapter.findOne({ 190 | model: "walletAddress", 191 | where: [ 192 | { field: "address", operator: "eq", value: walletAddress }, 193 | ], 194 | }); 195 | 196 | if (anyWalletAddress) { 197 | // Same address exists on different chain, get that user 198 | user = await ctx.context.adapter.findOne({ 199 | model: "user", 200 | where: [ 201 | { 202 | field: "id", 203 | operator: "eq", 204 | value: anyWalletAddress.userId, 205 | }, 206 | ], 207 | }); 208 | } 209 | } 210 | 211 | // Create new user if none exists 212 | if (!user) { 213 | const domain = 214 | options.emailDomainName ?? getOrigin(ctx.context.baseURL); 215 | // Use checksummed address for email generation 216 | const userEmail = 217 | !isAnon && email ? email : `${walletAddress}@${domain}`; 218 | const { name, avatar } = 219 | (await options.ensLookup?.({ walletAddress })) ?? {}; 220 | 221 | user = await ctx.context.internalAdapter.createUser({ 222 | name: name ?? walletAddress, 223 | email: userEmail, 224 | image: avatar ?? "", 225 | }); 226 | 227 | // Create wallet address record 228 | await ctx.context.adapter.create({ 229 | model: "walletAddress", 230 | data: { 231 | userId: user.id, 232 | address: walletAddress, 233 | chainId, 234 | isPrimary: true, // First address is primary 235 | createdAt: new Date(), 236 | }, 237 | }); 238 | 239 | // Create account record for wallet authentication 240 | await ctx.context.internalAdapter.createAccount({ 241 | userId: user.id, 242 | providerId: "siwe", 243 | accountId: `${walletAddress}:${chainId}`, 244 | createdAt: new Date(), 245 | updatedAt: new Date(), 246 | }); 247 | } else { 248 | // User exists, but check if this specific address/chain combo exists 249 | if (!existingWalletAddress) { 250 | // Add this new chainId to existing user's addresses 251 | await ctx.context.adapter.create({ 252 | model: "walletAddress", 253 | data: { 254 | userId: user.id, 255 | address: walletAddress, 256 | chainId, 257 | isPrimary: false, // Additional addresses are not primary by default 258 | createdAt: new Date(), 259 | }, 260 | }); 261 | 262 | // Create account record for this new wallet+chain combination 263 | await ctx.context.internalAdapter.createAccount({ 264 | userId: user.id, 265 | providerId: "siwe", 266 | accountId: `${walletAddress}:${chainId}`, 267 | createdAt: new Date(), 268 | updatedAt: new Date(), 269 | }); 270 | } 271 | } 272 | 273 | const session = await ctx.context.internalAdapter.createSession( 274 | user.id, 275 | ); 276 | 277 | if (!session) { 278 | throw new APIError("INTERNAL_SERVER_ERROR", { 279 | message: "Internal Server Error", 280 | status: 500, 281 | }); 282 | } 283 | 284 | await setSessionCookie(ctx, { session, user }); 285 | 286 | return ctx.json({ 287 | token: session.token, 288 | success: true, 289 | user: { 290 | id: user.id, 291 | walletAddress, 292 | chainId, 293 | }, 294 | }); 295 | } catch (error: unknown) { 296 | if (error instanceof APIError) throw error; 297 | throw new APIError("UNAUTHORIZED", { 298 | message: "Something went wrong. Please try again later.", 299 | error: error instanceof Error ? error.message : "Unknown error", 300 | status: 401, 301 | }); 302 | } 303 | }, 304 | ), 305 | }, 306 | }) satisfies BetterAuthPlugin; 307 | ``` -------------------------------------------------------------------------------- /docs/content/docs/plugins/generic-oauth.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Generic OAuth 3 | description: Authenticate users with any OAuth provider 4 | --- 5 | 6 | The Generic OAuth plugin provides a flexible way to integrate authentication with any OAuth provider. It supports both OAuth 2.0 and OpenID Connect (OIDC) flows, allowing you to easily add social login or custom OAuth authentication to your application. 7 | 8 | ## Installation 9 | 10 | <Steps> 11 | <Step> 12 | ### Add the plugin to your auth config 13 | 14 | To use the Generic OAuth plugin, add it to your auth config. 15 | 16 | ```ts title="auth.ts" 17 | import { betterAuth } from "better-auth" 18 | import { genericOAuth } from "better-auth/plugins" // [!code highlight] 19 | 20 | export const auth = betterAuth({ 21 | // ... other config options 22 | plugins: [ 23 | genericOAuth({ // [!code highlight] 24 | config: [ // [!code highlight] 25 | { // [!code highlight] 26 | providerId: "provider-id", // [!code highlight] 27 | clientId: "test-client-id", // [!code highlight] 28 | clientSecret: "test-client-secret", // [!code highlight] 29 | discoveryUrl: "https://auth.example.com/.well-known/openid-configuration", // [!code highlight] 30 | // ... other config options // [!code highlight] 31 | }, // [!code highlight] 32 | // Add more providers as needed // [!code highlight] 33 | ] // [!code highlight] 34 | }) // [!code highlight] 35 | ] 36 | }) 37 | ``` 38 | </Step> 39 | 40 | <Step> 41 | ### Add the client plugin 42 | 43 | Include the Generic OAuth client plugin in your authentication client instance. 44 | 45 | ```ts title="auth-client.ts" 46 | import { createAuthClient } from "better-auth/client" 47 | import { genericOAuthClient } from "better-auth/client/plugins" 48 | 49 | export const authClient = createAuthClient({ 50 | plugins: [ 51 | genericOAuthClient() 52 | ] 53 | }) 54 | ``` 55 | </Step> 56 | </Steps> 57 | 58 | ## Usage 59 | 60 | The Generic OAuth plugin provides endpoints for initiating the OAuth flow and handling the callback. Here's how to use them: 61 | 62 | ### Initiate OAuth Sign-In 63 | 64 | To start the OAuth sign-in process: 65 | 66 | <APIMethod path="/sign-in/oauth2" method="POST"> 67 | ```ts 68 | type signInWithOAuth2 = { 69 | /** 70 | * The provider ID for the OAuth provider. 71 | */ 72 | providerId: string = "provider-id" 73 | /** 74 | * The URL to redirect to after sign in. 75 | */ 76 | callbackURL?: string = "/dashboard" 77 | /** 78 | * The URL to redirect to if an error occurs. 79 | */ 80 | errorCallbackURL?: string = "/error-page" 81 | /** 82 | * The URL to redirect to after login if the user is new. 83 | */ 84 | newUserCallbackURL?: string = "/welcome" 85 | /** 86 | * Disable redirect. 87 | */ 88 | disableRedirect?: boolean = false 89 | /** 90 | * Scopes to be passed to the provider authorization request. 91 | */ 92 | scopes?: string[] = ["my-scope"] 93 | /** 94 | * Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider. 95 | */ 96 | requestSignUp?: boolean = false 97 | } 98 | ``` 99 | </APIMethod> 100 | 101 | ### Linking OAuth Accounts 102 | 103 | To link an OAuth account to an existing user: 104 | 105 | <APIMethod 106 | path="/oauth2/link" 107 | method="POST" 108 | requireSession 109 | > 110 | ```ts 111 | type oAuth2LinkAccount = { 112 | /** 113 | * The OAuth provider ID. 114 | */ 115 | providerId: string = "my-provider-id" 116 | /** 117 | * The URL to redirect to once the account linking was complete. 118 | */ 119 | callbackURL: string = "/successful-link" 120 | } 121 | ``` 122 | </APIMethod> 123 | 124 | ### Handle OAuth Callback 125 | 126 | The plugin mounts a route to handle the OAuth callback `/oauth2/callback/:providerId`. This means by default `${baseURL}/api/auth/oauth2/callback/:providerId` will be used as the callback URL. Make sure your OAuth provider is configured to use this URL. 127 | 128 | ## Configuration 129 | 130 | When adding the plugin to your auth config, you can configure multiple OAuth providers. Each provider configuration object supports the following options: 131 | 132 | ```ts 133 | interface GenericOAuthConfig { 134 | providerId: string; 135 | discoveryUrl?: string; 136 | authorizationUrl?: string; 137 | tokenUrl?: string; 138 | userInfoUrl?: string; 139 | clientId: string; 140 | clientSecret: string; 141 | scopes?: string[]; 142 | redirectURI?: string; 143 | responseType?: string; 144 | prompt?: string; 145 | pkce?: boolean; 146 | accessType?: string; 147 | getUserInfo?: (tokens: OAuth2Tokens) => Promise<User | null>; 148 | } 149 | ``` 150 | 151 | ### Other Provider Configurations 152 | 153 | **providerId**: A unique string to identify the OAuth provider configuration. 154 | 155 | **discoveryUrl**: (Optional) URL to fetch the provider's OAuth 2.0/OIDC configuration. If provided, endpoints like `authorizationUrl`, `tokenUrl`, and `userInfoUrl` can be auto-discovered. 156 | 157 | **authorizationUrl**: (Optional) The OAuth provider's authorization endpoint. Not required if using `discoveryUrl`. 158 | 159 | **tokenUrl**: (Optional) The OAuth provider's token endpoint. Not required if using `discoveryUrl`. 160 | 161 | **userInfoUrl**: (Optional) The endpoint to fetch user profile information. Not required if using `discoveryUrl`. 162 | 163 | **clientId**: The OAuth client ID issued by your provider. 164 | 165 | **clientSecret**: The OAuth client secret issued by your provider. 166 | 167 | **scopes**: (Optional) An array of scopes to request from the provider (e.g., `["openid", "email", "profile"]`). 168 | 169 | **redirectURI**: (Optional) The redirect URI to use for the OAuth flow. If not set, a default is constructed based on your app's base URL. 170 | 171 | **responseType**: (Optional) The OAuth response type. Defaults to `"code"` for authorization code flow. 172 | 173 | **responseMode**: (Optional) The response mode for the authorization code request, such as `"query"` or `"form_post"`. 174 | 175 | **prompt**: (Optional) Controls the authentication experience (e.g., force login, consent, etc.). 176 | 177 | **pkce**: (Optional) If true, enables PKCE (Proof Key for Code Exchange) for enhanced security. Defaults to `false`. 178 | 179 | **accessType**: (Optional) The access type for the authorization request. Use `"offline"` to request a refresh token. 180 | 181 | **getUserInfo**: (Optional) A custom function to fetch user info from the provider, given the OAuth tokens. If not provided, a default fetch is used. 182 | 183 | **mapProfileToUser**: (Optional) A function to map the provider's user profile to your app's user object. Useful for custom field mapping or transformations. 184 | 185 | **authorizationUrlParams**: (Optional) Additional query parameters to add to the authorization URL. These can override default parameters. You can also provide a function that returns the parameters. 186 | 187 | **tokenUrlParams**: (Optional) Additional query parameters to add to the token URL. These can override default parameters. You can also provide a function that returns the parameters. 188 | 189 | **disableImplicitSignUp**: (Optional) If true, disables automatic sign-up for new users. Sign-in must be explicitly requested with sign-up intent. 190 | 191 | **disableSignUp**: (Optional) If true, disables sign-up for new users entirely. Only existing users can sign in. 192 | 193 | **authentication**: (Optional) The authentication method for token requests. Can be `'basic'` or `'post'`. Defaults to `'post'`. 194 | 195 | **discoveryHeaders**: (Optional) Custom headers to include in the discovery request. Useful for providers that require special headers. 196 | 197 | **authorizationHeaders**: (Optional) Custom headers to include in the authorization request. Useful for providers that require special headers. 198 | 199 | **overrideUserInfo**: (Optional) If true, the user's info in your database will be updated with the provider's info every time they sign in. Defaults to `false`. 200 | 201 | ## Advanced Usage 202 | 203 | ### Custom User Info Fetching 204 | 205 | You can provide a custom `getUserInfo` function to handle specific provider requirements: 206 | 207 | ```ts 208 | genericOAuth({ 209 | config: [ 210 | { 211 | providerId: "custom-provider", 212 | // ... other config options 213 | getUserInfo: async (tokens) => { 214 | // Custom logic to fetch and return user info 215 | const userInfo = await fetchUserInfoFromCustomProvider(tokens); 216 | return { 217 | id: userInfo.sub, 218 | email: userInfo.email, 219 | name: userInfo.name, 220 | // ... map other fields as needed 221 | }; 222 | } 223 | } 224 | ] 225 | }) 226 | ``` 227 | 228 | ### Map User Info Fields 229 | 230 | If the user info returned by the provider does not match the expected format, or you need to map additional fields, you can use the `mapProfileToUser`: 231 | 232 | ```ts 233 | genericOAuth({ 234 | config: [ 235 | { 236 | providerId: "custom-provider", 237 | // ... other config options 238 | mapProfileToUser: async (profile) => { 239 | return { 240 | firstName: profile.given_name, 241 | // ... map other fields as needed 242 | }; 243 | } 244 | } 245 | ] 246 | }) 247 | ``` 248 | 249 | ### Error Handling 250 | 251 | The plugin includes built-in error handling for common OAuth issues. Errors are typically redirected to your application's error page with an appropriate error message in the URL parameters. If the callback URL is not provided, the user will be redirected to Better Auth's default error page. 252 | 253 | ``` -------------------------------------------------------------------------------- /docs/content/docs/plugins/siwe.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Sign In With Ethereum (SIWE) 3 | description: Sign in with Ethereum plugin for Better Auth 4 | --- 5 | 6 | The Sign in with Ethereum (SIWE) plugin allows users to authenticate using their Ethereum wallets following the [ERC-4361 standard](https://eips.ethereum.org/EIPS/eip-4361). This plugin provides flexibility by allowing you to implement your own message verification and nonce generation logic. 7 | 8 | ## Installation 9 | 10 | <Steps> 11 | <Step> 12 | ### Add the Server Plugin 13 | 14 | Add the SIWE plugin to your auth configuration: 15 | 16 | ```ts title="auth.ts" 17 | import { betterAuth } from "better-auth"; 18 | import { siwe } from "better-auth/plugins"; 19 | 20 | export const auth = betterAuth({ 21 | plugins: [ 22 | siwe({ 23 | domain: "example.com", 24 | emailDomainName: "example.com", // optional 25 | anonymous: false, // optional, default is true 26 | getNonce: async () => { 27 | // Implement your nonce generation logic here 28 | return "your-secure-random-nonce"; 29 | }, 30 | verifyMessage: async (args) => { 31 | // Implement your SIWE message verification logic here 32 | // This should verify the signature against the message 33 | return true; // return true if signature is valid 34 | }, 35 | ensLookup: async (args) => { 36 | // Optional: Implement ENS lookup for user names and avatars 37 | return { 38 | name: "user.eth", 39 | avatar: "https://example.com/avatar.png" 40 | }; 41 | }, 42 | }), 43 | ], 44 | }); 45 | ``` 46 | </Step> 47 | 48 | <Step> 49 | ### Migrate the database 50 | 51 | Run the migration or generate the schema to add the necessary fields and tables to the database. 52 | 53 | <Tabs items={["migrate", "generate"]}> 54 | <Tab value="migrate"> 55 | ```bash 56 | npx @better-auth/cli migrate 57 | ``` 58 | </Tab> 59 | <Tab value="generate"> 60 | ```bash 61 | npx @better-auth/cli generate 62 | ``` 63 | </Tab> 64 | </Tabs> 65 | See the [Schema](#schema) section to add the fields manually. 66 | </Step> 67 | 68 | <Step> 69 | ### Add the Client Plugin 70 | 71 | ```ts title="auth-client.ts" 72 | import { createAuthClient } from "better-auth/client"; 73 | import { siweClient } from "better-auth/client/plugins"; 74 | 75 | export const authClient = createAuthClient({ 76 | plugins: [siweClient()], 77 | }); 78 | ``` 79 | </Step> 80 | 81 | </Steps> 82 | 83 | ## Usage 84 | 85 | ### Generate a Nonce 86 | 87 | Before signing a SIWE message, you need to generate a nonce for the wallet address: 88 | 89 | ```ts title="generate-nonce.ts" 90 | const { data, error } = await authClient.siwe.nonce({ 91 | walletAddress: "0x1234567890abcdef1234567890abcdef12345678", 92 | chainId: 1, // optional for Ethereum mainnet, required for other chains. Defaults to 1 93 | }); 94 | 95 | if (data) { 96 | console.log("Nonce:", data.nonce); 97 | } 98 | ``` 99 | 100 | ### Sign In with Ethereum 101 | 102 | After generating a nonce and creating a SIWE message, verify the signature to authenticate: 103 | 104 | ```ts title="sign-in-siwe.ts" 105 | const { data, error } = await authClient.siwe.verify({ 106 | message: "Your SIWE message string", 107 | signature: "0x...", // The signature from the user's wallet 108 | walletAddress: "0x1234567890abcdef1234567890abcdef12345678", 109 | chainId: 1, // optional for Ethereum mainnet, required for other chains. Must match Chain ID in SIWE message 110 | email: "[email protected]", // optional, required if anonymous is false 111 | }); 112 | 113 | if (data) { 114 | console.log("Authentication successful:", data.user); 115 | } 116 | ``` 117 | 118 | ### Chain-Specific Examples 119 | 120 | Here are examples for different blockchain networks: 121 | 122 | ```ts title="ethereum-mainnet.ts" 123 | // Ethereum Mainnet (chainId can be omitted, defaults to 1) 124 | const { data, error } = await authClient.siwe.verify({ 125 | message, 126 | signature, 127 | walletAddress, 128 | // chainId: 1 (default) 129 | }); 130 | ``` 131 | 132 | ```ts title="polygon.ts" 133 | // Polygon (chainId REQUIRED) 134 | const { data, error } = await authClient.siwe.verify({ 135 | message, 136 | signature, 137 | walletAddress, 138 | chainId: 137, // Required for Polygon 139 | }); 140 | ``` 141 | 142 | ```ts title="arbitrum.ts" 143 | // Arbitrum (chainId REQUIRED) 144 | const { data, error } = await authClient.siwe.verify({ 145 | message, 146 | signature, 147 | walletAddress, 148 | chainId: 42161, // Required for Arbitrum 149 | }); 150 | ``` 151 | 152 | ```ts title="base.ts" 153 | // Base (chainId REQUIRED) 154 | const { data, error } = await authClient.siwe.verify({ 155 | message, 156 | signature, 157 | walletAddress, 158 | chainId: 8453, // Required for Base 159 | }); 160 | ``` 161 | 162 | <Callout type="warning"> 163 | The `chainId` must match the Chain ID specified in your SIWE message. Verification will fail with a 401 error if there's a mismatch between the message's Chain ID and the `chainId` parameter. 164 | </Callout> 165 | 166 | ## Configuration Options 167 | 168 | ### Server Options 169 | 170 | The SIWE plugin accepts the following configuration options: 171 | 172 | - **domain**: The domain name of your application (required for SIWE message generation) 173 | - **emailDomainName**: The email domain name for creating user accounts when not using anonymous mode. Defaults to the domain from your base URL 174 | - **anonymous**: Whether to allow anonymous sign-ins without requiring an email. Default is `true` 175 | - **getNonce**: Function to generate a unique nonce for each sign-in attempt. You must implement this function to return a cryptographically secure random string. Must return a `Promise<string>` 176 | - **verifyMessage**: Function to verify the signed SIWE message. Receives message details and should return `Promise<boolean>` 177 | - **ensLookup**: Optional function to lookup ENS names and avatars for Ethereum addresses 178 | 179 | ### Client Options 180 | 181 | The SIWE client plugin doesn't require any configuration options, but you can pass them if needed for future extensibility: 182 | 183 | ```ts title="auth-client.ts" 184 | import { createAuthClient } from "better-auth/client"; 185 | import { siweClient } from "better-auth/client/plugins"; 186 | 187 | export const authClient = createAuthClient({ 188 | plugins: [ 189 | siweClient({ 190 | // Optional client configuration can go here 191 | }), 192 | ], 193 | }); 194 | ``` 195 | 196 | ## Schema 197 | 198 | The SIWE plugin adds a `walletAddress` table to store user wallet associations: 199 | 200 | | Field | Type | Description | 201 | | --------- | ------- | ----------------------------------------- | 202 | | id | string | Primary key | 203 | | userId | string | Reference to user.id | 204 | | address | string | Ethereum wallet address | 205 | | chainId | number | Chain ID (e.g., 1 for Ethereum mainnet) | 206 | | isPrimary | boolean | Whether this is the user's primary wallet | 207 | | createdAt | date | Creation timestamp | 208 | 209 | ## Example Implementation 210 | 211 | Here's a complete example showing how to implement SIWE authentication: 212 | 213 | ```ts title="auth.ts" 214 | import { betterAuth } from "better-auth"; 215 | import { siwe } from "better-auth/plugins"; 216 | import { generateRandomString } from "better-auth/crypto"; 217 | import { verifyMessage, createPublicClient, http } from "viem"; 218 | import { mainnet } from "viem/chains"; 219 | 220 | export const auth = betterAuth({ 221 | database: { 222 | // your database configuration 223 | }, 224 | plugins: [ 225 | siwe({ 226 | domain: "myapp.com", 227 | emailDomainName: "myapp.com", 228 | anonymous: false, 229 | getNonce: async () => { 230 | // Generate a cryptographically secure random nonce 231 | return generateRandomString(32); 232 | }, 233 | verifyMessage: async ({ message, signature, address }) => { 234 | try { 235 | // Verify the signature using viem (recommended) 236 | const isValid = await verifyMessage({ 237 | address: address as `0x${string}`, 238 | message, 239 | signature: signature as `0x${string}`, 240 | }); 241 | return isValid; 242 | } catch (error) { 243 | console.error("SIWE verification failed:", error); 244 | return false; 245 | } 246 | }, 247 | ensLookup: async ({ walletAddress }) => { 248 | try { 249 | // Optional: lookup ENS name and avatar using viem 250 | // You can use viem's ENS utilities here 251 | const client = createPublicClient({ 252 | chain: mainnet, 253 | transport: http(), 254 | }); 255 | 256 | const ensName = await client.getEnsName({ 257 | address: walletAddress as `0x${string}`, 258 | }); 259 | 260 | const ensAvatar = ensName 261 | ? await client.getEnsAvatar({ 262 | name: ensName, 263 | }) 264 | : null; 265 | 266 | return { 267 | name: ensName || walletAddress, 268 | avatar: ensAvatar || "", 269 | }; 270 | } catch { 271 | return { 272 | name: walletAddress, 273 | avatar: "", 274 | }; 275 | } 276 | }, 277 | }), 278 | ], 279 | }); 280 | ``` 281 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/adapters/kysely-adapter/test/adapter.kysely.mssql.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Kysely, MssqlDialect } from "kysely"; 2 | import { testAdapter } from "../../test-adapter"; 3 | import { kyselyAdapter } from "../kysely-adapter"; 4 | import { 5 | authFlowTestSuite, 6 | normalTestSuite, 7 | numberIdTestSuite, 8 | performanceTestSuite, 9 | transactionsTestSuite, 10 | } from "../../tests"; 11 | import { getMigrations } from "../../../db"; 12 | import * as Tedious from "tedious"; 13 | import * as Tarn from "tarn"; 14 | import type { BetterAuthOptions } from "@better-auth/core"; 15 | 16 | // We are not allowed to handle the mssql connection 17 | // we must let kysely handle it. This is because if kysely is already 18 | // handling it, and we were to connect it ourselves, it will create bugs. 19 | const dialect = new MssqlDialect({ 20 | tarn: { 21 | ...Tarn, 22 | options: { 23 | min: 0, 24 | max: 10, 25 | }, 26 | }, 27 | tedious: { 28 | ...Tedious, 29 | connectionFactory: () => 30 | new Tedious.Connection({ 31 | authentication: { 32 | options: { 33 | password: "Password123!", 34 | userName: "sa", 35 | }, 36 | type: "default", 37 | }, 38 | options: { 39 | database: "master", // Start with master database, will create better_auth if needed 40 | port: 1433, 41 | trustServerCertificate: true, 42 | encrypt: false, 43 | }, 44 | server: "localhost", 45 | }), 46 | TYPES: { 47 | ...Tedious.TYPES, 48 | DateTime: Tedious.TYPES.DateTime2, 49 | }, 50 | }, 51 | }); 52 | 53 | const kyselyDB = new Kysely({ 54 | dialect: dialect, 55 | }); 56 | 57 | // Create better_auth database if it doesn't exist 58 | const ensureDatabaseExists = async () => { 59 | try { 60 | console.log("Ensuring better_auth database exists..."); 61 | await kyselyDB.getExecutor().executeQuery({ 62 | sql: ` 63 | IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = 'better_auth') 64 | BEGIN 65 | CREATE DATABASE better_auth; 66 | PRINT 'Database better_auth created successfully'; 67 | END 68 | ELSE 69 | BEGIN 70 | PRINT 'Database better_auth already exists'; 71 | END 72 | `, 73 | parameters: [], 74 | query: { kind: "SelectQueryNode" }, 75 | queryId: { queryId: "ensure-db" }, 76 | }); 77 | console.log("Database check/creation completed"); 78 | } catch (error) { 79 | console.error("Failed to ensure database exists:", error); 80 | throw error; 81 | } 82 | }; 83 | 84 | // Warm up connection for CI environments 85 | const warmupConnection = async () => { 86 | const isCI = 87 | process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true"; 88 | if (isCI) { 89 | console.log("Warming up MSSQL connection for CI environment..."); 90 | console.log( 91 | `Environment: CI=${process.env.CI}, GITHUB_ACTIONS=${process.env.GITHUB_ACTIONS}`, 92 | ); 93 | 94 | try { 95 | await ensureDatabaseExists(); 96 | 97 | // Try a simple query to establish the connection 98 | await kyselyDB.getExecutor().executeQuery({ 99 | sql: "SELECT 1 as warmup, @@VERSION as version", 100 | parameters: [], 101 | query: { kind: "SelectQueryNode" }, 102 | queryId: { queryId: "warmup" }, 103 | }); 104 | console.log("Connection warmup successful"); 105 | } catch (error) { 106 | console.warn( 107 | "Connection warmup failed, will retry during validation:", 108 | error, 109 | ); 110 | // Log additional debugging info for CI 111 | if (isCI) { 112 | console.log("CI Debug Info:"); 113 | console.log("- MSSQL server may not be ready yet"); 114 | console.log("- Network connectivity issues possible"); 115 | console.log("- Database may not exist yet"); 116 | } 117 | } 118 | } else { 119 | // For local development, also ensure database exists 120 | await ensureDatabaseExists(); 121 | } 122 | }; 123 | 124 | // Add connection validation helper with CI-specific handling 125 | const validateConnection = async (retries: number = 10): Promise<boolean> => { 126 | const isCI = 127 | process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true"; 128 | const maxRetries = isCI ? 15 : retries; // More retries in CI 129 | const baseDelay = isCI ? 2000 : 1000; // Longer delays in CI 130 | 131 | console.log( 132 | `Validating connection (CI: ${isCI}, max retries: ${maxRetries})`, 133 | ); 134 | 135 | for (let i = 0; i < maxRetries; i++) { 136 | try { 137 | await query("SELECT 1 as test", isCI ? 10000 : 5000); 138 | console.log("Connection validated successfully"); 139 | return true; 140 | } catch (error) { 141 | console.warn( 142 | `Connection validation attempt ${i + 1}/${maxRetries} failed:`, 143 | error, 144 | ); 145 | if (i === maxRetries - 1) { 146 | console.error("All connection validation attempts failed"); 147 | return false; 148 | } 149 | // Exponential backoff with longer delays in CI 150 | const delay = baseDelay * Math.pow(1.5, i); 151 | console.log(`Waiting ${delay}ms before retry...`); 152 | await new Promise((resolve) => setTimeout(resolve, delay)); 153 | } 154 | } 155 | return false; 156 | }; 157 | 158 | const query = async (sql: string, timeoutMs: number = 30000) => { 159 | const isCI = 160 | process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true"; 161 | const actualTimeout = isCI ? Math.max(timeoutMs, 60000) : timeoutMs; // Minimum 60s timeout in CI 162 | 163 | try { 164 | console.log( 165 | `Executing SQL: ${sql.substring(0, 100)}... (timeout: ${actualTimeout}ms, CI: ${isCI})`, 166 | ); 167 | 168 | // Ensure we're using the better_auth database for queries 169 | const sqlWithContext = sql.includes("USE ") 170 | ? sql 171 | : `USE better_auth; ${sql}`; 172 | 173 | const result = (await Promise.race([ 174 | kyselyDB.getExecutor().executeQuery({ 175 | sql: sqlWithContext, 176 | parameters: [], 177 | query: { kind: "SelectQueryNode" }, 178 | queryId: { queryId: "" }, 179 | }), 180 | new Promise((_, reject) => 181 | setTimeout( 182 | () => reject(new Error(`Query timeout after ${actualTimeout}ms`)), 183 | actualTimeout, 184 | ), 185 | ), 186 | ])) as any; 187 | console.log(`Query completed successfully`); 188 | return { rows: result.rows, rowCount: result.rows.length }; 189 | } catch (error) { 190 | console.error(`Query failed: ${error}`); 191 | throw error; 192 | } 193 | }; 194 | 195 | const showDB = async () => { 196 | const DB = { 197 | users: await query("SELECT * FROM [user]"), 198 | sessions: await query("SELECT * FROM [session]"), 199 | accounts: await query("SELECT * FROM [account]"), 200 | verifications: await query("SELECT * FROM [verification]"), 201 | }; 202 | console.log(`DB`, DB); 203 | }; 204 | 205 | const resetDB = async (retryCount: number = 0) => { 206 | const isCI = 207 | process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true"; 208 | const maxRetries = isCI ? 3 : 1; // Allow retries in CI 209 | 210 | try { 211 | console.log( 212 | `Starting database reset... (attempt ${retryCount + 1}/${maxRetries + 1})`, 213 | ); 214 | 215 | // Warm up connection first (especially important for CI) 216 | await warmupConnection(); 217 | 218 | const isConnected = await validateConnection(); 219 | if (!isConnected) { 220 | throw new Error("Database connection validation failed"); 221 | } 222 | 223 | // First, try to disable foreign key checks and drop constraints 224 | await query( 225 | ` 226 | -- Disable all foreign key constraints 227 | EXEC sp_MSforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT all"; 228 | `, 229 | 15000, 230 | ); 231 | 232 | // Drop foreign key constraints 233 | await query( 234 | ` 235 | DECLARE @sql NVARCHAR(MAX) = ''; 236 | SELECT @sql = @sql + 'ALTER TABLE [' + TABLE_SCHEMA + '].[' + TABLE_NAME + '] DROP CONSTRAINT [' + CONSTRAINT_NAME + '];' + CHAR(13) 237 | FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS 238 | WHERE CONSTRAINT_TYPE = 'FOREIGN KEY' 239 | AND TABLE_CATALOG = DB_NAME(); 240 | IF LEN(@sql) > 0 241 | EXEC sp_executesql @sql; 242 | `, 243 | 15000, 244 | ); 245 | 246 | // Then drop all tables 247 | await query( 248 | ` 249 | DECLARE @sql NVARCHAR(MAX) = ''; 250 | SELECT @sql = @sql + 'DROP TABLE [' + TABLE_NAME + '];' + CHAR(13) 251 | FROM INFORMATION_SCHEMA.TABLES 252 | WHERE TABLE_TYPE = 'BASE TABLE' 253 | AND TABLE_CATALOG = DB_NAME() 254 | AND TABLE_SCHEMA = 'dbo'; 255 | IF LEN(@sql) > 0 256 | EXEC sp_executesql @sql; 257 | `, 258 | 15000, 259 | ); 260 | 261 | console.log("Database reset completed successfully"); 262 | } catch (error) { 263 | console.error("Database reset failed:", error); 264 | 265 | // Retry logic for CI environments 266 | if (retryCount < maxRetries) { 267 | const delay = 5000 * (retryCount + 1); // Increasing delay 268 | console.log( 269 | `Retrying in ${delay}ms... (attempt ${retryCount + 2}/${maxRetries + 1})`, 270 | ); 271 | await new Promise((resolve) => setTimeout(resolve, delay)); 272 | return resetDB(retryCount + 1); 273 | } 274 | 275 | // Final fallback - try to recreate the database 276 | try { 277 | console.log("Attempting database recreation..."); 278 | // This would require a separate connection to master database 279 | // For now, just throw the error with better context 280 | throw new Error(`Database reset failed completely: ${error}`); 281 | } catch (finalError) { 282 | console.error("Final fallback also failed:", finalError); 283 | throw new Error( 284 | `Database reset failed: ${error}. All fallback attempts failed: ${finalError}`, 285 | ); 286 | } 287 | } 288 | }; 289 | 290 | const { execute } = await testAdapter({ 291 | adapter: () => { 292 | return kyselyAdapter(kyselyDB, { 293 | type: "mssql", 294 | debugLogs: { isRunningAdapterTests: true }, 295 | }); 296 | }, 297 | async runMigrations(betterAuthOptions) { 298 | await resetDB(); 299 | const opts = Object.assign(betterAuthOptions, { 300 | database: { db: kyselyDB, type: "mssql" }, 301 | } satisfies BetterAuthOptions); 302 | const { runMigrations } = await getMigrations(opts); 303 | await runMigrations(); 304 | }, 305 | prefixTests: "mssql", 306 | tests: [ 307 | normalTestSuite(), 308 | transactionsTestSuite({ disableTests: { ALL: true } }), 309 | authFlowTestSuite({ showDB }), 310 | numberIdTestSuite(), 311 | performanceTestSuite({ dialect: "mssql" }), 312 | ], 313 | async onFinish() { 314 | kyselyDB.destroy(); 315 | }, 316 | }); 317 | execute(); 318 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/api/check-endpoint-conflicts.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, vi, beforeEach } from "vitest"; 2 | import { checkEndpointConflicts } from "./index"; 3 | import type { BetterAuthOptions, BetterAuthPlugin } from "@better-auth/core"; 4 | import { createEndpoint } from "better-call"; 5 | import type { InternalLogger, LogLevel } from "@better-auth/core/env"; 6 | 7 | export let mockLoggerLevel: LogLevel = "debug"; 8 | export const mockLogger = { 9 | error: vi.fn(), 10 | warn: vi.fn(), 11 | info: vi.fn(), 12 | debug: vi.fn(), 13 | success: vi.fn(), 14 | get level(): LogLevel { 15 | return mockLoggerLevel; 16 | }, 17 | } satisfies InternalLogger; 18 | 19 | describe("checkEndpointConflicts", () => { 20 | const endpoint = createEndpoint.create({}); 21 | 22 | beforeEach(() => { 23 | mockLoggerLevel = "debug"; 24 | mockLogger.error.mockReset(); 25 | mockLogger.warn.mockReset(); 26 | mockLogger.info.mockReset(); 27 | mockLogger.debug.mockReset(); 28 | mockLogger.success.mockReset(); 29 | }); 30 | 31 | it("should not log errors when there are no endpoint conflicts", () => { 32 | const plugin1: BetterAuthPlugin = { 33 | id: "plugin1", 34 | endpoints: { 35 | endpoint1: endpoint( 36 | "/api/endpoint1", 37 | { 38 | method: "GET", 39 | }, 40 | vi.fn(), 41 | ), 42 | endpoint2: endpoint( 43 | "/api/endpoint2", 44 | { 45 | method: "POST", 46 | }, 47 | vi.fn(), 48 | ), 49 | }, 50 | }; 51 | 52 | const plugin2: BetterAuthPlugin = { 53 | id: "plugin2", 54 | endpoints: { 55 | endpoint3: endpoint( 56 | "/api/endpoint3", 57 | { 58 | method: "GET", 59 | }, 60 | vi.fn(), 61 | ), 62 | endpoint4: endpoint( 63 | "/api/endpoint4", 64 | { 65 | method: "POST", 66 | }, 67 | vi.fn(), 68 | ), 69 | }, 70 | }; 71 | 72 | const options: BetterAuthOptions = { 73 | plugins: [plugin1, plugin2], 74 | }; 75 | 76 | checkEndpointConflicts(options, mockLogger); 77 | 78 | expect(mockLogger.error).not.toHaveBeenCalled(); 79 | }); 80 | 81 | it("should NOT log an error when two plugins use the same endpoint path with different methods", () => { 82 | const plugin1: BetterAuthPlugin = { 83 | id: "plugin1", 84 | endpoints: { 85 | endpoint1: endpoint( 86 | "/api/shared", 87 | { 88 | method: "GET", 89 | }, 90 | vi.fn(), 91 | ), 92 | }, 93 | }; 94 | 95 | const plugin2: BetterAuthPlugin = { 96 | id: "plugin2", 97 | endpoints: { 98 | endpoint2: endpoint( 99 | "/api/shared", 100 | { 101 | method: "POST", 102 | }, 103 | vi.fn(), 104 | ), 105 | }, 106 | }; 107 | 108 | const options: BetterAuthOptions = { 109 | plugins: [plugin1, plugin2], 110 | }; 111 | 112 | checkEndpointConflicts(options, mockLogger); 113 | 114 | // Should NOT report an error since methods are different 115 | expect(mockLogger.error).not.toHaveBeenCalled(); 116 | }); 117 | 118 | it("should log an error when two plugins use the same endpoint path with the same method", () => { 119 | const plugin1: BetterAuthPlugin = { 120 | id: "plugin1", 121 | endpoints: { 122 | endpoint1: endpoint( 123 | "/api/shared", 124 | { 125 | method: "GET", 126 | }, 127 | vi.fn(), 128 | ), 129 | }, 130 | }; 131 | 132 | const plugin2: BetterAuthPlugin = { 133 | id: "plugin2", 134 | endpoints: { 135 | endpoint2: endpoint( 136 | "/api/shared", 137 | { 138 | method: "GET", 139 | }, 140 | vi.fn(), 141 | ), 142 | }, 143 | }; 144 | 145 | const options: BetterAuthOptions = { 146 | plugins: [plugin1, plugin2], 147 | }; 148 | 149 | checkEndpointConflicts(options, mockLogger); 150 | 151 | expect(mockLogger.error).toHaveBeenCalledTimes(1); 152 | expect(mockLogger.error).toHaveBeenCalledWith( 153 | expect.stringContaining("Endpoint path conflicts detected"), 154 | ); 155 | expect(mockLogger.error).toHaveBeenCalledWith( 156 | expect.stringContaining( 157 | '"/api/shared" [GET] used by plugins: plugin1, plugin2', 158 | ), 159 | ); 160 | }); 161 | 162 | it("should NOT detect conflicts when plugins use different methods on same paths", () => { 163 | const plugin1: BetterAuthPlugin = { 164 | id: "plugin1", 165 | endpoints: { 166 | endpoint1: endpoint( 167 | "/api/resource1", 168 | { 169 | method: "GET", 170 | }, 171 | vi.fn(), 172 | ), 173 | endpoint2: endpoint( 174 | "/api/resource2", 175 | { 176 | method: "POST", 177 | }, 178 | vi.fn(), 179 | ), 180 | }, 181 | }; 182 | 183 | const plugin2: BetterAuthPlugin = { 184 | id: "plugin2", 185 | endpoints: { 186 | endpoint3: endpoint( 187 | "/api/resource1", 188 | { 189 | method: "POST", 190 | }, 191 | vi.fn(), 192 | ), 193 | }, 194 | }; 195 | 196 | const plugin3: BetterAuthPlugin = { 197 | id: "plugin3", 198 | endpoints: { 199 | endpoint4: endpoint( 200 | "/api/resource2", 201 | { 202 | method: "GET", 203 | }, 204 | vi.fn(), 205 | ), 206 | }, 207 | }; 208 | 209 | const options: BetterAuthOptions = { 210 | plugins: [plugin1, plugin2, plugin3], 211 | }; 212 | 213 | checkEndpointConflicts(options, mockLogger); 214 | 215 | // Should not report errors since all methods are different 216 | expect(mockLogger.error).not.toHaveBeenCalled(); 217 | }); 218 | 219 | it("should detect conflicts when plugins use the same method on the same path", () => { 220 | const plugin1: BetterAuthPlugin = { 221 | id: "plugin1", 222 | endpoints: { 223 | endpoint1: endpoint( 224 | "/api/conflict", 225 | { 226 | method: "GET", 227 | }, 228 | vi.fn(), 229 | ), 230 | }, 231 | }; 232 | 233 | const plugin2: BetterAuthPlugin = { 234 | id: "plugin2", 235 | endpoints: { 236 | endpoint2: endpoint( 237 | "/api/conflict", 238 | { 239 | method: "GET", 240 | }, 241 | vi.fn(), 242 | ), 243 | }, 244 | }; 245 | 246 | const options: BetterAuthOptions = { 247 | plugins: [plugin1, plugin2], 248 | }; 249 | 250 | checkEndpointConflicts(options, mockLogger); 251 | 252 | expect(mockLogger.error).toHaveBeenCalledTimes(1); 253 | const errorCall = mockLogger.error.mock.calls[0]![0]; 254 | expect(errorCall).toContain( 255 | '"/api/conflict" [GET] used by plugins: plugin1, plugin2', 256 | ); 257 | }); 258 | 259 | it("should allow multiple endpoints from the same plugin using the same path with different methods", () => { 260 | const plugin1: BetterAuthPlugin = { 261 | id: "plugin1", 262 | endpoints: { 263 | endpoint1: endpoint( 264 | "/api/same", 265 | { 266 | method: "GET", 267 | }, 268 | vi.fn(), 269 | ), 270 | endpoint2: endpoint( 271 | "/api/same", 272 | { 273 | method: "POST", 274 | }, 275 | vi.fn(), 276 | ), 277 | }, 278 | }; 279 | 280 | const options: BetterAuthOptions = { 281 | plugins: [plugin1], 282 | }; 283 | 284 | checkEndpointConflicts(options, mockLogger); 285 | 286 | // Should not report error since methods are different 287 | expect(mockLogger.error).not.toHaveBeenCalled(); 288 | }); 289 | 290 | it("should detect conflicts when same plugin has duplicate methods on same path", () => { 291 | const plugin1: BetterAuthPlugin = { 292 | id: "plugin1", 293 | endpoints: { 294 | endpoint1: endpoint( 295 | "/api/same", 296 | { 297 | method: "GET", 298 | }, 299 | vi.fn(), 300 | ), 301 | endpoint2: endpoint( 302 | "/api/same", 303 | { 304 | method: "GET", 305 | }, 306 | vi.fn(), 307 | ), 308 | }, 309 | }; 310 | 311 | const options: BetterAuthOptions = { 312 | plugins: [plugin1], 313 | }; 314 | 315 | checkEndpointConflicts(options, mockLogger); 316 | 317 | expect(mockLogger.error).toHaveBeenCalledTimes(1); 318 | expect(mockLogger.error).toHaveBeenCalledWith( 319 | expect.stringContaining('"/api/same" [GET] used by plugins: plugin1'), 320 | ); 321 | }); 322 | 323 | it("should allow three plugins on the same path with different methods", () => { 324 | const plugin1: BetterAuthPlugin = { 325 | id: "plugin1", 326 | endpoints: { 327 | endpoint1: endpoint( 328 | "/api/resource", 329 | { 330 | method: "GET", 331 | }, 332 | vi.fn(), 333 | ), 334 | }, 335 | }; 336 | 337 | const plugin2: BetterAuthPlugin = { 338 | id: "plugin2", 339 | endpoints: { 340 | endpoint2: endpoint( 341 | "/api/resource", 342 | { 343 | method: "POST", 344 | }, 345 | vi.fn(), 346 | ), 347 | }, 348 | }; 349 | 350 | const plugin3: BetterAuthPlugin = { 351 | id: "plugin3", 352 | endpoints: { 353 | endpoint3: endpoint( 354 | "/api/resource", 355 | { 356 | method: "DELETE", 357 | }, 358 | vi.fn(), 359 | ), 360 | }, 361 | }; 362 | 363 | const options: BetterAuthOptions = { 364 | plugins: [plugin1, plugin2, plugin3], 365 | }; 366 | 367 | checkEndpointConflicts(options, mockLogger); 368 | 369 | // Should not report error since all methods are different 370 | expect(mockLogger.error).not.toHaveBeenCalled(); 371 | }); 372 | 373 | it("should detect conflicts when endpoints don't specify a method (wildcard)", () => { 374 | const plugin1: BetterAuthPlugin = { 375 | id: "plugin1", 376 | endpoints: { 377 | endpoint1: endpoint( 378 | "/api/wildcard", 379 | { 380 | method: "*", 381 | }, 382 | vi.fn(), 383 | ), 384 | }, 385 | }; 386 | 387 | const plugin2: BetterAuthPlugin = { 388 | id: "plugin2", 389 | endpoints: { 390 | endpoint2: endpoint( 391 | "/api/wildcard", 392 | { 393 | method: "GET", 394 | }, 395 | vi.fn(), 396 | ), 397 | }, 398 | }; 399 | 400 | const options: BetterAuthOptions = { 401 | plugins: [plugin1, plugin2], 402 | }; 403 | 404 | checkEndpointConflicts(options, mockLogger); 405 | 406 | expect(mockLogger.error).toHaveBeenCalledTimes(1); 407 | expect(mockLogger.error).toHaveBeenCalledWith( 408 | expect.stringContaining('"/api/wildcard"'), 409 | ); 410 | }); 411 | 412 | it("should handle plugins with no endpoints", () => { 413 | const plugin1: BetterAuthPlugin = { 414 | id: "plugin1", 415 | }; 416 | 417 | const plugin2: BetterAuthPlugin = { 418 | id: "plugin2", 419 | endpoints: {}, 420 | }; 421 | 422 | const options: BetterAuthOptions = { 423 | plugins: [plugin1, plugin2], 424 | }; 425 | 426 | checkEndpointConflicts(options, mockLogger); 427 | 428 | expect(mockLogger.error).not.toHaveBeenCalled(); 429 | }); 430 | 431 | it("should handle options with no plugins", () => { 432 | const options: BetterAuthOptions = {}; 433 | 434 | checkEndpointConflicts(options, mockLogger); 435 | 436 | expect(mockLogger.error).not.toHaveBeenCalled(); 437 | }); 438 | 439 | it("should handle options with empty plugins array", () => { 440 | const options: BetterAuthOptions = { 441 | plugins: [], 442 | }; 443 | 444 | checkEndpointConflicts(options, mockLogger); 445 | 446 | expect(mockLogger.error).not.toHaveBeenCalled(); 447 | }); 448 | }); 449 | ``` -------------------------------------------------------------------------------- /docs/content/docs/plugins/email-otp.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Email OTP 3 | description: Email OTP plugin for Better Auth. 4 | --- 5 | 6 | The Email OTP plugin allows user to sign in, verify their email, or reset their password using a one-time password (OTP) sent to their email address. 7 | 8 | 9 | ## Installation 10 | 11 | <Steps> 12 | <Step> 13 | ### Add the plugin to your auth config 14 | 15 | Add the `emailOTP` plugin to your auth config and implement the `sendVerificationOTP()` method. 16 | 17 | ```ts title="auth.ts" 18 | import { betterAuth } from "better-auth" 19 | import { emailOTP } from "better-auth/plugins" // [!code highlight] 20 | 21 | export const auth = betterAuth({ 22 | // ... other config options 23 | plugins: [ 24 | emailOTP({ // [!code highlight] 25 | async sendVerificationOTP({ email, otp, type }) { // [!code highlight] 26 | if (type === "sign-in") { // [!code highlight] 27 | // Send the OTP for sign in // [!code highlight] 28 | } else if (type === "email-verification") { // [!code highlight] 29 | // Send the OTP for email verification // [!code highlight] 30 | } else { // [!code highlight] 31 | // Send the OTP for password reset // [!code highlight] 32 | } // [!code highlight] 33 | }, // [!code highlight] 34 | }) // [!code highlight] 35 | ] 36 | }) 37 | ``` 38 | </Step> 39 | <Step> 40 | ### Add the client plugin 41 | 42 | ```ts title="auth-client.ts" 43 | import { createAuthClient } from "better-auth/client" 44 | import { emailOTPClient } from "better-auth/client/plugins" 45 | 46 | export const authClient = createAuthClient({ 47 | plugins: [ 48 | emailOTPClient() 49 | ] 50 | }) 51 | ``` 52 | </Step> 53 | </Steps> 54 | 55 | ## Usage 56 | 57 | ### Send an OTP 58 | 59 | Use the `sendVerificationOtp()` method to send an OTP to the user's email address. 60 | 61 | <APIMethod path="/email-otp/send-verification-otp" method="POST"> 62 | ```ts 63 | type sendVerificationOTP = { 64 | /** 65 | * Email address to send the OTP. 66 | */ 67 | email: string = "[email protected]" 68 | /** 69 | * Type of the OTP. `sign-in`, `email-verification`, or `forget-password`. 70 | */ 71 | type: "email-verification" | "sign-in" | "forget-password" = "sign-in" 72 | } 73 | ``` 74 | </APIMethod> 75 | 76 | ### Check an OTP (optional) 77 | 78 | Use the `checkVerificationOtp()` method to check if an OTP is valid. 79 | 80 | <APIMethod path="/email-otp/check-verification-otp" method="POST"> 81 | ```ts 82 | type checkVerificationOTP = { 83 | /** 84 | * Email address to send the OTP. 85 | */ 86 | email: string = "[email protected]" 87 | /** 88 | * Type of the OTP. `sign-in`, `email-verification`, or `forget-password`. 89 | */ 90 | type: "email-verification" | "sign-in" | "forget-password" = "sign-in" 91 | /** 92 | * OTP sent to the email. 93 | */ 94 | otp: string = "123456" 95 | } 96 | ``` 97 | </APIMethod> 98 | 99 | ### Sign In with OTP 100 | 101 | To sign in with OTP, use the `sendVerificationOtp()` method to send a "sign-in" OTP to the user's email address. 102 | 103 | <APIMethod path="/email-otp/send-verification-otp" method="POST"> 104 | ```ts 105 | type sendVerificationOTP = { 106 | /** 107 | * Email address to send the OTP. 108 | */ 109 | email: string = "[email protected]" 110 | /** 111 | * Type of the OTP. 112 | */ 113 | type: "sign-in" = "sign-in" 114 | } 115 | ``` 116 | </APIMethod> 117 | 118 | Once the user provides the OTP, you can sign in the user using the `signIn.emailOtp()` method. 119 | 120 | <APIMethod path="/sign-in/email-otp" method="POST"> 121 | ```ts 122 | type signInEmailOTP = { 123 | /** 124 | * Email address to sign in. 125 | */ 126 | email: string = "[email protected]" 127 | /** 128 | * OTP sent to the email. 129 | */ 130 | otp: string = "123456" 131 | } 132 | ``` 133 | </APIMethod> 134 | 135 | <Callout> 136 | If the user is not registered, they'll be automatically registered. If you want to prevent this, you can pass `disableSignUp` as `true` in the [options](#options). 137 | </Callout> 138 | 139 | ### Verify Email with OTP 140 | 141 | To verify the user's email address with OTP, use the `sendVerificationOtp()` method to send an "email-verification" OTP to the user's email address. 142 | 143 | <APIMethod path="/email-otp/send-verification-otp" method="POST"> 144 | ```ts 145 | type sendVerificationOTP = { 146 | /** 147 | * Email address to send the OTP. 148 | */ 149 | email: string = "[email protected]" 150 | /** 151 | * Type of the OTP. 152 | */ 153 | type: "email-verification" = "email-verification" 154 | } 155 | ``` 156 | </APIMethod> 157 | 158 | Once the user provides the OTP, use the `verifyEmail()` method to complete email verification. 159 | 160 | <APIMethod path="/email-otp/verify-email" method="POST"> 161 | ```ts 162 | type verifyEmailOTP = { 163 | /** 164 | * Email address to verify. 165 | */ 166 | email: string = "[email protected]" 167 | /** 168 | * OTP to verify. 169 | */ 170 | otp: string = "123456" 171 | } 172 | ``` 173 | </APIMethod> 174 | 175 | ### Reset Password with OTP 176 | 177 | To reset the user's password with OTP, use the `forgetPassword.emailOTP()` method to send a "forget-password" OTP to the user's email address. 178 | 179 | <APIMethod path="/forget-password/email-otp" method="POST"> 180 | ```ts 181 | type forgetPasswordEmailOTP = { 182 | /** 183 | * Email address to send the OTP. 184 | */ 185 | email: string = "[email protected]" 186 | } 187 | ``` 188 | </APIMethod> 189 | 190 | Once the user provides the OTP, use the `checkVerificationOtp()` method to check if it's valid (optional). 191 | 192 | <APIMethod path="/email-otp/check-verification-otp" method="POST"> 193 | ```ts 194 | type checkVerificationOTP = { 195 | /** 196 | * Email address to send the OTP. 197 | */ 198 | email: string = "[email protected]" 199 | /** 200 | * Type of the OTP. 201 | */ 202 | type: "forget-password" = "forget-password" 203 | /** 204 | * OTP sent to the email. 205 | */ 206 | otp: string = "123456" 207 | } 208 | ``` 209 | </APIMethod> 210 | 211 | Then, use the `resetPassword()` method to reset the user's password. 212 | 213 | <APIMethod path="/email-otp/reset-password" method="POST"> 214 | ```ts 215 | type resetPasswordEmailOTP = { 216 | /** 217 | * Email address to reset the password. 218 | */ 219 | email: string = "[email protected]" 220 | /** 221 | * OTP sent to the email. 222 | */ 223 | otp: string = "123456" 224 | /** 225 | * New password. 226 | */ 227 | password: string = "new-secure-password" 228 | } 229 | ``` 230 | </APIMethod> 231 | 232 | ### Override Default Email Verification 233 | 234 | To override the default email verification, pass `overrideDefaultEmailVerification: true` in the options. This will make the system use an email OTP instead of the default verification link whenever email verification is triggered. In other words, the user will verify their email using an OTP rather than clicking a link. 235 | 236 | ```ts title="auth.ts" 237 | import { betterAuth } from "better-auth"; 238 | 239 | export const auth = betterAuth({ 240 | plugins: [ 241 | emailOTP({ 242 | overrideDefaultEmailVerification: true, // [!code highlight] 243 | async sendVerificationOTP({ email, otp, type }) { 244 | // Implement the sendVerificationOTP method to send the OTP to the user's email address 245 | }, 246 | }), 247 | ], 248 | }); 249 | ``` 250 | 251 | 252 | ## Options 253 | 254 | - `sendVerificationOTP`: A function that sends the OTP to the user's email address. The function receives an object with the following properties: 255 | - `email`: The user's email address. 256 | - `otp`: The OTP to send. 257 | - `type`: The type of OTP to send. Can be "sign-in", "email-verification", or "forget-password". 258 | 259 | - `otpLength`: The length of the OTP. Defaults to `6`. 260 | 261 | - `expiresIn`: The expiry time of the OTP in seconds. Defaults to `300` seconds. 262 | 263 | ```ts title="auth.ts" 264 | import { betterAuth } from "better-auth" 265 | 266 | export const auth = betterAuth({ 267 | plugins: [ 268 | emailOTP({ 269 | otpLength: 8, 270 | expiresIn: 600 271 | }) 272 | ] 273 | }) 274 | ``` 275 | 276 | - `sendVerificationOnSignUp`: A boolean value that determines whether to send the OTP when a user signs up. Defaults to `false`. 277 | 278 | - `disableSignUp`: A boolean value that determines whether to prevent automatic sign-up when the user is not registered. Defaults to `false`. 279 | 280 | - `generateOTP`: A function that generates the OTP. Defaults to a random 6-digit number. 281 | 282 | - `allowedAttempts`: The maximum number of attempts allowed for verifying an OTP. Defaults to `3`. After exceeding this limit, the OTP becomes invalid and the user needs to request a new one. 283 | 284 | ```ts title="auth.ts" 285 | import { betterAuth } from "better-auth" 286 | 287 | export const auth = betterAuth({ 288 | plugins: [ 289 | emailOTP({ 290 | allowedAttempts: 5, // Allow 5 attempts before invalidating the OTP 291 | expiresIn: 300 292 | }) 293 | ] 294 | }) 295 | ``` 296 | 297 | When the maximum attempts are exceeded, the `verifyOTP`, `signIn.emailOtp`, `verifyEmail`, and `resetPassword` methods will return an error with code `TOO_MANY_ATTEMPTS`. 298 | 299 | - `storeOTP`: The method to store the OTP in your database, wether `encrypted`, `hashed` or `plain` text. Default is `plain` text. 300 | 301 | <Callout> 302 | Note: This will not affect the OTP sent to the user, it will only affect the OTP stored in your database. 303 | </Callout> 304 | 305 | Alternatively, you can pass a custom encryptor or hasher to store the OTP in your database. 306 | 307 | **Custom encryptor** 308 | 309 | ```ts title="auth.ts" 310 | emailOTP({ 311 | storeOTP: { 312 | encrypt: async (otp) => { 313 | return myCustomEncryptor(otp); 314 | }, 315 | decrypt: async (otp) => { 316 | return myCustomDecryptor(otp); 317 | }, 318 | } 319 | }) 320 | ``` 321 | 322 | **Custom hasher** 323 | 324 | ```ts title="auth.ts" 325 | emailOTP({ 326 | storeOTP: { 327 | hash: async (otp) => { 328 | return myCustomHasher(otp); 329 | }, 330 | } 331 | }) 332 | ``` 333 | ``` -------------------------------------------------------------------------------- /docs/components/builder/beam.tsx: -------------------------------------------------------------------------------- ```typescript 1 | "use client"; 2 | import React from "react"; 3 | import { motion } from "framer-motion"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | export const BackgroundBeams = React.memo( 7 | ({ className }: { className?: string }) => { 8 | const paths = [ 9 | "M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875", 10 | "M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867", 11 | "M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859", 12 | "M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851", 13 | "M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843", 14 | "M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835", 15 | "M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827", 16 | "M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819", 17 | "M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811", 18 | "M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803", 19 | "M-310 -269C-310 -269 -242 136 222 263C686 390 754 795 754 795", 20 | "M-303 -277C-303 -277 -235 128 229 255C693 382 761 787 761 787", 21 | "M-296 -285C-296 -285 -228 120 236 247C700 374 768 779 768 779", 22 | "M-289 -293C-289 -293 -221 112 243 239C707 366 775 771 775 771", 23 | "M-282 -301C-282 -301 -214 104 250 231C714 358 782 763 782 763", 24 | "M-275 -309C-275 -309 -207 96 257 223C721 350 789 755 789 755", 25 | "M-268 -317C-268 -317 -200 88 264 215C728 342 796 747 796 747", 26 | "M-261 -325C-261 -325 -193 80 271 207C735 334 803 739 803 739", 27 | "M-254 -333C-254 -333 -186 72 278 199C742 326 810 731 810 731", 28 | "M-247 -341C-247 -341 -179 64 285 191C749 318 817 723 817 723", 29 | "M-240 -349C-240 -349 -172 56 292 183C756 310 824 715 824 715", 30 | "M-233 -357C-233 -357 -165 48 299 175C763 302 831 707 831 707", 31 | "M-226 -365C-226 -365 -158 40 306 167C770 294 838 699 838 699", 32 | "M-219 -373C-219 -373 -151 32 313 159C777 286 845 691 845 691", 33 | "M-212 -381C-212 -381 -144 24 320 151C784 278 852 683 852 683", 34 | "M-205 -389C-205 -389 -137 16 327 143C791 270 859 675 859 675", 35 | "M-198 -397C-198 -397 -130 8 334 135C798 262 866 667 866 667", 36 | "M-191 -405C-191 -405 -123 0 341 127C805 254 873 659 873 659", 37 | "M-184 -413C-184 -413 -116 -8 348 119C812 246 880 651 880 651", 38 | "M-177 -421C-177 -421 -109 -16 355 111C819 238 887 643 887 643", 39 | "M-170 -429C-170 -429 -102 -24 362 103C826 230 894 635 894 635", 40 | "M-163 -437C-163 -437 -95 -32 369 95C833 222 901 627 901 627", 41 | "M-156 -445C-156 -445 -88 -40 376 87C840 214 908 619 908 619", 42 | "M-149 -453C-149 -453 -81 -48 383 79C847 206 915 611 915 611", 43 | "M-142 -461C-142 -461 -74 -56 390 71C854 198 922 603 922 603", 44 | "M-135 -469C-135 -469 -67 -64 397 63C861 190 929 595 929 595", 45 | "M-128 -477C-128 -477 -60 -72 404 55C868 182 936 587 936 587", 46 | "M-121 -485C-121 -485 -53 -80 411 47C875 174 943 579 943 579", 47 | "M-114 -493C-114 -493 -46 -88 418 39C882 166 950 571 950 571", 48 | "M-107 -501C-107 -501 -39 -96 425 31C889 158 957 563 957 563", 49 | "M-100 -509C-100 -509 -32 -104 432 23C896 150 964 555 964 555", 50 | "M-93 -517C-93 -517 -25 -112 439 15C903 142 971 547 971 547", 51 | "M-86 -525C-86 -525 -18 -120 446 7C910 134 978 539 978 539", 52 | "M-79 -533C-79 -533 -11 -128 453 -1C917 126 985 531 985 531", 53 | "M-72 -541C-72 -541 -4 -136 460 -9C924 118 992 523 992 523", 54 | "M-65 -549C-65 -549 3 -144 467 -17C931 110 999 515 999 515", 55 | "M-58 -557C-58 -557 10 -152 474 -25C938 102 1006 507 1006 507", 56 | "M-51 -565C-51 -565 17 -160 481 -33C945 94 1013 499 1013 499", 57 | "M-44 -573C-44 -573 24 -168 488 -41C952 86 1020 491 1020 491", 58 | "M-37 -581C-37 -581 31 -176 495 -49C959 78 1027 483 1027 483", 59 | ]; 60 | return ( 61 | <div 62 | className={cn( 63 | "absolute h-full w-full inset-0 [mask-size:40px] [mask-repeat:no-repeat] flex items-center justify-center", 64 | className, 65 | )} 66 | > 67 | <svg 68 | className=" z-0 h-full w-full pointer-events-none absolute " 69 | width="100%" 70 | height="100%" 71 | viewBox="0 0 696 316" 72 | fill="none" 73 | xmlns="http://www.w3.org/2000/svg" 74 | > 75 | <path 76 | d="M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803M-310 -269C-310 -269 -242 136 222 263C686 390 754 795 754 795M-303 -277C-303 -277 -235 128 229 255C693 382 761 787 761 787M-296 -285C-296 -285 -228 120 236 247C700 374 768 779 768 779M-289 -293C-289 -293 -221 112 243 239C707 366 775 771 775 771M-282 -301C-282 -301 -214 104 250 231C714 358 782 763 782 763M-275 -309C-275 -309 -207 96 257 223C721 350 789 755 789 755M-268 -317C-268 -317 -200 88 264 215C728 342 796 747 796 747M-261 -325C-261 -325 -193 80 271 207C735 334 803 739 803 739M-254 -333C-254 -333 -186 72 278 199C742 326 810 731 810 731M-247 -341C-247 -341 -179 64 285 191C749 318 817 723 817 723M-240 -349C-240 -349 -172 56 292 183C756 310 824 715 824 715M-233 -357C-233 -357 -165 48 299 175C763 302 831 707 831 707M-226 -365C-226 -365 -158 40 306 167C770 294 838 699 838 699M-219 -373C-219 -373 -151 32 313 159C777 286 845 691 845 691M-212 -381C-212 -381 -144 24 320 151C784 278 852 683 852 683M-205 -389C-205 -389 -137 16 327 143C791 270 859 675 859 675M-198 -397C-198 -397 -130 8 334 135C798 262 866 667 866 667M-191 -405C-191 -405 -123 0 341 127C805 254 873 659 873 659M-184 -413C-184 -413 -116 -8 348 119C812 246 880 651 880 651M-177 -421C-177 -421 -109 -16 355 111C819 238 887 643 887 643M-170 -429C-170 -429 -102 -24 362 103C826 230 894 635 894 635M-163 -437C-163 -437 -95 -32 369 95C833 222 901 627 901 627M-156 -445C-156 -445 -88 -40 376 87C840 214 908 619 908 619M-149 -453C-149 -453 -81 -48 383 79C847 206 915 611 915 611M-142 -461C-142 -461 -74 -56 390 71C854 198 922 603 922 603M-135 -469C-135 -469 -67 -64 397 63C861 190 929 595 929 595M-128 -477C-128 -477 -60 -72 404 55C868 182 936 587 936 587M-121 -485C-121 -485 -53 -80 411 47C875 174 943 579 943 579M-114 -493C-114 -493 -46 -88 418 39C882 166 950 571 950 571M-107 -501C-107 -501 -39 -96 425 31C889 158 957 563 957 563M-100 -509C-100 -509 -32 -104 432 23C896 150 964 555 964 555M-93 -517C-93 -517 -25 -112 439 15C903 142 971 547 971 547M-86 -525C-86 -525 -18 -120 446 7C910 134 978 539 978 539M-79 -533C-79 -533 -11 -128 453 -1C917 126 985 531 985 531M-72 -541C-72 -541 -4 -136 460 -9C924 118 992 523 992 523M-65 -549C-65 -549 3 -144 467 -17C931 110 999 515 999 515M-58 -557C-58 -557 10 -152 474 -25C938 102 1006 507 1006 507M-51 -565C-51 -565 17 -160 481 -33C945 94 1013 499 1013 499M-44 -573C-44 -573 24 -168 488 -41C952 86 1020 491 1020 491M-37 -581C-37 -581 31 -176 495 -49C959 78 1027 483 1027 483M-30 -589C-30 -589 38 -184 502 -57C966 70 1034 475 1034 475M-23 -597C-23 -597 45 -192 509 -65C973 62 1041 467 1041 467M-16 -605C-16 -605 52 -200 516 -73C980 54 1048 459 1048 459M-9 -613C-9 -613 59 -208 523 -81C987 46 1055 451 1055 451M-2 -621C-2 -621 66 -216 530 -89C994 38 1062 443 1062 443M5 -629C5 -629 73 -224 537 -97C1001 30 1069 435 1069 435M12 -637C12 -637 80 -232 544 -105C1008 22 1076 427 1076 427M19 -645C19 -645 87 -240 551 -113C1015 14 1083 419 1083 419" 77 | stroke="url(#paint0_radial_242_278)" 78 | strokeOpacity="0.05" 79 | strokeWidth="0.5" 80 | ></path> 81 | 82 | {paths.map((path, index) => ( 83 | <motion.path 84 | key={`path-` + index} 85 | d={path} 86 | stroke={`url(#linearGradient-${index})`} 87 | strokeOpacity="0.4" 88 | strokeWidth="0.5" 89 | ></motion.path> 90 | ))} 91 | <defs> 92 | {paths.map((path, index) => ( 93 | <motion.linearGradient 94 | id={`linearGradient-${index}`} 95 | key={`gradient-${index}`} 96 | initial={{ 97 | x1: "0%", 98 | x2: "0%", 99 | y1: "0%", 100 | y2: "0%", 101 | }} 102 | animate={{ 103 | x1: ["0%", "100%"], 104 | x2: ["0%", "95%"], 105 | y1: ["0%", "100%"], 106 | y2: ["0%", `${93 + Math.random() * 8}%`], 107 | }} 108 | transition={{ 109 | duration: Math.random() * 10 + 10, 110 | ease: "easeInOut", 111 | repeat: Infinity, 112 | delay: Math.random() * 10, 113 | }} 114 | > 115 | <stop stopColor="#18CCFC" stopOpacity="0"></stop> 116 | <stop stopColor="#18CCFC"></stop> 117 | <stop offset="32.5%" stopColor="#6344F5"></stop> 118 | <stop offset="100%" stopColor="#AE48FF" stopOpacity="0"></stop> 119 | </motion.linearGradient> 120 | ))} 121 | 122 | <radialGradient 123 | id="paint0_radial_242_278" 124 | cx="0" 125 | cy="0" 126 | r="1" 127 | gradientUnits="userSpaceOnUse" 128 | gradientTransform="translate(352 34) rotate(90) scale(555 1560.62)" 129 | > 130 | <stop offset="0.0666667" stopColor="var(--neutral-300)"></stop> 131 | <stop offset="0.243243" stopColor="var(--neutral-300)"></stop> 132 | <stop offset="0.43594" stopColor="white" stopOpacity="0"></stop> 133 | </radialGradient> 134 | </defs> 135 | </svg> 136 | </div> 137 | ); 138 | }, 139 | ); 140 | 141 | BackgroundBeams.displayName = "BackgroundBeams"; 142 | ```