#
tokens: 48814/50000 15/1099 files (page 18/69)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 18 of 69. 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
├── 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-decelration
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── demo.ts
│       │   │   │   │   └── index.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.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.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
│   │   ├── tsconfig.test.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
│   │   │   ├── expo.test.ts
│   │   │   └── index.ts
│   │   ├── tsconfig.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.json
└── turbo.json
```

# Files

--------------------------------------------------------------------------------
/packages/better-auth/src/api/rate-limiter/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { RateLimit } from "../../types";
  2 | import { safeJSONParse } from "../../utils/json";
  3 | import { getIp } from "../../utils/get-request-ip";
  4 | import { wildcardMatch } from "../../utils/wildcard";
  5 | import type { AuthContext } from "@better-auth/core";
  6 | 
  7 | function shouldRateLimit(
  8 | 	max: number,
  9 | 	window: number,
 10 | 	rateLimitData: RateLimit,
 11 | ) {
 12 | 	const now = Date.now();
 13 | 	const windowInMs = window * 1000;
 14 | 	const timeSinceLastRequest = now - rateLimitData.lastRequest;
 15 | 	return timeSinceLastRequest < windowInMs && rateLimitData.count >= max;
 16 | }
 17 | 
 18 | function rateLimitResponse(retryAfter: number) {
 19 | 	return new Response(
 20 | 		JSON.stringify({
 21 | 			message: "Too many requests. Please try again later.",
 22 | 		}),
 23 | 		{
 24 | 			status: 429,
 25 | 			statusText: "Too Many Requests",
 26 | 			headers: {
 27 | 				"X-Retry-After": retryAfter.toString(),
 28 | 			},
 29 | 		},
 30 | 	);
 31 | }
 32 | 
 33 | function getRetryAfter(lastRequest: number, window: number) {
 34 | 	const now = Date.now();
 35 | 	const windowInMs = window * 1000;
 36 | 	return Math.ceil((lastRequest + windowInMs - now) / 1000);
 37 | }
 38 | 
 39 | function createDBStorage(ctx: AuthContext) {
 40 | 	const model = "rateLimit";
 41 | 	const db = ctx.adapter;
 42 | 	return {
 43 | 		get: async (key: string) => {
 44 | 			const res = await db.findMany<RateLimit>({
 45 | 				model,
 46 | 				where: [{ field: "key", value: key }],
 47 | 			});
 48 | 			const data = res[0];
 49 | 
 50 | 			if (typeof data?.lastRequest === "bigint") {
 51 | 				data.lastRequest = Number(data.lastRequest);
 52 | 			}
 53 | 
 54 | 			return data;
 55 | 		},
 56 | 		set: async (key: string, value: RateLimit, _update?: boolean) => {
 57 | 			try {
 58 | 				if (_update) {
 59 | 					await db.updateMany({
 60 | 						model,
 61 | 						where: [{ field: "key", value: key }],
 62 | 						update: {
 63 | 							count: value.count,
 64 | 							lastRequest: value.lastRequest,
 65 | 						},
 66 | 					});
 67 | 				} else {
 68 | 					await db.create({
 69 | 						model,
 70 | 						data: {
 71 | 							key,
 72 | 							count: value.count,
 73 | 							lastRequest: value.lastRequest,
 74 | 						},
 75 | 					});
 76 | 				}
 77 | 			} catch (e) {
 78 | 				ctx.logger.error("Error setting rate limit", e);
 79 | 			}
 80 | 		},
 81 | 	};
 82 | }
 83 | 
 84 | const memory = new Map<string, RateLimit>();
 85 | export function getRateLimitStorage(
 86 | 	ctx: AuthContext,
 87 | 	rateLimitSettings?: {
 88 | 		window?: number;
 89 | 	},
 90 | ) {
 91 | 	if (ctx.options.rateLimit?.customStorage) {
 92 | 		return ctx.options.rateLimit.customStorage;
 93 | 	}
 94 | 	const storage = ctx.rateLimit.storage;
 95 | 	if (storage === "secondary-storage") {
 96 | 		return {
 97 | 			get: async (key: string) => {
 98 | 				const data = await ctx.options.secondaryStorage?.get(key);
 99 | 				return data ? safeJSONParse<RateLimit>(data) : undefined;
100 | 			},
101 | 			set: async (key: string, value: RateLimit, _update?: boolean) => {
102 | 				const ttl =
103 | 					rateLimitSettings?.window ?? ctx.options.rateLimit?.window ?? 10;
104 | 				await ctx.options.secondaryStorage?.set?.(
105 | 					key,
106 | 					JSON.stringify(value),
107 | 					ttl,
108 | 				);
109 | 			},
110 | 		};
111 | 	} else if (storage === "memory") {
112 | 		return {
113 | 			async get(key: string) {
114 | 				return memory.get(key);
115 | 			},
116 | 			async set(key: string, value: RateLimit, _update?: boolean) {
117 | 				memory.set(key, value);
118 | 			},
119 | 		};
120 | 	}
121 | 	return createDBStorage(ctx);
122 | }
123 | 
124 | export async function onRequestRateLimit(req: Request, ctx: AuthContext) {
125 | 	if (!ctx.rateLimit.enabled) {
126 | 		return;
127 | 	}
128 | 	const path = new URL(req.url).pathname.replace(
129 | 		ctx.options.basePath || "/api/auth",
130 | 		"",
131 | 	);
132 | 	let window = ctx.rateLimit.window;
133 | 	let max = ctx.rateLimit.max;
134 | 	const ip = getIp(req, ctx.options);
135 | 	if (!ip) {
136 | 		return;
137 | 	}
138 | 	const key = ip + path;
139 | 	const specialRules = getDefaultSpecialRules();
140 | 	const specialRule = specialRules.find((rule) => rule.pathMatcher(path));
141 | 
142 | 	if (specialRule) {
143 | 		window = specialRule.window;
144 | 		max = specialRule.max;
145 | 	}
146 | 
147 | 	for (const plugin of ctx.options.plugins || []) {
148 | 		if (plugin.rateLimit) {
149 | 			const matchedRule = plugin.rateLimit.find((rule) =>
150 | 				rule.pathMatcher(path),
151 | 			);
152 | 			if (matchedRule) {
153 | 				window = matchedRule.window;
154 | 				max = matchedRule.max;
155 | 				break;
156 | 			}
157 | 		}
158 | 	}
159 | 
160 | 	if (ctx.rateLimit.customRules) {
161 | 		const _path = Object.keys(ctx.rateLimit.customRules).find((p) => {
162 | 			if (p.includes("*")) {
163 | 				const isMatch = wildcardMatch(p)(path);
164 | 				return isMatch;
165 | 			}
166 | 			return p === path;
167 | 		});
168 | 		if (_path) {
169 | 			const customRule = ctx.rateLimit.customRules[_path];
170 | 			const resolved =
171 | 				typeof customRule === "function" ? await customRule(req) : customRule;
172 | 			if (resolved) {
173 | 				window = resolved.window;
174 | 				max = resolved.max;
175 | 			}
176 | 
177 | 			if (resolved === false) {
178 | 				return;
179 | 			}
180 | 		}
181 | 	}
182 | 
183 | 	const storage = getRateLimitStorage(ctx, {
184 | 		window,
185 | 	});
186 | 	const data = await storage.get(key);
187 | 	const now = Date.now();
188 | 
189 | 	if (!data) {
190 | 		await storage.set(key, {
191 | 			key,
192 | 			count: 1,
193 | 			lastRequest: now,
194 | 		});
195 | 	} else {
196 | 		const timeSinceLastRequest = now - data.lastRequest;
197 | 
198 | 		if (shouldRateLimit(max, window, data)) {
199 | 			const retryAfter = getRetryAfter(data.lastRequest, window);
200 | 			return rateLimitResponse(retryAfter);
201 | 		} else if (timeSinceLastRequest > window * 1000) {
202 | 			// Reset the count if the window has passed since the last request
203 | 			await storage.set(
204 | 				key,
205 | 				{
206 | 					...data,
207 | 					count: 1,
208 | 					lastRequest: now,
209 | 				},
210 | 				true,
211 | 			);
212 | 		} else {
213 | 			await storage.set(
214 | 				key,
215 | 				{
216 | 					...data,
217 | 					count: data.count + 1,
218 | 					lastRequest: now,
219 | 				},
220 | 				true,
221 | 			);
222 | 		}
223 | 	}
224 | }
225 | 
226 | function getDefaultSpecialRules() {
227 | 	const specialRules = [
228 | 		{
229 | 			pathMatcher(path: string) {
230 | 				return (
231 | 					path.startsWith("/sign-in") ||
232 | 					path.startsWith("/sign-up") ||
233 | 					path.startsWith("/change-password") ||
234 | 					path.startsWith("/change-email")
235 | 				);
236 | 			},
237 | 			window: 10,
238 | 			max: 3,
239 | 		},
240 | 	];
241 | 	return specialRules;
242 | }
243 | 
```

