#
tokens: 45302/50000 4/1119 files (page 34/52)
lines: off (toggle) GitHub
raw markdown copy
This is page 34 of 52. Use http://codebase.md/better-auth/better-auth?lines=false&page={x} to view the full context.

# Directory Structure

```
├── .gitattributes
├── .github
│   ├── CODEOWNERS
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   ├── renovate.json5
│   └── workflows
│       ├── ci.yml
│       ├── e2e.yml
│       ├── preview.yml
│       └── release.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .vscode
│   └── settings.json
├── banner-dark.png
├── banner.png
├── biome.json
├── bump.config.ts
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── demo
│   ├── expo-example
│   │   ├── .env.example
│   │   ├── .gitignore
│   │   ├── app.config.ts
│   │   ├── assets
│   │   │   ├── bg-image.jpeg
│   │   │   ├── fonts
│   │   │   │   └── SpaceMono-Regular.ttf
│   │   │   ├── icon.png
│   │   │   └── images
│   │   │       ├── adaptive-icon.png
│   │   │       ├── favicon.png
│   │   │       ├── logo.png
│   │   │       ├── partial-react-logo.png
│   │   │       ├── react-logo.png
│   │   │       ├── [email protected]
│   │   │       ├── [email protected]
│   │   │       └── splash.png
│   │   ├── babel.config.js
│   │   ├── components.json
│   │   ├── expo-env.d.ts
│   │   ├── index.ts
│   │   ├── metro.config.js
│   │   ├── nativewind-env.d.ts
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── app
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── api
│   │   │   │   │   └── auth
│   │   │   │   │       └── [...route]+api.ts
│   │   │   │   ├── dashboard.tsx
│   │   │   │   ├── forget-password.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   └── sign-up.tsx
│   │   │   ├── components
│   │   │   │   ├── icons
│   │   │   │   │   └── google.tsx
│   │   │   │   └── ui
│   │   │   │       ├── avatar.tsx
│   │   │   │       ├── button.tsx
│   │   │   │       ├── card.tsx
│   │   │   │       ├── dialog.tsx
│   │   │   │       ├── input.tsx
│   │   │   │       ├── separator.tsx
│   │   │   │       └── text.tsx
│   │   │   ├── global.css
│   │   │   └── lib
│   │   │       ├── auth-client.ts
│   │   │       ├── auth.ts
│   │   │       ├── icons
│   │   │       │   ├── iconWithClassName.ts
│   │   │       │   └── X.tsx
│   │   │       └── utils.ts
│   │   ├── tailwind.config.js
│   │   └── tsconfig.json
│   ├── nextjs
│   │   ├── .env.example
│   │   ├── .gitignore
│   │   ├── app
│   │   │   ├── (auth)
│   │   │   │   ├── forget-password
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── reset-password
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── sign-in
│   │   │   │   │   ├── loading.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   └── two-factor
│   │   │   │       ├── otp
│   │   │   │       │   └── page.tsx
│   │   │   │       └── page.tsx
│   │   │   ├── accept-invitation
│   │   │   │   └── [id]
│   │   │   │       ├── invitation-error.tsx
│   │   │   │       └── page.tsx
│   │   │   ├── admin
│   │   │   │   └── page.tsx
│   │   │   ├── api
│   │   │   │   └── auth
│   │   │   │       └── [...all]
│   │   │   │           └── route.ts
│   │   │   ├── apps
│   │   │   │   └── register
│   │   │   │       └── page.tsx
│   │   │   ├── client-test
│   │   │   │   └── page.tsx
│   │   │   ├── dashboard
│   │   │   │   ├── change-plan.tsx
│   │   │   │   ├── client.tsx
│   │   │   │   ├── organization-card.tsx
│   │   │   │   ├── page.tsx
│   │   │   │   ├── upgrade-button.tsx
│   │   │   │   └── user-card.tsx
│   │   │   ├── device
│   │   │   │   ├── approve
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── denied
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── layout.tsx
│   │   │   │   ├── page.tsx
│   │   │   │   └── success
│   │   │   │       └── page.tsx
│   │   │   ├── favicon.ico
│   │   │   ├── features.tsx
│   │   │   ├── fonts
│   │   │   │   ├── GeistMonoVF.woff
│   │   │   │   └── GeistVF.woff
│   │   │   ├── globals.css
│   │   │   ├── layout.tsx
│   │   │   ├── oauth
│   │   │   │   └── authorize
│   │   │   │       ├── concet-buttons.tsx
│   │   │   │       └── page.tsx
│   │   │   ├── page.tsx
│   │   │   └── pricing
│   │   │       └── page.tsx
│   │   ├── components
│   │   │   ├── account-switch.tsx
│   │   │   ├── blocks
│   │   │   │   └── pricing.tsx
│   │   │   ├── logo.tsx
│   │   │   ├── one-tap.tsx
│   │   │   ├── sign-in-btn.tsx
│   │   │   ├── sign-in.tsx
│   │   │   ├── sign-up.tsx
│   │   │   ├── theme-provider.tsx
│   │   │   ├── theme-toggle.tsx
│   │   │   ├── tier-labels.tsx
│   │   │   ├── ui
│   │   │   │   ├── accordion.tsx
│   │   │   │   ├── alert-dialog.tsx
│   │   │   │   ├── alert.tsx
│   │   │   │   ├── aspect-ratio.tsx
│   │   │   │   ├── avatar.tsx
│   │   │   │   ├── badge.tsx
│   │   │   │   ├── breadcrumb.tsx
│   │   │   │   ├── button.tsx
│   │   │   │   ├── calendar.tsx
│   │   │   │   ├── card.tsx
│   │   │   │   ├── carousel.tsx
│   │   │   │   ├── chart.tsx
│   │   │   │   ├── checkbox.tsx
│   │   │   │   ├── collapsible.tsx
│   │   │   │   ├── command.tsx
│   │   │   │   ├── context-menu.tsx
│   │   │   │   ├── copy-button.tsx
│   │   │   │   ├── dialog.tsx
│   │   │   │   ├── drawer.tsx
│   │   │   │   ├── dropdown-menu.tsx
│   │   │   │   ├── form.tsx
│   │   │   │   ├── hover-card.tsx
│   │   │   │   ├── input-otp.tsx
│   │   │   │   ├── input.tsx
│   │   │   │   ├── label.tsx
│   │   │   │   ├── menubar.tsx
│   │   │   │   ├── navigation-menu.tsx
│   │   │   │   ├── pagination.tsx
│   │   │   │   ├── password-input.tsx
│   │   │   │   ├── popover.tsx
│   │   │   │   ├── progress.tsx
│   │   │   │   ├── radio-group.tsx
│   │   │   │   ├── resizable.tsx
│   │   │   │   ├── scroll-area.tsx
│   │   │   │   ├── select.tsx
│   │   │   │   ├── separator.tsx
│   │   │   │   ├── sheet.tsx
│   │   │   │   ├── skeleton.tsx
│   │   │   │   ├── slider.tsx
│   │   │   │   ├── sonner.tsx
│   │   │   │   ├── switch.tsx
│   │   │   │   ├── table.tsx
│   │   │   │   ├── tabs.tsx
│   │   │   │   ├── tabs2.tsx
│   │   │   │   ├── textarea.tsx
│   │   │   │   ├── toast.tsx
│   │   │   │   ├── toaster.tsx
│   │   │   │   ├── toggle-group.tsx
│   │   │   │   ├── toggle.tsx
│   │   │   │   └── tooltip.tsx
│   │   │   └── wrapper.tsx
│   │   ├── components.json
│   │   ├── hooks
│   │   │   └── use-toast.ts
│   │   ├── lib
│   │   │   ├── auth-client.ts
│   │   │   ├── auth-types.ts
│   │   │   ├── auth.ts
│   │   │   ├── email
│   │   │   │   ├── invitation.tsx
│   │   │   │   ├── resend.ts
│   │   │   │   └── reset-password.tsx
│   │   │   ├── metadata.ts
│   │   │   ├── shared.ts
│   │   │   └── utils.ts
│   │   ├── next.config.ts
│   │   ├── package.json
│   │   ├── postcss.config.mjs
│   │   ├── proxy.ts
│   │   ├── public
│   │   │   ├── __og.png
│   │   │   ├── _og.png
│   │   │   ├── favicon
│   │   │   │   ├── android-chrome-192x192.png
│   │   │   │   ├── android-chrome-512x512.png
│   │   │   │   ├── apple-touch-icon.png
│   │   │   │   ├── favicon-16x16.png
│   │   │   │   ├── favicon-32x32.png
│   │   │   │   ├── favicon.ico
│   │   │   │   ├── light
│   │   │   │   │   ├── android-chrome-192x192.png
│   │   │   │   │   ├── android-chrome-512x512.png
│   │   │   │   │   ├── apple-touch-icon.png
│   │   │   │   │   ├── favicon-16x16.png
│   │   │   │   │   ├── favicon-32x32.png
│   │   │   │   │   ├── favicon.ico
│   │   │   │   │   └── site.webmanifest
│   │   │   │   └── site.webmanifest
│   │   │   ├── logo.svg
│   │   │   └── og.png
│   │   ├── README.md
│   │   ├── tailwind.config.ts
│   │   ├── tsconfig.json
│   │   └── turbo.json
│   └── stateless
│       ├── .env.example
│       ├── .gitignore
│       ├── next.config.ts
│       ├── package.json
│       ├── postcss.config.mjs
│       ├── src
│       │   ├── app
│       │   │   ├── api
│       │   │   │   ├── auth
│       │   │   │   │   └── [...all]
│       │   │   │   │       └── route.ts
│       │   │   │   └── user
│       │   │   │       └── route.ts
│       │   │   ├── dashboard
│       │   │   │   └── page.tsx
│       │   │   ├── globals.css
│       │   │   ├── layout.tsx
│       │   │   └── page.tsx
│       │   └── lib
│       │       ├── auth-client.ts
│       │       └── auth.ts
│       ├── tailwind.config.ts
│       └── tsconfig.json
├── docker-compose.yml
├── docs
│   ├── .env.example
│   ├── .gitignore
│   ├── app
│   │   ├── api
│   │   │   ├── ai-chat
│   │   │   │   └── route.ts
│   │   │   ├── analytics
│   │   │   │   ├── conversation
│   │   │   │   │   └── route.ts
│   │   │   │   ├── event
│   │   │   │   │   └── route.ts
│   │   │   │   └── feedback
│   │   │   │       └── route.ts
│   │   │   ├── chat
│   │   │   │   └── route.ts
│   │   │   ├── og
│   │   │   │   └── route.tsx
│   │   │   ├── og-release
│   │   │   │   └── route.tsx
│   │   │   ├── search
│   │   │   │   └── route.ts
│   │   │   └── support
│   │   │       └── route.ts
│   │   ├── blog
│   │   │   ├── _components
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── blog-list.tsx
│   │   │   │   ├── changelog-layout.tsx
│   │   │   │   ├── default-changelog.tsx
│   │   │   │   ├── fmt-dates.tsx
│   │   │   │   ├── icons.tsx
│   │   │   │   ├── stat-field.tsx
│   │   │   │   └── support.tsx
│   │   │   ├── [[...slug]]
│   │   │   │   └── page.tsx
│   │   │   └── layout.tsx
│   │   ├── changelogs
│   │   │   ├── _components
│   │   │   │   ├── _layout.tsx
│   │   │   │   ├── changelog-layout.tsx
│   │   │   │   ├── default-changelog.tsx
│   │   │   │   ├── fmt-dates.tsx
│   │   │   │   ├── grid-pattern.tsx
│   │   │   │   ├── icons.tsx
│   │   │   │   └── stat-field.tsx
│   │   │   ├── [[...slug]]
│   │   │   │   └── page.tsx
│   │   │   └── layout.tsx
│   │   ├── community
│   │   │   ├── _components
│   │   │   │   ├── header.tsx
│   │   │   │   └── stats.tsx
│   │   │   └── page.tsx
│   │   ├── docs
│   │   │   ├── [[...slug]]
│   │   │   │   ├── page.client.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── layout.tsx
│   │   │   └── lib
│   │   │       └── get-llm-text.ts
│   │   ├── global.css
│   │   ├── layout.config.tsx
│   │   ├── layout.tsx
│   │   ├── llms.txt
│   │   │   ├── [...slug]
│   │   │   │   └── route.ts
│   │   │   └── route.ts
│   │   ├── not-found.tsx
│   │   ├── page.tsx
│   │   ├── reference
│   │   │   └── route.ts
│   │   ├── sitemap.xml
│   │   ├── static.json
│   │   │   └── route.ts
│   │   └── v1
│   │       ├── _components
│   │       │   └── v1-text.tsx
│   │       ├── bg-line.tsx
│   │       └── page.tsx
│   ├── assets
│   │   ├── Geist.ttf
│   │   └── GeistMono.ttf
│   ├── components
│   │   ├── ai-chat-modal.tsx
│   │   ├── anchor-scroll-fix.tsx
│   │   ├── api-method-tabs.tsx
│   │   ├── api-method.tsx
│   │   ├── banner.tsx
│   │   ├── blocks
│   │   │   └── features.tsx
│   │   ├── builder
│   │   │   ├── beam.tsx
│   │   │   ├── code-tabs
│   │   │   │   ├── code-editor.tsx
│   │   │   │   ├── code-tabs.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   ├── tab-bar.tsx
│   │   │   │   └── theme.ts
│   │   │   ├── index.tsx
│   │   │   ├── sign-in.tsx
│   │   │   ├── sign-up.tsx
│   │   │   ├── social-provider.tsx
│   │   │   ├── store.ts
│   │   │   └── tabs.tsx
│   │   ├── display-techstack.tsx
│   │   ├── divider-text.tsx
│   │   ├── docs
│   │   │   ├── docs.client.tsx
│   │   │   ├── docs.tsx
│   │   │   ├── layout
│   │   │   │   ├── nav.tsx
│   │   │   │   ├── theme-toggle.tsx
│   │   │   │   ├── toc-thumb.tsx
│   │   │   │   └── toc.tsx
│   │   │   ├── page.client.tsx
│   │   │   ├── page.tsx
│   │   │   ├── shared.tsx
│   │   │   └── ui
│   │   │       ├── button.tsx
│   │   │       ├── collapsible.tsx
│   │   │       ├── popover.tsx
│   │   │       └── scroll-area.tsx
│   │   ├── endpoint.tsx
│   │   ├── features.tsx
│   │   ├── floating-ai-search.tsx
│   │   ├── fork-button.tsx
│   │   ├── generate-apple-jwt.tsx
│   │   ├── generate-secret.tsx
│   │   ├── github-stat.tsx
│   │   ├── icons.tsx
│   │   ├── landing
│   │   │   ├── gradient-bg.tsx
│   │   │   ├── grid-pattern.tsx
│   │   │   ├── hero.tsx
│   │   │   ├── section-svg.tsx
│   │   │   ├── section.tsx
│   │   │   ├── spotlight.tsx
│   │   │   └── testimonials.tsx
│   │   ├── logo-context-menu.tsx
│   │   ├── logo.tsx
│   │   ├── markdown-renderer.tsx
│   │   ├── markdown.tsx
│   │   ├── mdx
│   │   │   ├── add-to-cursor.tsx
│   │   │   └── database-tables.tsx
│   │   ├── message-feedback.tsx
│   │   ├── mobile-search-icon.tsx
│   │   ├── nav-bar.tsx
│   │   ├── nav-link.tsx
│   │   ├── nav-mobile.tsx
│   │   ├── promo-card.tsx
│   │   ├── resource-card.tsx
│   │   ├── resource-grid.tsx
│   │   ├── resource-section.tsx
│   │   ├── ripple.tsx
│   │   ├── search-dialog.tsx
│   │   ├── side-bar.tsx
│   │   ├── sidebar-content.tsx
│   │   ├── techstack-icons.tsx
│   │   ├── theme-provider.tsx
│   │   ├── theme-toggler.tsx
│   │   └── ui
│   │       ├── accordion.tsx
│   │       ├── alert-dialog.tsx
│   │       ├── alert.tsx
│   │       ├── aside-link.tsx
│   │       ├── aspect-ratio.tsx
│   │       ├── avatar.tsx
│   │       ├── background-beams.tsx
│   │       ├── background-boxes.tsx
│   │       ├── badge.tsx
│   │       ├── breadcrumb.tsx
│   │       ├── button.tsx
│   │       ├── calendar.tsx
│   │       ├── callout.tsx
│   │       ├── card.tsx
│   │       ├── carousel.tsx
│   │       ├── chart.tsx
│   │       ├── checkbox.tsx
│   │       ├── code-block.tsx
│   │       ├── collapsible.tsx
│   │       ├── command.tsx
│   │       ├── context-menu.tsx
│   │       ├── dialog.tsx
│   │       ├── drawer.tsx
│   │       ├── dropdown-menu.tsx
│   │       ├── dynamic-code-block.tsx
│   │       ├── fade-in.tsx
│   │       ├── form.tsx
│   │       ├── hover-card.tsx
│   │       ├── input-otp.tsx
│   │       ├── input.tsx
│   │       ├── label.tsx
│   │       ├── menubar.tsx
│   │       ├── navigation-menu.tsx
│   │       ├── pagination.tsx
│   │       ├── popover.tsx
│   │       ├── progress.tsx
│   │       ├── radio-group.tsx
│   │       ├── resizable.tsx
│   │       ├── scroll-area.tsx
│   │       ├── select.tsx
│   │       ├── separator.tsx
│   │       ├── sheet.tsx
│   │       ├── sidebar.tsx
│   │       ├── skeleton.tsx
│   │       ├── slider.tsx
│   │       ├── sonner.tsx
│   │       ├── sparkles.tsx
│   │       ├── switch.tsx
│   │       ├── table.tsx
│   │       ├── tabs.tsx
│   │       ├── textarea.tsx
│   │       ├── toggle-group.tsx
│   │       ├── toggle.tsx
│   │       ├── tooltip-docs.tsx
│   │       ├── tooltip.tsx
│   │       └── use-copy-button.tsx
│   ├── components.json
│   ├── content
│   │   ├── blogs
│   │   │   ├── 0-supabase-auth-to-planetscale-migration.mdx
│   │   │   ├── 1-3.mdx
│   │   │   ├── authjs-joins-better-auth.mdx
│   │   │   └── seed-round.mdx
│   │   ├── changelogs
│   │   │   ├── 1-2.mdx
│   │   │   └── 1.0.mdx
│   │   └── docs
│   │       ├── adapters
│   │       │   ├── community-adapters.mdx
│   │       │   ├── drizzle.mdx
│   │       │   ├── mongo.mdx
│   │       │   ├── mssql.mdx
│   │       │   ├── mysql.mdx
│   │       │   ├── other-relational-databases.mdx
│   │       │   ├── postgresql.mdx
│   │       │   ├── prisma.mdx
│   │       │   └── sqlite.mdx
│   │       ├── authentication
│   │       │   ├── apple.mdx
│   │       │   ├── atlassian.mdx
│   │       │   ├── cognito.mdx
│   │       │   ├── discord.mdx
│   │       │   ├── dropbox.mdx
│   │       │   ├── email-password.mdx
│   │       │   ├── facebook.mdx
│   │       │   ├── figma.mdx
│   │       │   ├── github.mdx
│   │       │   ├── gitlab.mdx
│   │       │   ├── google.mdx
│   │       │   ├── huggingface.mdx
│   │       │   ├── kakao.mdx
│   │       │   ├── kick.mdx
│   │       │   ├── line.mdx
│   │       │   ├── linear.mdx
│   │       │   ├── linkedin.mdx
│   │       │   ├── microsoft.mdx
│   │       │   ├── naver.mdx
│   │       │   ├── notion.mdx
│   │       │   ├── other-social-providers.mdx
│   │       │   ├── paypal.mdx
│   │       │   ├── polar.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
│   ├── tsconfig.json
│   └── turbo.json
├── e2e
│   ├── integration
│   │   ├── package.json
│   │   ├── playwright.config.ts
│   │   ├── solid-vinxi
│   │   │   ├── .gitignore
│   │   │   ├── app.config.ts
│   │   │   ├── e2e
│   │   │   │   ├── test.spec.ts
│   │   │   │   └── utils.ts
│   │   │   ├── package.json
│   │   │   ├── public
│   │   │   │   └── favicon.ico
│   │   │   ├── src
│   │   │   │   ├── app.tsx
│   │   │   │   ├── entry-client.tsx
│   │   │   │   ├── entry-server.tsx
│   │   │   │   ├── global.d.ts
│   │   │   │   ├── lib
│   │   │   │   │   ├── auth-client.ts
│   │   │   │   │   └── auth.ts
│   │   │   │   └── routes
│   │   │   │       ├── [...404].tsx
│   │   │   │       ├── api
│   │   │   │       │   └── auth
│   │   │   │       │       └── [...all].ts
│   │   │   │       └── index.tsx
│   │   │   └── tsconfig.json
│   │   ├── test-utils
│   │   │   ├── package.json
│   │   │   └── src
│   │   │       └── playwright.ts
│   │   └── vanilla-node
│   │       ├── e2e
│   │       │   ├── app.ts
│   │       │   ├── domain.spec.ts
│   │       │   ├── postgres-js.spec.ts
│   │       │   ├── test.spec.ts
│   │       │   └── utils.ts
│   │       ├── index.html
│   │       ├── package.json
│   │       ├── src
│   │       │   ├── main.ts
│   │       │   └── vite-env.d.ts
│   │       ├── tsconfig.json
│   │       └── vite.config.ts
│   └── smoke
│       ├── package.json
│       ├── test
│       │   ├── bun.spec.ts
│       │   ├── cloudflare.spec.ts
│       │   ├── deno.spec.ts
│       │   ├── fixtures
│       │   │   ├── bun-simple.ts
│       │   │   ├── cloudflare
│       │   │   │   ├── .gitignore
│       │   │   │   ├── drizzle
│       │   │   │   │   ├── 0000_clean_vector.sql
│       │   │   │   │   └── meta
│       │   │   │   │       ├── _journal.json
│       │   │   │   │       └── 0000_snapshot.json
│       │   │   │   ├── drizzle.config.ts
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── auth-schema.ts
│       │   │   │   │   ├── db.ts
│       │   │   │   │   └── index.ts
│       │   │   │   ├── test
│       │   │   │   │   ├── apply-migrations.ts
│       │   │   │   │   ├── env.d.ts
│       │   │   │   │   └── index.test.ts
│       │   │   │   ├── tsconfig.json
│       │   │   │   ├── vitest.config.ts
│       │   │   │   ├── worker-configuration.d.ts
│       │   │   │   └── wrangler.json
│       │   │   ├── deno-simple.ts
│       │   │   ├── tsconfig-declaration
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── demo.ts
│       │   │   │   │   ├── index.ts
│       │   │   │   │   └── username.ts
│       │   │   │   └── tsconfig.json
│       │   │   ├── tsconfig-exact-optional-property-types
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── index.ts
│       │   │   │   │   ├── organization.ts
│       │   │   │   │   ├── user-additional-fields.ts
│       │   │   │   │   └── username.ts
│       │   │   │   └── tsconfig.json
│       │   │   ├── tsconfig-isolated-module-bundler
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   └── index.ts
│       │   │   │   └── tsconfig.json
│       │   │   ├── tsconfig-verbatim-module-syntax-node10
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   └── index.ts
│       │   │   │   └── tsconfig.json
│       │   │   └── vite
│       │   │       ├── package.json
│       │   │       ├── src
│       │   │       │   ├── client.ts
│       │   │       │   └── server.ts
│       │   │       ├── tsconfig.json
│       │   │       └── vite.config.ts
│       │   ├── ssr.ts
│       │   ├── typecheck.spec.ts
│       │   └── vite.spec.ts
│       └── tsconfig.json
├── LICENSE.md
├── package.json
├── packages
│   ├── better-auth
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── __snapshots__
│   │   │   │   └── init.test.ts.snap
│   │   │   ├── adapters
│   │   │   │   ├── adapter-factory
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── test
│   │   │   │   │   │   ├── __snapshots__
│   │   │   │   │   │   │   └── adapter-factory.test.ts.snap
│   │   │   │   │   │   └── adapter-factory.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── create-test-suite.ts
│   │   │   │   ├── drizzle-adapter
│   │   │   │   │   ├── drizzle-adapter.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── test
│   │   │   │   │       ├── .gitignore
│   │   │   │   │       ├── adapter.drizzle.mysql.test.ts
│   │   │   │   │       ├── adapter.drizzle.pg.test.ts
│   │   │   │   │       ├── adapter.drizzle.sqlite.test.ts
│   │   │   │   │       └── generate-schema.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kysely-adapter
│   │   │   │   │   ├── bun-sqlite-dialect.ts
│   │   │   │   │   ├── dialect.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── kysely-adapter.ts
│   │   │   │   │   ├── node-sqlite-dialect.ts
│   │   │   │   │   ├── test
│   │   │   │   │   │   ├── adapter.kysely.mssql.test.ts
│   │   │   │   │   │   ├── adapter.kysely.mysql.test.ts
│   │   │   │   │   │   ├── adapter.kysely.pg-custom-schema.test.ts
│   │   │   │   │   │   ├── adapter.kysely.pg.test.ts
│   │   │   │   │   │   ├── adapter.kysely.sqlite.test.ts
│   │   │   │   │   │   └── node-sqlite-dialect.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── memory-adapter
│   │   │   │   │   ├── adapter.memory.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── memory-adapter.ts
│   │   │   │   ├── mongodb-adapter
│   │   │   │   │   ├── adapter.mongo-db.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── mongodb-adapter.ts
│   │   │   │   ├── prisma-adapter
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── prisma-adapter.ts
│   │   │   │   │   └── test
│   │   │   │   │       ├── .gitignore
│   │   │   │   │       ├── base.prisma
│   │   │   │   │       ├── generate-auth-config.ts
│   │   │   │   │       ├── generate-prisma-schema.ts
│   │   │   │   │       ├── get-prisma-client.ts
│   │   │   │   │       ├── prisma.mysql.test.ts
│   │   │   │   │       ├── prisma.pg.test.ts
│   │   │   │   │       ├── prisma.sqlite.test.ts
│   │   │   │   │       └── push-prisma-schema.ts
│   │   │   │   ├── test-adapter.ts
│   │   │   │   ├── test.ts
│   │   │   │   ├── tests
│   │   │   │   │   ├── auth-flow.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── normal.ts
│   │   │   │   │   ├── number-id.ts
│   │   │   │   │   ├── performance.ts
│   │   │   │   │   └── transactions.ts
│   │   │   │   └── utils.ts
│   │   │   ├── api
│   │   │   │   ├── check-endpoint-conflicts.test.ts
│   │   │   │   ├── index.test.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── middlewares
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── origin-check.test.ts
│   │   │   │   │   └── origin-check.ts
│   │   │   │   ├── rate-limiter
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── rate-limiter.test.ts
│   │   │   │   ├── routes
│   │   │   │   │   ├── account.test.ts
│   │   │   │   │   ├── account.ts
│   │   │   │   │   ├── callback.ts
│   │   │   │   │   ├── email-verification.test.ts
│   │   │   │   │   ├── email-verification.ts
│   │   │   │   │   ├── error.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── ok.ts
│   │   │   │   │   ├── reset-password.test.ts
│   │   │   │   │   ├── reset-password.ts
│   │   │   │   │   ├── session-api.test.ts
│   │   │   │   │   ├── session.ts
│   │   │   │   │   ├── sign-in.test.ts
│   │   │   │   │   ├── sign-in.ts
│   │   │   │   │   ├── sign-out.test.ts
│   │   │   │   │   ├── sign-out.ts
│   │   │   │   │   ├── sign-up.test.ts
│   │   │   │   │   ├── sign-up.ts
│   │   │   │   │   ├── update-user.test.ts
│   │   │   │   │   └── update-user.ts
│   │   │   │   ├── to-auth-endpoints.test.ts
│   │   │   │   └── to-auth-endpoints.ts
│   │   │   ├── auth.test.ts
│   │   │   ├── auth.ts
│   │   │   ├── call.test.ts
│   │   │   ├── client
│   │   │   │   ├── client-ssr.test.ts
│   │   │   │   ├── client.test.ts
│   │   │   │   ├── config.ts
│   │   │   │   ├── fetch-plugins.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── lynx
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── lynx-store.ts
│   │   │   │   ├── parser.ts
│   │   │   │   ├── path-to-object.ts
│   │   │   │   ├── plugins
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── infer-plugin.ts
│   │   │   │   ├── proxy.ts
│   │   │   │   ├── query.ts
│   │   │   │   ├── react
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── react-store.ts
│   │   │   │   ├── session-atom.ts
│   │   │   │   ├── solid
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── solid-store.ts
│   │   │   │   ├── svelte
│   │   │   │   │   └── index.ts
│   │   │   │   ├── test-plugin.ts
│   │   │   │   ├── types.ts
│   │   │   │   ├── url.test.ts
│   │   │   │   ├── vanilla.ts
│   │   │   │   └── vue
│   │   │   │       ├── index.ts
│   │   │   │       └── vue-store.ts
│   │   │   ├── cookies
│   │   │   │   ├── check-cookies.ts
│   │   │   │   ├── cookie-utils.ts
│   │   │   │   ├── cookies.test.ts
│   │   │   │   └── index.ts
│   │   │   ├── crypto
│   │   │   │   ├── buffer.ts
│   │   │   │   ├── hash.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── jwt.ts
│   │   │   │   ├── password.test.ts
│   │   │   │   ├── password.ts
│   │   │   │   └── random.ts
│   │   │   ├── db
│   │   │   │   ├── db.test.ts
│   │   │   │   ├── field.ts
│   │   │   │   ├── get-migration-schema.test.ts
│   │   │   │   ├── get-migration.ts
│   │   │   │   ├── get-schema.ts
│   │   │   │   ├── get-tables.test.ts
│   │   │   │   ├── get-tables.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── internal-adapter.test.ts
│   │   │   │   ├── internal-adapter.ts
│   │   │   │   ├── schema.ts
│   │   │   │   ├── secondary-storage.test.ts
│   │   │   │   ├── to-zod.ts
│   │   │   │   ├── utils.ts
│   │   │   │   └── with-hooks.ts
│   │   │   ├── index.ts
│   │   │   ├── init.test.ts
│   │   │   ├── init.ts
│   │   │   ├── integrations
│   │   │   │   ├── next-js.ts
│   │   │   │   ├── node.ts
│   │   │   │   ├── react-start.ts
│   │   │   │   ├── solid-start.ts
│   │   │   │   └── svelte-kit.ts
│   │   │   ├── oauth2
│   │   │   │   ├── index.ts
│   │   │   │   ├── link-account.test.ts
│   │   │   │   ├── link-account.ts
│   │   │   │   ├── state.ts
│   │   │   │   └── utils.ts
│   │   │   ├── plugins
│   │   │   │   ├── access
│   │   │   │   │   ├── access.test.ts
│   │   │   │   │   ├── access.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── additional-fields
│   │   │   │   │   ├── additional-fields.test.ts
│   │   │   │   │   └── client.ts
│   │   │   │   ├── admin
│   │   │   │   │   ├── access
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   └── statement.ts
│   │   │   │   │   ├── admin.test.ts
│   │   │   │   │   ├── admin.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── error-codes.ts
│   │   │   │   │   ├── has-permission.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── anonymous
│   │   │   │   │   ├── anon.test.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── api-key
│   │   │   │   │   ├── api-key.test.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── rate-limit.ts
│   │   │   │   │   ├── routes
│   │   │   │   │   │   ├── create-api-key.ts
│   │   │   │   │   │   ├── delete-all-expired-api-keys.ts
│   │   │   │   │   │   ├── delete-api-key.ts
│   │   │   │   │   │   ├── get-api-key.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── list-api-keys.ts
│   │   │   │   │   │   ├── update-api-key.ts
│   │   │   │   │   │   └── verify-api-key.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── bearer
│   │   │   │   │   ├── bearer.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── captcha
│   │   │   │   │   ├── captcha.test.ts
│   │   │   │   │   ├── constants.ts
│   │   │   │   │   ├── error-codes.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   ├── utils.ts
│   │   │   │   │   └── verify-handlers
│   │   │   │   │       ├── captchafox.ts
│   │   │   │   │       ├── cloudflare-turnstile.ts
│   │   │   │   │       ├── google-recaptcha.ts
│   │   │   │   │       ├── h-captcha.ts
│   │   │   │   │       └── index.ts
│   │   │   │   ├── custom-session
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── custom-session.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── device-authorization
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── device-authorization.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── schema.ts
│   │   │   │   ├── email-otp
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── email-otp.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── generic-oauth
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── generic-oauth.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── haveibeenpwned
│   │   │   │   │   ├── haveibeenpwned.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── jwt
│   │   │   │   │   ├── adapter.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── jwt.test.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── sign.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── last-login-method
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── custom-prefix.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── last-login-method.test.ts
│   │   │   │   ├── magic-link
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── magic-link.test.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── mcp
│   │   │   │   │   ├── authorize.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── mcp.test.ts
│   │   │   │   ├── multi-session
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── multi-session.test.ts
│   │   │   │   ├── oauth-proxy
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── oauth-proxy.test.ts
│   │   │   │   ├── oidc-provider
│   │   │   │   │   ├── authorize.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── oidc.test.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   ├── ui.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── one-tap
│   │   │   │   │   ├── client.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── one-time-token
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── one-time-token.test.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── open-api
│   │   │   │   │   ├── generator.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── logo.ts
│   │   │   │   │   └── open-api.test.ts
│   │   │   │   ├── organization
│   │   │   │   │   ├── access
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   └── statement.ts
│   │   │   │   │   ├── adapter.ts
│   │   │   │   │   ├── call.ts
│   │   │   │   │   ├── client.test.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── error-codes.ts
│   │   │   │   │   ├── has-permission.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── organization-hook.test.ts
│   │   │   │   │   ├── organization.test.ts
│   │   │   │   │   ├── organization.ts
│   │   │   │   │   ├── permission.ts
│   │   │   │   │   ├── routes
│   │   │   │   │   │   ├── crud-access-control.test.ts
│   │   │   │   │   │   ├── crud-access-control.ts
│   │   │   │   │   │   ├── crud-invites.ts
│   │   │   │   │   │   ├── crud-members.test.ts
│   │   │   │   │   │   ├── crud-members.ts
│   │   │   │   │   │   ├── crud-org.test.ts
│   │   │   │   │   │   ├── crud-org.ts
│   │   │   │   │   │   └── crud-team.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── team.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── passkey
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── passkey.test.ts
│   │   │   │   ├── phone-number
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── phone-number-error.ts
│   │   │   │   │   └── phone-number.test.ts
│   │   │   │   ├── siwe
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── siwe.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── two-factor
│   │   │   │   │   ├── backup-codes
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── constant.ts
│   │   │   │   │   ├── error-code.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── otp
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── totp
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── two-factor.test.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   ├── utils.ts
│   │   │   │   │   └── verify-two-factor.ts
│   │   │   │   └── username
│   │   │   │       ├── client.ts
│   │   │   │       ├── error-codes.ts
│   │   │   │       ├── index.ts
│   │   │   │       ├── schema.ts
│   │   │   │       └── username.test.ts
│   │   │   ├── social-providers
│   │   │   │   └── index.ts
│   │   │   ├── social.test.ts
│   │   │   ├── test-utils
│   │   │   │   ├── headers.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── state.ts
│   │   │   │   └── test-instance.ts
│   │   │   ├── types
│   │   │   │   ├── adapter.ts
│   │   │   │   ├── api.ts
│   │   │   │   ├── helper.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── models.ts
│   │   │   │   ├── plugins.ts
│   │   │   │   └── types.test.ts
│   │   │   └── utils
│   │   │       ├── await-object.ts
│   │   │       ├── boolean.ts
│   │   │       ├── clone.ts
│   │   │       ├── constants.ts
│   │   │       ├── date.ts
│   │   │       ├── ensure-utc.ts
│   │   │       ├── get-request-ip.ts
│   │   │       ├── hashing.ts
│   │   │       ├── hide-metadata.ts
│   │   │       ├── id.ts
│   │   │       ├── import-util.ts
│   │   │       ├── index.ts
│   │   │       ├── is-atom.ts
│   │   │       ├── is-promise.ts
│   │   │       ├── json.ts
│   │   │       ├── merger.ts
│   │   │       ├── middleware-response.ts
│   │   │       ├── misc.ts
│   │   │       ├── password.ts
│   │   │       ├── plugin-helper.ts
│   │   │       ├── shim.ts
│   │   │       ├── time.ts
│   │   │       ├── url.ts
│   │   │       └── wildcard.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   ├── vitest.config.ts
│   │   └── vitest.setup.ts
│   ├── cli
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── commands
│   │   │   │   ├── generate.ts
│   │   │   │   ├── info.ts
│   │   │   │   ├── init.ts
│   │   │   │   ├── login.ts
│   │   │   │   ├── mcp.ts
│   │   │   │   ├── migrate.ts
│   │   │   │   └── secret.ts
│   │   │   ├── generators
│   │   │   │   ├── auth-config.ts
│   │   │   │   ├── drizzle.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kysely.ts
│   │   │   │   ├── prisma.ts
│   │   │   │   └── types.ts
│   │   │   ├── index.ts
│   │   │   └── utils
│   │   │       ├── add-svelte-kit-env-modules.ts
│   │   │       ├── check-package-managers.ts
│   │   │       ├── format-ms.ts
│   │   │       ├── get-config.ts
│   │   │       ├── get-package-info.ts
│   │   │       ├── get-tsconfig-info.ts
│   │   │       └── install-dependencies.ts
│   │   ├── test
│   │   │   ├── __snapshots__
│   │   │   │   ├── auth-schema-mysql-enum.txt
│   │   │   │   ├── auth-schema-mysql-number-id.txt
│   │   │   │   ├── auth-schema-mysql-passkey-number-id.txt
│   │   │   │   ├── auth-schema-mysql-passkey.txt
│   │   │   │   ├── auth-schema-mysql.txt
│   │   │   │   ├── auth-schema-number-id.txt
│   │   │   │   ├── auth-schema-pg-enum.txt
│   │   │   │   ├── auth-schema-pg-passkey.txt
│   │   │   │   ├── auth-schema-sqlite-enum.txt
│   │   │   │   ├── auth-schema-sqlite-number-id.txt
│   │   │   │   ├── auth-schema-sqlite-passkey-number-id.txt
│   │   │   │   ├── auth-schema-sqlite-passkey.txt
│   │   │   │   ├── auth-schema-sqlite.txt
│   │   │   │   ├── auth-schema.txt
│   │   │   │   ├── migrations.sql
│   │   │   │   ├── schema-mongodb.prisma
│   │   │   │   ├── schema-mysql-custom.prisma
│   │   │   │   ├── schema-mysql.prisma
│   │   │   │   ├── schema-numberid.prisma
│   │   │   │   └── schema.prisma
│   │   │   ├── generate-all-db.test.ts
│   │   │   ├── generate.test.ts
│   │   │   ├── get-config.test.ts
│   │   │   ├── info.test.ts
│   │   │   └── migrate.test.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── core
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── api
│   │   │   │   └── index.ts
│   │   │   ├── async_hooks
│   │   │   │   └── index.ts
│   │   │   ├── context
│   │   │   │   ├── endpoint-context.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── transaction.ts
│   │   │   ├── db
│   │   │   │   ├── adapter
│   │   │   │   │   └── index.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── plugin.ts
│   │   │   │   ├── schema
│   │   │   │   │   ├── account.ts
│   │   │   │   │   ├── rate-limit.ts
│   │   │   │   │   ├── session.ts
│   │   │   │   │   ├── shared.ts
│   │   │   │   │   ├── user.ts
│   │   │   │   │   └── verification.ts
│   │   │   │   └── type.ts
│   │   │   ├── env
│   │   │   │   ├── color-depth.ts
│   │   │   │   ├── env-impl.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── logger.test.ts
│   │   │   │   └── logger.ts
│   │   │   ├── error
│   │   │   │   ├── codes.ts
│   │   │   │   └── index.ts
│   │   │   ├── index.ts
│   │   │   ├── oauth2
│   │   │   │   ├── client-credentials-token.ts
│   │   │   │   ├── create-authorization-url.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── oauth-provider.ts
│   │   │   │   ├── refresh-access-token.ts
│   │   │   │   ├── utils.ts
│   │   │   │   └── validate-authorization-code.ts
│   │   │   ├── social-providers
│   │   │   │   ├── apple.ts
│   │   │   │   ├── atlassian.ts
│   │   │   │   ├── cognito.ts
│   │   │   │   ├── discord.ts
│   │   │   │   ├── dropbox.ts
│   │   │   │   ├── facebook.ts
│   │   │   │   ├── figma.ts
│   │   │   │   ├── github.ts
│   │   │   │   ├── gitlab.ts
│   │   │   │   ├── google.ts
│   │   │   │   ├── huggingface.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kakao.ts
│   │   │   │   ├── kick.ts
│   │   │   │   ├── line.ts
│   │   │   │   ├── linear.ts
│   │   │   │   ├── linkedin.ts
│   │   │   │   ├── microsoft-entra-id.ts
│   │   │   │   ├── naver.ts
│   │   │   │   ├── notion.ts
│   │   │   │   ├── paypal.ts
│   │   │   │   ├── polar.ts
│   │   │   │   ├── reddit.ts
│   │   │   │   ├── roblox.ts
│   │   │   │   ├── salesforce.ts
│   │   │   │   ├── slack.ts
│   │   │   │   ├── spotify.ts
│   │   │   │   ├── tiktok.ts
│   │   │   │   ├── twitch.ts
│   │   │   │   ├── twitter.ts
│   │   │   │   ├── vk.ts
│   │   │   │   └── zoom.ts
│   │   │   ├── types
│   │   │   │   ├── context.ts
│   │   │   │   ├── cookie.ts
│   │   │   │   ├── helper.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── init-options.ts
│   │   │   │   ├── plugin-client.ts
│   │   │   │   └── plugin.ts
│   │   │   └── utils
│   │   │       ├── error-codes.ts
│   │   │       └── index.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── expo
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   └── index.ts
│   │   ├── test
│   │   │   └── expo.test.ts
│   │   ├── tsconfig.json
│   │   ├── tsconfig.test.json
│   │   └── tsdown.config.ts
│   ├── sso
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── index.ts
│   │   │   ├── oidc.test.ts
│   │   │   └── saml.test.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── stripe
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── hooks.ts
│   │   │   ├── index.ts
│   │   │   ├── schema.ts
│   │   │   ├── stripe.test.ts
│   │   │   ├── types.ts
│   │   │   └── utils.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   └── telemetry
│       ├── package.json
│       ├── src
│       │   ├── detectors
│       │   │   ├── detect-auth-config.ts
│       │   │   ├── detect-database.ts
│       │   │   ├── detect-framework.ts
│       │   │   ├── detect-project-info.ts
│       │   │   ├── detect-runtime.ts
│       │   │   └── detect-system-info.ts
│       │   ├── index.ts
│       │   ├── project-id.ts
│       │   ├── telemetry.test.ts
│       │   ├── types.ts
│       │   └── utils
│       │       ├── hash.ts
│       │       ├── id.ts
│       │       ├── import-util.ts
│       │       └── package-json.ts
│       ├── tsconfig.json
│       └── tsdown.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── SECURITY.md
├── tsconfig.base.json
├── tsconfig.json
└── turbo.json
```

