#
tokens: 48842/50000 8/1118 files (page 28/52)
lines: off (toggle) GitHub
raw markdown copy
This is page 28 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
│   │       │   ├── reddit.mdx
│   │       │   ├── roblox.mdx
│   │       │   ├── salesforce.mdx
│   │       │   ├── slack.mdx
│   │       │   ├── spotify.mdx
│   │       │   ├── tiktok.mdx
│   │       │   ├── twitch.mdx
│   │       │   ├── twitter.mdx
│   │       │   ├── vk.mdx
│   │       │   └── zoom.mdx
│   │       ├── basic-usage.mdx
│   │       ├── comparison.mdx
│   │       ├── concepts
│   │       │   ├── api.mdx
│   │       │   ├── cli.mdx
│   │       │   ├── client.mdx
│   │       │   ├── cookies.mdx
│   │       │   ├── database.mdx
│   │       │   ├── email.mdx
│   │       │   ├── hooks.mdx
│   │       │   ├── oauth.mdx
│   │       │   ├── plugins.mdx
│   │       │   ├── rate-limit.mdx
│   │       │   ├── session-management.mdx
│   │       │   ├── typescript.mdx
│   │       │   └── users-accounts.mdx
│   │       ├── examples
│   │       │   ├── astro.mdx
│   │       │   ├── next-js.mdx
│   │       │   ├── nuxt.mdx
│   │       │   ├── remix.mdx
│   │       │   └── svelte-kit.mdx
│   │       ├── guides
│   │       │   ├── auth0-migration-guide.mdx
│   │       │   ├── browser-extension-guide.mdx
│   │       │   ├── clerk-migration-guide.mdx
│   │       │   ├── create-a-db-adapter.mdx
│   │       │   ├── next-auth-migration-guide.mdx
│   │       │   ├── optimizing-for-performance.mdx
│   │       │   ├── saml-sso-with-okta.mdx
│   │       │   ├── supabase-migration-guide.mdx
│   │       │   └── your-first-plugin.mdx
│   │       ├── installation.mdx
│   │       ├── integrations
│   │       │   ├── astro.mdx
│   │       │   ├── convex.mdx
│   │       │   ├── elysia.mdx
│   │       │   ├── expo.mdx
│   │       │   ├── express.mdx
│   │       │   ├── fastify.mdx
│   │       │   ├── hono.mdx
│   │       │   ├── lynx.mdx
│   │       │   ├── nestjs.mdx
│   │       │   ├── next.mdx
│   │       │   ├── nitro.mdx
│   │       │   ├── nuxt.mdx
│   │       │   ├── remix.mdx
│   │       │   ├── solid-start.mdx
│   │       │   ├── svelte-kit.mdx
│   │       │   ├── tanstack.mdx
│   │       │   └── waku.mdx
│   │       ├── introduction.mdx
│   │       ├── meta.json
│   │       ├── plugins
│   │       │   ├── 2fa.mdx
│   │       │   ├── admin.mdx
│   │       │   ├── anonymous.mdx
│   │       │   ├── api-key.mdx
│   │       │   ├── autumn.mdx
│   │       │   ├── bearer.mdx
│   │       │   ├── captcha.mdx
│   │       │   ├── community-plugins.mdx
│   │       │   ├── device-authorization.mdx
│   │       │   ├── dodopayments.mdx
│   │       │   ├── dub.mdx
│   │       │   ├── email-otp.mdx
│   │       │   ├── generic-oauth.mdx
│   │       │   ├── have-i-been-pwned.mdx
│   │       │   ├── jwt.mdx
│   │       │   ├── last-login-method.mdx
│   │       │   ├── magic-link.mdx
│   │       │   ├── mcp.mdx
│   │       │   ├── multi-session.mdx
│   │       │   ├── oauth-proxy.mdx
│   │       │   ├── oidc-provider.mdx
│   │       │   ├── one-tap.mdx
│   │       │   ├── one-time-token.mdx
│   │       │   ├── open-api.mdx
│   │       │   ├── organization.mdx
│   │       │   ├── passkey.mdx
│   │       │   ├── phone-number.mdx
│   │       │   ├── polar.mdx
│   │       │   ├── siwe.mdx
│   │       │   ├── sso.mdx
│   │       │   ├── stripe.mdx
│   │       │   └── username.mdx
│   │       └── reference
│   │           ├── contributing.mdx
│   │           ├── faq.mdx
│   │           ├── options.mdx
│   │           ├── resources.mdx
│   │           ├── security.mdx
│   │           └── telemetry.mdx
│   ├── hooks
│   │   └── use-mobile.ts
│   ├── ignore-build.sh
│   ├── lib
│   │   ├── blog.ts
│   │   ├── chat
│   │   │   └── inkeep-qa-schema.ts
│   │   ├── constants.ts
│   │   ├── export-search-indexes.ts
│   │   ├── inkeep-analytics.ts
│   │   ├── is-active.ts
│   │   ├── metadata.ts
│   │   ├── source.ts
│   │   └── utils.ts
│   ├── next.config.js
│   ├── package.json
│   ├── postcss.config.js
│   ├── proxy.ts
│   ├── public
│   │   ├── avatars
│   │   │   └── beka.jpg
│   │   ├── blogs
│   │   │   ├── authjs-joins.png
│   │   │   ├── seed-round.png
│   │   │   └── supabase-ps.png
│   │   ├── branding
│   │   │   ├── better-auth-brand-assets.zip
│   │   │   ├── better-auth-logo-dark.png
│   │   │   ├── better-auth-logo-dark.svg
│   │   │   ├── better-auth-logo-light.png
│   │   │   ├── better-auth-logo-light.svg
│   │   │   ├── better-auth-logo-wordmark-dark.png
│   │   │   ├── better-auth-logo-wordmark-dark.svg
│   │   │   ├── better-auth-logo-wordmark-light.png
│   │   │   └── better-auth-logo-wordmark-light.svg
│   │   ├── extension-id.png
│   │   ├── favicon
│   │   │   ├── android-chrome-192x192.png
│   │   │   ├── android-chrome-512x512.png
│   │   │   ├── apple-touch-icon.png
│   │   │   ├── favicon-16x16.png
│   │   │   ├── favicon-32x32.png
│   │   │   ├── favicon.ico
│   │   │   ├── light
│   │   │   │   ├── android-chrome-192x192.png
│   │   │   │   ├── android-chrome-512x512.png
│   │   │   │   ├── apple-touch-icon.png
│   │   │   │   ├── favicon-16x16.png
│   │   │   │   ├── favicon-32x32.png
│   │   │   │   ├── favicon.ico
│   │   │   │   └── site.webmanifest
│   │   │   └── site.webmanifest
│   │   ├── images
│   │   │   └── blogs
│   │   │       └── better auth (1).png
│   │   ├── logo.png
│   │   ├── logo.svg
│   │   ├── LogoDark.webp
│   │   ├── LogoLight.webp
│   │   ├── og.png
│   │   ├── open-api-reference.png
│   │   ├── people-say
│   │   │   ├── code-with-antonio.jpg
│   │   │   ├── dagmawi-babi.png
│   │   │   ├── dax.png
│   │   │   ├── dev-ed.png
│   │   │   ├── egoist.png
│   │   │   ├── guillermo-rauch.png
│   │   │   ├── jonathan-wilke.png
│   │   │   ├── josh-tried-coding.jpg
│   │   │   ├── kitze.jpg
│   │   │   ├── lazar-nikolov.png
│   │   │   ├── nizzy.png
│   │   │   ├── omar-mcadam.png
│   │   │   ├── ryan-vogel.jpg
│   │   │   ├── saltyatom.jpg
│   │   │   ├── sebastien-chopin.png
│   │   │   ├── shreyas-mididoddi.png
│   │   │   ├── tech-nerd.png
│   │   │   ├── theo.png
│   │   │   ├── vybhav-bhargav.png
│   │   │   └── xavier-pladevall.jpg
│   │   ├── plus.svg
│   │   ├── release-og
│   │   │   ├── 1-2.png
│   │   │   ├── 1-3.png
│   │   │   └── changelog-og.png
│   │   └── v1-og.png
│   ├── README.md
│   ├── scripts
│   │   ├── endpoint-to-doc
│   │   │   ├── index.ts
│   │   │   ├── input.ts
│   │   │   ├── output.mdx
│   │   │   └── readme.md
│   │   └── sync-orama.ts
│   ├── source.config.ts
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   └── turbo.json
├── e2e
│   ├── integration
│   │   ├── package.json
│   │   ├── playwright.config.ts
│   │   ├── solid-vinxi
│   │   │   ├── .gitignore
│   │   │   ├── app.config.ts
│   │   │   ├── e2e
│   │   │   │   ├── test.spec.ts
│   │   │   │   └── utils.ts
│   │   │   ├── package.json
│   │   │   ├── public
│   │   │   │   └── favicon.ico
│   │   │   ├── src
│   │   │   │   ├── app.tsx
│   │   │   │   ├── entry-client.tsx
│   │   │   │   ├── entry-server.tsx
│   │   │   │   ├── global.d.ts
│   │   │   │   ├── lib
│   │   │   │   │   ├── auth-client.ts
│   │   │   │   │   └── auth.ts
│   │   │   │   └── routes
│   │   │   │       ├── [...404].tsx
│   │   │   │       ├── api
│   │   │   │       │   └── auth
│   │   │   │       │       └── [...all].ts
│   │   │   │       └── index.tsx
│   │   │   └── tsconfig.json
│   │   ├── test-utils
│   │   │   ├── package.json
│   │   │   └── src
│   │   │       └── playwright.ts
│   │   └── vanilla-node
│   │       ├── e2e
│   │       │   ├── app.ts
│   │       │   ├── domain.spec.ts
│   │       │   ├── postgres-js.spec.ts
│   │       │   ├── test.spec.ts
│   │       │   └── utils.ts
│   │       ├── index.html
│   │       ├── package.json
│   │       ├── src
│   │       │   ├── main.ts
│   │       │   └── vite-env.d.ts
│   │       ├── tsconfig.json
│   │       └── vite.config.ts
│   └── smoke
│       ├── package.json
│       ├── test
│       │   ├── bun.spec.ts
│       │   ├── cloudflare.spec.ts
│       │   ├── deno.spec.ts
│       │   ├── fixtures
│       │   │   ├── bun-simple.ts
│       │   │   ├── cloudflare
│       │   │   │   ├── .gitignore
│       │   │   │   ├── drizzle
│       │   │   │   │   ├── 0000_clean_vector.sql
│       │   │   │   │   └── meta
│       │   │   │   │       ├── _journal.json
│       │   │   │   │       └── 0000_snapshot.json
│       │   │   │   ├── drizzle.config.ts
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── auth-schema.ts
│       │   │   │   │   ├── db.ts
│       │   │   │   │   └── index.ts
│       │   │   │   ├── test
│       │   │   │   │   ├── apply-migrations.ts
│       │   │   │   │   ├── env.d.ts
│       │   │   │   │   └── index.test.ts
│       │   │   │   ├── tsconfig.json
│       │   │   │   ├── vitest.config.ts
│       │   │   │   ├── worker-configuration.d.ts
│       │   │   │   └── wrangler.json
│       │   │   ├── deno-simple.ts
│       │   │   ├── tsconfig-declaration
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── demo.ts
│       │   │   │   │   ├── index.ts
│       │   │   │   │   └── username.ts
│       │   │   │   └── tsconfig.json
│       │   │   ├── tsconfig-exact-optional-property-types
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   ├── index.ts
│       │   │   │   │   ├── organization.ts
│       │   │   │   │   ├── user-additional-fields.ts
│       │   │   │   │   └── username.ts
│       │   │   │   └── tsconfig.json
│       │   │   ├── tsconfig-isolated-module-bundler
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   └── index.ts
│       │   │   │   └── tsconfig.json
│       │   │   ├── tsconfig-verbatim-module-syntax-node10
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   └── index.ts
│       │   │   │   └── tsconfig.json
│       │   │   └── vite
│       │   │       ├── package.json
│       │   │       ├── src
│       │   │       │   ├── client.ts
│       │   │       │   └── server.ts
│       │   │       ├── tsconfig.json
│       │   │       └── vite.config.ts
│       │   ├── ssr.ts
│       │   ├── typecheck.spec.ts
│       │   └── vite.spec.ts
│       └── tsconfig.json
├── LICENSE.md
├── package.json
├── packages
│   ├── better-auth
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── __snapshots__
│   │   │   │   └── init.test.ts.snap
│   │   │   ├── adapters
│   │   │   │   ├── adapter-factory
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── test
│   │   │   │   │   │   ├── __snapshots__
│   │   │   │   │   │   │   └── adapter-factory.test.ts.snap
│   │   │   │   │   │   └── adapter-factory.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── create-test-suite.ts
│   │   │   │   ├── drizzle-adapter
│   │   │   │   │   ├── drizzle-adapter.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── test
│   │   │   │   │       ├── .gitignore
│   │   │   │   │       ├── adapter.drizzle.mysql.test.ts
│   │   │   │   │       ├── adapter.drizzle.pg.test.ts
│   │   │   │   │       ├── adapter.drizzle.sqlite.test.ts
│   │   │   │   │       └── generate-schema.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kysely-adapter
│   │   │   │   │   ├── bun-sqlite-dialect.ts
│   │   │   │   │   ├── dialect.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── kysely-adapter.ts
│   │   │   │   │   ├── node-sqlite-dialect.ts
│   │   │   │   │   ├── test
│   │   │   │   │   │   ├── adapter.kysely.mssql.test.ts
│   │   │   │   │   │   ├── adapter.kysely.mysql.test.ts
│   │   │   │   │   │   ├── adapter.kysely.pg-custom-schema.test.ts
│   │   │   │   │   │   ├── adapter.kysely.pg.test.ts
│   │   │   │   │   │   ├── adapter.kysely.sqlite.test.ts
│   │   │   │   │   │   └── node-sqlite-dialect.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── memory-adapter
│   │   │   │   │   ├── adapter.memory.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── memory-adapter.ts
│   │   │   │   ├── mongodb-adapter
│   │   │   │   │   ├── adapter.mongo-db.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── mongodb-adapter.ts
│   │   │   │   ├── prisma-adapter
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── prisma-adapter.ts
│   │   │   │   │   └── test
│   │   │   │   │       ├── .gitignore
│   │   │   │   │       ├── base.prisma
│   │   │   │   │       ├── generate-auth-config.ts
│   │   │   │   │       ├── generate-prisma-schema.ts
│   │   │   │   │       ├── get-prisma-client.ts
│   │   │   │   │       ├── prisma.mysql.test.ts
│   │   │   │   │       ├── prisma.pg.test.ts
│   │   │   │   │       ├── prisma.sqlite.test.ts
│   │   │   │   │       └── push-prisma-schema.ts
│   │   │   │   ├── test-adapter.ts
│   │   │   │   ├── test.ts
│   │   │   │   ├── tests
│   │   │   │   │   ├── auth-flow.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── normal.ts
│   │   │   │   │   ├── number-id.ts
│   │   │   │   │   ├── performance.ts
│   │   │   │   │   └── transactions.ts
│   │   │   │   └── utils.ts
│   │   │   ├── api
│   │   │   │   ├── check-endpoint-conflicts.test.ts
│   │   │   │   ├── index.test.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── middlewares
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── origin-check.test.ts
│   │   │   │   │   └── origin-check.ts
│   │   │   │   ├── rate-limiter
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── rate-limiter.test.ts
│   │   │   │   ├── routes
│   │   │   │   │   ├── account.test.ts
│   │   │   │   │   ├── account.ts
│   │   │   │   │   ├── callback.ts
│   │   │   │   │   ├── email-verification.test.ts
│   │   │   │   │   ├── email-verification.ts
│   │   │   │   │   ├── error.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── ok.ts
│   │   │   │   │   ├── reset-password.test.ts
│   │   │   │   │   ├── reset-password.ts
│   │   │   │   │   ├── session-api.test.ts
│   │   │   │   │   ├── session.ts
│   │   │   │   │   ├── sign-in.test.ts
│   │   │   │   │   ├── sign-in.ts
│   │   │   │   │   ├── sign-out.test.ts
│   │   │   │   │   ├── sign-out.ts
│   │   │   │   │   ├── sign-up.test.ts
│   │   │   │   │   ├── sign-up.ts
│   │   │   │   │   ├── update-user.test.ts
│   │   │   │   │   └── update-user.ts
│   │   │   │   ├── to-auth-endpoints.test.ts
│   │   │   │   └── to-auth-endpoints.ts
│   │   │   ├── auth.test.ts
│   │   │   ├── auth.ts
│   │   │   ├── call.test.ts
│   │   │   ├── client
│   │   │   │   ├── client-ssr.test.ts
│   │   │   │   ├── client.test.ts
│   │   │   │   ├── config.ts
│   │   │   │   ├── fetch-plugins.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── lynx
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── lynx-store.ts
│   │   │   │   ├── parser.ts
│   │   │   │   ├── path-to-object.ts
│   │   │   │   ├── plugins
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── infer-plugin.ts
│   │   │   │   ├── proxy.ts
│   │   │   │   ├── query.ts
│   │   │   │   ├── react
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── react-store.ts
│   │   │   │   ├── session-atom.ts
│   │   │   │   ├── solid
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── solid-store.ts
│   │   │   │   ├── svelte
│   │   │   │   │   └── index.ts
│   │   │   │   ├── test-plugin.ts
│   │   │   │   ├── types.ts
│   │   │   │   ├── url.test.ts
│   │   │   │   ├── vanilla.ts
│   │   │   │   └── vue
│   │   │   │       ├── index.ts
│   │   │   │       └── vue-store.ts
│   │   │   ├── cookies
│   │   │   │   ├── check-cookies.ts
│   │   │   │   ├── cookie-utils.ts
│   │   │   │   ├── cookies.test.ts
│   │   │   │   └── index.ts
│   │   │   ├── crypto
│   │   │   │   ├── buffer.ts
│   │   │   │   ├── hash.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── jwt.ts
│   │   │   │   ├── password.test.ts
│   │   │   │   ├── password.ts
│   │   │   │   └── random.ts
│   │   │   ├── db
│   │   │   │   ├── db.test.ts
│   │   │   │   ├── field.ts
│   │   │   │   ├── get-migration-schema.test.ts
│   │   │   │   ├── get-migration.ts
│   │   │   │   ├── get-schema.ts
│   │   │   │   ├── get-tables.test.ts
│   │   │   │   ├── get-tables.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── internal-adapter.test.ts
│   │   │   │   ├── internal-adapter.ts
│   │   │   │   ├── schema.ts
│   │   │   │   ├── secondary-storage.test.ts
│   │   │   │   ├── to-zod.ts
│   │   │   │   ├── utils.ts
│   │   │   │   └── with-hooks.ts
│   │   │   ├── index.ts
│   │   │   ├── init.test.ts
│   │   │   ├── init.ts
│   │   │   ├── integrations
│   │   │   │   ├── next-js.ts
│   │   │   │   ├── node.ts
│   │   │   │   ├── react-start.ts
│   │   │   │   ├── solid-start.ts
│   │   │   │   └── svelte-kit.ts
│   │   │   ├── oauth2
│   │   │   │   ├── index.ts
│   │   │   │   ├── link-account.test.ts
│   │   │   │   ├── link-account.ts
│   │   │   │   ├── state.ts
│   │   │   │   └── utils.ts
│   │   │   ├── plugins
│   │   │   │   ├── access
│   │   │   │   │   ├── access.test.ts
│   │   │   │   │   ├── access.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── additional-fields
│   │   │   │   │   ├── additional-fields.test.ts
│   │   │   │   │   └── client.ts
│   │   │   │   ├── admin
│   │   │   │   │   ├── access
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   └── statement.ts
│   │   │   │   │   ├── admin.test.ts
│   │   │   │   │   ├── admin.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── error-codes.ts
│   │   │   │   │   ├── has-permission.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── anonymous
│   │   │   │   │   ├── anon.test.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── api-key
│   │   │   │   │   ├── api-key.test.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── rate-limit.ts
│   │   │   │   │   ├── routes
│   │   │   │   │   │   ├── create-api-key.ts
│   │   │   │   │   │   ├── delete-all-expired-api-keys.ts
│   │   │   │   │   │   ├── delete-api-key.ts
│   │   │   │   │   │   ├── get-api-key.ts
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── list-api-keys.ts
│   │   │   │   │   │   ├── update-api-key.ts
│   │   │   │   │   │   └── verify-api-key.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── bearer
│   │   │   │   │   ├── bearer.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── captcha
│   │   │   │   │   ├── captcha.test.ts
│   │   │   │   │   ├── constants.ts
│   │   │   │   │   ├── error-codes.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   ├── utils.ts
│   │   │   │   │   └── verify-handlers
│   │   │   │   │       ├── captchafox.ts
│   │   │   │   │       ├── cloudflare-turnstile.ts
│   │   │   │   │       ├── google-recaptcha.ts
│   │   │   │   │       ├── h-captcha.ts
│   │   │   │   │       └── index.ts
│   │   │   │   ├── custom-session
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── custom-session.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── device-authorization
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── device-authorization.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── schema.ts
│   │   │   │   ├── email-otp
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── email-otp.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── generic-oauth
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── generic-oauth.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── haveibeenpwned
│   │   │   │   │   ├── haveibeenpwned.test.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── jwt
│   │   │   │   │   ├── adapter.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── jwt.test.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── sign.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── last-login-method
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── custom-prefix.test.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── last-login-method.test.ts
│   │   │   │   ├── magic-link
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── magic-link.test.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── mcp
│   │   │   │   │   ├── authorize.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── mcp.test.ts
│   │   │   │   ├── multi-session
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── multi-session.test.ts
│   │   │   │   ├── oauth-proxy
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── oauth-proxy.test.ts
│   │   │   │   ├── oidc-provider
│   │   │   │   │   ├── authorize.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── oidc.test.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   ├── ui.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── one-tap
│   │   │   │   │   ├── client.ts
│   │   │   │   │   └── index.ts
│   │   │   │   ├── one-time-token
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── one-time-token.test.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── open-api
│   │   │   │   │   ├── generator.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── logo.ts
│   │   │   │   │   └── open-api.test.ts
│   │   │   │   ├── organization
│   │   │   │   │   ├── access
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   └── statement.ts
│   │   │   │   │   ├── adapter.ts
│   │   │   │   │   ├── call.ts
│   │   │   │   │   ├── client.test.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── error-codes.ts
│   │   │   │   │   ├── has-permission.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── organization-hook.test.ts
│   │   │   │   │   ├── organization.test.ts
│   │   │   │   │   ├── organization.ts
│   │   │   │   │   ├── permission.ts
│   │   │   │   │   ├── routes
│   │   │   │   │   │   ├── crud-access-control.test.ts
│   │   │   │   │   │   ├── crud-access-control.ts
│   │   │   │   │   │   ├── crud-invites.ts
│   │   │   │   │   │   ├── crud-members.test.ts
│   │   │   │   │   │   ├── crud-members.ts
│   │   │   │   │   │   ├── crud-org.test.ts
│   │   │   │   │   │   ├── crud-org.ts
│   │   │   │   │   │   └── crud-team.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── team.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── passkey
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── passkey.test.ts
│   │   │   │   ├── phone-number
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── phone-number-error.ts
│   │   │   │   │   └── phone-number.test.ts
│   │   │   │   ├── siwe
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── siwe.test.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── two-factor
│   │   │   │   │   ├── backup-codes
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── client.ts
│   │   │   │   │   ├── constant.ts
│   │   │   │   │   ├── error-code.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── otp
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── schema.ts
│   │   │   │   │   ├── totp
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   ├── two-factor.test.ts
│   │   │   │   │   ├── types.ts
│   │   │   │   │   ├── utils.ts
│   │   │   │   │   └── verify-two-factor.ts
│   │   │   │   └── username
│   │   │   │       ├── client.ts
│   │   │   │       ├── error-codes.ts
│   │   │   │       ├── index.ts
│   │   │   │       ├── schema.ts
│   │   │   │       └── username.test.ts
│   │   │   ├── social-providers
│   │   │   │   └── index.ts
│   │   │   ├── social.test.ts
│   │   │   ├── test-utils
│   │   │   │   ├── headers.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── state.ts
│   │   │   │   └── test-instance.ts
│   │   │   ├── types
│   │   │   │   ├── adapter.ts
│   │   │   │   ├── api.ts
│   │   │   │   ├── helper.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── models.ts
│   │   │   │   ├── plugins.ts
│   │   │   │   └── types.test.ts
│   │   │   └── utils
│   │   │       ├── await-object.ts
│   │   │       ├── boolean.ts
│   │   │       ├── clone.ts
│   │   │       ├── constants.ts
│   │   │       ├── date.ts
│   │   │       ├── ensure-utc.ts
│   │   │       ├── get-request-ip.ts
│   │   │       ├── hashing.ts
│   │   │       ├── hide-metadata.ts
│   │   │       ├── id.ts
│   │   │       ├── import-util.ts
│   │   │       ├── index.ts
│   │   │       ├── is-atom.ts
│   │   │       ├── is-promise.ts
│   │   │       ├── json.ts
│   │   │       ├── merger.ts
│   │   │       ├── middleware-response.ts
│   │   │       ├── misc.ts
│   │   │       ├── password.ts
│   │   │       ├── plugin-helper.ts
│   │   │       ├── shim.ts
│   │   │       ├── time.ts
│   │   │       ├── url.ts
│   │   │       └── wildcard.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   ├── vitest.config.ts
│   │   └── vitest.setup.ts
│   ├── cli
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── commands
│   │   │   │   ├── generate.ts
│   │   │   │   ├── info.ts
│   │   │   │   ├── init.ts
│   │   │   │   ├── login.ts
│   │   │   │   ├── mcp.ts
│   │   │   │   ├── migrate.ts
│   │   │   │   └── secret.ts
│   │   │   ├── generators
│   │   │   │   ├── auth-config.ts
│   │   │   │   ├── drizzle.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kysely.ts
│   │   │   │   ├── prisma.ts
│   │   │   │   └── types.ts
│   │   │   ├── index.ts
│   │   │   └── utils
│   │   │       ├── add-svelte-kit-env-modules.ts
│   │   │       ├── check-package-managers.ts
│   │   │       ├── format-ms.ts
│   │   │       ├── get-config.ts
│   │   │       ├── get-package-info.ts
│   │   │       ├── get-tsconfig-info.ts
│   │   │       └── install-dependencies.ts
│   │   ├── test
│   │   │   ├── __snapshots__
│   │   │   │   ├── auth-schema-mysql-enum.txt
│   │   │   │   ├── auth-schema-mysql-number-id.txt
│   │   │   │   ├── auth-schema-mysql-passkey-number-id.txt
│   │   │   │   ├── auth-schema-mysql-passkey.txt
│   │   │   │   ├── auth-schema-mysql.txt
│   │   │   │   ├── auth-schema-number-id.txt
│   │   │   │   ├── auth-schema-pg-enum.txt
│   │   │   │   ├── auth-schema-pg-passkey.txt
│   │   │   │   ├── auth-schema-sqlite-enum.txt
│   │   │   │   ├── auth-schema-sqlite-number-id.txt
│   │   │   │   ├── auth-schema-sqlite-passkey-number-id.txt
│   │   │   │   ├── auth-schema-sqlite-passkey.txt
│   │   │   │   ├── auth-schema-sqlite.txt
│   │   │   │   ├── auth-schema.txt
│   │   │   │   ├── migrations.sql
│   │   │   │   ├── schema-mongodb.prisma
│   │   │   │   ├── schema-mysql-custom.prisma
│   │   │   │   ├── schema-mysql.prisma
│   │   │   │   ├── schema-numberid.prisma
│   │   │   │   └── schema.prisma
│   │   │   ├── generate-all-db.test.ts
│   │   │   ├── generate.test.ts
│   │   │   ├── get-config.test.ts
│   │   │   ├── info.test.ts
│   │   │   └── migrate.test.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── core
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── api
│   │   │   │   └── index.ts
│   │   │   ├── async_hooks
│   │   │   │   └── index.ts
│   │   │   ├── context
│   │   │   │   ├── endpoint-context.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── transaction.ts
│   │   │   ├── db
│   │   │   │   ├── adapter
│   │   │   │   │   └── index.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── plugin.ts
│   │   │   │   ├── schema
│   │   │   │   │   ├── account.ts
│   │   │   │   │   ├── rate-limit.ts
│   │   │   │   │   ├── session.ts
│   │   │   │   │   ├── shared.ts
│   │   │   │   │   ├── user.ts
│   │   │   │   │   └── verification.ts
│   │   │   │   └── type.ts
│   │   │   ├── env
│   │   │   │   ├── color-depth.ts
│   │   │   │   ├── env-impl.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── logger.test.ts
│   │   │   │   └── logger.ts
│   │   │   ├── error
│   │   │   │   ├── codes.ts
│   │   │   │   └── index.ts
│   │   │   ├── index.ts
│   │   │   ├── oauth2
│   │   │   │   ├── client-credentials-token.ts
│   │   │   │   ├── create-authorization-url.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── oauth-provider.ts
│   │   │   │   ├── refresh-access-token.ts
│   │   │   │   ├── utils.ts
│   │   │   │   └── validate-authorization-code.ts
│   │   │   ├── social-providers
│   │   │   │   ├── apple.ts
│   │   │   │   ├── atlassian.ts
│   │   │   │   ├── cognito.ts
│   │   │   │   ├── discord.ts
│   │   │   │   ├── dropbox.ts
│   │   │   │   ├── facebook.ts
│   │   │   │   ├── figma.ts
│   │   │   │   ├── github.ts
│   │   │   │   ├── gitlab.ts
│   │   │   │   ├── google.ts
│   │   │   │   ├── huggingface.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── kakao.ts
│   │   │   │   ├── kick.ts
│   │   │   │   ├── line.ts
│   │   │   │   ├── linear.ts
│   │   │   │   ├── linkedin.ts
│   │   │   │   ├── microsoft-entra-id.ts
│   │   │   │   ├── naver.ts
│   │   │   │   ├── notion.ts
│   │   │   │   ├── paypal.ts
│   │   │   │   ├── reddit.ts
│   │   │   │   ├── roblox.ts
│   │   │   │   ├── salesforce.ts
│   │   │   │   ├── slack.ts
│   │   │   │   ├── spotify.ts
│   │   │   │   ├── tiktok.ts
│   │   │   │   ├── twitch.ts
│   │   │   │   ├── twitter.ts
│   │   │   │   ├── vk.ts
│   │   │   │   └── zoom.ts
│   │   │   ├── types
│   │   │   │   ├── context.ts
│   │   │   │   ├── cookie.ts
│   │   │   │   ├── helper.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── init-options.ts
│   │   │   │   ├── plugin-client.ts
│   │   │   │   └── plugin.ts
│   │   │   └── utils
│   │   │       ├── error-codes.ts
│   │   │       └── index.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── expo
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   └── index.ts
│   │   ├── test
│   │   │   └── expo.test.ts
│   │   ├── tsconfig.json
│   │   ├── tsconfig.test.json
│   │   └── tsdown.config.ts
│   ├── sso
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── index.ts
│   │   │   ├── oidc.test.ts
│   │   │   └── saml.test.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── stripe
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── client.ts
│   │   │   ├── hooks.ts
│   │   │   ├── index.ts
│   │   │   ├── schema.ts
│   │   │   ├── stripe.test.ts
│   │   │   ├── types.ts
│   │   │   └── utils.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   └── telemetry
│       ├── package.json
│       ├── src
│       │   ├── detectors
│       │   │   ├── detect-auth-config.ts
│       │   │   ├── detect-database.ts
│       │   │   ├── detect-framework.ts
│       │   │   ├── detect-project-info.ts
│       │   │   ├── detect-runtime.ts
│       │   │   └── detect-system-info.ts
│       │   ├── index.ts
│       │   ├── project-id.ts
│       │   ├── telemetry.test.ts
│       │   ├── types.ts
│       │   └── utils
│       │       ├── hash.ts
│       │       ├── id.ts
│       │       ├── import-util.ts
│       │       └── package-json.ts
│       ├── tsconfig.json
│       └── tsdown.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── SECURITY.md
├── tsconfig.base.json
├── tsconfig.json
└── turbo.json
```

# Files

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

```typescript
import { describe, expect } from "vitest";
import { getTestInstance } from "../../test-utils/test-instance";
import { organization } from "./organization";
import { createAuthClient } from "../../client";
import { organizationClient } from "./client";