--------------------------------------------------------------------------------
/docs/app/changelogs/[[...slug]]/page.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { changelogs } from "@/lib/source";
  2 | import { notFound } from "next/navigation";
  3 | import { absoluteUrl, formatDate } from "@/lib/utils";
  4 | import DatabaseTable from "@/components/mdx/database-tables";
  5 | import { cn } from "@/lib/utils";
  6 | import { Step, Steps } from "fumadocs-ui/components/steps";
  7 | import { Tab, Tabs } from "fumadocs-ui/components/tabs";
  8 | import { GenerateSecret } from "@/components/generate-secret";
  9 | import { AnimatePresence } from "@/components/ui/fade-in";
 10 | import { TypeTable } from "fumadocs-ui/components/type-table";
 11 | import { Features } from "@/components/blocks/features";
 12 | import { ForkButton } from "@/components/fork-button";
 13 | import Link from "next/link";
 14 | import defaultMdxComponents from "fumadocs-ui/mdx";
 15 | import { File, Folder, Files } from "fumadocs-ui/components/files";
 16 | import { Accordion, Accordions } from "fumadocs-ui/components/accordion";
 17 | import { Pre } from "fumadocs-ui/components/codeblock";
 18 | import ChangelogPage, { Glow } from "../_components/default-changelog";
 19 | import { IconLink } from "../_components/changelog-layout";
 20 | import { XIcon } from "../_components/icons";
 21 | import { StarField } from "../_components/stat-field";
 22 | import { GridPatterns } from "../_components/grid-pattern";
 23 | import { Callout } from "@/components/ui/callout";
 24 | 
 25 | const metaTitle = "Changelogs";
 26 | const metaDescription = "Latest changes , fixes and updates.";
 27 | const ogImage = "https://better-auth.com/release-og/changelog-og.png";
 28 | 
 29 | export default async function Page({
 30 | 	params,
 31 | }: {
 32 | 	params: Promise<{ slug?: string[] }>;
 33 | }) {
 34 | 	const { slug } = await params;
 35 | 	const page = changelogs.getPage(slug);
 36 | 	if (!slug) {
 37 | 		return <ChangelogPage />;
 38 | 	}
 39 | 	if (!page) {
 40 | 		notFound();
 41 | 	}
 42 | 	const MDX = page.data?.body;
 43 | 	const toc = page.data?.toc;
 44 | 	const { title, description, date } = page.data;
 45 | 	return (
 46 | 		<div className="md:grid md:grid-cols-2 items-start">
 47 | 			<div className="bg-gradient-to-tr hidden md:block overflow-hidden px-12 py-24 md:py-0 -mt-[100px] md:h-dvh relative md:sticky top-0 from-transparent dark:via-stone-950/5 via-stone-100/30 to-stone-200/20 dark:to-transparent/10">
 48 | 				<StarField className="top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2" />
 49 | 				<Glow />
 50 | 				<GridPatterns />
 51 | 				<div className="z-20 flex flex-col md:justify-center max-w-xl mx-auto h-full">
 52 | 					<div className="mt-14 mb-2 text-gray-600 dark:text-gray-300 flex items-center gap-x-1">
 53 | 						<p className="text-[12px] uppercase font-mono">
 54 | 							{formatDate(date)}
 55 | 						</p>
 56 | 					</div>
 57 | 					<h1 className=" font-sans mb-2 font-semibold tracking-tighter text-5xl">
 58 | 						{title}{" "}
 59 | 					</h1>
 60 | 					<p className="text-sm text-gray-600 mb-2 dark:text-gray-300">
 61 | 						{description}
 62 | 					</p>
 63 | 					<hr className="mt-4" />
 64 | 					<p className="absolute bottom-10 text-[0.8125rem]/6 text-gray-500">
 65 | 						<IconLink href="https://x.com/better_auth" icon={XIcon} compact>
 66 | 							BETTER-AUTH.
 67 | 						</IconLink>
 68 | 					</p>
 69 | 				</div>
 70 | 			</div>
 71 | 			<div className="px-4 relative md:px-8 pb-12 md:py-12">
 72 | 				<div className="absolute top-0 left-0 h-full -translate-x-full w-px bg-gradient-to-b from-black/5 dark:from-white/10 via-black/3 dark:via-white/5 to-transparent"></div>
 73 | 				<div className="prose pt-8 md:pt-0">
 74 | 					<MDX
 75 | 						components={{
 76 | 							...defaultMdxComponents,
 77 | 							Link: ({
 78 | 								className,
 79 | 								...props
 80 | 							}: React.ComponentProps<typeof Link>) => (
 81 | 								<Link
 82 | 									className={cn(
 83 | 										"font-medium underline underline-offset-4",
 84 | 										className,
 85 | 									)}
 86 | 									{...props}
 87 | 								/>
 88 | 							),
 89 | 							Step,
 90 | 							Steps,
 91 | 							File,
 92 | 							Folder,
 93 | 							Files,
 94 | 							Tab,
 95 | 							Tabs,
 96 | 							Pre: Pre,
 97 | 							GenerateSecret,
 98 | 							AnimatePresence,
 99 | 							TypeTable,
100 | 							Features,
101 | 							ForkButton,
102 | 							DatabaseTable,
103 | 							Accordion,
104 | 							Accordions,
105 | 							Callout: ({
106 | 								children,
107 | 								type,
108 | 								...props
109 | 							}: {
110 | 								children: React.ReactNode;
111 | 								type?: "info" | "warn" | "error" | "success" | "warning";
112 | 								[key: string]: any;
113 | 							}) => (
114 | 								<Callout type={type} {...props}>
115 | 									{children}
116 | 								</Callout>
117 | 							),
118 | 						}}
119 | 					/>
120 | 				</div>
121 | 			</div>
122 | 		</div>
123 | 	);
124 | }
125 | 
126 | export async function generateMetadata({
127 | 	params,
128 | }: {
129 | 	params: Promise<{ slug?: string[] }>;
130 | }) {
131 | 	const { slug } = await params;
132 | 	if (!slug) {
133 | 		return {
134 | 			metadataBase: new URL("https://better-auth.com/changelogs"),
135 | 			title: metaTitle,
136 | 			description: metaDescription,
137 | 			openGraph: {
138 | 				title: metaTitle,
139 | 				description: metaDescription,
140 | 				images: [
141 | 					{
142 | 						url: ogImage,
143 | 					},
144 | 				],
145 | 				url: "https://better-auth.com/changelogs",
146 | 			},
147 | 			twitter: {
148 | 				card: "summary_large_image",
149 | 				title: metaTitle,
150 | 				description: metaDescription,
151 | 				images: [ogImage],
152 | 			},
153 | 		};
154 | 	}
155 | 	const page = changelogs.getPage(slug);
156 | 	if (page == null) notFound();
157 | 	const baseUrl = process.env.NEXT_PUBLIC_URL || process.env.VERCEL_URL;
158 | 	const url = new URL(`${baseUrl}/release-og/${slug.join("")}.png`);
159 | 	const { title, description } = page.data;
160 | 
161 | 	return {
162 | 		title,
163 | 		description,
164 | 		openGraph: {
165 | 			title,
166 | 			description,
167 | 			type: "website",
168 | 			url: absoluteUrl(`changelogs/${slug.join("")}`),
169 | 			images: [
170 | 				{
171 | 					url: url.toString(),
172 | 					width: 1200,
173 | 					height: 630,
174 | 					alt: title,
175 | 				},
176 | 			],
177 | 		},
178 | 		twitter: {
179 | 			card: "summary_large_image",
180 | 			title,
181 | 			description,
182 | 			images: [url.toString()],
183 | 		},
184 | 	};
185 | }
186 | 
187 | export function generateStaticParams() {
188 | 	return changelogs.generateParams();
189 | }
190 | 
```

--------------------------------------------------------------------------------
/docs/app/docs/[[...slug]]/page.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { source } from "@/lib/source";
  2 | import { DocsPage, DocsBody, DocsTitle } from "@/components/docs/page";
  3 | import { notFound } from "next/navigation";
  4 | import { absoluteUrl } from "@/lib/utils";
  5 | import DatabaseTable from "@/components/mdx/database-tables";
  6 | import { cn } from "@/lib/utils";
  7 | import { Step, Steps } from "fumadocs-ui/components/steps";
  8 | import { Tab, Tabs } from "fumadocs-ui/components/tabs";
  9 | import { GenerateSecret } from "@/components/generate-secret";
 10 | import { AnimatePresence } from "@/components/ui/fade-in";
 11 | import { TypeTable } from "fumadocs-ui/components/type-table";
 12 | import { Features } from "@/components/blocks/features";
 13 | import { ForkButton } from "@/components/fork-button";
 14 | import Link from "next/link";
 15 | import defaultMdxComponents from "fumadocs-ui/mdx";
 16 | import {
 17 | 	CodeBlock,
 18 | 	Pre,
 19 | 	CodeBlockTab,
 20 | 	CodeBlockTabsList,
 21 | 	CodeBlockTabs,
 22 | } from "@/components/ui/code-block";
 23 | import { File, Folder, Files } from "fumadocs-ui/components/files";
 24 | import { AutoTypeTable } from "fumadocs-typescript/ui";
 25 | import { Accordion, Accordions } from "fumadocs-ui/components/accordion";
 26 | import { Endpoint } from "@/components/endpoint";
 27 | import { DividerText } from "@/components/divider-text";
 28 | import { APIMethod } from "@/components/api-method";
 29 | import { LLMCopyButton, ViewOptions } from "./page.client";
 30 | import { GenerateAppleJwt } from "@/components/generate-apple-jwt";
 31 | import { Callout } from "@/components/ui/callout";
 32 | import { AddToCursor } from "@/components/mdx/add-to-cursor";
 33 | export default async function Page({
 34 | 	params,
 35 | }: {
 36 | 	params: Promise<{ slug?: string[] }>;
 37 | }) {
 38 | 	const { slug } = await params;
 39 | 	const page = source.getPage(slug);
 40 | 
 41 | 	if (!page) {
 42 | 		notFound();
 43 | 	}
 44 | 
 45 | 	const MDX = page.data.body;
 46 | 	const avoidLLMHeader = ["Introduction", "Comparison"];
 47 | 	return (
 48 | 		<DocsPage
 49 | 			toc={page.data.toc}
 50 | 			full={page.data.full}
 51 | 			editOnGithub={{
 52 | 				owner: "better-auth",
 53 | 				repo: "better-auth",
 54 | 				sha: process.env.VERCEL_GIT_COMMIT_SHA || "main",
 55 | 				path: `/docs/content/docs/${page.path}`,
 56 | 			}}
 57 | 			tableOfContent={{
 58 | 				header: <div className="w-10 h-4"></div>,
 59 | 			}}
 60 | 		>
 61 | 			<DocsTitle>{page.data.title}</DocsTitle>
 62 | 			{!avoidLLMHeader.includes(page.data.title) && (
 63 | 				<div className="flex flex-row gap-2 items-center pb-3 border-b">
 64 | 					<LLMCopyButton />
 65 | 					<ViewOptions
 66 | 						markdownUrl={`${page.url}.mdx`}
 67 | 						githubUrl={`https://github.com/better-auth/better-auth/blob/main/docs/content/docs/${page.file.path}`}
 68 | 					/>
 69 | 				</div>
 70 | 			)}
 71 | 			<DocsBody>
 72 | 				<MDX
 73 | 					components={{
 74 | 						...defaultMdxComponents,
 75 | 						CodeBlockTabs: (props) => {
 76 | 							return (
 77 | 								<CodeBlockTabs
 78 | 									{...props}
 79 | 									className="p-0 border-0 rounded-lg bg-fd-secondary"
 80 | 								>
 81 | 									<div {...props}>{props.children}</div>
 82 | 								</CodeBlockTabs>
 83 | 							);
 84 | 						},
 85 | 						CodeBlockTabsList: (props) => {
 86 | 							return (
 87 | 								<CodeBlockTabsList
 88 | 									{...props}
 89 | 									className="pb-0 my-0 rounded-lg bg-fd-secondary"
 90 | 								/>
 91 | 							);
 92 | 						},
 93 | 						CodeBlockTab: (props) => {
 94 | 							return <CodeBlockTab {...props} className="p-0 m-0 rounded-lg" />;
 95 | 						},
 96 | 						pre: (props) => {
 97 | 							return (
 98 | 								<CodeBlock className="rounded-xl bg-fd-muted" {...props}>
 99 | 									<div style={{ minWidth: "100%", display: "table" }}>
100 | 										<Pre className="px-0 py-3 bg-fd-muted focus-visible:outline-none">
101 | 											{props.children}
102 | 										</Pre>
103 | 									</div>
104 | 								</CodeBlock>
105 | 							);
106 | 						},
107 | 						Link: ({
108 | 							className,
109 | 							...props
110 | 						}: React.ComponentProps<typeof Link>) => (
111 | 							<Link
112 | 								className={cn(
113 | 									"font-medium underline underline-offset-4",
114 | 									className,
115 | 								)}
116 | 								{...props}
117 | 							/>
118 | 						),
119 | 						Step,
120 | 						Steps,
121 | 						File,
122 | 						Folder,
123 | 						Files,
124 | 						Tab,
125 | 						Tabs,
126 | 						AutoTypeTable,
127 | 						GenerateSecret,
128 | 						GenerateAppleJwt,
129 | 						AnimatePresence,
130 | 						TypeTable,
131 | 						Features,
132 | 						ForkButton,
133 | 						AddToCursor,
134 | 						DatabaseTable,
135 | 						Accordion,
136 | 						Accordions,
137 | 						Endpoint,
138 | 						APIMethod,
139 | 						Callout: ({
140 | 							children,
141 | 							type,
142 | 							...props
143 | 						}: {
144 | 							children: React.ReactNode;
145 | 							type?: "info" | "warn" | "error" | "success" | "warning";
146 | 							[key: string]: any;
147 | 						}) => (
148 | 							<Callout type={type} {...props}>
149 | 								{children}
150 | 							</Callout>
151 | 						),
152 | 						DividerText,
153 | 						iframe: (props) => (
154 | 							<iframe {...props} className="w-full h-[500px]" />
155 | 						),
156 | 					}}
157 | 				/>
158 | 			</DocsBody>
159 | 		</DocsPage>
160 | 	);
161 | }
162 | 
163 | export async function generateStaticParams() {
164 | 	return source.generateParams();
165 | }
166 | 
167 | export async function generateMetadata({
168 | 	params,
169 | }: {
170 | 	params: Promise<{ slug?: string[] }>;
171 | }) {
172 | 	const { slug } = await params;
173 | 	const page = source.getPage(slug);
174 | 	if (page == null) notFound();
175 | 	const baseUrl = process.env.NEXT_PUBLIC_URL || process.env.VERCEL_URL;
176 | 	const url = new URL(`${baseUrl}/api/og`);
177 | 	const { title, description } = page.data;
178 | 	const pageSlug = page.file.path;
179 | 	url.searchParams.set("type", "Documentation");
180 | 	url.searchParams.set("mode", "dark");
181 | 	url.searchParams.set("heading", `${title}`);
182 | 
183 | 	return {
184 | 		title,
185 | 		description,
186 | 		openGraph: {
187 | 			title,
188 | 			description,
189 | 			type: "website",
190 | 			url: absoluteUrl(`docs/${pageSlug}`),
191 | 			images: [
192 | 				{
193 | 					url: url.toString(),
194 | 					width: 1200,
195 | 					height: 630,
196 | 					alt: title,
197 | 				},
198 | 			],
199 | 		},
200 | 		twitter: {
201 | 			card: "summary_large_image",
202 | 			title,
203 | 			description,
204 | 			images: [url.toString()],
205 | 		},
206 | 	};
207 | }
208 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/passkey/client.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { BetterFetch, BetterFetchOption } from "@better-fetch/fetch";
  2 | import {
  3 | 	WebAuthnError,
  4 | 	startAuthentication,
  5 | 	startRegistration,
  6 | } from "@simplewebauthn/browser";
  7 | import type {
  8 | 	PublicKeyCredentialCreationOptionsJSON,
  9 | 	PublicKeyCredentialRequestOptionsJSON,
 10 | } from "@simplewebauthn/browser";
 11 | import type { User, Session } from "../../types";
 12 | import type { passkey as passkeyPl, Passkey } from ".";
 13 | import type { BetterAuthClientPlugin, ClientStore } from "@better-auth/core";
 14 | import { useAuthQuery } from "../../client";
 15 | import { atom } from "nanostores";
 16 | 
 17 | export const getPasskeyActions = (
 18 | 	$fetch: BetterFetch,
 19 | 	{
 20 | 		$listPasskeys,
 21 | 		$store,
 22 | 	}: {
 23 | 		$listPasskeys: ReturnType<typeof atom<any>>;
 24 | 		$store: ClientStore;
 25 | 	},
 26 | ) => {
 27 | 	const signInPasskey = async (
 28 | 		opts?: {
 29 | 			autoFill?: boolean;
 30 | 			fetchOptions?: BetterFetchOption;
 31 | 		},
 32 | 		options?: BetterFetchOption,
 33 | 	) => {
 34 | 		const response = await $fetch<PublicKeyCredentialRequestOptionsJSON>(
 35 | 			"/passkey/generate-authenticate-options",
 36 | 			{
 37 | 				method: "POST",
 38 | 				throw: false,
 39 | 			},
 40 | 		);
 41 | 		if (!response.data) {
 42 | 			return response;
 43 | 		}
 44 | 		try {
 45 | 			const res = await startAuthentication({
 46 | 				optionsJSON: response.data,
 47 | 				useBrowserAutofill: opts?.autoFill,
 48 | 			});
 49 | 			const verified = await $fetch<{
 50 | 				session: Session;
 51 | 				user: User;
 52 | 			}>("/passkey/verify-authentication", {
 53 | 				body: {
 54 | 					response: res,
 55 | 				},
 56 | 				...opts?.fetchOptions,
 57 | 				...options,
 58 | 				method: "POST",
 59 | 				throw: false,
 60 | 			});
 61 | 			$listPasskeys.set(Math.random());
 62 | 			$store.notify("$sessionSignal");
 63 | 
 64 | 			return verified;
 65 | 		} catch (e) {
 66 | 			return {
 67 | 				data: null,
 68 | 				error: {
 69 | 					code: "AUTH_CANCELLED",
 70 | 					message: "auth cancelled",
 71 | 					status: 400,
 72 | 					statusText: "BAD_REQUEST",
 73 | 				},
 74 | 			};
 75 | 		}
 76 | 	};
 77 | 
 78 | 	const registerPasskey = async (
 79 | 		opts?: {
 80 | 			fetchOptions?: BetterFetchOption;
 81 | 			/**
 82 | 			 * The name of the passkey. This is used to
 83 | 			 * identify the passkey in the UI.
 84 | 			 */
 85 | 			name?: string;
 86 | 
 87 | 			/**
 88 | 			 * The type of attachment for the passkey. Defaults to both
 89 | 			 * platform and cross-platform allowed, with platform preferred.
 90 | 			 */
 91 | 			authenticatorAttachment?: "platform" | "cross-platform";
 92 | 
 93 | 			/**
 94 | 			 * Try to silently create a passkey with the password manager that the user just signed
 95 | 			 * in with.
 96 | 			 * @default false
 97 | 			 */
 98 | 			useAutoRegister?: boolean;
 99 | 		},
100 | 		fetchOpts?: BetterFetchOption,
101 | 	) => {
102 | 		const options = await $fetch<PublicKeyCredentialCreationOptionsJSON>(
103 | 			"/passkey/generate-register-options",
104 | 			{
105 | 				method: "GET",
106 | 				query: {
107 | 					...(opts?.authenticatorAttachment && {
108 | 						authenticatorAttachment: opts.authenticatorAttachment,
109 | 					}),
110 | 					...(opts?.name && {
111 | 						name: opts.name,
112 | 					}),
113 | 				},
114 | 				throw: false,
115 | 			},
116 | 		);
117 | 
118 | 		if (!options.data) {
119 | 			return options;
120 | 		}
121 | 		try {
122 | 			const res = await startRegistration({
123 | 				optionsJSON: options.data,
124 | 				useAutoRegister: opts?.useAutoRegister,
125 | 			});
126 | 			const verified = await $fetch<{
127 | 				passkey: Passkey;
128 | 			}>("/passkey/verify-registration", {
129 | 				...opts?.fetchOptions,
130 | 				...fetchOpts,
131 | 				body: {
132 | 					response: res,
133 | 					name: opts?.name,
134 | 				},
135 | 				method: "POST",
136 | 				throw: false,
137 | 			});
138 | 
139 | 			if (!verified.data) {
140 | 				return verified;
141 | 			}
142 | 			$listPasskeys.set(Math.random());
143 | 		} catch (e) {
144 | 			if (e instanceof WebAuthnError) {
145 | 				if (e.code === "ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED") {
146 | 					return {
147 | 						data: null,
148 | 						error: {
149 | 							code: e.code,
150 | 							message: "previously registered",
151 | 							status: 400,
152 | 							statusText: "BAD_REQUEST",
153 | 						},
154 | 					};
155 | 				}
156 | 				if (e.code === "ERROR_CEREMONY_ABORTED") {
157 | 					return {
158 | 						data: null,
159 | 						error: {
160 | 							code: e.code,
161 | 							message: "registration cancelled",
162 | 							status: 400,
163 | 							statusText: "BAD_REQUEST",
164 | 						},
165 | 					};
166 | 				}
167 | 				return {
168 | 					data: null,
169 | 					error: {
170 | 						code: e.code,
171 | 						message: e.message,
172 | 						status: 400,
173 | 						statusText: "BAD_REQUEST",
174 | 					},
175 | 				};
176 | 			}
177 | 			return {
178 | 				data: null,
179 | 				error: {
180 | 					code: "UNKNOWN_ERROR",
181 | 					message: e instanceof Error ? e.message : "unknown error",
182 | 					status: 500,
183 | 					statusText: "INTERNAL_SERVER_ERROR",
184 | 				},
185 | 			};
186 | 		}
187 | 	};
188 | 
189 | 	return {
190 | 		signIn: {
191 | 			/**
192 | 			 * Sign in with a registered passkey
193 | 			 */
194 | 			passkey: signInPasskey,
195 | 		},
196 | 		passkey: {
197 | 			/**
198 | 			 * Add a passkey to the user account
199 | 			 */
200 | 			addPasskey: registerPasskey,
201 | 		},
202 | 		/**
203 | 		 * Inferred Internal Types
204 | 		 */
205 | 		$Infer: {} as {
206 | 			Passkey: Passkey;
207 | 		},
208 | 	};
209 | };
210 | 
211 | export const passkeyClient = () => {
212 | 	const $listPasskeys = atom<any>();
213 | 	return {
214 | 		id: "passkey",
215 | 		$InferServerPlugin: {} as ReturnType<typeof passkeyPl>,
216 | 		getActions: ($fetch, $store) =>
217 | 			getPasskeyActions($fetch, {
218 | 				$listPasskeys,
219 | 				$store,
220 | 			}),
221 | 		getAtoms($fetch) {
222 | 			const listPasskeys = useAuthQuery<Passkey[]>(
223 | 				$listPasskeys,
224 | 				"/passkey/list-user-passkeys",
225 | 				$fetch,
226 | 				{
227 | 					method: "GET",
228 | 				},
229 | 			);
230 | 			return {
231 | 				listPasskeys,
232 | 				$listPasskeys,
233 | 			};
234 | 		},
235 | 		pathMethods: {
236 | 			"/passkey/register": "POST",
237 | 			"/passkey/authenticate": "POST",
238 | 		},
239 | 		atomListeners: [
240 | 			{
241 | 				matcher(path) {
242 | 					return (
243 | 						path === "/passkey/verify-registration" ||
244 | 						path === "/passkey/delete-passkey" ||
245 | 						path === "/passkey/update-passkey" ||
246 | 						path === "/sign-out"
247 | 					);
248 | 				},
249 | 				signal: "$listPasskeys",
250 | 			},
251 | 			{
252 | 				matcher: (path) => path === "/passkey/verify-authentication",
253 | 				signal: "$sessionSignal",
254 | 			},
255 | 		],
256 | 	} satisfies BetterAuthClientPlugin;
257 | };
258 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/one-tap/client.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { BetterFetchOption } from "@better-fetch/fetch";
  2 | import type { BetterAuthClientPlugin } from "@better-auth/core";
  3 | 
  4 | declare global {
  5 | 	interface Window {
  6 | 		google?: {
  7 | 			accounts: {
  8 | 				id: {
  9 | 					initialize: (config: any) => void;
 10 | 					prompt: (callback?: (notification: any) => void) => void;
 11 | 				};
 12 | 			};
 13 | 		};
 14 | 		googleScriptInitialized?: boolean;
 15 | 	}
 16 | }
 17 | 
 18 | export interface GoogleOneTapOptions {
 19 | 	/**
 20 | 	 * Google client ID
 21 | 	 */
 22 | 	clientId: string;
 23 | 	/**
 24 | 	 * Auto select the account if the user is already signed in
 25 | 	 */
 26 | 	autoSelect?: boolean;
 27 | 	/**
 28 | 	 * Cancel the flow when the user taps outside the prompt
 29 | 	 */
 30 | 	cancelOnTapOutside?: boolean;
 31 | 	/**
 32 | 	 * The mode to use for the Google One Tap flow
 33 | 	 *
 34 | 	 * popup: Use a popup window
 35 | 	 * redirect: Redirect the user to the Google One Tap flow
 36 | 	 *
 37 | 	 * @default "popup"
 38 | 	 */
 39 | 	uxMode?: "popup" | "redirect";
 40 | 	/**
 41 | 	 * The context to use for the Google One Tap flow. See https://developers.google.com/identity/gsi/web/reference/js-reference
 42 | 	 *
 43 | 	 * @default "signin"
 44 | 	 */
 45 | 	context?: "signin" | "signup" | "use";
 46 | 	/**
 47 | 	 * Additional configuration options to pass to the Google One Tap API.
 48 | 	 */
 49 | 	additionalOptions?: Record<string, any>;
 50 | 	/**
 51 | 	 * Configuration options for the prompt and exponential backoff behavior.
 52 | 	 */
 53 | 	promptOptions?: {
 54 | 		/**
 55 | 		 * Base delay (in milliseconds) for exponential backoff.
 56 | 		 * @default 1000
 57 | 		 */
 58 | 		baseDelay?: number;
 59 | 		/**
 60 | 		 * Maximum number of prompt attempts before calling onPromptNotification.
 61 | 		 * @default 5
 62 | 		 */
 63 | 		maxAttempts?: number;
 64 | 	};
 65 | }
 66 | 
 67 | export interface GoogleOneTapActionOptions
 68 | 	extends Omit<GoogleOneTapOptions, "clientId" | "promptOptions"> {
 69 | 	fetchOptions?: BetterFetchOption;
 70 | 	/**
 71 | 	 * Callback URL.
 72 | 	 */
 73 | 	callbackURL?: string;
 74 | 	/**
 75 | 	 * Optional callback that receives the prompt notification if (or when) the prompt is dismissed or skipped.
 76 | 	 * This lets you render an alternative UI (e.g. a Google Sign-In button) to restart the process.
 77 | 	 */
 78 | 	onPromptNotification?: (notification: any) => void;
 79 | }
 80 | 
 81 | let isRequestInProgress = false;
 82 | 
 83 | export const oneTapClient = (options: GoogleOneTapOptions) => {
 84 | 	return {
 85 | 		id: "one-tap",
 86 | 		getActions: ($fetch, _) => ({
 87 | 			oneTap: async (
 88 | 				opts?: GoogleOneTapActionOptions,
 89 | 				fetchOptions?: BetterFetchOption,
 90 | 			) => {
 91 | 				if (isRequestInProgress) {
 92 | 					console.warn(
 93 | 						"A Google One Tap request is already in progress. Please wait.",
 94 | 					);
 95 | 					return;
 96 | 				}
 97 | 
 98 | 				isRequestInProgress = true;
 99 | 
100 | 				try {
101 | 					if (typeof window === "undefined" || !window.document) {
102 | 						console.warn(
103 | 							"Google One Tap is only available in browser environments",
104 | 						);
105 | 						return;
106 | 					}
107 | 
108 | 					const { autoSelect, cancelOnTapOutside, context } = opts ?? {};
109 | 					const contextValue = context ?? options.context ?? "signin";
110 | 
111 | 					await loadGoogleScript();
112 | 
113 | 					await new Promise<void>((resolve, reject) => {
114 | 						let isResolved = false;
115 | 						const baseDelay = options.promptOptions?.baseDelay ?? 1000;
116 | 						const maxAttempts = options.promptOptions?.maxAttempts ?? 5;
117 | 
118 | 						window.google?.accounts.id.initialize({
119 | 							client_id: options.clientId,
120 | 							callback: async (response: { credential: string }) => {
121 | 								isResolved = true;
122 | 								try {
123 | 									await $fetch("/one-tap/callback", {
124 | 										method: "POST",
125 | 										body: { idToken: response.credential },
126 | 										...opts?.fetchOptions,
127 | 										...fetchOptions,
128 | 									});
129 | 
130 | 									if (
131 | 										(!opts?.fetchOptions && !fetchOptions) ||
132 | 										opts?.callbackURL
133 | 									) {
134 | 										window.location.href = opts?.callbackURL ?? "/";
135 | 									}
136 | 									resolve();
137 | 								} catch (error) {
138 | 									console.error("Error during One Tap callback:", error);
139 | 									reject(error);
140 | 								}
141 | 							},
142 | 							auto_select: autoSelect,
143 | 							cancel_on_tap_outside: cancelOnTapOutside,
144 | 							context: contextValue,
145 | 
146 | 							...options.additionalOptions,
147 | 						});
148 | 
149 | 						const handlePrompt = (attempt: number) => {
150 | 							if (isResolved) return;
151 | 
152 | 							window.google?.accounts.id.prompt((notification: any) => {
153 | 								if (isResolved) return;
154 | 
155 | 								if (
156 | 									notification.isDismissedMoment &&
157 | 									notification.isDismissedMoment()
158 | 								) {
159 | 									if (attempt < maxAttempts) {
160 | 										const delay = Math.pow(2, attempt) * baseDelay;
161 | 										setTimeout(() => handlePrompt(attempt + 1), delay);
162 | 									} else {
163 | 										opts?.onPromptNotification?.(notification);
164 | 									}
165 | 								} else if (
166 | 									notification.isSkippedMoment &&
167 | 									notification.isSkippedMoment()
168 | 								) {
169 | 									if (attempt < maxAttempts) {
170 | 										const delay = Math.pow(2, attempt) * baseDelay;
171 | 										setTimeout(() => handlePrompt(attempt + 1), delay);
172 | 									} else {
173 | 										opts?.onPromptNotification?.(notification);
174 | 									}
175 | 								}
176 | 							});
177 | 						};
178 | 
179 | 						handlePrompt(0);
180 | 					});
181 | 				} catch (error) {
182 | 					console.error("Error during Google One Tap flow:", error);
183 | 					throw error;
184 | 				} finally {
185 | 					isRequestInProgress = false;
186 | 				}
187 | 			},
188 | 		}),
189 | 		getAtoms($fetch) {
190 | 			return {};
191 | 		},
192 | 	} satisfies BetterAuthClientPlugin;
193 | };
194 | 
195 | const loadGoogleScript = (): Promise<void> => {
196 | 	return new Promise((resolve) => {
197 | 		if (window.googleScriptInitialized) {
198 | 			resolve();
199 | 			return;
200 | 		}
201 | 
202 | 		const script = document.createElement("script");
203 | 		script.src = "https://accounts.google.com/gsi/client";
204 | 		script.async = true;
205 | 		script.defer = true;
206 | 		script.onload = () => {
207 | 			window.googleScriptInitialized = true;
208 | 			resolve();
209 | 		};
210 | 		document.head.appendChild(script);
211 | 	});
212 | };
213 | 
```

--------------------------------------------------------------------------------
/docs/app/blog/_components/support.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | "use client";
  2 | import { Button } from "@/components/ui/button";
  3 | import {
  4 | 	Dialog,
  5 | 	DialogContent,
  6 | 	DialogDescription,
  7 | 	DialogFooter,
  8 | 	DialogHeader,
  9 | 	DialogTitle,
 10 | 	DialogTrigger,
 11 | } from "@/components/ui/dialog";
 12 | import { Input } from "@/components/ui/input";
 13 | import { Label } from "@/components/ui/label";
 14 | import {
 15 | 	Select,
 16 | 	SelectContent,
 17 | 	SelectItem,
 18 | 	SelectTrigger,
 19 | 	SelectValue,
 20 | } from "@/components/ui/select";
 21 | import { Textarea } from "@/components/ui/textarea";
 22 | import * as React from "react";
 23 | import {
 24 | 	Card,
 25 | 	CardDescription,
 26 | 	CardFooter,
 27 | 	CardHeader,
 28 | 	CardTitle,
 29 | } from "@/components/ui/card";
 30 | 
 31 | export function Support() {
 32 | 	const [open, setOpen] = React.useState(false);
 33 | 	const [submitting, setSubmitting] = React.useState(false);
 34 | 	const formRef = React.useRef<HTMLFormElement | null>(null);
 35 | 
 36 | 	async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
 37 | 		event.preventDefault();
 38 | 		if (submitting) return;
 39 | 		setSubmitting(true);
 40 | 		const form = new FormData(event.currentTarget);
 41 | 		const payload = {
 42 | 			name: String(form.get("name") || ""),
 43 | 			email: String(form.get("email") || ""),
 44 | 			company: String(form.get("company") || ""),
 45 | 			website: String(form.get("website") || ""),
 46 | 			userCount: String(form.get("userCount") || ""),
 47 | 			interest: String(form.get("interest") || ""),
 48 | 			features: String(form.get("features") || ""),
 49 | 			additional: String(form.get("additional") || ""),
 50 | 		};
 51 | 		try {
 52 | 			const res = await fetch("/api/support", {
 53 | 				method: "POST",
 54 | 				headers: { "Content-Type": "application/json" },
 55 | 				body: JSON.stringify(payload),
 56 | 			});
 57 | 			if (!res.ok) throw new Error("Failed to submit");
 58 | 			setOpen(false);
 59 | 			formRef.current?.reset();
 60 | 			// optionally add a toast later
 61 | 		} catch (e) {
 62 | 			console.error(e);
 63 | 			// optionally add error toast
 64 | 		} finally {
 65 | 			setSubmitting(false);
 66 | 		}
 67 | 	}
 68 | 
 69 | 	return (
 70 | 		<Card className="flex flex-col gap-3 rounded-none">
 71 | 			<CardHeader>
 72 | 				<CardTitle>Dedicated Support</CardTitle>
 73 | 				<CardDescription>
 74 | 					We're now offering on demand support for Better Auth and Auth.js.
 75 | 					Including help out migrations, consultations, premium dedicated
 76 | 					support and more. If you're interested, please get in touch.
 77 | 				</CardDescription>
 78 | 			</CardHeader>
 79 | 			<CardFooter>
 80 | 				<Dialog open={open} onOpenChange={setOpen}>
 81 | 					<div>
 82 | 						<DialogTrigger asChild>
 83 | 							<Button
 84 | 								type="button"
 85 | 								className="bg-blue-500 text-white hover:bg-blue-600 transition-colors cursor-pointer"
 86 | 							>
 87 | 								Request support
 88 | 							</Button>
 89 | 						</DialogTrigger>
 90 | 					</div>
 91 | 					<DialogContent>
 92 | 						<DialogHeader>
 93 | 							<DialogTitle>Request dedicated support</DialogTitle>
 94 | 							<DialogDescription>
 95 | 								Tell us about your team and what you're looking for.
 96 | 							</DialogDescription>
 97 | 						</DialogHeader>
 98 | 						<form ref={formRef} className="grid gap-4" onSubmit={onSubmit}>
 99 | 							<div className="grid gap-2">
100 | 								<Label htmlFor="name">Your name</Label>
101 | 								<Input id="name" name="name" placeholder="Jane Doe" required />
102 | 							</div>
103 | 							<div className="grid gap-2">
104 | 								<Label htmlFor="email">Work email</Label>
105 | 								<Input
106 | 									id="email"
107 | 									name="email"
108 | 									type="email"
109 | 									placeholder="[email protected]"
110 | 									required
111 | 								/>
112 | 							</div>
113 | 							<div className="grid gap-2">
114 | 								<Label htmlFor="company">Company</Label>
115 | 								<Input id="company" name="company" placeholder="Acme Inc." />
116 | 							</div>
117 | 							<div className="grid gap-2">
118 | 								<Label htmlFor="website">Website</Label>
119 | 								<Input
120 | 									id="website"
121 | 									name="website"
122 | 									placeholder="https://acme.com"
123 | 								/>
124 | 							</div>
125 | 							<div className="grid gap-2">
126 | 								<Label htmlFor="userCount">Users</Label>
127 | 								<Select name="userCount">
128 | 									<SelectTrigger id="userCount">
129 | 										<SelectValue placeholder="Select users" />
130 | 									</SelectTrigger>
131 | 									<SelectContent>
132 | 										<SelectItem value="<1k">Less than 1k</SelectItem>
133 | 										<SelectItem value="1k-10k">1k - 10k</SelectItem>
134 | 										<SelectItem value=">10k">More than 10k</SelectItem>
135 | 									</SelectContent>
136 | 								</Select>
137 | 							</div>
138 | 							<div className="grid gap-2">
139 | 								<Label htmlFor="interest">What are you interested in?</Label>
140 | 								<Select name="interest">
141 | 									<SelectTrigger id="interest">
142 | 										<SelectValue placeholder="Choose a package" />
143 | 									</SelectTrigger>
144 | 									<SelectContent>
145 | 										<SelectItem value="migration">Migration help</SelectItem>
146 | 										<SelectItem value="consultation">Consultation</SelectItem>
147 | 										<SelectItem value="support">Premium support</SelectItem>
148 | 										<SelectItem value="custom">Custom</SelectItem>
149 | 									</SelectContent>
150 | 								</Select>
151 | 							</div>
152 | 							<div className="grid gap-2">
153 | 								<Label htmlFor="features">
154 | 									Features or plugins of interest
155 | 								</Label>
156 | 								<Input
157 | 									id="features"
158 | 									name="features"
159 | 									placeholder="SAML, SIWE, WebAuthn, Organizations, ..."
160 | 								/>
161 | 							</div>
162 | 							<div className="grid gap-2">
163 | 								<Label htmlFor="additional">Anything else?</Label>
164 | 								<Textarea
165 | 									id="additional"
166 | 									name="additional"
167 | 									placeholder="Share more context, timelines, and expectations."
168 | 								/>
169 | 							</div>
170 | 							<DialogFooter>
171 | 								<Button type="submit" disabled={submitting}>
172 | 									{submitting ? "Submitting..." : "Submit"}
173 | 								</Button>
174 | 							</DialogFooter>
175 | 						</form>
176 | 					</DialogContent>
177 | 				</Dialog>
178 | 			</CardFooter>
179 | 		</Card>
180 | 	);
181 | }
182 | 
```

