This is page 26 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
│ │ │ ├── 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
│ │ │ │ ├── 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
--------------------------------------------------------------------------------
/packages/better-auth/src/api/routes/reset-password.ts:
--------------------------------------------------------------------------------
```typescript
1 | import * as z from "zod";
2 | import { createAuthEndpoint } from "@better-auth/core/api";
3 | import { APIError } from "better-call";
4 | import { getDate } from "../../utils/date";
5 | import { generateId } from "../../utils";
6 | import { BASE_ERROR_CODES } from "@better-auth/core/error";
7 | import { originCheck } from "../middlewares";
8 | import type { AuthContext } from "@better-auth/core";
9 |
10 | function redirectError(
11 | ctx: AuthContext,
12 | callbackURL: string | undefined,
13 | query?: Record<string, string>,
14 | ): string {
15 | const url = callbackURL
16 | ? new URL(callbackURL, ctx.baseURL)
17 | : new URL(`${ctx.baseURL}/error`);
18 | if (query)
19 | Object.entries(query).forEach(([k, v]) => url.searchParams.set(k, v));
20 | return url.href;
21 | }
22 |
23 | function redirectCallback(
24 | ctx: AuthContext,
25 | callbackURL: string,
26 | query?: Record<string, string>,
27 | ): string {
28 | const url = new URL(callbackURL, ctx.baseURL);
29 | if (query)
30 | Object.entries(query).forEach(([k, v]) => url.searchParams.set(k, v));
31 | return url.href;
32 | }
33 |
34 | export const requestPasswordReset = createAuthEndpoint(
35 | "/request-password-reset",
36 | {
37 | method: "POST",
38 | body: z.object({
39 | /**
40 | * The email address of the user to send a password reset email to.
41 | */
42 | email: z.email().meta({
43 | description:
44 | "The email address of the user to send a password reset email to",
45 | }),
46 | /**
47 | * The URL to redirect the user to reset their password.
48 | * If the token isn't valid or expired, it'll be redirected with a query parameter `?
49 | * error=INVALID_TOKEN`. If the token is valid, it'll be redirected with a query parameter `?
50 | * token=VALID_TOKEN
51 | */
52 | redirectTo: z
53 | .string()
54 | .meta({
55 | description:
56 | "The URL to redirect the user to reset their password. If the token isn't valid or expired, it'll be redirected with a query parameter `?error=INVALID_TOKEN`. If the token is valid, it'll be redirected with a query parameter `?token=VALID_TOKEN",
57 | })
58 | .optional(),
59 | }),
60 | metadata: {
61 | openapi: {
62 | description: "Send a password reset email to the user",
63 | responses: {
64 | "200": {
65 | description: "Success",
66 | content: {
67 | "application/json": {
68 | schema: {
69 | type: "object",
70 | properties: {
71 | status: {
72 | type: "boolean",
73 | },
74 | message: {
75 | type: "string",
76 | },
77 | },
78 | },
79 | },
80 | },
81 | },
82 | },
83 | },
84 | },
85 | },
86 | async (ctx) => {
87 | if (!ctx.context.options.emailAndPassword?.sendResetPassword) {
88 | ctx.context.logger.error(
89 | "Reset password isn't enabled.Please pass an emailAndPassword.sendResetPassword function in your auth config!",
90 | );
91 | throw new APIError("BAD_REQUEST", {
92 | message: "Reset password isn't enabled",
93 | });
94 | }
95 | const { email, redirectTo } = ctx.body;
96 |
97 | const user = await ctx.context.internalAdapter.findUserByEmail(email, {
98 | includeAccounts: true,
99 | });
100 | if (!user) {
101 | ctx.context.logger.error("Reset Password: User not found", { email });
102 | return ctx.json({
103 | status: true,
104 | message:
105 | "If this email exists in our system, check your email for the reset link",
106 | });
107 | }
108 | const defaultExpiresIn = 60 * 60 * 1;
109 | const expiresAt = getDate(
110 | ctx.context.options.emailAndPassword.resetPasswordTokenExpiresIn ||
111 | defaultExpiresIn,
112 | "sec",
113 | );
114 | const verificationToken = generateId(24);
115 | await ctx.context.internalAdapter.createVerificationValue({
116 | value: user.user.id,
117 | identifier: `reset-password:${verificationToken}`,
118 | expiresAt,
119 | });
120 | const callbackURL = redirectTo ? encodeURIComponent(redirectTo) : "";
121 | const url = `${ctx.context.baseURL}/reset-password/${verificationToken}?callbackURL=${callbackURL}`;
122 | await ctx.context.options.emailAndPassword.sendResetPassword(
123 | {
124 | user: user.user,
125 | url,
126 | token: verificationToken,
127 | },
128 | ctx.request,
129 | );
130 | return ctx.json({
131 | status: true,
132 | message:
133 | "If this email exists in our system, check your email for the reset link",
134 | });
135 | },
136 | );
137 |
138 | export const requestPasswordResetCallback = createAuthEndpoint(
139 | "/reset-password/:token",
140 | {
141 | method: "GET",
142 | query: z.object({
143 | callbackURL: z.string().meta({
144 | description: "The URL to redirect the user to reset their password",
145 | }),
146 | }),
147 | use: [originCheck((ctx) => ctx.query.callbackURL)],
148 | metadata: {
149 | openapi: {
150 | description: "Redirects the user to the callback URL with the token",
151 | responses: {
152 | "200": {
153 | description: "Success",
154 | content: {
155 | "application/json": {
156 | schema: {
157 | type: "object",
158 | properties: {
159 | token: {
160 | type: "string",
161 | },
162 | },
163 | },
164 | },
165 | },
166 | },
167 | },
168 | },
169 | },
170 | },
171 | async (ctx) => {
172 | const { token } = ctx.params;
173 | const { callbackURL } = ctx.query;
174 | if (!token || !callbackURL) {
175 | throw ctx.redirect(
176 | redirectError(ctx.context, callbackURL, { error: "INVALID_TOKEN" }),
177 | );
178 | }
179 | const verification =
180 | await ctx.context.internalAdapter.findVerificationValue(
181 | `reset-password:${token}`,
182 | );
183 | if (!verification || verification.expiresAt < new Date()) {
184 | throw ctx.redirect(
185 | redirectError(ctx.context, callbackURL, { error: "INVALID_TOKEN" }),
186 | );
187 | }
188 |
189 | throw ctx.redirect(redirectCallback(ctx.context, callbackURL, { token }));
190 | },
191 | );
192 |
193 | export const resetPassword = createAuthEndpoint(
194 | "/reset-password",
195 | {
196 | method: "POST",
197 | query: z
198 | .object({
199 | token: z.string().optional(),
200 | })
201 | .optional(),
202 | body: z.object({
203 | newPassword: z.string().meta({
204 | description: "The new password to set",
205 | }),
206 | token: z
207 | .string()
208 | .meta({
209 | description: "The token to reset the password",
210 | })
211 | .optional(),
212 | }),
213 | metadata: {
214 | openapi: {
215 | description: "Reset the password for a user",
216 | responses: {
217 | "200": {
218 | description: "Success",
219 | content: {
220 | "application/json": {
221 | schema: {
222 | type: "object",
223 | properties: {
224 | status: {
225 | type: "boolean",
226 | },
227 | },
228 | },
229 | },
230 | },
231 | },
232 | },
233 | },
234 | },
235 | },
236 | async (ctx) => {
237 | const token = ctx.body.token || ctx.query?.token;
238 | if (!token) {
239 | throw new APIError("BAD_REQUEST", {
240 | message: BASE_ERROR_CODES.INVALID_TOKEN,
241 | });
242 | }
243 |
244 | const { newPassword } = ctx.body;
245 |
246 | const minLength = ctx.context.password?.config.minPasswordLength;
247 | const maxLength = ctx.context.password?.config.maxPasswordLength;
248 | if (newPassword.length < minLength) {
249 | throw new APIError("BAD_REQUEST", {
250 | message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT,
251 | });
252 | }
253 | if (newPassword.length > maxLength) {
254 | throw new APIError("BAD_REQUEST", {
255 | message: BASE_ERROR_CODES.PASSWORD_TOO_LONG,
256 | });
257 | }
258 |
259 | const id = `reset-password:${token}`;
260 |
261 | const verification =
262 | await ctx.context.internalAdapter.findVerificationValue(id);
263 | if (!verification || verification.expiresAt < new Date()) {
264 | throw new APIError("BAD_REQUEST", {
265 | message: BASE_ERROR_CODES.INVALID_TOKEN,
266 | });
267 | }
268 | const userId = verification.value;
269 | const hashedPassword = await ctx.context.password.hash(newPassword);
270 | const accounts = await ctx.context.internalAdapter.findAccounts(userId);
271 | const account = accounts.find((ac) => ac.providerId === "credential");
272 | if (!account) {
273 | await ctx.context.internalAdapter.createAccount({
274 | userId,
275 | providerId: "credential",
276 | password: hashedPassword,
277 | accountId: userId,
278 | });
279 | } else {
280 | await ctx.context.internalAdapter.updatePassword(userId, hashedPassword);
281 | }
282 | await ctx.context.internalAdapter.deleteVerificationValue(verification.id);
283 |
284 | if (ctx.context.options.emailAndPassword?.onPasswordReset) {
285 | const user = await ctx.context.internalAdapter.findUserById(userId);
286 | if (user) {
287 | await ctx.context.options.emailAndPassword.onPasswordReset(
288 | {
289 | user,
290 | },
291 | ctx.request,
292 | );
293 | }
294 | }
295 | if (ctx.context.options.emailAndPassword?.revokeSessionsOnPasswordReset) {
296 | await ctx.context.internalAdapter.deleteSessions(userId);
297 | }
298 | return ctx.json({
299 | status: true,
300 | });
301 | },
302 | );
303 |
```
--------------------------------------------------------------------------------
/packages/better-auth/src/init.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { defu } from "defu";
2 | import { hashPassword, verifyPassword } from "./crypto/password";
3 | import { createInternalAdapter, getAuthTables, getMigrations } from "./db";
4 | import type { Entries } from "type-fest";
5 | import { getAdapter } from "./db/utils";
6 | import type { BetterAuthOptions, BetterAuthPlugin } from "@better-auth/core";
7 | import { DEFAULT_SECRET } from "./utils/constants";
8 | import { createCookieGetter, getCookies } from "./cookies";
9 | import { createLogger, isTest } from "@better-auth/core/env";
10 | import {
11 | type SocialProviders,
12 | socialProviders,
13 | } from "@better-auth/core/social-providers";
14 | import type { OAuthProvider } from "@better-auth/core/oauth2";
15 | import { generateId } from "./utils";
16 | import { env, isProduction } from "@better-auth/core/env";
17 | import { checkPassword } from "./utils/password";
18 | import { getBaseURL } from "./utils/url";
19 | import { BetterAuthError } from "@better-auth/core/error";
20 | import { createTelemetry } from "@better-auth/telemetry";
21 | import { getKyselyDatabaseType } from "./adapters/kysely-adapter";
22 | import { checkEndpointConflicts } from "./api";
23 | import { isPromise } from "./utils/is-promise";
24 | import type { AuthContext } from "@better-auth/core";
25 |
26 | export const init = async (options: BetterAuthOptions) => {
27 | const adapter = await getAdapter(options);
28 | const plugins = options.plugins || [];
29 | const internalPlugins = getInternalPlugins(options);
30 | const logger = createLogger(options.logger);
31 | const baseURL = getBaseURL(options.baseURL, options.basePath);
32 |
33 | const secret =
34 | options.secret ||
35 | env.BETTER_AUTH_SECRET ||
36 | env.AUTH_SECRET ||
37 | DEFAULT_SECRET;
38 |
39 | if (secret === DEFAULT_SECRET) {
40 | if (isProduction) {
41 | logger.error(
42 | "You are using the default secret. Please set `BETTER_AUTH_SECRET` in your environment variables or pass `secret` in your auth config.",
43 | );
44 | }
45 | }
46 |
47 | options = {
48 | ...options,
49 | secret,
50 | baseURL: baseURL ? new URL(baseURL).origin : "",
51 | basePath: options.basePath || "/api/auth",
52 | plugins: plugins.concat(internalPlugins),
53 | };
54 |
55 | checkEndpointConflicts(options, logger);
56 | const cookies = getCookies(options);
57 | const tables = getAuthTables(options);
58 | const providers: OAuthProvider[] = (
59 | Object.entries(
60 | options.socialProviders || {},
61 | ) as unknown as Entries<SocialProviders>
62 | )
63 | .map(([key, config]) => {
64 | if (config == null) {
65 | return null;
66 | }
67 | if (config.enabled === false) {
68 | return null;
69 | }
70 | if (!config.clientId) {
71 | logger.warn(
72 | `Social provider ${key} is missing clientId or clientSecret`,
73 | );
74 | }
75 | const provider = socialProviders[key](config as never);
76 | (provider as OAuthProvider).disableImplicitSignUp =
77 | config.disableImplicitSignUp;
78 | return provider;
79 | })
80 | .filter((x) => x !== null);
81 |
82 | const generateIdFunc: AuthContext["generateId"] = ({ model, size }) => {
83 | if (typeof (options.advanced as any)?.generateId === "function") {
84 | return (options.advanced as any).generateId({ model, size });
85 | }
86 | if (typeof options?.advanced?.database?.generateId === "function") {
87 | return options.advanced.database.generateId({ model, size });
88 | }
89 | return generateId(size);
90 | };
91 |
92 | const { publish } = await createTelemetry(options, {
93 | adapter: adapter.id,
94 | database:
95 | typeof options.database === "function"
96 | ? "adapter"
97 | : getKyselyDatabaseType(options.database) || "unknown",
98 | });
99 |
100 | let ctx: AuthContext = {
101 | appName: options.appName || "Better Auth",
102 | socialProviders: providers,
103 | options,
104 | oauthConfig: {
105 | storeStateStrategy:
106 | options.advanced?.oauthConfig?.storeStateStrategy || "database",
107 | skipStateCookieCheck:
108 | !!options.advanced?.oauthConfig?.skipStateCookieCheck,
109 | },
110 | tables,
111 | trustedOrigins: getTrustedOrigins(options),
112 | baseURL: baseURL || "",
113 | sessionConfig: {
114 | updateAge:
115 | options.session?.updateAge !== undefined
116 | ? options.session.updateAge
117 | : 24 * 60 * 60, // 24 hours
118 | expiresIn: options.session?.expiresIn || 60 * 60 * 24 * 7, // 7 days
119 | freshAge:
120 | options.session?.freshAge === undefined
121 | ? 60 * 60 * 24 // 24 hours
122 | : options.session.freshAge,
123 | },
124 | secret,
125 | rateLimit: {
126 | ...options.rateLimit,
127 | enabled: options.rateLimit?.enabled ?? isProduction,
128 | window: options.rateLimit?.window || 10,
129 | max: options.rateLimit?.max || 100,
130 | storage:
131 | options.rateLimit?.storage ||
132 | (options.secondaryStorage ? "secondary-storage" : "memory"),
133 | },
134 | authCookies: cookies,
135 | logger,
136 | generateId: generateIdFunc,
137 | session: null,
138 | secondaryStorage: options.secondaryStorage,
139 | password: {
140 | hash: options.emailAndPassword?.password?.hash || hashPassword,
141 | verify: options.emailAndPassword?.password?.verify || verifyPassword,
142 | config: {
143 | minPasswordLength: options.emailAndPassword?.minPasswordLength || 8,
144 | maxPasswordLength: options.emailAndPassword?.maxPasswordLength || 128,
145 | },
146 | checkPassword,
147 | },
148 | setNewSession(session) {
149 | this.newSession = session;
150 | },
151 | newSession: null,
152 | adapter: adapter,
153 | internalAdapter: createInternalAdapter(adapter, {
154 | options,
155 | logger,
156 | hooks: options.databaseHooks ? [options.databaseHooks] : [],
157 | generateId: generateIdFunc,
158 | }),
159 | createAuthCookie: createCookieGetter(options),
160 | async runMigrations() {
161 | //only run migrations if database is provided and it's not an adapter
162 | if (!options.database || "updateMany" in options.database) {
163 | throw new BetterAuthError(
164 | "Database is not provided or it's an adapter. Migrations are only supported with a database instance.",
165 | );
166 | }
167 | const { runMigrations } = await getMigrations(options);
168 | await runMigrations();
169 | },
170 | publishTelemetry: publish,
171 | skipCSRFCheck: !!options.advanced?.disableCSRFCheck,
172 | skipOriginCheck:
173 | options.advanced?.disableOriginCheck !== undefined
174 | ? options.advanced.disableOriginCheck
175 | : isTest()
176 | ? true
177 | : false,
178 | };
179 | const initOrPromise = runPluginInit(ctx);
180 | let context: AuthContext;
181 | if (isPromise(initOrPromise)) {
182 | ({ context } = await initOrPromise);
183 | } else {
184 | ({ context } = initOrPromise);
185 | }
186 | return context;
187 | };
188 |
189 | async function runPluginInit(ctx: AuthContext) {
190 | let options = ctx.options;
191 | const plugins = options.plugins || [];
192 | let context: AuthContext = ctx;
193 | const dbHooks: BetterAuthOptions["databaseHooks"][] = [];
194 | for (const plugin of plugins) {
195 | if (plugin.init) {
196 | let initPromise = plugin.init(context);
197 | let result: ReturnType<Required<BetterAuthPlugin>["init"]>;
198 | if (isPromise(initPromise)) {
199 | result = await initPromise;
200 | } else {
201 | result = initPromise;
202 | }
203 | if (typeof result === "object") {
204 | if (result.options) {
205 | const { databaseHooks, ...restOpts } = result.options;
206 | if (databaseHooks) {
207 | dbHooks.push(databaseHooks);
208 | }
209 | options = defu(options, restOpts);
210 | }
211 | if (result.context) {
212 | context = {
213 | ...context,
214 | ...(result.context as Partial<AuthContext>),
215 | };
216 | }
217 | }
218 | }
219 | }
220 | // Add the global database hooks last
221 | dbHooks.push(options.databaseHooks);
222 | context.internalAdapter = createInternalAdapter(ctx.adapter, {
223 | options,
224 | logger: ctx.logger,
225 | hooks: dbHooks.filter((u) => u !== undefined),
226 | generateId: ctx.generateId,
227 | });
228 | context.options = options;
229 | return { context };
230 | }
231 |
232 | function getInternalPlugins(options: BetterAuthOptions) {
233 | const plugins: BetterAuthPlugin[] = [];
234 | if (options.advanced?.crossSubDomainCookies?.enabled) {
235 | //TODO: add internal plugin
236 | }
237 | return plugins;
238 | }
239 |
240 | function getTrustedOrigins(options: BetterAuthOptions) {
241 | const baseURL = getBaseURL(options.baseURL, options.basePath);
242 | if (!baseURL) {
243 | return [];
244 | }
245 | const trustedOrigins = [new URL(baseURL).origin];
246 | if (options.trustedOrigins && Array.isArray(options.trustedOrigins)) {
247 | trustedOrigins.push(...options.trustedOrigins);
248 | }
249 | const envTrustedOrigins = env.BETTER_AUTH_TRUSTED_ORIGINS;
250 | if (envTrustedOrigins) {
251 | trustedOrigins.push(...envTrustedOrigins.split(","));
252 | }
253 | if (trustedOrigins.filter((x) => !x).length) {
254 | throw new BetterAuthError(
255 | "A provided trusted origin is invalid, make sure your trusted origins list is properly defined.",
256 | );
257 | }
258 | return trustedOrigins;
259 | }
260 |
```
--------------------------------------------------------------------------------
/demo/nextjs/components/sign-in.tsx:
--------------------------------------------------------------------------------
```typescript
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import {
5 | Card,
6 | CardContent,
7 | CardHeader,
8 | CardTitle,
9 | CardDescription,
10 | CardFooter,
11 | } from "@/components/ui/card";
12 | import { Input } from "@/components/ui/input";
13 | import { Label } from "@/components/ui/label";
14 | import { Checkbox } from "@/components/ui/checkbox";
15 | import { useState, useTransition } from "react";
16 | import { Loader2, Key } from "lucide-react";
17 | import { client, signIn } from "@/lib/auth-client";
18 | import Link from "next/link";
19 | import { cn } from "@/lib/utils";
20 | import { useRouter, useSearchParams } from "next/navigation";
21 | import { toast } from "sonner";
22 | import { getCallbackURL } from "@/lib/shared";
23 |
24 | export default function SignIn() {
25 | const [email, setEmail] = useState("");
26 | const [password, setPassword] = useState("");
27 | const [loading, startTransition] = useTransition();
28 | const [rememberMe, setRememberMe] = useState(false);
29 | const router = useRouter();
30 | const params = useSearchParams();
31 |
32 | const LastUsedIndicator = () => (
33 | <span className="ml-auto absolute top-0 right-0 px-2 py-1 text-xs bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 rounded-md font-medium">
34 | Last Used
35 | </span>
36 | );
37 |
38 | return (
39 | <Card className="max-w-md rounded-none">
40 | <CardHeader>
41 | <CardTitle className="text-lg md:text-xl">Sign In</CardTitle>
42 | <CardDescription className="text-xs md:text-sm">
43 | Enter your email below to login to your account
44 | </CardDescription>
45 | </CardHeader>
46 | <CardContent>
47 | <div className="grid gap-4">
48 | <div className="grid gap-2">
49 | <Label htmlFor="email">Email</Label>
50 | <Input
51 | id="email"
52 | type="email"
53 | placeholder="[email protected]"
54 | required
55 | onChange={(e) => {
56 | setEmail(e.target.value);
57 | }}
58 | value={email}
59 | />
60 | </div>
61 |
62 | <div className="grid gap-2">
63 | <div className="flex items-center">
64 | <Label htmlFor="password">Password</Label>
65 | <Link
66 | href="/forget-password"
67 | className="ml-auto inline-block text-sm underline"
68 | >
69 | Forgot your password?
70 | </Link>
71 | </div>
72 |
73 | <Input
74 | id="password"
75 | type="password"
76 | placeholder="password"
77 | autoComplete="password"
78 | value={password}
79 | onChange={(e) => setPassword(e.target.value)}
80 | />
81 | </div>
82 |
83 | <div className="flex items-center gap-2">
84 | <Checkbox
85 | id="remember"
86 | onClick={() => {
87 | setRememberMe(!rememberMe);
88 | }}
89 | />
90 | <Label htmlFor="remember">Remember me</Label>
91 | </div>
92 |
93 | <Button
94 | type="submit"
95 | className="w-full flex items-center justify-center"
96 | disabled={loading}
97 | onClick={async () => {
98 | startTransition(async () => {
99 | await signIn.email(
100 | { email, password, rememberMe },
101 | {
102 | onSuccess(context) {
103 | toast.success("Successfully signed in");
104 | router.push(getCallbackURL(params));
105 | },
106 | onError(context) {
107 | toast.error(context.error.message);
108 | },
109 | },
110 | );
111 | });
112 | }}
113 | >
114 | <div className="flex items-center justify-center w-full relative">
115 | {loading ? (
116 | <Loader2 size={16} className="animate-spin" />
117 | ) : (
118 | "Login"
119 | )}
120 | {client.isLastUsedLoginMethod("email") && <LastUsedIndicator />}
121 | </div>
122 | </Button>
123 |
124 | <div
125 | className={cn(
126 | "w-full gap-2 flex items-center",
127 | "justify-between flex-col",
128 | )}
129 | >
130 | <Button
131 | variant="outline"
132 | className={cn("w-full gap-2 flex relative")}
133 | onClick={async () => {
134 | await signIn.social({
135 | provider: "google",
136 | callbackURL: "/dashboard",
137 | });
138 | }}
139 | >
140 | <svg
141 | xmlns="http://www.w3.org/2000/svg"
142 | width="0.98em"
143 | height="1em"
144 | viewBox="0 0 256 262"
145 | >
146 | <path
147 | fill="#4285F4"
148 | d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
149 | ></path>
150 | <path
151 | fill="#34A853"
152 | d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
153 | ></path>
154 | <path
155 | fill="#FBBC05"
156 | d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z"
157 | ></path>
158 | <path
159 | fill="#EB4335"
160 | d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
161 | ></path>
162 | </svg>
163 | <span>Sign in with Google</span>
164 | {client.isLastUsedLoginMethod("google") && <LastUsedIndicator />}
165 | </Button>
166 | <Button
167 | variant="outline"
168 | className={cn("w-full gap-2 flex items-center relative")}
169 | onClick={async () => {
170 | await signIn.social({
171 | provider: "github",
172 | callbackURL: "/dashboard",
173 | });
174 | }}
175 | >
176 | <svg
177 | xmlns="http://www.w3.org/2000/svg"
178 | width="1em"
179 | height="1em"
180 | viewBox="0 0 24 24"
181 | >
182 | <path
183 | fill="currentColor"
184 | d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2"
185 | ></path>
186 | </svg>
187 | <span>Sign in with GitHub</span>
188 | {client.isLastUsedLoginMethod("github") && <LastUsedIndicator />}
189 | </Button>
190 | <Button
191 | variant="outline"
192 | className={cn("w-full gap-2 flex items-center relative")}
193 | onClick={async () => {
194 | await signIn.social({
195 | provider: "microsoft",
196 | callbackURL: "/dashboard",
197 | });
198 | }}
199 | >
200 | <svg
201 | xmlns="http://www.w3.org/2000/svg"
202 | width="1em"
203 | height="1em"
204 | viewBox="0 0 24 24"
205 | >
206 | <path
207 | fill="currentColor"
208 | d="M2 3h9v9H2zm9 19H2v-9h9zM21 3v9h-9V3zm0 19h-9v-9h9z"
209 | ></path>
210 | </svg>
211 | <span>Sign in with Microsoft</span>
212 | {client.isLastUsedLoginMethod("microsoft") && (
213 | <LastUsedIndicator />
214 | )}
215 | </Button>
216 | <Button
217 | variant="outline"
218 | className={cn("w-full gap-2 flex items-center relative")}
219 | onClick={async () => {
220 | await signIn.passkey({
221 | fetchOptions: {
222 | onSuccess() {
223 | toast.success("Successfully signed in");
224 | router.push(getCallbackURL(params));
225 | },
226 | onError(context) {
227 | toast.error(
228 | "Authentication failed: " + context.error.message,
229 | );
230 | },
231 | },
232 | });
233 | }}
234 | >
235 | <Key size={16} />
236 | <span>Sign in with Passkey</span>
237 | {client.isLastUsedLoginMethod("passkey") && <LastUsedIndicator />}
238 | </Button>
239 | </div>
240 | </div>
241 | </CardContent>
242 | <CardFooter>
243 | <div className="flex justify-center w-full border-t pt-4">
244 | <p className="text-center text-xs text-neutral-500">
245 | built with{" "}
246 | <Link
247 | href="https://better-auth.com"
248 | className="underline"
249 | target="_blank"
250 | >
251 | <span className="dark:text-white/70 cursor-pointer">
252 | better-auth.
253 | </span>
254 | </Link>
255 | </p>
256 | </div>
257 | </CardFooter>
258 | </Card>
259 | );
260 | }
261 |
```
--------------------------------------------------------------------------------
/packages/better-auth/src/api/routes/reset-password.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, expect, vi } from "vitest";
2 | import { getTestInstance } from "../../test-utils/test-instance";
3 | import type { Account } from "../../types";
4 |
5 | describe("forget password", async (it) => {
6 | const mockSendEmail = vi.fn();
7 | const mockonPasswordReset = vi.fn();
8 | let token = "";
9 |
10 | const { client, testUser, db } = await getTestInstance(
11 | {
12 | emailAndPassword: {
13 | enabled: true,
14 | async sendResetPassword({ url }) {
15 | token = url.split("?")[0]!.split("/").pop() || "";
16 | await mockSendEmail();
17 | },
18 | onPasswordReset: async ({ user }) => {
19 | await mockonPasswordReset(user);
20 | },
21 | },
22 | },
23 | {
24 | testWith: "sqlite",
25 | },
26 | );
27 | it("should send a reset password email when enabled", async () => {
28 | await client.requestPasswordReset({
29 | email: testUser.email,
30 | redirectTo: "http://localhost:3000",
31 | });
32 | expect(token.length).toBeGreaterThan(10);
33 | });
34 |
35 | it("should fail on invalid password", async () => {
36 | const res = await client.resetPassword(
37 | {
38 | newPassword: "short",
39 | },
40 | {
41 | query: {
42 | token,
43 | },
44 | },
45 | );
46 | expect(res.error?.status).toBe(400);
47 | });
48 |
49 | it("should verify the token", async () => {
50 | const newPassword = "new-password";
51 | const res = await client.resetPassword(
52 | {
53 | newPassword,
54 | },
55 | {
56 | query: {
57 | token,
58 | },
59 | },
60 | );
61 | expect(res.data).toMatchObject({
62 | status: true,
63 | });
64 | });
65 |
66 | it("should update account's updatedAt when resetting password", async () => {
67 | // Create a new user to test with
68 | const newHeaders = new Headers();
69 | const signUpRes = await client.signUp.email({
70 | name: "Test Reset User",
71 | email: "[email protected]",
72 | password: "originalPassword123",
73 | fetchOptions: {
74 | onSuccess(ctx) {
75 | const setCookie = ctx.response.headers.get("set-cookie");
76 | if (setCookie) {
77 | newHeaders.set("cookie", setCookie);
78 | }
79 | },
80 | },
81 | });
82 |
83 | const userId = signUpRes.data?.user.id;
84 | expect(userId).toBeDefined();
85 |
86 | // Get initial account data
87 | const initialAccounts: Account[] = await db.findMany({
88 | model: "account",
89 | where: [
90 | {
91 | field: "userId",
92 | value: userId!,
93 | },
94 | {
95 | field: "providerId",
96 | value: "credential",
97 | },
98 | ],
99 | });
100 | expect(initialAccounts.length).toBe(1);
101 | const initialUpdatedAt = initialAccounts[0]!.updatedAt;
102 |
103 | // Request password reset
104 | let resetToken = "";
105 | await client.requestPasswordReset({
106 | email: "[email protected]",
107 | redirectTo: "http://localhost:3000",
108 | });
109 |
110 | // Extract token from mock send email
111 | expect(token).toBeDefined();
112 | resetToken = token;
113 |
114 | // Wait a bit to ensure time difference
115 | await new Promise((resolve) => setTimeout(resolve, 100));
116 |
117 | // Reset password
118 | const resetRes = await client.resetPassword({
119 | newPassword: "newResetPassword123",
120 | token: resetToken,
121 | });
122 | expect(resetRes.data?.status).toBe(true);
123 |
124 | // Get updated account data
125 | const updatedAccounts: Account[] = await db.findMany({
126 | model: "account",
127 | where: [
128 | {
129 | field: "userId",
130 | value: userId!,
131 | },
132 | {
133 | field: "providerId",
134 | value: "credential",
135 | },
136 | ],
137 | });
138 | expect(updatedAccounts.length).toBe(1);
139 | const newUpdatedAt = updatedAccounts[0]!.updatedAt;
140 |
141 | // Verify updatedAt was refreshed
142 | expect(newUpdatedAt).not.toBe(initialUpdatedAt);
143 | expect(new Date(newUpdatedAt).getTime()).toBeGreaterThan(
144 | new Date(initialUpdatedAt).getTime(),
145 | );
146 |
147 | // Verify user can sign in with new password
148 | const signInRes = await client.signIn.email({
149 | email: "[email protected]",
150 | password: "newResetPassword123",
151 | });
152 | expect(signInRes.data?.user).toBeDefined();
153 | });
154 |
155 | it("should sign-in with the new password", async () => {
156 | const withOldCred = await client.signIn.email({
157 | email: testUser.email,
158 | password: testUser.email,
159 | });
160 | expect(withOldCred.error?.status).toBe(401);
161 | const newCred = await client.signIn.email({
162 | email: testUser.email,
163 | password: "new-password",
164 | });
165 | expect(newCred.data?.user).toBeDefined();
166 | });
167 |
168 | it("shouldn't allow the token to be used twice", async () => {
169 | const newPassword = "new-password";
170 | const res = await client.resetPassword(
171 | {
172 | newPassword,
173 | },
174 | {
175 | query: {
176 | token,
177 | },
178 | },
179 | );
180 |
181 | expect(res.error?.status).toBe(400);
182 | });
183 |
184 | it("should expire", async () => {
185 | const { client, signInWithTestUser, testUser } = await getTestInstance({
186 | emailAndPassword: {
187 | enabled: true,
188 | async sendResetPassword({ token: _token }) {
189 | token = _token;
190 | await mockSendEmail();
191 | },
192 | resetPasswordTokenExpiresIn: 10,
193 | },
194 | });
195 | const { runWithUser } = await signInWithTestUser();
196 | await runWithUser(async () => {
197 | await client.requestPasswordReset({
198 | email: testUser.email,
199 | redirectTo: "/sign-in",
200 | });
201 | });
202 | vi.useFakeTimers();
203 | await vi.advanceTimersByTimeAsync(1000 * 9);
204 | const callbackRes = await client.$fetch("/reset-password/:token", {
205 | params: {
206 | token,
207 | },
208 | query: {
209 | callbackURL: "/cb",
210 | },
211 | onError(context) {
212 | const location = context.response.headers.get("location");
213 | expect(location).not.toContain("error");
214 | expect(location).toContain("token");
215 | },
216 | });
217 | const res = await client.resetPassword({
218 | newPassword: "new-password",
219 | token,
220 | });
221 | expect(res.data?.status).toBe(true);
222 | await runWithUser(async () => {
223 | await client.requestPasswordReset({
224 | email: testUser.email,
225 | redirectTo: "/sign-in",
226 | });
227 | });
228 | vi.useFakeTimers();
229 | await vi.advanceTimersByTimeAsync(1000 * 11);
230 | const res2 = await client.resetPassword({
231 | newPassword: "new-password",
232 | token,
233 | });
234 | expect(mockonPasswordReset).toHaveBeenCalled();
235 | expect(res2.error?.status).toBe(400);
236 | });
237 |
238 | it("should allow callbackURL to have multiple query params", async () => {
239 | let url = "";
240 |
241 | const { client, testUser } = await getTestInstance({
242 | emailAndPassword: {
243 | enabled: true,
244 | async sendResetPassword(context) {
245 | url = context.url;
246 | await mockSendEmail();
247 | },
248 | resetPasswordTokenExpiresIn: 10,
249 | },
250 | });
251 |
252 | const queryParams = "foo=bar&baz=qux";
253 | const redirectTo = `http://localhost:3000?${queryParams}`;
254 | const res = await client.requestPasswordReset({
255 | email: testUser.email,
256 | redirectTo,
257 | });
258 |
259 | expect(res.data?.status).toBe(true);
260 | expect(url).not.toContain(queryParams);
261 | expect(url).toContain(`callbackURL=${encodeURIComponent(redirectTo)}`);
262 | });
263 | });
264 |
265 | describe("revoke sessions on password reset", async (it) => {
266 | const mockSendEmail = vi.fn();
267 | let token = "";
268 |
269 | const { client, testUser, signInWithTestUser } = await getTestInstance(
270 | {
271 | emailAndPassword: {
272 | enabled: true,
273 | async sendResetPassword({ url }) {
274 | token = url.split("?")[0]!.split("/").pop() || "";
275 | await mockSendEmail();
276 | },
277 | revokeSessionsOnPasswordReset: true,
278 | },
279 | },
280 | {
281 | testWith: "sqlite",
282 | },
283 | );
284 |
285 | it("should revoke other sessions when revokeSessionsOnPasswordReset is enabled", async () => {
286 | const { runWithUser } = await signInWithTestUser();
287 |
288 | await client.requestPasswordReset({
289 | email: testUser.email,
290 | redirectTo: "http://localhost:3000",
291 | });
292 |
293 | await client.resetPassword(
294 | {
295 | newPassword: "new-password",
296 | },
297 | {
298 | query: {
299 | token,
300 | },
301 | },
302 | );
303 |
304 | await runWithUser(async () => {
305 | const sessionAttempt = await client.getSession();
306 | expect(sessionAttempt.data).toBeNull();
307 | });
308 | });
309 |
310 | it("should not revoke other sessions by default", async () => {
311 | const { client, testUser, signInWithTestUser } = await getTestInstance(
312 | {
313 | emailAndPassword: {
314 | enabled: true,
315 | async sendResetPassword({ url }) {
316 | token = url.split("?")[0]!.split("/").pop() || "";
317 | await mockSendEmail();
318 | },
319 | },
320 | },
321 | {
322 | testWith: "sqlite",
323 | },
324 | );
325 |
326 | const { runWithUser } = await signInWithTestUser();
327 |
328 | await client.requestPasswordReset({
329 | email: testUser.email,
330 | redirectTo: "http://localhost:3000",
331 | });
332 |
333 | await client.resetPassword(
334 | {
335 | newPassword: "new-password",
336 | },
337 | {
338 | query: {
339 | token,
340 | },
341 | },
342 | );
343 |
344 | await runWithUser(async () => {
345 | const sessionAttempt = await client.getSession();
346 | expect(sessionAttempt.data?.user).toBeDefined();
347 | });
348 | });
349 | });
350 |
```
--------------------------------------------------------------------------------
/packages/telemetry/src/telemetry.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { beforeEach, describe, expect, it, vi } from "vitest";
2 | import { createTelemetry } from "./index";
3 | import type { TelemetryEvent } from "./types";
4 |
5 | vi.mock("@better-fetch/fetch", () => ({
6 | betterFetch: vi.fn(async () => ({ status: 200 })),
7 | }));
8 |
9 | vi.mock("./project-id", () => ({
10 | getProjectId: vi.fn(async () => "anon-123"),
11 | }));
12 |
13 | vi.mock("./detectors/detect-runtime", () => ({
14 | detectRuntime: vi.fn(() => ({ name: "node", version: "test" })),
15 | detectEnvironment: vi.fn(() => "test"),
16 | }));
17 |
18 | vi.mock("./detectors/detect-database", () => ({
19 | detectDatabase: vi.fn(async () => ({ name: "postgresql", version: "1.0.0" })),
20 | }));
21 |
22 | vi.mock("./detectors/detect-framework", () => ({
23 | detectFramework: vi.fn(async () => ({ name: "next", version: "15.0.0" })),
24 | }));
25 |
26 | vi.mock("./detectors/detect-system-info", () => ({
27 | detectSystemInfo: vi.fn(() => ({
28 | systemPlatform: "darwin",
29 | systemRelease: "24.6.0",
30 | systemArchitecture: "arm64",
31 | cpuCount: 8,
32 | cpuModel: "Apple M3",
33 | cpuSpeed: 3200,
34 | memory: 16 * 1024 * 1024 * 1024,
35 | isDocker: false,
36 | isTTY: true,
37 | isWSL: false,
38 | isCI: false,
39 | })),
40 | isCI: vi.fn(() => false),
41 | }));
42 |
43 | vi.mock("./detectors/detect-project-info", () => ({
44 | detectPackageManager: vi.fn(() => ({ name: "pnpm", version: "9.0.0" })),
45 | }));
46 |
47 | beforeEach(() => {
48 | vi.resetModules();
49 | vi.clearAllMocks();
50 | process.env.BETTER_AUTH_TELEMETRY = "";
51 | process.env.BETTER_AUTH_TELEMETRY_DEBUG = "";
52 | });
53 |
54 | describe("telemetry", () => {
55 | it("publishes events when enabled", async () => {
56 | let event: TelemetryEvent | undefined;
57 | const track = vi.fn().mockImplementation(async (e) => {
58 | event = e;
59 | });
60 | await createTelemetry(
61 | {
62 | baseURL: "http://localhost.com", //this shouldn't be tracked
63 | appName: "test", //this shouldn't be tracked
64 | advanced: {
65 | cookiePrefix: "test", //this shouldn't be tracked - should set to true
66 | crossSubDomainCookies: {
67 | domain: ".test.com", //this shouldn't be tracked - should set to true
68 | enabled: true,
69 | },
70 | },
71 | telemetry: { enabled: true },
72 | },
73 | { customTrack: track, skipTestCheck: true },
74 | );
75 | expect(event).toMatchObject({
76 | type: "init",
77 | payload: {
78 | config: {
79 | emailVerification: {
80 | sendVerificationEmail: false,
81 | sendOnSignUp: false,
82 | sendOnSignIn: false,
83 | autoSignInAfterVerification: false,
84 | expiresIn: undefined,
85 | onEmailVerification: false,
86 | afterEmailVerification: false,
87 | },
88 | emailAndPassword: {
89 | enabled: false,
90 | disableSignUp: false,
91 | requireEmailVerification: false,
92 | maxPasswordLength: undefined,
93 | minPasswordLength: undefined,
94 | sendResetPassword: false,
95 | resetPasswordTokenExpiresIn: undefined,
96 | onPasswordReset: false,
97 | password: { hash: false, verify: false },
98 | autoSignIn: false,
99 | revokeSessionsOnPasswordReset: false,
100 | },
101 | socialProviders: [],
102 | plugins: undefined,
103 | user: {
104 | modelName: undefined,
105 | fields: undefined,
106 | additionalFields: undefined,
107 | changeEmail: {
108 | enabled: undefined,
109 | sendChangeEmailVerification: false,
110 | },
111 | },
112 | verification: {
113 | modelName: undefined,
114 | disableCleanup: undefined,
115 | fields: undefined,
116 | },
117 | session: {
118 | modelName: undefined,
119 | additionalFields: undefined,
120 | cookieCache: { enabled: undefined, maxAge: undefined },
121 | disableSessionRefresh: undefined,
122 | expiresIn: undefined,
123 | fields: undefined,
124 | freshAge: undefined,
125 | preserveSessionInDatabase: undefined,
126 | storeSessionInDatabase: undefined,
127 | updateAge: undefined,
128 | },
129 | account: {
130 | modelName: undefined,
131 | fields: undefined,
132 | encryptOAuthTokens: undefined,
133 | updateAccountOnSignIn: undefined,
134 | accountLinking: {
135 | enabled: undefined,
136 | trustedProviders: undefined,
137 | updateUserInfoOnLink: undefined,
138 | allowUnlinkingAll: undefined,
139 | },
140 | },
141 | hooks: { after: false, before: false },
142 | secondaryStorage: false,
143 | advanced: {
144 | cookiePrefix: true,
145 | cookies: false,
146 | crossSubDomainCookies: {
147 | domain: true,
148 | enabled: true,
149 | additionalCookies: undefined,
150 | },
151 | database: {
152 | useNumberId: false,
153 | generateId: undefined,
154 | defaultFindManyLimit: undefined,
155 | },
156 | useSecureCookies: undefined,
157 | ipAddress: {
158 | disableIpTracking: undefined,
159 | ipAddressHeaders: undefined,
160 | },
161 | disableCSRFCheck: undefined,
162 | cookieAttributes: {
163 | expires: undefined,
164 | secure: undefined,
165 | sameSite: undefined,
166 | domain: false,
167 | path: undefined,
168 | httpOnly: undefined,
169 | },
170 | },
171 | trustedOrigins: undefined,
172 | rateLimit: {
173 | storage: undefined,
174 | modelName: undefined,
175 | window: undefined,
176 | customStorage: false,
177 | enabled: undefined,
178 | max: undefined,
179 | },
180 | onAPIError: {
181 | errorURL: undefined,
182 | onError: false,
183 | throw: undefined,
184 | },
185 | logger: { disabled: undefined, level: undefined, log: false },
186 | databaseHooks: {
187 | user: {
188 | create: {
189 | after: false,
190 | before: false,
191 | },
192 | update: {
193 | after: false,
194 | before: false,
195 | },
196 | },
197 | session: {
198 | create: {
199 | after: false,
200 | before: false,
201 | },
202 | update: {
203 | after: false,
204 | before: false,
205 | },
206 | },
207 | account: {
208 | create: {
209 | after: false,
210 | before: false,
211 | },
212 | update: {
213 | after: false,
214 | before: false,
215 | },
216 | },
217 | verification: {
218 | create: {
219 | after: false,
220 | before: false,
221 | },
222 | update: {
223 | after: false,
224 | before: false,
225 | },
226 | },
227 | },
228 | },
229 | runtime: { name: "node", version: "test" },
230 | database: { name: "postgresql", version: "1.0.0" },
231 | framework: { name: "next", version: "15.0.0" },
232 | environment: "test",
233 | systemInfo: {
234 | systemPlatform: "darwin",
235 | systemRelease: "24.6.0",
236 | systemArchitecture: "arm64",
237 | cpuCount: 8,
238 | cpuModel: "Apple M3",
239 | cpuSpeed: 3200,
240 | memory: 17179869184,
241 | isDocker: false,
242 | isTTY: true,
243 | isWSL: false,
244 | isCI: false,
245 | },
246 | packageManager: { name: "pnpm", version: "9.0.0" },
247 | },
248 | anonymousId: "anon-123",
249 | });
250 | });
251 |
252 | it("does not publish when disabled via env", async () => {
253 | process.env.BETTER_AUTH_TELEMETRY = "false";
254 | let event: TelemetryEvent | undefined;
255 | const track = vi.fn().mockImplementation(async (e) => {
256 | event = e;
257 | });
258 | await createTelemetry(
259 | {
260 | baseURL: "http://localhost",
261 | },
262 | { customTrack: track, skipTestCheck: true },
263 | );
264 | expect(event).toBeUndefined();
265 | expect(track).not.toBeCalled();
266 | });
267 |
268 | it("does not publish when disabled via option", async () => {
269 | let event: TelemetryEvent | undefined;
270 | const track = vi.fn().mockImplementation(async (e) => {
271 | event = e;
272 | });
273 | await createTelemetry(
274 | {
275 | baseURL: "http://localhost",
276 | telemetry: { enabled: false },
277 | },
278 | { customTrack: track, skipTestCheck: true },
279 | );
280 | expect(event).toBeUndefined();
281 | expect(track).not.toBeCalled();
282 | });
283 |
284 | it("shouldn't fail cause track isn't being reached", async () => {
285 | await expect(
286 | createTelemetry(
287 | {
288 | baseURL: "http://localhost",
289 | telemetry: { enabled: true },
290 | },
291 | {
292 | customTrack() {
293 | throw new Error("test");
294 | },
295 | skipTestCheck: true,
296 | },
297 | ),
298 | ).resolves.not.throw(Error);
299 | });
300 |
301 | it("initializes without Node built-ins in edge-like env (no process.cwd)", async () => {
302 | const originalProcess = globalThis.process;
303 | try {
304 | // Simulate an edge runtime where process exists minimally but has no cwd
305 | // so utils/package-json won't try to import fs/path
306 | (globalThis as any).process = { env: {} } as any;
307 | const track = vi.fn();
308 | await expect(
309 | createTelemetry(
310 | { baseURL: "https://example.com", telemetry: { enabled: true } },
311 | { customTrack: track, skipTestCheck: true },
312 | ),
313 | ).resolves.not.toThrow();
314 | // Should still attempt to publish init event
315 | expect(track).toHaveBeenCalled();
316 | } finally {
317 | // restore
318 | (globalThis as any).process = originalProcess as any;
319 | }
320 | });
321 | });
322 |
```
--------------------------------------------------------------------------------
/packages/better-auth/src/api/to-auth-endpoints.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {
2 | APIError,
3 | type EndpointContext,
4 | type EndpointOptions,
5 | type InputContext,
6 | toResponse,
7 | } from "better-call";
8 | import type { AuthEndpoint, AuthMiddleware } from "@better-auth/core/api";
9 | import { createDefu } from "defu";
10 | import { shouldPublishLog } from "@better-auth/core/env";
11 | import type { AuthContext, HookEndpointContext } from "@better-auth/core";
12 | import { runWithEndpointContext } from "@better-auth/core/context";
13 |
14 | type InternalContext = Partial<
15 | InputContext<string, any> & EndpointContext<string, any>
16 | > & {
17 | path: string;
18 | asResponse?: boolean;
19 | context: AuthContext & {
20 | logger: AuthContext["logger"];
21 | returned?: unknown;
22 | responseHeaders?: Headers;
23 | };
24 | };
25 |
26 | const defuReplaceArrays = createDefu((obj, key, value) => {
27 | if (Array.isArray(obj[key]) && Array.isArray(value)) {
28 | obj[key] = value;
29 | return true;
30 | }
31 | });
32 |
33 | export function toAuthEndpoints<
34 | const E extends Record<string, Omit<AuthEndpoint, "wrap">>,
35 | >(endpoints: E, ctx: AuthContext | Promise<AuthContext>): E {
36 | const api: Record<
37 | string,
38 | ((
39 | context: EndpointContext<string, any> & InputContext<string, any>,
40 | ) => Promise<any>) & {
41 | path?: string;
42 | options?: EndpointOptions;
43 | }
44 | > = {};
45 |
46 | for (const [key, endpoint] of Object.entries(endpoints)) {
47 | api[key] = async (
48 | context: Partial<
49 | InputContext<string, any> & EndpointContext<string, any>
50 | >,
51 | ) => {
52 | const authContext = await ctx;
53 | let internalContext: InternalContext = {
54 | ...context,
55 | context: {
56 | ...authContext,
57 | returned: undefined,
58 | responseHeaders: undefined,
59 | session: null,
60 | },
61 | path: endpoint.path,
62 | headers: context?.headers ? new Headers(context?.headers) : undefined,
63 | };
64 | return runWithEndpointContext(internalContext, async () => {
65 | const { beforeHooks, afterHooks } = getHooks(authContext);
66 | const before = await runBeforeHooks(internalContext, beforeHooks);
67 | /**
68 | * If `before.context` is returned, it should
69 | * get merged with the original context
70 | */
71 | if (
72 | "context" in before &&
73 | before.context &&
74 | typeof before.context === "object"
75 | ) {
76 | const { headers, ...rest } = before.context as {
77 | headers: Headers;
78 | };
79 | /**
80 | * Headers should be merged differently
81 | * so the hook doesn't override the whole
82 | * header
83 | */
84 | if (headers) {
85 | headers.forEach((value, key) => {
86 | (internalContext.headers as Headers).set(key, value);
87 | });
88 | }
89 | internalContext = defuReplaceArrays(rest, internalContext);
90 | } else if (before) {
91 | /* Return before hook response if it's anything other than a context return */
92 | return context?.asResponse
93 | ? toResponse(before, {
94 | headers: context?.headers,
95 | })
96 | : context?.returnHeaders
97 | ? {
98 | headers: context?.headers,
99 | response: before,
100 | }
101 | : before;
102 | }
103 |
104 | internalContext.asResponse = false;
105 | internalContext.returnHeaders = true;
106 | const result = (await runWithEndpointContext(internalContext, () =>
107 | (endpoint as any)(internalContext as any),
108 | ).catch((e: any) => {
109 | if (e instanceof APIError) {
110 | /**
111 | * API Errors from response are caught
112 | * and returned to hooks
113 | */
114 | return {
115 | response: e,
116 | headers: e.headers ? new Headers(e.headers) : null,
117 | };
118 | }
119 | throw e;
120 | })) as {
121 | headers: Headers;
122 | response: any;
123 | };
124 |
125 | //if response object is returned we skip after hooks and post processing
126 | if (result && result instanceof Response) {
127 | return result;
128 | }
129 |
130 | internalContext.context.returned = result.response;
131 | internalContext.context.responseHeaders = result.headers;
132 |
133 | const after = await runAfterHooks(internalContext, afterHooks);
134 |
135 | if (after.response) {
136 | result.response = after.response;
137 | }
138 |
139 | if (
140 | result.response instanceof APIError &&
141 | shouldPublishLog(authContext.logger.level, "debug")
142 | ) {
143 | // inherit stack from errorStack if debug mode is enabled
144 | result.response.stack = result.response.errorStack;
145 | }
146 |
147 | if (result.response instanceof APIError && !context?.asResponse) {
148 | throw result.response;
149 | }
150 |
151 | const response = context?.asResponse
152 | ? toResponse(result.response, {
153 | headers: result.headers,
154 | })
155 | : context?.returnHeaders
156 | ? {
157 | headers: result.headers,
158 | response: result.response,
159 | }
160 | : result.response;
161 | return response;
162 | });
163 | };
164 | api[key].path = endpoint.path;
165 | api[key].options = endpoint.options;
166 | }
167 | return api as unknown as E;
168 | }
169 |
170 | async function runBeforeHooks(
171 | context: InternalContext,
172 | hooks: {
173 | matcher: (context: HookEndpointContext) => boolean;
174 | handler: AuthMiddleware;
175 | }[],
176 | ) {
177 | let modifiedContext: Partial<InternalContext> = {};
178 |
179 | for (const hook of hooks) {
180 | if (hook.matcher(context)) {
181 | const result = await hook
182 | .handler({
183 | ...context,
184 | returnHeaders: false,
185 | })
186 | .catch((e: unknown) => {
187 | if (
188 | e instanceof APIError &&
189 | shouldPublishLog(context.context.logger.level, "debug")
190 | ) {
191 | // inherit stack from errorStack if debug mode is enabled
192 | e.stack = e.errorStack;
193 | }
194 | throw e;
195 | });
196 | if (result && typeof result === "object") {
197 | if ("context" in result && typeof result.context === "object") {
198 | const { headers, ...rest } =
199 | result.context as Partial<InternalContext>;
200 | if (headers instanceof Headers) {
201 | if (modifiedContext.headers) {
202 | headers.forEach((value, key) => {
203 | modifiedContext.headers?.set(key, value);
204 | });
205 | } else {
206 | modifiedContext.headers = headers;
207 | }
208 | }
209 | modifiedContext = defuReplaceArrays(rest, modifiedContext);
210 |
211 | continue;
212 | }
213 | return result;
214 | }
215 | }
216 | }
217 | return { context: modifiedContext };
218 | }
219 |
220 | async function runAfterHooks(
221 | context: InternalContext,
222 | hooks: {
223 | matcher: (context: HookEndpointContext) => boolean;
224 | handler: AuthMiddleware;
225 | }[],
226 | ) {
227 | for (const hook of hooks) {
228 | if (hook.matcher(context)) {
229 | const result = (await hook.handler(context).catch((e) => {
230 | if (e instanceof APIError) {
231 | if (shouldPublishLog(context.context.logger.level, "debug")) {
232 | // inherit stack from errorStack if debug mode is enabled
233 | e.stack = e.errorStack;
234 | }
235 | return {
236 | response: e,
237 | headers: e.headers ? new Headers(e.headers) : null,
238 | };
239 | }
240 | throw e;
241 | })) as {
242 | response: any;
243 | headers: Headers;
244 | };
245 | if (result.headers) {
246 | result.headers.forEach((value, key) => {
247 | if (!context.context.responseHeaders) {
248 | context.context.responseHeaders = new Headers({
249 | [key]: value,
250 | });
251 | } else {
252 | if (key.toLowerCase() === "set-cookie") {
253 | context.context.responseHeaders.append(key, value);
254 | } else {
255 | context.context.responseHeaders.set(key, value);
256 | }
257 | }
258 | });
259 | }
260 | if (result.response) {
261 | context.context.returned = result.response;
262 | }
263 | }
264 | }
265 | return {
266 | response: context.context.returned,
267 | headers: context.context.responseHeaders,
268 | };
269 | }
270 |
271 | function getHooks(authContext: AuthContext) {
272 | const plugins = authContext.options.plugins || [];
273 | const beforeHooks: {
274 | matcher: (context: HookEndpointContext) => boolean;
275 | handler: AuthMiddleware;
276 | }[] = [];
277 | const afterHooks: {
278 | matcher: (context: HookEndpointContext) => boolean;
279 | handler: AuthMiddleware;
280 | }[] = [];
281 | if (authContext.options.hooks?.before) {
282 | beforeHooks.push({
283 | matcher: () => true,
284 | handler: authContext.options.hooks.before,
285 | });
286 | }
287 | if (authContext.options.hooks?.after) {
288 | afterHooks.push({
289 | matcher: () => true,
290 | handler: authContext.options.hooks.after,
291 | });
292 | }
293 | const pluginBeforeHooks = plugins
294 | .map((plugin) => {
295 | if (plugin.hooks?.before) {
296 | return plugin.hooks.before;
297 | }
298 | })
299 | .filter((plugin) => plugin !== undefined)
300 | .flat();
301 | const pluginAfterHooks = plugins
302 | .map((plugin) => {
303 | if (plugin.hooks?.after) {
304 | return plugin.hooks.after;
305 | }
306 | })
307 | .filter((plugin) => plugin !== undefined)
308 | .flat();
309 |
310 | /**
311 | * Add plugin added hooks at last
312 | */
313 | pluginBeforeHooks.length && beforeHooks.push(...pluginBeforeHooks);
314 | pluginAfterHooks.length && afterHooks.push(...pluginAfterHooks);
315 |
316 | return {
317 | beforeHooks,
318 | afterHooks,
319 | };
320 | }
321 |
```
--------------------------------------------------------------------------------
/packages/cli/src/utils/get-config.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { loadConfig } from "c12";
2 | import type { BetterAuthOptions } from "better-auth";
3 | import { logger } from "better-auth";
4 | import path from "path";
5 | // @ts-expect-error
6 | import babelPresetTypeScript from "@babel/preset-typescript";
7 | // @ts-expect-error
8 | import babelPresetReact from "@babel/preset-react";
9 | import fs, { existsSync } from "fs";
10 | import { BetterAuthError } from "better-auth";
11 | import { addSvelteKitEnvModules } from "./add-svelte-kit-env-modules";
12 | import { getTsconfigInfo } from "./get-tsconfig-info";
13 | import type { JitiOptions } from "jiti";
14 |
15 | let possiblePaths = [
16 | "auth.ts",
17 | "auth.tsx",
18 | "auth.js",
19 | "auth.jsx",
20 | "auth.server.js",
21 | "auth.server.ts",
22 | ];
23 |
24 | possiblePaths = [
25 | ...possiblePaths,
26 | ...possiblePaths.map((it) => `lib/server/${it}`),
27 | ...possiblePaths.map((it) => `server/${it}`),
28 | ...possiblePaths.map((it) => `lib/${it}`),
29 | ...possiblePaths.map((it) => `utils/${it}`),
30 | ];
31 | possiblePaths = [
32 | ...possiblePaths,
33 | ...possiblePaths.map((it) => `src/${it}`),
34 | ...possiblePaths.map((it) => `app/${it}`),
35 | ];
36 |
37 | function resolveReferencePath(configDir: string, refPath: string): string {
38 | const resolvedPath = path.resolve(configDir, refPath);
39 |
40 | // If it ends with .json, treat as direct file reference
41 | if (refPath.endsWith(".json")) {
42 | return resolvedPath;
43 | }
44 |
45 | // If the exact path exists and is a file, use it
46 | if (fs.existsSync(resolvedPath)) {
47 | try {
48 | const stats = fs.statSync(resolvedPath);
49 | if (stats.isFile()) {
50 | return resolvedPath;
51 | }
52 | } catch {
53 | // Fall through to directory handling
54 | }
55 | }
56 |
57 | // Otherwise, assume directory reference
58 | return path.resolve(configDir, refPath, "tsconfig.json");
59 | }
60 |
61 | function getPathAliasesRecursive(
62 | tsconfigPath: string,
63 | visited = new Set<string>(),
64 | ): Record<string, string> {
65 | if (visited.has(tsconfigPath)) {
66 | return {};
67 | }
68 | visited.add(tsconfigPath);
69 |
70 | if (!fs.existsSync(tsconfigPath)) {
71 | logger.warn(`Referenced tsconfig not found: ${tsconfigPath}`);
72 | return {};
73 | }
74 |
75 | try {
76 | const tsConfig = getTsconfigInfo(undefined, tsconfigPath);
77 | const { paths = {}, baseUrl = "." } = tsConfig.compilerOptions || {};
78 | const result: Record<string, string> = {};
79 |
80 | const configDir = path.dirname(tsconfigPath);
81 | const obj = Object.entries(paths) as [string, string[]][];
82 | for (const [alias, aliasPaths] of obj) {
83 | for (const aliasedPath of aliasPaths) {
84 | const resolvedBaseUrl = path.resolve(configDir, baseUrl);
85 | const finalAlias = alias.slice(-1) === "*" ? alias.slice(0, -1) : alias;
86 | const finalAliasedPath =
87 | aliasedPath.slice(-1) === "*"
88 | ? aliasedPath.slice(0, -1)
89 | : aliasedPath;
90 |
91 | result[finalAlias || ""] = path.join(resolvedBaseUrl, finalAliasedPath);
92 | }
93 | }
94 |
95 | if (tsConfig.references) {
96 | for (const ref of tsConfig.references) {
97 | const refPath = resolveReferencePath(configDir, ref.path);
98 | const refAliases = getPathAliasesRecursive(refPath, visited);
99 | for (const [alias, aliasPath] of Object.entries(refAliases)) {
100 | if (!(alias in result)) {
101 | result[alias] = aliasPath;
102 | }
103 | }
104 | }
105 | }
106 |
107 | return result;
108 | } catch (error) {
109 | logger.warn(`Error parsing tsconfig at ${tsconfigPath}: ${error}`);
110 | return {};
111 | }
112 | }
113 |
114 | function getPathAliases(cwd: string): Record<string, string> | null {
115 | const tsConfigPath = path.join(cwd, "tsconfig.json");
116 | if (!fs.existsSync(tsConfigPath)) {
117 | return null;
118 | }
119 | try {
120 | const result = getPathAliasesRecursive(tsConfigPath);
121 | addSvelteKitEnvModules(result);
122 | return result;
123 | } catch (error) {
124 | console.error(error);
125 | throw new BetterAuthError("Error parsing tsconfig.json");
126 | }
127 | }
128 | /**
129 | * .tsx files are not supported by Jiti.
130 | */
131 | const jitiOptions = (cwd: string): JitiOptions => {
132 | const alias = getPathAliases(cwd) || {};
133 | return {
134 | transformOptions: {
135 | babel: {
136 | presets: [
137 | [
138 | babelPresetTypeScript,
139 | {
140 | isTSX: true,
141 | allExtensions: true,
142 | },
143 | ],
144 | [babelPresetReact, { runtime: "automatic" }],
145 | ],
146 | },
147 | },
148 | extensions: [".ts", ".tsx", ".js", ".jsx"],
149 | alias,
150 | };
151 | };
152 |
153 | const isDefaultExport = (
154 | object: Record<string, unknown>,
155 | ): object is BetterAuthOptions => {
156 | return (
157 | typeof object === "object" &&
158 | object !== null &&
159 | !Array.isArray(object) &&
160 | Object.keys(object).length > 0 &&
161 | "options" in object
162 | );
163 | };
164 | export async function getConfig({
165 | cwd,
166 | configPath,
167 | shouldThrowOnError = false,
168 | }: {
169 | cwd: string;
170 | configPath?: string;
171 | shouldThrowOnError?: boolean;
172 | }) {
173 | try {
174 | let configFile: BetterAuthOptions | null = null;
175 | if (configPath) {
176 | let resolvedPath: string = path.join(cwd, configPath);
177 | if (existsSync(configPath)) resolvedPath = configPath; // If the configPath is a file, use it as is, as it means the path wasn't relative.
178 | const { config } = await loadConfig<
179 | | {
180 | auth: {
181 | options: BetterAuthOptions;
182 | };
183 | }
184 | | {
185 | options: BetterAuthOptions;
186 | }
187 | >({
188 | configFile: resolvedPath,
189 | dotenv: true,
190 | jitiOptions: jitiOptions(cwd),
191 | });
192 | if (!("auth" in config) && !isDefaultExport(config)) {
193 | if (shouldThrowOnError) {
194 | throw new Error(
195 | `Couldn't read your auth config in ${resolvedPath}. Make sure to default export your auth instance or to export as a variable named auth.`,
196 | );
197 | }
198 | logger.error(
199 | `[#better-auth]: Couldn't read your auth config in ${resolvedPath}. Make sure to default export your auth instance or to export as a variable named auth.`,
200 | );
201 | process.exit(1);
202 | }
203 | configFile = "auth" in config ? config.auth?.options : config.options;
204 | }
205 |
206 | if (!configFile) {
207 | for (const possiblePath of possiblePaths) {
208 | try {
209 | const { config } = await loadConfig<{
210 | auth: {
211 | options: BetterAuthOptions;
212 | };
213 | default?: {
214 | options: BetterAuthOptions;
215 | };
216 | }>({
217 | configFile: possiblePath,
218 | jitiOptions: jitiOptions(cwd),
219 | });
220 | const hasConfig = Object.keys(config).length > 0;
221 | if (hasConfig) {
222 | configFile =
223 | config.auth?.options || config.default?.options || null;
224 | if (!configFile) {
225 | if (shouldThrowOnError) {
226 | throw new Error(
227 | "Couldn't read your auth config. Make sure to default export your auth instance or to export as a variable named auth.",
228 | );
229 | }
230 | logger.error("[#better-auth]: Couldn't read your auth config.");
231 | console.log("");
232 | logger.info(
233 | "[#better-auth]: Make sure to default export your auth instance or to export as a variable named auth.",
234 | );
235 | process.exit(1);
236 | }
237 | break;
238 | }
239 | } catch (e) {
240 | if (
241 | typeof e === "object" &&
242 | e &&
243 | "message" in e &&
244 | typeof e.message === "string" &&
245 | e.message.includes(
246 | "This module cannot be imported from a Client Component module",
247 | )
248 | ) {
249 | if (shouldThrowOnError) {
250 | throw new Error(
251 | `Please remove import 'server-only' from your auth config file temporarily. The CLI cannot resolve the configuration with it included. You can re-add it after running the CLI.`,
252 | );
253 | }
254 | logger.error(
255 | `Please remove import 'server-only' from your auth config file temporarily. The CLI cannot resolve the configuration with it included. You can re-add it after running the CLI.`,
256 | );
257 | process.exit(1);
258 | }
259 | if (shouldThrowOnError) {
260 | throw e;
261 | }
262 | logger.error("[#better-auth]: Couldn't read your auth config.", e);
263 | process.exit(1);
264 | }
265 | }
266 | }
267 | return configFile;
268 | } catch (e) {
269 | if (
270 | typeof e === "object" &&
271 | e &&
272 | "message" in e &&
273 | typeof e.message === "string" &&
274 | e.message.includes(
275 | "This module cannot be imported from a Client Component module",
276 | )
277 | ) {
278 | if (shouldThrowOnError) {
279 | throw new Error(
280 | `Please remove import 'server-only' from your auth config file temporarily. The CLI cannot resolve the configuration with it included. You can re-add it after running the CLI.`,
281 | );
282 | }
283 | logger.error(
284 | `Please remove import 'server-only' from your auth config file temporarily. The CLI cannot resolve the configuration with it included. You can re-add it after running the CLI.`,
285 | );
286 | process.exit(1);
287 | }
288 | if (shouldThrowOnError) {
289 | throw e;
290 | }
291 |
292 | logger.error("Couldn't read your auth config.", e);
293 | process.exit(1);
294 | }
295 | }
296 |
297 | export { possiblePaths };
298 |
```
--------------------------------------------------------------------------------
/e2e/smoke/test/fixtures/cloudflare/drizzle/meta/0000_snapshot.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "version": "6",
3 | "dialect": "sqlite",
4 | "id": "dde09aa0-ff07-4e38-a49a-742a3c2b7af4",
5 | "prevId": "00000000-0000-0000-0000-000000000000",
6 | "tables": {
7 | "account": {
8 | "name": "account",
9 | "columns": {
10 | "id": {
11 | "name": "id",
12 | "type": "text",
13 | "primaryKey": true,
14 | "notNull": true,
15 | "autoincrement": false
16 | },
17 | "account_id": {
18 | "name": "account_id",
19 | "type": "text",
20 | "primaryKey": false,
21 | "notNull": true,
22 | "autoincrement": false
23 | },
24 | "provider_id": {
25 | "name": "provider_id",
26 | "type": "text",
27 | "primaryKey": false,
28 | "notNull": true,
29 | "autoincrement": false
30 | },
31 | "user_id": {
32 | "name": "user_id",
33 | "type": "text",
34 | "primaryKey": false,
35 | "notNull": true,
36 | "autoincrement": false
37 | },
38 | "access_token": {
39 | "name": "access_token",
40 | "type": "text",
41 | "primaryKey": false,
42 | "notNull": false,
43 | "autoincrement": false
44 | },
45 | "refresh_token": {
46 | "name": "refresh_token",
47 | "type": "text",
48 | "primaryKey": false,
49 | "notNull": false,
50 | "autoincrement": false
51 | },
52 | "id_token": {
53 | "name": "id_token",
54 | "type": "text",
55 | "primaryKey": false,
56 | "notNull": false,
57 | "autoincrement": false
58 | },
59 | "access_token_expires_at": {
60 | "name": "access_token_expires_at",
61 | "type": "integer",
62 | "primaryKey": false,
63 | "notNull": false,
64 | "autoincrement": false
65 | },
66 | "refresh_token_expires_at": {
67 | "name": "refresh_token_expires_at",
68 | "type": "integer",
69 | "primaryKey": false,
70 | "notNull": false,
71 | "autoincrement": false
72 | },
73 | "scope": {
74 | "name": "scope",
75 | "type": "text",
76 | "primaryKey": false,
77 | "notNull": false,
78 | "autoincrement": false
79 | },
80 | "password": {
81 | "name": "password",
82 | "type": "text",
83 | "primaryKey": false,
84 | "notNull": false,
85 | "autoincrement": false
86 | },
87 | "created_at": {
88 | "name": "created_at",
89 | "type": "integer",
90 | "primaryKey": false,
91 | "notNull": true,
92 | "autoincrement": false
93 | },
94 | "updated_at": {
95 | "name": "updated_at",
96 | "type": "integer",
97 | "primaryKey": false,
98 | "notNull": true,
99 | "autoincrement": false
100 | }
101 | },
102 | "indexes": {},
103 | "foreignKeys": {
104 | "account_user_id_user_id_fk": {
105 | "name": "account_user_id_user_id_fk",
106 | "tableFrom": "account",
107 | "tableTo": "user",
108 | "columnsFrom": ["user_id"],
109 | "columnsTo": ["id"],
110 | "onDelete": "cascade",
111 | "onUpdate": "no action"
112 | }
113 | },
114 | "compositePrimaryKeys": {},
115 | "uniqueConstraints": {},
116 | "checkConstraints": {}
117 | },
118 | "session": {
119 | "name": "session",
120 | "columns": {
121 | "id": {
122 | "name": "id",
123 | "type": "text",
124 | "primaryKey": true,
125 | "notNull": true,
126 | "autoincrement": false
127 | },
128 | "expires_at": {
129 | "name": "expires_at",
130 | "type": "integer",
131 | "primaryKey": false,
132 | "notNull": true,
133 | "autoincrement": false
134 | },
135 | "token": {
136 | "name": "token",
137 | "type": "text",
138 | "primaryKey": false,
139 | "notNull": true,
140 | "autoincrement": false
141 | },
142 | "created_at": {
143 | "name": "created_at",
144 | "type": "integer",
145 | "primaryKey": false,
146 | "notNull": true,
147 | "autoincrement": false
148 | },
149 | "updated_at": {
150 | "name": "updated_at",
151 | "type": "integer",
152 | "primaryKey": false,
153 | "notNull": true,
154 | "autoincrement": false
155 | },
156 | "ip_address": {
157 | "name": "ip_address",
158 | "type": "text",
159 | "primaryKey": false,
160 | "notNull": false,
161 | "autoincrement": false
162 | },
163 | "user_agent": {
164 | "name": "user_agent",
165 | "type": "text",
166 | "primaryKey": false,
167 | "notNull": false,
168 | "autoincrement": false
169 | },
170 | "user_id": {
171 | "name": "user_id",
172 | "type": "text",
173 | "primaryKey": false,
174 | "notNull": true,
175 | "autoincrement": false
176 | }
177 | },
178 | "indexes": {
179 | "session_token_unique": {
180 | "name": "session_token_unique",
181 | "columns": ["token"],
182 | "isUnique": true
183 | }
184 | },
185 | "foreignKeys": {
186 | "session_user_id_user_id_fk": {
187 | "name": "session_user_id_user_id_fk",
188 | "tableFrom": "session",
189 | "tableTo": "user",
190 | "columnsFrom": ["user_id"],
191 | "columnsTo": ["id"],
192 | "onDelete": "cascade",
193 | "onUpdate": "no action"
194 | }
195 | },
196 | "compositePrimaryKeys": {},
197 | "uniqueConstraints": {},
198 | "checkConstraints": {}
199 | },
200 | "user": {
201 | "name": "user",
202 | "columns": {
203 | "id": {
204 | "name": "id",
205 | "type": "text",
206 | "primaryKey": true,
207 | "notNull": true,
208 | "autoincrement": false
209 | },
210 | "name": {
211 | "name": "name",
212 | "type": "text",
213 | "primaryKey": false,
214 | "notNull": true,
215 | "autoincrement": false
216 | },
217 | "email": {
218 | "name": "email",
219 | "type": "text",
220 | "primaryKey": false,
221 | "notNull": true,
222 | "autoincrement": false
223 | },
224 | "email_verified": {
225 | "name": "email_verified",
226 | "type": "integer",
227 | "primaryKey": false,
228 | "notNull": true,
229 | "autoincrement": false
230 | },
231 | "image": {
232 | "name": "image",
233 | "type": "text",
234 | "primaryKey": false,
235 | "notNull": false,
236 | "autoincrement": false
237 | },
238 | "created_at": {
239 | "name": "created_at",
240 | "type": "integer",
241 | "primaryKey": false,
242 | "notNull": true,
243 | "autoincrement": false
244 | },
245 | "updated_at": {
246 | "name": "updated_at",
247 | "type": "integer",
248 | "primaryKey": false,
249 | "notNull": true,
250 | "autoincrement": false
251 | }
252 | },
253 | "indexes": {
254 | "user_email_unique": {
255 | "name": "user_email_unique",
256 | "columns": ["email"],
257 | "isUnique": true
258 | }
259 | },
260 | "foreignKeys": {},
261 | "compositePrimaryKeys": {},
262 | "uniqueConstraints": {},
263 | "checkConstraints": {}
264 | },
265 | "verification": {
266 | "name": "verification",
267 | "columns": {
268 | "id": {
269 | "name": "id",
270 | "type": "text",
271 | "primaryKey": true,
272 | "notNull": true,
273 | "autoincrement": false
274 | },
275 | "identifier": {
276 | "name": "identifier",
277 | "type": "text",
278 | "primaryKey": false,
279 | "notNull": true,
280 | "autoincrement": false
281 | },
282 | "value": {
283 | "name": "value",
284 | "type": "text",
285 | "primaryKey": false,
286 | "notNull": true,
287 | "autoincrement": false
288 | },
289 | "expires_at": {
290 | "name": "expires_at",
291 | "type": "integer",
292 | "primaryKey": false,
293 | "notNull": true,
294 | "autoincrement": false
295 | },
296 | "created_at": {
297 | "name": "created_at",
298 | "type": "integer",
299 | "primaryKey": false,
300 | "notNull": false,
301 | "autoincrement": false
302 | },
303 | "updated_at": {
304 | "name": "updated_at",
305 | "type": "integer",
306 | "primaryKey": false,
307 | "notNull": false,
308 | "autoincrement": false
309 | }
310 | },
311 | "indexes": {},
312 | "foreignKeys": {},
313 | "compositePrimaryKeys": {},
314 | "uniqueConstraints": {},
315 | "checkConstraints": {}
316 | }
317 | },
318 | "views": {},
319 | "enums": {},
320 | "_meta": {
321 | "schemas": {},
322 | "tables": {},
323 | "columns": {}
324 | },
325 | "internal": {
326 | "indexes": {}
327 | }
328 | }
329 |
```
--------------------------------------------------------------------------------
/docs/app/docs/[[...slug]]/page.client.tsx:
--------------------------------------------------------------------------------
```typescript
1 | "use client";
2 | import { useState, useTransition } from "react";
3 | import {
4 | Check,
5 | Copy,
6 | ChevronDown,
7 | ExternalLink,
8 | MessageCircle,
9 | } from "lucide-react";
10 | import { cn } from "@/lib/utils";
11 | import { buttonVariants } from "@/components/ui/button";
12 | import {
13 | Popover,
14 | PopoverContent,
15 | PopoverTrigger,
16 | } from "fumadocs-ui/components/ui/popover";
17 | import { cva } from "class-variance-authority";
18 |
19 | import { type MouseEventHandler, useEffect, useRef } from "react";
20 | import { useEffectEvent } from "fumadocs-core/utils/use-effect-event";
21 |
22 | export function useCopyButton(
23 | onCopy: () => void | Promise<void>,
24 | ): [checked: boolean, onClick: MouseEventHandler] {
25 | const [checked, setChecked] = useState(false);
26 | const timeoutRef = useRef<number | null>(null);
27 |
28 | const onClick: MouseEventHandler = useEffectEvent(() => {
29 | if (timeoutRef.current) window.clearTimeout(timeoutRef.current);
30 | const res = Promise.resolve(onCopy());
31 |
32 | void res.then(() => {
33 | setChecked(true);
34 | timeoutRef.current = window.setTimeout(() => {
35 | setChecked(false);
36 | }, 1500);
37 | });
38 | });
39 |
40 | // Avoid updates after being unmounted
41 | useEffect(() => {
42 | return () => {
43 | if (timeoutRef.current) window.clearTimeout(timeoutRef.current);
44 | };
45 | }, []);
46 |
47 | return [checked, onClick];
48 | }
49 |
50 | const cache = new Map<string, string>();
51 |
52 | export function LLMCopyButton() {
53 | const [isLoading, startTransition] = useTransition();
54 | const [checked, onClick] = useCopyButton(async () => {
55 | startTransition(async () => {
56 | const url = window.location.pathname + ".mdx";
57 | const cached = cache.get(url);
58 |
59 | if (cached) {
60 | await navigator.clipboard.writeText(cached);
61 | } else {
62 | await navigator.clipboard.write([
63 | new ClipboardItem({
64 | "text/plain": fetch(url).then(async (res) => {
65 | const content = await res.text();
66 | cache.set(url, content);
67 |
68 | return content;
69 | }),
70 | }),
71 | ]);
72 | }
73 | });
74 | });
75 |
76 | return (
77 | <button
78 | disabled={isLoading}
79 | className={cn(
80 | buttonVariants({
81 | variant: "secondary",
82 | size: "sm",
83 | className: "gap-2 [&_svg]:size-3.5 [&_svg]:text-fd-muted-foreground",
84 | }),
85 | )}
86 | onClick={onClick}
87 | >
88 | {checked ? <Check /> : <Copy />}
89 | Copy Markdown
90 | </button>
91 | );
92 | }
93 |
94 | const optionVariants = cva(
95 | "text-sm p-2 rounded-lg inline-flex items-center gap-2 hover:text-fd-accent-foreground hover:bg-fd-accent [&_svg]:size-4",
96 | );
97 |
98 | export function ViewOptions(props: { markdownUrl: string; githubUrl: string }) {
99 | const markdownUrl = new URL(props.markdownUrl, "https://better-auth.com");
100 | const q = `Read ${markdownUrl}, I want to ask questions about it.`;
101 |
102 | const claudeUrl = new URL("https://claude.ai/new");
103 | claudeUrl.searchParams.set("q", q);
104 | const claude = claudeUrl.toString();
105 |
106 | const gptUrl = new URL("https://chatgpt.com/");
107 | gptUrl.searchParams.set("hints", "search");
108 | gptUrl.searchParams.set("q", q);
109 | const gpt = gptUrl.toString();
110 |
111 | const t3Url = new URL("https://t3.chat/new");
112 | t3Url.searchParams.set("q", q);
113 | const t3 = t3Url.toString();
114 |
115 | const copilotUrl = new URL("https://copilot.microsoft.com/");
116 | copilotUrl.searchParams.set("q", q);
117 | const copilot = copilotUrl.toString();
118 |
119 | return (
120 | <Popover>
121 | <PopoverTrigger
122 | className={cn(
123 | buttonVariants({
124 | variant: "secondary",
125 | size: "sm",
126 | className: "gap-2",
127 | }),
128 | )}
129 | >
130 | Open in
131 | <ChevronDown className="size-3.5 text-fd-muted-foreground" />
132 | </PopoverTrigger>
133 | <PopoverContent className="flex flex-col overflow-auto">
134 | {[
135 | {
136 | title: "Open in GitHub",
137 | href: props.githubUrl,
138 | icon: (
139 | <svg fill="currentColor" role="img" viewBox="0 0 24 24">
140 | <title>GitHub</title>
141 | <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
142 | </svg>
143 | ),
144 | },
145 | {
146 | title: "Open in ChatGPT",
147 | href: gpt,
148 | icon: (
149 | <svg
150 | role="img"
151 | viewBox="0 0 24 24"
152 | fill="currentColor"
153 | xmlns="http://www.w3.org/2000/svg"
154 | >
155 | <title>OpenAI</title>
156 | <path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
157 | </svg>
158 | ),
159 | },
160 | {
161 | title: "Open in Claude",
162 | href: claude,
163 | icon: (
164 | <svg
165 | fill="currentColor"
166 | role="img"
167 | viewBox="0 0 24 24"
168 | xmlns="http://www.w3.org/2000/svg"
169 | >
170 | <title>Anthropic</title>
171 | <path d="M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z" />
172 | </svg>
173 | ),
174 | },
175 | {
176 | title: "Open in T3 Chat",
177 | href: t3,
178 | icon: <MessageCircle />,
179 | },
180 | {
181 | title: "Open in Copilot",
182 | href: copilot,
183 | icon: (
184 | <svg
185 | fill="currentColor"
186 | role="img"
187 | viewBox="0 0 1322.9 1147.5"
188 | xmlns="http://www.w3.org/2000/svg"
189 | >
190 | <title>Microsoft</title>
191 | <path d="m711.19 265.2c-27.333 0-46.933 3.07-58.8 9.33 27.067-80.267 47.6-210.13 168-210.13 114.93 0 108.4 138.27 157.87 200.8zm107.33 112.93c-35.467 125.2-70 251.2-110.13 375.33-12.133 36.4-45.733 61.6-84 61.6h-136.27c9.3333-14 16.8-28.933 21.467-45.733 35.467-125.07 70-251.07 110.13-375.33 12.133-36.4 45.733-61.6 84-61.6h136.27c-9.3333 14-16.8 28.934-21.467 45.734m-316.13 704.8c-114.93 0-108.4-138.13-157.87-200.67h267.07c27.467 0 47.067-3.07 58.8-9.33-27.067 80.266-47.6 210-168 210m777.47-758.93h0.93c-32.667-38.266-82.267-57.866-146.67-57.866h-36.4c-34.533-2.8-65.333-26.134-76.533-58.8l-36.4-103.6c-21.463-61.737-80.263-103.74-145.73-103.74h-475.07c-175.6 0-251.2 225.07-292.27 361.33-38.267 127.07-126 341.73-24.267 462.13 46.667 55.067 116.67 57.867 183.07 57.867 34.533 2.8 65.333 26.133 76.533 58.8l36.4 103.6c21.467 61.733 80.267 103.73 145.6 103.73h475.2c175.47 0 251.07-225.07 292.27-361.33 30.8-100.8 68.133-224.93 66.267-324.8 0-50.534-11.2-100-42.933-137.33" />{" "}
192 | </svg>
193 | ),
194 | },
195 | ].map((item) => (
196 | <a
197 | key={item.href}
198 | href={item.href}
199 | rel="noreferrer noopener"
200 | target="_blank"
201 | className={cn(optionVariants())}
202 | >
203 | {item.icon}
204 | {item.title}
205 | <ExternalLink className="text-fd-muted-foreground size-3.5 ms-auto" />
206 | </a>
207 | ))}
208 | </PopoverContent>
209 | </Popover>
210 | );
211 | }
212 |
```
--------------------------------------------------------------------------------
/demo/nextjs/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 | const ChartContainer = ({ ref, id, className, children, config, ...props }) => {
38 | const uniqueId = React.useId();
39 | const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
40 |
41 | return (
42 | <ChartContext.Provider value={{ config }}>
43 | <div
44 | data-chart={chartId}
45 | ref={ref}
46 | className={cn(
47 | "flex aspect-video justify-center text-xs [&_.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-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.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 [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
48 | className,
49 | )}
50 | {...props}
51 | >
52 | <ChartStyle id={chartId} config={config} />
53 | <RechartsPrimitive.ResponsiveContainer>
54 | {children}
55 | </RechartsPrimitive.ResponsiveContainer>
56 | </div>
57 | </ChartContext.Provider>
58 | );
59 | };
60 | ChartContainer.displayName = "Chart";
61 |
62 | const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
63 | const colorConfig = Object.entries(config).filter(
64 | ([_, config]) => config.theme || config.color,
65 | );
66 |
67 | if (!colorConfig.length) {
68 | return null;
69 | }
70 |
71 | return (
72 | <style
73 | dangerouslySetInnerHTML={{
74 | __html: Object.entries(THEMES)
75 | .map(
76 | ([theme, prefix]) => `
77 | ${prefix} [data-chart=${id}] {
78 | ${colorConfig
79 | .map(([key, itemConfig]) => {
80 | const color =
81 | itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
82 | itemConfig.color;
83 | return color ? ` --color-${key}: ${color};` : null;
84 | })
85 | .join("\n")}
86 | }
87 | `,
88 | )
89 | .join("\n"),
90 | }}
91 | />
92 | );
93 | };
94 |
95 | const ChartTooltip = RechartsPrimitive.Tooltip;
96 |
97 | const ChartTooltipContent = ({
98 | ref,
99 | active,
100 | payload,
101 | className,
102 | indicator = "dot",
103 | hideLabel = false,
104 | hideIndicator = false,
105 | label,
106 | labelFormatter,
107 | labelClassName,
108 | formatter,
109 | color,
110 | nameKey,
111 | labelKey,
112 | }) => {
113 | const { config } = useChart();
114 |
115 | const tooltipLabel = React.useMemo(() => {
116 | if (hideLabel || !payload?.length) {
117 | return null;
118 | }
119 |
120 | const [item] = payload;
121 | const key = `${labelKey || item.dataKey || item.name || "value"}`;
122 | const itemConfig = getPayloadConfigFromPayload(config, item, key);
123 | const value =
124 | !labelKey && typeof label === "string"
125 | ? config[label as keyof typeof config]?.label || label
126 | : itemConfig?.label;
127 |
128 | if (labelFormatter) {
129 | return (
130 | <div className={cn("font-medium", labelClassName)}>
131 | {labelFormatter(value, payload)}
132 | </div>
133 | );
134 | }
135 |
136 | if (!value) {
137 | return null;
138 | }
139 |
140 | return <div className={cn("font-medium", labelClassName)}>{value}</div>;
141 | }, [
142 | label,
143 | labelFormatter,
144 | payload,
145 | hideLabel,
146 | labelClassName,
147 | config,
148 | labelKey,
149 | ]);
150 |
151 | if (!active || !payload?.length) {
152 | return null;
153 | }
154 |
155 | const nestLabel = payload.length === 1 && indicator !== "dot";
156 |
157 | return (
158 | <div
159 | ref={ref}
160 | className={cn(
161 | "grid min-w-32 items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
162 | className,
163 | )}
164 | >
165 | {!nestLabel ? tooltipLabel : null}
166 | <div className="grid gap-1.5">
167 | {payload.map((item, index) => {
168 | const key = `${nameKey || item.name || item.dataKey || "value"}`;
169 | const itemConfig = getPayloadConfigFromPayload(config, item, key);
170 | const indicatorColor = color || item.payload.fill || item.color;
171 |
172 | return (
173 | <div
174 | key={item.dataKey}
175 | className={cn(
176 | "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
177 | indicator === "dot" && "items-center",
178 | )}
179 | >
180 | {formatter && item?.value !== undefined && item.name ? (
181 | formatter(item.value, item.name, item, index, item.payload)
182 | ) : (
183 | <>
184 | {itemConfig?.icon ? (
185 | <itemConfig.icon />
186 | ) : (
187 | !hideIndicator && (
188 | <div
189 | className={cn(
190 | "shrink-0 rounded-[2px] border-border bg-(--color-bg)",
191 | {
192 | "h-2.5 w-2.5": indicator === "dot",
193 | "w-1": indicator === "line",
194 | "w-0 border-[1.5px] border-dashed bg-transparent":
195 | indicator === "dashed",
196 | "my-0.5": nestLabel && indicator === "dashed",
197 | },
198 | )}
199 | style={
200 | {
201 | "--color-bg": indicatorColor,
202 | "--color-border": indicatorColor,
203 | } as React.CSSProperties
204 | }
205 | />
206 | )
207 | )}
208 | <div
209 | className={cn(
210 | "flex flex-1 justify-between leading-none",
211 | nestLabel ? "items-end" : "items-center",
212 | )}
213 | >
214 | <div className="grid gap-1.5">
215 | {nestLabel ? tooltipLabel : null}
216 | <span className="text-muted-foreground">
217 | {itemConfig?.label || item.name}
218 | </span>
219 | </div>
220 | {item.value && (
221 | <span className="font-mono font-medium tabular-nums text-foreground">
222 | {item.value.toLocaleString()}
223 | </span>
224 | )}
225 | </div>
226 | </>
227 | )}
228 | </div>
229 | );
230 | })}
231 | </div>
232 | </div>
233 | );
234 | };
235 | ChartTooltipContent.displayName = "ChartTooltip";
236 |
237 | const ChartLegend = RechartsPrimitive.Legend;
238 |
239 | const ChartLegendContent = ({
240 | ref,
241 | className,
242 | hideIcon = false,
243 | payload,
244 | verticalAlign = "bottom",
245 | nameKey,
246 | }) => {
247 | const { config } = useChart();
248 |
249 | if (!payload?.length) {
250 | return null;
251 | }
252 |
253 | return (
254 | <div
255 | ref={ref}
256 | className={cn(
257 | "flex items-center justify-center gap-4",
258 | verticalAlign === "top" ? "pb-3" : "pt-3",
259 | className,
260 | )}
261 | >
262 | {payload.map((item) => {
263 | const key = `${nameKey || item.dataKey || "value"}`;
264 | const itemConfig = getPayloadConfigFromPayload(config, item, key);
265 |
266 | return (
267 | <div
268 | key={item.value}
269 | className={cn(
270 | "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
271 | )}
272 | >
273 | {itemConfig?.icon && !hideIcon ? (
274 | <itemConfig.icon />
275 | ) : (
276 | <div
277 | className="h-2 w-2 shrink-0 rounded-[2px]"
278 | style={{
279 | backgroundColor: item.color,
280 | }}
281 | />
282 | )}
283 | {itemConfig?.label}
284 | </div>
285 | );
286 | })}
287 | </div>
288 | );
289 | };
290 | ChartLegendContent.displayName = "ChartLegend";
291 |
292 | // Helper to extract item config from a payload.
293 | function getPayloadConfigFromPayload(
294 | config: ChartConfig,
295 | payload: unknown,
296 | key: string,
297 | ) {
298 | if (typeof payload !== "object" || payload === null) {
299 | return undefined;
300 | }
301 |
302 | const payloadPayload =
303 | "payload" in payload &&
304 | typeof payload.payload === "object" &&
305 | payload.payload !== null
306 | ? payload.payload
307 | : undefined;
308 |
309 | let configLabelKey: string = key;
310 |
311 | if (
312 | key in payload &&
313 | typeof payload[key as keyof typeof payload] === "string"
314 | ) {
315 | configLabelKey = payload[key as keyof typeof payload] as string;
316 | } else if (
317 | payloadPayload &&
318 | key in payloadPayload &&
319 | typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
320 | ) {
321 | configLabelKey = payloadPayload[
322 | key as keyof typeof payloadPayload
323 | ] as string;
324 | }
325 |
326 | return configLabelKey in config
327 | ? config[configLabelKey]
328 | : config[key as keyof typeof config];
329 | }
330 |
331 | export {
332 | ChartContainer,
333 | ChartTooltip,
334 | ChartTooltipContent,
335 | ChartLegend,
336 | ChartLegendContent,
337 | ChartStyle,
338 | };
339 |
```