describe("team", async (it) => {
	const { auth, signInWithTestUser, cookieSetter } = await getTestInstance({
		user: {
			modelName: "users",
		},
		plugins: [
			organization({
				async sendInvitationEmail() {},
				teams: {
					enabled: true,
				},
			}),
		],
		logger: {
			level: "error",
		},
	});

	const { headers } = await signInWithTestUser();
	const client = createAuthClient({
		plugins: [
			organizationClient({
				teams: {
					enabled: true,
				},
			}),
		],
		baseURL: "http://localhost:3000/api/auth",
		fetchOptions: {
			customFetchImpl: async (url, init) => {
				return auth.handler(new Request(url, init));
			},
		},
	});

	let organizationId: string;
	let teamId: string;
	let secondTeamId: string;

	const invitedUser = {
		email: "[email protected]",
		password: "password",
		name: "Invited User",
	};

	const signUpHeaders = new Headers();
	const signUpRes = await client.signUp.email(invitedUser, {
		onSuccess: cookieSetter(signUpHeaders),
	});

	it("should create an organization and a team", async () => {
		const createOrganizationResponse = await client.organization.create({
			name: "Test Organization",
			slug: "test-org",
			metadata: {
				test: "organization-metadata",
			},
			fetchOptions: {
				headers,
			},
		});

		organizationId = createOrganizationResponse.data?.id as string;
		expect(createOrganizationResponse.data?.name).toBe("Test Organization");
		expect(createOrganizationResponse.data?.slug).toBe("test-org");
		expect(createOrganizationResponse.data?.members.length).toBe(1);
		expect(createOrganizationResponse.data?.metadata?.test).toBe(
			"organization-metadata",
		);

		const createTeamResponse = await client.organization.createTeam(
			{
				name: "Development Team",
				organizationId,
			},
			{
				headers,
			},
		);

		teamId = createTeamResponse.data?.id as string;
		expect(createTeamResponse.data?.name).toBe("Development Team");
		expect(createTeamResponse.data?.organizationId).toBe(organizationId);

		const createSecondTeamResponse = await client.organization.createTeam(
			{
				name: "Marketing Team",
				organizationId,
			},
			{
				headers,
			},
		);

		secondTeamId = createSecondTeamResponse.data?.id as string;
		expect(createSecondTeamResponse.data?.name).toBe("Marketing Team");
		expect(createSecondTeamResponse.data?.organizationId).toBe(organizationId);
	});

	it("should invite member to team", async () => {
		expect(teamId).toBeDefined();

		const res = await client.organization.inviteMember(
			{
				teamId,
				email: invitedUser.email,
				role: "member",
			},
			{
				headers,
			},
		);

		expect(res.data).toMatchObject({
			email: invitedUser.email,
			role: "member",
			teamId,
		});

		const invitation = await client.organization.acceptInvitation(
			{
				invitationId: res.data?.id as string,
			},
			{
				headers: signUpHeaders,
			},
		);

		expect(invitation.data?.member).toMatchObject({
			role: "member",
			userId: signUpRes.data?.user.id,
		});
	});

	it("should add team to the member's list of teams", async () => {
		const listUserTeamsRes = await client.organization.listUserTeams(
			{},
			{
				headers: signUpHeaders,
			},
		);

		expect(listUserTeamsRes.error).toBeNull();
		expect(listUserTeamsRes.data).not.toBeNull();
		expect(listUserTeamsRes.data).toHaveLength(1);
	});

	it("should be able to list team members in the current active team", async () => {
		const activeTeamHeaders = new Headers();
		await client.organization.setActiveTeam(
			{
				teamId,
			},
			{
				headers: signUpHeaders,
				onSuccess: cookieSetter(activeTeamHeaders),
			},
		);

		const res = await client.organization.listTeamMembers(
			{},
			{
				headers: activeTeamHeaders,
			},
		);

		expect(res.error).toBeNull();
		expect(res.data).not.toBeNull();
		expect(res.data).toHaveLength(1);
	});

	it("should get full organization", async () => {
		const organization = await client.organization.getFullOrganization({
			fetchOptions: {
				headers,
			},
		});

		const teams = organization.data?.teams;
		expect(teams).toBeDefined();
		expect(teams?.length).toBe(3);

		const teamNames = teams?.map((team) => team.name);
		expect(teamNames).toContain("Development Team");
		expect(teamNames).toContain("Marketing Team");
	});

	it("should get all teams", async () => {
		const teamsResponse = await client.organization.listTeams({
			fetchOptions: { headers },
		});

		expect(teamsResponse.data).toBeInstanceOf(Array);
		expect(teamsResponse.data).toHaveLength(3);
	});

	it("should update a team", async () => {
		const updateTeamResponse = await client.organization.updateTeam({
			teamId,
			data: {
				name: "Updated Development Team",
			},
			fetchOptions: { headers },
		});

		expect(updateTeamResponse.data?.name).toBe("Updated Development Team");
		expect(updateTeamResponse.data?.id).toBe(teamId);
	});

	it("should remove a team", async () => {
		const teamsBeforeRemoval = await client.organization.listTeams({
			fetchOptions: { headers },
		});
		expect(teamsBeforeRemoval.data).toHaveLength(3);

		const removeTeamResponse = await client.organization.removeTeam({
			teamId,
			organizationId,
			fetchOptions: { headers },
		});

		expect(removeTeamResponse.data?.message).toBe("Team removed successfully.");

		const teamsAfterRemoval = await client.organization.listTeams({
			fetchOptions: { headers },
		});

		expect(teamsAfterRemoval.data).toHaveLength(2);
	});

	it("should not be able to remove the last team when allowRemovingAllTeams is not enabled", async () => {
		try {
			await client.organization.removeTeam({
				teamId: secondTeamId,
				organizationId,
				fetchOptions: { headers },
			});
			expect(true).toBe(false);
		} catch (error) {
			expect(error).toBeDefined();
		}
	});

	it("should not be allowed to invite a member to a team that's reached maximum members", async () => {
		const { auth, signInWithTestUser } = await getTestInstance({
			user: {
				modelName: "users",
			},
			plugins: [
				organization({
					teams: {
						enabled: true,
						maximumMembersPerTeam: 1,
					},
				}),
			],
			logger: {
				level: "error",
			},
		});

		const { headers } = await signInWithTestUser();
		const client = createAuthClient({
			plugins: [
				organizationClient({
					teams: {
						enabled: true,
					},
				}),
			],
			baseURL: "http://localhost:3000/api/auth",
			fetchOptions: {
				customFetchImpl: async (url, init) => {
					return auth.handler(new Request(url, init));
				},
			},
		});
		const createOrganizationResponse = await client.organization.create({
			name: "Test Organization",
			slug: "test-org",
			metadata: {
				test: "organization-metadata",
			},
			fetchOptions: {
				headers,
			},
		});
		expect(createOrganizationResponse.data?.id).toBeDefined();

		const createTeamResponse = await client.organization.createTeam(
			{
				name: "Development Team",
				organizationId: createOrganizationResponse.data?.id,
			},
			{
				headers,
			},
		);
		expect(createTeamResponse.data?.id).toBeDefined();

		const res = await client.organization.inviteMember(
			{
				teamId: createTeamResponse.data?.id,
				email: invitedUser.email,
				role: "member",
			},
			{
				headers,
			},
		);
		expect(res.data).toBeDefined();
		const newHeaders = new Headers();
		const signUpRes = await client.signUp.email(invitedUser, {
			onSuccess: cookieSetter(newHeaders),
		});

		expect(signUpRes.data?.user).toBeDefined();

		const acceptInvitationResponse = await client.organization.acceptInvitation(
			{
				invitationId: res.data?.id as string,
			},
			{
				headers: newHeaders,
			},
		);
		expect(acceptInvitationResponse.data).toBeDefined();

		const res2 = await client.organization.inviteMember(
			{
				teamId: createTeamResponse.data?.id,
				email: "[email protected]",
				role: "member",
			},
			{
				headers,
			},
		);
		expect(res2.data).toBeNull();
		expect(res2.error?.code).toEqual("TEAM_MEMBER_LIMIT_REACHED");
	});
});