--------------------------------------------------------------------------------
/docs/content/docs/guides/next-auth-migration-guide.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: Migrating from NextAuth.js to Better Auth
  3 | description: A step-by-step guide to transitioning from NextAuth.js to Better Auth.
  4 | ---
  5 | 
  6 | In this guide, we’ll walk through the steps to migrate a project from [NextAuth.js](https://authjs.dev/) to Better Auth, ensuring no loss of data or functionality. While this guide focuses on Next.js, it can be adapted for other frameworks as well.
  7 | 
  8 | ---
  9 | 
 10 | ## Before You Begin
 11 | 
 12 | Before starting the migration process, set up Better Auth in your project. Follow the [installation guide](/docs/installation) to get started.
 13 | 
 14 | ---
 15 | 
 16 | <Steps>
 17 | <Step>
 18 | ### Mapping Existing Columns
 19 | 
 20 | Instead of altering your existing database column names, you can map them to match Better Auth's expected structure. This allows you to retain your current database schema.
 21 | 
 22 | #### User Schema
 23 | 
 24 | Map the following fields in the user schema:
 25 | 
 26 | - (next-auth v4) `emailVerified`: datetime → boolean
 27 | 
 28 | #### Session Schema
 29 | 
 30 | Map the following fields in the session schema:
 31 | 
 32 | - `expires` → `expiresAt`
 33 | - `sessionToken` → `token`
 34 | - (next-auth v4) add `createdAt` with datetime type
 35 | - (next-auth v4) add `updatedAt` with datetime type
 36 | 
 37 | ```typescript title="auth.ts"
 38 | export const auth = betterAuth({
 39 |     // Other configs
 40 |     session: {
 41 |         fields: {
 42 |             expiresAt: "expires", // Map your existing `expires` field to Better Auth's `expiresAt`
 43 |             token: "sessionToken" // Map your existing `sessionToken` field to Better Auth's `token`
 44 |         }
 45 |     },
 46 | });
 47 | ```
 48 | 
 49 | Make sure to have `createdAt` and `updatedAt` fields on your session schema.
 50 | 
 51 | #### Account Schema
 52 | 
 53 | Map these fields in the account schema:
 54 | 
 55 | - (next-auth v4) `provider` → `providerId`
 56 | - `providerAccountId` → `accountId`
 57 | - `refresh_token` → `refreshToken`
 58 | - `access_token` → `accessToken`
 59 | - (next-auth v3) `access_token_expires` → `accessTokenExpiresAt` and int → datetime
 60 | - (next-auth v4) `expires_at` → `accessTokenExpiresAt` and int → datetime
 61 | - `id_token` → `idToken`
 62 | - (next-auth v4) add `createdAt` with datetime type
 63 | - (next-auth v4) add `updatedAt` with datetime type
 64 | 
 65 | Remove the `session_state`, `type`, and `token_type` fields, as they are not required by Better Auth.
 66 | 
 67 | ```typescript title="auth.ts"
 68 | export const auth = betterAuth({
 69 |     // Other configs
 70 |     account: {
 71 |         fields: {
 72 |             accountId: "providerAccountId",
 73 |             refreshToken: "refresh_token",
 74 |             accessToken: "access_token",
 75 |             accessTokenExpiresAt: "access_token_expires",
 76 |             idToken: "id_token",
 77 |         }
 78 |     },
 79 | });
 80 | ```
 81 | 
 82 | **Note:** If you use ORM adapters, you can map these fields in your schema file.
 83 | 
 84 | **Example with Prisma:**
 85 | 
 86 | ```prisma title="schema.prisma"
 87 | model Session {
 88 |     id          String   @id @default(cuid())
 89 |     expiresAt   DateTime @map("expires") // Map your existing `expires` field to Better Auth's `expiresAt`
 90 |     token       String   @map("sessionToken") // Map your existing `sessionToken` field to Better Auth's `token`
 91 |     userId      String
 92 |     user        User     @relation(fields: [userId], references: [id])
 93 | }
 94 | ```
 95 | 
 96 | Make sure to have `createdAt` and `updatedAt` fields on your account schema.
 97 | </Step>
 98 | <Step>
 99 | 
100 | ### Update the Route Handler
101 | 
102 | In the `app/api/auth` folder, rename the `[...nextauth]` file to `[...all]` to avoid confusion. Then, update the `route.ts` file as follows:
103 | 
104 | ```typescript title="app/api/auth/[...all]/route.ts"
105 | import { toNextJsHandler } from "better-auth/next-js";
106 | import { auth } from "~/server/auth";
107 | 
108 | export const { POST, GET } = toNextJsHandler(auth);
109 | ```
110 | </Step>
111 | 
112 | <Step>
113 | ### Update the Client
114 | 
115 | Create a file named `auth-client.ts` in the `lib` folder. Add the following code:
116 | 
117 | ```typescript title="auth-client.ts"
118 | import { createAuthClient } from "better-auth/react";
119 | 
120 | export const authClient = createAuthClient({
121 |     baseURL: process.env.BASE_URL! // Optional if the API base URL matches the frontend
122 | });
123 | 
124 | export const { signIn, signOut, useSession } = authClient;
125 | ```
126 | 
127 | #### Social Login Functions
128 | 
129 | Update your social login functions to use Better Auth. For example, for Discord:
130 | 
131 | ```typescript
132 | import { signIn } from "~/lib/auth-client";
133 | 
134 | export const signInDiscord = async () => {
135 |     const data = await signIn.social({
136 |         provider: "discord"
137 |     });
138 |     return data;
139 | };
140 | ```
141 | 
142 | #### Update `useSession` Calls
143 | 
144 | Replace `useSession` calls with Better Auth’s version. Example:
145 | 
146 | ```typescript title="Profile.tsx"
147 | import { useSession } from "~/lib/auth-client";
148 | 
149 | export const Profile = () => {
150 |     const { data } = useSession();
151 |     return (
152 |         <div>
153 |             <pre>
154 |                 {JSON.stringify(data, null, 2)}
155 |             </pre>
156 |         </div>
157 |     );
158 | };
159 | ```
160 | </Step>
161 | 
162 | <Step>
163 | 
164 | ### Server-Side Session Handling
165 | 
166 | Use the `auth` instance to get session data on the server:
167 | 
168 | ```typescript title="actions.ts"
169 | "use server";
170 | 
171 | import { auth } from "~/server/auth";
172 | import { headers } from "next/headers";
173 | 
174 | export const protectedAction = async () => {
175 |     const session = await auth.api.getSession({
176 |         headers: await headers(),
177 |     });
178 | };
179 | ```
180 | </Step>
181 | 
182 | <Step>
183 | ### Middleware
184 | 
185 | To protect routes with middleware, refer to the [Next.js middleware guide](/docs/integrations/next#middleware).
186 | </Step>
187 | </Steps>
188 | 
189 | 
190 | ## Wrapping Up
191 | 
192 | Congratulations! You’ve successfully migrated from NextAuth.js to Better Auth. For a complete implementation with multiple authentication methods, check out the [demo repository](https://github.com/Bekacru/t3-app-better-auth).
193 | 
194 | Better Auth offers greater flexibility and more features—be sure to explore the [documentation](/docs) to unlock its full potential.
195 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/init.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, expect, it, vi } from "vitest";
  2 | import { init } from "./init";
  3 | import Database from "better-sqlite3";
  4 | import { betterAuth } from "./auth";
  5 | import { createAuthClient } from "./client";
  6 | import { getTestInstance } from "./test-utils/test-instance";
  7 | 
  8 | describe("init", async () => {
  9 | 	const database = new Database(":memory:");
 10 | 
 11 | 	it("should match config", async () => {
 12 | 		const res = await init({
 13 | 			baseURL: "http://localhost:3000",
 14 | 			database,
 15 | 		});
 16 | 		expect(res).toMatchSnapshot();
 17 | 	});
 18 | 
 19 | 	it("should infer BASE_URL from env", async () => {
 20 | 		vi.stubEnv("BETTER_AUTH_URL", "http://localhost:5147");
 21 | 		const res = await init({
 22 | 			database,
 23 | 		});
 24 | 		expect(res.options.baseURL).toBe("http://localhost:5147");
 25 | 		expect(res.baseURL).toBe("http://localhost:5147/api/auth");
 26 | 		vi.unstubAllEnvs();
 27 | 	});
 28 | 
 29 | 	it("should respect base path", async () => {
 30 | 		const res = await init({
 31 | 			database,
 32 | 			basePath: "/custom-path",
 33 | 			baseURL: "http://localhost:5147",
 34 | 		});
 35 | 		expect(res.baseURL).toBe("http://localhost:5147/custom-path");
 36 | 	});
 37 | 
 38 | 	it("should work with base path", async () => {
 39 | 		const { client } = await getTestInstance({
 40 | 			basePath: "/custom-path",
 41 | 		});
 42 | 
 43 | 		await client.$fetch("/ok", {
 44 | 			onSuccess: (ctx) => {
 45 | 				expect(ctx.data).toMatchObject({
 46 | 					ok: true,
 47 | 				});
 48 | 			},
 49 | 		});
 50 | 	});
 51 | 
 52 | 	it("should execute plugins init", async () => {
 53 | 		const newBaseURL = "http://test.test";
 54 | 		const res = await init({
 55 | 			baseURL: "http://localhost:3000",
 56 | 			database,
 57 | 			plugins: [
 58 | 				{
 59 | 					id: "test",
 60 | 					init: () => {
 61 | 						return {
 62 | 							context: {
 63 | 								baseURL: newBaseURL,
 64 | 							},
 65 | 						};
 66 | 					},
 67 | 				},
 68 | 			],
 69 | 		});
 70 | 		expect(res.baseURL).toBe(newBaseURL);
 71 | 	});
 72 | 
 73 | 	it("should work with custom path", async () => {
 74 | 		const customPath = "/custom-path";
 75 | 		const ctx = await init({
 76 | 			database,
 77 | 			basePath: customPath,
 78 | 			baseURL: "http://localhost:3000",
 79 | 		});
 80 | 		expect(ctx.baseURL).toBe(`http://localhost:3000${customPath}`);
 81 | 
 82 | 		const res = betterAuth({
 83 | 			baseURL: "http://localhost:3000",
 84 | 			database,
 85 | 			basePath: customPath,
 86 | 		});
 87 | 
 88 | 		const client = createAuthClient({
 89 | 			baseURL: `http://localhost:3000/custom-path`,
 90 | 			fetchOptions: {
 91 | 				customFetchImpl: async (url, init) => {
 92 | 					return res.handler(new Request(url, init));
 93 | 				},
 94 | 			},
 95 | 		});
 96 | 		const ok = await client.$fetch("/ok");
 97 | 		expect(ok.data).toMatchObject({
 98 | 			ok: true,
 99 | 		});
100 | 	});
101 | 
102 | 	it("should allow plugins to set config values", async () => {
103 | 		const ctx = await init({
104 | 			database,
105 | 			baseURL: "http://localhost:3000",
106 | 			plugins: [
107 | 				{
108 | 					id: "test-plugin",
109 | 					init(ctx) {
110 | 						return {
111 | 							context: ctx,
112 | 							options: {
113 | 								emailAndPassword: {
114 | 									enabled: true,
115 | 								},
116 | 							},
117 | 						};
118 | 					},
119 | 				},
120 | 			],
121 | 		});
122 | 		expect(ctx.options.emailAndPassword?.enabled).toBe(true);
123 | 	});
124 | 
125 | 	it("should not allow plugins to set config values if they are set in the main config", async () => {
126 | 		const ctx = await init({
127 | 			database,
128 | 			baseURL: "http://localhost:3000",
129 | 			emailAndPassword: {
130 | 				enabled: false,
131 | 			},
132 | 			plugins: [
133 | 				{
134 | 					id: "test-plugin",
135 | 					init(ctx) {
136 | 						return {
137 | 							context: ctx,
138 | 							options: {
139 | 								emailAndPassword: {
140 | 									enabled: true,
141 | 								},
142 | 							},
143 | 						};
144 | 					},
145 | 				},
146 | 			],
147 | 		});
148 | 		expect(ctx.options.emailAndPassword?.enabled).toBe(false);
149 | 	});
150 | 
151 | 	it("should properly pass modfied context from one plugin to another", async () => {
152 | 		const mockProvider = {
153 | 			id: "test-oauth-provider",
154 | 			name: "Test OAuth Provider",
155 | 			createAuthorizationURL: vi.fn(),
156 | 			validateAuthorizationCode: vi.fn(),
157 | 			refreshAccessToken: vi.fn(),
158 | 			getUserInfo: vi.fn(),
159 | 		};
160 | 
161 | 		const ctx = await init({
162 | 			database,
163 | 			baseURL: "http://localhost:3000",
164 | 			socialProviders: {
165 | 				github: {
166 | 					clientId: "test-github-id",
167 | 					clientSecret: "test-github-secret",
168 | 				},
169 | 			},
170 | 			plugins: [
171 | 				{
172 | 					id: "test-oauth-plugin",
173 | 					init(ctx) {
174 | 						return {
175 | 							context: {
176 | 								socialProviders: [mockProvider, ...ctx.socialProviders],
177 | 							},
178 | 						};
179 | 					},
180 | 				},
181 | 				{
182 | 					id: "test-oauth-plugin-2",
183 | 					init(ctx) {
184 | 						return {
185 | 							context: ctx,
186 | 						};
187 | 					},
188 | 				},
189 | 			],
190 | 		});
191 | 		expect(ctx.socialProviders).toHaveLength(2);
192 | 		const testProvider = ctx.socialProviders.find(
193 | 			(p) => p.id === "test-oauth-provider",
194 | 		);
195 | 		expect(testProvider).toBeDefined();
196 | 		expect(testProvider?.refreshAccessToken).toBeDefined();
197 | 		const githubProvider = ctx.socialProviders.find((p) => p.id === "github");
198 | 		expect(githubProvider).toBeDefined();
199 | 	});
200 | 
201 | 	it("should init async plugin", async () => {
202 | 		const initFn = vi.fn(async () => {
203 | 			await new Promise((r) => setTimeout(r, 100));
204 | 			return {
205 | 				context: {
206 | 					baseURL: "http://async.test",
207 | 				},
208 | 			};
209 | 		});
210 | 		await init({
211 | 			baseURL: "http://localhost:3000",
212 | 			database,
213 | 			plugins: [
214 | 				{
215 | 					id: "test-async",
216 | 					init: initFn,
217 | 				},
218 | 			],
219 | 		});
220 | 		expect(initFn).toHaveBeenCalled();
221 | 	});
222 | 
223 | 	it("handles empty basePath", async () => {
224 | 		const res = await init({
225 | 			database,
226 | 			baseURL: "http://localhost:5147/",
227 | 			basePath: "",
228 | 		});
229 | 		expect(res.baseURL).toBe("http://localhost:5147");
230 | 	});
231 | 
232 | 	it("handles root basePath", async () => {
233 | 		const res = await init({
234 | 			database,
235 | 			baseURL: "http://localhost:5147/",
236 | 			basePath: "/",
237 | 		});
238 | 		expect(res.baseURL).toBe("http://localhost:5147");
239 | 	});
240 | 
241 | 	it("normalizes trailing slashes with default path", async () => {
242 | 		const res = await init({
243 | 			database,
244 | 			baseURL: "http://localhost:5147////",
245 | 		});
246 | 		expect(res.baseURL).toBe("http://localhost:5147/api/auth");
247 | 	});
248 | });
249 | 
```

--------------------------------------------------------------------------------
/demo/nextjs/components/ui/carousel.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | "use client";
  2 | 
  3 | import * as React from "react";
  4 | import { ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons";
  5 | import useEmblaCarousel, {
  6 | 	type UseEmblaCarouselType,
  7 | } from "embla-carousel-react";
  8 | 
  9 | import { cn } from "@/lib/utils";
 10 | import { Button } from "@/components/ui/button";
 11 | 
 12 | type CarouselApi = UseEmblaCarouselType[1];
 13 | type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
 14 | type CarouselOptions = UseCarouselParameters[0];
 15 | type CarouselPlugin = UseCarouselParameters[1];
 16 | 
 17 | type CarouselProps = {
 18 | 	opts?: CarouselOptions;
 19 | 	plugins?: CarouselPlugin;
 20 | 	orientation?: "horizontal" | "vertical";
 21 | 	setApi?: (api: CarouselApi) => void;
 22 | };
 23 | 
 24 | type CarouselContextProps = {
 25 | 	carouselRef: ReturnType<typeof useEmblaCarousel>[0];
 26 | 	api: ReturnType<typeof useEmblaCarousel>[1];
 27 | 	scrollPrev: () => void;
 28 | 	scrollNext: () => void;
 29 | 	canScrollPrev: boolean;
 30 | 	canScrollNext: boolean;
 31 | } & CarouselProps;
 32 | 
 33 | const CarouselContext = React.createContext<CarouselContextProps | null>(null);
 34 | 
 35 | function useCarousel() {
 36 | 	const context = React.useContext(CarouselContext);
 37 | 
 38 | 	if (!context) {
 39 | 		throw new Error("useCarousel must be used within a <Carousel />");
 40 | 	}
 41 | 
 42 | 	return context;
 43 | }
 44 | 
 45 | const Carousel = ({
 46 | 	ref,
 47 | 	orientation = "horizontal",
 48 | 	opts,
 49 | 	setApi,
 50 | 	plugins,
 51 | 	className,
 52 | 	children,
 53 | 	...props
 54 | }) => {
 55 | 	const [carouselRef, api] = useEmblaCarousel(
 56 | 		{
 57 | 			...opts,
 58 | 			axis: orientation === "horizontal" ? "x" : "y",
 59 | 		},
 60 | 		plugins,
 61 | 	);
 62 | 	const [canScrollPrev, setCanScrollPrev] = React.useState(false);
 63 | 	const [canScrollNext, setCanScrollNext] = React.useState(false);
 64 | 
 65 | 	const onSelect = React.useCallback((api: CarouselApi) => {
 66 | 		if (!api) {
 67 | 			return;
 68 | 		}
 69 | 
 70 | 		setCanScrollPrev(api.canScrollPrev());
 71 | 		setCanScrollNext(api.canScrollNext());
 72 | 	}, []);
 73 | 
 74 | 	const scrollPrev = React.useCallback(() => {
 75 | 		api?.scrollPrev();
 76 | 	}, [api]);
 77 | 
 78 | 	const scrollNext = React.useCallback(() => {
 79 | 		api?.scrollNext();
 80 | 	}, [api]);
 81 | 
 82 | 	const handleKeyDown = React.useCallback(
 83 | 		(event: React.KeyboardEvent<HTMLDivElement>) => {
 84 | 			if (event.key === "ArrowLeft") {
 85 | 				event.preventDefault();
 86 | 				scrollPrev();
 87 | 			} else if (event.key === "ArrowRight") {
 88 | 				event.preventDefault();
 89 | 				scrollNext();
 90 | 			}
 91 | 		},
 92 | 		[scrollPrev, scrollNext],
 93 | 	);
 94 | 
 95 | 	React.useEffect(() => {
 96 | 		if (!api || !setApi) {
 97 | 			return;
 98 | 		}
 99 | 
100 | 		setApi(api);
101 | 	}, [api, setApi]);
102 | 
103 | 	React.useEffect(() => {
104 | 		if (!api) {
105 | 			return;
106 | 		}
107 | 
108 | 		onSelect(api);
109 | 		api.on("reInit", onSelect);
110 | 		api.on("select", onSelect);
111 | 
112 | 		return () => {
113 | 			api?.off("select", onSelect);
114 | 		};
115 | 	}, [api, onSelect]);
116 | 
117 | 	return (
118 | 		<CarouselContext.Provider
119 | 			value={{
120 | 				carouselRef,
121 | 				api: api,
122 | 				opts,
123 | 				orientation:
124 | 					orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
125 | 				scrollPrev,
126 | 				scrollNext,
127 | 				canScrollPrev,
128 | 				canScrollNext,
129 | 			}}
130 | 		>
131 | 			<div
132 | 				ref={ref}
133 | 				onKeyDownCapture={handleKeyDown}
134 | 				className={cn("relative", className)}
135 | 				role="region"
136 | 				aria-roledescription="carousel"
137 | 				{...props}
138 | 			>
139 | 				{children}
140 | 			</div>
141 | 		</CarouselContext.Provider>
142 | 	);
143 | };
144 | Carousel.displayName = "Carousel";
145 | 
146 | const CarouselContent = ({
147 | 	ref,
148 | 	className,
149 | 	...props
150 | }: React.HTMLAttributes<HTMLDivElement> & {
151 | 	ref: React.RefObject<HTMLDivElement>;
152 | }) => {
153 | 	const { carouselRef, orientation } = useCarousel();
154 | 
155 | 	return (
156 | 		<div ref={carouselRef} className="overflow-hidden">
157 | 			<div
158 | 				ref={ref}
159 | 				className={cn(
160 | 					"flex",
161 | 					orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
162 | 					className,
163 | 				)}
164 | 				{...props}
165 | 			/>
166 | 		</div>
167 | 	);
168 | };
169 | CarouselContent.displayName = "CarouselContent";
170 | 
171 | const CarouselItem = ({
172 | 	ref,
173 | 	className,
174 | 	...props
175 | }: React.HTMLAttributes<HTMLDivElement> & {
176 | 	ref: React.RefObject<HTMLDivElement>;
177 | }) => {
178 | 	const { orientation } = useCarousel();
179 | 
180 | 	return (
181 | 		<div
182 | 			ref={ref}
183 | 			role="group"
184 | 			aria-roledescription="slide"
185 | 			className={cn(
186 | 				"min-w-0 shrink-0 grow-0 basis-full",
187 | 				orientation === "horizontal" ? "pl-4" : "pt-4",
188 | 				className,
189 | 			)}
190 | 			{...props}
191 | 		/>
192 | 	);
193 | };
194 | CarouselItem.displayName = "CarouselItem";
195 | 
196 | const CarouselPrevious = ({
197 | 	ref,
198 | 	className,
199 | 	variant = "outline",
200 | 	size = "icon",
201 | 	...props
202 | }: React.ComponentProps<typeof Button> & {
203 | 	ref: React.RefObject<HTMLButtonElement>;
204 | }) => {
205 | 	const { orientation, scrollPrev, canScrollPrev } = useCarousel();
206 | 
207 | 	return (
208 | 		<Button
209 | 			ref={ref}
210 | 			variant={variant}
211 | 			size={size}
212 | 			className={cn(
213 | 				"absolute  h-8 w-8 rounded-full",
214 | 				orientation === "horizontal"
215 | 					? "-left-12 top-1/2 -translate-y-1/2"
216 | 					: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
217 | 				className,
218 | 			)}
219 | 			disabled={!canScrollPrev}
220 | 			onClick={scrollPrev}
221 | 			{...props}
222 | 		>
223 | 			<ArrowLeftIcon className="h-4 w-4" />
224 | 			<span className="sr-only">Previous slide</span>
225 | 		</Button>
226 | 	);
227 | };
228 | CarouselPrevious.displayName = "CarouselPrevious";
229 | 
230 | const CarouselNext = ({
231 | 	ref,
232 | 	className,
233 | 	variant = "outline",
234 | 	size = "icon",
235 | 	...props
236 | }: React.ComponentProps<typeof Button> & {
237 | 	ref: React.RefObject<HTMLButtonElement>;
238 | }) => {
239 | 	const { orientation, scrollNext, canScrollNext } = useCarousel();
240 | 
241 | 	return (
242 | 		<Button
243 | 			ref={ref}
244 | 			variant={variant}
245 | 			size={size}
246 | 			className={cn(
247 | 				"absolute h-8 w-8 rounded-full",
248 | 				orientation === "horizontal"
249 | 					? "-right-12 top-1/2 -translate-y-1/2"
250 | 					: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
251 | 				className,
252 | 			)}
253 | 			disabled={!canScrollNext}
254 | 			onClick={scrollNext}
255 | 			{...props}
256 | 		>
257 | 			<ArrowRightIcon className="h-4 w-4" />
258 | 			<span className="sr-only">Next slide</span>
259 | 		</Button>
260 | 	);
261 | };
262 | CarouselNext.displayName = "CarouselNext";
263 | 
264 | export {
265 | 	type CarouselApi,
266 | 	Carousel,
267 | 	CarouselContent,
268 | 	CarouselItem,
269 | 	CarouselPrevious,
270 | 	CarouselNext,
271 | };
272 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/mcp/authorize.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { APIError } from "better-call";
  2 | import { getSessionFromCtx } from "../../api";
  3 | import type {
  4 | 	AuthorizationQuery,
  5 | 	Client,
  6 | 	OIDCOptions,
  7 | } from "../oidc-provider/types";
  8 | import { generateRandomString } from "../../crypto";
  9 | import type { GenericEndpointContext } from "@better-auth/core";
 10 | 
 11 | function redirectErrorURL(url: string, error: string, description: string) {
 12 | 	return `${
 13 | 		url.includes("?") ? "&" : "?"
 14 | 	}error=${error}&error_description=${description}`;
 15 | }
 16 | 
 17 | export async function authorizeMCPOAuth(
 18 | 	ctx: GenericEndpointContext,
 19 | 	options: OIDCOptions,
 20 | ) {
 21 | 	ctx.setHeader("Access-Control-Allow-Origin", "*");
 22 | 	ctx.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
 23 | 	ctx.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
 24 | 	ctx.setHeader("Access-Control-Max-Age", "86400");
 25 | 	const opts = {
 26 | 		codeExpiresIn: 600,
 27 | 		defaultScope: "openid",
 28 | 		...options,
 29 | 		scopes: [
 30 | 			"openid",
 31 | 			"profile",
 32 | 			"email",
 33 | 			"offline_access",
 34 | 			...(options?.scopes || []),
 35 | 		],
 36 | 	};
 37 | 	if (!ctx.request) {
 38 | 		throw new APIError("UNAUTHORIZED", {
 39 | 			error_description: "request not found",
 40 | 			error: "invalid_request",
 41 | 		});
 42 | 	}
 43 | 	const session = await getSessionFromCtx(ctx);
 44 | 	if (!session) {
 45 | 		/**
 46 | 		 * If the user is not logged in, we need to redirect them to the
 47 | 		 * login page.
 48 | 		 */
 49 | 		await ctx.setSignedCookie(
 50 | 			"oidc_login_prompt",
 51 | 			JSON.stringify(ctx.query),
 52 | 			ctx.context.secret,
 53 | 			{
 54 | 				maxAge: 600,
 55 | 				path: "/",
 56 | 				sameSite: "lax",
 57 | 			},
 58 | 		);
 59 | 		const queryFromURL = ctx.request.url?.split("?")[1]!;
 60 | 		throw ctx.redirect(`${options.loginPage}?${queryFromURL}`);
 61 | 	}
 62 | 
 63 | 	const query = ctx.query as AuthorizationQuery;
 64 | 	console.log(query);
 65 | 	if (!query.client_id) {
 66 | 		throw ctx.redirect(`${ctx.context.baseURL}/error?error=invalid_client`);
 67 | 	}
 68 | 
 69 | 	if (!query.response_type) {
 70 | 		throw ctx.redirect(
 71 | 			redirectErrorURL(
 72 | 				`${ctx.context.baseURL}/error`,
 73 | 				"invalid_request",
 74 | 				"response_type is required",
 75 | 			),
 76 | 		);
 77 | 	}
 78 | 
 79 | 	const client = await ctx.context.adapter
 80 | 		.findOne<Record<string, any>>({
 81 | 			model: "oauthApplication",
 82 | 			where: [
 83 | 				{
 84 | 					field: "clientId",
 85 | 					value: ctx.query.client_id,
 86 | 				},
 87 | 			],
 88 | 		})
 89 | 		.then((res) => {
 90 | 			if (!res) {
 91 | 				return null;
 92 | 			}
 93 | 			return {
 94 | 				...res,
 95 | 				redirectURLs: res.redirectURLs.split(","),
 96 | 				metadata: res.metadata ? JSON.parse(res.metadata) : {},
 97 | 			} as Client;
 98 | 		});
 99 | 	console.log(client);
100 | 	if (!client) {
101 | 		throw ctx.redirect(`${ctx.context.baseURL}/error?error=invalid_client`);
102 | 	}
103 | 	const redirectURI = client.redirectURLs.find(
104 | 		(url) => url === ctx.query.redirect_uri,
105 | 	);
106 | 
107 | 	if (!redirectURI || !query.redirect_uri) {
108 | 		/**
109 | 		 * show UI error here warning the user that the redirect URI is invalid
110 | 		 */
111 | 		throw new APIError("BAD_REQUEST", {
112 | 			message: "Invalid redirect URI",
113 | 		});
114 | 	}
115 | 	if (client.disabled) {
116 | 		throw ctx.redirect(`${ctx.context.baseURL}/error?error=client_disabled`);
117 | 	}
118 | 
119 | 	if (query.response_type !== "code") {
120 | 		throw ctx.redirect(
121 | 			`${ctx.context.baseURL}/error?error=unsupported_response_type`,
122 | 		);
123 | 	}
124 | 
125 | 	const requestScope =
126 | 		query.scope?.split(" ").filter((s) => s) || opts.defaultScope.split(" ");
127 | 	const invalidScopes = requestScope.filter((scope) => {
128 | 		return !opts.scopes.includes(scope);
129 | 	});
130 | 	if (invalidScopes.length) {
131 | 		throw ctx.redirect(
132 | 			redirectErrorURL(
133 | 				query.redirect_uri,
134 | 				"invalid_scope",
135 | 				`The following scopes are invalid: ${invalidScopes.join(", ")}`,
136 | 			),
137 | 		);
138 | 	}
139 | 
140 | 	if (
141 | 		(!query.code_challenge || !query.code_challenge_method) &&
142 | 		options.requirePKCE
143 | 	) {
144 | 		throw ctx.redirect(
145 | 			redirectErrorURL(
146 | 				query.redirect_uri,
147 | 				"invalid_request",
148 | 				"pkce is required",
149 | 			),
150 | 		);
151 | 	}
152 | 
153 | 	if (!query.code_challenge_method) {
154 | 		query.code_challenge_method = "plain";
155 | 	}
156 | 
157 | 	if (
158 | 		![
159 | 			"s256",
160 | 			options.allowPlainCodeChallengeMethod ? "plain" : "s256",
161 | 		].includes(query.code_challenge_method?.toLowerCase() || "")
162 | 	) {
163 | 		throw ctx.redirect(
164 | 			redirectErrorURL(
165 | 				query.redirect_uri,
166 | 				"invalid_request",
167 | 				"invalid code_challenge method",
168 | 			),
169 | 		);
170 | 	}
171 | 
172 | 	const code = generateRandomString(32, "a-z", "A-Z", "0-9");
173 | 	const codeExpiresInMs = opts.codeExpiresIn * 1000;
174 | 	const expiresAt = new Date(Date.now() + codeExpiresInMs);
175 | 	try {
176 | 		/**
177 | 		 * Save the code in the database
178 | 		 */
179 | 		await ctx.context.internalAdapter.createVerificationValue({
180 | 			value: JSON.stringify({
181 | 				clientId: client.clientId,
182 | 				redirectURI: query.redirect_uri,
183 | 				scope: requestScope,
184 | 				userId: session.user.id,
185 | 				authTime: new Date(session.session.createdAt).getTime(),
186 | 				/**
187 | 				 * If the prompt is set to `consent`, then we need
188 | 				 * to require the user to consent to the scopes.
189 | 				 *
190 | 				 * This means the code now needs to be treated as a
191 | 				 * consent request.
192 | 				 *
193 | 				 * once the user consents, the code will be updated
194 | 				 * with the actual code. This is to prevent the
195 | 				 * client from using the code before the user
196 | 				 * consents.
197 | 				 */
198 | 				requireConsent: query.prompt === "consent",
199 | 				state: query.prompt === "consent" ? query.state : null,
200 | 				codeChallenge: query.code_challenge,
201 | 				codeChallengeMethod: query.code_challenge_method,
202 | 				nonce: query.nonce,
203 | 			}),
204 | 			identifier: code,
205 | 			expiresAt,
206 | 		});
207 | 	} catch (e) {
208 | 		throw ctx.redirect(
209 | 			redirectErrorURL(
210 | 				query.redirect_uri,
211 | 				"server_error",
212 | 				"An error occurred while processing the request",
213 | 			),
214 | 		);
215 | 	}
216 | 
217 | 	const redirectURIWithCode = new URL(redirectURI);
218 | 	redirectURIWithCode.searchParams.set("code", code);
219 | 	redirectURIWithCode.searchParams.set("state", ctx.query.state);
220 | 
221 | 	if (query.prompt !== "consent") {
222 | 		throw ctx.redirect(redirectURIWithCode.toString());
223 | 	}
224 | 
225 | 	throw ctx.redirect(redirectURIWithCode.toString());
226 | }
227 | 
```

--------------------------------------------------------------------------------
/demo/nextjs/components/sign-up.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | "use client";
  2 | 
  3 | import { Button } from "@/components/ui/button";
  4 | import {
  5 | 	Card,
  6 | 	CardContent,
  7 | 	CardDescription,
  8 | 	CardFooter,
  9 | 	CardHeader,
 10 | 	CardTitle,
 11 | } from "@/components/ui/card";
 12 | import { Input } from "@/components/ui/input";
 13 | import { Label } from "@/components/ui/label";
 14 | import { useState, useTransition } from "react";
 15 | import { Loader2, X } from "lucide-react";
 16 | import { signUp } from "@/lib/auth-client";
 17 | import { toast } from "sonner";
 18 | import { useSearchParams, useRouter } from "next/navigation";
 19 | import Link from "next/link";
 20 | import { getCallbackURL } from "@/lib/shared";
 21 | 
 22 | export function SignUp() {
 23 | 	const [firstName, setFirstName] = useState("");
 24 | 	const [lastName, setLastName] = useState("");
 25 | 	const [email, setEmail] = useState("");
 26 | 	const [password, setPassword] = useState("");
 27 | 	const [passwordConfirmation, setPasswordConfirmation] = useState("");
 28 | 	const [image, setImage] = useState<File | null>(null);
 29 | 	const [imagePreview, setImagePreview] = useState<string | null>(null);
 30 | 	const router = useRouter();
 31 | 	const params = useSearchParams();
 32 | 	const [loading, startTransition] = useTransition();
 33 | 
 34 | 	const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
 35 | 		const file = e.target.files?.[0];
 36 | 		if (file) {
 37 | 			setImage(file);
 38 | 			setImagePreview((preview) => {
 39 | 				if (preview) {
 40 | 					URL.revokeObjectURL(preview);
 41 | 				}
 42 | 				return URL.createObjectURL(file);
 43 | 			});
 44 | 		}
 45 | 	};
 46 | 
 47 | 	return (
 48 | 		<Card className="z-50 rounded-md rounded-t-none max-w-md">
 49 | 			<CardHeader>
 50 | 				<CardTitle className="text-lg md:text-xl">Sign Up</CardTitle>
 51 | 				<CardDescription className="text-xs md:text-sm">
 52 | 					Enter your information to create an account
 53 | 				</CardDescription>
 54 | 			</CardHeader>
 55 | 			<CardContent>
 56 | 				<div className="grid gap-4">
 57 | 					<div className="grid grid-cols-2 gap-4">
 58 | 						<div className="grid gap-2">
 59 | 							<Label htmlFor="first-name">First name</Label>
 60 | 							<Input
 61 | 								id="first-name"
 62 | 								placeholder="Max"
 63 | 								required
 64 | 								onChange={(e) => {
 65 | 									setFirstName(e.target.value);
 66 | 								}}
 67 | 								value={firstName}
 68 | 							/>
 69 | 						</div>
 70 | 						<div className="grid gap-2">
 71 | 							<Label htmlFor="last-name">Last name</Label>
 72 | 							<Input
 73 | 								id="last-name"
 74 | 								placeholder="Robinson"
 75 | 								required
 76 | 								onChange={(e) => {
 77 | 									setLastName(e.target.value);
 78 | 								}}
 79 | 								value={lastName}
 80 | 							/>
 81 | 						</div>
 82 | 					</div>
 83 | 					<div className="grid gap-2">
 84 | 						<Label htmlFor="email">Email</Label>
 85 | 						<Input
 86 | 							id="email"
 87 | 							type="email"
 88 | 							placeholder="[email protected]"
 89 | 							required
 90 | 							onChange={(e) => {
 91 | 								setEmail(e.target.value);
 92 | 							}}
 93 | 							value={email}
 94 | 						/>
 95 | 					</div>
 96 | 					<div className="grid gap-2">
 97 | 						<Label htmlFor="password">Password</Label>
 98 | 						<Input
 99 | 							id="password"
100 | 							type="password"
101 | 							value={password}
102 | 							onChange={(e) => setPassword(e.target.value)}
103 | 							autoComplete="new-password"
104 | 							placeholder="Password"
105 | 						/>
106 | 					</div>
107 | 					<div className="grid gap-2">
108 | 						<Label htmlFor="password">Confirm Password</Label>
109 | 						<Input
110 | 							id="password_confirmation"
111 | 							type="password"
112 | 							value={passwordConfirmation}
113 | 							onChange={(e) => setPasswordConfirmation(e.target.value)}
114 | 							autoComplete="new-password"
115 | 							placeholder="Confirm Password"
116 | 						/>
117 | 					</div>
118 | 					<div className="grid gap-2">
119 | 						<Label htmlFor="image">Profile Image (optional)</Label>
120 | 						<div className="flex items-end gap-4">
121 | 							{imagePreview && (
122 | 								<div className="relative w-16 h-16 rounded-sm overflow-hidden">
123 | 									<img
124 | 										src={imagePreview}
125 | 										alt="Profile preview"
126 | 										className="object-cover w-full h-full"
127 | 									/>
128 | 								</div>
129 | 							)}
130 | 							<div className="flex items-center gap-2 w-full">
131 | 								<Input
132 | 									id="image"
133 | 									type="file"
134 | 									accept="image/*"
135 | 									onChange={handleImageChange}
136 | 									className="w-full"
137 | 								/>
138 | 								{imagePreview && (
139 | 									<X
140 | 										className="cursor-pointer"
141 | 										onClick={() => {
142 | 											setImage(null);
143 | 											setImagePreview(null);
144 | 										}}
145 | 									/>
146 | 								)}
147 | 							</div>
148 | 						</div>
149 | 					</div>
150 | 					<Button
151 | 						type="submit"
152 | 						className="w-full"
153 | 						disabled={loading}
154 | 						onClick={async () => {
155 | 							startTransition(async () => {
156 | 								await signUp.email({
157 | 									email,
158 | 									password,
159 | 									name: `${firstName} ${lastName}`,
160 | 									image: image ? await convertImageToBase64(image) : "",
161 | 									callbackURL: "/dashboard",
162 | 									fetchOptions: {
163 | 										onError: (ctx) => {
164 | 											toast.error(ctx.error.message);
165 | 										},
166 | 										onSuccess: async () => {
167 | 											toast.success("Successfully signed up");
168 | 											router.push(getCallbackURL(params));
169 | 										},
170 | 									},
171 | 								});
172 | 							});
173 | 						}}
174 | 					>
175 | 						{loading ? (
176 | 							<Loader2 size={16} className="animate-spin" />
177 | 						) : (
178 | 							"Create an account"
179 | 						)}
180 | 					</Button>
181 | 				</div>
182 | 			</CardContent>
183 | 			<CardFooter>
184 | 				<div className="flex justify-center w-full border-t pt-4">
185 | 					<p className="text-center text-xs text-neutral-500">
186 | 						built with{" "}
187 | 						<Link
188 | 							href="https://better-auth.com"
189 | 							className="underline"
190 | 							target="_blank"
191 | 						>
192 | 							<span className="dark:text-white/70 cursor-pointer">
193 | 								better-auth.
194 | 							</span>
195 | 						</Link>
196 | 					</p>
197 | 				</div>
198 | 			</CardFooter>
199 | 		</Card>
200 | 	);
201 | }
202 | 
203 | async function convertImageToBase64(file: File): Promise<string> {
204 | 	return new Promise((resolve, reject) => {
205 | 		const reader = new FileReader();
206 | 		reader.onloadend = () => resolve(reader.result as string);
207 | 		reader.onerror = reject;
208 | 		reader.readAsDataURL(file);
209 | 	});
210 | }
211 | 
```

--------------------------------------------------------------------------------
/demo/nextjs/app/dashboard/change-plan.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { Button } from "@/components/ui/button";
  2 | import {
  3 | 	Dialog,
  4 | 	DialogContent,
  5 | 	DialogDescription,
  6 | 	DialogHeader,
  7 | 	DialogTitle,
  8 | 	DialogTrigger,
  9 | } from "@/components/ui/dialog";
 10 | import { Label } from "@/components/ui/label";
 11 | import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
 12 | import { client } from "@/lib/auth-client";
 13 | import { cn } from "@/lib/utils";
 14 | import { ArrowUpFromLine, CreditCard, RefreshCcw } from "lucide-react";
 15 | import { useId, useState } from "react";
 16 | import { toast } from "sonner";
 17 | 
 18 | function Component(props: { currentPlan?: string; isTrial?: boolean }) {
 19 | 	const [selectedPlan, setSelectedPlan] = useState("plus");
 20 | 	const id = useId();
 21 | 	return (
 22 | 		<Dialog>
 23 | 			<DialogTrigger asChild>
 24 | 				<Button
 25 | 					variant={!props.currentPlan ? "default" : "outline"}
 26 | 					size="sm"
 27 | 					className={cn(
 28 | 						"gap-2",
 29 | 						!props.currentPlan &&
 30 | 							" bg-linear-to-br from-purple-100 to-stone-300",
 31 | 					)}
 32 | 				>
 33 | 					{props.currentPlan ? (
 34 | 						<RefreshCcw className="opacity-80" size={14} strokeWidth={2} />
 35 | 					) : (
 36 | 						<ArrowUpFromLine className="opacity-80" size={14} strokeWidth={2} />
 37 | 					)}
 38 | 					{props.currentPlan ? "Change Plan" : "Upgrade Plan"}
 39 | 				</Button>
 40 | 			</DialogTrigger>
 41 | 			<DialogContent>
 42 | 				<div className="mb-2 flex flex-col gap-2">
 43 | 					<div
 44 | 						className="flex size-11 shrink-0 items-center justify-center rounded-full border border-border"
 45 | 						aria-hidden="true"
 46 | 					>
 47 | 						{props.currentPlan ? (
 48 | 							<RefreshCcw className="opacity-80" size={16} strokeWidth={2} />
 49 | 						) : (
 50 | 							<CreditCard className="opacity-80" size={16} strokeWidth={2} />
 51 | 						)}
 52 | 					</div>
 53 | 					<DialogHeader>
 54 | 						<DialogTitle className="text-left">
 55 | 							{!props.currentPlan ? "Upgrade" : "Change"} your plan
 56 | 						</DialogTitle>
 57 | 						<DialogDescription className="text-left">
 58 | 							Pick one of the following plans.
 59 | 						</DialogDescription>
 60 | 					</DialogHeader>
 61 | 				</div>
 62 | 
 63 | 				<form className="space-y-5">
 64 | 					<RadioGroup
 65 | 						className="gap-2"
 66 | 						defaultValue="2"
 67 | 						value={selectedPlan}
 68 | 						onValueChange={(value) => setSelectedPlan(value)}
 69 | 					>
 70 | 						<div className="relative flex w-full items-center gap-2 rounded-lg border border-input px-4 py-3 shadow-sm shadow-black/5 has-data-[state=checked]:border-ring has-data-[state=checked]:bg-accent">
 71 | 							<RadioGroupItem
 72 | 								value="plus"
 73 | 								id={`${id}-1`}
 74 | 								aria-describedby={`${id}-1-description`}
 75 | 								className="order-1 after:absolute after:inset-0"
 76 | 							/>
 77 | 							<div className="grid grow gap-1">
 78 | 								<Label htmlFor={`${id}-1`}>Plus</Label>
 79 | 								<p
 80 | 									id={`${id}-1-description`}
 81 | 									className="text-xs text-muted-foreground"
 82 | 								>
 83 | 									$20/month
 84 | 								</p>
 85 | 							</div>
 86 | 						</div>
 87 | 						<div className="relative flex w-full items-center gap-2 rounded-lg border border-input px-4 py-3 shadow-sm shadow-black/5 has-data-[state=checked]:border-ring has-data-[state=checked]:bg-accent">
 88 | 							<RadioGroupItem
 89 | 								value="pro"
 90 | 								id={`${id}-2`}
 91 | 								aria-describedby={`${id}-2-description`}
 92 | 								className="order-1 after:absolute after:inset-0"
 93 | 							/>
 94 | 							<div className="grid grow gap-1">
 95 | 								<Label htmlFor={`${id}-2`}>Pro</Label>
 96 | 								<p
 97 | 									id={`${id}-2-description`}
 98 | 									className="text-xs text-muted-foreground"
 99 | 								>
100 | 									$200/month
101 | 								</p>
102 | 							</div>
103 | 						</div>
104 | 						<div className="relative flex w-full items-center gap-2 rounded-lg border border-input px-4 py-3 shadow-sm shadow-black/5 has-data-[state=checked]:border-ring has-data-[state=checked]:bg-accent">
105 | 							<RadioGroupItem
106 | 								value="enterprise"
107 | 								id={`${id}-3`}
108 | 								aria-describedby={`${id}-3-description`}
109 | 								className="order-1 after:absolute after:inset-0"
110 | 							/>
111 | 							<div className="grid grow gap-1">
112 | 								<Label htmlFor={`${id}-3`}>Enterprise</Label>
113 | 								<p
114 | 									id={`${id}-3-description`}
115 | 									className="text-xs text-muted-foreground"
116 | 								>
117 | 									Contact our sales team
118 | 								</p>
119 | 							</div>
120 | 						</div>
121 | 					</RadioGroup>
122 | 
123 | 					<div className="space-y-3">
124 | 						<p className="text-xs text-white/70 text-center">
125 | 							note: all upgrades takes effect immediately and you'll be charged
126 | 							the new amount on your next billing cycle.
127 | 						</p>
128 | 					</div>
129 | 
130 | 					<div className="grid gap-2">
131 | 						<Button
132 | 							type="button"
133 | 							className="w-full"
134 | 							disabled={
135 | 								selectedPlan === props.currentPlan?.toLowerCase() &&
136 | 								!props.isTrial
137 | 							}
138 | 							onClick={async () => {
139 | 								if (selectedPlan === "enterprise") {
140 | 									return;
141 | 								}
142 | 								await client.subscription.upgrade(
143 | 									{
144 | 										plan: selectedPlan,
145 | 									},
146 | 									{
147 | 										onError: (ctx) => {
148 | 											toast.error(ctx.error.message);
149 | 										},
150 | 									},
151 | 								);
152 | 							}}
153 | 						>
154 | 							{selectedPlan === props.currentPlan?.toLowerCase()
155 | 								? props.isTrial
156 | 									? "Upgrade"
157 | 									: "Current Plan"
158 | 								: selectedPlan === "plus"
159 | 									? !props.currentPlan
160 | 										? "Upgrade"
161 | 										: "Downgrade"
162 | 									: selectedPlan === "pro"
163 | 										? "Upgrade"
164 | 										: "Contact us"}
165 | 						</Button>
166 | 						{props.currentPlan && (
167 | 							<Button
168 | 								type="button"
169 | 								variant="destructive"
170 | 								className="w-full"
171 | 								onClick={async () => {
172 | 									await client.subscription.cancel(
173 | 										{
174 | 											returnUrl: "/dashboard",
175 | 										},
176 | 										{
177 | 											onError: (ctx) => {
178 | 												toast.error(ctx.error.message);
179 | 											},
180 | 										},
181 | 									);
182 | 								}}
183 | 							>
184 | 								Cancel Plan
185 | 							</Button>
186 | 						)}
187 | 					</div>
188 | 				</form>
189 | 			</DialogContent>
190 | 		</Dialog>
191 | 	);
192 | }
193 | 
194 | export { Component };
195 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/oauth2/link-account.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, expect, it, beforeAll, afterAll, afterEach } from "vitest";
  2 | import { setupServer } from "msw/node";
  3 | import { http, HttpResponse } from "msw";
  4 | import { getTestInstance } from "../test-utils/test-instance";
  5 | import type { GoogleProfile } from "@better-auth/core/social-providers";
  6 | import { DEFAULT_SECRET } from "../utils/constants";
  7 | import { signJWT } from "../crypto";
  8 | import type { User } from "../types";
  9 | 
 10 | let mockEmail = "";
 11 | let mockEmailVerified = true;
 12 | 
 13 | const server = setupServer();
 14 | 
 15 | beforeAll(() => {
 16 | 	server.listen({ onUnhandledRequest: "bypass" });
 17 | });
 18 | 
 19 | afterEach(() => {
 20 | 	server.resetHandlers();
 21 | });
 22 | 
 23 | afterAll(() => server.close());
 24 | 
 25 | describe("oauth2 - email verification on link", async () => {
 26 | 	const { auth, client, cookieSetter } = await getTestInstance({
 27 | 		socialProviders: {
 28 | 			google: {
 29 | 				clientId: "test",
 30 | 				clientSecret: "test",
 31 | 				enabled: true,
 32 | 			},
 33 | 		},
 34 | 		emailAndPassword: {
 35 | 			enabled: true,
 36 | 			requireEmailVerification: true,
 37 | 		},
 38 | 		account: {
 39 | 			accountLinking: {
 40 | 				enabled: true,
 41 | 				trustedProviders: ["google"],
 42 | 			},
 43 | 		},
 44 | 	});
 45 | 
 46 | 	const ctx = await auth.$context;
 47 | 
 48 | 	async function linkGoogleAccount() {
 49 | 		server.use(
 50 | 			http.post("https://oauth2.googleapis.com/token", async () => {
 51 | 				const profile: GoogleProfile = {
 52 | 					email: mockEmail,
 53 | 					email_verified: mockEmailVerified,
 54 | 					name: "Test User",
 55 | 					picture: "https://example.com/photo.jpg",
 56 | 					exp: 1234567890,
 57 | 					sub: "google_oauth_sub_1234567890",
 58 | 					iat: 1234567890,
 59 | 					aud: "test",
 60 | 					azp: "test",
 61 | 					nbf: 1234567890,
 62 | 					iss: "test",
 63 | 					locale: "en",
 64 | 					jti: "test",
 65 | 					given_name: "Test",
 66 | 					family_name: "User",
 67 | 				};
 68 | 				const idToken = await signJWT(profile, DEFAULT_SECRET);
 69 | 				return HttpResponse.json({
 70 | 					access_token: "test_access_token",
 71 | 					refresh_token: "test_refresh_token",
 72 | 					id_token: idToken,
 73 | 				});
 74 | 			}),
 75 | 		);
 76 | 
 77 | 		const oAuthHeaders = new Headers();
 78 | 		const signInRes = await client.signIn.social({
 79 | 			provider: "google",
 80 | 			callbackURL: "/",
 81 | 			fetchOptions: {
 82 | 				onSuccess: cookieSetter(oAuthHeaders),
 83 | 			},
 84 | 		});
 85 | 		const state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
 86 | 		await client.$fetch("/callback/google", {
 87 | 			query: { state, code: "test_code" },
 88 | 			method: "GET",
 89 | 			headers: oAuthHeaders,
 90 | 			onError(context) {
 91 | 				expect(context.response.status).toBe(302);
 92 | 			},
 93 | 		});
 94 | 	}
 95 | 
 96 | 	it("should update emailVerified when linking account with verified email", async () => {
 97 | 		const testEmail = "[email protected]";
 98 | 
 99 | 		// Create user with unverified email
100 | 		mockEmail = testEmail;
101 | 		mockEmailVerified = false;
102 | 
103 | 		const signUpRes = await client.signUp.email({
104 | 			email: testEmail,
105 | 			password: "password123",
106 | 			name: "Test User",
107 | 		});
108 | 
109 | 		const userId = signUpRes.data!.user.id;
110 | 
111 | 		// Verify initial state
112 | 		let user = await ctx.adapter.findOne<User>({
113 | 			model: "user",
114 | 			where: [{ field: "id", value: userId }],
115 | 		});
116 | 		expect(user?.emailVerified).toBe(false);
117 | 
118 | 		// Link with Google account that has verified email
119 | 		mockEmailVerified = true;
120 | 		await linkGoogleAccount();
121 | 
122 | 		// Verify email is now verified
123 | 		user = await ctx.adapter.findOne<User>({
124 | 			model: "user",
125 | 			where: [{ field: "id", value: userId }],
126 | 		});
127 | 		expect(user?.emailVerified).toBe(true);
128 | 	});
129 | 
130 | 	it("should not update emailVerified when provider reports unverified", async () => {
131 | 		const testEmail = "[email protected]";
132 | 
133 | 		// Create user with unverified email
134 | 		mockEmail = testEmail;
135 | 		mockEmailVerified = false;
136 | 
137 | 		const signUpRes = await client.signUp.email({
138 | 			email: testEmail,
139 | 			password: "password123",
140 | 			name: "Unverified User",
141 | 		});
142 | 
143 | 		const userId = signUpRes.data!.user.id;
144 | 
145 | 		// Link Google account with unverified email from provider
146 | 		await linkGoogleAccount();
147 | 
148 | 		// Verify email remains unverified
149 | 		const user = await ctx.adapter.findOne<User>({
150 | 			model: "user",
151 | 			where: [{ field: "id", value: userId }],
152 | 		});
153 | 		expect(user?.emailVerified).toBe(false);
154 | 	});
155 | 
156 | 	it("should not update emailVerified when email addresses don't match", async () => {
157 | 		const userEmail = "[email protected]";
158 | 		const googleEmail = "[email protected]";
159 | 
160 | 		// Create user with one email
161 | 		mockEmail = userEmail;
162 | 		mockEmailVerified = false;
163 | 
164 | 		const signUpRes = await client.signUp.email({
165 | 			email: userEmail,
166 | 			password: "password123",
167 | 			name: "Test User",
168 | 		});
169 | 
170 | 		const userId = signUpRes.data!.user.id;
171 | 
172 | 		// Verify initial state
173 | 		let user = await ctx.adapter.findOne<User>({
174 | 			model: "user",
175 | 			where: [{ field: "id", value: userId }],
176 | 		});
177 | 		expect(user?.emailVerified).toBe(false);
178 | 
179 | 		// Try to link with Google using different email (verified)
180 | 		mockEmail = googleEmail;
181 | 		mockEmailVerified = true;
182 | 		await linkGoogleAccount();
183 | 
184 | 		// Verify emailVerified remains false (emails don't match)
185 | 		user = await ctx.adapter.findOne<User>({
186 | 			model: "user",
187 | 			where: [{ field: "id", value: userId }],
188 | 		});
189 | 		expect(user?.emailVerified).toBe(false);
190 | 	});
191 | 
192 | 	it("should handle already verified emails gracefully", async () => {
193 | 		const testEmail = "[email protected]";
194 | 
195 | 		// Create user with verified email
196 | 		mockEmail = testEmail;
197 | 		mockEmailVerified = true;
198 | 
199 | 		const signUpRes = await client.signUp.email({
200 | 			email: testEmail,
201 | 			password: "password123",
202 | 			name: "Verified User",
203 | 		});
204 | 
205 | 		const userId = signUpRes.data!.user.id;
206 | 
207 | 		// Manually set emailVerified to true
208 | 		await ctx.adapter.update({
209 | 			model: "user",
210 | 			where: [{ field: "id", value: userId }],
211 | 			update: { emailVerified: true },
212 | 		});
213 | 
214 | 		// Link with Google account (also verified)
215 | 		await linkGoogleAccount();
216 | 
217 | 		// Verify email remains verified
218 | 		const user = await ctx.adapter.findOne<User>({
219 | 			model: "user",
220 | 			where: [{ field: "id", value: userId }],
221 | 		});
222 | 		expect(user?.emailVerified).toBe(true);
223 | 	});
224 | });
225 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/api/middlewares/origin-check.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { APIError } from "better-call";
  2 | import { createAuthMiddleware } from "@better-auth/core/api";
  3 | import { wildcardMatch } from "../../utils/wildcard";
  4 | import { getHost, getOrigin, getProtocol } from "../../utils/url";
  5 | import type { GenericEndpointContext } from "@better-auth/core";
  6 | 
  7 | /**
  8 |  * A middleware to validate callbackURL and origin against
  9 |  * trustedOrigins.
 10 |  */
 11 | export const originCheckMiddleware = createAuthMiddleware(async (ctx) => {
 12 | 	if (ctx.request?.method !== "POST" || !ctx.request) {
 13 | 		return;
 14 | 	}
 15 | 	const headers = ctx.request?.headers;
 16 | 	const request = ctx.request;
 17 | 	const { body, query, context } = ctx;
 18 | 	/**
 19 | 	 * We only allow requests with the x-auth-request header set to
 20 | 	 * true or application/json content type. This is to prevent
 21 | 	 * simple requests from being processed
 22 | 	 */
 23 | 	if (isSimpleRequest(headers) && !ctx.context.skipCSRFCheck) {
 24 | 		throw new APIError("FORBIDDEN", { message: "Invalid request" });
 25 | 	}
 26 | 	const originHeader = headers?.get("origin") || headers?.get("referer") || "";
 27 | 	const callbackURL = body?.callbackURL || query?.callbackURL;
 28 | 	const redirectURL = body?.redirectTo;
 29 | 	const errorCallbackURL = body?.errorCallbackURL;
 30 | 	const newUserCallbackURL = body?.newUserCallbackURL;
 31 | 
 32 | 	const trustedOrigins: string[] = Array.isArray(context.options.trustedOrigins)
 33 | 		? context.trustedOrigins
 34 | 		: [
 35 | 				...context.trustedOrigins,
 36 | 				...((await context.options.trustedOrigins?.(request)) || []),
 37 | 			];
 38 | 	const useCookies = headers?.has("cookie");
 39 | 
 40 | 	const matchesPattern = (url: string, pattern: string): boolean => {
 41 | 		if (url.startsWith("/")) {
 42 | 			return false;
 43 | 		}
 44 | 		if (pattern.includes("*")) {
 45 | 			// For protocol-specific wildcards, match the full origin
 46 | 			if (pattern.includes("://")) {
 47 | 				return wildcardMatch(pattern)(getOrigin(url) || url);
 48 | 			}
 49 | 			// For host-only wildcards, match just the host
 50 | 			return wildcardMatch(pattern)(getHost(url));
 51 | 		}
 52 | 
 53 | 		const protocol = getProtocol(url);
 54 | 		return protocol === "http:" || protocol === "https:" || !protocol
 55 | 			? pattern === getOrigin(url)
 56 | 			: url.startsWith(pattern);
 57 | 	};
 58 | 	const validateURL = (url: string | undefined, label: string) => {
 59 | 		if (!url) {
 60 | 			return;
 61 | 		}
 62 | 		const isTrustedOrigin = trustedOrigins.some(
 63 | 			(origin) =>
 64 | 				matchesPattern(url, origin) ||
 65 | 				(url?.startsWith("/") &&
 66 | 					label !== "origin" &&
 67 | 					/^\/(?!\/|\\|%2f|%5c)[\w\-.\+/@]*(?:\?[\w\-.\+/=&%@]*)?$/.test(url)),
 68 | 		);
 69 | 		if (!isTrustedOrigin) {
 70 | 			ctx.context.logger.error(`Invalid ${label}: ${url}`);
 71 | 			ctx.context.logger.info(
 72 | 				`If it's a valid URL, please add ${url} to trustedOrigins in your auth config\n`,
 73 | 				`Current list of trustedOrigins: ${trustedOrigins}`,
 74 | 			);
 75 | 			throw new APIError("FORBIDDEN", { message: `Invalid ${label}` });
 76 | 		}
 77 | 	};
 78 | 	if (
 79 | 		useCookies &&
 80 | 		!ctx.context.skipCSRFCheck &&
 81 | 		!ctx.context.skipOriginCheck
 82 | 	) {
 83 | 		if (!originHeader || originHeader === "null") {
 84 | 			throw new APIError("FORBIDDEN", { message: "Missing or null Origin" });
 85 | 		}
 86 | 		validateURL(originHeader, "origin");
 87 | 	}
 88 | 	callbackURL && validateURL(callbackURL, "callbackURL");
 89 | 	redirectURL && validateURL(redirectURL, "redirectURL");
 90 | 	errorCallbackURL && validateURL(errorCallbackURL, "errorCallbackURL");
 91 | 	newUserCallbackURL && validateURL(newUserCallbackURL, "newUserCallbackURL");
 92 | });
 93 | 
 94 | export const originCheck = (
 95 | 	getValue: (ctx: GenericEndpointContext) => string | string[],
 96 | ) =>
 97 | 	createAuthMiddleware(async (ctx) => {
 98 | 		if (!ctx.request) {
 99 | 			return;
100 | 		}
101 | 		const { context } = ctx;
102 | 		const callbackURL = getValue(ctx);
103 | 		const trustedOrigins: string[] = Array.isArray(
104 | 			context.options.trustedOrigins,
105 | 		)
106 | 			? context.trustedOrigins
107 | 			: [
108 | 					...context.trustedOrigins,
109 | 					...((await context.options.trustedOrigins?.(ctx.request)) || []),
110 | 				];
111 | 
112 | 		const matchesPattern = (url: string, pattern: string): boolean => {
113 | 			if (url.startsWith("/")) {
114 | 				return false;
115 | 			}
116 | 			if (pattern.includes("*")) {
117 | 				// For protocol-specific wildcards, match the full origin
118 | 				if (pattern.includes("://")) {
119 | 					return wildcardMatch(pattern)(getOrigin(url) || url);
120 | 				}
121 | 				// For host-only wildcards, match just the host
122 | 				return wildcardMatch(pattern)(getHost(url));
123 | 			}
124 | 			const protocol = getProtocol(url);
125 | 			return protocol === "http:" || protocol === "https:" || !protocol
126 | 				? pattern === getOrigin(url)
127 | 				: url.startsWith(pattern);
128 | 		};
129 | 
130 | 		const validateURL = (url: string | undefined, label: string) => {
131 | 			if (!url) {
132 | 				return;
133 | 			}
134 | 			const isTrustedOrigin = trustedOrigins.some(
135 | 				(origin) =>
136 | 					matchesPattern(url, origin) ||
137 | 					(url?.startsWith("/") &&
138 | 						label !== "origin" &&
139 | 						/^\/(?!\/|\\|%2f|%5c)[\w\-.\+/@]*(?:\?[\w\-.\+/=&%@]*)?$/.test(
140 | 							url,
141 | 						)),
142 | 			);
143 | 			if (!isTrustedOrigin) {
144 | 				ctx.context.logger.error(`Invalid ${label}: ${url}`);
145 | 				ctx.context.logger.info(
146 | 					`If it's a valid URL, please add ${url} to trustedOrigins in your auth config\n`,
147 | 					`Current list of trustedOrigins: ${trustedOrigins}`,
148 | 				);
149 | 				throw new APIError("FORBIDDEN", { message: `Invalid ${label}` });
150 | 			}
151 | 		};
152 | 		const callbacks = Array.isArray(callbackURL) ? callbackURL : [callbackURL];
153 | 		for (const url of callbacks) {
154 | 			validateURL(url, "callbackURL");
155 | 		}
156 | 	});
157 | 
158 | export function isSimpleRequest(headers: Headers) {
159 | 	const SIMPLE_HEADERS = [
160 | 		"accept",
161 | 		"accept-language",
162 | 		"content-language",
163 | 		"content-type",
164 | 	];
165 | 	const SIMPLE_CONTENT_TYPES = [
166 | 		"application/x-www-form-urlencoded",
167 | 		"multipart/form-data",
168 | 		"text/plain",
169 | 	];
170 | 	for (const [key, value] of headers.entries()) {
171 | 		if (!SIMPLE_HEADERS.includes(key.toLowerCase())) {
172 | 			return false; // has non-simple header
173 | 		}
174 | 		if (
175 | 			key.toLowerCase() === "content-type" &&
176 | 			!SIMPLE_CONTENT_TYPES.includes(
177 | 				value?.split(";")[0]?.trim()?.toLowerCase() || "",
178 | 			)
179 | 		) {
180 | 			return false;
181 | 		}
182 | 	}
183 | 	return true;
184 | }
185 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/api-key/routes/list-api-keys.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { sessionMiddleware } from "../../../api";
  2 | import { createAuthEndpoint } from "@better-auth/core/api";
  3 | import type { apiKeySchema } from "../schema";
  4 | import type { ApiKey } from "../types";
  5 | import type { PredefinedApiKeyOptions } from ".";
  6 | import { safeJSONParse } from "../../../utils/json";
  7 | import { API_KEY_TABLE_NAME } from "..";
  8 | import type { AuthContext } from "@better-auth/core";
  9 | export function listApiKeys({
 10 | 	opts,
 11 | 	schema,
 12 | 	deleteAllExpiredApiKeys,
 13 | }: {
 14 | 	opts: PredefinedApiKeyOptions;
 15 | 	schema: ReturnType<typeof apiKeySchema>;
 16 | 	deleteAllExpiredApiKeys(
 17 | 		ctx: AuthContext,
 18 | 		byPassLastCheckTime?: boolean,
 19 | 	): void;
 20 | }) {
 21 | 	return createAuthEndpoint(
 22 | 		"/api-key/list",
 23 | 		{
 24 | 			method: "GET",
 25 | 			use: [sessionMiddleware],
 26 | 			metadata: {
 27 | 				openapi: {
 28 | 					description: "List all API keys for the authenticated user",
 29 | 					responses: {
 30 | 						"200": {
 31 | 							description: "API keys retrieved successfully",
 32 | 							content: {
 33 | 								"application/json": {
 34 | 									schema: {
 35 | 										type: "array",
 36 | 										items: {
 37 | 											type: "object",
 38 | 											properties: {
 39 | 												id: {
 40 | 													type: "string",
 41 | 													description: "ID",
 42 | 												},
 43 | 												name: {
 44 | 													type: "string",
 45 | 													nullable: true,
 46 | 													description: "The name of the key",
 47 | 												},
 48 | 												start: {
 49 | 													type: "string",
 50 | 													nullable: true,
 51 | 													description:
 52 | 														"Shows the first few characters of the API key, including the prefix. This allows you to show those few characters in the UI to make it easier for users to identify the API key.",
 53 | 												},
 54 | 												prefix: {
 55 | 													type: "string",
 56 | 													nullable: true,
 57 | 													description:
 58 | 														"The API Key prefix. Stored as plain text.",
 59 | 												},
 60 | 												userId: {
 61 | 													type: "string",
 62 | 													description: "The owner of the user id",
 63 | 												},
 64 | 												refillInterval: {
 65 | 													type: "number",
 66 | 													nullable: true,
 67 | 													description:
 68 | 														"The interval in milliseconds between refills of the `remaining` count. Example: 3600000 // refill every hour (3600000ms = 1h)",
 69 | 												},
 70 | 												refillAmount: {
 71 | 													type: "number",
 72 | 													nullable: true,
 73 | 													description: "The amount to refill",
 74 | 												},
 75 | 												lastRefillAt: {
 76 | 													type: "string",
 77 | 													format: "date-time",
 78 | 													nullable: true,
 79 | 													description: "The last refill date",
 80 | 												},
 81 | 												enabled: {
 82 | 													type: "boolean",
 83 | 													description: "Sets if key is enabled or disabled",
 84 | 													default: true,
 85 | 												},
 86 | 												rateLimitEnabled: {
 87 | 													type: "boolean",
 88 | 													description:
 89 | 														"Whether the key has rate limiting enabled",
 90 | 												},
 91 | 												rateLimitTimeWindow: {
 92 | 													type: "number",
 93 | 													nullable: true,
 94 | 													description: "The duration in milliseconds",
 95 | 												},
 96 | 												rateLimitMax: {
 97 | 													type: "number",
 98 | 													nullable: true,
 99 | 													description:
100 | 														"Maximum amount of requests allowed within a window",
101 | 												},
102 | 												requestCount: {
103 | 													type: "number",
104 | 													description:
105 | 														"The number of requests made within the rate limit time window",
106 | 												},
107 | 												remaining: {
108 | 													type: "number",
109 | 													nullable: true,
110 | 													description:
111 | 														"Remaining requests (every time api key is used this should updated and should be updated on refill as well)",
112 | 												},
113 | 												lastRequest: {
114 | 													type: "string",
115 | 													format: "date-time",
116 | 													nullable: true,
117 | 													description: "When last request occurred",
118 | 												},
119 | 												expiresAt: {
120 | 													type: "string",
121 | 													format: "date-time",
122 | 													nullable: true,
123 | 													description: "Expiry date of a key",
124 | 												},
125 | 												createdAt: {
126 | 													type: "string",
127 | 													format: "date-time",
128 | 													description: "created at",
129 | 												},
130 | 												updatedAt: {
131 | 													type: "string",
132 | 													format: "date-time",
133 | 													description: "updated at",
134 | 												},
135 | 												metadata: {
136 | 													type: "object",
137 | 													nullable: true,
138 | 													additionalProperties: true,
139 | 													description: "Extra metadata about the apiKey",
140 | 												},
141 | 												permissions: {
142 | 													type: "string",
143 | 													nullable: true,
144 | 													description:
145 | 														"Permissions for the api key (stored as JSON string)",
146 | 												},
147 | 											},
148 | 											required: [
149 | 												"id",
150 | 												"userId",
151 | 												"enabled",
152 | 												"rateLimitEnabled",
153 | 												"requestCount",
154 | 												"createdAt",
155 | 												"updatedAt",
156 | 											],
157 | 										},
158 | 									},
159 | 								},
160 | 							},
161 | 						},
162 | 					},
163 | 				},
164 | 			},
165 | 		},
166 | 		async (ctx) => {
167 | 			const session = ctx.context.session;
168 | 			let apiKeys = await ctx.context.adapter.findMany<ApiKey>({
169 | 				model: API_KEY_TABLE_NAME,
170 | 				where: [
171 | 					{
172 | 						field: "userId",
173 | 						value: session.user.id,
174 | 					},
175 | 				],
176 | 			});
177 | 
178 | 			deleteAllExpiredApiKeys(ctx.context);
179 | 			apiKeys = apiKeys.map((apiKey) => {
180 | 				return {
181 | 					...apiKey,
182 | 					metadata: schema.apikey.fields.metadata.transform.output(
183 | 						apiKey.metadata as never as string,
184 | 					),
185 | 				};
186 | 			});
187 | 
188 | 			let returningApiKey = apiKeys.map((x) => {
189 | 				const { key, ...returningApiKey } = x;
190 | 				return {
191 | 					...returningApiKey,
192 | 					permissions: returningApiKey.permissions
193 | 						? safeJSONParse<{
194 | 								[key: string]: string[];
195 | 							}>(returningApiKey.permissions)
196 | 						: null,
197 | 				};
198 | 			});
199 | 
200 | 			return ctx.json(returningApiKey);
201 | 		},
202 | 	);
203 | }
204 | 
```
Page 18/69FirstPrevNextLast