# Files

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/organization/routes/crud-access-control.test.ts:
--------------------------------------------------------------------------------

```typescript
import type { DBFieldAttribute } from "@better-auth/core/db";
import { describe, expect, expectTypeOf } from "vitest";
import { createAuthClient } from "../../../client";
import { parseSetCookieHeader } from "../../../cookies";
import { getTestInstance } from "../../../test-utils/test-instance";
import { createAccessControl } from "../../access";
import { adminAc, defaultStatements, memberAc, ownerAc } from "../access";
import { inferOrgAdditionalFields, organizationClient } from "../client";
import { ORGANIZATION_ERROR_CODES } from "../error-codes";
import { organization } from "../organization";

describe("dynamic access control", async (it) => {
	const ac = createAccessControl({
		project: ["create", "read", "update", "delete"],
		sales: ["create", "read", "update", "delete"],
		...defaultStatements,
	});
	const owner = ac.newRole({
		project: ["create", "delete", "update", "read"],
		sales: ["create", "read", "update", "delete"],
		...ownerAc.statements,
	});
	const admin = ac.newRole({
		project: ["create", "read", "delete", "update"],
		sales: ["create", "read"],
		...adminAc.statements,
	});
	const member = ac.newRole({
		project: ["read"],
		sales: ["read"],
		...memberAc.statements,
	});

	const additionalFields = {
		color: {
			type: "string",
			defaultValue: "#ffffff",
			required: true,
		},
		serverOnlyValue: {
			type: "string",
			defaultValue: "server-only-value",
			input: false,
			required: true,
		},
	} satisfies Record<string, DBFieldAttribute>;

	const { auth, customFetchImpl, sessionSetter, signInWithTestUser } =
		await getTestInstance({
			plugins: [
				organization({
					ac,
					roles: {
						admin,
						member,
						owner,
					},
					dynamicAccessControl: {
						enabled: true,
					},
					schema: {
						organizationRole: {
							additionalFields,
						},
					},
				}),
			],
		});

	const authClient = createAuthClient({
		baseURL: "http://localhost:3000",
		plugins: [
			organizationClient({
				ac,
				roles: {
					admin,
					member,
					owner,
				},
				dynamicAccessControl: {
					enabled: true,
				},
				schema: inferOrgAdditionalFields<typeof auth>(),
			}),
		],
		fetchOptions: {
			customFetchImpl,
		},
	});
	const {
		organization: { checkRolePermission, hasPermission, create },
	} = authClient;

	const { headers, user, session } = await signInWithTestUser();

	async function createUser({ role }: { role: "admin" | "member" | "owner" }) {
		const normalUserDetails = {
			email: `some-test-user-${crypto.randomUUID()}@email.com`,
			name: `some-test-user`,
			password: `some-test-user-${crypto.randomUUID()}`,
		};
		const normalUser = await auth.api.signUpEmail({ body: normalUserDetails });
		const member = await auth.api.addMember({
			body: {
				role: role || "member",
				userId: normalUser.user.id,
				organizationId: org.data?.id,
			},
			headers,
		});
		if (!member) throw new Error("Member not found");
		let userHeaders = new Headers();
		await authClient.signIn.email({
			email: normalUserDetails.email,
			password: normalUserDetails.password,
			fetchOptions: {
				onSuccess: (context) => {
					const header = context.response.headers.get("set-cookie");
					const cookies = parseSetCookieHeader(header || "");
					const signedCookie = cookies.get("better-auth.session_token")?.value;
					userHeaders.set(
						"cookie",
						`better-auth.session_token=${signedCookie}`,
					);
				},
			},
		});
		await authClient.organization.setActive({
			organizationId: org.data?.id,
			fetchOptions: {
				headers: userHeaders,
			},
		});

		return { headers: userHeaders, user: normalUser, member };
	}

	const org = await create(
		{
			name: "test",
			slug: "test",
			metadata: {
				test: "test",
			},
		},
		{
			onSuccess: sessionSetter(headers),
			headers,
		},
	);
	if (!org.data) throw new Error("Organization not created");
	const memberInfo = await auth.api.getActiveMember({ headers });
	if (!memberInfo) throw new Error("Member info not found");

	// Create an admin user in the org.
	const {
		headers: adminHeaders,
		user: adminUser,
		member: adminMember,
	} = await createUser({
		role: "admin",
	});

	// Create normal users in the org.
	const {
		headers: normalHeaders,
		user: normalUser,
		member: normalMember,
	} = await createUser({
		role: "member",
	});

	/**
	 * The following test will:
	 * - Creation of a new role
	 * - Updating their own role to the newly created one (from owner to the new one)
	 * - Tests the `hasPermission` endpoint against the new role, for both a success and a failure case.
	 * - Additional fields passed in body, and correct return value & types.
	 */
	it("should successfully create a new role", async () => {
		// Create a new "test" role with permissions to create a project.
		const permission = {
			project: ["create"],
		};
		const testRole = await authClient.organization.createRole(
			{
				role: "test",
				permission,
				additionalFields: {
					color: "#000000",
				},
			},
			{
				headers,
			},
		);
		expect(testRole.error).toBeNull();
		expect(testRole.data?.success).toBe(true);
		expect(testRole.data?.roleData.permission).toEqual(permission);
		expect(testRole.data?.roleData.color).toBe("#000000");
		expect(testRole.data?.roleData.serverOnlyValue).toBe("server-only-value");
		expectTypeOf(testRole.data?.roleData.serverOnlyValue).toEqualTypeOf<
			string | undefined
		>();
		expectTypeOf(testRole.data?.roleData.role).toEqualTypeOf<
			string | undefined
		>();
		if (!testRole.data) return;

		// Update the role to use the new one.

		await auth.api.updateMemberRole({
			body: { memberId: normalMember.id, role: testRole.data.roleData.role },
			headers,
		});

		// Test against `hasPermission` endpoint
		// Should fail because the user doesn't have the permission to delete a project.
		const shouldFail = await auth.api.hasPermission({
			body: {
				organizationId: org.data?.id,
				permissions: {
					project: ["delete"],
				},
			},
			headers: normalHeaders,
		});
		expect(shouldFail.success).toBe(false);

		// Should pass because the user has the permission to create a project.
		const shouldPass = await auth.api.hasPermission({
			body: {
				organizationId: org.data?.id,
				permissions: {
					project: ["create"],
				},
			},
			headers: normalHeaders,
		});
		expect(shouldPass.success).toBe(true);
	});

	it("should not be allowed to create a role without the right ac resource permissions", async () => {
		const testRole = await authClient.organization.createRole(
			{
				role: `test-${crypto.randomUUID()}`,
				permission: {
					project: ["create"],
				},
				additionalFields: {
					color: "#000000",
				},
			},
			{
				headers: normalHeaders,
			},
		);
		expect(testRole.data).toBeNull();
		if (!testRole.error) throw new Error("Test role error not found");
		expect(testRole.error.message).toEqual(
			ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE,
		);
	});

	it("should not be allowed to create a role with higher permissions than the current role", async () => {
		const testRole = await authClient.organization.createRole(
			{
				role: `test-${crypto.randomUUID()}`,
				permission: {
					sales: ["create", "delete", "create", "update", "read"], // Intentionally duplicate the "create" permission.
				},
				additionalFields: {
					color: "#000000",
				},
			},
			{
				headers: adminHeaders,
			},
		);
		expect(testRole.data).toBeNull();
		if (testRole.data) throw new Error("Test role created");
		expect(
			testRole.error.message?.startsWith(
				ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_ROLE,
			),
		).toBe(true);
		expect("missingPermissions" in testRole.error).toBe(true);
		if (!("missingPermissions" in testRole.error)) return;
		expect(testRole.error.missingPermissions).toEqual([
			"sales:delete",
			"sales:update",
		]);
	});

	it("should not be allowed to create a role which is either predefined or already exists in DB", async () => {
		const testRole = await authClient.organization.createRole(
			{
				role: "admin", // This is a predefined role.
				permission: {
					project: ["create"],
				},
				additionalFields: {
					color: "#000000",
				},
			},
			{
				headers,
			},
		);
		expect(testRole.data).toBeNull();
		if (!testRole.error) throw new Error("Test role error not found");
		expect(testRole.error.message).toEqual(
			ORGANIZATION_ERROR_CODES.ROLE_NAME_IS_ALREADY_TAKEN,
		);

		const testRole2 = await authClient.organization.createRole(
			{
				role: "test", // This is a role that was created in the previous test.
				permission: {
					project: ["create"],
				},
				additionalFields: {
					color: "#000000",
				},
			},
			{
				headers,
			},
		);
		expect(testRole2.data).toBeNull();
		if (!testRole2.error) throw new Error("Test role error not found");
		expect(testRole2.error.message).toEqual(
			ORGANIZATION_ERROR_CODES.ROLE_NAME_IS_ALREADY_TAKEN,
		);
	});

	it("should delete a role by id", async () => {
		const testRole = await authClient.organization.createRole(
			{
				role: `test-${crypto.randomUUID()}`,
				permission: {
					project: ["create"],
				},
				additionalFields: {
					color: "#000000",
				},
			},
			{
				headers,
			},
		);
		if (!testRole.data) throw testRole.error;
		const roleId = testRole.data.roleData.id;

		const res = await auth.api.deleteOrgRole({
			body: { roleId },
			headers,
		});
		expect(res).not.toBeNull();
	});

	it("should delete a role by name", async () => {
		const testRole = await authClient.organization.createRole(
			{
				role: `test-${crypto.randomUUID()}`,
				permission: {
					project: ["create"],
				},
				additionalFields: {
					color: "#000000",
				},
			},
			{
				headers,
			},
		);
		if (!testRole.data) throw testRole.error;
		const roleName = testRole.data.roleData.role;

		const res = await auth.api.deleteOrgRole({
			body: { roleName },
			headers,
		});
		expect(res).not.toBeNull();
	});

	it("should not be allowed to delete a role without nessesary permissions", async () => {
		const testRole = await authClient.organization.createRole(
			{
				role: `test-${crypto.randomUUID()}`,
				permission: {
					project: ["create"],
				},
				additionalFields: {
					color: "#000000",
				},
			},
			{
				headers: adminHeaders,
			},
		);
		if (!testRole.data) throw testRole.error;
		expect(
			auth.api.deleteOrgRole({
				body: { roleName: testRole.data.roleData.role },
				headers: normalHeaders,
			}),
		).rejects.toThrow(
			ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_A_ROLE,
		);
	});

	it("should not be allowed to delete a role that doesn't exist", async () => {
		try {
			const res = await auth.api.deleteOrgRole({
				body: { roleName: "non-existent-role" },
				headers,
			});
			expect(res).toBeNull();
		} catch (error: any) {
			if ("body" in error && "message" in error.body) {
				expect(error.body.message).toBe(
					ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
				);
			} else {
				throw error;
			}
		}
	});

	it("should list roles", async () => {
		const permission = {
			project: ["create"],
			ac: ["read", "update", "create", "delete"],
		};
		await authClient.organization.createRole(
			{
				role: `list-test-role`,
				permission,
				additionalFields: {
					color: "#123",
				},
			},
			{
				headers,
			},
		);

		const res = await auth.api.listOrgRoles({ headers });
		expect(res).not.toBeNull();
		expect(res.length).toBeGreaterThan(0);
		expect(typeof res[0]!.permission === "string").toBe(false);
		const foundRole = res.find((x) => x.role === "list-test-role");
		expect(foundRole).not.toBeNull();
		expect(foundRole?.permission).toEqual(permission);
		expect(foundRole?.color).toBe(`#123`);
		expectTypeOf(foundRole?.color).toEqualTypeOf<string | undefined>();
		expectTypeOf(foundRole?.serverOnlyValue).toEqualTypeOf<
			string | undefined
		>();
	});

	it("should not be allowed to list roles without nessesary permissions", async () => {
		expect(auth.api.listOrgRoles({ headers: normalHeaders })).rejects.toThrow(
			ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE,
		);
	});

	it("should get a role by id", async () => {
		const testRole = await authClient.organization.createRole(
			{
				role: `read-test-role-${crypto.randomUUID()}`,
				permission: {
					project: ["create"],
				},
				additionalFields: {
					color: "#000000",
				},
			},
			{
				headers,
			},
		);
		if (!testRole.data) throw testRole.error;
		const roleId = testRole.data.roleData.id;
		const res = await auth.api.getOrgRole({
			query: {
				roleId,
				organizationId: org.data?.id,
			},
			headers,
		});
		expect(res).not.toBeNull();
		expect(res.role).toBe(testRole.data.roleData.role);
		expect(res.permission).toEqual(testRole.data.roleData.permission);
		expect(res.color).toBe("#000000");
		expectTypeOf(res.color).toEqualTypeOf<string>();
	});

	it("should get a role by name", async () => {
		const testRole = await authClient.organization.createRole(
			{
				role: `read-test-role-${crypto.randomUUID()}`,
				permission: {
					project: ["create"],
				},
				additionalFields: {
					color: "#000000",
				},
			},
			{
				headers,
			},
		);
		if (!testRole.data) throw testRole.error;
		const roleName = testRole.data.roleData.role;

		const res = await auth.api.getOrgRole({
			query: {
				roleName,
				organizationId: org.data?.id,
			},
			headers,
		});
		expect(res).not.toBeNull();
		expect(res.role).toBe(testRole.data.roleData.role);
		expect(res.permission).toEqual(testRole.data.roleData.permission);
		expect(res.color).toBe("#000000");
		expectTypeOf(res.color).toEqualTypeOf<string>();
	});

	it("should update a role's permission by id", async () => {
		const testRole = await authClient.organization.createRole(
			{
				role: `update-test-role-${crypto.randomUUID()}`,
				permission: {
					project: ["create"],
				},
				additionalFields: {
					color: "#000000",
				},
			},
			{
				headers,
			},
		);
		if (!testRole.data) throw testRole.error;
		const roleId = testRole.data.roleData.id;
		const res = await auth.api.updateOrgRole({
			body: {
				roleId,
				data: { permission: { project: ["create", "delete"] } },
			},
			headers,
		});
		expect(res).not.toBeNull();
		expect(res.roleData.role).toBe(testRole.data.roleData.role);
		expect(res.roleData.permission).toEqual({ project: ["create", "delete"] });
	});

	it("should update a role's name by name", async () => {
		const testRole = await authClient.organization.createRole(
			{
				role: `test-${crypto.randomUUID()}`,
				permission: {
					project: ["create"],
				},
				additionalFields: {
					color: "#000000",
				},
			},
			{ headers },
		);
		if (!testRole.data) throw testRole.error;
		const roleName = testRole.data.roleData.role;

		const res = await auth.api.updateOrgRole({
			body: { roleName, data: { roleName: `updated-${roleName}` } },
			headers,
		});
		expect(res).not.toBeNull();
		expect(res.roleData.role).toBe(`updated-${roleName}`);

		const res2 = await auth.api.getOrgRole({
			query: {
				roleName: `updated-${roleName}`,
				organizationId: org.data?.id,
			},
			headers,
		});
		expect(res2).not.toBeNull();
		expect(res2.role).toBe(`updated-${roleName}`);
	});

	it("should not be allowed to update a role without the right ac resource permissions", async () => {
		const testRole = await authClient.organization.createRole(
			{
				role: `update-not-allowed-${crypto.randomUUID()}`,
				permission: {
					project: ["create"],
				},
				additionalFields: {
					color: "#000000",
				},
			},
			{ headers },
		);
		if (!testRole.data) throw testRole.error;
		const roleId = testRole.data.roleData.id;
		await expect(
			auth.api.updateOrgRole({
				body: {
					roleId,
					data: { roleName: `updated-${testRole.data.roleData.role}` },
				},
				headers: normalHeaders,
			}),
		).rejects.toThrow();
	});

	it("should be able to update additional fields", async () => {
		const testRole = await authClient.organization.createRole(
			{
				role: `test-${crypto.randomUUID()}`,
				permission: {
					project: ["create"],
				},
				additionalFields: {
					color: "#000000",
					//@ts-expect-error - intentionally invalid key
					someInvalidKey: "this would be ignored by zod",
				},
			},
			{
				headers,
			},
		);
		if (!testRole.data) throw testRole.error;
		const roleId = testRole.data.roleData.id;
		const res = await auth.api.updateOrgRole({
			body: { roleId, data: { color: "#111111" } },
			headers,
		});
		expect(res).not.toBeNull();
		expect(res.roleData.color).toBe("#111111");
		//@ts-expect-error - intentionally invalid key
		expect(res.roleData.someInvalidKey).toBeUndefined();
	});

	/**
	 * Security test cases for the privilege escalation vulnerability fix
	 * These tests verify that member queries properly filter by userId to prevent
	 * unauthorized privilege escalation where any member could gain admin permissions
	 */
	it("should not allow member to list roles using another member's permissions", async () => {
		// Create a fresh member for this test to avoid role contamination
		const {
			headers: freshMemberHeaders,
			user: freshMemberUser,
			member: freshMember,
		} = await createUser({
			role: "member",
		});

		// Create a test role that only admin can read
		const adminOnlyRole = await authClient.organization.createRole(
			{
				role: `admin-only-${crypto.randomUUID()}`,
				permission: {
					project: ["delete"],
				},
				additionalFields: {
					color: "#ff0000",
				},
			},
			{
				headers,
			},
		);
		if (!adminOnlyRole.data) throw adminOnlyRole.error;

		// Try to list roles as a regular member - should succeed but with member permissions
		const listAsMembers = await auth.api.listOrgRoles({
			query: { organizationId: org.data?.id },
			headers: freshMemberHeaders,
		});

		// Member should be able to list roles (they have ac:read permission)
		expect(listAsMembers).toBeDefined();
		expect(Array.isArray(listAsMembers)).toBe(true);
	});

	it("should not allow member to get role details using another member's permissions", async () => {
		// Create a fresh member for this test to avoid role contamination
		const {
			headers: freshMemberHeaders,
			user: freshMemberUser,
			member: freshMember,
		} = await createUser({
			role: "member",
		});

		// Create a test role
		const testRole = await authClient.organization.createRole(
			{
				role: `test-get-role-${crypto.randomUUID()}`,
				permission: {
					project: ["read"],
				},
				additionalFields: {
					color: "#ff0000",
				},
			},
			{
				headers,
			},
		);
		if (!testRole.data) throw testRole.error;

		// Try to get role as a regular member - should succeed with member permissions
		const getRoleAsMember = await auth.api.getOrgRole({
			query: {
				organizationId: org.data?.id,
				roleId: testRole.data.roleData.id,
			},
			headers: freshMemberHeaders,
		});

		// Member should be able to read the role (they have ac:read permission)
		expect(getRoleAsMember).toBeDefined();
		expect(getRoleAsMember.id).toBe(testRole.data.roleData.id);
	});

	it("should not allow member to update roles without proper permissions (privilege escalation test)", async () => {
		// Create a fresh member for this test to avoid role contamination
		const {
			headers: freshMemberHeaders,
			user: freshMemberUser,
			member: freshMember,
		} = await createUser({
			role: "member",
		});

		// Create a test role that the owner will create
		const vulnerableRole = await authClient.organization.createRole(
			{
				role: `vulnerable-role-${crypto.randomUUID()}`,
				permission: {
					project: ["read"],
				},
				additionalFields: {
					color: "#ff0000",
				},
			},
			{
				headers, // owner headers
			},
		);
		if (!vulnerableRole.data) throw vulnerableRole.error;

		// Regular member should NOT be able to update the role
		// This tests the privilege escalation vulnerability fix
		await expect(
			auth.api.updateOrgRole({
				body: {
					roleId: vulnerableRole.data.roleData.id,
					data: {
						permission: {
							ac: ["create", "update", "delete"], // Try to escalate privileges
							organization: ["update", "delete"],
							project: ["create", "read", "update", "delete"],
						},
					},
				},
				headers: freshMemberHeaders, // member headers
			}),
		).rejects.toThrow();

		// Verify the role permissions haven't changed
		const roleCheck = await auth.api.getOrgRole({
			query: {
				organizationId: org.data?.id,
				roleId: vulnerableRole.data.roleData.id,
			},
			headers,
		});
		expect(roleCheck.permission).toEqual({
			project: ["read"],
		});
	});

	it("should properly identify the correct member when checking permissions", async () => {
		// Create a fresh member for this test to avoid role contamination
		const {
			headers: freshMemberHeaders,
			user: freshMemberUser,
			member: freshMember,
		} = await createUser({
			role: "member",
		});

		// This test ensures that the member lookup uses both organizationId AND userId
		// Create a role that only owner can update
		const ownerOnlyRole = await authClient.organization.createRole(
			{
				role: `owner-only-update-${crypto.randomUUID()}`,
				permission: {
					sales: ["delete"],
				},
				additionalFields: {
					color: "#ff0000",
				},
			},
			{
				headers, // owner headers
			},
		);
		if (!ownerOnlyRole.data) throw ownerOnlyRole.error;

		// Member should not be able to update (doesn't have ac:update)
		await expect(
			auth.api.updateOrgRole({
				body: {
					roleId: ownerOnlyRole.data.roleData.id,
					data: {
						roleName: "hijacked-role",
					},
				},
				headers: freshMemberHeaders,
			}),
		).rejects.toThrow(
			ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_A_ROLE,
		);

		// Admin should be able to update (has ac:update)
		const adminUpdate = await auth.api.updateOrgRole({
			body: {
				roleId: ownerOnlyRole.data.roleData.id,
				data: {
					roleName: `admin-updated-${ownerOnlyRole.data.roleData.role}`,
				},
			},
			headers: adminHeaders,
		});
		expect(adminUpdate).toBeDefined();
		expect(adminUpdate.roleData.role).toContain("admin-updated");
	});

	it("should not allow cross-organization privilege escalation", async () => {
		// Create a fresh member for this test to avoid role contamination
		const {
			headers: freshMemberHeaders,
			user: freshMemberUser,
			member: freshMember,
		} = await createUser({
			role: "member",
		});

		// Create a second organization
		const org2 = await authClient.organization.create(
			{
				name: "second-org",
				slug: `second-org-${crypto.randomUUID()}`,
			},
			{
				onSuccess: sessionSetter(headers),
				headers,
			},
		);
		if (!org2.data) throw new Error("Second organization not created");

		// Try to list roles from org1 while active in org2 - should fail
		await authClient.organization.setActive({
			organizationId: org2.data.id,
			fetchOptions: {
				headers: freshMemberHeaders,
			},
		});

		// This should fail because the member is not in org2
		await expect(
			auth.api.listOrgRoles({
				query: { organizationId: org2.data.id },
				headers: freshMemberHeaders,
			}),
		).rejects.toThrow("You are not a member of this organization");

		// Switch back to org1
		await authClient.organization.setActive({
			organizationId: org.data?.id,
			fetchOptions: {
				headers: freshMemberHeaders,
			},
		});
	});
});

