#
tokens: 49337/50000 10/1118 files (page 26/71)
lines: on (toggle) GitHub
raw markdown copy reset
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 | 
```
Page 26/71FirstPrevNextLast