describe("mulit team support", async (it) => {
	const { auth, signInWithTestUser, cookieSetter } = await getTestInstance(
		{
			plugins: [
				organization({
					async sendInvitationEmail() {},
					teams: {
						enabled: true,
						defaultTeam: {
							enabled: true,
						},
					},
				}),
			],
			logger: {
				level: "error",
			},
		},
		{
			testWith: "sqlite",
		},
	);

	const admin = await signInWithTestUser();

	const invitedUser = await auth.api.signUpEmail({
		body: {
			name: "Invited User",
			email: "[email protected]",
			password: "password",
		},
		returnHeaders: true,
	});

	let organizationId: string | null = null;

	let team1Id: string | null = null;
	let team2Id: string | null = null;
	let team3Id: string | null = null;

	let invitationId: string | null = null;

	it("should create an organization to test multi team support", async () => {
		const organization = await auth.api.createOrganization({
			headers: admin.headers,
			body: {
				name: "Test Organization",
				slug: "test-org",
				metadata: {
					test: "organization-metadata",
				},
			},
		});

		expect(organization?.id).toBeDefined();
		expect(organization?.name).toBe("Test Organization");

		organizationId = organization?.id as string;
	});

	it("should create 3 teams", async () => {
		expect(organizationId).toBeDefined();
		if (!organizationId) throw Error("can not run test");

		const team1 = await auth.api.createTeam({
			headers: admin.headers,
			body: {
				name: "Team One",
				organizationId,
			},
		});

		expect(team1.id).toBeDefined();
		expect(team1.organizationId).toBe(organizationId);

		team1Id = team1.id;

		const team2 = await auth.api.createTeam({
			headers: admin.headers,
			body: {
				name: "Team Two",
				organizationId,
			},
		});

		expect(team2.id).toBeDefined();
		expect(team2.organizationId).toBe(organizationId);

		team2Id = team2.id;

		const team3 = await auth.api.createTeam({
			headers: admin.headers,
			body: {
				name: "Team Three",
				organizationId,
			},
		});

		expect(team3.id).toBeDefined();
		expect(team3.organizationId).toBe(organizationId);

		team3Id = team3.id;
	});

	it("should invite user to all 3 teams", async () => {
		expect(organizationId).toBeDefined();
		expect(team1Id).toBeDefined();
		expect(team2Id).toBeDefined();
		expect(team3Id).toBeDefined();

		if (!organizationId || !team1Id || !team2Id || !team3Id)
			throw Error("can not run test");

		const invitation = await auth.api.createInvitation({
			headers: admin.headers,
			body: {
				email: invitedUser.response.user.email,
				role: "member",
				organizationId,
				teamId: [team1Id, team2Id, team3Id],
			},
		});

		expect(invitation.id).toBeDefined();
		expect((invitation as any).teamId).toBe(
			[team1Id, team2Id, team3Id].join(","),
		);

		invitationId = invitation.id!;
	});

	it("should accept invite and join all 3 teams", async () => {
		expect(invitationId).toBeDefined();

		if (!invitationId) throw Error("can not run test");

		const accept = await auth.api.acceptInvitation({
			headers: { cookie: invitedUser.headers.getSetCookie()[0]! },
			body: {
				invitationId,
			},
		});

		expect(accept?.member).toBeDefined();
		expect(accept?.invitation).toBeDefined();
	});

	it("should have jonied all 3 teams", async () => {
		expect(invitationId).toBeDefined();

		if (!invitationId) throw Error("can not run test");

		const teams = await auth.api.listUserTeams({
			headers: { cookie: invitedUser.headers.getSetCookie()[0]! },
		});

		expect(teams).toHaveLength(3);
	});

	let activeTeamCookie: string | null = null;

	it("should allow you to set one of the teams as active", async () => {
		expect(team1Id).toBeDefined();
		expect(organizationId).toBeDefined();

		if (!team1Id || !organizationId) throw Error("can not run test");

		const team = await auth.api.setActiveTeam({
			headers: { cookie: invitedUser.headers.getSetCookie()[0]! },
			body: {
				teamId: team1Id,
			},
			returnHeaders: true,
		});

		expect(team.response?.id).toBe(team1Id);
		expect(team.response?.organizationId).toBe(organizationId);

		activeTeamCookie = team.headers.getSetCookie()[0]!;
	});

	it("should allow you to list team members of the current active team", async () => {
		expect(activeTeamCookie).toBeDefined();

		if (!activeTeamCookie) throw Error("can not run test");

		const members = await auth.api.listTeamMembers({
			headers: { cookie: activeTeamCookie },
		});

		expect(members).toHaveLength(1);
		expect(members.at(0)!.teamId).toBe(team1Id);
	});

	it("should allow user to list team members of any team the user is in", async () => {
		expect(team2Id).toBeDefined();
		expect(team3Id).toBeDefined();

		if (!team2Id || !team3Id) throw Error("can not run test");

		const team2Members = await auth.api.listTeamMembers({
			headers: { cookie: invitedUser.headers.getSetCookie()[0]! },
			query: {
				teamId: team2Id,
			},
		});

		expect(team2Members).toHaveLength(1);
		expect(team2Members.at(0)!.teamId).toBe(team2Id);

		const team3Members = await auth.api.listTeamMembers({
			headers: { cookie: invitedUser.headers.getSetCookie()[0]! },
			query: {
				teamId: team3Id,
			},
		});

		expect(team3Members).toHaveLength(1);
		expect(team3Members.at(0)!.teamId).toBe(team3Id);
	});

	let team4Id: string | null = null;
	it("should directly add a member to a team", async () => {
		expect(organizationId).toBeDefined();
		if (!organizationId) throw Error("can not run test");

		const team = await auth.api.createTeam({
			headers: admin.headers,
			body: {
				name: "Team Four",
				organizationId,
			},
		});

		const teamMember = await auth.api.addTeamMember({
			headers: admin.headers,
			body: {
				userId: invitedUser.response.user.id,
				teamId: team.id,
			},
		});

		expect(teamMember.teamId).toBe(team.id);
		expect(teamMember.userId).toBe(invitedUser.response.user.id);

		const teams = await auth.api.listUserTeams({
			headers: { cookie: invitedUser.headers.getSetCookie()[0]! },
		});

		expect(teams).toHaveLength(4);

		team4Id = team.id;
	});

	it("should remove a member from a team", async () => {
		expect(team4Id).toBeDefined();
		if (!team4Id) throw Error("can not run test");

		await auth.api.removeTeamMember({
			headers: admin.headers,
			body: {
				userId: invitedUser.response.user.id,
				teamId: team4Id,
			},
		});

		const teams = await auth.api.listUserTeams({
			headers: { cookie: invitedUser.headers.getSetCookie()[0]! },
		});

		expect(teams).toHaveLength(3);
	});

	it("should create invitation without teamId", async () => {
		expect(organizationId).toBeDefined();
		if (!organizationId) throw Error("can not run test");

		const invitation = await auth.api.createInvitation({
			headers: admin.headers,
			body: {
				email: "[email protected]",
				role: "member",
				organizationId,
			},
		});

		expect(invitation.id).toBeDefined();
		expect((invitation as any).teamId).toBeNull();
		expect((invitation as any).teamId).not.toBe("");
	});
});

```

--------------------------------------------------------------------------------
/docs/content/docs/plugins/polar.mdx:
--------------------------------------------------------------------------------

```markdown
---
title: Polar
description: Better Auth Plugin for Payment and Checkouts using Polar
---