```

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

```typescript
import { createAuthEndpoint } from "@better-auth/core/api";
import { APIError } from "better-call";
import * as z from "zod";
import { getSessionFromCtx, requestOnlySessionMiddleware } from "../../../api";
import { setSessionCookie } from "../../../cookies";
import {
	type InferAdditionalFieldsFromPluginOptions,
	toZodSchema,
} from "../../../db";
import { getOrgAdapter } from "../adapter";
import { orgMiddleware, orgSessionMiddleware } from "../call";
import { ORGANIZATION_ERROR_CODES } from "../error-codes";
import { hasPermission } from "../has-permission";
import type {
	InferInvitation,
	InferMember,
	InferOrganization,
	Member,
	Team,
	TeamMember,
} from "../schema";
import type { OrganizationOptions } from "../types";

export const createOrganization = <O extends OrganizationOptions>(
	options?: O,
) => {
	const additionalFieldsSchema = toZodSchema({
		fields: options?.schema?.organization?.additionalFields || {},
		isClientSide: true,
	});
	const baseSchema = z.object({
		name: z.string().min(1).meta({
			description: "The name of the organization",
		}),
		slug: z.string().min(1).meta({
			description: "The slug of the organization",
		}),
		userId: z.coerce
			.string()
			.meta({
				description:
					'The user id of the organization creator. If not provided, the current user will be used. Should only be used by admins or when called by the server. server-only. Eg: "user-id"',
			})
			.optional(),
		logo: z
			.string()
			.meta({
				description: "The logo of the organization",
			})
			.optional(),
		metadata: z
			.record(z.string(), z.any())
			.meta({
				description: "The metadata of the organization",
			})
			.optional(),
		keepCurrentActiveOrganization: z
			.boolean()
			.meta({
				description:
					"Whether to keep the current active organization active after creating a new one. Eg: true",
			})
			.optional(),
	});

	type Body = InferAdditionalFieldsFromPluginOptions<"organization", O> &
		z.infer<typeof baseSchema>;

	return createAuthEndpoint(
		"/organization/create",
		{
			method: "POST",
			body: z.object({
				...baseSchema.shape,
				...additionalFieldsSchema.shape,
			}),
			use: [orgMiddleware],
			metadata: {
				$Infer: {
					body: {} as Body,
				},
				openapi: {
					description: "Create an organization",
					responses: {
						"200": {
							description: "Success",
							content: {
								"application/json": {
									schema: {
										type: "object",
										description: "The organization that was created",
										$ref: "#/components/schemas/Organization",
									},
								},
							},
						},
					},
				},
			},
		},
		async (ctx) => {
			const session = await getSessionFromCtx(ctx);

			if (!session && (ctx.request || ctx.headers)) {
				throw new APIError("UNAUTHORIZED");
			}
			let user = session?.user || null;
			if (!user) {
				if (!ctx.body.userId) {
					throw new APIError("UNAUTHORIZED");
				}
				user = await ctx.context.internalAdapter.findUserById(ctx.body.userId);
			}
			if (!user) {
				return ctx.json(null, {
					status: 401,
				});
			}
			const options = ctx.context.orgOptions;
			const canCreateOrg =
				typeof options?.allowUserToCreateOrganization === "function"
					? await options.allowUserToCreateOrganization(user)
					: options?.allowUserToCreateOrganization === undefined
						? true
						: options.allowUserToCreateOrganization;

			if (!canCreateOrg) {
				throw new APIError("FORBIDDEN", {
					message:
						ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION,
				});
			}
			const adapter = getOrgAdapter<O>(ctx.context, options as O);

			const userOrganizations = await adapter.listOrganizations(user.id);
			const hasReachedOrgLimit =
				typeof options.organizationLimit === "number"
					? userOrganizations.length >= options.organizationLimit
					: typeof options.organizationLimit === "function"
						? await options.organizationLimit(user)
						: false;

			if (hasReachedOrgLimit) {
				throw new APIError("FORBIDDEN", {
					message:
						ORGANIZATION_ERROR_CODES.YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS,
				});
			}

			const existingOrganization = await adapter.findOrganizationBySlug(
				ctx.body.slug,
			);
			if (existingOrganization) {
				throw new APIError("BAD_REQUEST", {
					message: ORGANIZATION_ERROR_CODES.ORGANIZATION_ALREADY_EXISTS,
				});
			}

			let {
				keepCurrentActiveOrganization: _,
				userId: __,
				...orgData
			} = ctx.body;

			if (options.organizationCreation?.beforeCreate) {
				const response = await options.organizationCreation.beforeCreate(
					{
						organization: {
							...orgData,
							createdAt: new Date(),
						},
						user,
					},
					ctx.request,
				);
				if (response && typeof response === "object" && "data" in response) {
					orgData = {
						...ctx.body,
						...response.data,
					};
				}
			}

			if (options?.organizationHooks?.beforeCreateOrganization) {
				const response =
					await options?.organizationHooks.beforeCreateOrganization({
						organization: orgData,
						user,
					});
				if (response && typeof response === "object" && "data" in response) {
					orgData = {
						...ctx.body,
						...response.data,
					};
				}
			}

			const organization = await adapter.createOrganization({
				organization: {
					...orgData,
					createdAt: new Date(),
				},
			});

			let member:
				| (Member & InferAdditionalFieldsFromPluginOptions<"member", O, false>)
				| undefined;
			let teamMember: TeamMember | null = null;
			let data = {
				userId: user.id,
				organizationId: organization.id,
				role: ctx.context.orgOptions.creatorRole || "owner",
			};
			if (options?.organizationHooks?.beforeAddMember) {
				const response = await options?.organizationHooks.beforeAddMember({
					member: {
						userId: user.id,
						organizationId: organization.id,
						role: ctx.context.orgOptions.creatorRole || "owner",
					},
					user,
					organization,
				});
				if (response && typeof response === "object" && "data" in response) {
					data = {
						...data,
						...response.data,
					};
				}
			}
			member = await adapter.createMember(data);
			if (options?.organizationHooks?.afterAddMember) {
				await options?.organizationHooks.afterAddMember({
					member,
					user,
					organization,
				});
			}
			if (
				options?.teams?.enabled &&
				options.teams.defaultTeam?.enabled !== false
			) {
				let teamData = {
					organizationId: organization.id,
					name: `${organization.name}`,
					createdAt: new Date(),
				};
				if (options?.organizationHooks?.beforeCreateTeam) {
					const response = await options?.organizationHooks.beforeCreateTeam({
						team: {
							organizationId: organization.id,
							name: `${organization.name}`,
						},
						user,
						organization,
					});
					if (response && typeof response === "object" && "data" in response) {
						teamData = {
							...teamData,
							...response.data,
						};
					}
				}
				const defaultTeam =
					(await options.teams.defaultTeam?.customCreateDefaultTeam?.(
						organization,
						ctx.request,
					)) || (await adapter.createTeam(teamData));

				teamMember = await adapter.findOrCreateTeamMember({
					teamId: defaultTeam.id,
					userId: user.id,
				});

				if (options?.organizationHooks?.afterCreateTeam) {
					await options?.organizationHooks.afterCreateTeam({
						team: defaultTeam,
						user,
						organization,
					});
				}
			}

			if (options.organizationCreation?.afterCreate) {
				await options.organizationCreation.afterCreate(
					{
						organization,
						user,
						member,
					},
					ctx.request,
				);
			}

			if (options?.organizationHooks?.afterCreateOrganization) {
				await options?.organizationHooks.afterCreateOrganization({
					organization,
					user,
					member,
				});
			}

			if (ctx.context.session && !ctx.body.keepCurrentActiveOrganization) {
				await adapter.setActiveOrganization(
					ctx.context.session.session.token,
					organization.id,
					ctx,
				);
			}

			if (
				teamMember &&
				ctx.context.session &&
				!ctx.body.keepCurrentActiveOrganization
			) {
				await adapter.setActiveTeam(
					ctx.context.session.session.token,
					teamMember.teamId,
					ctx,
				);
			}

			return ctx.json({
				...organization,
				metadata:
					organization.metadata && typeof organization.metadata === "string"
						? JSON.parse(organization.metadata)
						: organization.metadata,
				members: [member],
			});
		},
	);
};

