#
tokens: 47496/50000 2/1099 files (page 58/69)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 58 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/stripe/src/index.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import {
   2 | 	type GenericEndpointContext,
   3 | 	type BetterAuthPlugin,
   4 | 	logger,
   5 | } from "better-auth";
   6 | import {
   7 | 	createAuthEndpoint,
   8 | 	createAuthMiddleware,
   9 | } from "@better-auth/core/api";
  10 | import Stripe from "stripe";
  11 | import { type Stripe as StripeType } from "stripe";
  12 | import * as z from "zod/v4";
  13 | import {
  14 | 	sessionMiddleware,
  15 | 	APIError,
  16 | 	originCheck,
  17 | 	getSessionFromCtx,
  18 | } from "better-auth/api";
  19 | import {
  20 | 	onCheckoutSessionCompleted,
  21 | 	onSubscriptionDeleted,
  22 | 	onSubscriptionUpdated,
  23 | } from "./hooks";
  24 | import type {
  25 | 	InputSubscription,
  26 | 	StripeOptions,
  27 | 	StripePlan,
  28 | 	Subscription,
  29 | } from "./types";
  30 | import { getPlanByName, getPlanByPriceInfo, getPlans } from "./utils";
  31 | import { getSchema } from "./schema";
  32 | import { defu } from "defu";
  33 | import { defineErrorCodes } from "@better-auth/core/utils";
  34 | 
  35 | const STRIPE_ERROR_CODES = defineErrorCodes({
  36 | 	SUBSCRIPTION_NOT_FOUND: "Subscription not found",
  37 | 	SUBSCRIPTION_PLAN_NOT_FOUND: "Subscription plan not found",
  38 | 	ALREADY_SUBSCRIBED_PLAN: "You're already subscribed to this plan",
  39 | 	UNABLE_TO_CREATE_CUSTOMER: "Unable to create customer",
  40 | 	FAILED_TO_FETCH_PLANS: "Failed to fetch plans",
  41 | 	EMAIL_VERIFICATION_REQUIRED:
  42 | 		"Email verification is required before you can subscribe to a plan",
  43 | 	SUBSCRIPTION_NOT_ACTIVE: "Subscription is not active",
  44 | 	SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION:
  45 | 		"Subscription is not scheduled for cancellation",
  46 | });
  47 | 
  48 | const getUrl = (ctx: GenericEndpointContext, url: string) => {
  49 | 	if (url.startsWith("http")) {
  50 | 		return url;
  51 | 	}
  52 | 	return `${ctx.context.options.baseURL}${
  53 | 		url.startsWith("/") ? url : `/${url}`
  54 | 	}`;
  55 | };
  56 | 
  57 | async function resolvePriceIdFromLookupKey(
  58 | 	stripeClient: Stripe,
  59 | 	lookupKey: string,
  60 | ): Promise<string | undefined> {
  61 | 	if (!lookupKey) return undefined;
  62 | 	const prices = await stripeClient.prices.list({
  63 | 		lookup_keys: [lookupKey],
  64 | 		active: true,
  65 | 		limit: 1,
  66 | 	});
  67 | 	return prices.data[0]?.id;
  68 | }
  69 | 
  70 | export const stripe = <O extends StripeOptions>(options: O) => {
  71 | 	const client = options.stripeClient;
  72 | 
  73 | 	const referenceMiddleware = (
  74 | 		action:
  75 | 			| "upgrade-subscription"
  76 | 			| "list-subscription"
  77 | 			| "cancel-subscription"
  78 | 			| "restore-subscription"
  79 | 			| "billing-portal",
  80 | 	) =>
  81 | 		createAuthMiddleware(async (ctx) => {
  82 | 			const session = ctx.context.session;
  83 | 			if (!session) {
  84 | 				throw new APIError("UNAUTHORIZED");
  85 | 			}
  86 | 			const referenceId =
  87 | 				ctx.body?.referenceId || ctx.query?.referenceId || session.user.id;
  88 | 
  89 | 			if (ctx.body?.referenceId && !options.subscription?.authorizeReference) {
  90 | 				logger.error(
  91 | 					`Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`,
  92 | 				);
  93 | 				throw new APIError("BAD_REQUEST", {
  94 | 					message:
  95 | 						"Reference id is not allowed. Read server logs for more details.",
  96 | 				});
  97 | 			}
  98 | 			/**
  99 | 			 * if referenceId is the same as the active session user's id
 100 | 			 */
 101 | 			const sameReference =
 102 | 				ctx.query?.referenceId === session.user.id ||
 103 | 				ctx.body?.referenceId === session.user.id;
 104 | 			const isAuthorized =
 105 | 				ctx.body?.referenceId || ctx.query?.referenceId
 106 | 					? (await options.subscription?.authorizeReference?.(
 107 | 							{
 108 | 								user: session.user,
 109 | 								session: session.session,
 110 | 								referenceId,
 111 | 								action,
 112 | 							},
 113 | 							ctx,
 114 | 						)) || sameReference
 115 | 					: true;
 116 | 			if (!isAuthorized) {
 117 | 				throw new APIError("UNAUTHORIZED", {
 118 | 					message: "Unauthorized",
 119 | 				});
 120 | 			}
 121 | 		});
 122 | 
 123 | 	const subscriptionEndpoints = {
 124 | 		/**
 125 | 		 * ### Endpoint
 126 | 		 *
 127 | 		 * POST `/subscription/upgrade`
 128 | 		 *
 129 | 		 * ### API Methods
 130 | 		 *
 131 | 		 * **server:**
 132 | 		 * `auth.api.upgradeSubscription`
 133 | 		 *
 134 | 		 * **client:**
 135 | 		 * `authClient.subscription.upgrade`
 136 | 		 *
 137 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/stripe#api-method-subscription-upgrade)
 138 | 		 */
 139 | 		upgradeSubscription: createAuthEndpoint(
 140 | 			"/subscription/upgrade",
 141 | 			{
 142 | 				method: "POST",
 143 | 				body: z.object({
 144 | 					/**
 145 | 					 * The name of the plan to subscribe
 146 | 					 */
 147 | 					plan: z.string().meta({
 148 | 						description: 'The name of the plan to upgrade to. Eg: "pro"',
 149 | 					}),
 150 | 					/**
 151 | 					 * If annual plan should be applied.
 152 | 					 */
 153 | 					annual: z
 154 | 						.boolean()
 155 | 						.meta({
 156 | 							description: "Whether to upgrade to an annual plan. Eg: true",
 157 | 						})
 158 | 						.optional(),
 159 | 					/**
 160 | 					 * Reference id of the subscription to upgrade
 161 | 					 * This is used to identify the subscription to upgrade
 162 | 					 * If not provided, the user's id will be used
 163 | 					 */
 164 | 					referenceId: z
 165 | 						.string()
 166 | 						.meta({
 167 | 							description:
 168 | 								'Reference id of the subscription to upgrade. Eg: "123"',
 169 | 						})
 170 | 						.optional(),
 171 | 					/**
 172 | 					 * This is to allow a specific subscription to be upgrade.
 173 | 					 * If subscription id is provided, and subscription isn't found,
 174 | 					 * it'll throw an error.
 175 | 					 */
 176 | 					subscriptionId: z
 177 | 						.string()
 178 | 						.meta({
 179 | 							description:
 180 | 								'The id of the subscription to upgrade. Eg: "sub_123"',
 181 | 						})
 182 | 						.optional(),
 183 | 					/**
 184 | 					 * Any additional data you want to store in your database
 185 | 					 * subscriptions
 186 | 					 */
 187 | 					metadata: z.record(z.string(), z.any()).optional(),
 188 | 					/**
 189 | 					 * If a subscription
 190 | 					 */
 191 | 					seats: z
 192 | 						.number()
 193 | 						.meta({
 194 | 							description:
 195 | 								"Number of seats to upgrade to (if applicable). Eg: 1",
 196 | 						})
 197 | 						.optional(),
 198 | 					/**
 199 | 					 * Success URL to redirect back after successful subscription
 200 | 					 */
 201 | 					successUrl: z
 202 | 						.string()
 203 | 						.meta({
 204 | 							description:
 205 | 								'Callback URL to redirect back after successful subscription. Eg: "https://example.com/success"',
 206 | 						})
 207 | 						.default("/"),
 208 | 					/**
 209 | 					 * Cancel URL
 210 | 					 */
 211 | 					cancelUrl: z
 212 | 						.string()
 213 | 						.meta({
 214 | 							description:
 215 | 								'If set, checkout shows a back button and customers will be directed here if they cancel payment. Eg: "https://example.com/pricing"',
 216 | 						})
 217 | 						.default("/"),
 218 | 					/**
 219 | 					 * Return URL
 220 | 					 */
 221 | 					returnUrl: z
 222 | 						.string()
 223 | 						.meta({
 224 | 							description:
 225 | 								'URL to take customers to when they click on the billing portal’s link to return to your website. Eg: "https://example.com/dashboard"',
 226 | 						})
 227 | 						.optional(),
 228 | 					/**
 229 | 					 * Disable Redirect
 230 | 					 */
 231 | 					disableRedirect: z
 232 | 						.boolean()
 233 | 						.meta({
 234 | 							description:
 235 | 								"Disable redirect after successful subscription. Eg: true",
 236 | 						})
 237 | 						.default(false),
 238 | 				}),
 239 | 				use: [
 240 | 					sessionMiddleware,
 241 | 					originCheck((c) => {
 242 | 						return [c.body.successURL as string, c.body.cancelURL as string];
 243 | 					}),
 244 | 					referenceMiddleware("upgrade-subscription"),
 245 | 				],
 246 | 			},
 247 | 			async (ctx) => {
 248 | 				const { user, session } = ctx.context.session;
 249 | 				if (
 250 | 					!user.emailVerified &&
 251 | 					options.subscription?.requireEmailVerification
 252 | 				) {
 253 | 					throw new APIError("BAD_REQUEST", {
 254 | 						message: STRIPE_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED,
 255 | 					});
 256 | 				}
 257 | 				const referenceId = ctx.body.referenceId || user.id;
 258 | 				const plan = await getPlanByName(options, ctx.body.plan);
 259 | 				if (!plan) {
 260 | 					throw new APIError("BAD_REQUEST", {
 261 | 						message: STRIPE_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND,
 262 | 					});
 263 | 				}
 264 | 				const subscriptionToUpdate = ctx.body.subscriptionId
 265 | 					? await ctx.context.adapter.findOne<Subscription>({
 266 | 							model: "subscription",
 267 | 							where: [
 268 | 								{
 269 | 									field: "id",
 270 | 									value: ctx.body.subscriptionId,
 271 | 									connector: "OR",
 272 | 								},
 273 | 								{
 274 | 									field: "stripeSubscriptionId",
 275 | 									value: ctx.body.subscriptionId,
 276 | 									connector: "OR",
 277 | 								},
 278 | 							],
 279 | 						})
 280 | 					: referenceId
 281 | 						? await ctx.context.adapter.findOne<Subscription>({
 282 | 								model: "subscription",
 283 | 								where: [{ field: "referenceId", value: referenceId }],
 284 | 							})
 285 | 						: null;
 286 | 
 287 | 				if (ctx.body.subscriptionId && !subscriptionToUpdate) {
 288 | 					throw new APIError("BAD_REQUEST", {
 289 | 						message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
 290 | 					});
 291 | 				}
 292 | 
 293 | 				let customerId =
 294 | 					subscriptionToUpdate?.stripeCustomerId || user.stripeCustomerId;
 295 | 
 296 | 				if (!customerId) {
 297 | 					try {
 298 | 						// Try to find existing Stripe customer by email
 299 | 						const existingCustomers = await client.customers.list({
 300 | 							email: user.email,
 301 | 							limit: 1,
 302 | 						});
 303 | 
 304 | 						let stripeCustomer = existingCustomers.data[0];
 305 | 
 306 | 						if (!stripeCustomer) {
 307 | 							stripeCustomer = await client.customers.create({
 308 | 								email: user.email,
 309 | 								name: user.name,
 310 | 								metadata: {
 311 | 									...ctx.body.metadata,
 312 | 									userId: user.id,
 313 | 								},
 314 | 							});
 315 | 						}
 316 | 
 317 | 						// Update local DB with Stripe customer ID
 318 | 						await ctx.context.adapter.update({
 319 | 							model: "user",
 320 | 							update: {
 321 | 								stripeCustomerId: stripeCustomer.id,
 322 | 							},
 323 | 							where: [
 324 | 								{
 325 | 									field: "id",
 326 | 									value: user.id,
 327 | 								},
 328 | 							],
 329 | 						});
 330 | 
 331 | 						customerId = stripeCustomer.id;
 332 | 					} catch (e: any) {
 333 | 						ctx.context.logger.error(e);
 334 | 						throw new APIError("BAD_REQUEST", {
 335 | 							message: STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER,
 336 | 						});
 337 | 					}
 338 | 				}
 339 | 
 340 | 				const subscriptions = subscriptionToUpdate
 341 | 					? [subscriptionToUpdate]
 342 | 					: await ctx.context.adapter.findMany<Subscription>({
 343 | 							model: "subscription",
 344 | 							where: [
 345 | 								{
 346 | 									field: "referenceId",
 347 | 									value: ctx.body.referenceId || user.id,
 348 | 								},
 349 | 							],
 350 | 						});
 351 | 
 352 | 				const activeOrTrialingSubscription = subscriptions.find(
 353 | 					(sub) => sub.status === "active" || sub.status === "trialing",
 354 | 				);
 355 | 
 356 | 				const activeSubscriptions = await client.subscriptions
 357 | 					.list({
 358 | 						customer: customerId,
 359 | 					})
 360 | 					.then((res) =>
 361 | 						res.data.filter(
 362 | 							(sub) => sub.status === "active" || sub.status === "trialing",
 363 | 						),
 364 | 					);
 365 | 
 366 | 				const activeSubscription = activeSubscriptions.find((sub) => {
 367 | 					// If we have a specific subscription to update, match by ID
 368 | 					if (
 369 | 						subscriptionToUpdate?.stripeSubscriptionId ||
 370 | 						ctx.body.subscriptionId
 371 | 					) {
 372 | 						return (
 373 | 							sub.id === subscriptionToUpdate?.stripeSubscriptionId ||
 374 | 							sub.id === ctx.body.subscriptionId
 375 | 						);
 376 | 					}
 377 | 					// Only find subscription for the same referenceId to avoid mixing personal and org subscriptions
 378 | 					if (activeOrTrialingSubscription?.stripeSubscriptionId) {
 379 | 						return sub.id === activeOrTrialingSubscription.stripeSubscriptionId;
 380 | 					}
 381 | 					return false;
 382 | 				});
 383 | 
 384 | 				// Also find any incomplete subscription that we can reuse
 385 | 				const incompleteSubscription = subscriptions.find(
 386 | 					(sub) => sub.status === "incomplete",
 387 | 				);
 388 | 
 389 | 				if (
 390 | 					activeOrTrialingSubscription &&
 391 | 					activeOrTrialingSubscription.status === "active" &&
 392 | 					activeOrTrialingSubscription.plan === ctx.body.plan &&
 393 | 					activeOrTrialingSubscription.seats === (ctx.body.seats || 1)
 394 | 				) {
 395 | 					throw new APIError("BAD_REQUEST", {
 396 | 						message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN,
 397 | 					});
 398 | 				}
 399 | 
 400 | 				if (activeSubscription && customerId) {
 401 | 					// Find the corresponding database subscription for this Stripe subscription
 402 | 					let dbSubscription = await ctx.context.adapter.findOne<Subscription>({
 403 | 						model: "subscription",
 404 | 						where: [
 405 | 							{
 406 | 								field: "stripeSubscriptionId",
 407 | 								value: activeSubscription.id,
 408 | 							},
 409 | 						],
 410 | 					});
 411 | 
 412 | 					// If no database record exists for this Stripe subscription, update the existing one
 413 | 					if (!dbSubscription && activeOrTrialingSubscription) {
 414 | 						await ctx.context.adapter.update<InputSubscription>({
 415 | 							model: "subscription",
 416 | 							update: {
 417 | 								stripeSubscriptionId: activeSubscription.id,
 418 | 								updatedAt: new Date(),
 419 | 							},
 420 | 							where: [
 421 | 								{
 422 | 									field: "id",
 423 | 									value: activeOrTrialingSubscription.id,
 424 | 								},
 425 | 							],
 426 | 						});
 427 | 						dbSubscription = activeOrTrialingSubscription;
 428 | 					}
 429 | 
 430 | 					// Resolve price ID if using lookup keys
 431 | 					let priceIdToUse: string | undefined = undefined;
 432 | 					if (ctx.body.annual) {
 433 | 						priceIdToUse = plan.annualDiscountPriceId;
 434 | 						if (!priceIdToUse && plan.annualDiscountLookupKey) {
 435 | 							priceIdToUse = await resolvePriceIdFromLookupKey(
 436 | 								client,
 437 | 								plan.annualDiscountLookupKey,
 438 | 							);
 439 | 						}
 440 | 					} else {
 441 | 						priceIdToUse = plan.priceId;
 442 | 						if (!priceIdToUse && plan.lookupKey) {
 443 | 							priceIdToUse = await resolvePriceIdFromLookupKey(
 444 | 								client,
 445 | 								plan.lookupKey,
 446 | 							);
 447 | 						}
 448 | 					}
 449 | 
 450 | 					if (!priceIdToUse) {
 451 | 						throw ctx.error("BAD_REQUEST", {
 452 | 							message: "Price ID not found for the selected plan",
 453 | 						});
 454 | 					}
 455 | 
 456 | 					const { url } = await client.billingPortal.sessions
 457 | 						.create({
 458 | 							customer: customerId,
 459 | 							return_url: getUrl(ctx, ctx.body.returnUrl || "/"),
 460 | 							flow_data: {
 461 | 								type: "subscription_update_confirm",
 462 | 								after_completion: {
 463 | 									type: "redirect",
 464 | 									redirect: {
 465 | 										return_url: getUrl(ctx, ctx.body.returnUrl || "/"),
 466 | 									},
 467 | 								},
 468 | 								subscription_update_confirm: {
 469 | 									subscription: activeSubscription.id,
 470 | 									items: [
 471 | 										{
 472 | 											id: activeSubscription.items.data[0]?.id as string,
 473 | 											quantity: ctx.body.seats || 1,
 474 | 											price: priceIdToUse,
 475 | 										},
 476 | 									],
 477 | 								},
 478 | 							},
 479 | 						})
 480 | 						.catch(async (e) => {
 481 | 							throw ctx.error("BAD_REQUEST", {
 482 | 								message: e.message,
 483 | 								code: e.code,
 484 | 							});
 485 | 						});
 486 | 					return ctx.json({
 487 | 						url,
 488 | 						redirect: true,
 489 | 					});
 490 | 				}
 491 | 
 492 | 				let subscription: Subscription | undefined =
 493 | 					activeOrTrialingSubscription || incompleteSubscription;
 494 | 
 495 | 				if (incompleteSubscription && !activeOrTrialingSubscription) {
 496 | 					const updated = await ctx.context.adapter.update<InputSubscription>({
 497 | 						model: "subscription",
 498 | 						update: {
 499 | 							plan: plan.name.toLowerCase(),
 500 | 							seats: ctx.body.seats || 1,
 501 | 							updatedAt: new Date(),
 502 | 						},
 503 | 						where: [
 504 | 							{
 505 | 								field: "id",
 506 | 								value: incompleteSubscription.id,
 507 | 							},
 508 | 						],
 509 | 					});
 510 | 					subscription = (updated as Subscription) || incompleteSubscription;
 511 | 				}
 512 | 
 513 | 				if (!subscription) {
 514 | 					subscription = await ctx.context.adapter.create<
 515 | 						InputSubscription,
 516 | 						Subscription
 517 | 					>({
 518 | 						model: "subscription",
 519 | 						data: {
 520 | 							plan: plan.name.toLowerCase(),
 521 | 							stripeCustomerId: customerId,
 522 | 							status: "incomplete",
 523 | 							referenceId,
 524 | 							seats: ctx.body.seats || 1,
 525 | 						},
 526 | 					});
 527 | 				}
 528 | 
 529 | 				if (!subscription) {
 530 | 					ctx.context.logger.error("Subscription ID not found");
 531 | 					throw new APIError("INTERNAL_SERVER_ERROR");
 532 | 				}
 533 | 
 534 | 				const params = await options.subscription?.getCheckoutSessionParams?.(
 535 | 					{
 536 | 						user,
 537 | 						session,
 538 | 						plan,
 539 | 						subscription,
 540 | 					},
 541 | 					ctx.request,
 542 | 					//@ts-expect-error
 543 | 					ctx,
 544 | 				);
 545 | 
 546 | 				const hasEverTrialed = subscriptions.some((s) => {
 547 | 					// Check if user has ever had a trial for any plan (not just the same plan)
 548 | 					// This prevents users from getting multiple trials by switching plans
 549 | 					const hadTrial =
 550 | 						!!(s.trialStart || s.trialEnd) || s.status === "trialing";
 551 | 					return hadTrial;
 552 | 				});
 553 | 
 554 | 				const freeTrial =
 555 | 					!hasEverTrialed && plan.freeTrial
 556 | 						? { trial_period_days: plan.freeTrial.days }
 557 | 						: undefined;
 558 | 
 559 | 				let priceIdToUse: string | undefined = undefined;
 560 | 				if (ctx.body.annual) {
 561 | 					priceIdToUse = plan.annualDiscountPriceId;
 562 | 					if (!priceIdToUse && plan.annualDiscountLookupKey) {
 563 | 						priceIdToUse = await resolvePriceIdFromLookupKey(
 564 | 							client,
 565 | 							plan.annualDiscountLookupKey,
 566 | 						);
 567 | 					}
 568 | 				} else {
 569 | 					priceIdToUse = plan.priceId;
 570 | 					if (!priceIdToUse && plan.lookupKey) {
 571 | 						priceIdToUse = await resolvePriceIdFromLookupKey(
 572 | 							client,
 573 | 							plan.lookupKey,
 574 | 						);
 575 | 					}
 576 | 				}
 577 | 				const checkoutSession = await client.checkout.sessions
 578 | 					.create(
 579 | 						{
 580 | 							...(customerId
 581 | 								? {
 582 | 										customer: customerId,
 583 | 										customer_update: {
 584 | 											name: "auto",
 585 | 											address: "auto",
 586 | 										},
 587 | 									}
 588 | 								: {
 589 | 										customer_email: session.user.email,
 590 | 									}),
 591 | 							success_url: getUrl(
 592 | 								ctx,
 593 | 								`${
 594 | 									ctx.context.baseURL
 595 | 								}/subscription/success?callbackURL=${encodeURIComponent(
 596 | 									ctx.body.successUrl,
 597 | 								)}&subscriptionId=${encodeURIComponent(subscription.id)}`,
 598 | 							),
 599 | 							cancel_url: getUrl(ctx, ctx.body.cancelUrl),
 600 | 							line_items: [
 601 | 								{
 602 | 									price: priceIdToUse,
 603 | 									quantity: ctx.body.seats || 1,
 604 | 								},
 605 | 							],
 606 | 							subscription_data: {
 607 | 								...freeTrial,
 608 | 							},
 609 | 							mode: "subscription",
 610 | 							client_reference_id: referenceId,
 611 | 							...params?.params,
 612 | 							metadata: {
 613 | 								userId: user.id,
 614 | 								subscriptionId: subscription.id,
 615 | 								referenceId,
 616 | 								...params?.params?.metadata,
 617 | 							},
 618 | 						},
 619 | 						params?.options,
 620 | 					)
 621 | 					.catch(async (e) => {
 622 | 						throw ctx.error("BAD_REQUEST", {
 623 | 							message: e.message,
 624 | 							code: e.code,
 625 | 						});
 626 | 					});
 627 | 				return ctx.json({
 628 | 					...checkoutSession,
 629 | 					redirect: !ctx.body.disableRedirect,
 630 | 				});
 631 | 			},
 632 | 		),
 633 | 		cancelSubscriptionCallback: createAuthEndpoint(
 634 | 			"/subscription/cancel/callback",
 635 | 			{
 636 | 				method: "GET",
 637 | 				query: z.record(z.string(), z.any()).optional(),
 638 | 				use: [originCheck((ctx) => ctx.query.callbackURL)],
 639 | 			},
 640 | 			async (ctx) => {
 641 | 				if (!ctx.query || !ctx.query.callbackURL || !ctx.query.subscriptionId) {
 642 | 					throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
 643 | 				}
 644 | 				const session = await getSessionFromCtx<{ stripeCustomerId: string }>(
 645 | 					ctx,
 646 | 				);
 647 | 				if (!session) {
 648 | 					throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
 649 | 				}
 650 | 				const { user } = session;
 651 | 				const { callbackURL, subscriptionId } = ctx.query;
 652 | 
 653 | 				if (user?.stripeCustomerId) {
 654 | 					try {
 655 | 						const subscription =
 656 | 							await ctx.context.adapter.findOne<Subscription>({
 657 | 								model: "subscription",
 658 | 								where: [
 659 | 									{
 660 | 										field: "id",
 661 | 										value: subscriptionId,
 662 | 									},
 663 | 								],
 664 | 							});
 665 | 						if (
 666 | 							!subscription ||
 667 | 							subscription.cancelAtPeriodEnd ||
 668 | 							subscription.status === "canceled"
 669 | 						) {
 670 | 							throw ctx.redirect(getUrl(ctx, callbackURL));
 671 | 						}
 672 | 
 673 | 						const stripeSubscription = await client.subscriptions.list({
 674 | 							customer: user.stripeCustomerId,
 675 | 							status: "active",
 676 | 						});
 677 | 						const currentSubscription = stripeSubscription.data.find(
 678 | 							(sub) => sub.id === subscription.stripeSubscriptionId,
 679 | 						);
 680 | 						if (currentSubscription?.cancel_at_period_end === true) {
 681 | 							await ctx.context.adapter.update({
 682 | 								model: "subscription",
 683 | 								update: {
 684 | 									status: currentSubscription?.status,
 685 | 									cancelAtPeriodEnd: true,
 686 | 								},
 687 | 								where: [
 688 | 									{
 689 | 										field: "id",
 690 | 										value: subscription.id,
 691 | 									},
 692 | 								],
 693 | 							});
 694 | 							await options.subscription?.onSubscriptionCancel?.({
 695 | 								subscription,
 696 | 								cancellationDetails: currentSubscription.cancellation_details,
 697 | 								stripeSubscription: currentSubscription,
 698 | 								event: undefined,
 699 | 							});
 700 | 						}
 701 | 					} catch (error) {
 702 | 						ctx.context.logger.error(
 703 | 							"Error checking subscription status from Stripe",
 704 | 							error,
 705 | 						);
 706 | 					}
 707 | 				}
 708 | 				throw ctx.redirect(getUrl(ctx, callbackURL));
 709 | 			},
 710 | 		),
 711 | 		/**
 712 | 		 * ### Endpoint
 713 | 		 *
 714 | 		 * POST `/subscription/cancel`
 715 | 		 *
 716 | 		 * ### API Methods
 717 | 		 *
 718 | 		 * **server:**
 719 | 		 * `auth.api.cancelSubscription`
 720 | 		 *
 721 | 		 * **client:**
 722 | 		 * `authClient.subscription.cancel`
 723 | 		 *
 724 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/stripe#api-method-subscription-cancel)
 725 | 		 */
 726 | 		cancelSubscription: createAuthEndpoint(
 727 | 			"/subscription/cancel",
 728 | 			{
 729 | 				method: "POST",
 730 | 				body: z.object({
 731 | 					referenceId: z
 732 | 						.string()
 733 | 						.meta({
 734 | 							description:
 735 | 								"Reference id of the subscription to cancel. Eg: '123'",
 736 | 						})
 737 | 						.optional(),
 738 | 					subscriptionId: z
 739 | 						.string()
 740 | 						.meta({
 741 | 							description:
 742 | 								"The id of the subscription to cancel. Eg: 'sub_123'",
 743 | 						})
 744 | 						.optional(),
 745 | 					returnUrl: z.string().meta({
 746 | 						description:
 747 | 							'URL to take customers to when they click on the billing portal’s link to return to your website. Eg: "https://example.com/dashboard"',
 748 | 					}),
 749 | 				}),
 750 | 				use: [
 751 | 					sessionMiddleware,
 752 | 					originCheck((ctx) => ctx.body.returnUrl),
 753 | 					referenceMiddleware("cancel-subscription"),
 754 | 				],
 755 | 			},
 756 | 			async (ctx) => {
 757 | 				const referenceId =
 758 | 					ctx.body?.referenceId || ctx.context.session.user.id;
 759 | 				const subscription = ctx.body.subscriptionId
 760 | 					? await ctx.context.adapter.findOne<Subscription>({
 761 | 							model: "subscription",
 762 | 							where: [
 763 | 								{
 764 | 									field: "id",
 765 | 									value: ctx.body.subscriptionId,
 766 | 								},
 767 | 							],
 768 | 						})
 769 | 					: await ctx.context.adapter
 770 | 							.findMany<Subscription>({
 771 | 								model: "subscription",
 772 | 								where: [{ field: "referenceId", value: referenceId }],
 773 | 							})
 774 | 							.then((subs) =>
 775 | 								subs.find(
 776 | 									(sub) => sub.status === "active" || sub.status === "trialing",
 777 | 								),
 778 | 							);
 779 | 
 780 | 				if (!subscription || !subscription.stripeCustomerId) {
 781 | 					throw ctx.error("BAD_REQUEST", {
 782 | 						message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
 783 | 					});
 784 | 				}
 785 | 				const activeSubscriptions = await client.subscriptions
 786 | 					.list({
 787 | 						customer: subscription.stripeCustomerId,
 788 | 					})
 789 | 					.then((res) =>
 790 | 						res.data.filter(
 791 | 							(sub) => sub.status === "active" || sub.status === "trialing",
 792 | 						),
 793 | 					);
 794 | 				if (!activeSubscriptions.length) {
 795 | 					/**
 796 | 					 * If the subscription is not found, we need to delete the subscription
 797 | 					 * from the database. This is a rare case and should not happen.
 798 | 					 */
 799 | 					await ctx.context.adapter.deleteMany({
 800 | 						model: "subscription",
 801 | 						where: [
 802 | 							{
 803 | 								field: "referenceId",
 804 | 								value: referenceId,
 805 | 							},
 806 | 						],
 807 | 					});
 808 | 					throw ctx.error("BAD_REQUEST", {
 809 | 						message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
 810 | 					});
 811 | 				}
 812 | 				const activeSubscription = activeSubscriptions.find(
 813 | 					(sub) => sub.id === subscription.stripeSubscriptionId,
 814 | 				);
 815 | 				if (!activeSubscription) {
 816 | 					throw ctx.error("BAD_REQUEST", {
 817 | 						message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
 818 | 					});
 819 | 				}
 820 | 				const { url } = await client.billingPortal.sessions
 821 | 					.create({
 822 | 						customer: subscription.stripeCustomerId,
 823 | 						return_url: getUrl(
 824 | 							ctx,
 825 | 							`${
 826 | 								ctx.context.baseURL
 827 | 							}/subscription/cancel/callback?callbackURL=${encodeURIComponent(
 828 | 								ctx.body?.returnUrl || "/",
 829 | 							)}&subscriptionId=${encodeURIComponent(subscription.id)}`,
 830 | 						),
 831 | 						flow_data: {
 832 | 							type: "subscription_cancel",
 833 | 							subscription_cancel: {
 834 | 								subscription: activeSubscription.id,
 835 | 							},
 836 | 						},
 837 | 					})
 838 | 					.catch(async (e) => {
 839 | 						if (e.message.includes("already set to be cancel")) {
 840 | 							/**
 841 | 							 * incase we missed the event from stripe, we set it manually
 842 | 							 * this is a rare case and should not happen
 843 | 							 */
 844 | 							if (!subscription.cancelAtPeriodEnd) {
 845 | 								await ctx.context.adapter.update({
 846 | 									model: "subscription",
 847 | 									update: {
 848 | 										cancelAtPeriodEnd: true,
 849 | 									},
 850 | 									where: [
 851 | 										{
 852 | 											field: "referenceId",
 853 | 											value: referenceId,
 854 | 										},
 855 | 									],
 856 | 								});
 857 | 							}
 858 | 						}
 859 | 						throw ctx.error("BAD_REQUEST", {
 860 | 							message: e.message,
 861 | 							code: e.code,
 862 | 						});
 863 | 					});
 864 | 				return {
 865 | 					url,
 866 | 					redirect: true,
 867 | 				};
 868 | 			},
 869 | 		),
 870 | 		restoreSubscription: createAuthEndpoint(
 871 | 			"/subscription/restore",
 872 | 			{
 873 | 				method: "POST",
 874 | 				body: z.object({
 875 | 					referenceId: z
 876 | 						.string()
 877 | 						.meta({
 878 | 							description:
 879 | 								"Reference id of the subscription to restore. Eg: '123'",
 880 | 						})
 881 | 						.optional(),
 882 | 					subscriptionId: z
 883 | 						.string()
 884 | 						.meta({
 885 | 							description:
 886 | 								"The id of the subscription to restore. Eg: 'sub_123'",
 887 | 						})
 888 | 						.optional(),
 889 | 				}),
 890 | 				use: [sessionMiddleware, referenceMiddleware("restore-subscription")],
 891 | 			},
 892 | 			async (ctx) => {
 893 | 				const referenceId =
 894 | 					ctx.body?.referenceId || ctx.context.session.user.id;
 895 | 
 896 | 				const subscription = ctx.body.subscriptionId
 897 | 					? await ctx.context.adapter.findOne<Subscription>({
 898 | 							model: "subscription",
 899 | 							where: [
 900 | 								{
 901 | 									field: "id",
 902 | 									value: ctx.body.subscriptionId,
 903 | 								},
 904 | 							],
 905 | 						})
 906 | 					: await ctx.context.adapter
 907 | 							.findMany<Subscription>({
 908 | 								model: "subscription",
 909 | 								where: [
 910 | 									{
 911 | 										field: "referenceId",
 912 | 										value: referenceId,
 913 | 									},
 914 | 								],
 915 | 							})
 916 | 							.then((subs) =>
 917 | 								subs.find(
 918 | 									(sub) => sub.status === "active" || sub.status === "trialing",
 919 | 								),
 920 | 							);
 921 | 				if (!subscription || !subscription.stripeCustomerId) {
 922 | 					throw ctx.error("BAD_REQUEST", {
 923 | 						message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
 924 | 					});
 925 | 				}
 926 | 				if (
 927 | 					subscription.status != "active" &&
 928 | 					subscription.status != "trialing"
 929 | 				) {
 930 | 					throw ctx.error("BAD_REQUEST", {
 931 | 						message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_ACTIVE,
 932 | 					});
 933 | 				}
 934 | 				if (!subscription.cancelAtPeriodEnd) {
 935 | 					throw ctx.error("BAD_REQUEST", {
 936 | 						message:
 937 | 							STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION,
 938 | 					});
 939 | 				}
 940 | 
 941 | 				const activeSubscription = await client.subscriptions
 942 | 					.list({
 943 | 						customer: subscription.stripeCustomerId,
 944 | 					})
 945 | 					.then(
 946 | 						(res) =>
 947 | 							res.data.filter(
 948 | 								(sub) => sub.status === "active" || sub.status === "trialing",
 949 | 							)[0],
 950 | 					);
 951 | 				if (!activeSubscription) {
 952 | 					throw ctx.error("BAD_REQUEST", {
 953 | 						message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
 954 | 					});
 955 | 				}
 956 | 
 957 | 				try {
 958 | 					const newSub = await client.subscriptions.update(
 959 | 						activeSubscription.id,
 960 | 						{
 961 | 							cancel_at_period_end: false,
 962 | 						},
 963 | 					);
 964 | 
 965 | 					await ctx.context.adapter.update({
 966 | 						model: "subscription",
 967 | 						update: {
 968 | 							cancelAtPeriodEnd: false,
 969 | 							updatedAt: new Date(),
 970 | 						},
 971 | 						where: [
 972 | 							{
 973 | 								field: "id",
 974 | 								value: subscription.id,
 975 | 							},
 976 | 						],
 977 | 					});
 978 | 
 979 | 					return ctx.json(newSub);
 980 | 				} catch (error) {
 981 | 					ctx.context.logger.error("Error restoring subscription", error);
 982 | 					throw new APIError("BAD_REQUEST", {
 983 | 						message: STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER,
 984 | 					});
 985 | 				}
 986 | 			},
 987 | 		),
 988 | 		/**
 989 | 		 * ### Endpoint
 990 | 		 *
 991 | 		 * GET `/subscription/list`
 992 | 		 *
 993 | 		 * ### API Methods
 994 | 		 *
 995 | 		 * **server:**
 996 | 		 * `auth.api.listActiveSubscriptions`
 997 | 		 *
 998 | 		 * **client:**
 999 | 		 * `authClient.subscription.list`
1000 | 		 *
1001 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/stripe#api-method-subscription-list)
1002 | 		 */
1003 | 		listActiveSubscriptions: createAuthEndpoint(
1004 | 			"/subscription/list",
1005 | 			{
1006 | 				method: "GET",
1007 | 				query: z.optional(
1008 | 					z.object({
1009 | 						referenceId: z
1010 | 							.string()
1011 | 							.meta({
1012 | 								description:
1013 | 									"Reference id of the subscription to list. Eg: '123'",
1014 | 							})
1015 | 							.optional(),
1016 | 					}),
1017 | 				),
1018 | 				use: [sessionMiddleware, referenceMiddleware("list-subscription")],
1019 | 			},
1020 | 			async (ctx) => {
1021 | 				const subscriptions = await ctx.context.adapter.findMany<Subscription>({
1022 | 					model: "subscription",
1023 | 					where: [
1024 | 						{
1025 | 							field: "referenceId",
1026 | 							value: ctx.query?.referenceId || ctx.context.session.user.id,
1027 | 						},
1028 | 					],
1029 | 				});
1030 | 				if (!subscriptions.length) {
1031 | 					return [];
1032 | 				}
1033 | 				const plans = await getPlans(options);
1034 | 				if (!plans) {
1035 | 					return [];
1036 | 				}
1037 | 				const subs = subscriptions
1038 | 					.map((sub) => {
1039 | 						const plan = plans.find(
1040 | 							(p) => p.name.toLowerCase() === sub.plan.toLowerCase(),
1041 | 						);
1042 | 						return {
1043 | 							...sub,
1044 | 							limits: plan?.limits,
1045 | 							priceId: plan?.priceId,
1046 | 						};
1047 | 					})
1048 | 					.filter((sub) => {
1049 | 						return sub.status === "active" || sub.status === "trialing";
1050 | 					});
1051 | 				return ctx.json(subs);
1052 | 			},
1053 | 		),
1054 | 		subscriptionSuccess: createAuthEndpoint(
1055 | 			"/subscription/success",
1056 | 			{
1057 | 				method: "GET",
1058 | 				query: z.record(z.string(), z.any()).optional(),
1059 | 				use: [originCheck((ctx) => ctx.query.callbackURL)],
1060 | 			},
1061 | 			async (ctx) => {
1062 | 				if (!ctx.query || !ctx.query.callbackURL || !ctx.query.subscriptionId) {
1063 | 					throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
1064 | 				}
1065 | 				const session = await getSessionFromCtx<{ stripeCustomerId: string }>(
1066 | 					ctx,
1067 | 				);
1068 | 				if (!session) {
1069 | 					throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
1070 | 				}
1071 | 				const { user } = session;
1072 | 				const { callbackURL, subscriptionId } = ctx.query;
1073 | 
1074 | 				const subscription = await ctx.context.adapter.findOne<Subscription>({
1075 | 					model: "subscription",
1076 | 					where: [
1077 | 						{
1078 | 							field: "id",
1079 | 							value: subscriptionId,
1080 | 						},
1081 | 					],
1082 | 				});
1083 | 
1084 | 				if (
1085 | 					subscription?.status === "active" ||
1086 | 					subscription?.status === "trialing"
1087 | 				) {
1088 | 					return ctx.redirect(getUrl(ctx, callbackURL));
1089 | 				}
1090 | 				const customerId =
1091 | 					subscription?.stripeCustomerId || user.stripeCustomerId;
1092 | 
1093 | 				if (customerId) {
1094 | 					try {
1095 | 						const stripeSubscription = await client.subscriptions
1096 | 							.list({
1097 | 								customer: customerId,
1098 | 								status: "active",
1099 | 							})
1100 | 							.then((res) => res.data[0]);
1101 | 
1102 | 						if (stripeSubscription) {
1103 | 							const plan = await getPlanByPriceInfo(
1104 | 								options,
1105 | 								stripeSubscription.items.data[0]?.price.id!,
1106 | 								stripeSubscription.items.data[0]?.price.lookup_key!,
1107 | 							);
1108 | 
1109 | 							if (plan && subscription) {
1110 | 								await ctx.context.adapter.update({
1111 | 									model: "subscription",
1112 | 									update: {
1113 | 										status: stripeSubscription.status,
1114 | 										seats: stripeSubscription.items.data[0]?.quantity || 1,
1115 | 										plan: plan.name.toLowerCase(),
1116 | 										periodEnd: new Date(
1117 | 											stripeSubscription.items.data[0]?.current_period_end! *
1118 | 												1000,
1119 | 										),
1120 | 										periodStart: new Date(
1121 | 											stripeSubscription.items.data[0]?.current_period_start! *
1122 | 												1000,
1123 | 										),
1124 | 										stripeSubscriptionId: stripeSubscription.id,
1125 | 										...(stripeSubscription.trial_start &&
1126 | 										stripeSubscription.trial_end
1127 | 											? {
1128 | 													trialStart: new Date(
1129 | 														stripeSubscription.trial_start * 1000,
1130 | 													),
1131 | 													trialEnd: new Date(
1132 | 														stripeSubscription.trial_end * 1000,
1133 | 													),
1134 | 												}
1135 | 											: {}),
1136 | 									},
1137 | 									where: [
1138 | 										{
1139 | 											field: "id",
1140 | 											value: subscription.id,
1141 | 										},
1142 | 									],
1143 | 								});
1144 | 							}
1145 | 						}
1146 | 					} catch (error) {
1147 | 						ctx.context.logger.error(
1148 | 							"Error fetching subscription from Stripe",
1149 | 							error,
1150 | 						);
1151 | 					}
1152 | 				}
1153 | 				throw ctx.redirect(getUrl(ctx, callbackURL));
1154 | 			},
1155 | 		),
1156 | 		createBillingPortal: createAuthEndpoint(
1157 | 			"/subscription/billing-portal",
1158 | 			{
1159 | 				method: "POST",
1160 | 				body: z.object({
1161 | 					locale: z
1162 | 						.custom<StripeType.Checkout.Session.Locale>((localization) => {
1163 | 							return typeof localization === "string";
1164 | 						})
1165 | 						.optional(),
1166 | 					referenceId: z.string().optional(),
1167 | 					returnUrl: z.string().default("/"),
1168 | 				}),
1169 | 				use: [
1170 | 					sessionMiddleware,
1171 | 					originCheck((ctx) => ctx.body.returnUrl),
1172 | 					referenceMiddleware("billing-portal"),
1173 | 				],
1174 | 			},
1175 | 			async (ctx) => {
1176 | 				const { user } = ctx.context.session;
1177 | 				const referenceId = ctx.body.referenceId || user.id;
1178 | 
1179 | 				let customerId = user.stripeCustomerId;
1180 | 
1181 | 				if (!customerId) {
1182 | 					const subscription = await ctx.context.adapter
1183 | 						.findMany<Subscription>({
1184 | 							model: "subscription",
1185 | 							where: [
1186 | 								{
1187 | 									field: "referenceId",
1188 | 									value: referenceId,
1189 | 								},
1190 | 							],
1191 | 						})
1192 | 						.then((subs) =>
1193 | 							subs.find(
1194 | 								(sub) => sub.status === "active" || sub.status === "trialing",
1195 | 							),
1196 | 						);
1197 | 
1198 | 					customerId = subscription?.stripeCustomerId;
1199 | 				}
1200 | 
1201 | 				if (!customerId) {
1202 | 					throw new APIError("BAD_REQUEST", {
1203 | 						message: "No Stripe customer found for this user",
1204 | 					});
1205 | 				}
1206 | 
1207 | 				try {
1208 | 					const { url } = await client.billingPortal.sessions.create({
1209 | 						locale: ctx.body.locale,
1210 | 						customer: customerId,
1211 | 						return_url: getUrl(ctx, ctx.body.returnUrl),
1212 | 					});
1213 | 
1214 | 					return ctx.json({
1215 | 						url,
1216 | 						redirect: true,
1217 | 					});
1218 | 				} catch (error: any) {
1219 | 					ctx.context.logger.error(
1220 | 						"Error creating billing portal session",
1221 | 						error,
1222 | 					);
1223 | 					throw new APIError("BAD_REQUEST", {
1224 | 						message: error.message,
1225 | 					});
1226 | 				}
1227 | 			},
1228 | 		),
1229 | 	} as const;
1230 | 	return {
1231 | 		id: "stripe",
1232 | 		endpoints: {
1233 | 			stripeWebhook: createAuthEndpoint(
1234 | 				"/stripe/webhook",
1235 | 				{
1236 | 					method: "POST",
1237 | 					metadata: {
1238 | 						isAction: false,
1239 | 					},
1240 | 					cloneRequest: true,
1241 | 					//don't parse the body
1242 | 					disableBody: true,
1243 | 				},
1244 | 				async (ctx) => {
1245 | 					if (!ctx.request?.body) {
1246 | 						throw new APIError("INTERNAL_SERVER_ERROR");
1247 | 					}
1248 | 					const buf = await ctx.request.text();
1249 | 					const sig = ctx.request.headers.get("stripe-signature") as string;
1250 | 					const webhookSecret = options.stripeWebhookSecret;
1251 | 					let event: Stripe.Event;
1252 | 					try {
1253 | 						if (!sig || !webhookSecret) {
1254 | 							throw new APIError("BAD_REQUEST", {
1255 | 								message: "Stripe webhook secret not found",
1256 | 							});
1257 | 						}
1258 | 						// Support both Stripe v18 (constructEvent) and v19+ (constructEventAsync)
1259 | 						if (typeof client.webhooks.constructEventAsync === "function") {
1260 | 							// Stripe v19+ - use async method
1261 | 							event = await client.webhooks.constructEventAsync(
1262 | 								buf,
1263 | 								sig,
1264 | 								webhookSecret,
1265 | 							);
1266 | 						} else {
1267 | 							// Stripe v18 - use sync method
1268 | 							event = client.webhooks.constructEvent(buf, sig, webhookSecret);
1269 | 						}
1270 | 					} catch (err: any) {
1271 | 						ctx.context.logger.error(`${err.message}`);
1272 | 						throw new APIError("BAD_REQUEST", {
1273 | 							message: `Webhook Error: ${err.message}`,
1274 | 						});
1275 | 					}
1276 | 					if (!event) {
1277 | 						throw new APIError("BAD_REQUEST", {
1278 | 							message: "Failed to construct event",
1279 | 						});
1280 | 					}
1281 | 					try {
1282 | 						switch (event.type) {
1283 | 							case "checkout.session.completed":
1284 | 								await onCheckoutSessionCompleted(ctx, options, event);
1285 | 								await options.onEvent?.(event);
1286 | 								break;
1287 | 							case "customer.subscription.updated":
1288 | 								await onSubscriptionUpdated(ctx, options, event);
1289 | 								await options.onEvent?.(event);
1290 | 								break;
1291 | 							case "customer.subscription.deleted":
1292 | 								await onSubscriptionDeleted(ctx, options, event);
1293 | 								await options.onEvent?.(event);
1294 | 								break;
1295 | 							default:
1296 | 								await options.onEvent?.(event);
1297 | 								break;
1298 | 						}
1299 | 					} catch (e: any) {
1300 | 						ctx.context.logger.error(
1301 | 							`Stripe webhook failed. Error: ${e.message}`,
1302 | 						);
1303 | 						throw new APIError("BAD_REQUEST", {
1304 | 							message: "Webhook error: See server logs for more information.",
1305 | 						});
1306 | 					}
1307 | 					return ctx.json({ success: true });
1308 | 				},
1309 | 			),
1310 | 			...((options.subscription?.enabled
1311 | 				? subscriptionEndpoints
1312 | 				: {}) as O["subscription"] extends {
1313 | 				enabled: boolean;
1314 | 			}
1315 | 				? typeof subscriptionEndpoints
1316 | 				: {}),
1317 | 		},
1318 | 		init(ctx) {
1319 | 			return {
1320 | 				options: {
1321 | 					databaseHooks: {
1322 | 						user: {
1323 | 							create: {
1324 | 								async after(user, ctx) {
1325 | 									if (ctx && options.createCustomerOnSignUp) {
1326 | 										let extraCreateParams: Partial<Stripe.CustomerCreateParams> =
1327 | 											{};
1328 | 										if (options.getCustomerCreateParams) {
1329 | 											extraCreateParams = await options.getCustomerCreateParams(
1330 | 												user,
1331 | 												ctx,
1332 | 											);
1333 | 										}
1334 | 
1335 | 										const params: Stripe.CustomerCreateParams = defu(
1336 | 											{
1337 | 												email: user.email,
1338 | 												name: user.name,
1339 | 												metadata: {
1340 | 													userId: user.id,
1341 | 												},
1342 | 											},
1343 | 											extraCreateParams,
1344 | 										);
1345 | 										const stripeCustomer =
1346 | 											await client.customers.create(params);
1347 | 										await ctx.context.internalAdapter.updateUser(user.id, {
1348 | 											stripeCustomerId: stripeCustomer.id,
1349 | 										});
1350 | 										await options.onCustomerCreate?.(
1351 | 											{
1352 | 												stripeCustomer,
1353 | 												user: {
1354 | 													...user,
1355 | 													stripeCustomerId: stripeCustomer.id,
1356 | 												},
1357 | 											},
1358 | 											ctx,
1359 | 										);
1360 | 									}
1361 | 								},
1362 | 							},
1363 | 							update: {
1364 | 								async after(user, ctx) {
1365 | 									if (!ctx) return;
1366 | 
1367 | 									try {
1368 | 										// Cast user to include stripeCustomerId (added by the stripe plugin schema)
1369 | 										const userWithStripe = user as typeof user & {
1370 | 											stripeCustomerId?: string;
1371 | 										};
1372 | 
1373 | 										// Only proceed if user has a Stripe customer ID
1374 | 										if (!userWithStripe.stripeCustomerId) return;
1375 | 
1376 | 										// Get the user from the database to check if email actually changed
1377 | 										// The 'user' parameter here is the freshly updated user
1378 | 										// We need to check if the Stripe customer's email matches
1379 | 										const stripeCustomer = await client.customers.retrieve(
1380 | 											userWithStripe.stripeCustomerId,
1381 | 										);
1382 | 
1383 | 										// Check if customer was deleted
1384 | 										if (stripeCustomer.deleted) {
1385 | 											ctx.context.logger.warn(
1386 | 												`Stripe customer ${userWithStripe.stripeCustomerId} was deleted, cannot update email`,
1387 | 											);
1388 | 											return;
1389 | 										}
1390 | 
1391 | 										// If Stripe customer email doesn't match the user's current email, update it
1392 | 										if (stripeCustomer.email !== user.email) {
1393 | 											await client.customers.update(
1394 | 												userWithStripe.stripeCustomerId,
1395 | 												{
1396 | 													email: user.email,
1397 | 												},
1398 | 											);
1399 | 											ctx.context.logger.info(
1400 | 												`Updated Stripe customer email from ${stripeCustomer.email} to ${user.email}`,
1401 | 											);
1402 | 										}
1403 | 									} catch (e: any) {
1404 | 										// Ignore errors - this is a best-effort sync
1405 | 										// Email might have been deleted or Stripe customer might not exist
1406 | 										ctx.context.logger.error(
1407 | 											`Failed to sync email to Stripe customer: ${e.message}`,
1408 | 											e,
1409 | 										);
1410 | 									}
1411 | 								},
1412 | 							},
1413 | 						},
1414 | 					},
1415 | 				},
1416 | 			};
1417 | 		},
1418 | 		schema: getSchema(options),
1419 | 		$ERROR_CODES: STRIPE_ERROR_CODES,
1420 | 	} satisfies BetterAuthPlugin;
1421 | };
1422 | 
1423 | export type { Subscription, StripePlan };
1424 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/admin/admin.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import * as z from "zod";
   2 | import { APIError, getSessionFromCtx } from "../../api";
   3 | import {
   4 | 	createAuthEndpoint,
   5 | 	createAuthMiddleware,
   6 | } from "@better-auth/core/api";
   7 | import { type Session } from "../../types";
   8 | import type { BetterAuthPlugin } from "@better-auth/core";
   9 | import type { Where } from "@better-auth/core/db/adapter";
  10 | import { deleteSessionCookie, setSessionCookie } from "../../cookies";
  11 | import { getDate } from "../../utils/date";
  12 | import { getEndpointResponse } from "../../utils/plugin-helper";
  13 | import { mergeSchema, parseUserOutput } from "../../db/schema";
  14 | import { type AccessControl } from "../access";
  15 | import { ADMIN_ERROR_CODES } from "./error-codes";
  16 | import { defaultStatements } from "./access";
  17 | import { hasPermission } from "./has-permission";
  18 | import { BASE_ERROR_CODES } from "@better-auth/core/error";
  19 | import { schema } from "./schema";
  20 | import type {
  21 | 	AdminOptions,
  22 | 	InferAdminRolesFromOption,
  23 | 	SessionWithImpersonatedBy,
  24 | 	UserWithRole,
  25 | } from "./types";
  26 | 
  27 | function parseRoles(roles: string | string[]): string {
  28 | 	return Array.isArray(roles) ? roles.join(",") : roles;
  29 | }
  30 | 
  31 | export const admin = <O extends AdminOptions>(options?: O) => {
  32 | 	const opts = {
  33 | 		defaultRole: options?.defaultRole ?? "user",
  34 | 		adminRoles: options?.adminRoles ?? ["admin"],
  35 | 		bannedUserMessage:
  36 | 			options?.bannedUserMessage ??
  37 | 			"You have been banned from this application. Please contact support if you believe this is an error.",
  38 | 		...options,
  39 | 	};
  40 | 	type DefaultStatements = typeof defaultStatements;
  41 | 	type Statements = O["ac"] extends AccessControl<infer S>
  42 | 		? S
  43 | 		: DefaultStatements;
  44 | 
  45 | 	type PermissionType = {
  46 | 		[key in keyof Statements]?: Array<
  47 | 			Statements[key] extends readonly unknown[]
  48 | 				? Statements[key][number]
  49 | 				: never
  50 | 		>;
  51 | 	};
  52 | 	type PermissionExclusive =
  53 | 		| {
  54 | 				/**
  55 | 				 * @deprecated Use `permissions` instead
  56 | 				 */
  57 | 				permission: PermissionType;
  58 | 				permissions?: never;
  59 | 		  }
  60 | 		| {
  61 | 				permissions: PermissionType;
  62 | 				permission?: never;
  63 | 		  };
  64 | 
  65 | 	/**
  66 | 	 * Ensures a valid session, if not will throw.
  67 | 	 * Will also provide additional types on the user to include role types.
  68 | 	 */
  69 | 	const adminMiddleware = createAuthMiddleware(async (ctx) => {
  70 | 		const session = await getSessionFromCtx(ctx);
  71 | 		if (!session) {
  72 | 			throw new APIError("UNAUTHORIZED");
  73 | 		}
  74 | 		return {
  75 | 			session,
  76 | 		} as {
  77 | 			session: {
  78 | 				user: UserWithRole;
  79 | 				session: Session;
  80 | 			};
  81 | 		};
  82 | 	});
  83 | 
  84 | 	return {
  85 | 		id: "admin",
  86 | 		init() {
  87 | 			return {
  88 | 				options: {
  89 | 					databaseHooks: {
  90 | 						user: {
  91 | 							create: {
  92 | 								async before(user) {
  93 | 									return {
  94 | 										data: {
  95 | 											role: options?.defaultRole ?? "user",
  96 | 											...user,
  97 | 										},
  98 | 									};
  99 | 								},
 100 | 							},
 101 | 						},
 102 | 						session: {
 103 | 							create: {
 104 | 								async before(session, ctx) {
 105 | 									if (!ctx) {
 106 | 										return;
 107 | 									}
 108 | 									const user = (await ctx.context.internalAdapter.findUserById(
 109 | 										session.userId,
 110 | 									)) as UserWithRole;
 111 | 
 112 | 									if (user.banned) {
 113 | 										if (
 114 | 											user.banExpires &&
 115 | 											new Date(user.banExpires).getTime() < Date.now()
 116 | 										) {
 117 | 											await ctx.context.internalAdapter.updateUser(
 118 | 												session.userId,
 119 | 												{
 120 | 													banned: false,
 121 | 													banReason: null,
 122 | 													banExpires: null,
 123 | 												},
 124 | 											);
 125 | 											return;
 126 | 										}
 127 | 
 128 | 										if (
 129 | 											ctx &&
 130 | 											(ctx.path.startsWith("/callback") ||
 131 | 												ctx.path.startsWith("/oauth2/callback"))
 132 | 										) {
 133 | 											const redirectURI =
 134 | 												ctx.context.options.onAPIError?.errorURL ||
 135 | 												`${ctx.context.baseURL}/error`;
 136 | 											throw ctx.redirect(
 137 | 												`${redirectURI}?error=banned&error_description=${opts.bannedUserMessage}`,
 138 | 											);
 139 | 										}
 140 | 
 141 | 										throw new APIError("FORBIDDEN", {
 142 | 											message: opts.bannedUserMessage,
 143 | 											code: "BANNED_USER",
 144 | 										});
 145 | 									}
 146 | 								},
 147 | 							},
 148 | 						},
 149 | 					},
 150 | 				},
 151 | 			};
 152 | 		},
 153 | 		hooks: {
 154 | 			after: [
 155 | 				{
 156 | 					matcher(context) {
 157 | 						return context.path === "/list-sessions";
 158 | 					},
 159 | 					handler: createAuthMiddleware(async (ctx) => {
 160 | 						const response =
 161 | 							await getEndpointResponse<SessionWithImpersonatedBy[]>(ctx);
 162 | 
 163 | 						if (!response) {
 164 | 							return;
 165 | 						}
 166 | 						const newJson = response.filter((session) => {
 167 | 							return !session.impersonatedBy;
 168 | 						});
 169 | 
 170 | 						return ctx.json(newJson);
 171 | 					}),
 172 | 				},
 173 | 			],
 174 | 		},
 175 | 		endpoints: {
 176 | 			/**
 177 | 			 * ### Endpoint
 178 | 			 *
 179 | 			 * POST `/admin/set-role`
 180 | 			 *
 181 | 			 * ### API Methods
 182 | 			 *
 183 | 			 * **server:**
 184 | 			 * `auth.api.setRole`
 185 | 			 *
 186 | 			 * **client:**
 187 | 			 * `authClient.admin.setRole`
 188 | 			 *
 189 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-set-role)
 190 | 			 */
 191 | 			setRole: createAuthEndpoint(
 192 | 				"/admin/set-role",
 193 | 				{
 194 | 					method: "POST",
 195 | 					body: z.object({
 196 | 						userId: z.coerce.string().meta({
 197 | 							description: "The user id",
 198 | 						}),
 199 | 						role: z
 200 | 							.union([
 201 | 								z.string().meta({
 202 | 									description: "The role to set. `admin` or `user` by default",
 203 | 								}),
 204 | 								z.array(
 205 | 									z.string().meta({
 206 | 										description:
 207 | 											"The roles to set. `admin` or `user` by default",
 208 | 									}),
 209 | 								),
 210 | 							])
 211 | 							.meta({
 212 | 								description:
 213 | 									"The role to set, this can be a string or an array of strings. Eg: `admin` or `[admin, user]`",
 214 | 							}),
 215 | 					}),
 216 | 					requireHeaders: true,
 217 | 					use: [adminMiddleware],
 218 | 					metadata: {
 219 | 						openapi: {
 220 | 							operationId: "setRole",
 221 | 							summary: "Set the role of a user",
 222 | 							description: "Set the role of a user",
 223 | 							responses: {
 224 | 								200: {
 225 | 									description: "User role updated",
 226 | 									content: {
 227 | 										"application/json": {
 228 | 											schema: {
 229 | 												type: "object",
 230 | 												properties: {
 231 | 													user: {
 232 | 														$ref: "#/components/schemas/User",
 233 | 													},
 234 | 												},
 235 | 											},
 236 | 										},
 237 | 									},
 238 | 								},
 239 | 							},
 240 | 						},
 241 | 						$Infer: {
 242 | 							body: {} as {
 243 | 								userId: string;
 244 | 								role:
 245 | 									| InferAdminRolesFromOption<O>
 246 | 									| InferAdminRolesFromOption<O>[];
 247 | 							},
 248 | 						},
 249 | 					},
 250 | 				},
 251 | 				async (ctx) => {
 252 | 					const canSetRole = hasPermission({
 253 | 						userId: ctx.context.session.user.id,
 254 | 						role: ctx.context.session.user.role,
 255 | 						options: opts,
 256 | 						permissions: {
 257 | 							user: ["set-role"],
 258 | 						},
 259 | 					});
 260 | 					if (!canSetRole) {
 261 | 						throw new APIError("FORBIDDEN", {
 262 | 							message:
 263 | 								ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE,
 264 | 						});
 265 | 					}
 266 | 
 267 | 					const updatedUser = await ctx.context.internalAdapter.updateUser(
 268 | 						ctx.body.userId,
 269 | 						{
 270 | 							role: parseRoles(ctx.body.role),
 271 | 						},
 272 | 					);
 273 | 					return ctx.json({
 274 | 						user: updatedUser as UserWithRole,
 275 | 					});
 276 | 				},
 277 | 			),
 278 | 			getUser: createAuthEndpoint(
 279 | 				"/admin/get-user",
 280 | 				{
 281 | 					method: "GET",
 282 | 					query: z.object({
 283 | 						id: z.string().meta({
 284 | 							description: "The id of the User",
 285 | 						}),
 286 | 					}),
 287 | 					use: [adminMiddleware],
 288 | 					metadata: {
 289 | 						openapi: {
 290 | 							operationId: "getUser",
 291 | 							summary: "Get an existing user",
 292 | 							description: "Get an existing user",
 293 | 							responses: {
 294 | 								200: {
 295 | 									description: "User",
 296 | 									content: {
 297 | 										"application/json": {
 298 | 											schema: {
 299 | 												type: "object",
 300 | 												properties: {
 301 | 													user: {
 302 | 														$ref: "#/components/schemas/User",
 303 | 													},
 304 | 												},
 305 | 											},
 306 | 										},
 307 | 									},
 308 | 								},
 309 | 							},
 310 | 						},
 311 | 					},
 312 | 				},
 313 | 				async (ctx) => {
 314 | 					const { id } = ctx.query;
 315 | 
 316 | 					const canGetUser = hasPermission({
 317 | 						userId: ctx.context.session.user.id,
 318 | 						role: ctx.context.session.user.role,
 319 | 						options: opts,
 320 | 						permissions: {
 321 | 							user: ["get"],
 322 | 						},
 323 | 					});
 324 | 
 325 | 					if (!canGetUser) {
 326 | 						throw ctx.error("FORBIDDEN", {
 327 | 							message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_GET_USER,
 328 | 							code: "YOU_ARE_NOT_ALLOWED_TO_GET_USER",
 329 | 						});
 330 | 					}
 331 | 
 332 | 					const user = await ctx.context.internalAdapter.findUserById(id);
 333 | 
 334 | 					if (!user) {
 335 | 						throw new APIError("NOT_FOUND", {
 336 | 							message: BASE_ERROR_CODES.USER_NOT_FOUND,
 337 | 						});
 338 | 					}
 339 | 
 340 | 					return parseUserOutput(ctx.context.options, user);
 341 | 				},
 342 | 			),
 343 | 			/**
 344 | 			 * ### Endpoint
 345 | 			 *
 346 | 			 * POST `/admin/create-user`
 347 | 			 *
 348 | 			 * ### API Methods
 349 | 			 *
 350 | 			 * **server:**
 351 | 			 * `auth.api.createUser`
 352 | 			 *
 353 | 			 * **client:**
 354 | 			 * `authClient.admin.createUser`
 355 | 			 *
 356 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-create-user)
 357 | 			 */
 358 | 			createUser: createAuthEndpoint(
 359 | 				"/admin/create-user",
 360 | 				{
 361 | 					method: "POST",
 362 | 					body: z.object({
 363 | 						email: z.string().meta({
 364 | 							description: "The email of the user",
 365 | 						}),
 366 | 						password: z.string().meta({
 367 | 							description: "The password of the user",
 368 | 						}),
 369 | 						name: z.string().meta({
 370 | 							description: "The name of the user",
 371 | 						}),
 372 | 						role: z
 373 | 							.union([
 374 | 								z.string().meta({
 375 | 									description: "The role of the user",
 376 | 								}),
 377 | 								z.array(
 378 | 									z.string().meta({
 379 | 										description: "The roles of user",
 380 | 									}),
 381 | 								),
 382 | 							])
 383 | 							.optional()
 384 | 							.meta({
 385 | 								description: `A string or array of strings representing the roles to apply to the new user. Eg: \"user\"`,
 386 | 							}),
 387 | 						/**
 388 | 						 * extra fields for user
 389 | 						 */
 390 | 						data: z.record(z.string(), z.any()).optional().meta({
 391 | 							description:
 392 | 								"Extra fields for the user. Including custom additional fields.",
 393 | 						}),
 394 | 					}),
 395 | 					metadata: {
 396 | 						openapi: {
 397 | 							operationId: "createUser",
 398 | 							summary: "Create a new user",
 399 | 							description: "Create a new user",
 400 | 							responses: {
 401 | 								200: {
 402 | 									description: "User created",
 403 | 									content: {
 404 | 										"application/json": {
 405 | 											schema: {
 406 | 												type: "object",
 407 | 												properties: {
 408 | 													user: {
 409 | 														$ref: "#/components/schemas/User",
 410 | 													},
 411 | 												},
 412 | 											},
 413 | 										},
 414 | 									},
 415 | 								},
 416 | 							},
 417 | 						},
 418 | 						$Infer: {
 419 | 							body: {} as {
 420 | 								email: string;
 421 | 								password: string;
 422 | 								name: string;
 423 | 								role?:
 424 | 									| InferAdminRolesFromOption<O>
 425 | 									| InferAdminRolesFromOption<O>[];
 426 | 								data?: Record<string, any>;
 427 | 							},
 428 | 						},
 429 | 					},
 430 | 				},
 431 | 				async (ctx) => {
 432 | 					const session = await getSessionFromCtx<{ role: string }>(ctx);
 433 | 					if (!session && (ctx.request || ctx.headers)) {
 434 | 						throw ctx.error("UNAUTHORIZED");
 435 | 					}
 436 | 					if (session) {
 437 | 						const canCreateUser = hasPermission({
 438 | 							userId: session.user.id,
 439 | 							role: session.user.role,
 440 | 							options: opts,
 441 | 							permissions: {
 442 | 								user: ["create"],
 443 | 							},
 444 | 						});
 445 | 						if (!canCreateUser) {
 446 | 							throw new APIError("FORBIDDEN", {
 447 | 								message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS,
 448 | 							});
 449 | 						}
 450 | 					}
 451 | 					const existUser = await ctx.context.internalAdapter.findUserByEmail(
 452 | 						ctx.body.email,
 453 | 					);
 454 | 					if (existUser) {
 455 | 						throw new APIError("BAD_REQUEST", {
 456 | 							message: ADMIN_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL,
 457 | 						});
 458 | 					}
 459 | 					const user =
 460 | 						await ctx.context.internalAdapter.createUser<UserWithRole>({
 461 | 							email: ctx.body.email,
 462 | 							name: ctx.body.name,
 463 | 							role:
 464 | 								(ctx.body.role && parseRoles(ctx.body.role)) ??
 465 | 								options?.defaultRole ??
 466 | 								"user",
 467 | 							...ctx.body.data,
 468 | 						});
 469 | 
 470 | 					if (!user) {
 471 | 						throw new APIError("INTERNAL_SERVER_ERROR", {
 472 | 							message: ADMIN_ERROR_CODES.FAILED_TO_CREATE_USER,
 473 | 						});
 474 | 					}
 475 | 					const hashedPassword = await ctx.context.password.hash(
 476 | 						ctx.body.password,
 477 | 					);
 478 | 					await ctx.context.internalAdapter.linkAccount({
 479 | 						accountId: user.id,
 480 | 						providerId: "credential",
 481 | 						password: hashedPassword,
 482 | 						userId: user.id,
 483 | 					});
 484 | 					return ctx.json({
 485 | 						user: user as UserWithRole,
 486 | 					});
 487 | 				},
 488 | 			),
 489 | 			/**
 490 | 			 * ### Endpoint
 491 | 			 *
 492 | 			 * POST `/admin/update-user`
 493 | 			 *
 494 | 			 * ### API Methods
 495 | 			 *
 496 | 			 * **server:**
 497 | 			 * `auth.api.adminUpdateUser`
 498 | 			 *
 499 | 			 * **client:**
 500 | 			 * `authClient.admin.updateUser`
 501 | 			 *
 502 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-update-user)
 503 | 			 */
 504 | 			adminUpdateUser: createAuthEndpoint(
 505 | 				"/admin/update-user",
 506 | 				{
 507 | 					method: "POST",
 508 | 					body: z.object({
 509 | 						userId: z.coerce.string().meta({
 510 | 							description: "The user id",
 511 | 						}),
 512 | 						data: z.record(z.any(), z.any()).meta({
 513 | 							description: "The user data to update",
 514 | 						}),
 515 | 					}),
 516 | 					use: [adminMiddleware],
 517 | 					metadata: {
 518 | 						openapi: {
 519 | 							operationId: "updateUser",
 520 | 							summary: "Update a user",
 521 | 							description: "Update a user's details",
 522 | 							responses: {
 523 | 								200: {
 524 | 									description: "User updated",
 525 | 									content: {
 526 | 										"application/json": {
 527 | 											schema: {
 528 | 												type: "object",
 529 | 												properties: {
 530 | 													user: {
 531 | 														$ref: "#/components/schemas/User",
 532 | 													},
 533 | 												},
 534 | 											},
 535 | 										},
 536 | 									},
 537 | 								},
 538 | 							},
 539 | 						},
 540 | 					},
 541 | 				},
 542 | 				async (ctx) => {
 543 | 					const canUpdateUser = hasPermission({
 544 | 						userId: ctx.context.session.user.id,
 545 | 						role: ctx.context.session.user.role,
 546 | 						options: opts,
 547 | 						permissions: {
 548 | 							user: ["update"],
 549 | 						},
 550 | 					});
 551 | 					if (!canUpdateUser) {
 552 | 						throw ctx.error("FORBIDDEN", {
 553 | 							message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS,
 554 | 							code: "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS",
 555 | 						});
 556 | 					}
 557 | 
 558 | 					if (Object.keys(ctx.body.data).length === 0) {
 559 | 						throw new APIError("BAD_REQUEST", {
 560 | 							message: ADMIN_ERROR_CODES.NO_DATA_TO_UPDATE,
 561 | 						});
 562 | 					}
 563 | 					if (ctx.body.data?.role) {
 564 | 						ctx.body.data.role = parseRoles(ctx.body.data.role);
 565 | 					}
 566 | 					const updatedUser = await ctx.context.internalAdapter.updateUser(
 567 | 						ctx.body.userId,
 568 | 						ctx.body.data,
 569 | 					);
 570 | 
 571 | 					return ctx.json(updatedUser as UserWithRole);
 572 | 				},
 573 | 			),
 574 | 			listUsers: createAuthEndpoint(
 575 | 				"/admin/list-users",
 576 | 				{
 577 | 					method: "GET",
 578 | 					use: [adminMiddleware],
 579 | 					query: z.object({
 580 | 						searchValue: z.string().optional().meta({
 581 | 							description: 'The value to search for. Eg: "some name"',
 582 | 						}),
 583 | 						searchField: z
 584 | 							.enum(["email", "name"])
 585 | 							.meta({
 586 | 								description:
 587 | 									'The field to search in, defaults to email. Can be `email` or `name`. Eg: "name"',
 588 | 							})
 589 | 							.optional(),
 590 | 						searchOperator: z
 591 | 							.enum(["contains", "starts_with", "ends_with"])
 592 | 							.meta({
 593 | 								description:
 594 | 									'The operator to use for the search. Can be `contains`, `starts_with` or `ends_with`. Eg: "contains"',
 595 | 							})
 596 | 							.optional(),
 597 | 						limit: z
 598 | 							.string()
 599 | 							.meta({
 600 | 								description: "The number of users to return",
 601 | 							})
 602 | 							.or(z.number())
 603 | 							.optional(),
 604 | 						offset: z
 605 | 							.string()
 606 | 							.meta({
 607 | 								description: "The offset to start from",
 608 | 							})
 609 | 							.or(z.number())
 610 | 							.optional(),
 611 | 						sortBy: z
 612 | 							.string()
 613 | 							.meta({
 614 | 								description: "The field to sort by",
 615 | 							})
 616 | 							.optional(),
 617 | 						sortDirection: z
 618 | 							.enum(["asc", "desc"])
 619 | 							.meta({
 620 | 								description: "The direction to sort by",
 621 | 							})
 622 | 							.optional(),
 623 | 						filterField: z
 624 | 							.string()
 625 | 							.meta({
 626 | 								description: "The field to filter by",
 627 | 							})
 628 | 							.optional(),
 629 | 						filterValue: z
 630 | 							.string()
 631 | 							.meta({
 632 | 								description: "The value to filter by",
 633 | 							})
 634 | 							.or(z.number())
 635 | 							.or(z.boolean())
 636 | 							.optional(),
 637 | 						filterOperator: z
 638 | 							.enum(["eq", "ne", "lt", "lte", "gt", "gte", "contains"])
 639 | 							.meta({
 640 | 								description: "The operator to use for the filter",
 641 | 							})
 642 | 							.optional(),
 643 | 					}),
 644 | 					metadata: {
 645 | 						openapi: {
 646 | 							operationId: "listUsers",
 647 | 							summary: "List users",
 648 | 							description: "List users",
 649 | 							responses: {
 650 | 								200: {
 651 | 									description: "List of users",
 652 | 									content: {
 653 | 										"application/json": {
 654 | 											schema: {
 655 | 												type: "object",
 656 | 												properties: {
 657 | 													users: {
 658 | 														type: "array",
 659 | 														items: {
 660 | 															$ref: "#/components/schemas/User",
 661 | 														},
 662 | 													},
 663 | 													total: {
 664 | 														type: "number",
 665 | 													},
 666 | 													limit: {
 667 | 														type: "number",
 668 | 													},
 669 | 													offset: {
 670 | 														type: "number",
 671 | 													},
 672 | 												},
 673 | 												required: ["users", "total"],
 674 | 											},
 675 | 										},
 676 | 									},
 677 | 								},
 678 | 							},
 679 | 						},
 680 | 					},
 681 | 				},
 682 | 				async (ctx) => {
 683 | 					const session = ctx.context.session;
 684 | 					const canListUsers = hasPermission({
 685 | 						userId: ctx.context.session.user.id,
 686 | 						role: session.user.role,
 687 | 						options: opts,
 688 | 						permissions: {
 689 | 							user: ["list"],
 690 | 						},
 691 | 					});
 692 | 					if (!canListUsers) {
 693 | 						throw new APIError("FORBIDDEN", {
 694 | 							message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_LIST_USERS,
 695 | 						});
 696 | 					}
 697 | 
 698 | 					const where: Where[] = [];
 699 | 
 700 | 					if (ctx.query?.searchValue) {
 701 | 						where.push({
 702 | 							field: ctx.query.searchField || "email",
 703 | 							operator: ctx.query.searchOperator || "contains",
 704 | 							value: ctx.query.searchValue,
 705 | 						});
 706 | 					}
 707 | 
 708 | 					if (ctx.query?.filterValue) {
 709 | 						where.push({
 710 | 							field: ctx.query.filterField || "email",
 711 | 							operator: ctx.query.filterOperator || "eq",
 712 | 							value: ctx.query.filterValue,
 713 | 						});
 714 | 					}
 715 | 
 716 | 					try {
 717 | 						const users = await ctx.context.internalAdapter.listUsers(
 718 | 							Number(ctx.query?.limit) || undefined,
 719 | 							Number(ctx.query?.offset) || undefined,
 720 | 							ctx.query?.sortBy
 721 | 								? {
 722 | 										field: ctx.query.sortBy,
 723 | 										direction: ctx.query.sortDirection || "asc",
 724 | 									}
 725 | 								: undefined,
 726 | 							where.length ? where : undefined,
 727 | 						);
 728 | 						const total = await ctx.context.internalAdapter.countTotalUsers(
 729 | 							where.length ? where : undefined,
 730 | 						);
 731 | 						return ctx.json({
 732 | 							users: users as UserWithRole[],
 733 | 							total: total,
 734 | 							limit: Number(ctx.query?.limit) || undefined,
 735 | 							offset: Number(ctx.query?.offset) || undefined,
 736 | 						});
 737 | 					} catch (e) {
 738 | 						return ctx.json({
 739 | 							users: [],
 740 | 							total: 0,
 741 | 						});
 742 | 					}
 743 | 				},
 744 | 			),
 745 | 			/**
 746 | 			 * ### Endpoint
 747 | 			 *
 748 | 			 * POST `/admin/list-user-sessions`
 749 | 			 *
 750 | 			 * ### API Methods
 751 | 			 *
 752 | 			 * **server:**
 753 | 			 * `auth.api.listUserSessions`
 754 | 			 *
 755 | 			 * **client:**
 756 | 			 * `authClient.admin.listUserSessions`
 757 | 			 *
 758 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-list-user-sessions)
 759 | 			 */
 760 | 			listUserSessions: createAuthEndpoint(
 761 | 				"/admin/list-user-sessions",
 762 | 				{
 763 | 					method: "POST",
 764 | 					use: [adminMiddleware],
 765 | 					body: z.object({
 766 | 						userId: z.coerce.string().meta({
 767 | 							description: "The user id",
 768 | 						}),
 769 | 					}),
 770 | 					metadata: {
 771 | 						openapi: {
 772 | 							operationId: "listUserSessions",
 773 | 							summary: "List user sessions",
 774 | 							description: "List user sessions",
 775 | 							responses: {
 776 | 								200: {
 777 | 									description: "List of user sessions",
 778 | 									content: {
 779 | 										"application/json": {
 780 | 											schema: {
 781 | 												type: "object",
 782 | 												properties: {
 783 | 													sessions: {
 784 | 														type: "array",
 785 | 														items: {
 786 | 															$ref: "#/components/schemas/Session",
 787 | 														},
 788 | 													},
 789 | 												},
 790 | 											},
 791 | 										},
 792 | 									},
 793 | 								},
 794 | 							},
 795 | 						},
 796 | 					},
 797 | 				},
 798 | 				async (ctx) => {
 799 | 					const session = ctx.context.session;
 800 | 					const canListSessions = hasPermission({
 801 | 						userId: ctx.context.session.user.id,
 802 | 						role: session.user.role,
 803 | 						options: opts,
 804 | 						permissions: {
 805 | 							session: ["list"],
 806 | 						},
 807 | 					});
 808 | 					if (!canListSessions) {
 809 | 						throw new APIError("FORBIDDEN", {
 810 | 							message:
 811 | 								ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS,
 812 | 						});
 813 | 					}
 814 | 
 815 | 					const sessions: SessionWithImpersonatedBy[] =
 816 | 						await ctx.context.internalAdapter.listSessions(ctx.body.userId);
 817 | 					return {
 818 | 						sessions: sessions,
 819 | 					};
 820 | 				},
 821 | 			),
 822 | 			/**
 823 | 			 * ### Endpoint
 824 | 			 *
 825 | 			 * POST `/admin/unban-user`
 826 | 			 *
 827 | 			 * ### API Methods
 828 | 			 *
 829 | 			 * **server:**
 830 | 			 * `auth.api.unbanUser`
 831 | 			 *
 832 | 			 * **client:**
 833 | 			 * `authClient.admin.unbanUser`
 834 | 			 *
 835 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-unban-user)
 836 | 			 */
 837 | 			unbanUser: createAuthEndpoint(
 838 | 				"/admin/unban-user",
 839 | 				{
 840 | 					method: "POST",
 841 | 					body: z.object({
 842 | 						userId: z.coerce.string().meta({
 843 | 							description: "The user id",
 844 | 						}),
 845 | 					}),
 846 | 					use: [adminMiddleware],
 847 | 					metadata: {
 848 | 						openapi: {
 849 | 							operationId: "unbanUser",
 850 | 							summary: "Unban a user",
 851 | 							description: "Unban a user",
 852 | 							responses: {
 853 | 								200: {
 854 | 									description: "User unbanned",
 855 | 									content: {
 856 | 										"application/json": {
 857 | 											schema: {
 858 | 												type: "object",
 859 | 												properties: {
 860 | 													user: {
 861 | 														$ref: "#/components/schemas/User",
 862 | 													},
 863 | 												},
 864 | 											},
 865 | 										},
 866 | 									},
 867 | 								},
 868 | 							},
 869 | 						},
 870 | 					},
 871 | 				},
 872 | 				async (ctx) => {
 873 | 					const session = ctx.context.session;
 874 | 					const canBanUser = hasPermission({
 875 | 						userId: ctx.context.session.user.id,
 876 | 						role: session.user.role,
 877 | 						options: opts,
 878 | 						permissions: {
 879 | 							user: ["ban"],
 880 | 						},
 881 | 					});
 882 | 					if (!canBanUser) {
 883 | 						throw new APIError("FORBIDDEN", {
 884 | 							message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS,
 885 | 						});
 886 | 					}
 887 | 
 888 | 					const user = await ctx.context.internalAdapter.updateUser(
 889 | 						ctx.body.userId,
 890 | 						{
 891 | 							banned: false,
 892 | 							banExpires: null,
 893 | 							banReason: null,
 894 | 							updatedAt: new Date(),
 895 | 						},
 896 | 					);
 897 | 					return ctx.json({
 898 | 						user: user,
 899 | 					});
 900 | 				},
 901 | 			),
 902 | 			/**
 903 | 			 * ### Endpoint
 904 | 			 *
 905 | 			 * POST `/admin/ban-user`
 906 | 			 *
 907 | 			 * ### API Methods
 908 | 			 *
 909 | 			 * **server:**
 910 | 			 * `auth.api.banUser`
 911 | 			 *
 912 | 			 * **client:**
 913 | 			 * `authClient.admin.banUser`
 914 | 			 *
 915 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-ban-user)
 916 | 			 */
 917 | 			banUser: createAuthEndpoint(
 918 | 				"/admin/ban-user",
 919 | 				{
 920 | 					method: "POST",
 921 | 					body: z.object({
 922 | 						userId: z.coerce.string().meta({
 923 | 							description: "The user id",
 924 | 						}),
 925 | 						/**
 926 | 						 * Reason for the ban
 927 | 						 */
 928 | 						banReason: z
 929 | 							.string()
 930 | 							.meta({
 931 | 								description: "The reason for the ban",
 932 | 							})
 933 | 							.optional(),
 934 | 						/**
 935 | 						 * Number of seconds until the ban expires
 936 | 						 */
 937 | 						banExpiresIn: z
 938 | 							.number()
 939 | 							.meta({
 940 | 								description: "The number of seconds until the ban expires",
 941 | 							})
 942 | 							.optional(),
 943 | 					}),
 944 | 					use: [adminMiddleware],
 945 | 					metadata: {
 946 | 						openapi: {
 947 | 							operationId: "banUser",
 948 | 							summary: "Ban a user",
 949 | 							description: "Ban a user",
 950 | 							responses: {
 951 | 								200: {
 952 | 									description: "User banned",
 953 | 									content: {
 954 | 										"application/json": {
 955 | 											schema: {
 956 | 												type: "object",
 957 | 												properties: {
 958 | 													user: {
 959 | 														$ref: "#/components/schemas/User",
 960 | 													},
 961 | 												},
 962 | 											},
 963 | 										},
 964 | 									},
 965 | 								},
 966 | 							},
 967 | 						},
 968 | 					},
 969 | 				},
 970 | 				async (ctx) => {
 971 | 					const session = ctx.context.session;
 972 | 					const canBanUser = hasPermission({
 973 | 						userId: ctx.context.session.user.id,
 974 | 						role: session.user.role,
 975 | 						options: opts,
 976 | 						permissions: {
 977 | 							user: ["ban"],
 978 | 						},
 979 | 					});
 980 | 					if (!canBanUser) {
 981 | 						throw new APIError("FORBIDDEN", {
 982 | 							message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_BAN_USERS,
 983 | 						});
 984 | 					}
 985 | 
 986 | 					const foundUser = await ctx.context.internalAdapter.findUserById(
 987 | 						ctx.body.userId,
 988 | 					);
 989 | 
 990 | 					if (!foundUser) {
 991 | 						throw new APIError("NOT_FOUND", {
 992 | 							message: BASE_ERROR_CODES.USER_NOT_FOUND,
 993 | 						});
 994 | 					}
 995 | 
 996 | 					if (ctx.body.userId === ctx.context.session.user.id) {
 997 | 						throw new APIError("BAD_REQUEST", {
 998 | 							message: ADMIN_ERROR_CODES.YOU_CANNOT_BAN_YOURSELF,
 999 | 						});
1000 | 					}
1001 | 					const user = await ctx.context.internalAdapter.updateUser(
1002 | 						ctx.body.userId,
1003 | 						{
1004 | 							banned: true,
1005 | 							banReason:
1006 | 								ctx.body.banReason || options?.defaultBanReason || "No reason",
1007 | 							banExpires: ctx.body.banExpiresIn
1008 | 								? getDate(ctx.body.banExpiresIn, "sec")
1009 | 								: options?.defaultBanExpiresIn
1010 | 									? getDate(options.defaultBanExpiresIn, "sec")
1011 | 									: undefined,
1012 | 							updatedAt: new Date(),
1013 | 						},
1014 | 					);
1015 | 					//revoke all sessions
1016 | 					await ctx.context.internalAdapter.deleteSessions(ctx.body.userId);
1017 | 					return ctx.json({
1018 | 						user: user,
1019 | 					});
1020 | 				},
1021 | 			),
1022 | 			/**
1023 | 			 * ### Endpoint
1024 | 			 *
1025 | 			 * POST `/admin/impersonate-user`
1026 | 			 *
1027 | 			 * ### API Methods
1028 | 			 *
1029 | 			 * **server:**
1030 | 			 * `auth.api.impersonateUser`
1031 | 			 *
1032 | 			 * **client:**
1033 | 			 * `authClient.admin.impersonateUser`
1034 | 			 *
1035 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-impersonate-user)
1036 | 			 */
1037 | 			impersonateUser: createAuthEndpoint(
1038 | 				"/admin/impersonate-user",
1039 | 				{
1040 | 					method: "POST",
1041 | 					body: z.object({
1042 | 						userId: z.coerce.string().meta({
1043 | 							description: "The user id",
1044 | 						}),
1045 | 					}),
1046 | 					use: [adminMiddleware],
1047 | 					metadata: {
1048 | 						openapi: {
1049 | 							operationId: "impersonateUser",
1050 | 							summary: "Impersonate a user",
1051 | 							description: "Impersonate a user",
1052 | 							responses: {
1053 | 								200: {
1054 | 									description: "Impersonation session created",
1055 | 									content: {
1056 | 										"application/json": {
1057 | 											schema: {
1058 | 												type: "object",
1059 | 												properties: {
1060 | 													session: {
1061 | 														$ref: "#/components/schemas/Session",
1062 | 													},
1063 | 													user: {
1064 | 														$ref: "#/components/schemas/User",
1065 | 													},
1066 | 												},
1067 | 											},
1068 | 										},
1069 | 									},
1070 | 								},
1071 | 							},
1072 | 						},
1073 | 					},
1074 | 				},
1075 | 				async (ctx) => {
1076 | 					const canImpersonateUser = hasPermission({
1077 | 						userId: ctx.context.session.user.id,
1078 | 						role: ctx.context.session.user.role,
1079 | 						options: opts,
1080 | 						permissions: {
1081 | 							user: ["impersonate"],
1082 | 						},
1083 | 					});
1084 | 					if (!canImpersonateUser) {
1085 | 						throw new APIError("FORBIDDEN", {
1086 | 							message:
1087 | 								ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS,
1088 | 						});
1089 | 					}
1090 | 
1091 | 					const targetUser = await ctx.context.internalAdapter.findUserById(
1092 | 						ctx.body.userId,
1093 | 					);
1094 | 
1095 | 					if (!targetUser) {
1096 | 						throw new APIError("NOT_FOUND", {
1097 | 							message: "User not found",
1098 | 						});
1099 | 					}
1100 | 
1101 | 					const session = await ctx.context.internalAdapter.createSession(
1102 | 						targetUser.id,
1103 | 						true,
1104 | 						{
1105 | 							impersonatedBy: ctx.context.session.user.id,
1106 | 							expiresAt: options?.impersonationSessionDuration
1107 | 								? getDate(options.impersonationSessionDuration, "sec")
1108 | 								: getDate(60 * 60, "sec"), // 1 hour
1109 | 						},
1110 | 						true,
1111 | 					);
1112 | 					if (!session) {
1113 | 						throw new APIError("INTERNAL_SERVER_ERROR", {
1114 | 							message: ADMIN_ERROR_CODES.FAILED_TO_CREATE_USER,
1115 | 						});
1116 | 					}
1117 | 					const authCookies = ctx.context.authCookies;
1118 | 					deleteSessionCookie(ctx);
1119 | 					const dontRememberMeCookie = await ctx.getSignedCookie(
1120 | 						ctx.context.authCookies.dontRememberToken.name,
1121 | 						ctx.context.secret,
1122 | 					);
1123 | 					const adminCookieProp = ctx.context.createAuthCookie("admin_session");
1124 | 					await ctx.setSignedCookie(
1125 | 						adminCookieProp.name,
1126 | 						`${ctx.context.session.session.token}:${
1127 | 							dontRememberMeCookie || ""
1128 | 						}`,
1129 | 						ctx.context.secret,
1130 | 						authCookies.sessionToken.options,
1131 | 					);
1132 | 					await setSessionCookie(
1133 | 						ctx,
1134 | 						{
1135 | 							session: session,
1136 | 							user: targetUser,
1137 | 						},
1138 | 						true,
1139 | 					);
1140 | 					return ctx.json({
1141 | 						session: session,
1142 | 						user: targetUser,
1143 | 					});
1144 | 				},
1145 | 			),
1146 | 			/**
1147 | 			 * ### Endpoint
1148 | 			 *
1149 | 			 * POST `/admin/stop-impersonating`
1150 | 			 *
1151 | 			 * ### API Methods
1152 | 			 *
1153 | 			 * **server:**
1154 | 			 * `auth.api.stopImpersonating`
1155 | 			 *
1156 | 			 * **client:**
1157 | 			 * `authClient.admin.stopImpersonating`
1158 | 			 *
1159 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-stop-impersonating)
1160 | 			 */
1161 | 			stopImpersonating: createAuthEndpoint(
1162 | 				"/admin/stop-impersonating",
1163 | 				{
1164 | 					method: "POST",
1165 | 					requireHeaders: true,
1166 | 				},
1167 | 				async (ctx) => {
1168 | 					const session = await getSessionFromCtx<
1169 | 						{},
1170 | 						{
1171 | 							impersonatedBy: string;
1172 | 						}
1173 | 					>(ctx);
1174 | 					if (!session) {
1175 | 						throw new APIError("UNAUTHORIZED");
1176 | 					}
1177 | 					if (!session.session.impersonatedBy) {
1178 | 						throw new APIError("BAD_REQUEST", {
1179 | 							message: "You are not impersonating anyone",
1180 | 						});
1181 | 					}
1182 | 					const user = await ctx.context.internalAdapter.findUserById(
1183 | 						session.session.impersonatedBy,
1184 | 					);
1185 | 					if (!user) {
1186 | 						throw new APIError("INTERNAL_SERVER_ERROR", {
1187 | 							message: "Failed to find user",
1188 | 						});
1189 | 					}
1190 | 					const adminCookieName =
1191 | 						ctx.context.createAuthCookie("admin_session").name;
1192 | 					const adminCookie = await ctx.getSignedCookie(
1193 | 						adminCookieName,
1194 | 						ctx.context.secret,
1195 | 					);
1196 | 
1197 | 					if (!adminCookie) {
1198 | 						throw new APIError("INTERNAL_SERVER_ERROR", {
1199 | 							message: "Failed to find admin session",
1200 | 						});
1201 | 					}
1202 | 					const [adminSessionToken, dontRememberMeCookie] =
1203 | 						adminCookie?.split(":");
1204 | 					const adminSession = await ctx.context.internalAdapter.findSession(
1205 | 						adminSessionToken!,
1206 | 					);
1207 | 					if (!adminSession || adminSession.session.userId !== user.id) {
1208 | 						throw new APIError("INTERNAL_SERVER_ERROR", {
1209 | 							message: "Failed to find admin session",
1210 | 						});
1211 | 					}
1212 | 					await ctx.context.internalAdapter.deleteSession(
1213 | 						session.session.token,
1214 | 					);
1215 | 					await setSessionCookie(ctx, adminSession, !!dontRememberMeCookie);
1216 | 					return ctx.json(adminSession);
1217 | 				},
1218 | 			),
1219 | 			/**
1220 | 			 * ### Endpoint
1221 | 			 *
1222 | 			 * POST `/admin/revoke-user-session`
1223 | 			 *
1224 | 			 * ### API Methods
1225 | 			 *
1226 | 			 * **server:**
1227 | 			 * `auth.api.revokeUserSession`
1228 | 			 *
1229 | 			 * **client:**
1230 | 			 * `authClient.admin.revokeUserSession`
1231 | 			 *
1232 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-revoke-user-session)
1233 | 			 */
1234 | 			revokeUserSession: createAuthEndpoint(
1235 | 				"/admin/revoke-user-session",
1236 | 				{
1237 | 					method: "POST",
1238 | 					body: z.object({
1239 | 						sessionToken: z.string().meta({
1240 | 							description: "The session token",
1241 | 						}),
1242 | 					}),
1243 | 					use: [adminMiddleware],
1244 | 					metadata: {
1245 | 						openapi: {
1246 | 							operationId: "revokeUserSession",
1247 | 							summary: "Revoke a user session",
1248 | 							description: "Revoke a user session",
1249 | 							responses: {
1250 | 								200: {
1251 | 									description: "Session revoked",
1252 | 									content: {
1253 | 										"application/json": {
1254 | 											schema: {
1255 | 												type: "object",
1256 | 												properties: {
1257 | 													success: {
1258 | 														type: "boolean",
1259 | 													},
1260 | 												},
1261 | 											},
1262 | 										},
1263 | 									},
1264 | 								},
1265 | 							},
1266 | 						},
1267 | 					},
1268 | 				},
1269 | 				async (ctx) => {
1270 | 					const session = ctx.context.session;
1271 | 					const canRevokeSession = hasPermission({
1272 | 						userId: ctx.context.session.user.id,
1273 | 						role: session.user.role,
1274 | 						options: opts,
1275 | 						permissions: {
1276 | 							session: ["revoke"],
1277 | 						},
1278 | 					});
1279 | 					if (!canRevokeSession) {
1280 | 						throw new APIError("FORBIDDEN", {
1281 | 							message:
1282 | 								ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS,
1283 | 						});
1284 | 					}
1285 | 
1286 | 					await ctx.context.internalAdapter.deleteSession(
1287 | 						ctx.body.sessionToken,
1288 | 					);
1289 | 					return ctx.json({
1290 | 						success: true,
1291 | 					});
1292 | 				},
1293 | 			),
1294 | 			/**
1295 | 			 * ### Endpoint
1296 | 			 *
1297 | 			 * POST `/admin/revoke-user-sessions`
1298 | 			 *
1299 | 			 * ### API Methods
1300 | 			 *
1301 | 			 * **server:**
1302 | 			 * `auth.api.revokeUserSessions`
1303 | 			 *
1304 | 			 * **client:**
1305 | 			 * `authClient.admin.revokeUserSessions`
1306 | 			 *
1307 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-revoke-user-sessions)
1308 | 			 */
1309 | 			revokeUserSessions: createAuthEndpoint(
1310 | 				"/admin/revoke-user-sessions",
1311 | 				{
1312 | 					method: "POST",
1313 | 					body: z.object({
1314 | 						userId: z.coerce.string().meta({
1315 | 							description: "The user id",
1316 | 						}),
1317 | 					}),
1318 | 					use: [adminMiddleware],
1319 | 					metadata: {
1320 | 						openapi: {
1321 | 							operationId: "revokeUserSessions",
1322 | 							summary: "Revoke all user sessions",
1323 | 							description: "Revoke all user sessions",
1324 | 							responses: {
1325 | 								200: {
1326 | 									description: "Sessions revoked",
1327 | 									content: {
1328 | 										"application/json": {
1329 | 											schema: {
1330 | 												type: "object",
1331 | 												properties: {
1332 | 													success: {
1333 | 														type: "boolean",
1334 | 													},
1335 | 												},
1336 | 											},
1337 | 										},
1338 | 									},
1339 | 								},
1340 | 							},
1341 | 						},
1342 | 					},
1343 | 				},
1344 | 				async (ctx) => {
1345 | 					const session = ctx.context.session;
1346 | 					const canRevokeSession = hasPermission({
1347 | 						userId: ctx.context.session.user.id,
1348 | 						role: session.user.role,
1349 | 						options: opts,
1350 | 						permissions: {
1351 | 							session: ["revoke"],
1352 | 						},
1353 | 					});
1354 | 					if (!canRevokeSession) {
1355 | 						throw new APIError("FORBIDDEN", {
1356 | 							message:
1357 | 								ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS,
1358 | 						});
1359 | 					}
1360 | 
1361 | 					await ctx.context.internalAdapter.deleteSessions(ctx.body.userId);
1362 | 					return ctx.json({
1363 | 						success: true,
1364 | 					});
1365 | 				},
1366 | 			),
1367 | 			/**
1368 | 			 * ### Endpoint
1369 | 			 *
1370 | 			 * POST `/admin/remove-user`
1371 | 			 *
1372 | 			 * ### API Methods
1373 | 			 *
1374 | 			 * **server:**
1375 | 			 * `auth.api.removeUser`
1376 | 			 *
1377 | 			 * **client:**
1378 | 			 * `authClient.admin.removeUser`
1379 | 			 *
1380 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-remove-user)
1381 | 			 */
1382 | 			removeUser: createAuthEndpoint(
1383 | 				"/admin/remove-user",
1384 | 				{
1385 | 					method: "POST",
1386 | 					body: z.object({
1387 | 						userId: z.coerce.string().meta({
1388 | 							description: "The user id",
1389 | 						}),
1390 | 					}),
1391 | 					use: [adminMiddleware],
1392 | 					metadata: {
1393 | 						openapi: {
1394 | 							operationId: "removeUser",
1395 | 							summary: "Remove a user",
1396 | 							description:
1397 | 								"Delete a user and all their sessions and accounts. Cannot be undone.",
1398 | 							responses: {
1399 | 								200: {
1400 | 									description: "User removed",
1401 | 									content: {
1402 | 										"application/json": {
1403 | 											schema: {
1404 | 												type: "object",
1405 | 												properties: {
1406 | 													success: {
1407 | 														type: "boolean",
1408 | 													},
1409 | 												},
1410 | 											},
1411 | 										},
1412 | 									},
1413 | 								},
1414 | 							},
1415 | 						},
1416 | 					},
1417 | 				},
1418 | 				async (ctx) => {
1419 | 					const session = ctx.context.session;
1420 | 					const canDeleteUser = hasPermission({
1421 | 						userId: ctx.context.session.user.id,
1422 | 						role: session.user.role,
1423 | 						options: opts,
1424 | 						permissions: {
1425 | 							user: ["delete"],
1426 | 						},
1427 | 					});
1428 | 					if (!canDeleteUser) {
1429 | 						throw new APIError("FORBIDDEN", {
1430 | 							message: ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS,
1431 | 						});
1432 | 					}
1433 | 
1434 | 					if (ctx.body.userId === ctx.context.session.user.id) {
1435 | 						throw new APIError("BAD_REQUEST", {
1436 | 							message: ADMIN_ERROR_CODES.YOU_CANNOT_REMOVE_YOURSELF,
1437 | 						});
1438 | 					}
1439 | 
1440 | 					const user = await ctx.context.internalAdapter.findUserById(
1441 | 						ctx.body.userId,
1442 | 					);
1443 | 
1444 | 					if (!user) {
1445 | 						throw new APIError("NOT_FOUND", {
1446 | 							message: "User not found",
1447 | 						});
1448 | 					}
1449 | 
1450 | 					await ctx.context.internalAdapter.deleteUser(ctx.body.userId);
1451 | 					return ctx.json({
1452 | 						success: true,
1453 | 					});
1454 | 				},
1455 | 			),
1456 | 			/**
1457 | 			 * ### Endpoint
1458 | 			 *
1459 | 			 * POST `/admin/set-user-password`
1460 | 			 *
1461 | 			 * ### API Methods
1462 | 			 *
1463 | 			 * **server:**
1464 | 			 * `auth.api.setUserPassword`
1465 | 			 *
1466 | 			 * **client:**
1467 | 			 * `authClient.admin.setUserPassword`
1468 | 			 *
1469 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-set-user-password)
1470 | 			 */
1471 | 			setUserPassword: createAuthEndpoint(
1472 | 				"/admin/set-user-password",
1473 | 				{
1474 | 					method: "POST",
1475 | 					body: z.object({
1476 | 						newPassword: z
1477 | 							.string()
1478 | 							.nonempty("newPassword cannot be empty")
1479 | 							.meta({
1480 | 								description: "The new password",
1481 | 							}),
1482 | 						userId: z.coerce.string().nonempty("userId cannot be empty").meta({
1483 | 							description: "The user id",
1484 | 						}),
1485 | 					}),
1486 | 					use: [adminMiddleware],
1487 | 					metadata: {
1488 | 						openapi: {
1489 | 							operationId: "setUserPassword",
1490 | 							summary: "Set a user's password",
1491 | 							description: "Set a user's password",
1492 | 							responses: {
1493 | 								200: {
1494 | 									description: "Password set",
1495 | 									content: {
1496 | 										"application/json": {
1497 | 											schema: {
1498 | 												type: "object",
1499 | 												properties: {
1500 | 													status: {
1501 | 														type: "boolean",
1502 | 													},
1503 | 												},
1504 | 											},
1505 | 										},
1506 | 									},
1507 | 								},
1508 | 							},
1509 | 						},
1510 | 					},
1511 | 				},
1512 | 				async (ctx) => {
1513 | 					const canSetUserPassword = hasPermission({
1514 | 						userId: ctx.context.session.user.id,
1515 | 						role: ctx.context.session.user.role,
1516 | 						options: opts,
1517 | 						permissions: {
1518 | 							user: ["set-password"],
1519 | 						},
1520 | 					});
1521 | 					if (!canSetUserPassword) {
1522 | 						throw new APIError("FORBIDDEN", {
1523 | 							message:
1524 | 								ADMIN_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD,
1525 | 						});
1526 | 					}
1527 | 
1528 | 					const { newPassword, userId } = ctx.body;
1529 | 					const minPasswordLength =
1530 | 						ctx.context.password.config.minPasswordLength;
1531 | 					if (newPassword.length < minPasswordLength) {
1532 | 						ctx.context.logger.error("Password is too short");
1533 | 						throw new APIError("BAD_REQUEST", {
1534 | 							message: BASE_ERROR_CODES.PASSWORD_TOO_SHORT,
1535 | 						});
1536 | 					}
1537 | 					const maxPasswordLength =
1538 | 						ctx.context.password.config.maxPasswordLength;
1539 | 					if (newPassword.length > maxPasswordLength) {
1540 | 						ctx.context.logger.error("Password is too long");
1541 | 						throw new APIError("BAD_REQUEST", {
1542 | 							message: BASE_ERROR_CODES.PASSWORD_TOO_LONG,
1543 | 						});
1544 | 					}
1545 | 					const hashedPassword = await ctx.context.password.hash(newPassword);
1546 | 					await ctx.context.internalAdapter.updatePassword(
1547 | 						userId,
1548 | 						hashedPassword,
1549 | 					);
1550 | 					return ctx.json({
1551 | 						status: true,
1552 | 					});
1553 | 				},
1554 | 			),
1555 | 			/**
1556 | 			 * ### Endpoint
1557 | 			 *
1558 | 			 * POST `/admin/has-permission`
1559 | 			 *
1560 | 			 * ### API Methods
1561 | 			 *
1562 | 			 * **server:**
1563 | 			 * `auth.api.userHasPermission`
1564 | 			 *
1565 | 			 * **client:**
1566 | 			 * `authClient.admin.hasPermission`
1567 | 			 *
1568 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/admin#api-method-admin-has-permission)
1569 | 			 */
1570 | 			userHasPermission: createAuthEndpoint(
1571 | 				"/admin/has-permission",
1572 | 				{
1573 | 					method: "POST",
1574 | 					body: z
1575 | 						.object({
1576 | 							userId: z.coerce.string().optional().meta({
1577 | 								description: `The user id. Eg: "user-id"`,
1578 | 							}),
1579 | 							role: z.string().optional().meta({
1580 | 								description: `The role to check permission for. Eg: "admin"`,
1581 | 							}),
1582 | 						})
1583 | 						.and(
1584 | 							z.union([
1585 | 								z.object({
1586 | 									permission: z.record(z.string(), z.array(z.string())),
1587 | 									permissions: z.undefined(),
1588 | 								}),
1589 | 								z.object({
1590 | 									permission: z.undefined(),
1591 | 									permissions: z.record(z.string(), z.array(z.string())),
1592 | 								}),
1593 | 							]),
1594 | 						),
1595 | 					metadata: {
1596 | 						openapi: {
1597 | 							description: "Check if the user has permission",
1598 | 							requestBody: {
1599 | 								content: {
1600 | 									"application/json": {
1601 | 										schema: {
1602 | 											type: "object",
1603 | 											properties: {
1604 | 												permission: {
1605 | 													type: "object",
1606 | 													description: "The permission to check",
1607 | 													deprecated: true,
1608 | 												},
1609 | 												permissions: {
1610 | 													type: "object",
1611 | 													description: "The permission to check",
1612 | 												},
1613 | 											},
1614 | 											required: ["permissions"],
1615 | 										},
1616 | 									},
1617 | 								},
1618 | 							},
1619 | 							responses: {
1620 | 								"200": {
1621 | 									description: "Success",
1622 | 									content: {
1623 | 										"application/json": {
1624 | 											schema: {
1625 | 												type: "object",
1626 | 												properties: {
1627 | 													error: {
1628 | 														type: "string",
1629 | 													},
1630 | 													success: {
1631 | 														type: "boolean",
1632 | 													},
1633 | 												},
1634 | 												required: ["success"],
1635 | 											},
1636 | 										},
1637 | 									},
1638 | 								},
1639 | 							},
1640 | 						},
1641 | 						$Infer: {
1642 | 							body: {} as PermissionExclusive & {
1643 | 								userId?: string;
1644 | 								role?: InferAdminRolesFromOption<O>;
1645 | 							},
1646 | 						},
1647 | 					},
1648 | 				},
1649 | 				async (ctx) => {
1650 | 					if (!ctx.body?.permission && !ctx.body?.permissions) {
1651 | 						throw new APIError("BAD_REQUEST", {
1652 | 							message:
1653 | 								"invalid permission check. no permission(s) were passed.",
1654 | 						});
1655 | 					}
1656 | 					const session = await getSessionFromCtx(ctx);
1657 | 
1658 | 					if (!session && (ctx.request || ctx.headers)) {
1659 | 						throw new APIError("UNAUTHORIZED");
1660 | 					}
1661 | 					if (!session && !ctx.body.userId && !ctx.body.role) {
1662 | 						throw new APIError("BAD_REQUEST", {
1663 | 							message: "user id or role is required",
1664 | 						});
1665 | 					}
1666 | 					const user =
1667 | 						session?.user ||
1668 | 						(ctx.body.role
1669 | 							? { id: ctx.body.userId || "", role: ctx.body.role }
1670 | 							: null) ||
1671 | 						((await ctx.context.internalAdapter.findUserById(
1672 | 							ctx.body.userId as string,
1673 | 						)) as { role?: string; id: string });
1674 | 					if (!user) {
1675 | 						throw new APIError("BAD_REQUEST", {
1676 | 							message: "user not found",
1677 | 						});
1678 | 					}
1679 | 					const result = hasPermission({
1680 | 						userId: user.id,
1681 | 						role: user.role,
1682 | 						options: options as AdminOptions,
1683 | 						permissions: (ctx.body.permissions ?? ctx.body.permission) as any,
1684 | 					});
1685 | 					return ctx.json({
1686 | 						error: null,
1687 | 						success: result,
1688 | 					});
1689 | 				},
1690 | 			),
1691 | 		},
1692 | 		$ERROR_CODES: ADMIN_ERROR_CODES,
1693 | 		schema: mergeSchema(schema, opts.schema),
1694 | 		options: options as any,
1695 | 	} satisfies BetterAuthPlugin;
1696 | };
1697 | 
```
Page 58/69FirstPrevNextLast