[Polar](https://polar.sh) is a developer first payment infrastructure. Out of the box it provides a lot of developer first integrations for payments, checkouts and more. This plugin helps you integrate Polar with Better Auth to make your auth + payments flow seamless.

<Callout>
  This plugin is maintained by Polar team. For bugs, issues or feature requests,
  please visit the [Polar GitHub
  repo](https://github.com/polarsource/polar-adapters).
</Callout>

## Features

- Checkout Integration
- Customer Portal
- Automatic Customer creation on signup
- Event Ingestion & Customer Meters for flexible Usage Based Billing
- Handle Polar Webhooks securely with signature verification
- Reference System to associate purchases with organizations

## Installation

```bash
pnpm add better-auth @polar-sh/better-auth @polar-sh/sdk
```

## Preparation

Go to your Polar Organization Settings, and create an Organization Access Token. Add it to your environment.

```bash
# .env
POLAR_ACCESS_TOKEN=...
```

### Configuring BetterAuth Server

The Polar plugin comes with a handful additional plugins which adds functionality to your stack.

- Checkout - Enables a seamless checkout integration
- Portal - Makes it possible for your customers to manage their orders, subscriptions & granted benefits
- Usage - Simple extension for listing customer meters & ingesting events for Usage Based Billing
- Webhooks - Listen for relevant Polar webhooks

```typescript
import { betterAuth } from "better-auth";
import { polar, checkout, portal, usage, webhooks } from "@polar-sh/better-auth";
import { Polar } from "@polar-sh/sdk";

const polarClient = new Polar({
    accessToken: process.env.POLAR_ACCESS_TOKEN,
    // Use 'sandbox' if you're using the Polar Sandbox environment
    // Remember that access tokens, products, etc. are completely separated between environments.
    // Access tokens obtained in Production are for instance not usable in the Sandbox environment.
    server: 'sandbox'
});

const auth = betterAuth({
    // ... Better Auth config
    plugins: [
        polar({
            client: polarClient,
            createCustomerOnSignUp: true,
            use: [
                checkout({
                    products: [
                        {
                            productId: "123-456-789", // ID of Product from Polar Dashboard
                            slug: "pro" // Custom slug for easy reference in Checkout URL, e.g. /checkout/pro
                        }
                    ],
                    successUrl: "/success?checkout_id={CHECKOUT_ID}",
                    authenticatedUsersOnly: true
                }),
                portal(),
                usage(),
                webhooks({
                    secret: process.env.POLAR_WEBHOOK_SECRET,
                    onCustomerStateChanged: (payload) => // Triggered when anything regarding a customer changes
                    onOrderPaid: (payload) => // Triggered when an order was paid (purchase, subscription renewal, etc.)
                    ...  // Over 25 granular webhook handlers
                    onPayload: (payload) => // Catch-all for all events
                })
            ],
        })
    ]
});
```

### Configuring BetterAuth Client

You will be using the BetterAuth Client to interact with the Polar functionalities.

```typescript
import { createAuthClient } from "better-auth/react";
import { polarClient } from "@polar-sh/better-auth";

// This is all that is needed
// All Polar plugins, etc. should be attached to the server-side BetterAuth config
export const authClient = createAuthClient({
  plugins: [polarClient()],
});
```

## Configuration Options

```typescript
import { betterAuth } from "better-auth";
import {
  polar,
  checkout,
  portal,
  usage,
  webhooks,
} from "@polar-sh/better-auth";
import { Polar } from "@polar-sh/sdk";

const polarClient = new Polar({
  accessToken: process.env.POLAR_ACCESS_TOKEN,
  // Use 'sandbox' if you're using the Polar Sandbox environment
  // Remember that access tokens, products, etc. are completely separated between environments.
  // Access tokens obtained in Production are for instance not usable in the Sandbox environment.
  server: "sandbox",
});

const auth = betterAuth({
  // ... Better Auth config
  plugins: [
    polar({
      client: polarClient,
      createCustomerOnSignUp: true,
      getCustomerCreateParams: ({ user }, request) => ({
        metadata: {
          myCustomProperty: 123,
        },
      }),
      use: [
        // This is where you add Polar plugins
      ],
    }),
  ],
});
```

### Required Options

- `client`: Polar SDK client instance

### Optional Options

- `createCustomerOnSignUp`: Automatically create a Polar customer when a user signs up
- `getCustomerCreateParams`: Custom function to provide additional customer creation metadata

### Customers

When `createCustomerOnSignUp` is enabled, a new Polar Customer is automatically created when a new User is added in the Better-Auth Database.

All new customers are created with an associated `externalId`, which is the ID of your User in the Database. This allows us to skip any Polar to User mapping in your Database.

## Checkout Plugin

To support checkouts in your app, simply pass the Checkout plugin to the use-property.

```typescript
import { polar, checkout } from "@polar-sh/better-auth";

const auth = betterAuth({
    // ... Better Auth config
    plugins: [
        polar({
            ...
            use: [
                checkout({
                    // Optional field - will make it possible to pass a slug to checkout instead of Product ID
                    products: [ { productId: "123-456-789", slug: "pro" } ],
                    // Relative URL to return to when checkout is successfully completed
                    successUrl: "/success?checkout_id={CHECKOUT_ID}",
                    // Wheather you want to allow unauthenticated checkout sessions or not
                    authenticatedUsersOnly: true
                })
            ],
        })
    ]
});
```

When checkouts are enabled, you're able to initialize Checkout Sessions using the checkout-method on the BetterAuth Client. This will redirect the user to the Product Checkout.

```typescript
await authClient.checkout({
  // Any Polar Product ID can be passed here
  products: ["e651f46d-ac20-4f26-b769-ad088b123df2"],
  // Or, if you setup "products" in the Checkout Config, you can pass the slug
  slug: "pro",
});
```

Checkouts will automatically carry the authenticated User as the customer to the checkout. Email-address will be "locked-in".

If `authenticatedUsersOnly` is `false` - then it will be possible to trigger checkout sessions without any associated customer.

### Organization Support

This plugin supports the Organization plugin. If you pass the organization ID to the Checkout referenceId, you will be able to keep track of purchases made from organization members.

```typescript
const organizationId = (await authClient.organization.list())?.data?.[0]?.id,

await authClient.checkout({
    // Any Polar Product ID can be passed here
    products: ["e651f46d-ac20-4f26-b769-ad088b123df2"],
    // Or, if you setup "products" in the Checkout Config, you can pass the slug
    slug: 'pro',
    // Reference ID will be saved as `referenceId` in the metadata of the checkout, order & subscription object
    referenceId: organizationId
});
```

## Portal Plugin

A plugin which enables customer management of their purchases, orders and subscriptions.

```typescript
import { polar, checkout, portal } from "@polar-sh/better-auth";

const auth = betterAuth({
    // ... Better Auth config
    plugins: [
        polar({
            ...
            use: [
                checkout(...),
                portal()
            ],
        })
    ]
});
```

The portal-plugin gives the BetterAuth Client a set of customer management methods, scoped under `authClient.customer`.

### Customer Portal Management

The following method will redirect the user to the Polar Customer Portal, where they can see orders, purchases, subscriptions, benefits, etc.

```typescript
await authClient.customer.portal();
```

### Customer State

The portal plugin also adds a convenient state-method for retrieving the general Customer State.

```typescript
const { data: customerState } = await authClient.customer.state();
```

The customer state object contains:

- All the data about the customer.
- The list of their active subscriptions
  - Note: This does not include subscriptions done by a parent organization. See the subscription list-method below for more information.
- The list of their granted benefits.
- The list of their active meters, with their current balance.

Thus, with that single object, you have all the required information to check if you should provision access to your service or not.

[You can learn more about the Polar Customer State in the Polar Docs](https://docs.polar.sh/integrate/customer-state).

### Benefits, Orders & Subscriptions

The portal plugin adds 3 convenient methods for listing benefits, orders & subscriptions relevant to the authenticated user/customer.

[All of these methods use the Polar CustomerPortal APIs](https://docs.polar.sh/api-reference/customer-portal)

#### Benefits

This method only lists granted benefits for the authenticated user/customer.

```typescript
const { data: benefits } = await authClient.customer.benefits.list({
  query: {
    page: 1,
    limit: 10,
  },
});
```

#### Orders

This method lists orders like purchases and subscription renewals for the authenticated user/customer.

```typescript
const { data: orders } = await authClient.customer.orders.list({
  query: {
    page: 1,
    limit: 10,
    productBillingType: "one_time", // or 'recurring'
  },
});
```

#### Subscriptions

This method lists the subscriptions associated with authenticated user/customer.

```typescript
const { data: subscriptions } = await authClient.customer.subscriptions.list({
  query: {
    page: 1,
    limit: 10,
    active: true,
  },
});
```

**Important** - Organization Support

This will **not** return subscriptions made by a parent organization to the authenticated user.

However, you can pass a `referenceId` to this method. This will return all subscriptions associated with that referenceId instead of subscriptions associated with the user.

So in order to figure out if a user should have access, pass the user's organization ID to see if there is an active subscription for that organization.

```typescript
const organizationId = (await authClient.organization.list())?.data?.[0]?.id,

const { data: subscriptions } = await authClient.customer.orders.list({
    query: {
	    page: 1,
		limit: 10,
		active: true,
        referenceId: organizationId
    },
});

const userShouldHaveAccess = subscriptions.some(
    sub => // Your logic to check subscription product or whatever.
)
```

## Usage Plugin

A simple plugin for Usage Based Billing.

```typescript
import { polar, checkout, portal, usage } from "@polar-sh/better-auth";

const auth = betterAuth({
    // ... Better Auth config
    plugins: [
        polar({
            ...
            use: [
                checkout(...),
                portal(),
                usage()
            ],
        })
    ]
});
```

### Event Ingestion

Polar's Usage Based Billing builds entirely on event ingestion. Ingest events from your application, create Meters to represent that usage, and add metered prices to Products to charge for it.

[Learn more about Usage Based Billing in the Polar Docs.](https://docs.polar.sh/features/usage-based-billing/introduction)

```typescript
const { data: ingested } = await authClient.usage.ingest({
  event: "file-uploads",
  metadata: {
    uploadedFiles: 12,
  },
});
```

The authenticated user is automatically associated with the ingested event.

### Customer Meters

A simple method for listing the authenticated user's Usage Meters, or as we call them, Customer Meters.

Customer Meter's contains all information about their consumtion on your defined meters.

- Customer Information
- Meter Information
- Customer Meter Information
  - Consumed Units
  - Credited Units
  - Balance

```typescript
const { data: customerMeters } = await authClient.usage.meters.list({
  query: {
    page: 1,
    limit: 10,
  },
});
```

## Webhooks Plugin

The Webhooks plugin can be used to capture incoming events from your Polar organization.

```typescript
import { polar, webhooks } from "@polar-sh/better-auth";

const auth = betterAuth({
    // ... Better Auth config
    plugins: [
        polar({
            ...
            use: [
                webhooks({
                    secret: process.env.POLAR_WEBHOOK_SECRET,
                    onCustomerStateChanged: (payload) => // Triggered when anything regarding a customer changes
                    onOrderPaid: (payload) => // Triggered when an order was paid (purchase, subscription renewal, etc.)
                    ...  // Over 25 granular webhook handlers
                    onPayload: (payload) => // Catch-all for all events
                })
            ],
        })
    ]
});
```

Configure a Webhook endpoint in your Polar Organization Settings page. Webhook endpoint is configured at /polar/webhooks.

Add the secret to your environment.

```bash
# .env
POLAR_WEBHOOK_SECRET=...
```

The plugin supports handlers for all Polar webhook events:

- `onPayload` - Catch-all handler for any incoming Webhook event
- `onCheckoutCreated` - Triggered when a checkout is created
- `onCheckoutUpdated` - Triggered when a checkout is updated
- `onOrderCreated` - Triggered when an order is created
- `onOrderPaid` - Triggered when an order is paid
- `onOrderRefunded` - Triggered when an order is refunded
- `onRefundCreated` - Triggered when a refund is created
- `onRefundUpdated` - Triggered when a refund is updated
- `onSubscriptionCreated` - Triggered when a subscription is created
- `onSubscriptionUpdated` - Triggered when a subscription is updated
- `onSubscriptionActive` - Triggered when a subscription becomes active
- `onSubscriptionCanceled` - Triggered when a subscription is canceled
- `onSubscriptionRevoked` - Triggered when a subscription is revoked
- `onSubscriptionUncanceled` - Triggered when a subscription cancellation is reversed
- `onProductCreated` - Triggered when a product is created
- `onProductUpdated` - Triggered when a product is updated
- `onOrganizationUpdated` - Triggered when an organization is updated
- `onBenefitCreated` - Triggered when a benefit is created
- `onBenefitUpdated` - Triggered when a benefit is updated
- `onBenefitGrantCreated` - Triggered when a benefit grant is created
- `onBenefitGrantUpdated` - Triggered when a benefit grant is updated
- `onBenefitGrantRevoked` - Triggered when a benefit grant is revoked
- `onCustomerCreated` - Triggered when a customer is created
- `onCustomerUpdated` - Triggered when a customer is updated
- `onCustomerDeleted` - Triggered when a customer is deleted
- `onCustomerStateChanged` - Triggered when a customer is created

```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/username/index.ts:
--------------------------------------------------------------------------------

```typescript
import * as z from "zod";
import {
	createAuthEndpoint,
	createAuthMiddleware,
} from "@better-auth/core/api";
import type { BetterAuthPlugin } from "@better-auth/core";
import { APIError } from "better-call";
import type { Account, User } from "@better-auth/core/db";
import { setSessionCookie } from "../../cookies";
import { BASE_ERROR_CODES } from "@better-auth/core/error";
import { getSchema, type UsernameSchema } from "./schema";
import { mergeSchema } from "../../db";
import { USERNAME_ERROR_CODES as ERROR_CODES } from "./error-codes";
import { createEmailVerificationToken } from "../../api";
import type { InferOptionSchema } from "../../types/plugins";

export { USERNAME_ERROR_CODES } from "./error-codes";

export type UsernameOptions = {
	schema?: InferOptionSchema<UsernameSchema>;
	/**
	 * The minimum length of the username
	 *
	 * @default 3
	 */
	minUsernameLength?: number;
	/**
	 * The maximum length of the username
	 *
	 * @default 30
	 */
	maxUsernameLength?: number;
	/**
	 * A function to validate the username
	 *
	 * By default, the username should only contain alphanumeric characters and underscores
	 */
	usernameValidator?: (username: string) => boolean | Promise<boolean>;
	/**
	 * A function to validate the display username
	 *
	 * By default, no validation is applied to display username
	 */
	displayUsernameValidator?: (
		displayUsername: string,
	) => boolean | Promise<boolean>;
	/**
	 * A function to normalize the username
	 *
	 * @default (username) => username.toLowerCase()
	 */
	usernameNormalization?: ((username: string) => string) | false;
	/**
	 * A function to normalize the display username
	 *
	 * @default false
	 */
	displayUsernameNormalization?: ((displayUsername: string) => string) | false;
	/**
	 * The order of validation
	 *
	 * @default { username: "pre-normalization", displayUsername: "pre-normalization" }
	 */
	validationOrder?: {
		/**
		 * The order of username validation
		 *
		 * @default "pre-normalization"
		 */
		username?: "pre-normalization" | "post-normalization";
		/**
		 * The order of display username validation
		 *
		 * @default "pre-normalization"
		 */
		displayUsername?: "pre-normalization" | "post-normalization";
	};
};

function defaultUsernameValidator(username: string) {
	return /^[a-zA-Z0-9_.]+$/.test(username);
}

export const username = (options?: UsernameOptions) => {
	const normalizer = (username: string) => {
		if (options?.usernameNormalization === false) {
			return username;
		}
		if (options?.usernameNormalization) {
			return options.usernameNormalization(username);
		}
		return username.toLowerCase();
	};

	const displayUsernameNormalizer = (displayUsername: string) => {
		return options?.displayUsernameNormalization
			? options.displayUsernameNormalization(displayUsername)
			: displayUsername;
	};

	return {
		id: "username",
		init(ctx) {
			return {
				options: {
					databaseHooks: {
						user: {
							create: {
								async before(user, context) {
									const username =
										"username" in user ? (user.username as string) : null;
									const displayUsername =
										"displayUsername" in user
											? (user.displayUsername as string)
											: null;

									return {
										data: {
											...user,
											...(username ? { username: normalizer(username) } : {}),
											...(displayUsername
												? {
														displayUsername:
															displayUsernameNormalizer(displayUsername),
													}
												: {}),
										},
									};
								},
							},
							update: {
								async before(user, context) {
									const username =
										"username" in user ? (user.username as string) : null;
									const displayUsername =
										"displayUsername" in user
											? (user.displayUsername as string)
											: null;

									return {
										data: {
											...user,
											...(username ? { username: normalizer(username) } : {}),
											...(displayUsername
												? {
														displayUsername:
															displayUsernameNormalizer(displayUsername),
													}
												: {}),
										},
									};
								},
							},
						},
					},
				},
			};
		},
		endpoints: {
			signInUsername: createAuthEndpoint(
				"/sign-in/username",
				{
					method: "POST",
					body: z.object({
						username: z
							.string()
							.meta({ description: "The username of the user" }),
						password: z
							.string()
							.meta({ description: "The password of the user" }),
						rememberMe: z
							.boolean()
							.meta({
								description: "Remember the user session",
							})
							.optional(),
						callbackURL: z
							.string()
							.meta({
								description: "The URL to redirect to after email verification",
							})
							.optional(),
					}),
					metadata: {
						openapi: {
							summary: "Sign in with username",
							description: "Sign in with username",
							responses: {
								200: {
									description: "Success",
									content: {
										"application/json": {
											schema: {
												type: "object",
												properties: {
													token: {
														type: "string",
														description:
															"Session token for the authenticated session",
													},
													user: {
														$ref: "#/components/schemas/User",
													},
												},
												required: ["token", "user"],
											},
										},
									},
								},
								422: {
									description: "Unprocessable Entity. Validation error",
									content: {
										"application/json": {
											schema: {
												type: "object",
												properties: {
													message: {
														type: "string",
													},
												},
											},
										},
									},
								},
							},
						},
					},
				},
				async (ctx) => {
					if (!ctx.body.username || !ctx.body.password) {
						ctx.context.logger.error("Username or password not found");
						throw new APIError("UNAUTHORIZED", {
							message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
						});
					}

					const username =
						options?.validationOrder?.username === "pre-normalization"
							? normalizer(ctx.body.username)
							: ctx.body.username;

					const minUsernameLength = options?.minUsernameLength || 3;
					const maxUsernameLength = options?.maxUsernameLength || 30;

					if (username.length < minUsernameLength) {
						ctx.context.logger.error("Username too short", {
							username,
						});
						throw new APIError("UNPROCESSABLE_ENTITY", {
							message: ERROR_CODES.USERNAME_TOO_SHORT,
						});
					}

					if (username.length > maxUsernameLength) {
						ctx.context.logger.error("Username too long", {
							username,
						});
						throw new APIError("UNPROCESSABLE_ENTITY", {
							message: ERROR_CODES.USERNAME_TOO_LONG,
						});
					}

					const validator =
						options?.usernameValidator || defaultUsernameValidator;

					if (!validator(username)) {
						throw new APIError("UNPROCESSABLE_ENTITY", {
							message: ERROR_CODES.INVALID_USERNAME,
						});
					}

					const user = await ctx.context.adapter.findOne<
						User & { username: string; displayUsername: string }
					>({
						model: "user",
						where: [
							{
								field: "username",
								value: normalizer(username),
							},
						],
					});
					if (!user) {
						// Hash password to prevent timing attacks from revealing valid usernames
						// By hashing passwords for invalid usernames, we ensure consistent response times
						await ctx.context.password.hash(ctx.body.password);
						ctx.context.logger.error("User not found", {
							username,
						});
						throw new APIError("UNAUTHORIZED", {
							message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
						});
					}

					const account = await ctx.context.adapter.findOne<Account>({
						model: "account",
						where: [
							{
								field: "userId",
								value: user.id,
							},
							{
								field: "providerId",
								value: "credential",
							},
						],
					});
					if (!account) {
						throw new APIError("UNAUTHORIZED", {
							message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
						});
					}
					const currentPassword = account?.password;
					if (!currentPassword) {
						ctx.context.logger.error("Password not found", {
							username,
						});
						throw new APIError("UNAUTHORIZED", {
							message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
						});
					}
					const validPassword = await ctx.context.password.verify({
						hash: currentPassword,
						password: ctx.body.password,
					});
					if (!validPassword) {
						ctx.context.logger.error("Invalid password");
						throw new APIError("UNAUTHORIZED", {
							message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
						});
					}

					if (
						ctx.context.options?.emailAndPassword?.requireEmailVerification &&
						!user.emailVerified
					) {
						if (
							!ctx.context.options?.emailVerification?.sendVerificationEmail
						) {
							throw new APIError("FORBIDDEN", {
								message: ERROR_CODES.EMAIL_NOT_VERIFIED,
							});
						}

						if (ctx.context.options?.emailVerification?.sendOnSignIn) {
							const token = await createEmailVerificationToken(
								ctx.context.secret,
								user.email,
								undefined,
								ctx.context.options.emailVerification?.expiresIn,
							);
							const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${
								ctx.body.callbackURL || "/"
							}`;
							await ctx.context.options.emailVerification.sendVerificationEmail(
								{
									user: user,
									url,
									token,
								},
								ctx.request,
							);
						}

						throw new APIError("FORBIDDEN", {
							message: ERROR_CODES.EMAIL_NOT_VERIFIED,
						});
					}

					const session = await ctx.context.internalAdapter.createSession(
						user.id,
						ctx.body.rememberMe === false,
					);
					if (!session) {
						return ctx.json(null, {
							status: 500,
							body: {
								message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
							},
						});
					}
					await setSessionCookie(
						ctx,
						{ session, user },
						ctx.body.rememberMe === false,
					);
					return ctx.json({
						token: session.token,
						user: {
							id: user.id,
							email: user.email,
							emailVerified: user.emailVerified,
							username: user.username,
							displayUsername: user.displayUsername,
							name: user.name,
							image: user.image,
							createdAt: user.createdAt,
							updatedAt: user.updatedAt,
						},
					});
				},
			),
			isUsernameAvailable: createAuthEndpoint(
				"/is-username-available",
				{
					method: "POST",
					body: z.object({
						username: z.string().meta({
							description: "The username to check",
						}),
					}),
				},
				async (ctx) => {
					const username = ctx.body.username;
					if (!username) {
						throw new APIError("UNPROCESSABLE_ENTITY", {
							message: ERROR_CODES.INVALID_USERNAME,
						});
					}

					const minUsernameLength = options?.minUsernameLength || 3;
					const maxUsernameLength = options?.maxUsernameLength || 30;

					if (username.length < minUsernameLength) {
						throw new APIError("UNPROCESSABLE_ENTITY", {
							message: ERROR_CODES.USERNAME_TOO_SHORT,
						});
					}

					if (username.length > maxUsernameLength) {
						throw new APIError("UNPROCESSABLE_ENTITY", {
							message: ERROR_CODES.USERNAME_TOO_LONG,
						});
					}

					const validator =
						options?.usernameValidator || defaultUsernameValidator;

					if (!(await validator(username))) {
						throw new APIError("UNPROCESSABLE_ENTITY", {
							message: ERROR_CODES.INVALID_USERNAME,
						});
					}
					const user = await ctx.context.adapter.findOne<User>({
						model: "user",
						where: [
							{
								field: "username",
								value: normalizer(username),
							},
						],
					});
					if (user) {
						return ctx.json({
							available: false,
						});
					}
					return ctx.json({
						available: true,
					});
				},
			),
		},
		schema: mergeSchema(
			getSchema({
				username: normalizer,
				displayUsername: displayUsernameNormalizer,
			}),
			options?.schema,
		),
		hooks: {
			before: [
				{
					matcher(context) {
						return (
							context.path === "/sign-up/email" ||
							context.path === "/update-user"
						);
					},
					handler: createAuthMiddleware(async (ctx) => {
						const username =
							typeof ctx.body.username === "string" &&
							options?.validationOrder?.username === "post-normalization"
								? normalizer(ctx.body.username)
								: ctx.body.username;

						if (username !== undefined && typeof username === "string") {
							const minUsernameLength = options?.minUsernameLength || 3;
							const maxUsernameLength = options?.maxUsernameLength || 30;
							if (username.length < minUsernameLength) {
								throw new APIError("BAD_REQUEST", {
									message: ERROR_CODES.USERNAME_TOO_SHORT,
								});
							}

							if (username.length > maxUsernameLength) {
								throw new APIError("BAD_REQUEST", {
									message: ERROR_CODES.USERNAME_TOO_LONG,
								});
							}

							const validator =
								options?.usernameValidator || defaultUsernameValidator;

							const valid = await validator(username);
							if (!valid) {
								throw new APIError("BAD_REQUEST", {
									message: ERROR_CODES.INVALID_USERNAME,
								});
							}
							const user = await ctx.context.adapter.findOne<User>({
								model: "user",
								where: [
									{
										field: "username",
										value: username,
									},
								],
							});

							const blockChangeSignUp = ctx.path === "/sign-up/email" && user;
							const blockChangeUpdateUser =
								ctx.path === "/update-user" &&
								user &&
								ctx.context.session &&
								user.id !== ctx.context.session.session.userId;
							if (blockChangeSignUp || blockChangeUpdateUser) {
								throw new APIError("BAD_REQUEST", {
									message: ERROR_CODES.USERNAME_IS_ALREADY_TAKEN,
								});
							}
						}

						const displayUsername =
							typeof ctx.body.displayUsername === "string" &&
							options?.validationOrder?.displayUsername === "post-normalization"
								? displayUsernameNormalizer(ctx.body.displayUsername)
								: ctx.body.displayUsername;

						if (
							displayUsername !== undefined &&
							typeof displayUsername === "string"
						) {
							if (options?.displayUsernameValidator) {
								const valid =
									await options.displayUsernameValidator(displayUsername);
								if (!valid) {
									throw new APIError("BAD_REQUEST", {
										message: ERROR_CODES.INVALID_DISPLAY_USERNAME,
									});
								}
							}
						}
					}),
				},
				{
					matcher(context) {
						return (
							context.path === "/sign-up/email" ||
							context.path === "/update-user"
						);
					},
					handler: createAuthMiddleware(async (ctx) => {
						if (ctx.body.username && !ctx.body.displayUsername) {
							ctx.body.displayUsername = ctx.body.username;
						}
						if (ctx.body.displayUsername && !ctx.body.username) {
							ctx.body.username = ctx.body.displayUsername;
						}
					}),
				},
			],
		},
		$ERROR_CODES: ERROR_CODES,
	} satisfies BetterAuthPlugin;
};

```

--------------------------------------------------------------------------------
/docs/content/docs/concepts/users-accounts.mdx:
--------------------------------------------------------------------------------

```markdown
---
title: User & Accounts
description: User and account management.
---

Beyond authenticating users, Better Auth also provides a set of methods to manage users. This includes, updating user information, changing passwords, and more.

The user table stores the authentication data of the user [Click here to view the schema](/docs/concepts/database#user).

The user table can be extended using [additional fields](/docs/concepts/database#extending-core-schema) or by plugins to store additional data.

## Update User

### Update User Information

To update user information, you can use the `updateUser` function provided by the client. The `updateUser` function takes an object with the following properties:

```ts
await authClient.updateUser({
    image: "https://example.com/image.jpg",
    name: "John Doe",
})
```
### Change Email

To allow users to change their email, first enable the `changeEmail` feature, which is disabled by default. Set `changeEmail.enabled` to `true`:

```ts
export const auth = betterAuth({
    user: {
        changeEmail: {
            enabled: true,
        }
    }
})
```

For users with a verified email, provide the `sendChangeEmailVerification` function. This function triggers when a user changes their email, sending a verification email with a URL and token. If the current email isn't verified, the change happens immediately without verification.

```ts
export const auth = betterAuth({
    user: {
        changeEmail: {
            enabled: true,
            sendChangeEmailVerification: async ({ user, newEmail, url, token }, request) => {
                await sendEmail({
                    to: user.email, // verification email must be sent to the current user email to approve the change
                    subject: 'Approve email change',
                    text: `Click the link to approve the change: ${url}`
                })
            }
        }
    }
})
```

Once enabled, use the `changeEmail` function on the client to update a user’s email. The user must verify their current email before changing it.

```ts
await authClient.changeEmail({
    newEmail: "[email protected]",
    callbackURL: "/dashboard", //to redirect after verification
});
```

After verification, the new email is updated in the user table, and a confirmation is sent to the new address.

<Callout type="warn">
    If the current email is unverified, the new email is updated without the verification step.
</Callout>
 
### Change Password
A user's password isn't stored in the user table. Instead, it's stored in the account table. To change the password of a user, you can use one of the following approaches:


<APIMethod path="/change-password" method="POST" requireSession>
```ts
type changePassword = {
    /**
     * The new password to set 
     */
    newPassword: string = "newpassword1234"
    /**
     * The current user password 
     */
    currentPassword: string = "oldpassword1234"
    /**
     * When set to true, all other active sessions for this user will be invalidated
     */
    revokeOtherSessions?: boolean = true
}
```
</APIMethod>

### Set Password

If a user was registered using OAuth or other providers, they won't have a password or a credential account. In this case, you can use the `setPassword` action to set a password for the user. For security reasons, this function can only be called from the server. We recommend having users go through a 'forgot password' flow to set a password for their account.

```ts
await auth.api.setPassword({
    body: { newPassword: "password" },
    headers: // headers containing the user's session token
});
```

## Delete User

Better Auth provides a utility to hard delete a user from your database. It's disabled by default, but you can enable it easily by passing `enabled:true`

```ts
export const auth = betterAuth({
    //...other config
    user: {
        deleteUser: { // [!code highlight]
            enabled: true // [!code highlight]
        } // [!code highlight]
    }
})
```

Once enabled, you can call `authClient.deleteUser` to permanently delete user data from your database.

### Adding Verification Before Deletion

For added security, you’ll likely want to confirm the user’s intent before deleting their account. A common approach is to send a verification email. Better Auth provides a `sendDeleteAccountVerification` utility for this purpose. 
This is especially needed if you have OAuth setup and want them to be able to delete their account without forcing them to login again for a fresh session.

Here’s how you can set it up:

```ts
export const auth = betterAuth({
    user: {
        deleteUser: {
            enabled: true,
            sendDeleteAccountVerification: async (
                {
                    user,   // The user object
                    url, // The auto-generated URL for deletion
                    token  // The verification token  (can be used to generate custom URL)
                },
                request  // The original request object (optional)
            ) => {
                // Your email sending logic here
                // Example: sendEmail(data.user.email, "Verify Deletion", data.url);
            },
        },
    },
});
```

**How callback verification works:**

- **Callback URL**: The URL provided in `sendDeleteAccountVerification` is a pre-generated link that deletes the user data when accessed.

```ts title="delete-user.ts"
await authClient.deleteUser({
    callbackURL: "/goodbye" // you can provide a callback URL to redirect after deletion
});
```

- **Authentication Check**: The user must be signed in to the account they’re attempting to delete.
If they aren’t signed in, the deletion process will fail. 

If you have sent a custom URL, you can use the `deleteUser` method with the token to delete the user.

```ts title="delete-user.ts"
await authClient.deleteUser({
    token
});
```

### Authentication Requirements

To delete a user, the user must meet one of the following requirements:

1. A valid password

if the user has a password, they can delete their account by providing the password.

```ts title="delete-user.ts"
await authClient.deleteUser({
    password: "password"
});
```

2. Fresh session

The user must have a `fresh` session token, meaning the user must have signed in recently. This is checked if the password is not provided.

<Callout type="warn">
By default `session.freshAge` is set to `60 * 60 * 24` (1 day). You can change this value by passing the `session` object to the `auth` configuration. If it is set to `0`, the freshness check is disabled. It is recommended not to disable this check if you are not using email verification for deleting the account.
</Callout>

```ts title="delete-user.ts"
await authClient.deleteUser();
```

3. Enabled email verification (needed for OAuth users)

As OAuth users don't have a password, we need to send a verification email to confirm the user's intent to delete their account. If you have already added the `sendDeleteAccountVerification` callback, you can just call the `deleteUser` method without providing any other information.

```ts title="delete-user.ts"
await authClient.deleteUser();
```

4. If you have a custom delete account page and sent that url via the `sendDeleteAccountVerification` callback.
Then you need to call the `deleteUser` method with the token to complete the deletion.

```ts title="delete-user.ts"
await authClient.deleteUser({
    token
});
```

### Callbacks

**beforeDelete**: This callback is called before the user is deleted. You can use this callback to perform any cleanup or additional checks before deleting the user.

```ts title="auth.ts"
export const auth = betterAuth({
    user: {
        deleteUser: {
            enabled: true,
            beforeDelete: async (user) => {
                // Perform any cleanup or additional checks here
            },
        },
    },
});
```
you can also throw `APIError` to interrupt the deletion process.

```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { APIError } from "better-auth/api";

export const auth = betterAuth({
    user: {
        deleteUser: {
            enabled: true,
            beforeDelete: async (user, request) => {
                if (user.email.includes("admin")) {
                    throw new APIError("BAD_REQUEST", {
                        message: "Admin accounts can't be deleted",
                    });
                }
            },
        },
    },
});
```

**afterDelete**: This callback is called after the user is deleted. You can use this callback to perform any cleanup or additional actions after the user is deleted.

```ts title="auth.ts"
export const auth = betterAuth({
    user: {
        deleteUser: {
            enabled: true,
            afterDelete: async (user, request) => {
                // Perform any cleanup or additional actions here
            },
        },
    },
});
```

## Accounts

Better Auth supports multiple authentication methods. Each authentication method is called a provider. For example, email and password authentication is a provider, Google authentication is a provider, etc.

When a user signs in using a provider, an account is created for the user. The account stores the authentication data returned by the provider. This data includes the access token, refresh token, and other information returned by the provider.

The account table stores the authentication data of the user [Click here to view the schema](/docs/concepts/database#account)


### List User Accounts

To list user accounts you can use `client.user.listAccounts` method. Which will return all accounts associated with a user.

```ts
const accounts = await authClient.listAccounts();
```

### Token Encryption

Better Auth doesn’t encrypt tokens by default and that’s intentional. We want you to have full control over how encryption and decryption are handled, rather than baking in behavior that could be confusing or limiting. If you need to store encrypted tokens (like accessToken or refreshToken), you can use databaseHooks to encrypt them before they’re saved to your database.

```ts
export const auth = betterAuth({
    databaseHooks: {
        account: {
            create: {
                before(account, context) {
                    const withEncryptedTokens = { ...account };
                    if (account.accessToken) {
                        const encryptedAccessToken = encrypt(account.accessToken)  // [!code highlight]
                        withEncryptedTokens.accessToken = encryptedAccessToken;
                    }
                    if (account.refreshToken) {
                        const encryptedRefreshToken = encrypt(account.refreshToken); // [!code highlight]
                        withEncryptedTokens.refreshToken = encryptedRefreshToken;
                    }
                    return {
                        data: withEncryptedTokens
                    }
                },
            }
        }
    }
})
```

Then whenever you retrieve back the account make sure to decrypt the tokens before using them.

### Account Linking

Account linking enables users to associate multiple authentication methods with a single account. With Better Auth, users can connect additional social sign-ons or OAuth providers to their existing accounts if the provider confirms the user's email as verified.

If account linking is disabled, no accounts can be linked, regardless of the provider or email verification status.

```ts title="auth.ts"
export const auth = betterAuth({
    account: {
        accountLinking: {
            enabled: true, 
        }
    },
});
```

#### Forced Linking

You can specify a list of "trusted providers." When a user logs in using a trusted provider, their account will be automatically linked even if the provider doesn’t confirm the email verification status. Use this with caution as it may increase the risk of account takeover.

```ts title="auth.ts"
export const auth = betterAuth({
    account: {
        accountLinking: {
            enabled: true,
            trustedProviders: ["google", "github"]
        }
    },
});
```

#### Manually Linking Accounts

Users already signed in can manually link their account to additional social providers or credential-based accounts.

- **Linking Social Accounts:** Use the `linkSocial` method on the client to link a social provider to the user's account.

  ```ts
  await authClient.linkSocial({
      provider: "google", // Provider to link
      callbackURL: "/callback" // Callback URL after linking completes
  });
  ```

  You can also request specific scopes when linking a social account, which can be different from the scopes used during the initial authentication:

  ```ts
  await authClient.linkSocial({
      provider: "google",
      callbackURL: "/callback",
      scopes: ["https://www.googleapis.com/auth/drive.readonly"] // Request additional scopes
  });
  ```

  You can also link accounts using ID tokens directly, without redirecting to the provider's OAuth flow:

  ```ts
  await authClient.linkSocial({
      provider: "google",
      idToken: {
          token: "id_token_from_provider",
          nonce: "nonce_used_for_token", // Optional
          accessToken: "access_token", // Optional, may be required by some providers
          refreshToken: "refresh_token" // Optional
      }
  });
  ```

  This is useful when you already have valid tokens from the provider, for example:
  - After signing in with a native SDK
  - When using a mobile app that handles authentication
  - When implementing custom OAuth flows

  The ID token must be valid and the provider must support ID token verification.

  If you want your users to be able to link a social account with a different email address than the user, or if you want to use a provider that does not return email addresses, you will need to enable this in the account linking settings. 
  ```ts title="auth.ts"
  export const auth = betterAuth({
      account: {
          accountLinking: {
              allowDifferentEmails: true
          }
      },
  });
  ```

  If you want the newly linked accounts to update the user information, you need to enable this in the account linking settings. 

  ```ts title="auth.ts"
  export const auth = betterAuth({
      account: {
          accountLinking: {
              updateUserInfoOnLink: true
          }
      },
  });
  ```

- **Linking Credential-Based Accounts:** To link a credential-based account (e.g., email and password), users can initiate a "forgot password" flow, or you can call the `setPassword` method on the server. 

  ```ts
  await auth.api.setPassword({
      headers: /* headers containing the user's session token */,
      password: /* new password */
  });
  ```

<Callout>
`setPassword` can't be called from the client for security reasons.
</Callout>

### Account Unlinking

You can unlink a user account by providing a `providerId`.

```ts
await authClient.unlinkAccount({
    providerId: "google"
});

// Unlink a specific account
await authClient.unlinkAccount({
    providerId: "google",
    accountId: "123"
});
```

If the account doesn't exist, it will throw an error. Additionally, if the user only has one account, unlinking will be prevented to stop account lockout (unless `allowUnlinkingAll` is set to `true`).

```ts title="auth.ts"
export const auth = betterAuth({
    account: {
        accountLinking: {
            allowUnlinkingAll: true
        }
    },
});
```


```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/two-factor/two-factor.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, expect, it, vi } from "vitest";
import { getTestInstance } from "../../test-utils/test-instance";
import { TWO_FACTOR_ERROR_CODES, twoFactor, twoFactorClient } from ".";
import { createAuthClient } from "../../client";
import { parseSetCookieHeader } from "../../cookies";
import type { TwoFactorTable, UserWithTwoFactor } from "./types";
import { DEFAULT_SECRET } from "../../utils/constants";
import { symmetricDecrypt } from "../../crypto";
import { convertSetCookieToCookie } from "../../test-utils/headers";
import { createOTP } from "@better-auth/utils/otp";

describe("two factor", async () => {
	let OTP = "";
	const { testUser, customFetchImpl, sessionSetter, db, auth } =
		await getTestInstance({
			secret: DEFAULT_SECRET,
			plugins: [
				twoFactor({
					otpOptions: {
						sendOTP({ otp }) {
							OTP = otp;
						},
					},
				}),
			],
		});

	const headers = new Headers();

	const client = createAuthClient({
		plugins: [twoFactorClient()],
		fetchOptions: {
			customFetchImpl,
			baseURL: "http://localhost:3000/api/auth",
		},
	});
	const session = await client.signIn.email({
		email: testUser.email,
		password: testUser.password,
		fetchOptions: {
			onSuccess: sessionSetter(headers),
		},
	});
	if (!session) {
		throw new Error("No session");
	}

	it("should return uri and backup codes and shouldn't enable twoFactor yet", async () => {
		const res = await client.twoFactor.enable({
			password: testUser.password,
			fetchOptions: {
				headers,
			},
		});
		expect(res.data?.backupCodes.length).toEqual(10);
		expect(res.data?.totpURI).toBeDefined();
		const dbUser = await db.findOne<UserWithTwoFactor>({
			model: "user",
			where: [
				{
					field: "id",
					value: session.data?.user.id as string,
				},
			],
		});
		const twoFactor = await db.findOne<TwoFactorTable>({
			model: "twoFactor",
			where: [
				{
					field: "userId",
					value: session.data?.user.id as string,
				},
			],
		});
		expect(dbUser?.twoFactorEnabled).toBe(false);
		expect(twoFactor?.secret).toBeDefined();
		expect(twoFactor?.backupCodes).toBeDefined();
	});

	it("should use custom issuer from request parameter", async () => {
		const CUSTOM_ISSUER = "Custom App Name";
		const res = await client.twoFactor.enable({
			password: testUser.password,
			issuer: CUSTOM_ISSUER,
			fetchOptions: {
				headers,
			},
		});

		const totpURI = res.data?.totpURI;
		expect(totpURI).toMatch(
			new RegExp(`^otpauth://totp/${encodeURIComponent(CUSTOM_ISSUER)}:`),
		);
		expect(totpURI).toContain(`&issuer=Custom+App+Name&`);
	});

	it("should fallback to appName when no issuer provided", async () => {
		const res = await client.twoFactor.enable({
			password: testUser.password,
			fetchOptions: {
				headers,
			},
		});

		const totpURI = res.data?.totpURI;
		expect(totpURI).toMatch(/^otpauth:\/\/totp\/Better%20Auth:/);
		expect(totpURI).toContain("&issuer=Better+Auth&");
	});

	it("should enable twoFactor", async () => {
		const twoFactor = await db.findOne<TwoFactorTable>({
			model: "twoFactor",
			where: [
				{
					field: "userId",
					value: session.data?.user.id as string,
				},
			],
		});
		if (!twoFactor) {
			throw new Error("No two factor");
		}

		const decrypted = await symmetricDecrypt({
			key: DEFAULT_SECRET,
			data: twoFactor.secret,
		});
		const code = await createOTP(decrypted).totp();

		const res = await client.twoFactor.verifyTotp({
			code,
			fetchOptions: {
				headers,
				onSuccess: sessionSetter(headers),
			},
		});
		expect(res.data?.token).toBeDefined();
	});

	it("should require two factor", async () => {
		const headers = new Headers();
		const res = await client.signIn.email({
			email: testUser.email,
			password: testUser.password,
			rememberMe: false,
			fetchOptions: {
				onResponse(context) {
					const parsed = parseSetCookieHeader(
						context.response.headers.get("Set-Cookie") || "",
					);
					expect(parsed.get("better-auth.session_token")?.value).toBe("");
					expect(parsed.get("better-auth.two_factor")?.value).toBeDefined();
					expect(parsed.get("better-auth.dont_remember")?.value).toBeDefined();
					headers.append(
						"cookie",
						`better-auth.two_factor=${
							parsed.get("better-auth.two_factor")?.value
						}`,
					);
					headers.append(
						"cookie",
						`better-auth.dont_remember=${
							parsed.get("better-auth.dont_remember")?.value
						}`,
					);
				},
			},
		});
		expect((res.data as any)?.twoFactorRedirect).toBe(true);
		await client.twoFactor.sendOtp({
			fetchOptions: {
				headers,
			},
		});

		const verifyRes = await client.twoFactor.verifyOtp({
			code: OTP,
			fetchOptions: {
				headers,
				onResponse(context) {
					const parsed = parseSetCookieHeader(
						context.response.headers.get("Set-Cookie") || "",
					);
					expect(parsed.get("better-auth.session_token")?.value).toBeDefined();
					// max age should be undefined because we are not using remember me
					expect(
						parsed.get("better-auth.session_token")?.["max-age"],
					).not.toBeDefined();
				},
			},
		});
		expect(verifyRes.data?.token).toBeDefined();
	});

	it("should fail if two factor cookie is missing", async () => {
		const res = await client.twoFactor.verifyTotp({
			code: "123456",
			fetchOptions: {
				headers,
			},
		});
		expect(res.error?.message).toBe(
			TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE,
		);
	});

	let backupCodes: string[] = [];
	it("should generate backup codes", async () => {
		await client.twoFactor.enable({
			password: testUser.password,
			fetchOptions: {
				headers,
			},
		});
		const backupCodesRes = await client.twoFactor.generateBackupCodes({
			fetchOptions: {
				headers,
			},
			password: testUser.password,
		});
		expect(backupCodesRes.data?.backupCodes).toBeDefined();
		backupCodes = backupCodesRes.data?.backupCodes || [];
	});

	it("should allow sign in with backup code", async () => {
		const headers = new Headers();
		await client.signIn.email({
			email: testUser.email,
			password: testUser.password,
			fetchOptions: {
				onSuccess(context) {
					const parsed = parseSetCookieHeader(
						context.response.headers.get("Set-Cookie") || "",
					);
					const token = parsed.get("better-auth.session_token")?.value;
					expect(token).toBe("");
					headers.append(
						"cookie",
						`better-auth.two_factor=${
							parsed.get("better-auth.two_factor")?.value
						}`,
					);
				},
			},
		});
		const backupCode = backupCodes[0]!;

		let parsedCookies = new Map();
		await client.twoFactor.verifyBackupCode({
			code: backupCode,
			fetchOptions: {
				headers,
				onSuccess(context) {
					parsedCookies = parseSetCookieHeader(
						context.response.headers.get("Set-Cookie") || "",
					);
				},
			},
		});
		const token = parsedCookies.get("better-auth.session_token")?.value;
		expect(token?.length).toBeGreaterThan(0);
		const currentBackupCodes = await auth.api.viewBackupCodes({
			body: {
				userId: session.data?.user.id!,
			},
		});
		expect(currentBackupCodes.backupCodes).toBeDefined();
		expect(currentBackupCodes.backupCodes).not.toContain(backupCode);

		const res = await client.twoFactor.verifyBackupCode({
			code: "invalid-code",
			fetchOptions: {
				headers,
				onSuccess(context) {
					const parsed = parseSetCookieHeader(
						context.response.headers.get("Set-Cookie") || "",
					);
					const token = parsed.get("better-auth.session_token")?.value;
					expect(token?.length).toBeGreaterThan(0);
				},
			},
		});
		expect(res.error?.message).toBe("Invalid backup code");
	});

	it("should trust device", async () => {
		const headers = new Headers();
		const res = await client.signIn.email({
			email: testUser.email,
			password: testUser.password,
			fetchOptions: {
				onSuccess(context) {
					const parsed = parseSetCookieHeader(
						context.response.headers.get("Set-Cookie") || "",
					);
					headers.append(
						"cookie",
						`better-auth.two_factor=${
							parsed.get("better-auth.two_factor")?.value
						}`,
					);
				},
			},
		});
		expect((res.data as any)?.twoFactorRedirect).toBe(true);
		const otpRes = await client.twoFactor.sendOtp({
			fetchOptions: {
				headers,
				onSuccess(context) {
					const parsed = parseSetCookieHeader(
						context.response.headers.get("Set-Cookie") || "",
					);
					headers.append(
						"cookie",
						`better-auth.otp.counter=${
							parsed.get("better-auth.otp_counter")?.value
						}`,
					);
				},
			},
		});
		const newHeaders = new Headers();
		await client.twoFactor.verifyOtp({
			trustDevice: true,
			code: OTP,
			fetchOptions: {
				headers,
				onSuccess(context) {
					const parsed = parseSetCookieHeader(
						context.response.headers.get("Set-Cookie") || "",
					);
					newHeaders.set(
						"cookie",
						`better-auth.trust_device=${
							parsed.get("better-auth.trust_device")?.value
						}`,
					);
				},
			},
		});

		const signInRes = await client.signIn.email({
			email: testUser.email,
			password: testUser.password,
			fetchOptions: {
				headers: newHeaders,
			},
		});
		expect(signInRes.data?.user).toBeDefined();
	});

	it("should limit OTP verification attempts", async () => {
		const headers = new Headers();
		// Sign in to trigger 2FA
		await client.signIn.email({
			email: testUser.email,
			password: testUser.password,
			fetchOptions: {
				onSuccess(context) {
					const parsed = parseSetCookieHeader(
						context.response.headers.get("Set-Cookie") || "",
					);
					headers.append(
						"cookie",
						`better-auth.two_factor=${
							parsed.get("better-auth.two_factor")?.value
						}`,
					);
				},
			},
		});
		await client.twoFactor.sendOtp({
			fetchOptions: {
				headers,
			},
		});
		for (let i = 0; i < 5; i++) {
			const res = await client.twoFactor.verifyOtp({
				code: "000000", // Invalid code
				fetchOptions: {
					headers,
				},
			});
			expect(res.error?.message).toBe("Invalid code");
		}

		// Next attempt should be blocked
		const res = await client.twoFactor.verifyOtp({
			code: OTP, // Even with correct code
			fetchOptions: {
				headers,
			},
		});
		expect(res.error?.message).toBe(
			"Too many attempts. Please request a new code.",
		);
	});

	it("should disable two factor", async () => {
		const res = await client.twoFactor.disable({
			password: testUser.password,
			fetchOptions: {
				headers,
			},
		});

		expect(res.data?.status).toBe(true);
		const dbUser = await db.findOne<UserWithTwoFactor>({
			model: "user",
			where: [
				{
					field: "id",
					value: session.data?.user.id as string,
				},
			],
		});
		expect(dbUser?.twoFactorEnabled).toBe(false);

		const signInRes = await client.signIn.email({
			email: testUser.email,
			password: testUser.password,
		});
		expect(signInRes.data?.user).toBeDefined();
	});
});

describe("two factor auth API", async () => {
	let OTP = "";
	const sendOTP = vi.fn();
	const { auth, signInWithTestUser, testUser } = await getTestInstance({
		secret: DEFAULT_SECRET,
		plugins: [
			twoFactor({
				otpOptions: {
					sendOTP({ otp }) {
						OTP = otp;
						sendOTP(otp);
					},
				},
				skipVerificationOnEnable: true,
			}),
		],
	});
	let { headers } = await signInWithTestUser();

	it("enable two factor", async () => {
		const res = await auth.api.enableTwoFactor({
			body: {
				password: testUser.password,
			},
			headers,
			asResponse: true,
		});
		headers = convertSetCookieToCookie(res.headers);

		const json = (await res.json()) as {
			status: boolean;
			backupCodes: string[];
			totpURI: string;
		};
		expect(json.backupCodes.length).toBe(10);
		expect(json.totpURI).toBeDefined();
		const session = await auth.api.getSession({
			headers,
		});
		expect(session?.user.twoFactorEnabled).toBe(true);
	});

	it("should get totp uri", async () => {
		const res = await auth.api.getTOTPURI({
			headers,
			body: {
				password: testUser.password,
			},
		});
		expect(res.totpURI).toBeDefined();
	});

	it("should request second factor", async () => {
		const signInRes = await auth.api.signInEmail({
			body: {
				email: testUser.email,
				password: testUser.password,
			},
			asResponse: true,
		});

		headers = convertSetCookieToCookie(signInRes.headers);

		expect(signInRes).toBeInstanceOf(Response);
		expect(signInRes.status).toBe(200);
		const parsed = parseSetCookieHeader(
			signInRes.headers.get("Set-Cookie") || "",
		);
		const twoFactorCookie = parsed.get("better-auth.two_factor");
		expect(twoFactorCookie).toBeDefined();
		const sessionToken = parsed.get("better-auth.session_token");
		expect(sessionToken?.value).toBeFalsy();
	});

	it("should send otp", async () => {
		await auth.api.sendTwoFactorOTP({
			headers,
			body: {
				trustDevice: false,
			},
		});
		expect(OTP.length).toBe(6);
		expect(sendOTP).toHaveBeenCalledWith(OTP);
	});

	it("should verify otp", async () => {
		const res = await auth.api.verifyTwoFactorOTP({
			headers,
			body: {
				code: OTP,
			},
			asResponse: true,
		});
		expect(res.status).toBe(200);
		expect(res.headers.get("Set-Cookie")).toBeDefined();
		headers = convertSetCookieToCookie(res.headers);
	});

	it("should disable two factor", async () => {
		const res = await auth.api.disableTwoFactor({
			headers,
			body: {
				password: testUser.password,
			},
			asResponse: true,
		});
		headers = convertSetCookieToCookie(res.headers);
		expect(res.status).toBe(200);
		const session = await auth.api.getSession({
			headers,
		});
		expect(session?.user.twoFactorEnabled).toBe(false);
	});
});

describe("view backup codes", async () => {
	const sendOTP = vi.fn();
	const { auth, signInWithTestUser, testUser, db } = await getTestInstance({
		secret: DEFAULT_SECRET,
		plugins: [
			twoFactor({
				otpOptions: {
					sendOTP({ otp }) {
						sendOTP(otp);
					},
				},
				skipVerificationOnEnable: true,
			}),
		],
	});
	let { headers } = await signInWithTestUser();

	let session = await auth.api.getSession({ headers });
	const userId = session?.user.id!;

	it("should return parsed array of backup codes, not JSON string", async () => {
		const enableRes = await auth.api.enableTwoFactor({
			body: { password: testUser.password },
			headers,
			asResponse: true,
		});

		expect(enableRes.status).toBe(200);
		headers = convertSetCookieToCookie(enableRes.headers);

		const enableJson = (await enableRes.json()) as {
			backupCodes: string[];
		};

		const viewResult = await auth.api.viewBackupCodes({
			body: { userId },
		});

		expect(typeof viewResult.backupCodes).not.toBe("string");
		expect(Array.isArray(viewResult.backupCodes)).toBe(true);
		expect(viewResult.backupCodes.length).toBe(10);
		viewResult.backupCodes.forEach((code) => {
			expect(typeof code).toBe("string");
			expect(code.length).toBeGreaterThan(0);
		});
		expect(viewResult.backupCodes).toEqual(enableJson.backupCodes);
		expect(viewResult.status).toBe(true);
	});

	it("should return array after generating new backup codes", async () => {
		const generateResult = await auth.api.generateBackupCodes({
			body: { password: testUser.password },
			headers,
		});

		expect(generateResult.backupCodes).toBeDefined();
		expect(generateResult.backupCodes.length).toBe(10);

		const viewResult = await auth.api.viewBackupCodes({
			body: { userId },
		});

		expect(viewResult.status).toBe(true);
		expect(typeof viewResult.backupCodes).not.toBe("string");
		expect(Array.isArray(viewResult.backupCodes)).toBe(true);
		expect(viewResult.backupCodes.length).toBe(10);
		viewResult.backupCodes.forEach((code) => {
			expect(typeof code).toBe("string");
			expect(code.length).toBeGreaterThan(0);
		});
		expect(viewResult.backupCodes).toEqual(generateResult.backupCodes);
	});
});

```

--------------------------------------------------------------------------------
/packages/sso/src/oidc.test.ts:
--------------------------------------------------------------------------------

```typescript
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { getTestInstanceMemory as getTestInstance } from "better-auth/test";
import { sso } from ".";
import { OAuth2Server } from "oauth2-mock-server";
import { betterFetch } from "@better-fetch/fetch";
import { organization } from "better-auth/plugins";
import { createAuthClient } from "better-auth/client";
import { ssoClient } from "./client";

let server = new OAuth2Server();

describe("SSO", async () => {
	const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
		await getTestInstance({
			plugins: [sso(), organization()],
		});

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

	beforeAll(async () => {
		await server.issuer.keys.generate("RS256");
		server.issuer.on;
		await server.start(8080, "localhost");
		console.log("Issuer URL:", server.issuer.url); // -> http://localhost:8080
	});

	afterAll(async () => {
		await server.stop().catch(() => {});
	});

	server.service.on("beforeUserinfo", (userInfoResponse, req) => {
		userInfoResponse.body = {
			email: "[email protected]",
			name: "OAuth2 Test",
			sub: "oauth2",
			picture: "https://test.com/picture.png",
			email_verified: true,
		};
		userInfoResponse.statusCode = 200;
	});

	server.service.on("beforeTokenSigning", (token, req) => {
		token.payload.email = "sso-user@localhost:8000.com";
		token.payload.email_verified = true;
		token.payload.name = "Test User";
		token.payload.picture = "https://test.com/picture.png";
	});

	async function simulateOAuthFlow(
		authUrl: string,
		headers: Headers,
		fetchImpl?: (...args: any) => any,
	) {
		let location: string | null = null;
		await betterFetch(authUrl, {
			method: "GET",
			redirect: "manual",
			onError(context) {
				location = context.response.headers.get("location");
			},
		});

		if (!location) throw new Error("No redirect location found");
		const newHeaders = new Headers();
		let callbackURL = "";
		await betterFetch(location, {
			method: "GET",
			customFetchImpl: fetchImpl || customFetchImpl,
			headers,
			onError(context) {
				callbackURL = context.response.headers.get("location") || "";
				cookieSetter(newHeaders)(context);
			},
		});

		return { callbackURL, headers: newHeaders };
	}

	it("should register a new SSO provider", async () => {
		const { headers } = await signInWithTestUser();
		const provider = await auth.api.registerSSOProvider({
			body: {
				issuer: server.issuer.url!,
				domain: "localhost.com",
				oidcConfig: {
					clientId: "test",
					clientSecret: "test",
					authorizationEndpoint: `${server.issuer.url}/authorize`,
					tokenEndpoint: `${server.issuer.url}/token`,
					jwksEndpoint: `${server.issuer.url}/jwks`,
					discoveryEndpoint: `${server.issuer.url}/.well-known/openid-configuration`,
					mapping: {
						id: "sub",
						email: "email",
						emailVerified: "email_verified",
						name: "name",
						image: "picture",
					},
				},
				providerId: "test",
			},
			headers,
		});
		expect(provider).toMatchObject({
			id: expect.any(String),
			issuer: "http://localhost:8080",
			oidcConfig: {
				issuer: "http://localhost:8080",
				clientId: "test",
				clientSecret: "test",
				authorizationEndpoint: "http://localhost:8080/authorize",
				tokenEndpoint: "http://localhost:8080/token",
				jwksEndpoint: "http://localhost:8080/jwks",
				discoveryEndpoint:
					"http://localhost:8080/.well-known/openid-configuration",
				mapping: {
					id: "sub",
					email: "email",
					emailVerified: "email_verified",
					name: "name",
					image: "picture",
				},
			},
			userId: expect.any(String),
		});
	});

	it("should fail to register a new SSO provider with invalid issuer", async () => {
		const { headers } = await signInWithTestUser();

		try {
			await auth.api.registerSSOProvider({
				body: {
					issuer: "invalid",
					domain: "localhost",
					providerId: "test",
					oidcConfig: {
						clientId: "test",
						clientSecret: "test",
					},
				},
				headers,
			});
		} catch (e) {
			expect(e).toMatchObject({
				status: "BAD_REQUEST",
				body: {
					message: "Invalid issuer. Must be a valid URL",
				},
			});
		}
	});

	it("should not allow creating a provider with duplicate providerId", async () => {
		const { headers } = await signInWithTestUser();

		await auth.api.registerSSOProvider({
			body: {
				issuer: server.issuer.url!,
				domain: "duplicate.com",
				providerId: "duplicate-oidc-provider",
				oidcConfig: {
					clientId: "test",
					clientSecret: "test",
				},
			},
			headers,
		});

		await expect(
			auth.api.registerSSOProvider({
				body: {
					issuer: server.issuer.url!,
					domain: "another-duplicate.com",
					providerId: "duplicate-oidc-provider",
					oidcConfig: {
						clientId: "test2",
						clientSecret: "test2",
					},
				},
				headers,
			}),
		).rejects.toMatchObject({
			status: "UNPROCESSABLE_ENTITY",
			body: {
				message: "SSO provider with this providerId already exists",
			},
		});
	});

	it("should sign in with SSO provider with email matching", async () => {
		const headers = new Headers();
		const res = await authClient.signIn.sso({
			email: "[email protected]",
			callbackURL: "/dashboard",
			fetchOptions: {
				throw: true,
				onSuccess: cookieSetter(headers),
			},
		});
		expect(res.url).toContain("http://localhost:8080/authorize");
		expect(res.url).toContain(
			"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
		);
		expect(res.url).toContain("login_hint=my-email%40localhost.com");
		const { callbackURL } = await simulateOAuthFlow(res.url, headers);
		expect(callbackURL).toContain("/dashboard");
	});

	it("should sign in with SSO provider with domain", async () => {
		const headers = new Headers();
		const res = await authClient.signIn.sso({
			email: "[email protected]",
			domain: "localhost.com",
			callbackURL: "/dashboard",
			fetchOptions: {
				throw: true,
				onSuccess: cookieSetter(headers),
			},
		});
		expect(res.url).toContain("http://localhost:8080/authorize");
		expect(res.url).toContain(
			"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
		);
		const { callbackURL } = await simulateOAuthFlow(res.url, headers);
		expect(callbackURL).toContain("/dashboard");
	});

	it("should sign in with SSO provider with providerId", async () => {
		const headers = new Headers();
		const res = await authClient.signIn.sso({
			providerId: "test",
			loginHint: "[email protected]",
			callbackURL: "/dashboard",
			fetchOptions: {
				throw: true,
				onSuccess: cookieSetter(headers),
			},
		});
		expect(res.url).toContain("http://localhost:8080/authorize");
		expect(res.url).toContain(
			"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
		);
		expect(res.url).toContain("login_hint=user%40example.com");

		const { callbackURL } = await simulateOAuthFlow(res.url, headers);
		expect(callbackURL).toContain("/dashboard");
	});
});

describe("SSO disable implicit sign in", async () => {
	const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
		await getTestInstance({
			plugins: [sso({ disableImplicitSignUp: true }), organization()],
		});

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

	beforeAll(async () => {
		await server.issuer.keys.generate("RS256");
		server.issuer.on;
		await server.start(8080, "localhost");
		console.log("Issuer URL:", server.issuer.url); // -> http://localhost:8080
	});

	afterAll(async () => {
		await server.stop();
	});

	server.service.on("beforeUserinfo", (userInfoResponse, req) => {
		userInfoResponse.body = {
			email: "[email protected]",
			name: "OAuth2 Test",
			sub: "oauth2",
			picture: "https://test.com/picture.png",
			email_verified: true,
		};
		userInfoResponse.statusCode = 200;
	});

	server.service.on("beforeTokenSigning", (token, req) => {
		token.payload.email = "sso-user@localhost:8000.com";
		token.payload.email_verified = true;
		token.payload.name = "Test User";
		token.payload.picture = "https://test.com/picture.png";
	});

	async function simulateOAuthFlow(
		authUrl: string,
		headers: Headers,
		fetchImpl?: (...args: any) => any,
	) {
		let location: string | null = null;
		await betterFetch(authUrl, {
			method: "GET",
			redirect: "manual",
			onError(context) {
				location = context.response.headers.get("location");
			},
		});

		if (!location) throw new Error("No redirect location found");
		const newHeaders = new Headers(headers);
		let callbackURL = "";
		await betterFetch(location, {
			method: "GET",
			customFetchImpl: fetchImpl || customFetchImpl,
			headers,
			onError(context) {
				callbackURL = context.response.headers.get("location") || "";
				cookieSetter(newHeaders)(context);
			},
		});

		return { callbackURL, headers: newHeaders };
	}

	it("should register a new SSO provider", async () => {
		const { headers } = await signInWithTestUser();
		const provider = await auth.api.registerSSOProvider({
			body: {
				issuer: server.issuer.url!,
				domain: "localhost.com",
				oidcConfig: {
					clientId: "test",
					clientSecret: "test",
					authorizationEndpoint: `${server.issuer.url}/authorize`,
					tokenEndpoint: `${server.issuer.url}/token`,
					jwksEndpoint: `${server.issuer.url}/jwks`,
					discoveryEndpoint: `${server.issuer.url}/.well-known/openid-configuration`,
					mapping: {
						id: "sub",
						email: "email",
						emailVerified: "email_verified",
						name: "name",
						image: "picture",
					},
				},
				providerId: "test",
			},
			headers,
		});
		expect(provider).toMatchObject({
			id: expect.any(String),
			issuer: "http://localhost:8080",
			oidcConfig: {
				issuer: "http://localhost:8080",
				clientId: "test",
				clientSecret: "test",
				authorizationEndpoint: "http://localhost:8080/authorize",
				tokenEndpoint: "http://localhost:8080/token",
				jwksEndpoint: "http://localhost:8080/jwks",
				discoveryEndpoint:
					"http://localhost:8080/.well-known/openid-configuration",
				mapping: {
					id: "sub",
					email: "email",
					emailVerified: "email_verified",
					name: "name",
					image: "picture",
				},
			},
			userId: expect.any(String),
		});
	});

	it("should not create user with SSO provider when sign ups are disabled", async () => {
		const headers = new Headers();
		const res = await authClient.signIn.sso({
			email: "[email protected]",
			callbackURL: "/dashboard",
			fetchOptions: {
				throw: true,
				onSuccess: cookieSetter(headers),
			},
		});
		expect(res.url).toContain("http://localhost:8080/authorize");
		expect(res.url).toContain(
			"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
		);
		const { callbackURL } = await simulateOAuthFlow(res.url, headers);
		expect(callbackURL).toContain(
			"/api/auth/error/error?error=signup disabled",
		);
	});

	it("should create user with SSO provider when sign ups are disabled but sign up is requested", async () => {
		const headers = new Headers();
		const res = await authClient.signIn.sso({
			email: "[email protected]",
			callbackURL: "/dashboard",
			requestSignUp: true,
			fetchOptions: {
				throw: true,
				onSuccess: cookieSetter(headers),
			},
		});
		expect(res.url).toContain("http://localhost:8080/authorize");
		expect(res.url).toContain(
			"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
		);
		const { callbackURL } = await simulateOAuthFlow(res.url, headers);
		expect(callbackURL).toContain("/dashboard");
	});
});

describe("provisioning", async (ctx) => {
	const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
		await getTestInstance({
			plugins: [sso(), organization()],
		});

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

	beforeAll(async () => {
		await server.issuer.keys.generate("RS256");
		server.issuer.on;
		await server.start(8080, "localhost");
		console.log("Issuer URL:", server.issuer.url); // -> http://localhost:8080
	});

	afterAll(async () => {
		await server.stop();
	});
	async function simulateOAuthFlow(
		authUrl: string,
		headers: Headers,
		fetchImpl?: (...args: any) => any,
	) {
		let location: string | null = null;
		await betterFetch(authUrl, {
			method: "GET",
			redirect: "manual",
			onError(context) {
				location = context.response.headers.get("location");
			},
		});

		if (!location) throw new Error("No redirect location found");

		let callbackURL = "";
		const newHeaders = new Headers();
		await betterFetch(location, {
			method: "GET",
			customFetchImpl: fetchImpl || customFetchImpl,
			headers,
			onError(context) {
				callbackURL = context.response.headers.get("location") || "";
				cookieSetter(newHeaders)(context);
			},
		});

		return callbackURL;
	}

	server.service.on("beforeUserinfo", (userInfoResponse, req) => {
		userInfoResponse.body = {
			email: "[email protected]",
			name: "OAuth2 Test",
			sub: "oauth2",
			picture: "https://test.com/picture.png",
			email_verified: true,
		};
		userInfoResponse.statusCode = 200;
	});

	server.service.on("beforeTokenSigning", (token, req) => {
		token.payload.email = "sso-user@localhost:8000.com";
		token.payload.email_verified = true;
		token.payload.name = "Test User";
		token.payload.picture = "https://test.com/picture.png";
	});
	it("should provision user", async () => {
		const { headers } = await signInWithTestUser();
		const organization = await auth.api.createOrganization({
			body: {
				name: "Localhost",
				slug: "localhost",
			},
			headers,
		});
		const provider = await auth.api.registerSSOProvider({
			body: {
				issuer: server.issuer.url!,
				domain: "localhost.com",
				oidcConfig: {
					clientId: "test",
					clientSecret: "test",
					authorizationEndpoint: `${server.issuer.url}/authorize`,
					tokenEndpoint: `${server.issuer.url}/token`,
					jwksEndpoint: `${server.issuer.url}/jwks`,
					discoveryEndpoint: `${server.issuer.url}/.well-known/openid-configuration`,
					mapping: {
						id: "sub",
						email: "email",
						emailVerified: "email_verified",
						name: "name",
						image: "picture",
					},
				},
				providerId: "test2",
				organizationId: organization?.id,
			},
			headers,
		});
		expect(provider).toMatchObject({
			organizationId: organization?.id,
		});
		const newHeaders = new Headers();
		const res = await authClient.signIn.sso({
			email: "[email protected]",
			callbackURL: "/dashboard",
			fetchOptions: {
				onSuccess: cookieSetter(newHeaders),
				throw: true,
			},
		});
		expect(res.url).toContain("http://localhost:8080/authorize");
		expect(res.url).toContain(
			"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
		);

		const callbackURL = await simulateOAuthFlow(res.url, newHeaders);
		expect(callbackURL).toContain("/dashboard");
		const org = await auth.api.getFullOrganization({
			query: {
				organizationId: organization?.id || "",
			},
			headers,
		});
		const member = org?.members.find(
			(m: any) => m.user.email === "sso-user@localhost:8000.com",
		);
		expect(member).toMatchObject({
			role: "member",
			user: {
				id: expect.any(String),
				name: "Test User",
				email: "sso-user@localhost:8000.com",
				image: "https://test.com/picture.png",
			},
		});
	});

	it("should sign in with SSO provide with org slug", async () => {
		const res = await auth.api.signInSSO({
			body: {
				organizationSlug: "localhost",
				callbackURL: "/dashboard",
			},
		});

		expect(res.url).toContain("http://localhost:8080/authorize");
	});
});

```

--------------------------------------------------------------------------------
/packages/better-auth/src/api/routes/session-api.test.ts:
--------------------------------------------------------------------------------

```typescript
import { beforeEach, describe, expect, expectTypeOf, it, vi } from "vitest";
import { getTestInstance } from "../../test-utils/test-instance";
import { parseSetCookieHeader } from "../../cookies";
import { getDate } from "../../utils/date";
import { memoryAdapter, type MemoryDB } from "../../adapters/memory-adapter";
import { runWithEndpointContext } from "@better-auth/core/context";
import type { GenericEndpointContext } from "@better-auth/core";

describe("session", async () => {
	const { client, testUser, sessionSetter, cookieSetter, auth } =
		await getTestInstance();

	it("should set cookies correctly on sign in", async () => {
		const headers = new Headers();
		await client.signIn.email(
			{
				email: testUser.email,
				password: testUser.password,
			},
			{
				onSuccess(context) {
					const header = context.response.headers.get("set-cookie");
					const cookies = parseSetCookieHeader(header || "");
					cookieSetter(headers)(context);
					const cookie = cookies.get("better-auth.session_token");
					expect(cookie).toMatchObject({
						value: expect.any(String),
						"max-age": 60 * 60 * 24 * 7,
						path: "/",
						samesite: "lax",
						httponly: true,
					});
				},
			},
		);
		const { data } = await client.getSession({
			fetchOptions: {
				headers,
			},
		});
		const expiresAt = new Date(data?.session.expiresAt || "");
		const now = new Date();

		expect(expiresAt.getTime()).toBeGreaterThan(
			now.getTime() + 6 * 24 * 60 * 60 * 1000,
		);
	});

	it("should return null when not authenticated", async () => {
		const response = await client.getSession();
		expect(response.data).toBeNull();
	});

	it("should update session when update age is reached", async () => {
		const { client, testUser } = await getTestInstance({
			session: {
				updateAge: 60,
				expiresIn: 60 * 2,
			},
		});
		let headers = new Headers();

		await client.signIn.email(
			{
				email: testUser.email,
				password: testUser.password,
			},
			{
				onSuccess(context) {
					const header = context.response.headers.get("set-cookie");
					const cookies = parseSetCookieHeader(header || "");
					const signedCookie = cookies.get("better-auth.session_token")?.value;
					headers.set("cookie", `better-auth.session_token=${signedCookie}`);
				},
			},
		);

		const data = await client.getSession({
			fetchOptions: {
				headers,
				throw: true,
			},
		});

		if (!data) {
			throw new Error("No session found");
		}
		expect(new Date(data?.session.expiresAt).getTime()).toBeGreaterThan(
			new Date(Date.now() + 1000 * 2 * 59).getTime(),
		);

		expect(new Date(data?.session.expiresAt).getTime()).toBeLessThan(
			new Date(Date.now() + 1000 * 2 * 60).getTime(),
		);
		for (const t of [60, 80, 100, 121]) {
			const span = new Date();
			span.setSeconds(span.getSeconds() + t);
			vi.setSystemTime(span);
			const response = await client.getSession({
				fetchOptions: {
					headers,
					onSuccess(context) {
						const parsed = parseSetCookieHeader(
							context.response.headers.get("set-cookie") || "",
						);
						const maxAge = parsed.get("better-auth.session_token")?.["max-age"];
						expect(maxAge).toBe(t === 121 ? 0 : 60 * 2);
					},
				},
			});
			if (t === 121) {
				//expired
				expect(response.data).toBeNull();
			} else {
				expect(
					new Date(response.data?.session.expiresAt!).getTime(),
				).toBeGreaterThan(new Date(Date.now() + 1000 * 2 * 59).getTime());
			}
		}
		vi.useRealTimers();
	});

	it("should update the session every time when set to 0", async () => {
		const { client, signInWithTestUser } = await getTestInstance({
			session: {
				updateAge: 0,
			},
		});
		const { runWithUser } = await signInWithTestUser();

		await runWithUser(async () => {
			const session = await client.getSession();

			vi.useFakeTimers();
			await vi.advanceTimersByTimeAsync(1000 * 60 * 5);
			const session2 = await client.getSession();
			expect(session2.data?.session.expiresAt).not.toBe(
				session.data?.session.expiresAt,
			);
			expect(
				new Date(session2.data!.session.expiresAt).getTime(),
			).toBeGreaterThan(new Date(session.data!.session.expiresAt).getTime());
		});
	});

	it("should handle 'don't remember me' option", async () => {
		let headers = new Headers();
		const res = await client.signIn.email(
			{
				email: testUser.email,
				password: testUser.password,
				rememberMe: false,
			},
			{
				onSuccess(context) {
					const header = context.response.headers.get("set-cookie");
					const cookies = parseSetCookieHeader(header || "");
					const signedCookie = cookies.get("better-auth.session_token")?.value;
					const dontRememberMe = cookies.get(
						"better-auth.dont_remember",
					)?.value;
					headers.set(
						"cookie",
						`better-auth.session_token=${signedCookie};better-auth.dont_remember=${dontRememberMe}`,
					);
				},
			},
		);
		const data = await client.getSession({
			fetchOptions: {
				headers,
				throw: true,
			},
		});
		if (!data) {
			throw new Error("No session found");
		}
		const expiresAt = data.session.expiresAt;
		expect(new Date(expiresAt).valueOf()).toBeLessThanOrEqual(
			getDate(1000 * 60 * 60 * 24).valueOf(),
		);
		const response = await client.getSession({
			fetchOptions: {
				headers,
			},
		});

		if (!response.data?.session) {
			throw new Error("No session found");
		}
		// Check that the session wasn't update
		expect(
			new Date(response.data.session.expiresAt).valueOf(),
		).toBeLessThanOrEqual(getDate(1000 * 60 * 60 * 24).valueOf());
	});

	it("should set cookies correctly on sign in after changing config", async () => {
		const headers = new Headers();
		await client.signIn.email(
			{
				email: testUser.email,
				password: testUser.password,
			},
			{
				onSuccess(context) {
					const header = context.response.headers.get("set-cookie");
					const cookies = parseSetCookieHeader(header || "");
					expect(cookies.get("better-auth.session_token")).toMatchObject({
						value: expect.any(String),
						"max-age": 60 * 60 * 24 * 7,
						path: "/",
						httponly: true,
						samesite: "lax",
					});
					headers.set(
						"cookie",
						`better-auth.session_token=${
							cookies.get("better-auth.session_token")?.value
						}`,
					);
				},
			},
		);
		const data = await client.getSession({
			fetchOptions: {
				headers,
				throw: true,
			},
		});
		if (!data) {
			throw new Error("No session found");
		}
		const expiresAt = new Date(data?.session?.expiresAt || "");
		const now = new Date();

		expect(expiresAt.getTime()).toBeGreaterThan(
			now.getTime() + 6 * 24 * 60 * 60 * 1000,
		);
	});

	it("should clear session on sign out", async () => {
		let headers = new Headers();
		const res = await client.signIn.email(
			{
				email: testUser.email,
				password: testUser.password,
			},
			{
				onSuccess(context) {
					const header = context.response.headers.get("set-cookie");
					const cookies = parseSetCookieHeader(header || "");
					const signedCookie = cookies.get("better-auth.session_token")?.value;
					headers.set("cookie", `better-auth.session_token=${signedCookie}`);
				},
			},
		);
		const data = await client.getSession({
			fetchOptions: {
				headers,
				throw: true,
			},
		});

		expect(data).not.toBeNull();
		await client.signOut({
			fetchOptions: {
				headers,
			},
		});
		const response = await client.getSession({
			fetchOptions: {
				headers,
			},
		});
		expect(response.data);
	});

	it("should list sessions", async () => {
		const headers = new Headers();
		await client.signIn.email(
			{
				email: testUser.email,
				password: testUser.password,
			},
			{
				onSuccess: sessionSetter(headers),
			},
		);

		const response = await client.listSessions({
			fetchOptions: {
				headers,
			},
		});

		expect(response.data?.length).toBeGreaterThan(1);
	});

	it("should revoke session", async () => {
		const headers = new Headers();
		const headers2 = new Headers();
		const res = await client.signIn.email({
			email: testUser.email,
			password: testUser.password,
			fetchOptions: {
				onSuccess: sessionSetter(headers),
			},
		});
		await client.signIn.email({
			email: testUser.email,
			password: testUser.password,
			fetchOptions: {
				onSuccess: sessionSetter(headers2),
			},
		});
		const session = await client.getSession({
			fetchOptions: {
				headers,
				throw: true,
			},
		});
		await client.revokeSession({
			fetchOptions: {
				headers,
			},
			token: session?.session?.token || "",
		});
		const newSession = await client.getSession({
			fetchOptions: {
				headers,
			},
		});
		expect(newSession.data).toBeNull();
		const revokeRes = await client.revokeSessions({
			fetchOptions: {
				headers: headers2,
			},
		});
		expect(revokeRes.data?.status).toBe(true);
	});

	it("should return session headers", async () => {
		const context = await auth.$context;
		await runWithEndpointContext(
			{
				context,
			} as unknown as GenericEndpointContext,
			async () => {
				const signInRes = await auth.api.signInEmail({
					body: {
						email: testUser.email,
						password: testUser.password,
					},
					returnHeaders: true,
				});

				const signInHeaders = new Headers();
				signInHeaders.set("cookie", signInRes.headers.getSetCookie()[0]!);

				const sessionResWithoutHeaders = await auth.api.getSession({
					headers: signInHeaders,
				});

				const sessionResWithHeaders = await auth.api.getSession({
					headers: signInHeaders,
					returnHeaders: true,
				});

				expect(sessionResWithHeaders.headers).toBeDefined();
				expect(sessionResWithHeaders.response?.user).toBeDefined();
				expect(sessionResWithHeaders.response?.session).toBeDefined();
				expectTypeOf({
					headers: sessionResWithHeaders.headers,
				}).toMatchObjectType<{
					headers: Headers;
				}>();

				// @ts-expect-error: headers should not exist on sessionResWithoutHeaders
				expect(sessionResWithoutHeaders.headers).toBeUndefined();

				const sessionResWithHeadersAndAsResponse = await auth.api.getSession({
					headers: signInHeaders,
					returnHeaders: true,
					asResponse: true,
				});

				expectTypeOf({
					res: sessionResWithHeadersAndAsResponse,
				}).toMatchObjectType<{ res: Response }>();

				expect(sessionResWithHeadersAndAsResponse.ok).toBe(true);
				expect(sessionResWithHeadersAndAsResponse.status).toBe(200);
			},
		);
	});
});

describe("session storage", async () => {
	let store = new Map<string, string>();
	const { client, signInWithTestUser, db } = await getTestInstance({
		secondaryStorage: {
			set(key, value, ttl) {
				store.set(key, value);
			},
			get(key) {
				return store.get(key) || null;
			},
			delete(key) {
				store.delete(key);
			},
		},
		rateLimit: {
			enabled: false,
		},
	});

	beforeEach(() => {
		store.clear();
	});

	it("should store session in secondary storage", async () => {
		//since the instance creates a session on init, we expect the store to have 2 item (1 for session and 1 for active sessions record for the user)
		expect(store.size).toBe(0);
		const { runWithUser } = await signInWithTestUser();
		expect(store.size).toBe(2);
		await runWithUser(async () => {
			const session = await client.getSession();
			expect(session.data).toMatchObject({
				session: {
					userId: expect.any(String),
					token: expect.any(String),
					expiresAt: expect.any(Date),
					ipAddress: expect.any(String),
					userAgent: expect.any(String),
				},
				user: {
					id: expect.any(String),
					name: "test user",
					email: "[email protected]",
					emailVerified: false,
					image: null,
					createdAt: expect.any(Date),
					updatedAt: expect.any(Date),
				},
			});
		});
	});

	it("should list sessions", async () => {
		const { runWithUser } = await signInWithTestUser();
		await runWithUser(async () => {
			const response = await client.listSessions();
			expect(response.data?.length).toBe(1);
		});
	});

	it("revoke session and list sessions", async () => {
		const { runWithUser } = await signInWithTestUser();
		await runWithUser(async () => {
			const session = await client.getSession();
			expect(session.data).not.toBeNull();
			expect(session.data?.session?.token).toBeDefined();
			const userId = session.data!.session.userId;
			const sessions = JSON.parse(store.get(`active-sessions-${userId}`)!);
			expect(sessions.length).toBe(1);
			const res = await client.revokeSession({
				token: session.data?.session?.token!,
			});
			expect(res.data?.status).toBe(true);
			const response = await client.listSessions();
			expect(response.data).toBe(null);
			expect(store.size).toBe(0);
		});
	});

	it("should revoke session", async () => {
		const { runWithUser } = await signInWithTestUser();
		await runWithUser(async () => {
			const session = await client.getSession();
			expect(session.data).not.toBeNull();
			const res = await client.revokeSession({
				token: session.data?.session?.token || "",
			});
			const revokedSession = await client.getSession();
			expect(revokedSession.data).toBeNull();
		});
	});
});

describe("cookie cache", async () => {
	const database: MemoryDB = {
		user: [],
		account: [],
		session: [],
		verification: [],
	};
	const adapter = memoryAdapter(database);

	const { client, testUser, auth, cookieSetter } = await getTestInstance({
		database: adapter,
		session: {
			additionalFields: {
				sensitiveData: {
					type: "string",
					returned: false,
					defaultValue: "sensitive-data",
				},
			},
			cookieCache: {
				enabled: true,
			},
		},
	});
	const ctx = await auth.$context;

	it("should cache cookies", async () => {});
	const fn = vi.spyOn(ctx.adapter, "findOne");

	const headers = new Headers();
	it("should cache cookies", async () => {
		await client.signIn.email(
			{
				email: testUser.email,
				password: testUser.password,
			},
			{
				onSuccess(context) {
					const header = context.response.headers.get("set-cookie");
					const cookies = parseSetCookieHeader(header || "");
					headers.set(
						"cookie",
						`better-auth.session_token=${
							cookies.get("better-auth.session_token")?.value
						};better-auth.session_data=${
							cookies.get("better-auth.session_data")?.value
						}`,
					);
				},
			},
		);
		expect(fn).toHaveBeenCalledTimes(1);
		const session = await client.getSession({
			fetchOptions: {
				headers,
			},
		});
		expect(session.data?.session).not.toHaveProperty("sensitiveData");
		expect(session.data).not.toBeNull();
		expect(fn).toHaveBeenCalledTimes(1);
	});

	it("should disable cookie cache", async () => {
		const ctx = await auth.$context;

		const s = await client.getSession({
			fetchOptions: {
				headers,
			},
		});
		expect(s.data?.user.emailVerified).toBe(false);
		await runWithEndpointContext(
			{
				context: ctx,
			} as unknown as GenericEndpointContext,
			async () => {
				await ctx.internalAdapter.updateUser(s.data?.user.id || "", {
					emailVerified: true,
				});
			},
		);
		expect(fn).toHaveBeenCalledTimes(1);

		const session = await client.getSession({
			query: {
				disableCookieCache: true,
			},
			fetchOptions: {
				headers,
			},
		});
		expect(session.data?.user.emailVerified).toBe(true);
		expect(session.data).not.toBeNull();
		expect(fn).toHaveBeenCalledTimes(3);
	});

	it("should reset cache when expires", async () => {
		expect(fn).toHaveBeenCalledTimes(3);
		await client.getSession({
			fetchOptions: {
				headers,
			},
		});
		vi.useFakeTimers();
		await vi.advanceTimersByTimeAsync(1000 * 60 * 10); // 10 minutes
		await client.getSession({
			fetchOptions: {
				headers,
				onSuccess(context) {
					cookieSetter(headers)(context);
				},
			},
		});
		expect(fn).toHaveBeenCalledTimes(5);
		await client.getSession({
			fetchOptions: {
				headers,
				onSuccess(context) {
					cookieSetter(headers)(context);
				},
			},
		});
		expect(fn).toHaveBeenCalledTimes(5);
	});
});

```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/oidc-provider/types.ts:
--------------------------------------------------------------------------------

```typescript
import type { InferOptionSchema, User } from "../../types";
import type { schema } from "./schema";

export interface OIDCOptions {
	/**
	 * The amount of time in seconds that the access token is valid for.
	 *
	 * @default 3600 (1 hour) - Recommended by the OIDC spec
	 */
	accessTokenExpiresIn?: number;
	/**
	 * Allow dynamic client registration.
	 */
	allowDynamicClientRegistration?: boolean;
	/**
	 * The metadata for the OpenID Connect provider.
	 */
	metadata?: Partial<OIDCMetadata>;
	/**
	 * The amount of time in seconds that the refresh token is valid for.
	 *
	 * @default 604800 (7 days) - Recommended by the OIDC spec
	 */
	refreshTokenExpiresIn?: number;
	/**
	 * The amount of time in seconds that the authorization code is valid for.
	 *
	 * @default 600 (10 minutes) - Recommended by the OIDC spec
	 */
	codeExpiresIn?: number;
	/**
	 * The scopes that the client is allowed to request.
	 *
	 * @see https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims
	 * @default
	 * ```ts
	 * ["openid", "profile", "email", "offline_access"]
	 * ```
	 */
	scopes?: string[];
	/**
	 * The default scope to use if the client does not provide one.
	 *
	 * @default "openid"
	 */
	defaultScope?: string;
	/**
	 * A URL to the consent page where the user will be redirected if the client
	 * requests consent.
	 *
	 * After the user consents, they should be redirected by the client to the
	 * `redirect_uri` with the authorization code.
	 *
	 * When the server redirects the user to the consent page, it will include the
	 * following query parameters:
	 * - `consent_code` - The consent code to identify the authorization request.
	 * - `client_id` - The ID of the client.
	 * - `scope` - The requested scopes.
	 *
	 * Once the user consents, you need to call the `/oauth2/consent` endpoint
	 * with `accept: true` and optionally the `consent_code` (if using URL parameter flow)
	 * to complete the authorization. This will return the client to the `redirect_uri`
	 * with the authorization code.
	 *
	 * @example
	 * ```ts
	 * consentPage: "/oauth/authorize"
	 * ```
	 */
	consentPage?: string;
	/**
	 * The HTML for the consent page. This is used if `consentPage` is not
	 * provided. This should be a function that returns an HTML string.
	 * The function will be called with the following props:
	 */
	getConsentHTML?: (props: {
		clientId: string;
		clientName: string;
		clientIcon?: string;
		clientMetadata: Record<string, any> | null;
		code: string;
		scopes: string[];
	}) => string;
	/**
	 * The URL to the login page. This is used if the client requests the `login`
	 * prompt.
	 */
	loginPage: string;
	/**
	 * Whether to require PKCE (proof key code exchange) or not
	 *
	 * According to OAuth2.1 spec this should be required. But in any
	 * case if you want to disable this you can use this options.
	 *
	 * @default true
	 */
	requirePKCE?: boolean;
	/**
	 * Allow plain to be used as a code challenge method.
	 *
	 * @default true
	 */
	allowPlainCodeChallengeMethod?: boolean;
	/**
	 * Custom function to generate a client ID.
	 */
	generateClientId?: () => string;
	/**
	 * Custom function to generate a client secret.
	 */
	generateClientSecret?: () => string;
	/**
	 * Get the additional user info claims
	 *
	 * This applies to the `userinfo` endpoint and the `id_token`.
	 *
	 * @param user - The user object.
	 * @param scopes - The scopes that the client requested.
	 * @param client - The client object.
	 * @returns The user info claim.
	 */
	getAdditionalUserInfoClaim?: (
		user: User & Record<string, any>,
		scopes: string[],
		client: Client,
	) => Record<string, any> | Promise<Record<string, any>>;
	/**
	 * Trusted clients that are configured directly in the provider options.
	 * These clients bypass database lookups and can optionally skip consent screens.
	 */
	trustedClients?: Client[];
	/**
	 * Store the client secret in your database in a secure way
	 * Note: This will not affect the client secret sent to the user, it will only affect the client secret stored in your database
	 *
	 * - "hashed" - The client secret is hashed using the `hash` function.
	 * - "plain" - The client secret is stored in the database in plain text.
	 * - "encrypted" - The client secret is encrypted using the `encrypt` function.
	 * - { hash: (clientSecret: string) => Promise<string> } - A function that hashes the client secret.
	 * - { encrypt: (clientSecret: string) => Promise<string>, decrypt: (clientSecret: string) => Promise<string> } - A function that encrypts and decrypts the client secret.
	 *
	 * @default "plain"
	 */
	storeClientSecret?:
		| "hashed"
		| "plain"
		| "encrypted"
		| { hash: (clientSecret: string) => Promise<string> }
		| {
				encrypt: (clientSecret: string) => Promise<string>;
				decrypt: (clientSecret: string) => Promise<string>;
		  };
	/**
	 * Whether to use the JWT plugin to sign the ID token.
	 *
	 * @default false
	 */
	useJWTPlugin?: boolean;
	/**
	 * Custom schema for the OIDC plugin
	 */
	schema?: InferOptionSchema<typeof schema>;
}

export interface AuthorizationQuery {
	/**
	 * The response type. Must be 'code' or 'token'. Code is for authorization code flow, token is
	 * for implicit flow.
	 */
	response_type: "code" | "token";
	/**
	 * The redirect URI for the client. Must be one of the registered redirect URLs for the client.
	 */
	redirect_uri?: string;
	/**
	 * The scope of the request. Must be a space-separated list of case sensitive strings.
	 *
	 * - "openid" is required for all requests
	 * - "profile" is required for requests that require user profile information.
	 * - "email" is required for requests that require user email information.
	 * - "offline_access" is required for requests that require a refresh token.
	 */
	scope?: string;
	/**
	 * Opaque value used to maintain state between the request and the callback. Typically,
	 * Cross-Site Request Forgery (CSRF, XSRF) mitigation is done by cryptographically binding the
	 * value of this parameter with a browser cookie.
	 *
	 * Note: Better Auth stores the state in a database instead of a cookie. - This is to minimize
	 * the complication with native apps and other clients that may not have access to cookies.
	 */
	state: string;
	/**
	 * The client ID. Must be the ID of a registered client.
	 */
	client_id: string;
	/**
	 * The prompt parameter is used to specify the type of user interaction that is required.
	 */
	prompt?: "none" | "consent" | "login" | "select_account";
	/**
	 * The display parameter is used to specify how the authorization server displays the
	 * authentication and consent user interface pages to the end user.
	 */
	display?: "page" | "popup" | "touch" | "wap";
	/**
	 * End-User's preferred languages and scripts for the user interface, represented as a
	 * space-separated list of BCP47 [RFC5646] language tag values, ordered by preference. For
	 * instance, the value "fr-CA fr en" represents a preference for French as spoken in Canada,
	 * then French (without a region designation), followed by English (without a region
	 * designation).
	 *
	 * Better Auth does not support this parameter yet. It'll not throw an error if it's provided,
	 *
	 * 🏗️ currently not implemented
	 */
	ui_locales?: string;
	/**
	 * The maximum authentication age.
	 *
	 * Specifies the allowable elapsed time in seconds since the last time the End-User was
	 * actively authenticated by the provider. If the elapsed time is greater than this value, the
	 * provider MUST attempt to actively re-authenticate the End-User.
	 *
	 * Note that max_age=0 is equivalent to prompt=login.
	 */
	max_age?: number;
	/**
	 * Requested Authentication Context Class Reference values.
	 *
	 * Space-separated string that
	 * specifies the acr values that the Authorization Server is being requested to use for
	 * processing this Authentication Request, with the values appearing in order of preference.
	 * The Authentication Context Class satisfied by the authentication performed is returned as
	 * the acr Claim Value, as specified in Section 2. The acr Claim is requested as a Voluntary
	 * Claim by this parameter.
	 */
	acr_values?: string;
	/**
	 * Hint to the Authorization Server about the login identifier the End-User might use to log in
	 * (if necessary). This hint can be used by an RP if it first asks the End-User for their
	 * e-mail address (or other identifier) and then wants to pass that value as a hint to the
	 * discovered authorization service. It is RECOMMENDED that the hint value match the value used
	 * for discovery. This value MAY also be a phone number in the format specified for the
	 * phone_number Claim. The use of this parameter is left to the OP's discretion.
	 */
	login_hint?: string;
	/**
	 * ID Token previously issued by the Authorization Server being passed as a hint about the
	 * End-User's current or past authenticated session with the Client.
	 *
	 * 🏗️ currently not implemented
	 */
	id_token_hint?: string;
	/**
	 * Code challenge
	 */
	code_challenge?: string;
	/**
	 * Code challenge method used
	 */
	code_challenge_method?: "plain" | "s256";
	/**
	 * String value used to associate a Client session with an ID Token, and to mitigate replay
	 * attacks. The value is passed through unmodified from the Authentication Request to the ID Token.
	 * If present in the ID Token, Clients MUST verify that the nonce Claim Value is equal to the
	 * value of the nonce parameter sent in the Authentication Request. If present in the
	 * Authentication Request, Authorization Servers MUST include a nonce Claim in the ID Token
	 * with the Claim Value being the nonce value sent in the Authentication Request.
	 */
	nonce?: string;
}

export interface Client {
	/**
	 * Client ID
	 *
	 * size 32
	 *
	 * as described on https://www.rfc-editor.org/rfc/rfc6749.html#section-2.2
	 */
	clientId: string;
	/**
	 * Client Secret
	 *
	 * A secret for the client, if required by the authorization server.
	 * Optional for public clients using PKCE.
	 *
	 * size 32
	 */
	clientSecret?: string;
	/**
	 * The client type
	 *
	 * as described on https://www.rfc-editor.org/rfc/rfc6749.html#section-2.1
	 *
	 * - web - A web application
	 * - native - A mobile application
	 * - user-agent-based - A user-agent-based application
	 * - public - A public client (PKCE-enabled, no client_secret)
	 */
	type: "web" | "native" | "user-agent-based" | "public";
	/**
	 * List of registered redirect URLs. Must include the whole URL, including the protocol, port,
	 * and path.
	 *
	 * For example, `https://example.com/auth/callback`
	 */
	redirectURLs: string[];
	/**
	 * The name of the client.
	 */
	name: string;
	/**
	 * The icon of the client.
	 */
	icon?: string;
	/**
	 * Additional metadata about the client.
	 */
	metadata: {
		[key: string]: any;
	} | null;
	/**
	 * Whether the client is disabled or not.
	 */
	disabled: boolean;
	/**
	 * Whether to skip the consent screen for this client.
	 * Only applies to trusted clients.
	 */
	skipConsent?: boolean;
}

export interface TokenBody {
	/**
	 * The grant type. Must be 'authorization_code' or 'refresh_token'.
	 */
	grant_type: "authorization_code" | "refresh_token";
	/**
	 * The authorization code received from the authorization server.
	 */
	code?: string;
	/**
	 * The redirect URI of the client.
	 */
	redirect_uri?: string;
	/**
	 * The client ID.
	 */
	client_id?: string;
	/**
	 * The client secret.
	 */
	client_secret?: string;
	/**
	 * The refresh token received from the authorization server.
	 */
	refresh_token?: string;
}

export interface CodeVerificationValue {
	/**
	 * The client ID
	 */
	clientId: string;
	/**
	 * The redirect URI for the client
	 */
	redirectURI: string;
	/**
	 * The scopes that the client requested
	 */
	scope: string[];
	/**
	 * The user ID
	 */
	userId: string;
	/**
	 * The time that the user authenticated
	 */
	authTime: number;
	/**
	 * Whether the user needs to consent to the scopes
	 * before the code can be exchanged for an access token.
	 *
	 * If this is true, then the code is treated as a consent
	 * request. Once the user consents, the code will be updated
	 * with the actual code.
	 */
	requireConsent: boolean;
	/**
	 * The state parameter from the request
	 *
	 * If the prompt is set to `consent`, then the state
	 * parameter is saved here. This is to prevent the client
	 * from using the code before the user consents.
	 */
	state: string | null;
	/**
	 * Code challenge
	 */
	codeChallenge?: string;
	/**
	 * Code Challenge Method
	 */
	codeChallengeMethod?: "sha256" | "plain";
	/**
	 * Nonce
	 */
	nonce?: string;
}

export interface OAuthAccessToken {
	/**
	 * The access token
	 */
	accessToken: string;
	/**
	 * The refresh token
	 */
	refreshToken: string;
	/**
	 * The time that the access token expires
	 */
	accessTokenExpiresAt: Date;
	/**
	 * The time that the refresh token expires
	 */
	refreshTokenExpiresAt: Date;
	/**
	 * The client ID
	 */
	clientId: string;
	/**
	 * The user ID
	 */
	userId: string;
	/**
	 * The scopes that the access token has access to
	 */
	scopes: string;
}

export interface OIDCMetadata {
	/**
	 * The issuer identifier, this is the URL of the provider and can be used to verify
	 * the `iss` claim in the ID token.
	 *
	 * default: the base URL of the server (e.g. `https://example.com`)
	 */
	issuer: string;
	/**
	 * The URL of the authorization endpoint.
	 *
	 * @default `/oauth2/authorize`
	 */
	authorization_endpoint: string;
	/**
	 * The URL of the token endpoint.
	 *
	 * @default `/oauth2/token`
	 */
	token_endpoint: string;
	/**
	 * The URL of the userinfo endpoint.
	 *
	 * @default `/oauth2/userinfo`
	 */
	userinfo_endpoint: string;
	/**
	 * The URL of the jwks_uri endpoint.
	 *
	 * For JWKS to work, you must install the `jwt` plugin.
	 *
	 * This value is automatically set to `/jwks` if the `jwt` plugin is installed.
	 *
	 * @default `/jwks`
	 */
	jwks_uri: string;
	/**
	 * The URL of the dynamic client registration endpoint.
	 *
	 * @default `/oauth2/register`
	 */
	registration_endpoint: string;
	/**
	 * Supported scopes.
	 */
	scopes_supported: string[];
	/**
	 * Supported response types.
	 *
	 * only `code` is supported.
	 */
	response_types_supported: ["code"];
	/**
	 * Supported response modes.
	 *
	 * `query`: the authorization code is returned in the query string
	 *
	 * only `query` is supported.
	 */
	response_modes_supported: ["query"];
	/**
	 * Supported grant types.
	 *
	 * The first element MUST be "authorization_code"; additional grant types like
	 * "refresh_token" can follow. Guarantees a non-empty array at the type level.
	 */
	grant_types_supported: [
		"authorization_code",
		...("authorization_code" | "refresh_token")[],
	];
	/**
	 * acr_values supported.
	 *
	 * - `urn:mace:incommon:iap:silver`: Silver level of assurance
	 * - `urn:mace:incommon:iap:bronze`: Bronze level of assurance
	 *
	 * only `urn:mace:incommon:iap:silver` and `urn:mace:incommon:iap:bronze` are supported.
	 *
	 *
	 * @default
	 * ["urn:mace:incommon:iap:silver", "urn:mace:incommon:iap:bronze"]
	 * @see https://incommon.org/federation/attributes.html
	 */
	acr_values_supported: string[];
	/**
	 * Supported subject types.
	 *
	 * pairwise: the subject identifier is unique to the client
	 * public: the subject identifier is unique to the server
	 *
	 * only `public` is supported.
	 */
	subject_types_supported: ["public"];
	/**
	 * Supported ID token signing algorithms.
	 */
	id_token_signing_alg_values_supported: string[];
	/**
	 * Supported token endpoint authentication methods.
	 *
	 * only `client_secret_basic` and `client_secret_post` are supported.
	 *
	 * @default
	 * ["client_secret_basic", "client_secret_post"]
	 */
	token_endpoint_auth_methods_supported: [
		"client_secret_basic",
		"client_secret_post",
		"none",
	];
	/**
	 * Supported claims.
	 *
	 * @default
	 * ["sub", "iss", "aud", "exp", "nbf", "iat", "jti", "email", "email_verified", "name"]
	 */
	claims_supported: string[];
	/**
	 * Supported code challenge methods.
	 *
	 * only `S256` is supported.
	 *
	 * @default ["S256"]
	 */
	code_challenge_methods_supported: ["S256"];
}

```
Page 28/52FirstPrevNextLast