export const checkOrganizationSlug = <O extends OrganizationOptions>(
	options: O,
) =>
	createAuthEndpoint(
		"/organization/check-slug",
		{
			method: "POST",
			body: z.object({
				slug: z.string().meta({
					description: 'The organization slug to check. Eg: "my-org"',
				}),
			}),
			use: [requestOnlySessionMiddleware, orgMiddleware],
		},
		async (ctx) => {
			const orgAdapter = getOrgAdapter<O>(ctx.context, options);
			const org = await orgAdapter.findOrganizationBySlug(ctx.body.slug);
			if (!org) {
				return ctx.json({
					status: true,
				});
			}
			throw new APIError("BAD_REQUEST", {
				message: "slug is taken",
			});
		},
	);

export const updateOrganization = <O extends OrganizationOptions>(
	options?: O,
) => {
	const additionalFieldsSchema = toZodSchema({
		fields: options?.schema?.organization?.additionalFields || {},
		isClientSide: true,
	});
	type Body = {
		data: {
			name?: string;
			slug?: string;
			logo?: string;
			metadata?: Record<string, any>;
		} & Partial<InferAdditionalFieldsFromPluginOptions<"organization", O>>;
		organizationId?: string | undefined;
	};
	return createAuthEndpoint(
		"/organization/update",
		{
			method: "POST",
			body: z.object({
				data: z
					.object({
						...additionalFieldsSchema.shape,
						name: z
							.string()
							.min(1)
							.meta({
								description: "The name of the organization",
							})
							.optional(),
						slug: z
							.string()
							.min(1)
							.meta({
								description: "The slug of the organization",
							})
							.optional(),
						logo: z
							.string()
							.meta({
								description: "The logo of the organization",
							})
							.optional(),
						metadata: z
							.record(z.string(), z.any())
							.meta({
								description: "The metadata of the organization",
							})
							.optional(),
					})
					.partial(),
				organizationId: z
					.string()
					.meta({
						description: 'The organization ID. Eg: "org-id"',
					})
					.optional(),
			}),
			requireHeaders: true,
			use: [orgMiddleware],
			metadata: {
				$Infer: {
					body: {} as Body,
				},
				openapi: {
					description: "Update an organization",
					responses: {
						"200": {
							description: "Success",
							content: {
								"application/json": {
									schema: {
										type: "object",
										description: "The updated organization",
										$ref: "#/components/schemas/Organization",
									},
								},
							},
						},
					},
				},
			},
		},
		async (ctx) => {
			const session = await ctx.context.getSession(ctx);
			if (!session) {
				throw new APIError("UNAUTHORIZED", {
					message: "User not found",
				});
			}
			const organizationId =
				ctx.body.organizationId || session.session.activeOrganizationId;
			if (!organizationId) {
				throw new APIError("BAD_REQUEST", {
					message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
				});
			}
			const adapter = getOrgAdapter<O>(ctx.context, options);
			const member = await adapter.findMemberByOrgId({
				userId: session.user.id,
				organizationId: organizationId,
			});
			if (!member) {
				throw new APIError("BAD_REQUEST", {
					message:
						ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
				});
			}
			const canUpdateOrg = await hasPermission(
				{
					permissions: {
						organization: ["update"],
					},
					role: member.role,
					options: ctx.context.orgOptions,
					organizationId,
				},
				ctx,
			);
			if (!canUpdateOrg) {
				throw new APIError("FORBIDDEN", {
					message:
						ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION,
				});
			}
			// Check if slug is being updated and validate uniqueness
			if (typeof ctx.body.data.slug === "string") {
				const existingOrganization = await adapter.findOrganizationBySlug(
					ctx.body.data.slug,
				);
				if (
					existingOrganization &&
					existingOrganization.id !== organizationId
				) {
					throw new APIError("BAD_REQUEST", {
						message: ORGANIZATION_ERROR_CODES.ORGANIZATION_SLUG_ALREADY_TAKEN,
					});
				}
			}
			if (options?.organizationHooks?.beforeUpdateOrganization) {
				const response =
					await options.organizationHooks.beforeUpdateOrganization({
						organization: ctx.body.data,
						user: session.user,
						member,
					});
				if (response && typeof response === "object" && "data" in response) {
					ctx.body.data = {
						...ctx.body.data,
						...response.data,
					};
				}
			}
			const updatedOrg = await adapter.updateOrganization(
				organizationId,
				ctx.body.data,
			);
			if (options?.organizationHooks?.afterUpdateOrganization) {
				await options.organizationHooks.afterUpdateOrganization({
					organization: updatedOrg,
					user: session.user,
					member,
				});
			}
			return ctx.json(updatedOrg);
		},
	);
};

