#
tokens: 40944/50000 2/1094 files (page 54/67)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 54 of 67. 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
│       │   │   │   │   └── 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-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
│   ├── cli
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── commands
│   │   │   │   ├── generate.ts
│   │   │   │   ├── info.ts
│   │   │   │   ├── init.ts
│   │   │   │   ├── login.ts
│   │   │   │   ├── mcp.ts
│   │   │   │   ├── migrate.ts
│   │   │   │   └── secret.ts
│   │   │   ├── generators
│   │   │   │   ├── auth-config.ts
│   │   │   │   ├── drizzle.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kysely.ts
│   │   │   │   ├── prisma.ts
│   │   │   │   └── types.ts
│   │   │   ├── index.ts
│   │   │   └── utils
│   │   │       ├── add-svelte-kit-env-modules.ts
│   │   │       ├── check-package-managers.ts
│   │   │       ├── format-ms.ts
│   │   │       ├── get-config.ts
│   │   │       ├── get-package-info.ts
│   │   │       ├── get-tsconfig-info.ts
│   │   │       └── install-dependencies.ts
│   │   ├── test
│   │   │   ├── __snapshots__
│   │   │   │   ├── auth-schema-mysql-enum.txt
│   │   │   │   ├── auth-schema-mysql-number-id.txt
│   │   │   │   ├── auth-schema-mysql-passkey-number-id.txt
│   │   │   │   ├── auth-schema-mysql-passkey.txt
│   │   │   │   ├── auth-schema-mysql.txt
│   │   │   │   ├── auth-schema-number-id.txt
│   │   │   │   ├── auth-schema-pg-enum.txt
│   │   │   │   ├── auth-schema-pg-passkey.txt
│   │   │   │   ├── auth-schema-sqlite-enum.txt
│   │   │   │   ├── auth-schema-sqlite-number-id.txt
│   │   │   │   ├── auth-schema-sqlite-passkey-number-id.txt
│   │   │   │   ├── auth-schema-sqlite-passkey.txt
│   │   │   │   ├── auth-schema-sqlite.txt
│   │   │   │   ├── auth-schema.txt
│   │   │   │   ├── migrations.sql
│   │   │   │   ├── schema-mongodb.prisma
│   │   │   │   ├── schema-mysql-custom.prisma
│   │   │   │   ├── schema-mysql.prisma
│   │   │   │   ├── schema-numberid.prisma
│   │   │   │   └── schema.prisma
│   │   │   ├── generate-all-db.test.ts
│   │   │   ├── generate.test.ts
│   │   │   ├── get-config.test.ts
│   │   │   ├── info.test.ts
│   │   │   └── migrate.test.ts
│   │   ├── tsconfig.json
│   │   ├── tsconfig.test.json
│   │   └── tsdown.config.ts
│   ├── core
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── api
│   │   │   │   └── index.ts
│   │   │   ├── async_hooks
│   │   │   │   └── index.ts
│   │   │   ├── context
│   │   │   │   ├── endpoint-context.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── transaction.ts
│   │   │   ├── db
│   │   │   │   ├── adapter
│   │   │   │   │   └── index.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── plugin.ts
│   │   │   │   ├── schema
│   │   │   │   │   ├── account.ts
│   │   │   │   │   ├── rate-limit.ts
│   │   │   │   │   ├── session.ts
│   │   │   │   │   ├── shared.ts
│   │   │   │   │   ├── user.ts
│   │   │   │   │   └── verification.ts
│   │   │   │   └── type.ts
│   │   │   ├── env
│   │   │   │   ├── color-depth.ts
│   │   │   │   ├── env-impl.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── logger.test.ts
│   │   │   │   └── logger.ts
│   │   │   ├── error
│   │   │   │   ├── codes.ts
│   │   │   │   └── index.ts
│   │   │   ├── index.ts
│   │   │   ├── oauth2
│   │   │   │   ├── client-credentials-token.ts
│   │   │   │   ├── create-authorization-url.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── oauth-provider.ts
│   │   │   │   ├── refresh-access-token.ts
│   │   │   │   ├── utils.ts
│   │   │   │   └── validate-authorization-code.ts
│   │   │   ├── social-providers
│   │   │   │   ├── apple.ts
│   │   │   │   ├── atlassian.ts
│   │   │   │   ├── cognito.ts
│   │   │   │   ├── discord.ts
│   │   │   │   ├── dropbox.ts
│   │   │   │   ├── facebook.ts
│   │   │   │   ├── figma.ts
│   │   │   │   ├── github.ts
│   │   │   │   ├── gitlab.ts
│   │   │   │   ├── google.ts
│   │   │   │   ├── huggingface.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kakao.ts
│   │   │   │   ├── kick.ts
│   │   │   │   ├── line.ts
│   │   │   │   ├── linear.ts
│   │   │   │   ├── linkedin.ts
│   │   │   │   ├── microsoft-entra-id.ts
│   │   │   │   ├── naver.ts
│   │   │   │   ├── notion.ts
│   │   │   │   ├── paypal.ts
│   │   │   │   ├── reddit.ts
│   │   │   │   ├── roblox.ts
│   │   │   │   ├── salesforce.ts
│   │   │   │   ├── slack.ts
│   │   │   │   ├── spotify.ts
│   │   │   │   ├── tiktok.ts
│   │   │   │   ├── twitch.ts
│   │   │   │   ├── twitter.ts
│   │   │   │   ├── vk.ts
│   │   │   │   └── zoom.ts
│   │   │   ├── types
│   │   │   │   ├── context.ts
│   │   │   │   ├── cookie.ts
│   │   │   │   ├── helper.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── init-options.ts
│   │   │   │   ├── plugin-client.ts
│   │   │   │   └── plugin.ts
│   │   │   └── utils
│   │   │       ├── error-codes.ts
│   │   │       └── index.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── expo
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── expo.test.ts
│   │   │   └── index.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── sso
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── index.ts
│   │   │   ├── oidc.test.ts
│   │   │   └── saml.test.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── stripe
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── hooks.ts
│   │   │   ├── index.ts
│   │   │   ├── schema.ts
│   │   │   ├── stripe.test.ts
│   │   │   ├── types.ts
│   │   │   └── utils.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   └── telemetry
│       ├── package.json
│       ├── src
│       │   ├── detectors
│       │   │   ├── detect-auth-config.ts
│       │   │   ├── detect-database.ts
│       │   │   ├── detect-framework.ts
│       │   │   ├── detect-project-info.ts
│       │   │   ├── detect-runtime.ts
│       │   │   └── detect-system-info.ts
│       │   ├── index.ts
│       │   ├── project-id.ts
│       │   ├── telemetry.test.ts
│       │   ├── types.ts
│       │   └── utils
│       │       ├── hash.ts
│       │       ├── id.ts
│       │       ├── import-util.ts
│       │       └── package-json.ts
│       ├── tsconfig.json
│       └── tsdown.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── SECURITY.md
├── tsconfig.json
└── turbo.json
```

# Files

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/email-otp/index.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 { BetterAuthPlugin } from "@better-auth/core";
   8 | import {
   9 | 	generateRandomString,
  10 | 	symmetricDecrypt,
  11 | 	symmetricEncrypt,
  12 | } from "../../crypto";
  13 | import { getDate } from "../../utils/date";
  14 | import { setCookieCache, setSessionCookie } from "../../cookies";
  15 | import { getEndpointResponse } from "../../utils/plugin-helper";
  16 | import { defaultKeyHasher, splitAtLastColon } from "./utils";
  17 | import type { GenericEndpointContext } from "@better-auth/core";
  18 | import { defineErrorCodes } from "@better-auth/core/utils";
  19 | 
  20 | export interface EmailOTPOptions {
  21 | 	/**
  22 | 	 * Function to send email verification
  23 | 	 */
  24 | 	sendVerificationOTP: (
  25 | 		data: {
  26 | 			email: string;
  27 | 			otp: string;
  28 | 			type: "sign-in" | "email-verification" | "forget-password";
  29 | 		},
  30 | 		request?: Request,
  31 | 	) => Promise<void>;
  32 | 	/**
  33 | 	 * Length of the OTP
  34 | 	 *
  35 | 	 * @default 6
  36 | 	 */
  37 | 	otpLength?: number;
  38 | 	/**
  39 | 	 * Expiry time of the OTP in seconds
  40 | 	 *
  41 | 	 * @default 300 (5 minutes)
  42 | 	 */
  43 | 	expiresIn?: number;
  44 | 	/**
  45 | 	 * Custom function to generate otp
  46 | 	 */
  47 | 	generateOTP?: (
  48 | 		data: {
  49 | 			email: string;
  50 | 			type: "sign-in" | "email-verification" | "forget-password";
  51 | 		},
  52 | 		request?: Request,
  53 | 	) => string | undefined;
  54 | 	/**
  55 | 	 * Send email verification on sign-up
  56 | 	 *
  57 | 	 * @Default false
  58 | 	 */
  59 | 	sendVerificationOnSignUp?: boolean;
  60 | 	/**
  61 | 	 * A boolean value that determines whether to prevent
  62 | 	 * automatic sign-up when the user is not registered.
  63 | 	 *
  64 | 	 * @Default false
  65 | 	 */
  66 | 	disableSignUp?: boolean;
  67 | 	/**
  68 | 	 * Allowed attempts for the OTP code
  69 | 	 * @default 3
  70 | 	 */
  71 | 	allowedAttempts?: number;
  72 | 	/**
  73 | 	 * Store the OTP in your database in a secure way
  74 | 	 * Note: This will not affect the OTP sent to the user, it will only affect the OTP stored in your database
  75 | 	 *
  76 | 	 * @default "plain"
  77 | 	 */
  78 | 	storeOTP?:
  79 | 		| "hashed"
  80 | 		| "plain"
  81 | 		| "encrypted"
  82 | 		| { hash: (otp: string) => Promise<string> }
  83 | 		| {
  84 | 				encrypt: (otp: string) => Promise<string>;
  85 | 				decrypt: (otp: string) => Promise<string>;
  86 | 		  };
  87 | 	/**
  88 | 	 * Override the default email verification to use email otp instead
  89 | 	 *
  90 | 	 * @default false
  91 | 	 */
  92 | 	overrideDefaultEmailVerification?: boolean;
  93 | }
  94 | 
  95 | const types = ["email-verification", "sign-in", "forget-password"] as const;
  96 | const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
  97 | const defaultOTPGenerator = (options: EmailOTPOptions) =>
  98 | 	generateRandomString(options.otpLength ?? 6, "0-9");
  99 | 
 100 | const ERROR_CODES = defineErrorCodes({
 101 | 	OTP_EXPIRED: "otp expired",
 102 | 	INVALID_OTP: "Invalid OTP",
 103 | 	INVALID_EMAIL: "Invalid email",
 104 | 	USER_NOT_FOUND: "User not found",
 105 | 	TOO_MANY_ATTEMPTS: "Too many attempts",
 106 | });
 107 | 
 108 | export const emailOTP = (options: EmailOTPOptions) => {
 109 | 	const opts = {
 110 | 		expiresIn: 5 * 60,
 111 | 		generateOTP: () => defaultOTPGenerator(options),
 112 | 		storeOTP: "plain",
 113 | 		...options,
 114 | 	} satisfies EmailOTPOptions;
 115 | 
 116 | 	async function storeOTP(ctx: GenericEndpointContext, otp: string) {
 117 | 		if (opts.storeOTP === "encrypted") {
 118 | 			return await symmetricEncrypt({
 119 | 				key: ctx.context.secret,
 120 | 				data: otp,
 121 | 			});
 122 | 		}
 123 | 		if (opts.storeOTP === "hashed") {
 124 | 			return await defaultKeyHasher(otp);
 125 | 		}
 126 | 		if (typeof opts.storeOTP === "object" && "hash" in opts.storeOTP) {
 127 | 			return await opts.storeOTP.hash(otp);
 128 | 		}
 129 | 		if (typeof opts.storeOTP === "object" && "encrypt" in opts.storeOTP) {
 130 | 			return await opts.storeOTP.encrypt(otp);
 131 | 		}
 132 | 
 133 | 		return otp;
 134 | 	}
 135 | 
 136 | 	async function verifyStoredOTP(
 137 | 		ctx: GenericEndpointContext,
 138 | 		storedOtp: string,
 139 | 		otp: string,
 140 | 	): Promise<boolean> {
 141 | 		if (opts.storeOTP === "encrypted") {
 142 | 			return (
 143 | 				(await symmetricDecrypt({
 144 | 					key: ctx.context.secret,
 145 | 					data: storedOtp,
 146 | 				})) === otp
 147 | 			);
 148 | 		}
 149 | 		if (opts.storeOTP === "hashed") {
 150 | 			const hashedOtp = await defaultKeyHasher(otp);
 151 | 			return hashedOtp === storedOtp;
 152 | 		}
 153 | 		if (typeof opts.storeOTP === "object" && "hash" in opts.storeOTP) {
 154 | 			const hashedOtp = await opts.storeOTP.hash(otp);
 155 | 			return hashedOtp === storedOtp;
 156 | 		}
 157 | 		if (typeof opts.storeOTP === "object" && "decrypt" in opts.storeOTP) {
 158 | 			const decryptedOtp = await opts.storeOTP.decrypt(storedOtp);
 159 | 			return decryptedOtp === otp;
 160 | 		}
 161 | 
 162 | 		return otp === storedOtp;
 163 | 	}
 164 | 	const endpoints = {
 165 | 		/**
 166 | 		 * ### Endpoint
 167 | 		 *
 168 | 		 * POST `/email-otp/send-verification-otp`
 169 | 		 *
 170 | 		 * ### API Methods
 171 | 		 *
 172 | 		 * **server:**
 173 | 		 * `auth.api.sendVerificationOTP`
 174 | 		 *
 175 | 		 * **client:**
 176 | 		 * `authClient.emailOtp.sendVerificationOtp`
 177 | 		 *
 178 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-email-otp-send-verification-otp)
 179 | 		 */
 180 | 		sendVerificationOTP: createAuthEndpoint(
 181 | 			"/email-otp/send-verification-otp",
 182 | 			{
 183 | 				method: "POST",
 184 | 				body: z.object({
 185 | 					email: z.string({}).meta({
 186 | 						description: "Email address to send the OTP",
 187 | 					}),
 188 | 					type: z.enum(types).meta({
 189 | 						description: "Type of the OTP",
 190 | 					}),
 191 | 				}),
 192 | 				metadata: {
 193 | 					openapi: {
 194 | 						description: "Send verification OTP",
 195 | 						responses: {
 196 | 							200: {
 197 | 								description: "Success",
 198 | 								content: {
 199 | 									"application/json": {
 200 | 										schema: {
 201 | 											type: "object",
 202 | 											properties: {
 203 | 												success: {
 204 | 													type: "boolean",
 205 | 												},
 206 | 											},
 207 | 										},
 208 | 									},
 209 | 								},
 210 | 							},
 211 | 						},
 212 | 					},
 213 | 				},
 214 | 			},
 215 | 			async (ctx) => {
 216 | 				if (!options?.sendVerificationOTP) {
 217 | 					ctx.context.logger.error(
 218 | 						"send email verification is not implemented",
 219 | 					);
 220 | 					throw new APIError("BAD_REQUEST", {
 221 | 						message: "send email verification is not implemented",
 222 | 					});
 223 | 				}
 224 | 				const email = ctx.body.email;
 225 | 				if (!emailRegex.test(email)) {
 226 | 					throw ctx.error("BAD_REQUEST", {
 227 | 						message: ERROR_CODES.INVALID_EMAIL,
 228 | 					});
 229 | 				}
 230 | 				if (opts.disableSignUp) {
 231 | 					const user = await ctx.context.internalAdapter.findUserByEmail(email);
 232 | 					if (!user) {
 233 | 						throw new APIError("BAD_REQUEST", {
 234 | 							message: ERROR_CODES.USER_NOT_FOUND,
 235 | 						});
 236 | 					}
 237 | 				} else if (ctx.body.type === "forget-password") {
 238 | 					const user = await ctx.context.internalAdapter.findUserByEmail(email);
 239 | 					if (!user) {
 240 | 						return ctx.json({
 241 | 							success: true,
 242 | 						});
 243 | 					}
 244 | 				}
 245 | 				let otp =
 246 | 					opts.generateOTP({ email, type: ctx.body.type }, ctx.request) ||
 247 | 					defaultOTPGenerator(opts);
 248 | 
 249 | 				let storedOTP = await storeOTP(ctx, otp);
 250 | 
 251 | 				await ctx.context.internalAdapter
 252 | 					.createVerificationValue({
 253 | 						value: `${storedOTP}:0`,
 254 | 						identifier: `${ctx.body.type}-otp-${email}`,
 255 | 						expiresAt: getDate(opts.expiresIn, "sec"),
 256 | 					})
 257 | 					.catch(async (error) => {
 258 | 						// might be duplicate key error
 259 | 						await ctx.context.internalAdapter.deleteVerificationByIdentifier(
 260 | 							`${ctx.body.type}-otp-${email}`,
 261 | 						);
 262 | 						//try again
 263 | 						await ctx.context.internalAdapter.createVerificationValue({
 264 | 							value: `${storedOTP}:0`,
 265 | 							identifier: `${ctx.body.type}-otp-${email}`,
 266 | 							expiresAt: getDate(opts.expiresIn, "sec"),
 267 | 						});
 268 | 					});
 269 | 				await options.sendVerificationOTP(
 270 | 					{
 271 | 						email,
 272 | 						otp,
 273 | 						type: ctx.body.type,
 274 | 					},
 275 | 					ctx.request,
 276 | 				);
 277 | 				return ctx.json({
 278 | 					success: true,
 279 | 				});
 280 | 			},
 281 | 		),
 282 | 	};
 283 | 
 284 | 	return {
 285 | 		id: "email-otp",
 286 | 		init(ctx) {
 287 | 			if (!opts.overrideDefaultEmailVerification) {
 288 | 				return;
 289 | 			}
 290 | 			return {
 291 | 				options: {
 292 | 					emailVerification: {
 293 | 						async sendVerificationEmail(data, request) {
 294 | 							await endpoints.sendVerificationOTP({
 295 | 								//@ts-expect-error - we need to pass the context
 296 | 								context: ctx,
 297 | 								request: request,
 298 | 								body: {
 299 | 									email: data.user.email,
 300 | 									type: "email-verification",
 301 | 								},
 302 | 								ctx,
 303 | 							});
 304 | 						},
 305 | 					},
 306 | 				},
 307 | 			};
 308 | 		},
 309 | 		endpoints: {
 310 | 			...endpoints,
 311 | 			createVerificationOTP: createAuthEndpoint(
 312 | 				"/email-otp/create-verification-otp",
 313 | 				{
 314 | 					method: "POST",
 315 | 					body: z.object({
 316 | 						email: z.string({}).meta({
 317 | 							description: "Email address to send the OTP",
 318 | 						}),
 319 | 						type: z.enum(types).meta({
 320 | 							required: true,
 321 | 							description: "Type of the OTP",
 322 | 						}),
 323 | 					}),
 324 | 					metadata: {
 325 | 						SERVER_ONLY: true,
 326 | 						openapi: {
 327 | 							description: "Create verification OTP",
 328 | 							responses: {
 329 | 								200: {
 330 | 									description: "Success",
 331 | 									content: {
 332 | 										"application/json": {
 333 | 											schema: {
 334 | 												type: "string",
 335 | 											},
 336 | 										},
 337 | 									},
 338 | 								},
 339 | 							},
 340 | 						},
 341 | 					},
 342 | 				},
 343 | 				async (ctx) => {
 344 | 					const email = ctx.body.email;
 345 | 					const otp =
 346 | 						opts.generateOTP({ email, type: ctx.body.type }, ctx.request) ||
 347 | 						defaultOTPGenerator(opts);
 348 | 					let storedOTP = await storeOTP(ctx, otp);
 349 | 					await ctx.context.internalAdapter.createVerificationValue({
 350 | 						value: `${storedOTP}:0`,
 351 | 						identifier: `${ctx.body.type}-otp-${email}`,
 352 | 						expiresAt: getDate(opts.expiresIn, "sec"),
 353 | 					});
 354 | 					return otp;
 355 | 				},
 356 | 			),
 357 | 			/**
 358 | 			 * ### Endpoint
 359 | 			 *
 360 | 			 * GET `/email-otp/get-verification-otp`
 361 | 			 *
 362 | 			 * ### API Methods
 363 | 			 *
 364 | 			 * **server:**
 365 | 			 * `auth.api.getVerificationOTP`
 366 | 			 *
 367 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-email-otp-get-verification-otp)
 368 | 			 */
 369 | 			getVerificationOTP: createAuthEndpoint(
 370 | 				"/email-otp/get-verification-otp",
 371 | 				{
 372 | 					method: "GET",
 373 | 					query: z.object({
 374 | 						email: z.string({}).meta({
 375 | 							description: "Email address the OTP was sent to",
 376 | 						}),
 377 | 						type: z.enum(types).meta({
 378 | 							required: true,
 379 | 							description: "Type of the OTP",
 380 | 						}),
 381 | 					}),
 382 | 					metadata: {
 383 | 						SERVER_ONLY: true,
 384 | 						openapi: {
 385 | 							description: "Get verification OTP",
 386 | 							responses: {
 387 | 								"200": {
 388 | 									description:
 389 | 										"OTP retrieved successfully or not found/expired",
 390 | 									content: {
 391 | 										"application/json": {
 392 | 											schema: {
 393 | 												type: "object",
 394 | 												properties: {
 395 | 													otp: {
 396 | 														type: "string",
 397 | 														nullable: true,
 398 | 														description:
 399 | 															"The stored OTP, or null if not found or expired",
 400 | 													},
 401 | 												},
 402 | 												required: ["otp"],
 403 | 											},
 404 | 										},
 405 | 									},
 406 | 								},
 407 | 							},
 408 | 						},
 409 | 					},
 410 | 				},
 411 | 				async (ctx) => {
 412 | 					const email = ctx.query.email;
 413 | 					const verificationValue =
 414 | 						await ctx.context.internalAdapter.findVerificationValue(
 415 | 							`${ctx.query.type}-otp-${email}`,
 416 | 						);
 417 | 					if (!verificationValue || verificationValue.expiresAt < new Date()) {
 418 | 						return ctx.json({
 419 | 							otp: null,
 420 | 						});
 421 | 					}
 422 | 					if (
 423 | 						opts.storeOTP === "hashed" ||
 424 | 						(typeof opts.storeOTP === "object" && "hash" in opts.storeOTP)
 425 | 					) {
 426 | 						throw new APIError("BAD_REQUEST", {
 427 | 							message: "OTP is hashed, cannot return the plain text OTP",
 428 | 						});
 429 | 					}
 430 | 
 431 | 					let [storedOtp, _attempts] = splitAtLastColon(
 432 | 						verificationValue.value,
 433 | 					);
 434 | 					let otp = storedOtp;
 435 | 					if (opts.storeOTP === "encrypted") {
 436 | 						otp = await symmetricDecrypt({
 437 | 							key: ctx.context.secret,
 438 | 							data: storedOtp,
 439 | 						});
 440 | 					}
 441 | 
 442 | 					if (typeof opts.storeOTP === "object" && "decrypt" in opts.storeOTP) {
 443 | 						otp = await opts.storeOTP.decrypt(storedOtp);
 444 | 					}
 445 | 
 446 | 					return ctx.json({
 447 | 						otp,
 448 | 					});
 449 | 				},
 450 | 			),
 451 | 			/**
 452 | 			 * ### Endpoint
 453 | 			 *
 454 | 			 * GET `/email-otp/check-verification-otp`
 455 | 			 *
 456 | 			 * ### API Methods
 457 | 			 *
 458 | 			 * **server:**
 459 | 			 * `auth.api.checkVerificationOTP`
 460 | 			 *
 461 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-email-otp-check-verification-otp)
 462 | 			 */
 463 | 			checkVerificationOTP: createAuthEndpoint(
 464 | 				"/email-otp/check-verification-otp",
 465 | 				{
 466 | 					method: "POST",
 467 | 					body: z.object({
 468 | 						email: z.string().meta({
 469 | 							description: "Email address the OTP was sent to",
 470 | 						}),
 471 | 						type: z.enum(types).meta({
 472 | 							required: true,
 473 | 							description: "Type of the OTP",
 474 | 						}),
 475 | 						otp: z.string().meta({
 476 | 							required: true,
 477 | 							description: "OTP to verify",
 478 | 						}),
 479 | 					}),
 480 | 					metadata: {
 481 | 						openapi: {
 482 | 							description: "Check if a verification OTP is valid",
 483 | 							responses: {
 484 | 								200: {
 485 | 									description: "Success",
 486 | 									content: {
 487 | 										"application/json": {
 488 | 											schema: {
 489 | 												type: "object",
 490 | 												properties: {
 491 | 													success: {
 492 | 														type: "boolean",
 493 | 													},
 494 | 												},
 495 | 											},
 496 | 										},
 497 | 									},
 498 | 								},
 499 | 							},
 500 | 						},
 501 | 					},
 502 | 				},
 503 | 				async (ctx) => {
 504 | 					const email = ctx.body.email;
 505 | 					if (!emailRegex.test(email)) {
 506 | 						throw new APIError("BAD_REQUEST", {
 507 | 							message: ERROR_CODES.INVALID_EMAIL,
 508 | 						});
 509 | 					}
 510 | 					const user = await ctx.context.internalAdapter.findUserByEmail(email);
 511 | 					if (!user) {
 512 | 						throw new APIError("BAD_REQUEST", {
 513 | 							message: ERROR_CODES.USER_NOT_FOUND,
 514 | 						});
 515 | 					}
 516 | 					const verificationValue =
 517 | 						await ctx.context.internalAdapter.findVerificationValue(
 518 | 							`${ctx.body.type}-otp-${email}`,
 519 | 						);
 520 | 					if (!verificationValue) {
 521 | 						throw new APIError("BAD_REQUEST", {
 522 | 							message: ERROR_CODES.INVALID_OTP,
 523 | 						});
 524 | 					}
 525 | 					if (verificationValue.expiresAt < new Date()) {
 526 | 						await ctx.context.internalAdapter.deleteVerificationValue(
 527 | 							verificationValue.id,
 528 | 						);
 529 | 						throw new APIError("BAD_REQUEST", {
 530 | 							message: ERROR_CODES.OTP_EXPIRED,
 531 | 						});
 532 | 					}
 533 | 
 534 | 					const [otpValue, attempts] = splitAtLastColon(
 535 | 						verificationValue.value,
 536 | 					);
 537 | 					const allowedAttempts = options?.allowedAttempts || 3;
 538 | 					if (attempts && parseInt(attempts) >= allowedAttempts) {
 539 | 						await ctx.context.internalAdapter.deleteVerificationValue(
 540 | 							verificationValue.id,
 541 | 						);
 542 | 						throw new APIError("FORBIDDEN", {
 543 | 							message: ERROR_CODES.TOO_MANY_ATTEMPTS,
 544 | 						});
 545 | 					}
 546 | 					const verified = await verifyStoredOTP(ctx, otpValue, ctx.body.otp);
 547 | 					if (!verified) {
 548 | 						await ctx.context.internalAdapter.updateVerificationValue(
 549 | 							verificationValue.id,
 550 | 							{
 551 | 								value: `${otpValue}:${parseInt(attempts || "0") + 1}`,
 552 | 							},
 553 | 						);
 554 | 						throw new APIError("BAD_REQUEST", {
 555 | 							message: ERROR_CODES.INVALID_OTP,
 556 | 						});
 557 | 					}
 558 | 					return ctx.json({
 559 | 						success: true,
 560 | 					});
 561 | 				},
 562 | 			),
 563 | 			/**
 564 | 			 * ### Endpoint
 565 | 			 *
 566 | 			 * POST `/email-otp/verify-email`
 567 | 			 *
 568 | 			 * ### API Methods
 569 | 			 *
 570 | 			 * **server:**
 571 | 			 * `auth.api.verifyEmailOTP`
 572 | 			 *
 573 | 			 * **client:**
 574 | 			 * `authClient.emailOtp.verifyEmail`
 575 | 			 *
 576 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-email-otp-verify-email)
 577 | 			 */
 578 | 			verifyEmailOTP: createAuthEndpoint(
 579 | 				"/email-otp/verify-email",
 580 | 				{
 581 | 					method: "POST",
 582 | 					body: z.object({
 583 | 						email: z.string({}).meta({
 584 | 							description: "Email address to verify",
 585 | 						}),
 586 | 						otp: z.string().meta({
 587 | 							required: true,
 588 | 							description: "OTP to verify",
 589 | 						}),
 590 | 					}),
 591 | 					metadata: {
 592 | 						openapi: {
 593 | 							description: "Verify email with OTP",
 594 | 							responses: {
 595 | 								200: {
 596 | 									description: "Success",
 597 | 									content: {
 598 | 										"application/json": {
 599 | 											schema: {
 600 | 												type: "object",
 601 | 												properties: {
 602 | 													status: {
 603 | 														type: "boolean",
 604 | 														description:
 605 | 															"Indicates if the verification was successful",
 606 | 														enum: [true],
 607 | 													},
 608 | 													token: {
 609 | 														type: "string",
 610 | 														nullable: true,
 611 | 														description:
 612 | 															"Session token if autoSignInAfterVerification is enabled, otherwise null",
 613 | 													},
 614 | 													user: {
 615 | 														$ref: "#/components/schemas/User",
 616 | 													},
 617 | 													required: ["status", "token", "user"],
 618 | 												},
 619 | 											},
 620 | 										},
 621 | 									},
 622 | 								},
 623 | 							},
 624 | 						},
 625 | 					},
 626 | 				},
 627 | 				async (ctx) => {
 628 | 					const email = ctx.body.email;
 629 | 					if (!emailRegex.test(email)) {
 630 | 						throw new APIError("BAD_REQUEST", {
 631 | 							message: ERROR_CODES.INVALID_EMAIL,
 632 | 						});
 633 | 					}
 634 | 					const verificationValue =
 635 | 						await ctx.context.internalAdapter.findVerificationValue(
 636 | 							`email-verification-otp-${email}`,
 637 | 						);
 638 | 
 639 | 					if (!verificationValue) {
 640 | 						throw new APIError("BAD_REQUEST", {
 641 | 							message: ERROR_CODES.INVALID_OTP,
 642 | 						});
 643 | 					}
 644 | 					if (verificationValue.expiresAt < new Date()) {
 645 | 						throw new APIError("BAD_REQUEST", {
 646 | 							message: ERROR_CODES.OTP_EXPIRED,
 647 | 						});
 648 | 					}
 649 | 
 650 | 					const [otpValue, attempts] = splitAtLastColon(
 651 | 						verificationValue.value,
 652 | 					);
 653 | 					const allowedAttempts = options?.allowedAttempts || 3;
 654 | 					if (attempts && parseInt(attempts) >= allowedAttempts) {
 655 | 						await ctx.context.internalAdapter.deleteVerificationValue(
 656 | 							verificationValue.id,
 657 | 						);
 658 | 						throw new APIError("FORBIDDEN", {
 659 | 							message: ERROR_CODES.TOO_MANY_ATTEMPTS,
 660 | 						});
 661 | 					}
 662 | 					const verified = await verifyStoredOTP(ctx, otpValue, ctx.body.otp);
 663 | 					if (!verified) {
 664 | 						await ctx.context.internalAdapter.updateVerificationValue(
 665 | 							verificationValue.id,
 666 | 							{
 667 | 								value: `${otpValue}:${parseInt(attempts || "0") + 1}`,
 668 | 							},
 669 | 						);
 670 | 						throw new APIError("BAD_REQUEST", {
 671 | 							message: ERROR_CODES.INVALID_OTP,
 672 | 						});
 673 | 					}
 674 | 					await ctx.context.internalAdapter.deleteVerificationValue(
 675 | 						verificationValue.id,
 676 | 					);
 677 | 					const user = await ctx.context.internalAdapter.findUserByEmail(email);
 678 | 					if (!user) {
 679 | 						throw new APIError("BAD_REQUEST", {
 680 | 							message: ERROR_CODES.USER_NOT_FOUND,
 681 | 						});
 682 | 					}
 683 | 					const updatedUser = await ctx.context.internalAdapter.updateUser(
 684 | 						user.user.id,
 685 | 						{
 686 | 							email,
 687 | 							emailVerified: true,
 688 | 						},
 689 | 					);
 690 | 					await ctx.context.options.emailVerification?.onEmailVerification?.(
 691 | 						updatedUser,
 692 | 						ctx.request,
 693 | 					);
 694 | 
 695 | 					if (
 696 | 						ctx.context.options.emailVerification?.autoSignInAfterVerification
 697 | 					) {
 698 | 						const session = await ctx.context.internalAdapter.createSession(
 699 | 							updatedUser.id,
 700 | 						);
 701 | 						await setSessionCookie(ctx, {
 702 | 							session,
 703 | 							user: updatedUser,
 704 | 						});
 705 | 						return ctx.json({
 706 | 							status: true,
 707 | 							token: session.token,
 708 | 							user: {
 709 | 								id: updatedUser.id,
 710 | 								email: updatedUser.email,
 711 | 								emailVerified: updatedUser.emailVerified,
 712 | 								name: updatedUser.name,
 713 | 								image: updatedUser.image,
 714 | 								createdAt: updatedUser.createdAt,
 715 | 								updatedAt: updatedUser.updatedAt,
 716 | 							},
 717 | 						});
 718 | 					}
 719 | 					const currentSession = await getSessionFromCtx(ctx);
 720 | 					if (currentSession && updatedUser.emailVerified) {
 721 | 						const dontRememberMeCookie = await ctx.getSignedCookie(
 722 | 							ctx.context.authCookies.dontRememberToken.name,
 723 | 							ctx.context.secret,
 724 | 						);
 725 | 						await setCookieCache(
 726 | 							ctx,
 727 | 							{
 728 | 								session: currentSession.session,
 729 | 								user: {
 730 | 									...currentSession.user,
 731 | 									emailVerified: true,
 732 | 								},
 733 | 							},
 734 | 							!!dontRememberMeCookie,
 735 | 						);
 736 | 					}
 737 | 					return ctx.json({
 738 | 						status: true,
 739 | 						token: null,
 740 | 						user: {
 741 | 							id: updatedUser.id,
 742 | 							email: updatedUser.email,
 743 | 							emailVerified: updatedUser.emailVerified,
 744 | 							name: updatedUser.name,
 745 | 							image: updatedUser.image,
 746 | 							createdAt: updatedUser.createdAt,
 747 | 							updatedAt: updatedUser.updatedAt,
 748 | 						},
 749 | 					});
 750 | 				},
 751 | 			),
 752 | 			/**
 753 | 			 * ### Endpoint
 754 | 			 *
 755 | 			 * POST `/sign-in/email-otp`
 756 | 			 *
 757 | 			 * ### API Methods
 758 | 			 *
 759 | 			 * **server:**
 760 | 			 * `auth.api.signInEmailOTP`
 761 | 			 *
 762 | 			 * **client:**
 763 | 			 * `authClient.signIn.emailOtp`
 764 | 			 *
 765 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-sign-in-email-otp)
 766 | 			 */
 767 | 			signInEmailOTP: createAuthEndpoint(
 768 | 				"/sign-in/email-otp",
 769 | 				{
 770 | 					method: "POST",
 771 | 					body: z.object({
 772 | 						email: z.string({}).meta({
 773 | 							description: "Email address to sign in",
 774 | 						}),
 775 | 						otp: z.string().meta({
 776 | 							required: true,
 777 | 							description: "OTP sent to the email",
 778 | 						}),
 779 | 					}),
 780 | 					metadata: {
 781 | 						openapi: {
 782 | 							description: "Sign in with OTP",
 783 | 							responses: {
 784 | 								200: {
 785 | 									description: "Success",
 786 | 									content: {
 787 | 										"application/json": {
 788 | 											schema: {
 789 | 												type: "object",
 790 | 												properties: {
 791 | 													token: {
 792 | 														type: "string",
 793 | 														description:
 794 | 															"Session token for the authenticated session",
 795 | 													},
 796 | 													user: {
 797 | 														$ref: "#/components/schemas/User",
 798 | 													},
 799 | 												},
 800 | 												required: ["token", "user"],
 801 | 											},
 802 | 										},
 803 | 									},
 804 | 								},
 805 | 							},
 806 | 						},
 807 | 					},
 808 | 				},
 809 | 				async (ctx) => {
 810 | 					const email = ctx.body.email;
 811 | 					const verificationValue =
 812 | 						await ctx.context.internalAdapter.findVerificationValue(
 813 | 							`sign-in-otp-${email}`,
 814 | 						);
 815 | 					if (!verificationValue) {
 816 | 						throw new APIError("BAD_REQUEST", {
 817 | 							message: ERROR_CODES.INVALID_OTP,
 818 | 						});
 819 | 					}
 820 | 					if (verificationValue.expiresAt < new Date()) {
 821 | 						throw new APIError("BAD_REQUEST", {
 822 | 							message: ERROR_CODES.OTP_EXPIRED,
 823 | 						});
 824 | 					}
 825 | 					const [otpValue, attempts] = splitAtLastColon(
 826 | 						verificationValue.value,
 827 | 					);
 828 | 					const allowedAttempts = options?.allowedAttempts || 3;
 829 | 					if (attempts && parseInt(attempts) >= allowedAttempts) {
 830 | 						await ctx.context.internalAdapter.deleteVerificationValue(
 831 | 							verificationValue.id,
 832 | 						);
 833 | 						throw new APIError("FORBIDDEN", {
 834 | 							message: ERROR_CODES.TOO_MANY_ATTEMPTS,
 835 | 						});
 836 | 					}
 837 | 					const verified = await verifyStoredOTP(ctx, otpValue, ctx.body.otp);
 838 | 					if (!verified) {
 839 | 						await ctx.context.internalAdapter.updateVerificationValue(
 840 | 							verificationValue.id,
 841 | 							{
 842 | 								value: `${otpValue}:${parseInt(attempts || "0") + 1}`,
 843 | 							},
 844 | 						);
 845 | 						throw new APIError("BAD_REQUEST", {
 846 | 							message: ERROR_CODES.INVALID_OTP,
 847 | 						});
 848 | 					}
 849 | 					await ctx.context.internalAdapter.deleteVerificationValue(
 850 | 						verificationValue.id,
 851 | 					);
 852 | 					const user = await ctx.context.internalAdapter.findUserByEmail(email);
 853 | 					if (!user) {
 854 | 						if (opts.disableSignUp) {
 855 | 							throw new APIError("BAD_REQUEST", {
 856 | 								message: ERROR_CODES.USER_NOT_FOUND,
 857 | 							});
 858 | 						}
 859 | 						const newUser = await ctx.context.internalAdapter.createUser({
 860 | 							email,
 861 | 							emailVerified: true,
 862 | 							name: "",
 863 | 						});
 864 | 						const session = await ctx.context.internalAdapter.createSession(
 865 | 							newUser.id,
 866 | 						);
 867 | 						await setSessionCookie(ctx, {
 868 | 							session,
 869 | 							user: newUser,
 870 | 						});
 871 | 						return ctx.json({
 872 | 							token: session.token,
 873 | 							user: {
 874 | 								id: newUser.id,
 875 | 								email: newUser.email,
 876 | 								emailVerified: newUser.emailVerified,
 877 | 								name: newUser.name,
 878 | 								image: newUser.image,
 879 | 								createdAt: newUser.createdAt,
 880 | 								updatedAt: newUser.updatedAt,
 881 | 							},
 882 | 						});
 883 | 					}
 884 | 
 885 | 					if (!user.user.emailVerified) {
 886 | 						await ctx.context.internalAdapter.updateUser(user.user.id, {
 887 | 							emailVerified: true,
 888 | 						});
 889 | 					}
 890 | 
 891 | 					const session = await ctx.context.internalAdapter.createSession(
 892 | 						user.user.id,
 893 | 					);
 894 | 					await setSessionCookie(ctx, {
 895 | 						session,
 896 | 						user: user.user,
 897 | 					});
 898 | 					return ctx.json({
 899 | 						token: session.token,
 900 | 						user: {
 901 | 							id: user.user.id,
 902 | 							email: user.user.email,
 903 | 							emailVerified: user.user.emailVerified,
 904 | 							name: user.user.name,
 905 | 							image: user.user.image,
 906 | 							createdAt: user.user.createdAt,
 907 | 							updatedAt: user.user.updatedAt,
 908 | 						},
 909 | 					});
 910 | 				},
 911 | 			),
 912 | 			/**
 913 | 			 * ### Endpoint
 914 | 			 *
 915 | 			 * POST `/forget-password/email-otp`
 916 | 			 *
 917 | 			 * ### API Methods
 918 | 			 *
 919 | 			 * **server:**
 920 | 			 * `auth.api.forgetPasswordEmailOTP`
 921 | 			 *
 922 | 			 * **client:**
 923 | 			 * `authClient.forgetPassword.emailOtp`
 924 | 			 *
 925 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-forget-password-email-otp)
 926 | 			 */
 927 | 			forgetPasswordEmailOTP: createAuthEndpoint(
 928 | 				"/forget-password/email-otp",
 929 | 				{
 930 | 					method: "POST",
 931 | 					body: z.object({
 932 | 						email: z.string().meta({
 933 | 							description: "Email address to send the OTP",
 934 | 						}),
 935 | 					}),
 936 | 					metadata: {
 937 | 						openapi: {
 938 | 							description: "Send a password reset OTP to the user",
 939 | 							responses: {
 940 | 								200: {
 941 | 									description: "Success",
 942 | 									content: {
 943 | 										"application/json": {
 944 | 											schema: {
 945 | 												type: "object",
 946 | 												properties: {
 947 | 													success: {
 948 | 														type: "boolean",
 949 | 														description:
 950 | 															"Indicates if the OTP was sent successfully",
 951 | 													},
 952 | 												},
 953 | 											},
 954 | 										},
 955 | 									},
 956 | 								},
 957 | 							},
 958 | 						},
 959 | 					},
 960 | 				},
 961 | 				async (ctx) => {
 962 | 					const email = ctx.body.email;
 963 | 					const user = await ctx.context.internalAdapter.findUserByEmail(email);
 964 | 					if (!user) {
 965 | 						throw new APIError("BAD_REQUEST", {
 966 | 							message: ERROR_CODES.USER_NOT_FOUND,
 967 | 						});
 968 | 					}
 969 | 					const otp =
 970 | 						opts.generateOTP({ email, type: "forget-password" }, ctx.request) ||
 971 | 						defaultOTPGenerator(opts);
 972 | 					let storedOTP = await storeOTP(ctx, otp);
 973 | 					await ctx.context.internalAdapter.createVerificationValue({
 974 | 						value: `${storedOTP}:0`,
 975 | 						identifier: `forget-password-otp-${email}`,
 976 | 						expiresAt: getDate(opts.expiresIn, "sec"),
 977 | 					});
 978 | 					await options.sendVerificationOTP(
 979 | 						{
 980 | 							email,
 981 | 							otp,
 982 | 							type: "forget-password",
 983 | 						},
 984 | 						ctx.request,
 985 | 					);
 986 | 					return ctx.json({
 987 | 						success: true,
 988 | 					});
 989 | 				},
 990 | 			),
 991 | 			/**
 992 | 			 * ### Endpoint
 993 | 			 *
 994 | 			 * POST `/email-otp/reset-password`
 995 | 			 *
 996 | 			 * ### API Methods
 997 | 			 *
 998 | 			 * **server:**
 999 | 			 * `auth.api.resetPasswordEmailOTP`
1000 | 			 *
1001 | 			 * **client:**
1002 | 			 * `authClient.emailOtp.resetPassword`
1003 | 			 *
1004 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/email-otp#api-method-email-otp-reset-password)
1005 | 			 */
1006 | 			resetPasswordEmailOTP: createAuthEndpoint(
1007 | 				"/email-otp/reset-password",
1008 | 				{
1009 | 					method: "POST",
1010 | 					body: z.object({
1011 | 						email: z.string().meta({
1012 | 							description: "Email address to reset the password",
1013 | 						}),
1014 | 						otp: z.string().meta({
1015 | 							description: "OTP sent to the email",
1016 | 						}),
1017 | 						password: z.string().meta({
1018 | 							description: "New password",
1019 | 						}),
1020 | 					}),
1021 | 					metadata: {
1022 | 						openapi: {
1023 | 							description: "Reset user password with OTP",
1024 | 							responses: {
1025 | 								200: {
1026 | 									description: "Success",
1027 | 									contnt: {
1028 | 										"application/json": {
1029 | 											schema: {
1030 | 												type: "object",
1031 | 												properties: {
1032 | 													success: {
1033 | 														type: "boolean",
1034 | 													},
1035 | 												},
1036 | 											},
1037 | 										},
1038 | 									},
1039 | 								},
1040 | 							},
1041 | 						},
1042 | 					},
1043 | 				},
1044 | 				async (ctx) => {
1045 | 					const email = ctx.body.email;
1046 | 					const user = await ctx.context.internalAdapter.findUserByEmail(
1047 | 						email,
1048 | 						{
1049 | 							includeAccounts: true,
1050 | 						},
1051 | 					);
1052 | 					if (!user) {
1053 | 						throw new APIError("BAD_REQUEST", {
1054 | 							message: ERROR_CODES.USER_NOT_FOUND,
1055 | 						});
1056 | 					}
1057 | 					const verificationValue =
1058 | 						await ctx.context.internalAdapter.findVerificationValue(
1059 | 							`forget-password-otp-${email}`,
1060 | 						);
1061 | 					if (!verificationValue) {
1062 | 						throw new APIError("BAD_REQUEST", {
1063 | 							message: ERROR_CODES.INVALID_OTP,
1064 | 						});
1065 | 					}
1066 | 					if (verificationValue.expiresAt < new Date()) {
1067 | 						await ctx.context.internalAdapter.deleteVerificationValue(
1068 | 							verificationValue.id,
1069 | 						);
1070 | 						throw new APIError("BAD_REQUEST", {
1071 | 							message: ERROR_CODES.OTP_EXPIRED,
1072 | 						});
1073 | 					}
1074 | 					const [otpValue, attempts] = splitAtLastColon(
1075 | 						verificationValue.value,
1076 | 					);
1077 | 					const allowedAttempts = options?.allowedAttempts || 3;
1078 | 					if (attempts && parseInt(attempts) >= allowedAttempts) {
1079 | 						await ctx.context.internalAdapter.deleteVerificationValue(
1080 | 							verificationValue.id,
1081 | 						);
1082 | 						throw new APIError("FORBIDDEN", {
1083 | 							message: ERROR_CODES.TOO_MANY_ATTEMPTS,
1084 | 						});
1085 | 					}
1086 | 					const verified = await verifyStoredOTP(ctx, otpValue, ctx.body.otp);
1087 | 					if (!verified) {
1088 | 						await ctx.context.internalAdapter.updateVerificationValue(
1089 | 							verificationValue.id,
1090 | 							{
1091 | 								value: `${otpValue}:${parseInt(attempts || "0") + 1}`,
1092 | 							},
1093 | 						);
1094 | 						throw new APIError("BAD_REQUEST", {
1095 | 							message: ERROR_CODES.INVALID_OTP,
1096 | 						});
1097 | 					}
1098 | 					await ctx.context.internalAdapter.deleteVerificationValue(
1099 | 						verificationValue.id,
1100 | 					);
1101 | 					const passwordHash = await ctx.context.password.hash(
1102 | 						ctx.body.password,
1103 | 					);
1104 | 					const account = user.accounts.find(
1105 | 						(account) => account.providerId === "credential",
1106 | 					);
1107 | 					if (!account) {
1108 | 						await ctx.context.internalAdapter.createAccount({
1109 | 							userId: user.user.id,
1110 | 							providerId: "credential",
1111 | 							accountId: user.user.id,
1112 | 							password: passwordHash,
1113 | 						});
1114 | 					} else {
1115 | 						await ctx.context.internalAdapter.updatePassword(
1116 | 							user.user.id,
1117 | 							passwordHash,
1118 | 						);
1119 | 					}
1120 | 
1121 | 					if (ctx.context.options.emailAndPassword?.onPasswordReset) {
1122 | 						await ctx.context.options.emailAndPassword.onPasswordReset(
1123 | 							{
1124 | 								user: user.user,
1125 | 							},
1126 | 							ctx.request,
1127 | 						);
1128 | 					}
1129 | 
1130 | 					if (!user.user.emailVerified) {
1131 | 						await ctx.context.internalAdapter.updateUser(user.user.id, {
1132 | 							emailVerified: true,
1133 | 						});
1134 | 					}
1135 | 
1136 | 					return ctx.json({
1137 | 						success: true,
1138 | 					});
1139 | 				},
1140 | 			),
1141 | 		},
1142 | 		hooks: {
1143 | 			after: [
1144 | 				{
1145 | 					matcher(context) {
1146 | 						return !!(
1147 | 							context.path?.startsWith("/sign-up") &&
1148 | 							opts.sendVerificationOnSignUp
1149 | 						);
1150 | 					},
1151 | 					handler: createAuthMiddleware(async (ctx) => {
1152 | 						const response = await getEndpointResponse<{
1153 | 							user: { email: string };
1154 | 						}>(ctx);
1155 | 						const email = response?.user.email;
1156 | 						if (email) {
1157 | 							const otp =
1158 | 								opts.generateOTP({ email, type: ctx.body.type }, ctx.request) ||
1159 | 								defaultOTPGenerator(opts);
1160 | 							let storedOTP = await storeOTP(ctx, otp);
1161 | 							await ctx.context.internalAdapter.createVerificationValue({
1162 | 								value: `${storedOTP}:0`,
1163 | 								identifier: `email-verification-otp-${email}`,
1164 | 								expiresAt: getDate(opts.expiresIn, "sec"),
1165 | 							});
1166 | 							await options.sendVerificationOTP(
1167 | 								{
1168 | 									email,
1169 | 									otp,
1170 | 									type: "email-verification",
1171 | 								},
1172 | 								ctx.request,
1173 | 							);
1174 | 						}
1175 | 					}),
1176 | 				},
1177 | 			],
1178 | 		},
1179 | 		$ERROR_CODES: ERROR_CODES,
1180 | 		rateLimit: [
1181 | 			{
1182 | 				pathMatcher(path) {
1183 | 					return path === "/email-otp/send-verification-otp";
1184 | 				},
1185 | 				window: 60,
1186 | 				max: 3,
1187 | 			},
1188 | 			{
1189 | 				pathMatcher(path) {
1190 | 					return path === "/email-otp/check-verification-otp";
1191 | 				},
1192 | 				window: 60,
1193 | 				max: 3,
1194 | 			},
1195 | 			{
1196 | 				pathMatcher(path) {
1197 | 					return path === "/email-otp/verify-email";
1198 | 				},
1199 | 				window: 60,
1200 | 				max: 3,
1201 | 			},
1202 | 			{
1203 | 				pathMatcher(path) {
1204 | 					return path === "/sign-in/email-otp";
1205 | 				},
1206 | 				window: 60,
1207 | 				max: 3,
1208 | 			},
1209 | 		],
1210 | 	} satisfies BetterAuthPlugin;
1211 | };
1212 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/organization/routes/crud-team.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import * as z from "zod";
   2 | import { createAuthEndpoint } from "@better-auth/core/api";
   3 | import { getOrgAdapter } from "../adapter";
   4 | import { orgMiddleware, orgSessionMiddleware } from "../call";
   5 | import { APIError } from "better-call";
   6 | import { getSessionFromCtx } from "../../../api";
   7 | import { ORGANIZATION_ERROR_CODES } from "../error-codes";
   8 | import type { OrganizationOptions } from "../types";
   9 | import { teamSchema } from "../schema";
  10 | import { hasPermission } from "../has-permission";
  11 | import { setSessionCookie } from "../../../cookies";
  12 | import {
  13 | 	toZodSchema,
  14 | 	type InferAdditionalFieldsFromPluginOptions,
  15 | } from "../../../db";
  16 | import type { PrettifyDeep } from "../../../types/helper";
  17 | 
  18 | export const createTeam = <O extends OrganizationOptions>(options: O) => {
  19 | 	const additionalFieldsSchema = toZodSchema({
  20 | 		fields: options?.schema?.team?.additionalFields ?? {},
  21 | 		isClientSide: true,
  22 | 	});
  23 | 	const baseSchema = z.object({
  24 | 		name: z.string().meta({
  25 | 			description: 'The name of the team. Eg: "my-team"',
  26 | 		}),
  27 | 		organizationId: z
  28 | 			.string()
  29 | 			.meta({
  30 | 				description:
  31 | 					'The organization ID which the team will be created in. Defaults to the active organization. Eg: "organization-id"',
  32 | 			})
  33 | 			.optional(),
  34 | 	});
  35 | 	return createAuthEndpoint(
  36 | 		"/organization/create-team",
  37 | 		{
  38 | 			method: "POST",
  39 | 			body: z.object({
  40 | 				...baseSchema.shape,
  41 | 				...additionalFieldsSchema.shape,
  42 | 			}),
  43 | 			use: [orgMiddleware],
  44 | 			metadata: {
  45 | 				$Infer: {
  46 | 					body: {} as z.infer<typeof baseSchema> &
  47 | 						InferAdditionalFieldsFromPluginOptions<"team", O>,
  48 | 				},
  49 | 				openapi: {
  50 | 					description: "Create a new team within an organization",
  51 | 					responses: {
  52 | 						"200": {
  53 | 							description: "Team created successfully",
  54 | 							content: {
  55 | 								"application/json": {
  56 | 									schema: {
  57 | 										type: "object",
  58 | 										properties: {
  59 | 											id: {
  60 | 												type: "string",
  61 | 												description: "Unique identifier of the created team",
  62 | 											},
  63 | 											name: {
  64 | 												type: "string",
  65 | 												description: "Name of the team",
  66 | 											},
  67 | 											organizationId: {
  68 | 												type: "string",
  69 | 												description:
  70 | 													"ID of the organization the team belongs to",
  71 | 											},
  72 | 											createdAt: {
  73 | 												type: "string",
  74 | 												format: "date-time",
  75 | 												description: "Timestamp when the team was created",
  76 | 											},
  77 | 											updatedAt: {
  78 | 												type: "string",
  79 | 												format: "date-time",
  80 | 												description: "Timestamp when the team was last updated",
  81 | 											},
  82 | 										},
  83 | 										required: [
  84 | 											"id",
  85 | 											"name",
  86 | 											"organizationId",
  87 | 											"createdAt",
  88 | 											"updatedAt",
  89 | 										],
  90 | 									},
  91 | 								},
  92 | 							},
  93 | 						},
  94 | 					},
  95 | 				},
  96 | 			},
  97 | 		},
  98 | 		async (ctx) => {
  99 | 			const session = await getSessionFromCtx(ctx);
 100 | 			const organizationId =
 101 | 				ctx.body.organizationId || session?.session.activeOrganizationId;
 102 | 			if (!session && (ctx.request || ctx.headers)) {
 103 | 				throw new APIError("UNAUTHORIZED");
 104 | 			}
 105 | 
 106 | 			if (!organizationId) {
 107 | 				throw new APIError("BAD_REQUEST", {
 108 | 					message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
 109 | 				});
 110 | 			}
 111 | 			const adapter = getOrgAdapter<O>(ctx.context, options as O);
 112 | 			if (session) {
 113 | 				const member = await adapter.findMemberByOrgId({
 114 | 					userId: session.user.id,
 115 | 					organizationId,
 116 | 				});
 117 | 				if (!member) {
 118 | 					throw new APIError("FORBIDDEN", {
 119 | 						message:
 120 | 							ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION,
 121 | 					});
 122 | 				}
 123 | 				const canCreate = await hasPermission(
 124 | 					{
 125 | 						role: member.role,
 126 | 						options: ctx.context.orgOptions,
 127 | 						permissions: {
 128 | 							team: ["create"],
 129 | 						},
 130 | 						organizationId,
 131 | 					},
 132 | 					ctx,
 133 | 				);
 134 | 
 135 | 				if (!canCreate) {
 136 | 					throw new APIError("FORBIDDEN", {
 137 | 						message:
 138 | 							ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION,
 139 | 					});
 140 | 				}
 141 | 			}
 142 | 
 143 | 			const existingTeams = await adapter.listTeams(organizationId);
 144 | 			const maximum =
 145 | 				typeof ctx.context.orgOptions.teams?.maximumTeams === "function"
 146 | 					? await ctx.context.orgOptions.teams?.maximumTeams(
 147 | 							{
 148 | 								organizationId,
 149 | 								session,
 150 | 							},
 151 | 							ctx.request,
 152 | 						)
 153 | 					: ctx.context.orgOptions.teams?.maximumTeams;
 154 | 
 155 | 			const maxTeamsReached = maximum ? existingTeams.length >= maximum : false;
 156 | 			if (maxTeamsReached) {
 157 | 				throw new APIError("BAD_REQUEST", {
 158 | 					message:
 159 | 						ORGANIZATION_ERROR_CODES.YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS,
 160 | 				});
 161 | 			}
 162 | 			const { name, organizationId: _, ...additionalFields } = ctx.body;
 163 | 
 164 | 			const organization = await adapter.findOrganizationById(organizationId);
 165 | 			if (!organization) {
 166 | 				throw new APIError("BAD_REQUEST", {
 167 | 					message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
 168 | 				});
 169 | 			}
 170 | 
 171 | 			let teamData = {
 172 | 				name,
 173 | 				organizationId,
 174 | 				createdAt: new Date(),
 175 | 				updatedAt: new Date(),
 176 | 				...additionalFields,
 177 | 			};
 178 | 
 179 | 			// Run beforeCreateTeam hook
 180 | 			if (options?.organizationHooks?.beforeCreateTeam) {
 181 | 				const response = await options?.organizationHooks.beforeCreateTeam({
 182 | 					team: {
 183 | 						name,
 184 | 						organizationId,
 185 | 						...additionalFields,
 186 | 					},
 187 | 					user: session?.user,
 188 | 					organization,
 189 | 				});
 190 | 				if (response && typeof response === "object" && "data" in response) {
 191 | 					teamData = {
 192 | 						...teamData,
 193 | 						...response.data,
 194 | 					};
 195 | 				}
 196 | 			}
 197 | 
 198 | 			const createdTeam = await adapter.createTeam(teamData);
 199 | 
 200 | 			// Run afterCreateTeam hook
 201 | 			if (options?.organizationHooks?.afterCreateTeam) {
 202 | 				await options?.organizationHooks.afterCreateTeam({
 203 | 					team: createdTeam,
 204 | 					user: session?.user,
 205 | 					organization,
 206 | 				});
 207 | 			}
 208 | 
 209 | 			return ctx.json(createdTeam);
 210 | 		},
 211 | 	);
 212 | };
 213 | 
 214 | export const removeTeam = <O extends OrganizationOptions>(options: O) =>
 215 | 	createAuthEndpoint(
 216 | 		"/organization/remove-team",
 217 | 		{
 218 | 			method: "POST",
 219 | 			body: z.object({
 220 | 				teamId: z.string().meta({
 221 | 					description: `The team ID of the team to remove. Eg: "team-id"`,
 222 | 				}),
 223 | 				organizationId: z
 224 | 					.string()
 225 | 					.meta({
 226 | 						description: `The organization ID which the team falls under. If not provided, it will default to the user's active organization. Eg: "organization-id"`,
 227 | 					})
 228 | 					.optional(),
 229 | 			}),
 230 | 			use: [orgMiddleware],
 231 | 			metadata: {
 232 | 				openapi: {
 233 | 					description: "Remove a team from an organization",
 234 | 					responses: {
 235 | 						"200": {
 236 | 							description: "Team removed successfully",
 237 | 							content: {
 238 | 								"application/json": {
 239 | 									schema: {
 240 | 										type: "object",
 241 | 										properties: {
 242 | 											message: {
 243 | 												type: "string",
 244 | 												description:
 245 | 													"Confirmation message indicating successful removal",
 246 | 												enum: ["Team removed successfully."],
 247 | 											},
 248 | 										},
 249 | 										required: ["message"],
 250 | 									},
 251 | 								},
 252 | 							},
 253 | 						},
 254 | 					},
 255 | 				},
 256 | 			},
 257 | 		},
 258 | 		async (ctx) => {
 259 | 			const session = await getSessionFromCtx(ctx);
 260 | 			const organizationId =
 261 | 				ctx.body.organizationId || session?.session.activeOrganizationId;
 262 | 			if (!organizationId) {
 263 | 				return ctx.json(null, {
 264 | 					status: 400,
 265 | 					body: {
 266 | 						message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
 267 | 					},
 268 | 				});
 269 | 			}
 270 | 			if (!session && (ctx.request || ctx.headers)) {
 271 | 				throw new APIError("UNAUTHORIZED");
 272 | 			}
 273 | 			const adapter = getOrgAdapter<O>(ctx.context, options);
 274 | 			if (session) {
 275 | 				const member = await adapter.findMemberByOrgId({
 276 | 					userId: session.user.id,
 277 | 					organizationId,
 278 | 				});
 279 | 
 280 | 				if (!member || session.session?.activeTeamId === ctx.body.teamId) {
 281 | 					throw new APIError("FORBIDDEN", {
 282 | 						message:
 283 | 							ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM,
 284 | 					});
 285 | 				}
 286 | 
 287 | 				const canRemove = await hasPermission(
 288 | 					{
 289 | 						role: member.role,
 290 | 						options: ctx.context.orgOptions,
 291 | 						permissions: {
 292 | 							team: ["delete"],
 293 | 						},
 294 | 						organizationId,
 295 | 					},
 296 | 					ctx,
 297 | 				);
 298 | 
 299 | 				if (!canRemove) {
 300 | 					throw new APIError("FORBIDDEN", {
 301 | 						message:
 302 | 							ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_TEAMS_IN_THIS_ORGANIZATION,
 303 | 					});
 304 | 				}
 305 | 			}
 306 | 			const team = await adapter.findTeamById({
 307 | 				teamId: ctx.body.teamId,
 308 | 				organizationId,
 309 | 			});
 310 | 			if (!team || team.organizationId !== organizationId) {
 311 | 				throw new APIError("BAD_REQUEST", {
 312 | 					message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
 313 | 				});
 314 | 			}
 315 | 
 316 | 			if (!ctx.context.orgOptions.teams?.allowRemovingAllTeams) {
 317 | 				const teams = await adapter.listTeams(organizationId);
 318 | 				if (teams.length <= 1) {
 319 | 					throw new APIError("BAD_REQUEST", {
 320 | 						message: ORGANIZATION_ERROR_CODES.UNABLE_TO_REMOVE_LAST_TEAM,
 321 | 					});
 322 | 				}
 323 | 			}
 324 | 
 325 | 			const organization = await adapter.findOrganizationById(organizationId);
 326 | 			if (!organization) {
 327 | 				throw new APIError("BAD_REQUEST", {
 328 | 					message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
 329 | 				});
 330 | 			}
 331 | 
 332 | 			// Run beforeDeleteTeam hook
 333 | 			if (options?.organizationHooks?.beforeDeleteTeam) {
 334 | 				await options?.organizationHooks.beforeDeleteTeam({
 335 | 					team,
 336 | 					user: session?.user,
 337 | 					organization,
 338 | 				});
 339 | 			}
 340 | 
 341 | 			await adapter.deleteTeam(team.id);
 342 | 
 343 | 			// Run afterDeleteTeam hook
 344 | 			if (options?.organizationHooks?.afterDeleteTeam) {
 345 | 				await options?.organizationHooks.afterDeleteTeam({
 346 | 					team,
 347 | 					user: session?.user,
 348 | 					organization,
 349 | 				});
 350 | 			}
 351 | 
 352 | 			return ctx.json({ message: "Team removed successfully." });
 353 | 		},
 354 | 	);
 355 | 
 356 | export const updateTeam = <O extends OrganizationOptions>(options: O) => {
 357 | 	const additionalFieldsSchema = toZodSchema({
 358 | 		fields: options?.schema?.team?.additionalFields ?? {},
 359 | 		isClientSide: true,
 360 | 	});
 361 | 
 362 | 	type Body = {
 363 | 		teamId: string;
 364 | 		data: Partial<
 365 | 			PrettifyDeep<
 366 | 				Omit<z.infer<typeof teamSchema>, "id" | "createdAt" | "updatedAt">
 367 | 			> &
 368 | 				InferAdditionalFieldsFromPluginOptions<"team", O>
 369 | 		>;
 370 | 	};
 371 | 
 372 | 	return createAuthEndpoint(
 373 | 		"/organization/update-team",
 374 | 		{
 375 | 			method: "POST",
 376 | 			body: z.object({
 377 | 				teamId: z.string().meta({
 378 | 					description: `The ID of the team to be updated. Eg: "team-id"`,
 379 | 				}),
 380 | 				data: z
 381 | 					.object({
 382 | 						...teamSchema.shape,
 383 | 						...additionalFieldsSchema.shape,
 384 | 					})
 385 | 					.partial(),
 386 | 			}),
 387 | 			requireHeaders: true,
 388 | 			use: [orgMiddleware, orgSessionMiddleware],
 389 | 			metadata: {
 390 | 				$Infer: { body: {} as Body },
 391 | 				openapi: {
 392 | 					description: "Update an existing team in an organization",
 393 | 					responses: {
 394 | 						"200": {
 395 | 							description: "Team updated successfully",
 396 | 							content: {
 397 | 								"application/json": {
 398 | 									schema: {
 399 | 										type: "object",
 400 | 										properties: {
 401 | 											id: {
 402 | 												type: "string",
 403 | 												description: "Unique identifier of the updated team",
 404 | 											},
 405 | 											name: {
 406 | 												type: "string",
 407 | 												description: "Updated name of the team",
 408 | 											},
 409 | 											organizationId: {
 410 | 												type: "string",
 411 | 												description:
 412 | 													"ID of the organization the team belongs to",
 413 | 											},
 414 | 											createdAt: {
 415 | 												type: "string",
 416 | 												format: "date-time",
 417 | 												description: "Timestamp when the team was created",
 418 | 											},
 419 | 											updatedAt: {
 420 | 												type: "string",
 421 | 												format: "date-time",
 422 | 												description: "Timestamp when the team was last updated",
 423 | 											},
 424 | 										},
 425 | 										required: [
 426 | 											"id",
 427 | 											"name",
 428 | 											"organizationId",
 429 | 											"createdAt",
 430 | 											"updatedAt",
 431 | 										],
 432 | 									},
 433 | 								},
 434 | 							},
 435 | 						},
 436 | 					},
 437 | 				},
 438 | 			},
 439 | 		},
 440 | 		async (ctx) => {
 441 | 			const session = ctx.context.session;
 442 | 			const organizationId =
 443 | 				ctx.body.data.organizationId || session.session.activeOrganizationId;
 444 | 			if (!organizationId) {
 445 | 				return ctx.json(null, {
 446 | 					status: 400,
 447 | 					body: {
 448 | 						message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
 449 | 					},
 450 | 				});
 451 | 			}
 452 | 			const adapter = getOrgAdapter<O>(ctx.context, options);
 453 | 			const member = await adapter.findMemberByOrgId({
 454 | 				userId: session.user.id,
 455 | 				organizationId,
 456 | 			});
 457 | 
 458 | 			if (!member) {
 459 | 				throw new APIError("FORBIDDEN", {
 460 | 					message:
 461 | 						ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM,
 462 | 				});
 463 | 			}
 464 | 
 465 | 			const canUpdate = await hasPermission(
 466 | 				{
 467 | 					role: member.role,
 468 | 					options: ctx.context.orgOptions,
 469 | 					permissions: {
 470 | 						team: ["update"],
 471 | 					},
 472 | 					organizationId,
 473 | 				},
 474 | 				ctx,
 475 | 			);
 476 | 
 477 | 			if (!canUpdate) {
 478 | 				throw new APIError("FORBIDDEN", {
 479 | 					message:
 480 | 						ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM,
 481 | 				});
 482 | 			}
 483 | 
 484 | 			const team = await adapter.findTeamById({
 485 | 				teamId: ctx.body.teamId,
 486 | 				organizationId,
 487 | 			});
 488 | 
 489 | 			if (!team || team.organizationId !== organizationId) {
 490 | 				throw new APIError("BAD_REQUEST", {
 491 | 					message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
 492 | 				});
 493 | 			}
 494 | 
 495 | 			const { name, organizationId: __, ...additionalFields } = ctx.body.data;
 496 | 
 497 | 			const organization = await adapter.findOrganizationById(organizationId);
 498 | 			if (!organization) {
 499 | 				throw new APIError("BAD_REQUEST", {
 500 | 					message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
 501 | 				});
 502 | 			}
 503 | 
 504 | 			const updates = {
 505 | 				name,
 506 | 				...additionalFields,
 507 | 			};
 508 | 
 509 | 			// Run beforeUpdateTeam hook
 510 | 			if (options?.organizationHooks?.beforeUpdateTeam) {
 511 | 				const response = await options?.organizationHooks.beforeUpdateTeam({
 512 | 					team,
 513 | 					updates,
 514 | 					user: session.user,
 515 | 					organization,
 516 | 				});
 517 | 				if (response && typeof response === "object" && "data" in response) {
 518 | 					// Allow the hook to modify the updates
 519 | 					const modifiedUpdates = response.data;
 520 | 					const updatedTeam = await adapter.updateTeam(
 521 | 						team.id,
 522 | 						modifiedUpdates,
 523 | 					);
 524 | 
 525 | 					// Run afterUpdateTeam hook
 526 | 					if (options?.organizationHooks?.afterUpdateTeam) {
 527 | 						await options?.organizationHooks.afterUpdateTeam({
 528 | 							team: updatedTeam,
 529 | 							user: session.user,
 530 | 							organization,
 531 | 						});
 532 | 					}
 533 | 
 534 | 					return ctx.json(updatedTeam);
 535 | 				}
 536 | 			}
 537 | 
 538 | 			const updatedTeam = await adapter.updateTeam(team.id, updates);
 539 | 
 540 | 			// Run afterUpdateTeam hook
 541 | 			if (options?.organizationHooks?.afterUpdateTeam) {
 542 | 				await options?.organizationHooks.afterUpdateTeam({
 543 | 					team: updatedTeam,
 544 | 					user: session.user,
 545 | 					organization,
 546 | 				});
 547 | 			}
 548 | 
 549 | 			return ctx.json(updatedTeam);
 550 | 		},
 551 | 	);
 552 | };
 553 | 
 554 | export const listOrganizationTeams = <O extends OrganizationOptions>(
 555 | 	options: O,
 556 | ) =>
 557 | 	createAuthEndpoint(
 558 | 		"/organization/list-teams",
 559 | 		{
 560 | 			method: "GET",
 561 | 			query: z.optional(
 562 | 				z.object({
 563 | 					organizationId: z
 564 | 						.string()
 565 | 						.meta({
 566 | 							description: `The organization ID which the teams are under to list. Defaults to the users active organization. Eg: "organziation-id"`,
 567 | 						})
 568 | 						.optional(),
 569 | 				}),
 570 | 			),
 571 | 			requireHeaders: true,
 572 | 			metadata: {
 573 | 				openapi: {
 574 | 					description: "List all teams in an organization",
 575 | 					responses: {
 576 | 						"200": {
 577 | 							description: "Teams retrieved successfully",
 578 | 							content: {
 579 | 								"application/json": {
 580 | 									schema: {
 581 | 										type: "array",
 582 | 										items: {
 583 | 											type: "object",
 584 | 											properties: {
 585 | 												id: {
 586 | 													type: "string",
 587 | 													description: "Unique identifier of the team",
 588 | 												},
 589 | 												name: {
 590 | 													type: "string",
 591 | 													description: "Name of the team",
 592 | 												},
 593 | 												organizationId: {
 594 | 													type: "string",
 595 | 													description:
 596 | 														"ID of the organization the team belongs to",
 597 | 												},
 598 | 												createdAt: {
 599 | 													type: "string",
 600 | 													format: "date-time",
 601 | 													description: "Timestamp when the team was created",
 602 | 												},
 603 | 												updatedAt: {
 604 | 													type: "string",
 605 | 													format: "date-time",
 606 | 													description:
 607 | 														"Timestamp when the team was last updated",
 608 | 												},
 609 | 											},
 610 | 											required: [
 611 | 												"id",
 612 | 												"name",
 613 | 												"organizationId",
 614 | 												"createdAt",
 615 | 												"updatedAt",
 616 | 											],
 617 | 										},
 618 | 										description:
 619 | 											"Array of team objects within the organization",
 620 | 									},
 621 | 								},
 622 | 							},
 623 | 						},
 624 | 					},
 625 | 				},
 626 | 			},
 627 | 			use: [orgMiddleware, orgSessionMiddleware],
 628 | 		},
 629 | 		async (ctx) => {
 630 | 			const session = ctx.context.session;
 631 | 			const organizationId =
 632 | 				ctx.query?.organizationId || session?.session.activeOrganizationId;
 633 | 			if (!organizationId) {
 634 | 				throw ctx.error("BAD_REQUEST", {
 635 | 					message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
 636 | 				});
 637 | 			}
 638 | 			const adapter = getOrgAdapter<O>(ctx.context, options);
 639 | 			const member = await adapter.findMemberByOrgId({
 640 | 				userId: session.user.id,
 641 | 				organizationId: organizationId || "",
 642 | 			});
 643 | 			if (!member) {
 644 | 				throw new APIError("FORBIDDEN", {
 645 | 					message:
 646 | 						ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION,
 647 | 				});
 648 | 			}
 649 | 			const teams = await adapter.listTeams(organizationId);
 650 | 			return ctx.json(teams);
 651 | 		},
 652 | 	);
 653 | 
 654 | export const setActiveTeam = <O extends OrganizationOptions>(options: O) =>
 655 | 	createAuthEndpoint(
 656 | 		"/organization/set-active-team",
 657 | 		{
 658 | 			method: "POST",
 659 | 			body: z.object({
 660 | 				teamId: z
 661 | 					.string()
 662 | 					.meta({
 663 | 						description:
 664 | 							"The team id to set as active. It can be null to unset the active team",
 665 | 					})
 666 | 					.nullable()
 667 | 					.optional(),
 668 | 			}),
 669 | 			use: [orgSessionMiddleware, orgMiddleware],
 670 | 			metadata: {
 671 | 				openapi: {
 672 | 					description: "Set the active team",
 673 | 					responses: {
 674 | 						"200": {
 675 | 							description: "Success",
 676 | 							content: {
 677 | 								"application/json": {
 678 | 									schema: {
 679 | 										type: "object",
 680 | 										description: "The team",
 681 | 										$ref: "#/components/schemas/Team",
 682 | 									},
 683 | 								},
 684 | 							},
 685 | 						},
 686 | 					},
 687 | 				},
 688 | 			},
 689 | 		},
 690 | 		async (ctx) => {
 691 | 			const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
 692 | 			const session = ctx.context.session;
 693 | 
 694 | 			if (ctx.body.teamId === null) {
 695 | 				const sessionTeamId = session.session.activeTeamId;
 696 | 				if (!sessionTeamId) {
 697 | 					return ctx.json(null);
 698 | 				}
 699 | 
 700 | 				const updatedSession = await adapter.setActiveTeam(
 701 | 					session.session.token,
 702 | 					null,
 703 | 					ctx,
 704 | 				);
 705 | 
 706 | 				await setSessionCookie(ctx, {
 707 | 					session: updatedSession,
 708 | 					user: session.user,
 709 | 				});
 710 | 
 711 | 				return ctx.json(null);
 712 | 			}
 713 | 
 714 | 			let teamId: string;
 715 | 
 716 | 			if (!ctx.body.teamId) {
 717 | 				const sessionTeamId = session.session.activeTeamId;
 718 | 				if (!sessionTeamId) {
 719 | 					return ctx.json(null);
 720 | 				} else {
 721 | 					teamId = sessionTeamId;
 722 | 				}
 723 | 			} else {
 724 | 				teamId = ctx.body.teamId;
 725 | 			}
 726 | 
 727 | 			const team = await adapter.findTeamById({ teamId });
 728 | 
 729 | 			if (!team) {
 730 | 				throw new APIError("BAD_REQUEST", {
 731 | 					message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
 732 | 				});
 733 | 			}
 734 | 
 735 | 			const member = await adapter.findTeamMember({
 736 | 				teamId,
 737 | 				userId: session.user.id,
 738 | 			});
 739 | 
 740 | 			if (!member) {
 741 | 				throw new APIError("FORBIDDEN", {
 742 | 					message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM,
 743 | 				});
 744 | 			}
 745 | 
 746 | 			const updatedSession = await adapter.setActiveTeam(
 747 | 				session.session.token,
 748 | 				team.id,
 749 | 				ctx,
 750 | 			);
 751 | 
 752 | 			await setSessionCookie(ctx, {
 753 | 				session: updatedSession,
 754 | 				user: session.user,
 755 | 			});
 756 | 
 757 | 			return ctx.json(team);
 758 | 		},
 759 | 	);
 760 | 
 761 | export const listUserTeams = <O extends OrganizationOptions>(options: O) =>
 762 | 	createAuthEndpoint(
 763 | 		"/organization/list-user-teams",
 764 | 		{
 765 | 			method: "GET",
 766 | 			metadata: {
 767 | 				openapi: {
 768 | 					description: "List all teams that the current user is a part of.",
 769 | 					responses: {
 770 | 						"200": {
 771 | 							description: "Teams retrieved successfully",
 772 | 							content: {
 773 | 								"application/json": {
 774 | 									schema: {
 775 | 										type: "array",
 776 | 										items: {
 777 | 											type: "object",
 778 | 											description: "The team",
 779 | 											$ref: "#/components/schemas/Team",
 780 | 										},
 781 | 										description:
 782 | 											"Array of team objects within the organization",
 783 | 									},
 784 | 								},
 785 | 							},
 786 | 						},
 787 | 					},
 788 | 				},
 789 | 			},
 790 | 			use: [orgMiddleware, orgSessionMiddleware],
 791 | 		},
 792 | 		async (ctx) => {
 793 | 			const session = ctx.context.session;
 794 | 			const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
 795 | 			const teams = await adapter.listTeamsByUser({
 796 | 				userId: session.user.id,
 797 | 			});
 798 | 
 799 | 			return ctx.json(teams);
 800 | 		},
 801 | 	);
 802 | 
 803 | export const listTeamMembers = <O extends OrganizationOptions>(options: O) =>
 804 | 	createAuthEndpoint(
 805 | 		"/organization/list-team-members",
 806 | 		{
 807 | 			method: "GET",
 808 | 			query: z.optional(
 809 | 				z.object({
 810 | 					teamId: z.string().optional().meta({
 811 | 						description:
 812 | 							"The team whose members we should return. If this is not provided the members of the current active team get returned.",
 813 | 					}),
 814 | 				}),
 815 | 			),
 816 | 			metadata: {
 817 | 				openapi: {
 818 | 					description: "List the members of the given team.",
 819 | 					responses: {
 820 | 						"200": {
 821 | 							description: "Teams retrieved successfully",
 822 | 							content: {
 823 | 								"application/json": {
 824 | 									schema: {
 825 | 										type: "array",
 826 | 										items: {
 827 | 											type: "object",
 828 | 											description: "The team member",
 829 | 											properties: {
 830 | 												id: {
 831 | 													type: "string",
 832 | 													description: "Unique identifier of the team member",
 833 | 												},
 834 | 												userId: {
 835 | 													type: "string",
 836 | 													description: "The user ID of the team member",
 837 | 												},
 838 | 												teamId: {
 839 | 													type: "string",
 840 | 													description:
 841 | 														"The team ID of the team the team member is in",
 842 | 												},
 843 | 												createdAt: {
 844 | 													type: "string",
 845 | 													format: "date-time",
 846 | 													description:
 847 | 														"Timestamp when the team member was created",
 848 | 												},
 849 | 											},
 850 | 											required: ["id", "userId", "teamId", "createdAt"],
 851 | 										},
 852 | 										description: "Array of team member objects within the team",
 853 | 									},
 854 | 								},
 855 | 							},
 856 | 						},
 857 | 					},
 858 | 				},
 859 | 			},
 860 | 			use: [orgMiddleware, orgSessionMiddleware],
 861 | 		},
 862 | 		async (ctx) => {
 863 | 			const session = ctx.context.session;
 864 | 			const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
 865 | 			let teamId = ctx.query?.teamId || session?.session.activeTeamId;
 866 | 			if (!teamId) {
 867 | 				throw new APIError("BAD_REQUEST", {
 868 | 					message: ORGANIZATION_ERROR_CODES.YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM,
 869 | 				});
 870 | 			}
 871 | 			const member = await adapter.findTeamMember({
 872 | 				userId: session.user.id,
 873 | 				teamId,
 874 | 			});
 875 | 
 876 | 			if (!member) {
 877 | 				throw new APIError("BAD_REQUEST", {
 878 | 					message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM,
 879 | 				});
 880 | 			}
 881 | 			const members = await adapter.listTeamMembers({
 882 | 				teamId,
 883 | 			});
 884 | 			return ctx.json(members);
 885 | 		},
 886 | 	);
 887 | 
 888 | export const addTeamMember = <O extends OrganizationOptions>(options: O) =>
 889 | 	createAuthEndpoint(
 890 | 		"/organization/add-team-member",
 891 | 		{
 892 | 			method: "POST",
 893 | 			body: z.object({
 894 | 				teamId: z.string().meta({
 895 | 					description: "The team the user should be a member of.",
 896 | 				}),
 897 | 
 898 | 				userId: z.coerce.string().meta({
 899 | 					description:
 900 | 						"The user Id which represents the user to be added as a member.",
 901 | 				}),
 902 | 			}),
 903 | 			metadata: {
 904 | 				openapi: {
 905 | 					description: "The newly created member",
 906 | 					responses: {
 907 | 						"200": {
 908 | 							description: "Team member created successfully",
 909 | 							content: {
 910 | 								"application/json": {
 911 | 									schema: {
 912 | 										type: "object",
 913 | 										description: "The team member",
 914 | 										properties: {
 915 | 											id: {
 916 | 												type: "string",
 917 | 												description: "Unique identifier of the team member",
 918 | 											},
 919 | 											userId: {
 920 | 												type: "string",
 921 | 												description: "The user ID of the team member",
 922 | 											},
 923 | 											teamId: {
 924 | 												type: "string",
 925 | 												description:
 926 | 													"The team ID of the team the team member is in",
 927 | 											},
 928 | 											createdAt: {
 929 | 												type: "string",
 930 | 												format: "date-time",
 931 | 												description:
 932 | 													"Timestamp when the team member was created",
 933 | 											},
 934 | 										},
 935 | 										required: ["id", "userId", "teamId", "createdAt"],
 936 | 									},
 937 | 								},
 938 | 							},
 939 | 						},
 940 | 					},
 941 | 				},
 942 | 			},
 943 | 			use: [orgMiddleware, orgSessionMiddleware],
 944 | 		},
 945 | 		async (ctx) => {
 946 | 			const session = ctx.context.session;
 947 | 			const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
 948 | 
 949 | 			if (!session.session.activeOrganizationId) {
 950 | 				throw new APIError("BAD_REQUEST", {
 951 | 					message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
 952 | 				});
 953 | 			}
 954 | 
 955 | 			const currentMember = await adapter.findMemberByOrgId({
 956 | 				userId: session.user.id,
 957 | 				organizationId: session.session.activeOrganizationId,
 958 | 			});
 959 | 
 960 | 			if (!currentMember) {
 961 | 				throw new APIError("BAD_REQUEST", {
 962 | 					message:
 963 | 						ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
 964 | 				});
 965 | 			}
 966 | 
 967 | 			const canUpdateMember = await hasPermission(
 968 | 				{
 969 | 					role: currentMember.role,
 970 | 					options: ctx.context.orgOptions,
 971 | 					permissions: {
 972 | 						member: ["update"],
 973 | 					},
 974 | 					organizationId: session.session.activeOrganizationId,
 975 | 				},
 976 | 				ctx,
 977 | 			);
 978 | 
 979 | 			if (!canUpdateMember) {
 980 | 				throw new APIError("FORBIDDEN", {
 981 | 					message:
 982 | 						ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER,
 983 | 				});
 984 | 			}
 985 | 
 986 | 			const toBeAddedMember = await adapter.findMemberByOrgId({
 987 | 				userId: ctx.body.userId,
 988 | 				organizationId: session.session.activeOrganizationId,
 989 | 			});
 990 | 
 991 | 			if (!toBeAddedMember) {
 992 | 				throw new APIError("BAD_REQUEST", {
 993 | 					message:
 994 | 						ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
 995 | 				});
 996 | 			}
 997 | 
 998 | 			const team = await adapter.findTeamById({
 999 | 				teamId: ctx.body.teamId,
1000 | 				organizationId: session.session.activeOrganizationId,
1001 | 			});
1002 | 
1003 | 			if (!team) {
1004 | 				throw new APIError("BAD_REQUEST", {
1005 | 					message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
1006 | 				});
1007 | 			}
1008 | 
1009 | 			const organization = await adapter.findOrganizationById(
1010 | 				session.session.activeOrganizationId,
1011 | 			);
1012 | 			if (!organization) {
1013 | 				throw new APIError("BAD_REQUEST", {
1014 | 					message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
1015 | 				});
1016 | 			}
1017 | 
1018 | 			const userBeingAdded = await ctx.context.internalAdapter.findUserById(
1019 | 				ctx.body.userId,
1020 | 			);
1021 | 			if (!userBeingAdded) {
1022 | 				throw new APIError("BAD_REQUEST", {
1023 | 					message: "User not found",
1024 | 				});
1025 | 			}
1026 | 
1027 | 			// Run beforeAddTeamMember hook
1028 | 			if (options?.organizationHooks?.beforeAddTeamMember) {
1029 | 				const response = await options?.organizationHooks.beforeAddTeamMember({
1030 | 					teamMember: {
1031 | 						teamId: ctx.body.teamId,
1032 | 						userId: ctx.body.userId,
1033 | 					},
1034 | 					team,
1035 | 					user: userBeingAdded,
1036 | 					organization,
1037 | 				});
1038 | 				if (response && typeof response === "object" && "data" in response) {
1039 | 					// Allow the hook to modify the data
1040 | 				}
1041 | 			}
1042 | 
1043 | 			const teamMember = await adapter.findOrCreateTeamMember({
1044 | 				teamId: ctx.body.teamId,
1045 | 				userId: ctx.body.userId,
1046 | 			});
1047 | 
1048 | 			// Run afterAddTeamMember hook
1049 | 			if (options?.organizationHooks?.afterAddTeamMember) {
1050 | 				await options?.organizationHooks.afterAddTeamMember({
1051 | 					teamMember,
1052 | 					team,
1053 | 					user: userBeingAdded,
1054 | 					organization,
1055 | 				});
1056 | 			}
1057 | 
1058 | 			return ctx.json(teamMember);
1059 | 		},
1060 | 	);
1061 | 
1062 | export const removeTeamMember = <O extends OrganizationOptions>(options: O) =>
1063 | 	createAuthEndpoint(
1064 | 		"/organization/remove-team-member",
1065 | 		{
1066 | 			method: "POST",
1067 | 			body: z.object({
1068 | 				teamId: z.string().meta({
1069 | 					description: "The team the user should be removed from.",
1070 | 				}),
1071 | 
1072 | 				userId: z.coerce.string().meta({
1073 | 					description: "The user which should be removed from the team.",
1074 | 				}),
1075 | 			}),
1076 | 			metadata: {
1077 | 				openapi: {
1078 | 					description: "Remove a member from a team",
1079 | 					responses: {
1080 | 						"200": {
1081 | 							description: "Team member removed successfully",
1082 | 							content: {
1083 | 								"application/json": {
1084 | 									schema: {
1085 | 										type: "object",
1086 | 										properties: {
1087 | 											message: {
1088 | 												type: "string",
1089 | 												description:
1090 | 													"Confirmation message indicating successful removal",
1091 | 												enum: ["Team member removed successfully."],
1092 | 											},
1093 | 										},
1094 | 										required: ["message"],
1095 | 									},
1096 | 								},
1097 | 							},
1098 | 						},
1099 | 					},
1100 | 				},
1101 | 			},
1102 | 			use: [orgMiddleware, orgSessionMiddleware],
1103 | 		},
1104 | 		async (ctx) => {
1105 | 			const session = ctx.context.session;
1106 | 			const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
1107 | 
1108 | 			if (!session.session.activeOrganizationId) {
1109 | 				throw new APIError("BAD_REQUEST", {
1110 | 					message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
1111 | 				});
1112 | 			}
1113 | 
1114 | 			const currentMember = await adapter.findMemberByOrgId({
1115 | 				userId: session.user.id,
1116 | 				organizationId: session.session.activeOrganizationId,
1117 | 			});
1118 | 
1119 | 			if (!currentMember) {
1120 | 				throw new APIError("BAD_REQUEST", {
1121 | 					message:
1122 | 						ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
1123 | 				});
1124 | 			}
1125 | 
1126 | 			const canDeleteMember = await hasPermission(
1127 | 				{
1128 | 					role: currentMember.role,
1129 | 					options: ctx.context.orgOptions,
1130 | 					permissions: {
1131 | 						member: ["delete"],
1132 | 					},
1133 | 					organizationId: session.session.activeOrganizationId,
1134 | 				},
1135 | 				ctx,
1136 | 			);
1137 | 
1138 | 			if (!canDeleteMember) {
1139 | 				throw new APIError("FORBIDDEN", {
1140 | 					message:
1141 | 						ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER,
1142 | 				});
1143 | 			}
1144 | 
1145 | 			const toBeAddedMember = await adapter.findMemberByOrgId({
1146 | 				userId: ctx.body.userId,
1147 | 				organizationId: session.session.activeOrganizationId,
1148 | 			});
1149 | 
1150 | 			if (!toBeAddedMember) {
1151 | 				throw new APIError("BAD_REQUEST", {
1152 | 					message:
1153 | 						ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
1154 | 				});
1155 | 			}
1156 | 
1157 | 			const team = await adapter.findTeamById({
1158 | 				teamId: ctx.body.teamId,
1159 | 				organizationId: session.session.activeOrganizationId,
1160 | 			});
1161 | 
1162 | 			if (!team) {
1163 | 				throw new APIError("BAD_REQUEST", {
1164 | 					message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
1165 | 				});
1166 | 			}
1167 | 
1168 | 			const organization = await adapter.findOrganizationById(
1169 | 				session.session.activeOrganizationId,
1170 | 			);
1171 | 			if (!organization) {
1172 | 				throw new APIError("BAD_REQUEST", {
1173 | 					message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
1174 | 				});
1175 | 			}
1176 | 
1177 | 			const userBeingRemoved = await ctx.context.internalAdapter.findUserById(
1178 | 				ctx.body.userId,
1179 | 			);
1180 | 			if (!userBeingRemoved) {
1181 | 				throw new APIError("BAD_REQUEST", {
1182 | 					message: "User not found",
1183 | 				});
1184 | 			}
1185 | 
1186 | 			const teamMember = await adapter.findTeamMember({
1187 | 				teamId: ctx.body.teamId,
1188 | 				userId: ctx.body.userId,
1189 | 			});
1190 | 
1191 | 			if (!teamMember) {
1192 | 				throw new APIError("BAD_REQUEST", {
1193 | 					message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM,
1194 | 				});
1195 | 			}
1196 | 
1197 | 			// Run beforeRemoveTeamMember hook
1198 | 			if (options?.organizationHooks?.beforeRemoveTeamMember) {
1199 | 				await options?.organizationHooks.beforeRemoveTeamMember({
1200 | 					teamMember,
1201 | 					team,
1202 | 					user: userBeingRemoved,
1203 | 					organization,
1204 | 				});
1205 | 			}
1206 | 
1207 | 			await adapter.removeTeamMember({
1208 | 				teamId: ctx.body.teamId,
1209 | 				userId: ctx.body.userId,
1210 | 			});
1211 | 
1212 | 			// Run afterRemoveTeamMember hook
1213 | 			if (options?.organizationHooks?.afterRemoveTeamMember) {
1214 | 				await options?.organizationHooks.afterRemoveTeamMember({
1215 | 					teamMember,
1216 | 					team,
1217 | 					user: userBeingRemoved,
1218 | 					organization,
1219 | 				});
1220 | 			}
1221 | 
1222 | 			return ctx.json({ message: "Team member removed successfully." });
1223 | 		},
1224 | 	);
1225 | 
```
Page 54/67FirstPrevNextLast