export const deleteOrganization = <O extends OrganizationOptions>(
	options: O,
) => {
	return createAuthEndpoint(
		"/organization/delete",
		{
			method: "POST",
			body: z.object({
				organizationId: z.string().meta({
					description: "The organization id to delete",
				}),
			}),
			requireHeaders: true,
			use: [orgMiddleware],
			metadata: {
				openapi: {
					description: "Delete an organization",
					responses: {
						"200": {
							description: "Success",
							content: {
								"application/json": {
									schema: {
										type: "string",
										description: "The organization id that was deleted",
									},
								},
							},
						},
					},
				},
			},
		},
		async (ctx) => {
			const disableOrganizationDeletion =
				ctx.context.orgOptions.organizationDeletion?.disabled ||
				ctx.context.orgOptions.disableOrganizationDeletion;
			if (disableOrganizationDeletion) {
				if (ctx.context.orgOptions.organizationDeletion?.disabled) {
					ctx.context.logger.info(
						"`organizationDeletion.disabled` is deprecated. Use `disableOrganizationDeletion` instead",
					);
				}
				throw new APIError("NOT_FOUND", {
					message: "Organization deletion is disabled",
				});
			}
			const session = await ctx.context.getSession(ctx);
			if (!session) {
				throw new APIError("UNAUTHORIZED", { status: 401 });
			}

			const organizationId = ctx.body.organizationId;
			if (!organizationId) {
				return ctx.json(null, {
					status: 400,
					body: {
						message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
					},
				});
			}
			const adapter = getOrgAdapter<O>(ctx.context, options);
			const member = await adapter.findMemberByOrgId({
				userId: session.user.id,
				organizationId: organizationId,
			});
			if (!member) {
				throw new APIError("BAD_REQUEST", {
					message:
						ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
				});
			}
			const canDeleteOrg = await hasPermission(
				{
					role: member.role,
					permissions: {
						organization: ["delete"],
					},
					organizationId,
					options: ctx.context.orgOptions,
				},
				ctx,
			);
			if (!canDeleteOrg) {
				throw new APIError("FORBIDDEN", {
					message:
						ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION,
				});
			}
			if (organizationId === session.session.activeOrganizationId) {
				/**
				 * If the organization is deleted, we set the active organization to null
				 */
				await adapter.setActiveOrganization(session.session.token, null, ctx);
			}

			const org = await adapter.findOrganizationById(organizationId);
			if (!org) {
				throw new APIError("BAD_REQUEST");
			}
			if (options?.organizationHooks?.beforeDeleteOrganization) {
				await options.organizationHooks.beforeDeleteOrganization({
					organization: org,
					user: session.user,
				});
			}
			await adapter.deleteOrganization(organizationId);
			if (options?.organizationHooks?.afterDeleteOrganization) {
				await options.organizationHooks.afterDeleteOrganization({
					organization: org,
					user: session.user,
				});
			}
			return ctx.json(org);
		},
	);
};
export const getFullOrganization = <O extends OrganizationOptions>(
	options: O,
) =>
	createAuthEndpoint(
		"/organization/get-full-organization",
		{
			method: "GET",
			query: z.optional(
				z.object({
					organizationId: z
						.string()
						.meta({
							description: "The organization id to get",
						})
						.optional(),
					organizationSlug: z
						.string()
						.meta({
							description: "The organization slug to get",
						})
						.optional(),
					membersLimit: z
						.number()
						.or(z.string().transform((val) => parseInt(val)))
						.meta({
							description:
								"The limit of members to get. By default, it uses the membershipLimit option which defaults to 100.",
						})
						.optional(),
				}),
			),
			requireHeaders: true,
			use: [orgMiddleware, orgSessionMiddleware],
			metadata: {
				openapi: {
					description: "Get the full organization",
					responses: {
						"200": {
							description: "Success",
							content: {
								"application/json": {
									schema: {
										type: "object",
										description: "The organization",
										$ref: "#/components/schemas/Organization",
									},
								},
							},
						},
					},
				},
			},
		},
		async (ctx) => {
			const session = ctx.context.session;
			const organizationId =
				ctx.query?.organizationSlug ||
				ctx.query?.organizationId ||
				session.session.activeOrganizationId;
			// return null if no organization is found to avoid erroring since this is a usual scenario
			if (!organizationId) {
				return ctx.json(null, {
					status: 200,
				});
			}
			const adapter = getOrgAdapter<O>(ctx.context, options);
			const organization = await adapter.findFullOrganization({
				organizationId,
				isSlug: !!ctx.query?.organizationSlug,
				includeTeams: ctx.context.orgOptions.teams?.enabled,
				membersLimit: ctx.query?.membersLimit,
			});
			if (!organization) {
				throw new APIError("BAD_REQUEST", {
					message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
				});
			}
			const isMember = await adapter.checkMembership({
				userId: session.user.id,
				organizationId: organization.id,
			});
			if (!isMember) {
				await adapter.setActiveOrganization(session.session.token, null, ctx);
				throw new APIError("FORBIDDEN", {
					message:
						ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
				});
			}
			type OrganizationReturn = O["teams"] extends { enabled: true }
				? {
						members: InferMember<O>[];
						invitations: InferInvitation<O>[];
						teams: Team[];
					} & InferOrganization<O>
				: {
						members: InferMember<O>[];
						invitations: InferInvitation<O>[];
					} & InferOrganization<O>;
			return ctx.json(organization as unknown as OrganizationReturn);
		},
	);

export const setActiveOrganization = <O extends OrganizationOptions>(
	options: O,
) => {
	return createAuthEndpoint(
		"/organization/set-active",
		{
			method: "POST",
			body: z.object({
				organizationId: z
					.string()
					.meta({
						description:
							'The organization id to set as active. It can be null to unset the active organization. Eg: "org-id"',
					})
					.nullable()
					.optional(),
				organizationSlug: z
					.string()
					.meta({
						description:
							'The organization slug to set as active. It can be null to unset the active organization if organizationId is not provided. Eg: "org-slug"',
					})
					.optional(),
			}),
			use: [orgSessionMiddleware, orgMiddleware],
			metadata: {
				openapi: {
					description: "Set the active organization",
					responses: {
						"200": {
							description: "Success",
							content: {
								"application/json": {
									schema: {
										type: "object",
										description: "The organization",
										$ref: "#/components/schemas/Organization",
									},
								},
							},
						},
					},
				},
			},
		},
		async (ctx) => {
			const adapter = getOrgAdapter<O>(ctx.context, options);
			const session = ctx.context.session;
			let organizationId = ctx.body.organizationId;
			let organizationSlug = ctx.body.organizationSlug;

			if (organizationId === null) {
				const sessionOrgId = session.session.activeOrganizationId;
				if (!sessionOrgId) {
					return ctx.json(null);
				}
				const updatedSession = await adapter.setActiveOrganization(
					session.session.token,
					null,
					ctx,
				);
				await setSessionCookie(ctx, {
					session: updatedSession,
					user: session.user,
				});
				return ctx.json(null);
			}

			if (!organizationId && !organizationSlug) {
				const sessionOrgId = session.session.activeOrganizationId;
				if (!sessionOrgId) {
					return ctx.json(null);
				}
				organizationId = sessionOrgId;
			}

			if (organizationSlug && !organizationId) {
				const organization =
					await adapter.findOrganizationBySlug(organizationSlug);
				if (!organization) {
					throw new APIError("BAD_REQUEST", {
						message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
					});
				}
				organizationId = organization.id;
			}

			if (!organizationId) {
				throw new APIError("BAD_REQUEST", {
					message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
				});
			}

			const isMember = await adapter.checkMembership({
				userId: session.user.id,
				organizationId,
			});
			if (!isMember) {
				await adapter.setActiveOrganization(session.session.token, null, ctx);
				throw new APIError("FORBIDDEN", {
					message:
						ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
				});
			}

			let organization = await adapter.findOrganizationById(organizationId);
			if (!organization) {
				throw new APIError("BAD_REQUEST", {
					message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
				});
			}
			const updatedSession = await adapter.setActiveOrganization(
				session.session.token,
				organization.id,
				ctx,
			);
			await setSessionCookie(ctx, {
				session: updatedSession,
				user: session.user,
			});
			type OrganizationReturn = O["teams"] extends { enabled: true }
				? {
						members: InferMember<O>[];
						invitations: InferInvitation<O>[];
						teams: Team[];
					} & InferOrganization<O>
				: {
						members: InferMember<O>[];
						invitations: InferInvitation<O>[];
					} & InferOrganization<O>;
			return ctx.json(organization as unknown as OrganizationReturn);
		},
	);
};

export const listOrganizations = <O extends OrganizationOptions>(options: O) =>
	createAuthEndpoint(
		"/organization/list",
		{
			method: "GET",
			use: [orgMiddleware, orgSessionMiddleware],
			metadata: {
				openapi: {
					description: "List all organizations",
					responses: {
						"200": {
							description: "Success",
							content: {
								"application/json": {
									schema: {
										type: "array",
										items: {
											$ref: "#/components/schemas/Organization",
										},
									},
								},
							},
						},
					},
				},
			},
		},
		async (ctx) => {
			const adapter = getOrgAdapter<O>(ctx.context, options);
			const organizations = await adapter.listOrganizations(
				ctx.context.session.user.id,
			);
			return ctx.json(organizations);
		},
	);

```

--------------------------------------------------------------------------------
/docs/components/builder/index.tsx:
--------------------------------------------------------------------------------

```typescript
import { useAtom } from "jotai";
import { Moon, PlusIcon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { useState } from "react";
import { cn } from "@/lib/utils";
import {
	Card,
	CardContent,
	CardFooter,
	CardHeader,
	CardTitle,
} from "../ui/card";
import {
	Dialog,
	DialogContent,
	DialogDescription,
	DialogHeader,
	DialogTitle,
	DialogTrigger,
} from "../ui/dialog";
import { Label } from "../ui/label";
import { ScrollArea } from "../ui/scroll-area";
import { Separator } from "../ui/separator";
import { Switch } from "../ui/switch";
import CodeTabs from "./code-tabs";
import SignIn from "./sign-in";
import { SignUp } from "./sign-up";
import { socialProviders } from "./social-provider";
import { optionsAtom } from "./store";
import { AuthTabs } from "./tabs";

const frameworks = [
	{
		title: "Next.js",
		description: "The React Framework for Production",
		Icon: () => (
			<svg
				xmlns="http://www.w3.org/2000/svg"
				width="2em"
				height="2em"
				viewBox="0 0 15 15"
			>
				<path
					fill="currentColor"
					fillRule="evenodd"
					d="M0 7.5a7.5 7.5 0 1 1 11.698 6.216L4.906 4.21A.5.5 0 0 0 4 4.5V12h1V6.06l5.83 8.162A7.5 7.5 0 0 1 0 7.5M10 10V4h1v6z"
					clipRule="evenodd"
				></path>
			</svg>
		),
	},
	{
		title: "Nuxt",
		description: "The Intuitive Vue Framework",
		Icon: () => (
			<svg
				xmlns="http://www.w3.org/2000/svg"
				width="2em"
				height="2em"
				viewBox="0 0 256 256"
			>
				<g fill="none">
					<rect width="256" height="256" fill="#242938" rx="60"></rect>
					<path
						fill="#00DC82"
						d="M138.787 189.333h68.772c2.184.001 4.33-.569 6.222-1.652a12.4 12.4 0 0 0 4.554-4.515a12.24 12.24 0 0 0-.006-12.332l-46.185-79.286a12.4 12.4 0 0 0-4.553-4.514a12.53 12.53 0 0 0-12.442 0a12.4 12.4 0 0 0-4.553 4.514l-11.809 20.287l-23.09-39.67a12.4 12.4 0 0 0-4.555-4.513a12.54 12.54 0 0 0-12.444 0a12.4 12.4 0 0 0-4.555 4.513L36.67 170.834a12.24 12.24 0 0 0-.005 12.332a12.4 12.4 0 0 0 4.554 4.515a12.5 12.5 0 0 0 6.222 1.652h43.17c17.104 0 29.718-7.446 38.397-21.973l21.072-36.169l11.287-19.356l33.873 58.142h-45.16zm-48.88-19.376l-30.127-.007l45.16-77.518l22.533 38.759l-15.087 25.906c-5.764 9.426-12.312 12.86-22.48 12.86"
					></path>
				</g>
			</svg>
		),
	},
	{
		title: "SvelteKit",
		description: "Web development for the rest of us",
		Icon: () => (
			<svg
				xmlns="http://www.w3.org/2000/svg"
				width="2em"
				height="2em"
				viewBox="0 0 256 256"
			>
				<g fill="none">
					<rect width="256" height="256" fill="#FF3E00" rx="60"></rect>
					<g clipPath="url(#skillIconsSvelte0)">
						<path
							fill="#fff"
							d="M193.034 61.797c-16.627-23.95-49.729-30.966-73.525-15.865L77.559 72.78c-11.44 7.17-19.372 18.915-21.66 32.186c-1.984 11.136-.306 22.576 5.033 32.492c-3.66 5.491-6.102 11.593-7.17 18c-2.44 13.576.764 27.61 8.696 38.745c16.78 23.95 49.728 30.966 73.525 15.865l41.949-26.695c11.441-7.17 19.373-18.915 21.661-32.187c1.983-11.135.305-22.576-5.034-32.491c3.661-5.492 6.102-11.593 7.17-18c2.593-13.729-.61-27.763-8.695-38.898"
						></path>
						<path
							fill="#FF3E00"
							d="M115.39 196.491a33.25 33.25 0 0 1-35.695-13.271c-4.881-6.712-6.712-15.101-5.34-23.339c.306-1.373.611-2.593.916-3.966l.763-2.44L78.169 155a55.6 55.6 0 0 0 16.475 8.237l1.525.458l-.152 1.525c-.153 2.136.458 4.424 1.678 6.255c2.441 3.508 6.712 5.186 10.83 4.118c.916-.305 1.831-.61 2.594-1.068l41.796-26.695c2.136-1.372 3.509-3.355 3.966-5.796s-.152-5.034-1.525-7.017c-2.441-3.509-6.712-5.034-10.831-3.966c-.915.305-1.83.61-2.593 1.068l-16.017 10.22c-2.593 1.678-5.491 2.898-8.542 3.661a33.25 33.25 0 0 1-35.695-13.271c-4.729-6.712-6.712-15.102-5.186-23.339c1.372-7.932 6.254-15.102 13.118-19.373l41.949-26.695c2.593-1.678 5.492-2.898 8.543-3.814a33.25 33.25 0 0 1 35.695 13.272c4.881 6.712 6.711 15.101 5.339 23.339c-.306 1.373-.611 2.593-1.068 3.966l-.763 2.44l-2.136-1.525a55.6 55.6 0 0 0-16.474-8.237l-1.526-.458l.153-1.525c.153-2.136-.458-4.424-1.678-6.255c-2.441-3.508-6.712-5.034-10.83-3.966c-.916.305-1.831.61-2.594 1.068l-41.796 26.695c-2.136 1.373-3.509 3.356-3.966 5.797s.152 5.034 1.525 7.017c2.441 3.508 6.712 5.033 10.831 3.966c.915-.305 1.83-.611 2.593-1.068l16.017-10.22c2.593-1.678 5.491-2.899 8.542-3.814a33.25 33.25 0 0 1 35.695 13.271c4.881 6.712 6.712 15.102 5.339 23.339c-1.373 7.932-6.254 15.102-13.119 19.373l-41.949 26.695c-2.593 1.678-5.491 2.898-8.542 3.813"
						></path>
					</g>
					<defs>
						<clipPath id="skillIconsSvelte0">
							<path fill="#fff" d="M53 38h149.644v180H53z"></path>
						</clipPath>
					</defs>
				</g>
			</svg>
		),
	},
	{
		title: "SolidStart",
		description: "Fine-grained reactivity goes fullstack",
		Icon: () => (
			<svg
				data-hk="00000010210"
				width="2em"
				height="2em"
				viewBox="0 0 500 500"
				fill="none"
				xmlns="http://www.w3.org/2000/svg"
				role="presentation"
			>
				<path
					d="M233.205 430.856L304.742 425.279C304.742 425.279 329.208 421.295 343.569 397.659L293.041 385.443L233.205 430.856Z"
					fill="url(#paint0_linear_1_2)"
				></path>
				<path
					d="M134.278 263.278C113.003 264.341 73.6443 268.059 73.6443 268.059L245.173 392.614L284.265 402.44L343.569 397.925L170.977 273.105C170.977 273.105 157.148 263.278 137.203 263.278C136.139 263.278 135.342 263.278 134.278 263.278Z"
					fill="url(#paint1_linear_1_2)"
				></path>
				<path
					d="M355.536 238.58L429.2 234.065C429.2 234.065 454.464 230.348 468.825 206.977L416.435 193.964L355.536 238.58Z"
					fill="url(#paint2_linear_1_2)"
				></path>
				<path
					d="M251.289 68.6128C229.217 69.4095 188.795 72.5964 188.795 72.5964L367.503 200.072L407.926 210.429L469.09 206.712L289.318 78.9702C289.318 78.9702 274.426 68.6128 253.417 68.6128C252.885 68.6128 252.087 68.6128 251.289 68.6128Z"
					fill="url(#paint3_linear_1_2)"
				></path>
				<path
					d="M31.0946 295.679C30.8287 295.945 30.8287 296.21 30.8287 296.475L77.8993 330.469L202.623 420.764C228.95 439.62 264.586 431.653 282.67 402.44L187.465 333.921L110.077 277.62C100.504 270.715 89.8663 267.528 79.2289 267.528C60.6134 267.528 42.2639 277.354 31.0946 295.679Z"
					fill="url(#paint4_linear_1_2)"
				></path>
				<path
					d="M147.043 99.9505C147.043 100.216 146.776 100.482 146.511 100.747L195.442 135.538L244.374 170.062L325.751 227.957C353.142 247.345 389.841 239.642 407.925 210.695L358.461 175.374L308.997 140.318L228.153 82.6881C218.047 75.5177 206.611 72.0652 195.442 72.0652C176.561 72.3308 158.212 81.8915 147.043 99.9505Z"
					fill="url(#paint5_linear_1_2)"
				></path>
				<path
					d="M112.471 139.255L175.497 208.305C178.423 212.289 181.614 216.006 185.337 219.193L308.199 354.105L369.364 350.387C387.448 321.439 380.002 282.135 352.611 262.748L271.234 204.852L222.568 170.328L173.636 135.538L112.471 139.255Z"
					fill="url(#paint6_linear_1_2)"
				></path>
				<path
					d="M111.939 140.052C94.1213 168.734 101.567 207.509 128.427 226.629L209.005 283.994L258.735 319.049L308.199 354.105C326.283 325.158 318.836 285.852 291.445 266.465L112.471 139.255C112.471 139.521 112.204 139.787 111.939 140.052Z"
					fill="url(#paint7_linear_1_2)"
				></path>
				<defs>
					<linearGradient
						id="paint0_linear_1_2"
						x1="359.728"
						y1="56.8062"
						x2="265.623"
						y2="521.28"
						gradientUnits="userSpaceOnUse"
					>
						<stop stopColor="#1593F5"></stop>
						<stop offset="1" stopColor="#0084CE"></stop>
					</linearGradient>
					<linearGradient
						id="paint1_linear_1_2"
						x1="350.496"
						y1="559.872"
						x2="-44.0802"
						y2="-73.2062"
						gradientUnits="userSpaceOnUse"
					>
						<stop stopColor="#1593F5"></stop>
						<stop offset="1" stopColor="#0084CE"></stop>
					</linearGradient>
					<linearGradient
						id="paint2_linear_1_2"
						x1="610.25"
						y1="570.526"
						x2="372.635"
						y2="144.034"
						gradientUnits="userSpaceOnUse"
					>
						<stop stopColor="white"></stop>
						<stop offset="1" stopColor="#15ABFF"></stop>
					</linearGradient>
					<linearGradient
						id="paint3_linear_1_2"
						x1="188.808"
						y1="-180.608"
						x2="390.515"
						y2="281.703"
						gradientUnits="userSpaceOnUse"
					>
						<stop stopColor="white"></stop>
						<stop offset="1" stopColor="#79CFFF"></stop>
					</linearGradient>
					<linearGradient
						id="paint4_linear_1_2"
						x1="415.84"
						y1="-4.74684"
						x2="95.1922"
						y2="439.83"
						gradientUnits="userSpaceOnUse"
					>
						<stop stopColor="#0057E5"></stop>
						<stop offset="1" stopColor="#0084CE"></stop>
					</linearGradient>
					<linearGradient
						id="paint5_linear_1_2"
						x1="343.141"
						y1="-21.5427"
						x2="242.301"
						y2="256.708"
						gradientUnits="userSpaceOnUse"
					>
						<stop stopColor="white"></stop>
						<stop offset="1" stopColor="#15ABFF"></stop>
					</linearGradient>
					<linearGradient
						id="paint6_linear_1_2"
						x1="469.095"
						y1="533.421"
						x2="-37.6939"
						y2="-135.731"
						gradientUnits="userSpaceOnUse"
					>
						<stop stopColor="white"></stop>
						<stop offset="1" stopColor="#79CFFF"></stop>
					</linearGradient>
					<linearGradient
						id="paint7_linear_1_2"
						x1="380.676"
						y1="-89.0869"
						x2="120.669"
						y2="424.902"
						gradientUnits="userSpaceOnUse"
					>
						<stop stopColor="white"></stop>
						<stop offset="1" stopColor="#79CFFF"></stop>
					</linearGradient>
				</defs>
			</svg>
		),
	},
];

export function Builder() {
	const [currentStep, setCurrentStep] = useState(0);

	const [options, setOptions] = useAtom(optionsAtom);
	const { setTheme, resolvedTheme } = useTheme();
	return (
		<Dialog>
			<DialogTrigger asChild>
				<button className="bg-stone-950 no-underline group cursor-pointer relative  p-px text-xs font-semibold leading-6  text-white md:inline-block hidden">
					<span className="absolute inset-0 overflow-hidden rounded-sm">
						<span className="absolute inset-0 rounded-sm bg-[image:radial-gradient(75%_100%_at_50%_0%,rgba(56,189,248,0.6)_0%,rgba(56,189,248,0)_75%)] opacity-0 transition-opacity duration-500 group-hover:opacity-100"></span>
					</span>
					<div className="relative flex space-x-2 items-center z-10 rounded-none bg-zinc-950 py-2 px-4 ring-1 ring-white/10 ">
						<PlusIcon size={14} />
						<span>Create Sign in Box</span>
					</div>
					<span className="absolute -bottom-0 left-[1.125rem] h-px w-[calc(100%-2.25rem)] bg-gradient-to-r from-emerald-400/0 via-stone-800/90 to-emerald-400/0 transition-opacity duration-500 group-hover:opacity-40"></span>
				</button>
			</DialogTrigger>
			<DialogContent className="max-w-7xl h-5/6 overflow-clip !rounded-none">
				<DialogHeader>
					<DialogTitle>Create Sign in Box</DialogTitle>
					<DialogDescription>
						Configure the sign in box to your liking and copy the code to your
						application.
					</DialogDescription>
				</DialogHeader>

				<div className="flex gap-4 md:gap-12 flex-col md:flex-row items-center md:items-start">
					<div className={cn("w-4/12")}>
						<div
							className="overflow-scroll h-[580px] relative"
							style={{
								scrollbarWidth: "none",
								scrollbarColor: "transparent transparent",
								//@ts-expect-error
								"&::-webkit-scrollbar": {
									display: "none",
								},
							}}
						>
							{options.signUp ? (
								<AuthTabs
									tabs={[
										{
											title: "Sign In",
											value: "sign-in",
											content: <SignIn />,
										},
										{
											title: "Sign Up",
											value: "sign-up",
											content: <SignUp />,
										},
									]}
								/>
							) : (
								<SignIn />
							)}
						</div>
					</div>
					<ScrollArea
						className="w-[45%] flex-grow"
						style={{
							scrollbarWidth: "none",
							scrollbarColor: "transparent transparent",
							//@ts-expect-error
							"&::-webkit-scrollbar": {
								display: "none",
							},
						}}
					>
						<div className="h-[580px]">
							{currentStep === 0 ? (
								<Card className="rounded-none flex-grow h-full">
									<CardHeader className="flex flex-row justify-between">
										<CardTitle>Configuration</CardTitle>
										<div
											className="cursor-pointer"
											onClick={() => {
												if (resolvedTheme === "dark") {
													setTheme("light");
												} else {
													setTheme("dark");
												}
											}}
										>
											{resolvedTheme === "dark" ? (
												<Moon onClick={() => setTheme("light")} size={18} />
											) : (
												<Sun onClick={() => setTheme("dark")} size={18} />
											)}
										</div>
									</CardHeader>
									<CardContent className="max-h-[400px] overflow-scroll">
										<div className="flex flex-col gap-2">
											<div>
												<Label>Email & Password</Label>
											</div>
											<Separator />
											<div className="flex items-center justify-between">
												<div className="flex items-center">
													<Label
														className="cursor-pointer"
														htmlFor="email-provider-email"
													>
														Enabled
													</Label>
												</div>
												<Switch
													id="email-provider-email"
													checked={options.email}
													onCheckedChange={(checked) => {
														setOptions((prev) => ({
															...prev,
															email: checked,
															magicLink: checked ? false : prev.magicLink,
															signUp: checked,
														}));
													}}
												/>
											</div>
											<div className="flex items-center justify-between">
												<div className="flex items-center gap-2">
													<Label
														className="cursor-pointer"
														htmlFor="email-provider-remember-me"
													>
														Remember Me
													</Label>
												</div>
												<Switch
													id="email-provider-remember-me"
													checked={options.rememberMe}
													onCheckedChange={(checked) => {
														setOptions((prev) => ({
															...prev,
															rememberMe: checked,
														}));
													}}
												/>
											</div>
											<div className="flex items-center justify-between">
												<div className="flex items-center gap-2">
													<Label
														className="cursor-pointer"
														htmlFor="email-provider-forget-password"
													>
														Forget Password
													</Label>
												</div>
												<Switch
													id="email-provider-forget-password"
													checked={options.requestPasswordReset}
													onCheckedChange={(checked) => {
														setOptions((prev) => ({
															...prev,
															requestPasswordReset: checked,
														}));
													}}
												/>
											</div>
										</div>
										<div className="flex flex-col gap-2 mt-4">
											<div>
												<Label>Social Providers</Label>
											</div>
											<Separator />
											{Object.entries(socialProviders).map(
												([provider, { Icon }]) => (
													<div
														className="flex items-center justify-between"
														key={provider}
													>
														<div className="flex items-center gap-2">
															<Icon />
															<Label
																className="cursor-pointer"
																htmlFor={"social-provider".concat(
																	"-",
																	provider,
																)}
															>
																{provider.charAt(0).toUpperCase() +
																	provider.slice(1)}
															</Label>
														</div>
														<Switch
															id={"social-provider".concat("-", provider)}
															checked={options.socialProviders.includes(
																provider,
															)}
															onCheckedChange={(checked) => {
																setOptions((prev) => ({
																	...prev,
																	socialProviders: checked
																		? [...prev.socialProviders, provider]
																		: prev.socialProviders.filter(
																				(p) => p !== provider,
																			),
																}));
															}}
														/>
													</div>
												),
											)}
										</div>
										<div className="flex flex-col gap-2 mt-4">
											<div>
												<Label>Plugins</Label>
											</div>
											<Separator />
											<div className="flex items-center justify-between">
												<div className="flex items-center gap-2">
													<svg
														xmlns="http://www.w3.org/2000/svg"
														width="1em"
														height="1em"
														viewBox="0 0 24 24"
													>
														<path
															fill="currentColor"
															d="M5 20q-.825 0-1.412-.587T3 18v-.8q0-.85.438-1.562T4.6 14.55q1.55-.775 3.15-1.162T11 13q.35 0 .7.013t.7.062q.275.025.437.213t.163.462q.05 1.175.575 2.213t1.4 1.762q.175.125.275.313t.1.412V19q0 .425-.288.713T14.35 20zm6-8q-1.65 0-2.825-1.175T7 8t1.175-2.825T11 4t2.825 1.175T15 8t-1.175 2.825T11 12m7.5 2q.425 0 .713-.288T19.5 13t-.288-.712T18.5 12t-.712.288T17.5 13t.288.713t.712.287m.15 8.65l-1-1q-.05-.05-.15-.35v-4.45q-1.1-.325-1.8-1.237T15 13.5q0-1.45 1.025-2.475T18.5 10t2.475 1.025T22 13.5q0 1.125-.638 2t-1.612 1.25l.9.9q.15.15.15.35t-.15.35l-.8.8q-.15.15-.15.35t.15.35l.8.8q.15.15.15.35t-.15.35l-1.3 1.3q-.15.15-.35.15t-.35-.15"
														></path>
													</svg>
													<Label
														className="cursor-pointer"
														htmlFor="plugin-passkey"
													>
														Passkey
													</Label>
												</div>
												<Switch
													id="plugin-passkey"
													checked={options.passkey}
													onCheckedChange={(checked) => {
														setOptions((prev) => ({
															...prev,
															passkey: checked,
														}));
													}}
												/>
											</div>

											<div className="flex items-center justify-between">
												<div className="flex items-center gap-2">
													<svg
														xmlns="http://www.w3.org/2000/svg"
														width="1em"
														height="1em"
														viewBox="0 0 24 24"
													>
														<g fill="none">
															<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path>
															<path
																fill="currentColor"
																d="M17.5 3a4.5 4.5 0 0 1 4.495 4.288L22 7.5V15a2 2 0 0 1-1.85 1.995L20 17h-3v3a1 1 0 0 1-1.993.117L15 20v-3H4a2 2 0 0 1-1.995-1.85L2 15V7.5a4.5 4.5 0 0 1 4.288-4.495L6.5 3zm-11 2A2.5 2.5 0 0 0 4 7.5V15h5V7.5A2.5 2.5 0 0 0 6.5 5M7 8a1 1 0 0 1 .117 1.993L7 10H6a1 1 0 0 1-.117-1.993L6 8z"
															></path>
														</g>
													</svg>
													<Label
														className="cursor-pointer"
														htmlFor="plugin-otp-magic-link"
													>
														Magic Link
													</Label>
												</div>
												<Switch
													id="plugin-otp-magic-link"
													checked={options.magicLink}
													onCheckedChange={(checked) => {
														setOptions((prev) => ({
															...prev,
															magicLink: checked,
															email: checked ? false : prev.email,
															signUp: checked ? false : prev.signUp,
														}));
													}}
												/>
											</div>
										</div>
										<div className="mt-4">
											<Separator />
											<div className="flex items-center justify-between mt-2">
												<Label
													className="cursor-pointer"
													htmlFor="label-powered-by"
												>
													Show Built with label
												</Label>
												<Switch
													id="label-powered-by"
													checked={options.label}
													onCheckedChange={(checked) => {
														setOptions((prev) => ({
															...prev,
															label: checked,
														}));
													}}
												/>
											</div>
										</div>
									</CardContent>
									<CardFooter>
										<button
											className="bg-stone-950 no-underline group cursor-pointer relative shadow-2xl shadow-zinc-900 rounded-sm p-px text-xs font-semibold leading-6  text-white inline-block w-full"
											onClick={() => {
												setCurrentStep(currentStep + 1);
											}}
										>
											<span className="absolute inset-0 overflow-hidden rounded-sm">
												<span className="absolute inset-0 rounded-sm bg-[image:radial-gradient(75%_100%_at_50%_0%,rgba(56,189,248,0.6)_0%,rgba(56,189,248,0)_75%)] opacity-0 transition-opacity duration-500 group-hover:opacity-100"></span>
											</span>
											<div className="relative flex space-x-2 items-center z-10 rounded-none bg-zinc-950 py-2 px-4 ring-1 ring-white/10 justify-center">
												<span>Continue</span>
											</div>
											<span className="absolute -bottom-0 left-[1.125rem] h-px w-[calc(100%-2.25rem)] bg-gradient-to-r from-emerald-400/0 via-stone-800/90 to-emerald-400/0 transition-opacity duration-500 group-hover:opacity-40"></span>
										</button>
									</CardFooter>
								</Card>
							) : currentStep === 1 ? (
								<Card className="rounded-none flex-grow  h-full">
									<CardHeader>
										<CardTitle>Choose Framework</CardTitle>
										<p
											className="text-blue-400 hover:underline mt-1 text-sm cursor-pointer"
											onClick={() => {
												setCurrentStep(0);
											}}
										>
											Go Back
										</p>
									</CardHeader>
									<CardContent className="flex items-start gap-2 flex-wrap justify-between">
										{frameworks.map((fm) => (
											<div
												onClick={() => {
													if (fm.title === "Next.js") {
														setCurrentStep(currentStep + 1);
													}
												}}
												className={cn(
													"flex flex-col items-center gap-4 border p-6 rounded-md w-5/12 flex-grow h-44 relative",
													fm.title !== "Next.js"
														? "opacity-55"
														: "hover:ring-1 transition-all ring-border hover:bg-background duration-200 ease-in-out cursor-pointer",
												)}
												key={fm.title}
											>
												{fm.title !== "Next.js" && (
													<span className="absolute top-4 right-4 text-xs">
														Coming Soon
													</span>
												)}
												<fm.Icon />
												<Label className="text-2xl">{fm.title}</Label>
												<p className="text-sm">{fm.description}</p>
											</div>
										))}
									</CardContent>
								</Card>
							) : (
								<Card className="rounded-none w-full overflow-y-hidden h-full overflow-auto">
									<CardHeader>
										<div className="flex flex-col -mb-2 items-start">
											<CardTitle>Code</CardTitle>
										</div>
									</CardHeader>
									<CardContent>
										<div className="flex gap-2 items-baseline">
											<p>
												Copy the code below and paste it in your application to
												get started.
											</p>
											<p
												className="text-blue-400 hover:underline mt-1 text-sm cursor-pointer"
												onClick={() => {
													setCurrentStep(0);
												}}
											>
												Go Back
											</p>
										</div>
										<div>
											<CodeTabs />
										</div>
									</CardContent>
								</Card>
							)}
						</div>
					</ScrollArea>
				</div>
			</DialogContent>
		</Dialog>
	);
}

```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/email-otp/email-otp.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, expect, it, vi } from "vitest";
import { createAuthClient } from "../../client";
import { getTestInstance } from "../../test-utils/test-instance";
import { bearer } from "../bearer";
import { emailOTP } from ".";
import { emailOTPClient } from "./client";
import { splitAtLastColon } from "./utils";

describe("email-otp", async () => {
	const otpFn = vi.fn();
	let otp = "";
	const { client, testUser, auth } = await getTestInstance(
		{
			plugins: [
				bearer(),
				emailOTP({
					async sendVerificationOTP({ email, otp: _otp, type }) {
						otp = _otp;
						otpFn(email, _otp, type);
					},
					sendVerificationOnSignUp: true,
				}),
			],
			emailVerification: {
				autoSignInAfterVerification: true,
			},
		},
		{
			clientOptions: {
				plugins: [emailOTPClient()],
			},
		},
	);

	it("should verify email with otp", async () => {
		const res = await client.emailOtp.sendVerificationOtp({
			email: testUser.email,
			type: "email-verification",
		});
		expect(res.data?.success).toBe(true);
		expect(otp.length).toBe(6);
		expect(otpFn).toHaveBeenCalledWith(
			testUser.email,
			otp,
			"email-verification",
		);
		const verifiedUser = await client.emailOtp.verifyEmail({
			email: testUser.email,
			otp,
		});
		expect(verifiedUser.data?.status).toBe(true);
	});

	it("should sign-in with otp", async () => {
		const res = await client.emailOtp.sendVerificationOtp({
			email: testUser.email,
			type: "sign-in",
		});
		expect(res.data?.success).toBe(true);
		expect(otp.length).toBe(6);
		expect(otpFn).toHaveBeenCalledWith(testUser.email, otp, "sign-in");
		const verifiedUser = await client.signIn.emailOtp(
			{
				email: testUser.email,
				otp,
			},
			{
				onSuccess: (ctx) => {
					const header = ctx.response.headers.get("set-cookie");
					expect(header).toContain("better-auth.session_token");
				},
			},
		);
		expect(verifiedUser.data?.token).toBeDefined();
	});

	it("should sign-up with otp", async () => {
		const testUser2 = {
			email: "[email protected]",
		};
		await client.emailOtp.sendVerificationOtp({
			email: testUser2.email,
			type: "sign-in",
		});
		const newUser = await client.signIn.emailOtp(
			{
				email: testUser2.email,
				otp,
			},
			{
				onSuccess: (ctx) => {
					const header = ctx.response.headers.get("set-cookie");
					expect(header).toContain("better-auth.session_token");
				},
			},
		);
		expect(newUser.data?.token).toBeDefined();
	});

	it("should send verification otp on sign-up", async () => {
		const testUser2 = {
			email: "[email protected]",
			password: "password",
			name: "test",
		};
		await client.signUp.email(testUser2);
		expect(otpFn).toHaveBeenCalledWith(
			testUser2.email,
			otp,
			"email-verification",
		);
	});

	it("should send forget password otp", async () => {
		await client.emailOtp.sendVerificationOtp({
			email: testUser.email,
			type: "forget-password",
		});
	});

	it("should reset password", async () => {
		await client.emailOtp.resetPassword({
			email: testUser.email,
			otp,
			password: "changed-password",
		});
		const { data } = await client.signIn.email({
			email: testUser.email,
			password: "changed-password",
		});
		expect(data?.user).toBeDefined();
	});

	it("should call onPasswordReset callback when resetting password", async () => {
		const onPasswordResetMock = vi.fn();
		const { client, testUser } = await getTestInstance(
			{
				plugins: [
					bearer(),
					emailOTP({
						async sendVerificationOTP({ email, otp: _otp, type }) {
							otp = _otp;
							otpFn(email, _otp, type);
						},
						sendVerificationOnSignUp: true,
					}),
				],
				emailAndPassword: {
					enabled: true,
					onPasswordReset: onPasswordResetMock,
				},
			},
			{
				clientOptions: {
					plugins: [emailOTPClient()],
				},
			},
		);

		await client.emailOtp.sendVerificationOtp({
			email: testUser.email,
			type: "forget-password",
		});

		await client.emailOtp.resetPassword({
			email: testUser.email,
			otp,
			password: "new-password",
		});

		expect(onPasswordResetMock).toHaveBeenCalledWith(
			{ user: expect.objectContaining({ email: testUser.email }) },
			expect.any(Object),
		);
	});

	it("should reset password and create credential account", async () => {
		const testUser2 = {
			email: "[email protected]",
		};
		await client.emailOtp.sendVerificationOtp({
			email: testUser2.email,
			type: "sign-in",
		});
		await client.signIn.emailOtp(
			{
				email: testUser2.email,
				otp,
			},
			{
				onSuccess: (ctx) => {
					const header = ctx.response.headers.get("set-cookie");
					expect(header).toContain("better-auth.session_token");
				},
			},
		);
		await client.emailOtp.sendVerificationOtp({
			email: testUser2.email,
			type: "forget-password",
		});
		await client.emailOtp.resetPassword({
			email: testUser2.email,
			otp,
			password: "password",
		});
		const res = await client.signIn.email({
			email: testUser2.email,
			password: "password",
		});
		expect(res.data?.token).toBeDefined();
	});

	it("should fail on invalid email", async () => {
		const res = await client.emailOtp.sendVerificationOtp({
			email: "invalid-email",
			type: "email-verification",
		});
		expect(res.error?.status).toBe(400);
		expect(res.error?.code).toBe("INVALID_EMAIL");
	});

	it("should fail on expired otp", async () => {
		await client.emailOtp.sendVerificationOtp({
			email: testUser.email,
			type: "email-verification",
		});
		vi.useFakeTimers();
		await vi.advanceTimersByTimeAsync(1000 * 60 * 6);
		const res = await client.emailOtp.verifyEmail({
			email: testUser.email,
			otp,
		});
		expect(res.error?.status).toBe(400);
		expect(res.error?.code).toBe("OTP_EXPIRED");
	});

	it("should not fail on time elapsed", async () => {
		await client.emailOtp.sendVerificationOtp({
			email: testUser.email,
			type: "email-verification",
		});
		vi.useFakeTimers();
		await vi.advanceTimersByTimeAsync(1000 * 60 * 4);
		const res = await client.emailOtp.verifyEmail({
			email: testUser.email,
			otp,
		});
		const session = await client.getSession({
			fetchOptions: {
				headers: {
					Authorization: `Bearer ${res.data?.token}`,
				},
			},
		});
		expect(res.data?.status).toBe(true);
		expect(session.data?.user.emailVerified).toBe(true);
	});

	it("should create verification otp on server", async () => {
		otp = await auth.api.createVerificationOTP({
			body: {
				type: "sign-in",
				email: "[email protected]",
			},
		});
		otp = await auth.api.createVerificationOTP({
			body: {
				type: "sign-in",
				email: "[email protected]",
			},
		});
		expect(otp.length).toBe(6);
	});

	it("should get verification otp on server", async () => {
		const res = await auth.api.getVerificationOTP({
			query: {
				email: "[email protected]",
				type: "sign-in",
			},
		});
	});

	it("should work with custom options", async () => {
		const { client, testUser, auth } = await getTestInstance(
			{
				plugins: [
					bearer(),
					emailOTP({
						async sendVerificationOTP({ email, otp: _otp, type }) {
							otp = _otp;
							otpFn(email, _otp, type);
						},
						sendVerificationOnSignUp: true,
						expiresIn: 10,
						otpLength: 8,
					}),
				],
				emailVerification: {
					autoSignInAfterVerification: true,
				},
			},
			{
				clientOptions: {
					plugins: [emailOTPClient()],
				},
			},
		);
		await client.emailOtp.sendVerificationOtp({
			type: "email-verification",
			email: testUser.email,
		});
		expect(otp.length).toBe(8);
		vi.useFakeTimers();
		await vi.advanceTimersByTimeAsync(11 * 1000);
		const verifyRes = await client.emailOtp.verifyEmail({
			email: testUser.email,
			otp,
		});
		expect(verifyRes.error?.code).toBe("OTP_EXPIRED");
	});
});

describe("email-otp-verify", async () => {
	const otpFn = vi.fn();
	const otp = [""];
	const { client, testUser, auth } = await getTestInstance(
		{
			plugins: [
				emailOTP({
					async sendVerificationOTP({ email, otp: _otp, type }) {
						otp.push(_otp);
						otpFn(email, _otp, type);
					},
					sendVerificationOnSignUp: true,
					disableSignUp: true,
				}),
			],
		},
		{
			clientOptions: {
				plugins: [emailOTPClient()],
			},
		},
	);

	it("should return USER_NOT_FOUND error when disableSignUp and user not registered", async () => {
		const response = await client.emailOtp.sendVerificationOtp({
			email: "[email protected]",
			type: "email-verification",
		});

		expect(response.error?.message).toBe("User not found");
		// Existing user should still succeed
		const successRes = await client.emailOtp.sendVerificationOtp({
			email: testUser.email,
			type: "email-verification",
		});
		expect(successRes.error).toBeFalsy();
	});

	it("should verify email with last otp", async () => {
		await client.emailOtp.sendVerificationOtp({
			email: testUser.email,
			type: "email-verification",
		});
		await client.emailOtp.sendVerificationOtp({
			email: testUser.email,
			type: "email-verification",
		});
		await client.emailOtp.sendVerificationOtp({
			email: testUser.email,
			type: "email-verification",
		});
	});

	it("should block after exceeding allowed attempts", async () => {
		await client.emailOtp.sendVerificationOtp({
			email: testUser.email,
			type: "email-verification",
		});

		for (let i = 0; i < 3; i++) {
			const res = await client.emailOtp.verifyEmail({
				email: testUser.email,
				otp: "wrong-otp",
			});
			expect(res.error?.status).toBe(400);
			expect(res.error?.message).toBe("Invalid OTP");
		}

		//Try one more time - should be blocked
		const res = await client.emailOtp.verifyEmail({
			email: testUser.email,
			otp: "000000",
		});
		expect(res.error?.status).toBe(403);
		expect(res.error?.message).toBe("Too many attempts");
	});

	it("should block reset password after exceeding allowed attempts", async () => {
		await client.emailOtp.sendVerificationOtp({
			email: testUser.email,
			type: "forget-password",
		});

		for (let i = 0; i < 3; i++) {
			const res = await client.emailOtp.resetPassword({
				email: testUser.email,
				otp: "wrong-otp",
				password: "new-password",
			});
			expect(res.error?.status).toBe(400);
			expect(res.error?.message).toBe("Invalid OTP");
		}

		// Try one more time - should be blocked
		const res = await client.emailOtp.resetPassword({
			email: testUser.email,
			otp: "000000",
			password: "new-password",
		});
		expect(res.error?.status).toBe(403);
		expect(res.error?.message).toBe("Too many attempts");
	});
});

describe("custom rate limiting storage", async () => {
	const { client, testUser } = await getTestInstance({
		rateLimit: {
			enabled: true,
		},
		plugins: [
			emailOTP({
				async sendVerificationOTP(data, request) {},
			}),
		],
	});

	it.each([
		{
			path: "/email-otp/send-verification-otp",
			body: {
				email: "[email protected]",
				type: "sign-in",
			},
		},
		{
			path: "/sign-in/email-otp",
			body: {
				email: "[email protected]",
				otp: "12312",
			},
		},
		{
			path: "/email-otp/verify-email",
			body: {
				email: "[email protected]",
				otp: "12312",
			},
		},
	])("should rate limit send verification endpoint", async ({ path, body }) => {
		for (let i = 0; i < 10; i++) {
			const response = await client.$fetch(path, {
				method: "POST",
				body,
			});
			if (i >= 3) {
				expect(response.error?.status).toBe(429);
			}
		}
		vi.useFakeTimers();
		await vi.advanceTimersByTimeAsync(60 * 1000);
		const response = await client.$fetch(path, {
			method: "POST",
			body,
		});
		expect(response.error?.status).not.toBe(429);
	});
});

describe("custom generate otpFn", async () => {
	const { client, testUser } = await getTestInstance(
		{
			plugins: [
				emailOTP({
					async sendVerificationOTP(data, request) {},
					generateOTP(data, request) {
						return "123456";
					},
				}),
			],
		},
		{
			clientOptions: {
				plugins: [emailOTPClient()],
			},
		},
	);

	it("should generate otp", async () => {
		const res = await client.emailOtp.sendVerificationOtp({
			email: testUser.email,
			type: "email-verification",
		});
		expect(res.data?.success).toBe(true);
	});

	it("should verify email with otp", async () => {
		const res = await client.emailOtp.verifyEmail({
			email: testUser.email,
			otp: "123456",
		});
		expect(res.data?.status).toBe(true);
	});
});

describe("custom storeOTP", async () => {
	// Testing hashed OTPs.
	describe("hashed", async () => {
		let sendVerificationOtpFn = async (data: {
			email: string;
			otp: string;
			type: "sign-in" | "email-verification" | "forget-password";
		}) => {};

		function getTheSentOTP() {
			let gotOtp: string | null = null;
			let sub = (otp: string) => {};
			sendVerificationOtpFn = async (data) => {
				gotOtp = data.otp;
				sub(data.otp);
			};
			return {
				get: () =>
					new Promise<string>((resolve) => {
						if (gotOtp) {
							resolve(gotOtp);
						} else {
							sub = (otp) => {
								gotOtp = otp;
								resolve(otp);
							};
						}
					}),
			};
		}

		const { client, testUser, auth } = await getTestInstance(
			{
				plugins: [
					emailOTP({
						sendVerificationOTP: async (d) => {
							await sendVerificationOtpFn(d);
						},
						storeOTP: "hashed",
					}),
				],
			},
			{
				clientOptions: {
					plugins: [emailOTPClient()],
				},
			},
		);
		const authCtx = await auth.$context;
		const userEmail1 = `${crypto.randomUUID()}@email.com`;

		let validOTP = "";

		it("should create a hashed otp", async () => {
			const { get } = getTheSentOTP();
			await client.emailOtp.sendVerificationOtp({
				email: userEmail1,
				type: "sign-in",
			});
			const verificationValue =
				await authCtx.internalAdapter.findVerificationValue(
					`sign-in-otp-${userEmail1}`,
				);

			const storedOtp = verificationValue?.value || "";
			const otp = await get();
			validOTP = otp;
			expect(storedOtp.length !== 0).toBe(true);
			expect(splitAtLastColon(storedOtp)[0]).not.toBe(otp);
			expect(storedOtp.endsWith(":0")).toBe(true);
		});

		it("should not be allowed to get otp if storeOTP is hashed", async () => {
			try {
				await auth.api.getVerificationOTP({
					query: {
						email: userEmail1,
						type: "sign-in",
					},
				});
			} catch (error: any) {
				expect(error.statusCode).toBe(400);
				expect(error.status).toBe("BAD_REQUEST");
				expect(error.body.code).toBe(
					"OTP_IS_HASHED_CANNOT_RETURN_THE_PLAIN_TEXT_OTP",
				);
				return;
			}
			// Should not reach here given the above should throw and thus return.
			expect(true).toBe(false);
		});

		it("should be able to sign in with normal otp", async () => {
			const res = await client.signIn.emailOtp({
				email: userEmail1,
				otp: validOTP,
			});
			expect(res.data?.user.email).toBe(userEmail1);
			expect(res.data?.token).toBeDefined();
		});
	});

	// Testing encrypted OTPs.
	describe("encrypted", async () => {
		let sendVerificationOtpFn = async (data: {
			email: string;
			otp: string;
			type: "sign-in" | "email-verification" | "forget-password";
		}) => {};

		function getTheSentOTP() {
			let gotOtp: string | null = null;
			let sub = (otp: string) => {};
			sendVerificationOtpFn = async (data) => {
				gotOtp = data.otp;
				sub(data.otp);
			};
			return {
				get: () =>
					new Promise<string>((resolve) => {
						if (gotOtp) {
							resolve(gotOtp);
						} else {
							sub = (otp) => {
								gotOtp = otp;
								resolve(otp);
							};
						}
					}),
			};
		}

		const { client, testUser, auth } = await getTestInstance(
			{
				plugins: [
					emailOTP({
						sendVerificationOTP: async (d) => {
							await sendVerificationOtpFn(d);
						},
						storeOTP: "encrypted",
					}),
				],
			},
			{
				clientOptions: {
					plugins: [emailOTPClient()],
				},
			},
		);
		const authCtx = await auth.$context;
		const userEmail1 = `${crypto.randomUUID()}@email.com`;

		let encryptedOtp = "";
		let validOTP = "";

		it("should create an encrypted otp", async () => {
			const { get } = getTheSentOTP();
			await client.emailOtp.sendVerificationOtp({
				email: userEmail1,
				type: "sign-in",
			});
			const verificationValue =
				await authCtx.internalAdapter.findVerificationValue(
					`sign-in-otp-${userEmail1}`,
				);

			const storedOtp = verificationValue?.value || "";
			const otp = await get();
			expect(storedOtp.length !== 0).toBe(true);
			expect(splitAtLastColon(storedOtp)[0]).not.toBe(otp);
			expect(storedOtp.endsWith(":0")).toBe(true);
			encryptedOtp = storedOtp;
			validOTP = otp;
		});

		it("should be allowed to get otp if storeOTP is encrypted", async () => {
			try {
				const res = await auth.api.getVerificationOTP({
					query: {
						email: userEmail1,
						type: "sign-in",
					},
				});
				if (!res.otp) {
					expect(true).toBe(false);
					return;
				}
				expect(res.otp).toEqual(validOTP);
				expect(res.otp.length).toBe(6);
			} catch (error: any) {
				expect(error).not.toBeDefined();
			}
		});

		it("should be able to sign in with encrypted otp", async () => {
			const res = await client.signIn.emailOtp({
				email: userEmail1,
				otp: validOTP,
			});
			expect(res.data?.user.email).toBe(userEmail1);
			expect(res.data?.token).toBeDefined();
		});
	});

	describe("custom encryptor", async () => {
		let sendVerificationOtpFn = async (data: {
			email: string;
			otp: string;
			type: "sign-in" | "email-verification" | "forget-password";
		}) => {};

		function getTheSentOTP() {
			let gotOtp: string | null = null;
			let sub = (otp: string) => {};
			sendVerificationOtpFn = async (data) => {
				gotOtp = data.otp;
				sub(data.otp);
			};
			return {
				get: () =>
					new Promise<string>((resolve) => {
						if (gotOtp) {
							resolve(gotOtp);
						} else {
							sub = (otp) => {
								gotOtp = otp;
								resolve(otp);
							};
						}
					}),
			};
		}

		const { client, testUser, auth } = await getTestInstance(
			{
				plugins: [
					emailOTP({
						sendVerificationOTP: async (d) => {
							await sendVerificationOtpFn(d);
						},
						storeOTP: {
							encrypt: async (otp) => {
								return otp + "encrypted";
							},
							decrypt: async (otp) => {
								return otp.replace("encrypted", "");
							},
						},
					}),
				],
			},
			{
				clientOptions: {
					plugins: [emailOTPClient()],
				},
			},
		);
		const authCtx = await auth.$context;

		let validOTP = "";
		let userEmail1 = `${crypto.randomUUID()}@email.com`;

		it("should create a custom encryptor otp", async () => {
			const { get } = getTheSentOTP();
			await client.emailOtp.sendVerificationOtp({
				email: userEmail1,
				type: "sign-in",
			});
			const verificationValue =
				await authCtx.internalAdapter.findVerificationValue(
					`sign-in-otp-${userEmail1}`,
				);
			const storedOtp = verificationValue?.value || "";
			const otp = await get();
			expect(storedOtp.length !== 0).toBe(true);
			expect(splitAtLastColon(storedOtp)[0]).not.toBe(otp);
			expect(storedOtp.endsWith(":0")).toBe(true);
			validOTP = otp;
		});

		it("should be allowed to get otp if storeOTP is custom encryptor", async () => {
			try {
				const res = await auth.api.getVerificationOTP({
					query: {
						email: userEmail1,
						type: "sign-in",
					},
				});
				if (!res.otp) {
					expect(true).toBe(false);
					return;
				}
				expect(res.otp).toEqual(validOTP);
				expect(res.otp.length).toBe(6);
			} catch (error: any) {
				console.error(error);
				expect(error).not.toBeDefined();
			}
		});

		it("should be able to sign in with custom encryptor otp", async () => {
			const res = await client.signIn.emailOtp({
				email: userEmail1,
				otp: validOTP,
			});
			expect(res.data?.user.email).toBe(userEmail1);
			expect(res.data?.token).toBeDefined();
		});
	});

	describe("custom hasher", async () => {
		let sendVerificationOtpFn = async (data: {
			email: string;
			otp: string;
			type: "sign-in" | "email-verification" | "forget-password";
		}) => {};

		function getTheSentOTP() {
			let gotOtp: string | null = null;
			let sub = (otp: string) => {};
			sendVerificationOtpFn = async (data) => {
				gotOtp = data.otp;
				sub(data.otp);
			};
			return {
				get: () =>
					new Promise<string>((resolve) => {
						if (gotOtp) {
							resolve(gotOtp);
						} else {
							sub = (otp) => {
								gotOtp = otp;
								resolve(otp);
							};
						}
					}),
			};
		}

		const { client, testUser, auth } = await getTestInstance(
			{
				plugins: [
					emailOTP({
						sendVerificationOTP: async (d) => {
							await sendVerificationOtpFn(d);
						},
						storeOTP: {
							hash: async (otp) => {
								return otp + "hashed";
							},
						},
					}),
				],
			},
			{
				clientOptions: {
					plugins: [emailOTPClient()],
				},
			},
		);
		const authCtx = await auth.$context;

		let validOTP = "";
		let userEmail1 = `${crypto.randomUUID()}@email.com`;

		it("should create a custom hasher otp", async () => {
			const { get } = getTheSentOTP();
			await client.emailOtp.sendVerificationOtp({
				email: userEmail1,
				type: "sign-in",
			});
			const verificationValue =
				await authCtx.internalAdapter.findVerificationValue(
					`sign-in-otp-${userEmail1}`,
				);
			const storedOtp = verificationValue?.value || "";
			const otp = await get();
			expect(storedOtp.length !== 0).toBe(true);
			expect(splitAtLastColon(storedOtp)[0]).not.toBe(otp);
			expect(storedOtp.endsWith(":0")).toBe(true);
			validOTP = otp;
		});

		it("should be allowed to get otp if storeOTP is custom hasher", async () => {
			try {
				const result = await auth.api.getVerificationOTP({
					query: {
						email: userEmail1,
						type: "sign-in",
					},
				});
			} catch (error: any) {
				expect(error.statusCode).toBe(400);
				expect(error.status).toBe("BAD_REQUEST");
				expect(error.body.code).toBe(
					"OTP_IS_HASHED_CANNOT_RETURN_THE_PLAIN_TEXT_OTP",
				);
				return;
			}
			// Should not reach here given the above should throw and thus return.
			expect(true).toBe(false);
		});

		it("should be able to sign in with custom hasher otp", async () => {
			const res = await client.signIn.emailOtp({
				email: userEmail1,
				otp: validOTP,
			});
			expect(res.data?.user.email).toBe(userEmail1);
			expect(res.data?.token).toBeDefined();
		});
	});
});

describe("override default email verification", async () => {
	let otp = "";
	const { cookieSetter, customFetchImpl } = await getTestInstance({
		emailAndPassword: {
			enabled: true,
		},
		emailVerification: {
			sendOnSignUp: true,
		},
		plugins: [
			emailOTP({
				async sendVerificationOTP(data, request) {
					otp = data.otp;
				},
				overrideDefaultEmailVerification: true,
			}),
		],
	});

	const client = createAuthClient({
		plugins: [emailOTPClient()],
		baseURL: "http://localhost:3000",
		fetchOptions: {
			customFetchImpl,
		},
	});

	const headers = new Headers();
	it("should send verification email on sign up", async () => {
		await client.signUp.email(
			{
				email: "[email protected]",
				password: "password",
				name: "Test User",
			},
			{
				onSuccess: cookieSetter(headers),
			},
		);
		expect(otp.length).toBe(6);
	});

	it("should verify email with otp", async () => {
		const res = await client.emailOtp.verifyEmail({
			email: "[email protected]",
			otp,
		});
		expect(res.data?.status).toBe(true);
		expect(res.data?.user.emailVerified).toBe(true);
	});

	it("should by default not override default email verification", async () => {
		const sendVerificationOTP = vi.fn();
		const { client } = await getTestInstance({
			emailAndPassword: {
				enabled: true,
			},
			emailVerification: {
				sendOnSignUp: true,
				async sendVerificationEmail(data, request) {
					sendVerificationOTP(data, request);
				},
			},
			plugins: [
				emailOTP({
					async sendVerificationOTP(data, request) {
						//
					},
				}),
			],
		});
		await client.signUp.email(
			{
				email: "[email protected]",
				password: "password",
				name: "Test User",
			},
			{
				onSuccess: cookieSetter(headers),
			},
		);
		expect(sendVerificationOTP).toHaveBeenCalled();
	});
});

```
Page 34/52FirstPrevNextLast