#
tokens: 46213/50000 3/1091 files (page 49/67)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 49 of 67. Use http://codebase.md/better-auth/better-auth?lines=true&page={x} to view the full context.

# Directory Structure

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

# Files

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

```typescript
   1 | import {
   2 | 	generateAuthenticationOptions,
   3 | 	generateRegistrationOptions,
   4 | 	verifyAuthenticationResponse,
   5 | 	verifyRegistrationResponse,
   6 | } from "@simplewebauthn/server";
   7 | import type {
   8 | 	AuthenticationResponseJSON,
   9 | 	AuthenticatorTransportFuture,
  10 | 	CredentialDeviceType,
  11 | 	PublicKeyCredentialCreationOptionsJSON,
  12 | } from "@simplewebauthn/server";
  13 | import { APIError } from "better-call";
  14 | import { generateRandomString } from "../../crypto/random";
  15 | import * as z from "zod";
  16 | import { createAuthEndpoint } from "@better-auth/core/middleware";
  17 | import { sessionMiddleware } from "../../api";
  18 | import { freshSessionMiddleware, getSessionFromCtx } from "../../api/routes";
  19 | import type { InferOptionSchema } from "../../types/plugins";
  20 | import type { BetterAuthPlugin } from "@better-auth/core";
  21 | import { setSessionCookie } from "../../cookies";
  22 | import { generateId } from "../../utils";
  23 | import { mergeSchema } from "../../db/schema";
  24 | import { base64 } from "@better-auth/utils/base64";
  25 | import type { BetterAuthPluginDBSchema } from "@better-auth/core/db";
  26 | import { defineErrorCodes } from "@better-auth/core/utils";
  27 | 
  28 | interface WebAuthnChallengeValue {
  29 | 	expectedChallenge: string;
  30 | 	userData: {
  31 | 		id: string;
  32 | 	};
  33 | }
  34 | 
  35 | const ERROR_CODES = defineErrorCodes({
  36 | 	CHALLENGE_NOT_FOUND: "Challenge not found",
  37 | 	YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY:
  38 | 		"You are not allowed to register this passkey",
  39 | 	FAILED_TO_VERIFY_REGISTRATION: "Failed to verify registration",
  40 | 	PASSKEY_NOT_FOUND: "Passkey not found",
  41 | 	AUTHENTICATION_FAILED: "Authentication failed",
  42 | 	UNABLE_TO_CREATE_SESSION: "Unable to create session",
  43 | 	FAILED_TO_UPDATE_PASSKEY: "Failed to update passkey",
  44 | });
  45 | 
  46 | function getRpID(options: PasskeyOptions, baseURL?: string) {
  47 | 	return (
  48 | 		options.rpID || (baseURL ? new URL(baseURL).hostname : "localhost") // default rpID
  49 | 	);
  50 | }
  51 | 
  52 | export interface PasskeyOptions {
  53 | 	/**
  54 | 	 * A unique identifier for your website. 'localhost' is okay for
  55 | 	 * local dev
  56 | 	 *
  57 | 	 * @default "localhost"
  58 | 	 */
  59 | 	rpID?: string;
  60 | 	/**
  61 | 	 * Human-readable title for your website
  62 | 	 *
  63 | 	 * @default "Better Auth"
  64 | 	 */
  65 | 	rpName?: string;
  66 | 	/**
  67 | 	 * The URL at which registrations and authentications should occur.
  68 | 	 * `http://localhost` and `http://localhost:PORT` are also valid.
  69 | 	 * Do NOT include any trailing /
  70 | 	 *
  71 | 	 * if this isn't provided. The client itself will
  72 | 	 * pass this value.
  73 | 	 */
  74 | 	origin?: string | string[] | null;
  75 | 
  76 | 	/**
  77 | 	 * Allow customization of the authenticatorSelection options
  78 | 	 * during passkey registration.
  79 | 	 */
  80 | 	authenticatorSelection?: AuthenticatorSelectionCriteria;
  81 | 
  82 | 	/**
  83 | 	 * Advanced options
  84 | 	 */
  85 | 	advanced?: {
  86 | 		webAuthnChallengeCookie?: string;
  87 | 	};
  88 | 	/**
  89 | 	 * Schema for the passkey model
  90 | 	 */
  91 | 	schema?: InferOptionSchema<typeof schema>;
  92 | }
  93 | 
  94 | export type Passkey = {
  95 | 	id: string;
  96 | 	name?: string;
  97 | 	publicKey: string;
  98 | 	userId: string;
  99 | 	credentialID: string;
 100 | 	counter: number;
 101 | 	deviceType: CredentialDeviceType;
 102 | 	backedUp: boolean;
 103 | 	transports?: string;
 104 | 	createdAt: Date;
 105 | 	aaguid?: string;
 106 | };
 107 | 
 108 | export const passkey = (options?: PasskeyOptions) => {
 109 | 	const opts = {
 110 | 		origin: null,
 111 | 		...options,
 112 | 		advanced: {
 113 | 			webAuthnChallengeCookie: "better-auth-passkey",
 114 | 			...options?.advanced,
 115 | 		},
 116 | 	};
 117 | 	const expirationTime = new Date(Date.now() + 1000 * 60 * 5);
 118 | 	const currentTime = new Date();
 119 | 	const maxAgeInSeconds = Math.floor(
 120 | 		(expirationTime.getTime() - currentTime.getTime()) / 1000,
 121 | 	);
 122 | 
 123 | 	return {
 124 | 		id: "passkey",
 125 | 		endpoints: {
 126 | 			generatePasskeyRegistrationOptions: createAuthEndpoint(
 127 | 				"/passkey/generate-register-options",
 128 | 				{
 129 | 					method: "GET",
 130 | 					use: [freshSessionMiddleware],
 131 | 					query: z
 132 | 						.object({
 133 | 							authenticatorAttachment: z
 134 | 								.enum(["platform", "cross-platform"])
 135 | 								.optional(),
 136 | 							name: z.string().optional(),
 137 | 						})
 138 | 						.optional(),
 139 | 					metadata: {
 140 | 						client: false,
 141 | 						openapi: {
 142 | 							description: "Generate registration options for a new passkey",
 143 | 							responses: {
 144 | 								200: {
 145 | 									description: "Success",
 146 | 									parameters: {
 147 | 										query: {
 148 | 											authenticatorAttachment: {
 149 | 												description: `Type of authenticator to use for registration.
 150 |                           "platform" for device-specific authenticators,
 151 |                           "cross-platform" for authenticators that can be used across devices.`,
 152 | 												required: false,
 153 | 											},
 154 | 											name: {
 155 | 												description: `Optional custom name for the passkey.
 156 |                           This can help identify the passkey when managing multiple credentials.`,
 157 | 												required: false,
 158 | 											},
 159 | 										},
 160 | 									},
 161 | 									content: {
 162 | 										"application/json": {
 163 | 											schema: {
 164 | 												type: "object",
 165 | 												properties: {
 166 | 													challenge: {
 167 | 														type: "string",
 168 | 													},
 169 | 													rp: {
 170 | 														type: "object",
 171 | 														properties: {
 172 | 															name: {
 173 | 																type: "string",
 174 | 															},
 175 | 															id: {
 176 | 																type: "string",
 177 | 															},
 178 | 														},
 179 | 													},
 180 | 													user: {
 181 | 														type: "object",
 182 | 														properties: {
 183 | 															id: {
 184 | 																type: "string",
 185 | 															},
 186 | 															name: {
 187 | 																type: "string",
 188 | 															},
 189 | 															displayName: {
 190 | 																type: "string",
 191 | 															},
 192 | 														},
 193 | 													},
 194 | 													pubKeyCredParams: {
 195 | 														type: "array",
 196 | 														items: {
 197 | 															type: "object",
 198 | 															properties: {
 199 | 																type: {
 200 | 																	type: "string",
 201 | 																},
 202 | 																alg: {
 203 | 																	type: "number",
 204 | 																},
 205 | 															},
 206 | 														},
 207 | 													},
 208 | 													timeout: {
 209 | 														type: "number",
 210 | 													},
 211 | 													excludeCredentials: {
 212 | 														type: "array",
 213 | 														items: {
 214 | 															type: "object",
 215 | 															properties: {
 216 | 																id: {
 217 | 																	type: "string",
 218 | 																},
 219 | 																type: {
 220 | 																	type: "string",
 221 | 																},
 222 | 																transports: {
 223 | 																	type: "array",
 224 | 																	items: {
 225 | 																		type: "string",
 226 | 																	},
 227 | 																},
 228 | 															},
 229 | 														},
 230 | 													},
 231 | 													authenticatorSelection: {
 232 | 														type: "object",
 233 | 														properties: {
 234 | 															authenticatorAttachment: {
 235 | 																type: "string",
 236 | 															},
 237 | 															requireResidentKey: {
 238 | 																type: "boolean",
 239 | 															},
 240 | 															userVerification: {
 241 | 																type: "string",
 242 | 															},
 243 | 														},
 244 | 													},
 245 | 													attestation: {
 246 | 														type: "string",
 247 | 													},
 248 | 
 249 | 													extensions: {
 250 | 														type: "object",
 251 | 													},
 252 | 												},
 253 | 											},
 254 | 										},
 255 | 									},
 256 | 								},
 257 | 							},
 258 | 						},
 259 | 					},
 260 | 				},
 261 | 				async (ctx) => {
 262 | 					const { session } = ctx.context;
 263 | 					const userPasskeys = await ctx.context.adapter.findMany<Passkey>({
 264 | 						model: "passkey",
 265 | 						where: [
 266 | 							{
 267 | 								field: "userId",
 268 | 								value: session.user.id,
 269 | 							},
 270 | 						],
 271 | 					});
 272 | 					const userID = new TextEncoder().encode(
 273 | 						generateRandomString(32, "a-z", "0-9"),
 274 | 					);
 275 | 					let options: PublicKeyCredentialCreationOptionsJSON;
 276 | 					options = await generateRegistrationOptions({
 277 | 						rpName: opts.rpName || ctx.context.appName,
 278 | 						rpID: getRpID(opts, ctx.context.options.baseURL),
 279 | 						userID,
 280 | 						userName: ctx.query?.name || session.user.email || session.user.id,
 281 | 						userDisplayName: session.user.email || session.user.id,
 282 | 						attestationType: "none",
 283 | 						excludeCredentials: userPasskeys.map((passkey) => ({
 284 | 							id: passkey.credentialID,
 285 | 							transports: passkey.transports?.split(
 286 | 								",",
 287 | 							) as AuthenticatorTransportFuture[],
 288 | 						})),
 289 | 						authenticatorSelection: {
 290 | 							residentKey: "preferred",
 291 | 							userVerification: "preferred",
 292 | 							...(opts.authenticatorSelection || {}),
 293 | 							...(ctx.query?.authenticatorAttachment
 294 | 								? {
 295 | 										authenticatorAttachment: ctx.query.authenticatorAttachment,
 296 | 									}
 297 | 								: {}),
 298 | 						},
 299 | 					});
 300 | 					const id = generateId(32);
 301 | 					const webAuthnCookie = ctx.context.createAuthCookie(
 302 | 						opts.advanced.webAuthnChallengeCookie,
 303 | 					);
 304 | 					await ctx.setSignedCookie(
 305 | 						webAuthnCookie.name,
 306 | 						id,
 307 | 						ctx.context.secret,
 308 | 						{
 309 | 							...webAuthnCookie.attributes,
 310 | 							maxAge: maxAgeInSeconds,
 311 | 						},
 312 | 					);
 313 | 					await ctx.context.internalAdapter.createVerificationValue(
 314 | 						{
 315 | 							identifier: id,
 316 | 							value: JSON.stringify({
 317 | 								expectedChallenge: options.challenge,
 318 | 								userData: {
 319 | 									id: session.user.id,
 320 | 								},
 321 | 							}),
 322 | 							expiresAt: expirationTime,
 323 | 						},
 324 | 						ctx,
 325 | 					);
 326 | 					return ctx.json(options, {
 327 | 						status: 200,
 328 | 					});
 329 | 				},
 330 | 			),
 331 | 			generatePasskeyAuthenticationOptions: createAuthEndpoint(
 332 | 				"/passkey/generate-authenticate-options",
 333 | 				{
 334 | 					method: "POST",
 335 | 					metadata: {
 336 | 						openapi: {
 337 | 							description: "Generate authentication options for a passkey",
 338 | 							responses: {
 339 | 								200: {
 340 | 									description: "Success",
 341 | 									content: {
 342 | 										"application/json": {
 343 | 											schema: {
 344 | 												type: "object",
 345 | 												properties: {
 346 | 													challenge: {
 347 | 														type: "string",
 348 | 													},
 349 | 													rp: {
 350 | 														type: "object",
 351 | 														properties: {
 352 | 															name: {
 353 | 																type: "string",
 354 | 															},
 355 | 															id: {
 356 | 																type: "string",
 357 | 															},
 358 | 														},
 359 | 													},
 360 | 													user: {
 361 | 														type: "object",
 362 | 														properties: {
 363 | 															id: {
 364 | 																type: "string",
 365 | 															},
 366 | 															name: {
 367 | 																type: "string",
 368 | 															},
 369 | 															displayName: {
 370 | 																type: "string",
 371 | 															},
 372 | 														},
 373 | 													},
 374 | 													timeout: {
 375 | 														type: "number",
 376 | 													},
 377 | 													allowCredentials: {
 378 | 														type: "array",
 379 | 														items: {
 380 | 															type: "object",
 381 | 															properties: {
 382 | 																id: {
 383 | 																	type: "string",
 384 | 																},
 385 | 																type: {
 386 | 																	type: "string",
 387 | 																},
 388 | 																transports: {
 389 | 																	type: "array",
 390 | 																	items: {
 391 | 																		type: "string",
 392 | 																	},
 393 | 																},
 394 | 															},
 395 | 														},
 396 | 													},
 397 | 													userVerification: {
 398 | 														type: "string",
 399 | 													},
 400 | 													authenticatorSelection: {
 401 | 														type: "object",
 402 | 														properties: {
 403 | 															authenticatorAttachment: {
 404 | 																type: "string",
 405 | 															},
 406 | 															requireResidentKey: {
 407 | 																type: "boolean",
 408 | 															},
 409 | 															userVerification: {
 410 | 																type: "string",
 411 | 															},
 412 | 														},
 413 | 													},
 414 | 													extensions: {
 415 | 														type: "object",
 416 | 													},
 417 | 												},
 418 | 											},
 419 | 										},
 420 | 									},
 421 | 								},
 422 | 							},
 423 | 						},
 424 | 					},
 425 | 				},
 426 | 				async (ctx) => {
 427 | 					const session = await getSessionFromCtx(ctx);
 428 | 					let userPasskeys: Passkey[] = [];
 429 | 					if (session) {
 430 | 						userPasskeys = await ctx.context.adapter.findMany<Passkey>({
 431 | 							model: "passkey",
 432 | 							where: [
 433 | 								{
 434 | 									field: "userId",
 435 | 									value: session.user.id,
 436 | 								},
 437 | 							],
 438 | 						});
 439 | 					}
 440 | 					const options = await generateAuthenticationOptions({
 441 | 						rpID: getRpID(opts, ctx.context.options.baseURL),
 442 | 						userVerification: "preferred",
 443 | 						...(userPasskeys.length
 444 | 							? {
 445 | 									allowCredentials: userPasskeys.map((passkey) => ({
 446 | 										id: passkey.credentialID,
 447 | 										transports: passkey.transports?.split(
 448 | 											",",
 449 | 										) as AuthenticatorTransportFuture[],
 450 | 									})),
 451 | 								}
 452 | 							: {}),
 453 | 					});
 454 | 					const data = {
 455 | 						expectedChallenge: options.challenge,
 456 | 						userData: {
 457 | 							id: session?.user.id || "",
 458 | 						},
 459 | 					};
 460 | 					const id = generateId(32);
 461 | 					const webAuthnCookie = ctx.context.createAuthCookie(
 462 | 						opts.advanced.webAuthnChallengeCookie,
 463 | 					);
 464 | 					await ctx.setSignedCookie(
 465 | 						webAuthnCookie.name,
 466 | 						id,
 467 | 						ctx.context.secret,
 468 | 						{
 469 | 							...webAuthnCookie.attributes,
 470 | 							maxAge: maxAgeInSeconds,
 471 | 						},
 472 | 					);
 473 | 					await ctx.context.internalAdapter.createVerificationValue(
 474 | 						{
 475 | 							identifier: id,
 476 | 							value: JSON.stringify(data),
 477 | 							expiresAt: expirationTime,
 478 | 						},
 479 | 						ctx,
 480 | 					);
 481 | 					return ctx.json(options, {
 482 | 						status: 200,
 483 | 					});
 484 | 				},
 485 | 			),
 486 | 			verifyPasskeyRegistration: createAuthEndpoint(
 487 | 				"/passkey/verify-registration",
 488 | 				{
 489 | 					method: "POST",
 490 | 					body: z.object({
 491 | 						response: z.any(),
 492 | 						name: z
 493 | 							.string()
 494 | 							.meta({
 495 | 								description: "Name of the passkey",
 496 | 							})
 497 | 							.optional(),
 498 | 					}),
 499 | 					use: [freshSessionMiddleware],
 500 | 					metadata: {
 501 | 						openapi: {
 502 | 							description: "Verify registration of a new passkey",
 503 | 							responses: {
 504 | 								200: {
 505 | 									description: "Success",
 506 | 									content: {
 507 | 										"application/json": {
 508 | 											schema: {
 509 | 												$ref: "#/components/schemas/Passkey",
 510 | 											},
 511 | 										},
 512 | 									},
 513 | 								},
 514 | 								400: {
 515 | 									description: "Bad request",
 516 | 								},
 517 | 							},
 518 | 						},
 519 | 					},
 520 | 				},
 521 | 				async (ctx) => {
 522 | 					const origin = options?.origin || ctx.headers?.get("origin") || "";
 523 | 					if (!origin) {
 524 | 						return ctx.json(null, {
 525 | 							status: 400,
 526 | 						});
 527 | 					}
 528 | 					const resp = ctx.body.response;
 529 | 					const webAuthnCookie = ctx.context.createAuthCookie(
 530 | 						opts.advanced.webAuthnChallengeCookie,
 531 | 					);
 532 | 					const challengeId = await ctx.getSignedCookie(
 533 | 						webAuthnCookie.name,
 534 | 						ctx.context.secret,
 535 | 					);
 536 | 					if (!challengeId) {
 537 | 						throw new APIError("BAD_REQUEST", {
 538 | 							message: ERROR_CODES.CHALLENGE_NOT_FOUND,
 539 | 						});
 540 | 					}
 541 | 
 542 | 					const data =
 543 | 						await ctx.context.internalAdapter.findVerificationValue(
 544 | 							challengeId,
 545 | 						);
 546 | 					if (!data) {
 547 | 						return ctx.json(null, {
 548 | 							status: 400,
 549 | 						});
 550 | 					}
 551 | 					const { expectedChallenge, userData } = JSON.parse(
 552 | 						data.value,
 553 | 					) as WebAuthnChallengeValue;
 554 | 
 555 | 					if (userData.id !== ctx.context.session.user.id) {
 556 | 						throw new APIError("UNAUTHORIZED", {
 557 | 							message: ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY,
 558 | 						});
 559 | 					}
 560 | 
 561 | 					try {
 562 | 						const verification = await verifyRegistrationResponse({
 563 | 							response: resp,
 564 | 							expectedChallenge,
 565 | 							expectedOrigin: origin,
 566 | 							expectedRPID: getRpID(opts, ctx.context.options.baseURL),
 567 | 							requireUserVerification: false,
 568 | 						});
 569 | 						const { verified, registrationInfo } = verification;
 570 | 						if (!verified || !registrationInfo) {
 571 | 							return ctx.json(null, {
 572 | 								status: 400,
 573 | 							});
 574 | 						}
 575 | 						const {
 576 | 							aaguid,
 577 | 							// credentialID,
 578 | 							// credentialPublicKey,
 579 | 							// counter,
 580 | 							credentialDeviceType,
 581 | 							credentialBackedUp,
 582 | 							credential,
 583 | 							credentialType,
 584 | 						} = registrationInfo;
 585 | 						const pubKey = base64.encode(credential.publicKey);
 586 | 						const newPasskey: Omit<Passkey, "id"> = {
 587 | 							name: ctx.body.name,
 588 | 							userId: userData.id,
 589 | 							credentialID: credential.id,
 590 | 							publicKey: pubKey,
 591 | 							counter: credential.counter,
 592 | 							deviceType: credentialDeviceType,
 593 | 							transports: resp.response.transports.join(","),
 594 | 							backedUp: credentialBackedUp,
 595 | 							createdAt: new Date(),
 596 | 							aaguid: aaguid,
 597 | 						};
 598 | 						const newPasskeyRes = await ctx.context.adapter.create<
 599 | 							Omit<Passkey, "id">,
 600 | 							Passkey
 601 | 						>({
 602 | 							model: "passkey",
 603 | 							data: newPasskey,
 604 | 						});
 605 | 						return ctx.json(newPasskeyRes, {
 606 | 							status: 200,
 607 | 						});
 608 | 					} catch (e) {
 609 | 						console.log(e);
 610 | 						throw new APIError("INTERNAL_SERVER_ERROR", {
 611 | 							message: ERROR_CODES.FAILED_TO_VERIFY_REGISTRATION,
 612 | 						});
 613 | 					}
 614 | 				},
 615 | 			),
 616 | 			verifyPasskeyAuthentication: createAuthEndpoint(
 617 | 				"/passkey/verify-authentication",
 618 | 				{
 619 | 					method: "POST",
 620 | 					body: z.object({
 621 | 						response: z.record(z.any(), z.any()),
 622 | 					}),
 623 | 					metadata: {
 624 | 						openapi: {
 625 | 							description: "Verify authentication of a passkey",
 626 | 							responses: {
 627 | 								200: {
 628 | 									description: "Success",
 629 | 									content: {
 630 | 										"application/json": {
 631 | 											schema: {
 632 | 												type: "object",
 633 | 												properties: {
 634 | 													session: {
 635 | 														$ref: "#/components/schemas/Session",
 636 | 													},
 637 | 													user: {
 638 | 														$ref: "#/components/schemas/User",
 639 | 													},
 640 | 												},
 641 | 											},
 642 | 										},
 643 | 									},
 644 | 								},
 645 | 							},
 646 | 						},
 647 | 						$Infer: {
 648 | 							body: {} as {
 649 | 								response: AuthenticationResponseJSON;
 650 | 							},
 651 | 						},
 652 | 					},
 653 | 				},
 654 | 				async (ctx) => {
 655 | 					const origin = options?.origin || ctx.headers?.get("origin") || "";
 656 | 					if (!origin) {
 657 | 						throw new APIError("BAD_REQUEST", {
 658 | 							message: "origin missing",
 659 | 						});
 660 | 					}
 661 | 					const resp = ctx.body.response;
 662 | 					const webAuthnCookie = ctx.context.createAuthCookie(
 663 | 						opts.advanced.webAuthnChallengeCookie,
 664 | 					);
 665 | 					const challengeId = await ctx.getSignedCookie(
 666 | 						webAuthnCookie.name,
 667 | 						ctx.context.secret,
 668 | 					);
 669 | 					if (!challengeId) {
 670 | 						throw new APIError("BAD_REQUEST", {
 671 | 							message: ERROR_CODES.CHALLENGE_NOT_FOUND,
 672 | 						});
 673 | 					}
 674 | 
 675 | 					const data =
 676 | 						await ctx.context.internalAdapter.findVerificationValue(
 677 | 							challengeId,
 678 | 						);
 679 | 					if (!data) {
 680 | 						throw new APIError("BAD_REQUEST", {
 681 | 							message: ERROR_CODES.CHALLENGE_NOT_FOUND,
 682 | 						});
 683 | 					}
 684 | 					const { expectedChallenge } = JSON.parse(
 685 | 						data.value,
 686 | 					) as WebAuthnChallengeValue;
 687 | 					const passkey = await ctx.context.adapter.findOne<Passkey>({
 688 | 						model: "passkey",
 689 | 						where: [
 690 | 							{
 691 | 								field: "credentialID",
 692 | 								value: resp.id,
 693 | 							},
 694 | 						],
 695 | 					});
 696 | 					if (!passkey) {
 697 | 						throw new APIError("UNAUTHORIZED", {
 698 | 							message: ERROR_CODES.PASSKEY_NOT_FOUND,
 699 | 						});
 700 | 					}
 701 | 					try {
 702 | 						const verification = await verifyAuthenticationResponse({
 703 | 							response: resp as AuthenticationResponseJSON,
 704 | 							expectedChallenge,
 705 | 							expectedOrigin: origin,
 706 | 							expectedRPID: getRpID(opts, ctx.context.options.baseURL),
 707 | 							credential: {
 708 | 								id: passkey.credentialID,
 709 | 								publicKey: base64.decode(passkey.publicKey),
 710 | 								counter: passkey.counter,
 711 | 								transports: passkey.transports?.split(
 712 | 									",",
 713 | 								) as AuthenticatorTransportFuture[],
 714 | 							},
 715 | 							requireUserVerification: false,
 716 | 						});
 717 | 						const { verified } = verification;
 718 | 						if (!verified)
 719 | 							throw new APIError("UNAUTHORIZED", {
 720 | 								message: ERROR_CODES.AUTHENTICATION_FAILED,
 721 | 							});
 722 | 
 723 | 						await ctx.context.adapter.update<Passkey>({
 724 | 							model: "passkey",
 725 | 							where: [
 726 | 								{
 727 | 									field: "id",
 728 | 									value: passkey.id,
 729 | 								},
 730 | 							],
 731 | 							update: {
 732 | 								counter: verification.authenticationInfo.newCounter,
 733 | 							},
 734 | 						});
 735 | 						const s = await ctx.context.internalAdapter.createSession(
 736 | 							passkey.userId,
 737 | 							ctx,
 738 | 						);
 739 | 						if (!s) {
 740 | 							throw new APIError("INTERNAL_SERVER_ERROR", {
 741 | 								message: ERROR_CODES.UNABLE_TO_CREATE_SESSION,
 742 | 							});
 743 | 						}
 744 | 						const user = await ctx.context.internalAdapter.findUserById(
 745 | 							passkey.userId,
 746 | 						);
 747 | 						if (!user) {
 748 | 							throw new APIError("INTERNAL_SERVER_ERROR", {
 749 | 								message: "User not found",
 750 | 							});
 751 | 						}
 752 | 						await setSessionCookie(ctx, {
 753 | 							session: s,
 754 | 							user,
 755 | 						});
 756 | 						return ctx.json(
 757 | 							{
 758 | 								session: s,
 759 | 							},
 760 | 							{
 761 | 								status: 200,
 762 | 							},
 763 | 						);
 764 | 					} catch (e) {
 765 | 						ctx.context.logger.error("Failed to verify authentication", e);
 766 | 						throw new APIError("BAD_REQUEST", {
 767 | 							message: ERROR_CODES.AUTHENTICATION_FAILED,
 768 | 						});
 769 | 					}
 770 | 				},
 771 | 			),
 772 | 			/**
 773 | 			 * ### Endpoint
 774 | 			 *
 775 | 			 * GET `/passkey/list-user-passkeys`
 776 | 			 *
 777 | 			 * ### API Methods
 778 | 			 *
 779 | 			 * **server:**
 780 | 			 * `auth.api.listPasskeys`
 781 | 			 *
 782 | 			 * **client:**
 783 | 			 * `authClient.passkey.listUserPasskeys`
 784 | 			 *
 785 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/passkey#api-method-passkey-list-user-passkeys)
 786 | 			 */
 787 | 			listPasskeys: createAuthEndpoint(
 788 | 				"/passkey/list-user-passkeys",
 789 | 				{
 790 | 					method: "GET",
 791 | 					use: [sessionMiddleware],
 792 | 					metadata: {
 793 | 						openapi: {
 794 | 							description: "List all passkeys for the authenticated user",
 795 | 							responses: {
 796 | 								"200": {
 797 | 									description: "Passkeys retrieved successfully",
 798 | 									content: {
 799 | 										"application/json": {
 800 | 											schema: {
 801 | 												type: "array",
 802 | 												items: {
 803 | 													$ref: "#/components/schemas/Passkey",
 804 | 													required: [
 805 | 														"id",
 806 | 														"userId",
 807 | 														"publicKey",
 808 | 														"createdAt",
 809 | 														"updatedAt",
 810 | 													],
 811 | 												},
 812 | 												description:
 813 | 													"Array of passkey objects associated with the user",
 814 | 											},
 815 | 										},
 816 | 									},
 817 | 								},
 818 | 							},
 819 | 						},
 820 | 					},
 821 | 				},
 822 | 				async (ctx) => {
 823 | 					const passkeys = await ctx.context.adapter.findMany<Passkey>({
 824 | 						model: "passkey",
 825 | 						where: [{ field: "userId", value: ctx.context.session.user.id }],
 826 | 					});
 827 | 					return ctx.json(passkeys, {
 828 | 						status: 200,
 829 | 					});
 830 | 				},
 831 | 			),
 832 | 			/**
 833 | 			 * ### Endpoint
 834 | 			 *
 835 | 			 * POST `/passkey/delete-passkey`
 836 | 			 *
 837 | 			 * ### API Methods
 838 | 			 *
 839 | 			 * **server:**
 840 | 			 * `auth.api.deletePasskey`
 841 | 			 *
 842 | 			 * **client:**
 843 | 			 * `authClient.passkey.deletePasskey`
 844 | 			 *
 845 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/passkey#api-method-passkey-delete-passkey)
 846 | 			 */
 847 | 			deletePasskey: createAuthEndpoint(
 848 | 				"/passkey/delete-passkey",
 849 | 				{
 850 | 					method: "POST",
 851 | 					body: z.object({
 852 | 						id: z.string().meta({
 853 | 							description:
 854 | 								'The ID of the passkey to delete. Eg: "some-passkey-id"',
 855 | 						}),
 856 | 					}),
 857 | 					use: [sessionMiddleware],
 858 | 					metadata: {
 859 | 						openapi: {
 860 | 							description: "Delete a specific passkey",
 861 | 							responses: {
 862 | 								"200": {
 863 | 									description: "Passkey deleted successfully",
 864 | 									content: {
 865 | 										"application/json": {
 866 | 											schema: {
 867 | 												type: "object",
 868 | 												properties: {
 869 | 													status: {
 870 | 														type: "boolean",
 871 | 														description:
 872 | 															"Indicates whether the deletion was successful",
 873 | 													},
 874 | 												},
 875 | 												required: ["status"],
 876 | 											},
 877 | 										},
 878 | 									},
 879 | 								},
 880 | 							},
 881 | 						},
 882 | 					},
 883 | 				},
 884 | 				async (ctx) => {
 885 | 					await ctx.context.adapter.delete<Passkey>({
 886 | 						model: "passkey",
 887 | 						where: [
 888 | 							{
 889 | 								field: "id",
 890 | 								value: ctx.body.id,
 891 | 							},
 892 | 						],
 893 | 					});
 894 | 					return ctx.json(null, {
 895 | 						status: 200,
 896 | 					});
 897 | 				},
 898 | 			),
 899 | 			/**
 900 | 			 * ### Endpoint
 901 | 			 *
 902 | 			 * POST `/passkey/update-passkey`
 903 | 			 *
 904 | 			 * ### API Methods
 905 | 			 *
 906 | 			 * **server:**
 907 | 			 * `auth.api.updatePasskey`
 908 | 			 *
 909 | 			 * **client:**
 910 | 			 * `authClient.passkey.updatePasskey`
 911 | 			 *
 912 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/passkey#api-method-passkey-update-passkey)
 913 | 			 */
 914 | 			updatePasskey: createAuthEndpoint(
 915 | 				"/passkey/update-passkey",
 916 | 				{
 917 | 					method: "POST",
 918 | 					body: z.object({
 919 | 						id: z.string().meta({
 920 | 							description: `The ID of the passkey which will be updated. Eg: \"passkey-id\"`,
 921 | 						}),
 922 | 						name: z.string().meta({
 923 | 							description: `The new name which the passkey will be updated to. Eg: \"my-new-passkey-name\"`,
 924 | 						}),
 925 | 					}),
 926 | 					use: [sessionMiddleware],
 927 | 					metadata: {
 928 | 						openapi: {
 929 | 							description: "Update a specific passkey's name",
 930 | 							responses: {
 931 | 								"200": {
 932 | 									description: "Passkey updated successfully",
 933 | 									content: {
 934 | 										"application/json": {
 935 | 											schema: {
 936 | 												type: "object",
 937 | 												properties: {
 938 | 													passkey: {
 939 | 														$ref: "#/components/schemas/Passkey",
 940 | 													},
 941 | 												},
 942 | 												required: ["passkey"],
 943 | 											},
 944 | 										},
 945 | 									},
 946 | 								},
 947 | 							},
 948 | 						},
 949 | 					},
 950 | 				},
 951 | 				async (ctx) => {
 952 | 					const passkey = await ctx.context.adapter.findOne<Passkey>({
 953 | 						model: "passkey",
 954 | 						where: [
 955 | 							{
 956 | 								field: "id",
 957 | 								value: ctx.body.id,
 958 | 							},
 959 | 						],
 960 | 					});
 961 | 
 962 | 					if (!passkey) {
 963 | 						throw new APIError("NOT_FOUND", {
 964 | 							message: ERROR_CODES.PASSKEY_NOT_FOUND,
 965 | 						});
 966 | 					}
 967 | 
 968 | 					if (passkey.userId !== ctx.context.session.user.id) {
 969 | 						throw new APIError("UNAUTHORIZED", {
 970 | 							message: ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY,
 971 | 						});
 972 | 					}
 973 | 
 974 | 					const updatedPasskey = await ctx.context.adapter.update<Passkey>({
 975 | 						model: "passkey",
 976 | 						where: [
 977 | 							{
 978 | 								field: "id",
 979 | 								value: ctx.body.id,
 980 | 							},
 981 | 						],
 982 | 						update: {
 983 | 							name: ctx.body.name,
 984 | 						},
 985 | 					});
 986 | 
 987 | 					if (!updatedPasskey) {
 988 | 						throw new APIError("INTERNAL_SERVER_ERROR", {
 989 | 							message: ERROR_CODES.FAILED_TO_UPDATE_PASSKEY,
 990 | 						});
 991 | 					}
 992 | 					return ctx.json(
 993 | 						{
 994 | 							passkey: updatedPasskey,
 995 | 						},
 996 | 						{
 997 | 							status: 200,
 998 | 						},
 999 | 					);
1000 | 				},
1001 | 			),
1002 | 		},
1003 | 		schema: mergeSchema(schema, options?.schema),
1004 | 		$ERROR_CODES: ERROR_CODES,
1005 | 	} satisfies BetterAuthPlugin;
1006 | };
1007 | 
1008 | const schema = {
1009 | 	passkey: {
1010 | 		fields: {
1011 | 			name: {
1012 | 				type: "string",
1013 | 				required: false,
1014 | 			},
1015 | 			publicKey: {
1016 | 				type: "string",
1017 | 				required: true,
1018 | 			},
1019 | 			userId: {
1020 | 				type: "string",
1021 | 				references: {
1022 | 					model: "user",
1023 | 					field: "id",
1024 | 				},
1025 | 				required: true,
1026 | 			},
1027 | 			credentialID: {
1028 | 				type: "string",
1029 | 				required: true,
1030 | 			},
1031 | 			counter: {
1032 | 				type: "number",
1033 | 				required: true,
1034 | 			},
1035 | 			deviceType: {
1036 | 				type: "string",
1037 | 				required: true,
1038 | 			},
1039 | 			backedUp: {
1040 | 				type: "boolean",
1041 | 				required: true,
1042 | 			},
1043 | 			transports: {
1044 | 				type: "string",
1045 | 				required: false,
1046 | 			},
1047 | 			createdAt: {
1048 | 				type: "date",
1049 | 				required: false,
1050 | 			},
1051 | 			aaguid: {
1052 | 				type: "string",
1053 | 				required: false,
1054 | 			},
1055 | 		},
1056 | 	},
1057 | } satisfies BetterAuthPluginDBSchema;
1058 | 
```

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

```typescript
   1 | import { APIError } from "better-call";
   2 | import * as z from "zod";
   3 | import type { BetterAuthPluginDBSchema } from "@better-auth/core/db";
   4 | import { createAuthEndpoint } from "@better-auth/core/middleware";
   5 | import { getSessionFromCtx } from "../../api/routes";
   6 | import type { BetterAuthPlugin } from "@better-auth/core";
   7 | import { shimContext } from "../../utils/shim";
   8 | import { type AccessControl } from "../access";
   9 | import { getOrgAdapter } from "./adapter";
  10 | import { orgSessionMiddleware } from "./call";
  11 | import {
  12 | 	acceptInvitation,
  13 | 	cancelInvitation,
  14 | 	createInvitation,
  15 | 	getInvitation,
  16 | 	listInvitations,
  17 | 	rejectInvitation,
  18 | 	listUserInvitations,
  19 | } from "./routes/crud-invites";
  20 | import {
  21 | 	addMember,
  22 | 	getActiveMember,
  23 | 	leaveOrganization,
  24 | 	listMembers,
  25 | 	removeMember,
  26 | 	updateMemberRole,
  27 | 	getActiveMemberRole,
  28 | } from "./routes/crud-members";
  29 | import {
  30 | 	checkOrganizationSlug,
  31 | 	createOrganization,
  32 | 	deleteOrganization,
  33 | 	getFullOrganization,
  34 | 	listOrganizations,
  35 | 	setActiveOrganization,
  36 | 	updateOrganization,
  37 | } from "./routes/crud-org";
  38 | import {
  39 | 	createTeam,
  40 | 	listOrganizationTeams,
  41 | 	removeTeam,
  42 | 	updateTeam,
  43 | 	setActiveTeam,
  44 | 	listUserTeams,
  45 | 	listTeamMembers,
  46 | 	addTeamMember,
  47 | 	removeTeamMember,
  48 | } from "./routes/crud-team";
  49 | import type {
  50 | 	InferInvitation,
  51 | 	InferMember,
  52 | 	InferOrganization,
  53 | 	Team,
  54 | 	TeamMember,
  55 | } from "./schema";
  56 | import {
  57 | 	createOrgRole,
  58 | 	deleteOrgRole,
  59 | 	listOrgRoles,
  60 | 	getOrgRole,
  61 | 	updateOrgRole,
  62 | } from "./routes/crud-access-control";
  63 | import { ORGANIZATION_ERROR_CODES } from "./error-codes";
  64 | import { defaultRoles, defaultStatements } from "./access";
  65 | import { hasPermission } from "./has-permission";
  66 | import type { OrganizationOptions } from "./types";
  67 | import type { AuthContext } from "@better-auth/core";
  68 | 
  69 | export function parseRoles(roles: string | string[]): string {
  70 | 	return Array.isArray(roles) ? roles.join(",") : roles;
  71 | }
  72 | 
  73 | /**
  74 |  * Organization plugin for Better Auth. Organization allows you to create teams, members,
  75 |  * and manage access control for your users.
  76 |  *
  77 |  * @example
  78 |  * ```ts
  79 |  * const auth = betterAuth({
  80 |  * 	plugins: [
  81 |  * 		organization({
  82 |  * 			allowUserToCreateOrganization: true,
  83 |  * 		}),
  84 |  * 	],
  85 |  * });
  86 |  * ```
  87 |  */
  88 | export const organization = <O extends OrganizationOptions>(options?: O) => {
  89 | 	let endpoints = {
  90 | 		/**
  91 | 		 * ### Endpoint
  92 | 		 *
  93 | 		 * POST `/organization/create`
  94 | 		 *
  95 | 		 * ### API Methods
  96 | 		 *
  97 | 		 * **server:**
  98 | 		 * `auth.api.createOrganization`
  99 | 		 *
 100 | 		 * **client:**
 101 | 		 * `authClient.organization.create`
 102 | 		 *
 103 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-create)
 104 | 		 */
 105 | 		createOrganization: createOrganization(options as O),
 106 | 		/**
 107 | 		 * ### Endpoint
 108 | 		 *
 109 | 		 * POST `/organization/update`
 110 | 		 *
 111 | 		 * ### API Methods
 112 | 		 *
 113 | 		 * **server:**
 114 | 		 * `auth.api.updateOrganization`
 115 | 		 *
 116 | 		 * **client:**
 117 | 		 * `authClient.organization.update`
 118 | 		 *
 119 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-update)
 120 | 		 */
 121 | 		updateOrganization: updateOrganization(options as O),
 122 | 		/**
 123 | 		 * ### Endpoint
 124 | 		 *
 125 | 		 * POST `/organization/delete`
 126 | 		 *
 127 | 		 * ### API Methods
 128 | 		 *
 129 | 		 * **server:**
 130 | 		 * `auth.api.deleteOrganization`
 131 | 		 *
 132 | 		 * **client:**
 133 | 		 * `authClient.organization.delete`
 134 | 		 *
 135 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-delete)
 136 | 		 */
 137 | 		deleteOrganization: deleteOrganization(options as O),
 138 | 		/**
 139 | 		 * ### Endpoint
 140 | 		 *
 141 | 		 * POST `/organization/set-active`
 142 | 		 *
 143 | 		 * ### API Methods
 144 | 		 *
 145 | 		 * **server:**
 146 | 		 * `auth.api.setActiveOrganization`
 147 | 		 *
 148 | 		 * **client:**
 149 | 		 * `authClient.organization.setActive`
 150 | 		 *
 151 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-set-active)
 152 | 		 */
 153 | 		setActiveOrganization: setActiveOrganization(options as O),
 154 | 		/**
 155 | 		 * ### Endpoint
 156 | 		 *
 157 | 		 * GET `/organization/get-full-organization`
 158 | 		 *
 159 | 		 * ### API Methods
 160 | 		 *
 161 | 		 * **server:**
 162 | 		 * `auth.api.getFullOrganization`
 163 | 		 *
 164 | 		 * **client:**
 165 | 		 * `authClient.organization.getFullOrganization`
 166 | 		 *
 167 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-get-full-organization)
 168 | 		 */
 169 | 		getFullOrganization: getFullOrganization(options as O),
 170 | 		/**
 171 | 		 * ### Endpoint
 172 | 		 *
 173 | 		 * GET `/organization/list`
 174 | 		 *
 175 | 		 * ### API Methods
 176 | 		 *
 177 | 		 * **server:**
 178 | 		 * `auth.api.listOrganizations`
 179 | 		 *
 180 | 		 * **client:**
 181 | 		 * `authClient.organization.list`
 182 | 		 *
 183 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-list)
 184 | 		 */
 185 | 		listOrganizations: listOrganizations(options as O),
 186 | 		/**
 187 | 		 * ### Endpoint
 188 | 		 *
 189 | 		 * POST `/organization/invite-member`
 190 | 		 *
 191 | 		 * ### API Methods
 192 | 		 *
 193 | 		 * **server:**
 194 | 		 * `auth.api.createInvitation`
 195 | 		 *
 196 | 		 * **client:**
 197 | 		 * `authClient.organization.inviteMember`
 198 | 		 *
 199 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-invite-member)
 200 | 		 */
 201 | 		createInvitation: createInvitation(options as O),
 202 | 		/**
 203 | 		 * ### Endpoint
 204 | 		 *
 205 | 		 * POST `/organization/cancel-invitation`
 206 | 		 *
 207 | 		 * ### API Methods
 208 | 		 *
 209 | 		 * **server:**
 210 | 		 * `auth.api.cancelInvitation`
 211 | 		 *
 212 | 		 * **client:**
 213 | 		 * `authClient.organization.cancelInvitation`
 214 | 		 *
 215 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-cancel-invitation)
 216 | 		 */
 217 | 		cancelInvitation: cancelInvitation(options as O),
 218 | 		/**
 219 | 		 * ### Endpoint
 220 | 		 *
 221 | 		 * POST `/organization/accept-invitation`
 222 | 		 *
 223 | 		 * ### API Methods
 224 | 		 *
 225 | 		 * **server:**
 226 | 		 * `auth.api.acceptInvitation`
 227 | 		 *
 228 | 		 * **client:**
 229 | 		 * `authClient.organization.acceptInvitation`
 230 | 		 *
 231 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-accept-invitation)
 232 | 		 */
 233 | 		acceptInvitation: acceptInvitation(options as O),
 234 | 		/**
 235 | 		 * ### Endpoint
 236 | 		 *
 237 | 		 * GET `/organization/get-invitation`
 238 | 		 *
 239 | 		 * ### API Methods
 240 | 		 *
 241 | 		 * **server:**
 242 | 		 * `auth.api.getInvitation`
 243 | 		 *
 244 | 		 * **client:**
 245 | 		 * `authClient.organization.getInvitation`
 246 | 		 *
 247 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-get-invitation)
 248 | 		 */
 249 | 		getInvitation: getInvitation(options as O),
 250 | 		/**
 251 | 		 * ### Endpoint
 252 | 		 *
 253 | 		 * POST `/organization/reject-invitation`
 254 | 		 *
 255 | 		 * ### API Methods
 256 | 		 *
 257 | 		 * **server:**
 258 | 		 * `auth.api.rejectInvitation`
 259 | 		 *
 260 | 		 * **client:**
 261 | 		 * `authClient.organization.rejectInvitation`
 262 | 		 *
 263 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-reject-invitation)
 264 | 		 */
 265 | 		rejectInvitation: rejectInvitation(options as O),
 266 | 		/**
 267 | 		 * ### Endpoint
 268 | 		 *
 269 | 		 * GET `/organization/list-invitations`
 270 | 		 *
 271 | 		 * ### API Methods
 272 | 		 *
 273 | 		 * **server:**
 274 | 		 * `auth.api.listInvitations`
 275 | 		 *
 276 | 		 * **client:**
 277 | 		 * `authClient.organization.listInvitations`
 278 | 		 *
 279 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-list-invitations)
 280 | 		 */
 281 | 		listInvitations: listInvitations(options as O),
 282 | 		/**
 283 | 		 * ### Endpoint
 284 | 		 *
 285 | 		 * GET `/organization/get-active-member`
 286 | 		 *
 287 | 		 * ### API Methods
 288 | 		 *
 289 | 		 * **server:**
 290 | 		 * `auth.api.getActiveMember`
 291 | 		 *
 292 | 		 * **client:**
 293 | 		 * `authClient.organization.getActiveMember`
 294 | 		 *
 295 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-get-active-member)
 296 | 		 */
 297 | 		getActiveMember: getActiveMember(options as O),
 298 | 		/**
 299 | 		 * ### Endpoint
 300 | 		 *
 301 | 		 * POST `/organization/check-slug`
 302 | 		 *
 303 | 		 * ### API Methods
 304 | 		 *
 305 | 		 * **server:**
 306 | 		 * `auth.api.checkOrganizationSlug`
 307 | 		 *
 308 | 		 * **client:**
 309 | 		 * `authClient.organization.checkSlug`
 310 | 		 *
 311 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-check-slug)
 312 | 		 */
 313 | 		checkOrganizationSlug: checkOrganizationSlug(options as O),
 314 | 		/**
 315 | 		 * ### Endpoint
 316 | 		 *
 317 | 		 * POST `/organization/add-member`
 318 | 		 *
 319 | 		 * ### API Methods
 320 | 		 *
 321 | 		 * **server:**
 322 | 		 * `auth.api.addMember`
 323 | 		 *
 324 | 		 * **client:**
 325 | 		 * `authClient.organization.addMember`
 326 | 		 *
 327 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-add-member)
 328 | 		 */
 329 | 
 330 | 		addMember: addMember<O>(options as O),
 331 | 		/**
 332 | 		 * ### Endpoint
 333 | 		 *
 334 | 		 * POST `/organization/remove-member`
 335 | 		 *
 336 | 		 * ### API Methods
 337 | 		 *
 338 | 		 * **server:**
 339 | 		 * `auth.api.removeMember`
 340 | 		 *
 341 | 		 * **client:**
 342 | 		 * `authClient.organization.removeMember`
 343 | 		 *
 344 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-remove-member)
 345 | 		 */
 346 | 		removeMember: removeMember(options as O),
 347 | 		/**
 348 | 		 * ### Endpoint
 349 | 		 *
 350 | 		 * POST `/organization/update-member-role`
 351 | 		 *
 352 | 		 * ### API Methods
 353 | 		 *
 354 | 		 * **server:**
 355 | 		 * `auth.api.updateMemberRole`
 356 | 		 *
 357 | 		 * **client:**
 358 | 		 * `authClient.organization.updateMemberRole`
 359 | 		 *
 360 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-update-member-role)
 361 | 		 */
 362 | 		updateMemberRole: updateMemberRole(options as O),
 363 | 		/**
 364 | 		 * ### Endpoint
 365 | 		 *
 366 | 		 * POST `/organization/leave`
 367 | 		 *
 368 | 		 * ### API Methods
 369 | 		 *
 370 | 		 * **server:**
 371 | 		 * `auth.api.leaveOrganization`
 372 | 		 *
 373 | 		 * **client:**
 374 | 		 * `authClient.organization.leave`
 375 | 		 *
 376 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-leave)
 377 | 		 */
 378 | 		leaveOrganization: leaveOrganization(options as O),
 379 | 		/**
 380 | 		 * ### Endpoint
 381 | 		 *
 382 | 		 * GET `/organization/list-members`
 383 | 		 *
 384 | 		 * ### API Methods
 385 | 		 *
 386 | 		 * **server:**
 387 | 		 * `auth.api.listMembers`
 388 | 		 *
 389 | 		 * **client:**
 390 | 		 * `authClient.organization.listMembers`
 391 | 		 *
 392 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-list-members)
 393 | 		 */
 394 | 		listUserInvitations: listUserInvitations(options as O),
 395 | 		/**
 396 | 		 * ### Endpoint
 397 | 		 *
 398 | 		 * GET `/organization/list-members`
 399 | 		 *
 400 | 		 * ### API Methods
 401 | 		 *
 402 | 		 * **server:**
 403 | 		 * `auth.api.listMembers`
 404 | 		 *
 405 | 		 * **client:**
 406 | 		 * `authClient.organization.listMembers`
 407 | 		 */
 408 | 		listMembers: listMembers(options as O),
 409 | 		/**
 410 | 		 * ### Endpoint
 411 | 		 *
 412 | 		 * GET `/organization/get-active-member-role`
 413 | 		 *
 414 | 		 * ### API Methods
 415 | 		 *
 416 | 		 * **server:**
 417 | 		 * `auth.api.getActiveMemberRole`
 418 | 		 *
 419 | 		 * **client:**
 420 | 		 * `authClient.organization.getActiveMemberRole`
 421 | 		 *
 422 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-get-active-member-role)
 423 | 		 */
 424 | 		getActiveMemberRole: getActiveMemberRole(options as O),
 425 | 	};
 426 | 	const teamSupport = options?.teams?.enabled;
 427 | 	const teamEndpoints = {
 428 | 		/**
 429 | 		 * ### Endpoint
 430 | 		 *
 431 | 		 * POST `/organization/create-team`
 432 | 		 *
 433 | 		 * ### API Methods
 434 | 		 *
 435 | 		 * **server:**
 436 | 		 * `auth.api.createTeam`
 437 | 		 *
 438 | 		 * **client:**
 439 | 		 * `authClient.organization.createTeam`
 440 | 		 *
 441 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-create-team)
 442 | 		 */
 443 | 		createTeam: createTeam(options as O),
 444 | 		/**
 445 | 		 * ### Endpoint
 446 | 		 *
 447 | 		 * GET `/organization/list-teams`
 448 | 		 *
 449 | 		 * ### API Methods
 450 | 		 *
 451 | 		 * **server:**
 452 | 		 * `auth.api.listOrganizationTeams`
 453 | 		 *
 454 | 		 * **client:**
 455 | 		 * `authClient.organization.listTeams`
 456 | 		 *
 457 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-list-teams)
 458 | 		 */
 459 | 		listOrganizationTeams: listOrganizationTeams(options as O),
 460 | 		/**
 461 | 		 * ### Endpoint
 462 | 		 *
 463 | 		 * POST `/organization/remove-team`
 464 | 		 *
 465 | 		 * ### API Methods
 466 | 		 *
 467 | 		 * **server:**
 468 | 		 * `auth.api.removeTeam`
 469 | 		 *
 470 | 		 * **client:**
 471 | 		 * `authClient.organization.removeTeam`
 472 | 		 *
 473 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-remove-team)
 474 | 		 */
 475 | 		removeTeam: removeTeam(options as O),
 476 | 		/**
 477 | 		 * ### Endpoint
 478 | 		 *
 479 | 		 * POST `/organization/update-team`
 480 | 		 *
 481 | 		 * ### API Methods
 482 | 		 *
 483 | 		 * **server:**
 484 | 		 * `auth.api.updateTeam`
 485 | 		 *
 486 | 		 * **client:**
 487 | 		 * `authClient.organization.updateTeam`
 488 | 		 *
 489 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-method-organization-update-team)
 490 | 		 */
 491 | 		updateTeam: updateTeam(options as O),
 492 | 		/**
 493 | 		 * ### Endpoint
 494 | 		 *
 495 | 		 * POST `/organization/set-active-team`
 496 | 		 *
 497 | 		 * ### API Methods
 498 | 		 *
 499 | 		 * **server:**
 500 | 		 * `auth.api.setActiveTeam`
 501 | 		 *
 502 | 		 * **client:**
 503 | 		 * `authClient.organization.setActiveTeam`
 504 | 		 *
 505 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-set-active-team)
 506 | 		 */
 507 | 		setActiveTeam: setActiveTeam(options as O),
 508 | 		/**
 509 | 		 * ### Endpoint
 510 | 		 *
 511 | 		 * GET `/organization/list-user-teams`
 512 | 		 *
 513 | 		 * ### API Methods
 514 | 		 *
 515 | 		 * **server:**
 516 | 		 * `auth.api.listUserTeams`
 517 | 		 *
 518 | 		 * **client:**
 519 | 		 * `authClient.organization.listUserTeams`
 520 | 		 *
 521 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-set-active-team)
 522 | 		 */
 523 | 		listUserTeams: listUserTeams(options as O),
 524 | 		/**
 525 | 		 * ### Endpoint
 526 | 		 *
 527 | 		 * POST `/organization/list-team-members`
 528 | 		 *
 529 | 		 * ### API Methods
 530 | 		 *
 531 | 		 * **server:**
 532 | 		 * `auth.api.listTeamMembers`
 533 | 		 *
 534 | 		 * **client:**
 535 | 		 * `authClient.organization.listTeamMembers`
 536 | 		 *
 537 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-set-active-team)
 538 | 		 */
 539 | 		listTeamMembers: listTeamMembers(options as O),
 540 | 		/**
 541 | 		 * ### Endpoint
 542 | 		 *
 543 | 		 * POST `/organization/add-team-member`
 544 | 		 *
 545 | 		 * ### API Methods
 546 | 		 *
 547 | 		 * **server:**
 548 | 		 * `auth.api.addTeamMember`
 549 | 		 *
 550 | 		 * **client:**
 551 | 		 * `authClient.organization.addTeamMember`
 552 | 		 *
 553 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-add-team-member)
 554 | 		 */
 555 | 		addTeamMember: addTeamMember(options as O),
 556 | 		/**
 557 | 		 * ### Endpoint
 558 | 		 *
 559 | 		 * POST `/organization/remove-team-member`
 560 | 		 *
 561 | 		 * ### API Methods
 562 | 		 *
 563 | 		 * **server:**
 564 | 		 * `auth.api.removeTeamMember`
 565 | 		 *
 566 | 		 * **client:**
 567 | 		 * `authClient.organization.removeTeamMember`
 568 | 		 *
 569 | 		 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/organization#api-remove-team-member)
 570 | 		 */
 571 | 		removeTeamMember: removeTeamMember(options as O),
 572 | 	};
 573 | 	if (teamSupport) {
 574 | 		endpoints = {
 575 | 			...endpoints,
 576 | 			...teamEndpoints,
 577 | 		};
 578 | 	}
 579 | 
 580 | 	const dynamicAccessControlEndpoints = {
 581 | 		createOrgRole: createOrgRole(options as O),
 582 | 		deleteOrgRole: deleteOrgRole(options as O),
 583 | 		listOrgRoles: listOrgRoles(options as O),
 584 | 		getOrgRole: getOrgRole(options as O),
 585 | 		updateOrgRole: updateOrgRole(options as O),
 586 | 	};
 587 | 	if (options?.dynamicAccessControl?.enabled) {
 588 | 		endpoints = {
 589 | 			...endpoints,
 590 | 			...dynamicAccessControlEndpoints,
 591 | 		};
 592 | 	}
 593 | 	const roles = {
 594 | 		...defaultRoles,
 595 | 		...options?.roles,
 596 | 	};
 597 | 
 598 | 	// Build team schema in a way that never introduces undefined values when spreading
 599 | 	const teamSchema = teamSupport
 600 | 		? ({
 601 | 				team: {
 602 | 					modelName: options?.schema?.team?.modelName,
 603 | 					fields: {
 604 | 						name: {
 605 | 							type: "string",
 606 | 							required: true,
 607 | 							fieldName: options?.schema?.team?.fields?.name,
 608 | 						},
 609 | 						organizationId: {
 610 | 							type: "string",
 611 | 							required: true,
 612 | 							references: {
 613 | 								model: "organization",
 614 | 								field: "id",
 615 | 							},
 616 | 							fieldName: options?.schema?.team?.fields?.organizationId,
 617 | 						},
 618 | 						createdAt: {
 619 | 							type: "date",
 620 | 							required: true,
 621 | 							fieldName: options?.schema?.team?.fields?.createdAt,
 622 | 						},
 623 | 						updatedAt: {
 624 | 							type: "date",
 625 | 							required: false,
 626 | 							fieldName: options?.schema?.team?.fields?.updatedAt,
 627 | 							onUpdate: () => new Date(),
 628 | 						},
 629 | 						...(options?.schema?.team?.additionalFields || {}),
 630 | 					},
 631 | 				},
 632 | 				teamMember: {
 633 | 					modelName: options?.schema?.teamMember?.modelName,
 634 | 					fields: {
 635 | 						teamId: {
 636 | 							type: "string",
 637 | 							required: true,
 638 | 							references: {
 639 | 								model: "team",
 640 | 								field: "id",
 641 | 							},
 642 | 							fieldName: options?.schema?.teamMember?.fields?.teamId,
 643 | 						},
 644 | 						userId: {
 645 | 							type: "string",
 646 | 							required: true,
 647 | 							references: {
 648 | 								model: "user",
 649 | 								field: "id",
 650 | 							},
 651 | 							fieldName: options?.schema?.teamMember?.fields?.userId,
 652 | 						},
 653 | 						createdAt: {
 654 | 							type: "date",
 655 | 							required: false,
 656 | 							fieldName: options?.schema?.teamMember?.fields?.createdAt,
 657 | 						},
 658 | 					},
 659 | 				},
 660 | 			} satisfies BetterAuthPluginDBSchema)
 661 | 		: {};
 662 | 
 663 | 	const organizationRoleSchema = options?.dynamicAccessControl?.enabled
 664 | 		? ({
 665 | 				organizationRole: {
 666 | 					fields: {
 667 | 						organizationId: {
 668 | 							type: "string",
 669 | 							required: true,
 670 | 							references: {
 671 | 								model: "organization",
 672 | 								field: "id",
 673 | 							},
 674 | 							fieldName:
 675 | 								options?.schema?.organizationRole?.fields?.organizationId,
 676 | 						},
 677 | 						role: {
 678 | 							type: "string",
 679 | 							required: true,
 680 | 							fieldName: options?.schema?.organizationRole?.fields?.role,
 681 | 						},
 682 | 						permission: {
 683 | 							type: "string",
 684 | 							required: true,
 685 | 							fieldName: options?.schema?.organizationRole?.fields?.permission,
 686 | 						},
 687 | 						createdAt: {
 688 | 							type: "date",
 689 | 							required: true,
 690 | 							defaultValue: () => new Date(),
 691 | 							fieldName: options?.schema?.organizationRole?.fields?.createdAt,
 692 | 						},
 693 | 						updatedAt: {
 694 | 							type: "date",
 695 | 							required: false,
 696 | 							fieldName: options?.schema?.organizationRole?.fields?.updatedAt,
 697 | 							onUpdate: () => new Date(),
 698 | 						},
 699 | 						...(options?.schema?.organizationRole?.additionalFields || {}),
 700 | 					},
 701 | 					modelName: options?.schema?.organizationRole?.modelName,
 702 | 				},
 703 | 			} satisfies BetterAuthPluginDBSchema)
 704 | 		: {};
 705 | 
 706 | 	const schema = {
 707 | 		...organizationRoleSchema,
 708 | 		...teamSchema,
 709 | 		...({
 710 | 			organization: {
 711 | 				modelName: options?.schema?.organization?.modelName,
 712 | 				fields: {
 713 | 					name: {
 714 | 						type: "string",
 715 | 						required: true,
 716 | 						sortable: true,
 717 | 						fieldName: options?.schema?.organization?.fields?.name,
 718 | 					},
 719 | 					slug: {
 720 | 						type: "string",
 721 | 						required: true,
 722 | 						unique: true,
 723 | 						sortable: true,
 724 | 						fieldName: options?.schema?.organization?.fields?.slug,
 725 | 					},
 726 | 					logo: {
 727 | 						type: "string",
 728 | 						required: false,
 729 | 						fieldName: options?.schema?.organization?.fields?.logo,
 730 | 					},
 731 | 					createdAt: {
 732 | 						type: "date",
 733 | 						required: true,
 734 | 						fieldName: options?.schema?.organization?.fields?.createdAt,
 735 | 					},
 736 | 					metadata: {
 737 | 						type: "string",
 738 | 						required: false,
 739 | 						fieldName: options?.schema?.organization?.fields?.metadata,
 740 | 					},
 741 | 					...(options?.schema?.organization?.additionalFields || {}),
 742 | 				},
 743 | 			},
 744 | 			member: {
 745 | 				modelName: options?.schema?.member?.modelName,
 746 | 				fields: {
 747 | 					organizationId: {
 748 | 						type: "string",
 749 | 						required: true,
 750 | 						references: {
 751 | 							model: "organization",
 752 | 							field: "id",
 753 | 						},
 754 | 						fieldName: options?.schema?.member?.fields?.organizationId,
 755 | 					},
 756 | 					userId: {
 757 | 						type: "string",
 758 | 						required: true,
 759 | 						fieldName: options?.schema?.member?.fields?.userId,
 760 | 						references: {
 761 | 							model: "user",
 762 | 							field: "id",
 763 | 						},
 764 | 					},
 765 | 					role: {
 766 | 						type: "string",
 767 | 						required: true,
 768 | 						sortable: true,
 769 | 						defaultValue: "member",
 770 | 						fieldName: options?.schema?.member?.fields?.role,
 771 | 					},
 772 | 					createdAt: {
 773 | 						type: "date",
 774 | 						required: true,
 775 | 						fieldName: options?.schema?.member?.fields?.createdAt,
 776 | 					},
 777 | 					...(options?.schema?.member?.additionalFields || {}),
 778 | 				},
 779 | 			},
 780 | 			invitation: {
 781 | 				modelName: options?.schema?.invitation?.modelName,
 782 | 				fields: {
 783 | 					organizationId: {
 784 | 						type: "string",
 785 | 						required: true,
 786 | 						references: {
 787 | 							model: "organization",
 788 | 							field: "id",
 789 | 						},
 790 | 						fieldName: options?.schema?.invitation?.fields?.organizationId,
 791 | 					},
 792 | 					email: {
 793 | 						type: "string",
 794 | 						required: true,
 795 | 						sortable: true,
 796 | 						fieldName: options?.schema?.invitation?.fields?.email,
 797 | 					},
 798 | 					role: {
 799 | 						type: "string",
 800 | 						required: false,
 801 | 						sortable: true,
 802 | 						fieldName: options?.schema?.invitation?.fields?.role,
 803 | 					},
 804 | 					...(teamSupport
 805 | 						? {
 806 | 								teamId: {
 807 | 									type: "string",
 808 | 									required: false,
 809 | 									sortable: true,
 810 | 									fieldName: options?.schema?.invitation?.fields?.teamId,
 811 | 								},
 812 | 							}
 813 | 						: {}),
 814 | 					status: {
 815 | 						type: "string",
 816 | 						required: true,
 817 | 						sortable: true,
 818 | 						defaultValue: "pending",
 819 | 						fieldName: options?.schema?.invitation?.fields?.status,
 820 | 					},
 821 | 					expiresAt: {
 822 | 						type: "date",
 823 | 						required: true,
 824 | 						fieldName: options?.schema?.invitation?.fields?.expiresAt,
 825 | 					},
 826 | 					createdAt: {
 827 | 						type: "date",
 828 | 						required: true,
 829 | 						fieldName: options?.schema?.invitation?.fields?.createdAt,
 830 | 						defaultValue: () => new Date(),
 831 | 					},
 832 | 					inviterId: {
 833 | 						type: "string",
 834 | 						references: {
 835 | 							model: "user",
 836 | 							field: "id",
 837 | 						},
 838 | 						fieldName: options?.schema?.invitation?.fields?.inviterId,
 839 | 						required: true,
 840 | 					},
 841 | 					...(options?.schema?.invitation?.additionalFields || {}),
 842 | 				},
 843 | 			},
 844 | 		} satisfies BetterAuthPluginDBSchema),
 845 | 	};
 846 | 
 847 | 	/**
 848 | 	 * the orgMiddleware type-asserts an empty object representing org options, roles, and a getSession function.
 849 | 	 * This `shimContext` function is used to add those missing properties to the context object.
 850 | 	 */
 851 | 	const api = shimContext(endpoints, {
 852 | 		orgOptions: options || {},
 853 | 		roles,
 854 | 		getSession: async (context: AuthContext) => {
 855 | 			//@ts-expect-error
 856 | 			return await getSessionFromCtx(context);
 857 | 		},
 858 | 	});
 859 | 
 860 | 	type DefaultStatements = typeof defaultStatements;
 861 | 	type Statements = O["ac"] extends AccessControl<infer S>
 862 | 		? S
 863 | 		: DefaultStatements;
 864 | 	type PermissionType = {
 865 | 		[key in keyof Statements]?: Array<
 866 | 			Statements[key] extends readonly unknown[]
 867 | 				? Statements[key][number]
 868 | 				: never
 869 | 		>;
 870 | 	};
 871 | 	type PermissionExclusive =
 872 | 		| {
 873 | 				/**
 874 | 				 * @deprecated Use `permissions` instead
 875 | 				 */
 876 | 				permission: PermissionType;
 877 | 				permissions?: never;
 878 | 		  }
 879 | 		| {
 880 | 				permissions: PermissionType;
 881 | 				permission?: never;
 882 | 		  };
 883 | 
 884 | 	type IncludeTeamEndpoints<ExistingEndpoints extends Record<string, any>> =
 885 | 		O["teams"] extends { enabled: true }
 886 | 			? ExistingEndpoints & typeof teamEndpoints
 887 | 			: ExistingEndpoints;
 888 | 
 889 | 	type IncludeDynamicAccessControlEndpoints<
 890 | 		ExistingEndpoints extends Record<string, any>,
 891 | 	> = O["dynamicAccessControl"] extends { enabled: true }
 892 | 		? ExistingEndpoints & typeof dynamicAccessControlEndpoints
 893 | 		: ExistingEndpoints;
 894 | 
 895 | 	type AllEndpoints = IncludeDynamicAccessControlEndpoints<
 896 | 		IncludeTeamEndpoints<typeof endpoints>
 897 | 	>;
 898 | 
 899 | 	return {
 900 | 		id: "organization",
 901 | 		endpoints: {
 902 | 			...(api as AllEndpoints),
 903 | 			hasPermission: createAuthEndpoint(
 904 | 				"/organization/has-permission",
 905 | 				{
 906 | 					method: "POST",
 907 | 					requireHeaders: true,
 908 | 					body: z
 909 | 						.object({
 910 | 							organizationId: z.string().optional(),
 911 | 						})
 912 | 						.and(
 913 | 							z.union([
 914 | 								z.object({
 915 | 									permission: z.record(z.string(), z.array(z.string())),
 916 | 									permissions: z.undefined(),
 917 | 								}),
 918 | 								z.object({
 919 | 									permission: z.undefined(),
 920 | 									permissions: z.record(z.string(), z.array(z.string())),
 921 | 								}),
 922 | 							]),
 923 | 						),
 924 | 					use: [orgSessionMiddleware],
 925 | 					metadata: {
 926 | 						$Infer: {
 927 | 							body: {} as PermissionExclusive & {
 928 | 								organizationId?: string;
 929 | 							},
 930 | 						},
 931 | 						openapi: {
 932 | 							description: "Check if the user has permission",
 933 | 							requestBody: {
 934 | 								content: {
 935 | 									"application/json": {
 936 | 										schema: {
 937 | 											type: "object",
 938 | 											properties: {
 939 | 												permission: {
 940 | 													type: "object",
 941 | 													description: "The permission to check",
 942 | 													deprecated: true,
 943 | 												},
 944 | 												permissions: {
 945 | 													type: "object",
 946 | 													description: "The permission to check",
 947 | 												},
 948 | 											},
 949 | 											required: ["permissions"],
 950 | 										},
 951 | 									},
 952 | 								},
 953 | 							},
 954 | 							responses: {
 955 | 								"200": {
 956 | 									description: "Success",
 957 | 									content: {
 958 | 										"application/json": {
 959 | 											schema: {
 960 | 												type: "object",
 961 | 												properties: {
 962 | 													error: {
 963 | 														type: "string",
 964 | 													},
 965 | 													success: {
 966 | 														type: "boolean",
 967 | 													},
 968 | 												},
 969 | 												required: ["success"],
 970 | 											},
 971 | 										},
 972 | 									},
 973 | 								},
 974 | 							},
 975 | 						},
 976 | 					},
 977 | 				},
 978 | 				async (ctx) => {
 979 | 					const activeOrganizationId =
 980 | 						ctx.body.organizationId ||
 981 | 						ctx.context.session.session.activeOrganizationId;
 982 | 					if (!activeOrganizationId) {
 983 | 						throw new APIError("BAD_REQUEST", {
 984 | 							message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
 985 | 						});
 986 | 					}
 987 | 					const adapter = getOrgAdapter<O>(ctx.context, options);
 988 | 					const member = await adapter.findMemberByOrgId({
 989 | 						userId: ctx.context.session.user.id,
 990 | 						organizationId: activeOrganizationId,
 991 | 					});
 992 | 					if (!member) {
 993 | 						throw new APIError("UNAUTHORIZED", {
 994 | 							message:
 995 | 								ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION,
 996 | 						});
 997 | 					}
 998 | 					const result = await hasPermission(
 999 | 						{
1000 | 							role: member.role,
1001 | 							options: options || {},
1002 | 							permissions: (ctx.body.permissions ?? ctx.body.permission) as any,
1003 | 							organizationId: activeOrganizationId,
1004 | 						},
1005 | 						ctx,
1006 | 					);
1007 | 
1008 | 					return ctx.json({
1009 | 						error: null,
1010 | 						success: result,
1011 | 					});
1012 | 				},
1013 | 			),
1014 | 		},
1015 | 		schema: {
1016 | 			...(schema as BetterAuthPluginDBSchema),
1017 | 			session: {
1018 | 				fields: {
1019 | 					activeOrganizationId: {
1020 | 						type: "string",
1021 | 						required: false,
1022 | 						fieldName: options?.schema?.session?.fields?.activeOrganizationId,
1023 | 					},
1024 | 					...(teamSupport
1025 | 						? {
1026 | 								activeTeamId: {
1027 | 									type: "string",
1028 | 									required: false,
1029 | 									fieldName: options?.schema?.session?.fields?.activeTeamId,
1030 | 								},
1031 | 							}
1032 | 						: {}),
1033 | 				} as unknown as O["teams"] extends {
1034 | 					enabled: true;
1035 | 				}
1036 | 					? {
1037 | 							activeTeamId: {
1038 | 								type: "string";
1039 | 								required: false;
1040 | 							};
1041 | 							activeOrganizationId: {
1042 | 								type: "string";
1043 | 								required: false;
1044 | 							};
1045 | 						}
1046 | 					: {
1047 | 							activeOrganizationId: {
1048 | 								type: "string";
1049 | 								required: false;
1050 | 							};
1051 | 						},
1052 | 			},
1053 | 		},
1054 | 		$Infer: {
1055 | 			Organization: {} as InferOrganization<O>,
1056 | 			Invitation: {} as InferInvitation<O>,
1057 | 			Member: {} as InferMember<O>,
1058 | 			Team: teamSupport ? ({} as Team) : ({} as any),
1059 | 			TeamMember: teamSupport ? ({} as TeamMember) : ({} as any),
1060 | 			ActiveOrganization: {} as Awaited<
1061 | 				ReturnType<ReturnType<typeof getFullOrganization<O>>>
1062 | 			>,
1063 | 		},
1064 | 		$ERROR_CODES: ORGANIZATION_ERROR_CODES,
1065 | 		options: options as O,
1066 | 	} satisfies BetterAuthPlugin;
1067 | };
1068 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/generic-oauth/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { betterFetch } from "@better-fetch/fetch";
  2 | import { APIError } from "better-call";
  3 | import { decodeJwt } from "jose";
  4 | import * as z from "zod";
  5 | import { createAuthEndpoint } from "@better-auth/core/middleware";
  6 | import { setSessionCookie } from "../../cookies";
  7 | import { BASE_ERROR_CODES } from "@better-auth/core/error";
  8 | import {
  9 | 	createAuthorizationURL,
 10 | 	validateAuthorizationCode,
 11 | } from "@better-auth/core/oauth2";
 12 | import type {
 13 | 	OAuth2Tokens,
 14 | 	OAuth2UserInfo,
 15 | 	OAuthProvider,
 16 | } from "@better-auth/core/oauth2";
 17 | import { handleOAuthUserInfo } from "../../oauth2/link-account";
 18 | import { refreshAccessToken } from "@better-auth/core/oauth2";
 19 | import { generateState, parseState } from "../../oauth2/state";
 20 | import type { User } from "../../types";
 21 | import type { BetterAuthPlugin } from "@better-auth/core";
 22 | import type { GenericEndpointContext } from "@better-auth/core";
 23 | import { sessionMiddleware } from "../../api";
 24 | import { defineErrorCodes } from "@better-auth/core/utils";
 25 | 
 26 | /**
 27 |  * Configuration interface for generic OAuth providers.
 28 |  */
 29 | export interface GenericOAuthConfig {
 30 | 	/** Unique identifier for the OAuth provider */
 31 | 	providerId: string;
 32 | 	/**
 33 | 	 * URL to fetch OAuth 2.0 configuration.
 34 | 	 * If provided, the authorization and token endpoints will be fetched from this URL.
 35 | 	 */
 36 | 	discoveryUrl?: string;
 37 | 	/**
 38 | 	 * URL for the authorization endpoint.
 39 | 	 * Optional if using discoveryUrl.
 40 | 	 */
 41 | 	authorizationUrl?: string;
 42 | 	/**
 43 | 	 * URL for the token endpoint.
 44 | 	 * Optional if using discoveryUrl.
 45 | 	 */
 46 | 	tokenUrl?: string;
 47 | 	/**
 48 | 	 * URL for the user info endpoint.
 49 | 	 * Optional if using discoveryUrl.
 50 | 	 */
 51 | 	userInfoUrl?: string;
 52 | 	/** OAuth client ID */
 53 | 	clientId: string;
 54 | 	/** OAuth client secret */
 55 | 	clientSecret?: string;
 56 | 	/**
 57 | 	 * Array of OAuth scopes to request.
 58 | 	 * @default []
 59 | 	 */
 60 | 	scopes?: string[];
 61 | 	/**
 62 | 	 * Custom redirect URI.
 63 | 	 * If not provided, a default URI will be constructed.
 64 | 	 */
 65 | 	redirectURI?: string;
 66 | 	/**
 67 | 	 * OAuth response type.
 68 | 	 * @default "code"
 69 | 	 */
 70 | 	responseType?: string;
 71 | 	/**
 72 | 	 * The response mode to use for the authorization code request.
 73 | 
 74 | 	 */
 75 | 	responseMode?: "query" | "form_post";
 76 | 	/**
 77 | 	 * Prompt parameter for the authorization request.
 78 | 	 * Controls the authentication experience for the user.
 79 | 	 */
 80 | 	prompt?: "none" | "login" | "consent" | "select_account";
 81 | 	/**
 82 | 	 * Whether to use PKCE (Proof Key for Code Exchange)
 83 | 	 * @default false
 84 | 	 */
 85 | 	pkce?: boolean;
 86 | 	/**
 87 | 	 * Access type for the authorization request.
 88 | 	 * Use "offline" to request a refresh token.
 89 | 	 */
 90 | 	accessType?: string;
 91 | 	/**
 92 | 	 * Custom function to fetch user info.
 93 | 	 * If provided, this function will be used instead of the default user info fetching logic.
 94 | 	 * @param tokens - The OAuth tokens received after successful authentication
 95 | 	 * @returns A promise that resolves to a User object or null
 96 | 	 */
 97 | 	getUserInfo?: (tokens: OAuth2Tokens) => Promise<OAuth2UserInfo | null>;
 98 | 	/**
 99 | 	 * Custom function to map the user profile to a User object.
100 | 	 */
101 | 	mapProfileToUser?: (
102 | 		profile: Record<string, any>,
103 | 	) => Partial<Partial<User>> | Promise<Partial<User>>;
104 | 	/**
105 | 	 * Additional search-params to add to the authorizationUrl.
106 | 	 * Warning: Search-params added here overwrite any default params.
107 | 	 */
108 | 	authorizationUrlParams?:
109 | 		| Record<string, string>
110 | 		| ((ctx: GenericEndpointContext) => Record<string, string>);
111 | 	/**
112 | 	 * Additional search-params to add to the tokenUrl.
113 | 	 * Warning: Search-params added here overwrite any default params.
114 | 	 */
115 | 	tokenUrlParams?:
116 | 		| Record<string, string>
117 | 		| ((ctx: GenericEndpointContext) => Record<string, string>);
118 | 	/**
119 | 	 * Disable implicit sign up for new users. When set to true for the provider,
120 | 	 * sign-in need to be called with with requestSignUp as true to create new users.
121 | 	 */
122 | 	disableImplicitSignUp?: boolean;
123 | 	/**
124 | 	 * Disable sign up for new users.
125 | 	 */
126 | 	disableSignUp?: boolean;
127 | 	/**
128 | 	 * Authentication method for token requests.
129 | 	 * @default "post"
130 | 	 */
131 | 	authentication?: "basic" | "post";
132 | 	/**
133 | 	 * Custom headers to include in the discovery request.
134 | 	 * Useful for providers like Epic that require specific headers (e.g., Epic-Client-ID).
135 | 	 */
136 | 	discoveryHeaders?: Record<string, string>;
137 | 	/**
138 | 	 * Custom headers to include in the authorization request.
139 | 	 * Useful for providers like Qonto that require specific headers (e.g., X-Qonto-Staging-Token for local development).
140 | 	 */
141 | 	authorizationHeaders?: Record<string, string>;
142 | 	/**
143 | 	 * Override user info with the provider info.
144 | 	 *
145 | 	 * This will update the user info with the provider info,
146 | 	 * when the user signs in with the provider.
147 | 	 * @default false
148 | 	 */
149 | 	overrideUserInfo?: boolean;
150 | }
151 | 
152 | interface GenericOAuthOptions {
153 | 	/**
154 | 	 * Array of OAuth provider configurations.
155 | 	 */
156 | 	config: GenericOAuthConfig[];
157 | }
158 | 
159 | async function getUserInfo(
160 | 	tokens: OAuth2Tokens,
161 | 	finalUserInfoUrl: string | undefined,
162 | ): Promise<OAuth2UserInfo | null> {
163 | 	if (tokens.idToken) {
164 | 		const decoded = decodeJwt(tokens.idToken) as {
165 | 			sub: string;
166 | 			email_verified: boolean;
167 | 			email: string;
168 | 			name: string;
169 | 			picture: string;
170 | 		};
171 | 		if (decoded) {
172 | 			if (decoded.sub && decoded.email) {
173 | 				return {
174 | 					id: decoded.sub,
175 | 					emailVerified: decoded.email_verified,
176 | 					image: decoded.picture,
177 | 					...decoded,
178 | 				};
179 | 			}
180 | 		}
181 | 	}
182 | 
183 | 	if (!finalUserInfoUrl) {
184 | 		return null;
185 | 	}
186 | 
187 | 	const userInfo = await betterFetch<{
188 | 		email: string;
189 | 		sub?: string;
190 | 		name: string;
191 | 		email_verified: boolean;
192 | 		picture: string;
193 | 	}>(finalUserInfoUrl, {
194 | 		method: "GET",
195 | 		headers: {
196 | 			Authorization: `Bearer ${tokens.accessToken}`,
197 | 		},
198 | 	});
199 | 	return {
200 | 		// @ts-expect-error sub is optional in the type
201 | 		id: userInfo.data?.sub,
202 | 		emailVerified: userInfo.data?.email_verified ?? false,
203 | 		email: userInfo.data?.email,
204 | 		image: userInfo.data?.picture,
205 | 		name: userInfo.data?.name,
206 | 		...userInfo.data,
207 | 	};
208 | }
209 | 
210 | const ERROR_CODES = defineErrorCodes({
211 | 	INVALID_OAUTH_CONFIGURATION: "Invalid OAuth configuration",
212 | });
213 | 
214 | /**
215 |  * A generic OAuth plugin that can be used to add OAuth support to any provider
216 |  */
217 | export const genericOAuth = (options: GenericOAuthOptions) => {
218 | 	return {
219 | 		id: "generic-oauth",
220 | 		init: (ctx) => {
221 | 			const genericProviders = options.config.map((c) => {
222 | 				let finalUserInfoUrl = c.userInfoUrl;
223 | 				return {
224 | 					id: c.providerId,
225 | 					name: c.providerId,
226 | 					createAuthorizationURL(data) {
227 | 						return createAuthorizationURL({
228 | 							id: c.providerId,
229 | 							options: {
230 | 								clientId: c.clientId,
231 | 								clientSecret: c.clientSecret,
232 | 								redirectURI: c.redirectURI,
233 | 							},
234 | 							authorizationEndpoint: c.authorizationUrl!,
235 | 							state: data.state,
236 | 							codeVerifier: c.pkce ? data.codeVerifier : undefined,
237 | 							scopes: c.scopes || [],
238 | 							redirectURI: `${ctx.baseURL}/oauth2/callback/${c.providerId}`,
239 | 						});
240 | 					},
241 | 					async validateAuthorizationCode(data) {
242 | 						let finalTokenUrl = c.tokenUrl;
243 | 						if (c.discoveryUrl) {
244 | 							const discovery = await betterFetch<{
245 | 								token_endpoint: string;
246 | 								userinfo_endpoint: string;
247 | 							}>(c.discoveryUrl, {
248 | 								method: "GET",
249 | 								headers: c.discoveryHeaders,
250 | 							});
251 | 							if (discovery.data) {
252 | 								finalTokenUrl = discovery.data.token_endpoint;
253 | 								finalUserInfoUrl = discovery.data.userinfo_endpoint;
254 | 							}
255 | 						}
256 | 						if (!finalTokenUrl) {
257 | 							throw new APIError("BAD_REQUEST", {
258 | 								message: "Invalid OAuth configuration. Token URL not found.",
259 | 							});
260 | 						}
261 | 						return validateAuthorizationCode({
262 | 							headers: c.authorizationHeaders,
263 | 							code: data.code,
264 | 							codeVerifier: data.codeVerifier,
265 | 							redirectURI: data.redirectURI,
266 | 							options: {
267 | 								clientId: c.clientId,
268 | 								clientSecret: c.clientSecret,
269 | 								redirectURI: c.redirectURI,
270 | 							},
271 | 							tokenEndpoint: finalTokenUrl,
272 | 							authentication: c.authentication,
273 | 						});
274 | 					},
275 | 					async refreshAccessToken(
276 | 						refreshToken: string,
277 | 					): Promise<OAuth2Tokens> {
278 | 						let finalTokenUrl = c.tokenUrl;
279 | 						if (c.discoveryUrl) {
280 | 							const discovery = await betterFetch<{
281 | 								token_endpoint: string;
282 | 							}>(c.discoveryUrl, {
283 | 								method: "GET",
284 | 								headers: c.discoveryHeaders,
285 | 							});
286 | 							if (discovery.data) {
287 | 								finalTokenUrl = discovery.data.token_endpoint;
288 | 							}
289 | 						}
290 | 						if (!finalTokenUrl) {
291 | 							throw new APIError("BAD_REQUEST", {
292 | 								message: "Invalid OAuth configuration. Token URL not found.",
293 | 							});
294 | 						}
295 | 						return refreshAccessToken({
296 | 							refreshToken,
297 | 							options: {
298 | 								clientId: c.clientId,
299 | 								clientSecret: c.clientSecret,
300 | 							},
301 | 							authentication: c.authentication,
302 | 							tokenEndpoint: finalTokenUrl,
303 | 						});
304 | 					},
305 | 
306 | 					async getUserInfo(tokens) {
307 | 						const userInfo = c.getUserInfo
308 | 							? await c.getUserInfo(tokens)
309 | 							: await getUserInfo(tokens, finalUserInfoUrl);
310 | 						if (!userInfo) {
311 | 							return null;
312 | 						}
313 | 						return {
314 | 							user: {
315 | 								id: userInfo?.id,
316 | 								email: userInfo?.email,
317 | 								emailVerified: userInfo?.emailVerified,
318 | 								image: userInfo?.image,
319 | 								name: userInfo?.name,
320 | 								...c.mapProfileToUser?.(userInfo),
321 | 							},
322 | 							data: userInfo,
323 | 						};
324 | 					},
325 | 				} as OAuthProvider;
326 | 			});
327 | 			return {
328 | 				context: {
329 | 					socialProviders: genericProviders.concat(ctx.socialProviders),
330 | 				},
331 | 			};
332 | 		},
333 | 		endpoints: {
334 | 			/**
335 | 			 * ### Endpoint
336 | 			 *
337 | 			 * POST `/sign-in/oauth2`
338 | 			 *
339 | 			 * ### API Methods
340 | 			 *
341 | 			 * **server:**
342 | 			 * `auth.api.signInWithOAuth2`
343 | 			 *
344 | 			 * **client:**
345 | 			 * `authClient.signIn.oauth2`
346 | 			 *
347 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/sign-in#api-method-sign-in-oauth2)
348 | 			 */
349 | 			signInWithOAuth2: createAuthEndpoint(
350 | 				"/sign-in/oauth2",
351 | 				{
352 | 					method: "POST",
353 | 					body: z.object({
354 | 						providerId: z.string().meta({
355 | 							description: "The provider ID for the OAuth provider",
356 | 						}),
357 | 						callbackURL: z
358 | 							.string()
359 | 							.meta({
360 | 								description: "The URL to redirect to after sign in",
361 | 							})
362 | 							.optional(),
363 | 						errorCallbackURL: z
364 | 							.string()
365 | 							.meta({
366 | 								description: "The URL to redirect to if an error occurs",
367 | 							})
368 | 							.optional(),
369 | 						newUserCallbackURL: z
370 | 							.string()
371 | 							.meta({
372 | 								description:
373 | 									'The URL to redirect to after login if the user is new. Eg: "/welcome"',
374 | 							})
375 | 							.optional(),
376 | 						disableRedirect: z
377 | 							.boolean()
378 | 							.meta({
379 | 								description: "Disable redirect",
380 | 							})
381 | 							.optional(),
382 | 						scopes: z
383 | 							.array(z.string())
384 | 							.meta({
385 | 								description:
386 | 									"Scopes to be passed to the provider authorization request.",
387 | 							})
388 | 							.optional(),
389 | 						requestSignUp: z
390 | 							.boolean()
391 | 							.meta({
392 | 								description:
393 | 									"Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider. Eg: false",
394 | 							})
395 | 							.optional(),
396 | 					}),
397 | 					metadata: {
398 | 						openapi: {
399 | 							description: "Sign in with OAuth2",
400 | 							responses: {
401 | 								200: {
402 | 									description: "Sign in with OAuth2",
403 | 									content: {
404 | 										"application/json": {
405 | 											schema: {
406 | 												type: "object",
407 | 												properties: {
408 | 													url: {
409 | 														type: "string",
410 | 													},
411 | 													redirect: {
412 | 														type: "boolean",
413 | 													},
414 | 												},
415 | 											},
416 | 										},
417 | 									},
418 | 								},
419 | 							},
420 | 						},
421 | 					},
422 | 				},
423 | 				async (ctx) => {
424 | 					const { providerId } = ctx.body;
425 | 					const config = options.config.find(
426 | 						(c) => c.providerId === providerId,
427 | 					);
428 | 					if (!config) {
429 | 						throw new APIError("BAD_REQUEST", {
430 | 							message: `No config found for provider ${providerId}`,
431 | 						});
432 | 					}
433 | 					const {
434 | 						discoveryUrl,
435 | 						authorizationUrl,
436 | 						tokenUrl,
437 | 						clientId,
438 | 						clientSecret,
439 | 						scopes,
440 | 						redirectURI,
441 | 						responseType,
442 | 						pkce,
443 | 						prompt,
444 | 						accessType,
445 | 						authorizationUrlParams,
446 | 						responseMode,
447 | 						authentication,
448 | 					} = config;
449 | 					let finalAuthUrl = authorizationUrl;
450 | 					let finalTokenUrl = tokenUrl;
451 | 					if (discoveryUrl) {
452 | 						const discovery = await betterFetch<{
453 | 							authorization_endpoint: string;
454 | 							token_endpoint: string;
455 | 						}>(discoveryUrl, {
456 | 							method: "GET",
457 | 							headers: config.discoveryHeaders,
458 | 							onError(context) {
459 | 								ctx.context.logger.error(context.error.message, context.error, {
460 | 									discoveryUrl,
461 | 								});
462 | 							},
463 | 						});
464 | 						if (discovery.data) {
465 | 							finalAuthUrl = discovery.data.authorization_endpoint;
466 | 							finalTokenUrl = discovery.data.token_endpoint;
467 | 						}
468 | 					}
469 | 					if (!finalAuthUrl || !finalTokenUrl) {
470 | 						throw new APIError("BAD_REQUEST", {
471 | 							message: ERROR_CODES.INVALID_OAUTH_CONFIGURATION,
472 | 						});
473 | 					}
474 | 					if (authorizationUrlParams) {
475 | 						const withAdditionalParams = new URL(finalAuthUrl);
476 | 						for (const [paramName, paramValue] of Object.entries(
477 | 							authorizationUrlParams,
478 | 						)) {
479 | 							withAdditionalParams.searchParams.set(paramName, paramValue);
480 | 						}
481 | 						finalAuthUrl = withAdditionalParams.toString();
482 | 					}
483 | 					const additionalParams =
484 | 						typeof authorizationUrlParams === "function"
485 | 							? authorizationUrlParams(ctx)
486 | 							: authorizationUrlParams;
487 | 
488 | 					const { state, codeVerifier } = await generateState(ctx);
489 | 					const authUrl = await createAuthorizationURL({
490 | 						id: providerId,
491 | 						options: {
492 | 							clientId,
493 | 							clientSecret,
494 | 							redirectURI,
495 | 						},
496 | 						authorizationEndpoint: finalAuthUrl,
497 | 						state,
498 | 						codeVerifier: pkce ? codeVerifier : undefined,
499 | 						scopes: ctx.body.scopes
500 | 							? [...ctx.body.scopes, ...(scopes || [])]
501 | 							: scopes || [],
502 | 						redirectURI: `${ctx.context.baseURL}/oauth2/callback/${providerId}`,
503 | 						prompt,
504 | 						accessType,
505 | 						responseType,
506 | 						responseMode,
507 | 						additionalParams,
508 | 					});
509 | 					return ctx.json({
510 | 						url: authUrl.toString(),
511 | 						redirect: !ctx.body.disableRedirect,
512 | 					});
513 | 				},
514 | 			),
515 | 			oAuth2Callback: createAuthEndpoint(
516 | 				"/oauth2/callback/:providerId",
517 | 				{
518 | 					method: "GET",
519 | 					query: z.object({
520 | 						code: z
521 | 							.string()
522 | 							.meta({
523 | 								description: "The OAuth2 code",
524 | 							})
525 | 							.optional(),
526 | 						error: z
527 | 							.string()
528 | 							.meta({
529 | 								description: "The error message, if any",
530 | 							})
531 | 							.optional(),
532 | 						error_description: z
533 | 							.string()
534 | 							.meta({
535 | 								description: "The error description, if any",
536 | 							})
537 | 							.optional(),
538 | 						state: z
539 | 							.string()
540 | 							.meta({
541 | 								description: "The state parameter from the OAuth2 request",
542 | 							})
543 | 							.optional(),
544 | 					}),
545 | 					metadata: {
546 | 						client: false,
547 | 						openapi: {
548 | 							description: "OAuth2 callback",
549 | 							responses: {
550 | 								200: {
551 | 									description: "OAuth2 callback",
552 | 									content: {
553 | 										"application/json": {
554 | 											schema: {
555 | 												type: "object",
556 | 												properties: {
557 | 													url: {
558 | 														type: "string",
559 | 													},
560 | 												},
561 | 											},
562 | 										},
563 | 									},
564 | 								},
565 | 							},
566 | 						},
567 | 					},
568 | 				},
569 | 				async (ctx) => {
570 | 					const defaultErrorURL =
571 | 						ctx.context.options.onAPIError?.errorURL ||
572 | 						`${ctx.context.baseURL}/error`;
573 | 					if (ctx.query.error || !ctx.query.code) {
574 | 						throw ctx.redirect(
575 | 							`${defaultErrorURL}?error=${
576 | 								ctx.query.error || "oAuth_code_missing"
577 | 							}&error_description=${ctx.query.error_description}`,
578 | 						);
579 | 					}
580 | 					const provider = options.config.find(
581 | 						(p) => p.providerId === ctx.params.providerId,
582 | 					);
583 | 
584 | 					if (!provider) {
585 | 						throw new APIError("BAD_REQUEST", {
586 | 							message: `No config found for provider ${ctx.params.providerId}`,
587 | 						});
588 | 					}
589 | 					let tokens: OAuth2Tokens | undefined = undefined;
590 | 					const parsedState = await parseState(ctx);
591 | 
592 | 					const {
593 | 						callbackURL,
594 | 						codeVerifier,
595 | 						errorURL,
596 | 						requestSignUp,
597 | 						newUserURL,
598 | 						link,
599 | 					} = parsedState;
600 | 					const code = ctx.query.code;
601 | 
602 | 					function redirectOnError(error: string) {
603 | 						const defaultErrorURL =
604 | 							ctx.context.options.onAPIError?.errorURL ||
605 | 							`${ctx.context.baseURL}/error`;
606 | 						let url = errorURL || defaultErrorURL;
607 | 						if (url.includes("?")) {
608 | 							url = `${url}&error=${error}`;
609 | 						} else {
610 | 							url = `${url}?error=${error}`;
611 | 						}
612 | 						throw ctx.redirect(url);
613 | 					}
614 | 
615 | 					let finalTokenUrl = provider.tokenUrl;
616 | 					let finalUserInfoUrl = provider.userInfoUrl;
617 | 					if (provider.discoveryUrl) {
618 | 						const discovery = await betterFetch<{
619 | 							token_endpoint: string;
620 | 							userinfo_endpoint: string;
621 | 						}>(provider.discoveryUrl, {
622 | 							method: "GET",
623 | 							headers: provider.discoveryHeaders,
624 | 						});
625 | 						if (discovery.data) {
626 | 							finalTokenUrl = discovery.data.token_endpoint;
627 | 							finalUserInfoUrl = discovery.data.userinfo_endpoint;
628 | 						}
629 | 					}
630 | 					try {
631 | 						if (!finalTokenUrl) {
632 | 							throw new APIError("BAD_REQUEST", {
633 | 								message: "Invalid OAuth configuration.",
634 | 							});
635 | 						}
636 | 						const additionalParams =
637 | 							typeof provider.tokenUrlParams === "function"
638 | 								? provider.tokenUrlParams(ctx)
639 | 								: provider.tokenUrlParams;
640 | 						tokens = await validateAuthorizationCode({
641 | 							headers: provider.authorizationHeaders,
642 | 							code,
643 | 							codeVerifier: provider.pkce ? codeVerifier : undefined,
644 | 							redirectURI: `${ctx.context.baseURL}/oauth2/callback/${provider.providerId}`,
645 | 							options: {
646 | 								clientId: provider.clientId,
647 | 								clientSecret: provider.clientSecret,
648 | 								redirectURI: provider.redirectURI,
649 | 							},
650 | 							tokenEndpoint: finalTokenUrl,
651 | 							authentication: provider.authentication,
652 | 							additionalParams,
653 | 						});
654 | 					} catch (e) {
655 | 						ctx.context.logger.error(
656 | 							e && typeof e === "object" && "name" in e
657 | 								? (e.name as string)
658 | 								: "",
659 | 							e,
660 | 						);
661 | 						throw redirectOnError("oauth_code_verification_failed");
662 | 					}
663 | 
664 | 					if (!tokens) {
665 | 						throw new APIError("BAD_REQUEST", {
666 | 							message: "Invalid OAuth configuration.",
667 | 						});
668 | 					}
669 | 					const userInfo: Omit<User, "createdAt" | "updatedAt"> =
670 | 						await (async function handleUserInfo() {
671 | 							const userInfo = (
672 | 								provider.getUserInfo
673 | 									? await provider.getUserInfo(tokens)
674 | 									: await getUserInfo(tokens, finalUserInfoUrl)
675 | 							) as OAuth2UserInfo | null;
676 | 							if (!userInfo) {
677 | 								throw redirectOnError("user_info_is_missing");
678 | 							}
679 | 							const mapUser = provider.mapProfileToUser
680 | 								? await provider.mapProfileToUser(userInfo)
681 | 								: userInfo;
682 | 							const email = mapUser.email
683 | 								? mapUser.email.toLowerCase()
684 | 								: userInfo.email?.toLowerCase();
685 | 							if (!email) {
686 | 								ctx.context.logger.error("Unable to get user info", userInfo);
687 | 								throw redirectOnError("email_is_missing");
688 | 							}
689 | 							const id = mapUser.id ? String(mapUser.id) : String(userInfo.id);
690 | 							const name = mapUser.name ? mapUser.name : userInfo.name;
691 | 							if (!name) {
692 | 								ctx.context.logger.error("Unable to get user info", userInfo);
693 | 								throw redirectOnError("name_is_missing");
694 | 							}
695 | 							return {
696 | 								...userInfo,
697 | 								...mapUser,
698 | 								email,
699 | 								id,
700 | 								name,
701 | 							};
702 | 						})();
703 | 					if (link) {
704 | 						if (
705 | 							ctx.context.options.account?.accountLinking
706 | 								?.allowDifferentEmails !== true &&
707 | 							link.email !== userInfo.email
708 | 						) {
709 | 							return redirectOnError("email_doesn't_match");
710 | 						}
711 | 						const existingAccount =
712 | 							await ctx.context.internalAdapter.findAccountByProviderId(
713 | 								String(userInfo.id),
714 | 								provider.providerId,
715 | 							);
716 | 						if (existingAccount) {
717 | 							if (existingAccount.userId !== link.userId) {
718 | 								return redirectOnError(
719 | 									"account_already_linked_to_different_user",
720 | 								);
721 | 							}
722 | 							const updateData = Object.fromEntries(
723 | 								Object.entries({
724 | 									accessToken: tokens.accessToken,
725 | 									idToken: tokens.idToken,
726 | 									refreshToken: tokens.refreshToken,
727 | 									accessTokenExpiresAt: tokens.accessTokenExpiresAt,
728 | 									refreshTokenExpiresAt: tokens.refreshTokenExpiresAt,
729 | 									scope: tokens.scopes?.join(","),
730 | 								}).filter(([_, value]) => value !== undefined),
731 | 							);
732 | 							await ctx.context.internalAdapter.updateAccount(
733 | 								existingAccount.id,
734 | 								updateData,
735 | 							);
736 | 						} else {
737 | 							const newAccount =
738 | 								await ctx.context.internalAdapter.createAccount({
739 | 									userId: link.userId,
740 | 									providerId: provider.providerId,
741 | 									accountId: userInfo.id,
742 | 									accessToken: tokens.accessToken,
743 | 									accessTokenExpiresAt: tokens.accessTokenExpiresAt,
744 | 									refreshTokenExpiresAt: tokens.refreshTokenExpiresAt,
745 | 									scope: tokens.scopes?.join(","),
746 | 									refreshToken: tokens.refreshToken,
747 | 									idToken: tokens.idToken,
748 | 								});
749 | 							if (!newAccount) {
750 | 								return redirectOnError("unable_to_link_account");
751 | 							}
752 | 						}
753 | 						let toRedirectTo: string;
754 | 						try {
755 | 							const url = callbackURL;
756 | 							toRedirectTo = url.toString();
757 | 						} catch {
758 | 							toRedirectTo = callbackURL;
759 | 						}
760 | 						throw ctx.redirect(toRedirectTo);
761 | 					}
762 | 
763 | 					const result = await handleOAuthUserInfo(ctx, {
764 | 						userInfo,
765 | 						account: {
766 | 							providerId: provider.providerId,
767 | 							accountId: userInfo.id,
768 | 							...tokens,
769 | 							scope: tokens.scopes?.join(","),
770 | 						},
771 | 						callbackURL: callbackURL,
772 | 						disableSignUp:
773 | 							(provider.disableImplicitSignUp && !requestSignUp) ||
774 | 							provider.disableSignUp,
775 | 						overrideUserInfo: provider.overrideUserInfo,
776 | 					});
777 | 
778 | 					if (result.error) {
779 | 						return redirectOnError(result.error.split(" ").join("_"));
780 | 					}
781 | 					const { session, user } = result.data!;
782 | 					await setSessionCookie(ctx, {
783 | 						session,
784 | 						user,
785 | 					});
786 | 					let toRedirectTo: string;
787 | 					try {
788 | 						const url = result.isRegister
789 | 							? newUserURL || callbackURL
790 | 							: callbackURL;
791 | 						toRedirectTo = url.toString();
792 | 					} catch {
793 | 						toRedirectTo = result.isRegister
794 | 							? newUserURL || callbackURL
795 | 							: callbackURL;
796 | 					}
797 | 					throw ctx.redirect(toRedirectTo);
798 | 				},
799 | 			),
800 | 			/**
801 | 			 * ### Endpoint
802 | 			 *
803 | 			 * POST `/oauth2/link`
804 | 			 *
805 | 			 * ### API Methods
806 | 			 *
807 | 			 * **server:**
808 | 			 * `auth.api.oAuth2LinkAccount`
809 | 			 *
810 | 			 * **client:**
811 | 			 * `authClient.oauth2.link`
812 | 			 *
813 | 			 * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/generic-oauth#api-method-oauth2-link)
814 | 			 */
815 | 			oAuth2LinkAccount: createAuthEndpoint(
816 | 				"/oauth2/link",
817 | 				{
818 | 					method: "POST",
819 | 					body: z.object({
820 | 						providerId: z.string(),
821 | 						/**
822 | 						 * Callback URL to redirect to after the user has signed in.
823 | 						 */
824 | 						callbackURL: z.string(),
825 | 						/**
826 | 						 * Additional scopes to request when linking the account.
827 | 						 * This is useful for requesting additional permissions when
828 | 						 * linking a social account compared to the initial authentication.
829 | 						 */
830 | 						scopes: z
831 | 							.array(z.string())
832 | 							.meta({
833 | 								description:
834 | 									"Additional scopes to request when linking the account",
835 | 							})
836 | 							.optional(),
837 | 						/**
838 | 						 * The URL to redirect to if there is an error during the link process.
839 | 						 */
840 | 						errorCallbackURL: z
841 | 							.string()
842 | 							.meta({
843 | 								description:
844 | 									"The URL to redirect to if there is an error during the link process",
845 | 							})
846 | 							.optional(),
847 | 					}),
848 | 					use: [sessionMiddleware],
849 | 					metadata: {
850 | 						openapi: {
851 | 							description: "Link an OAuth2 account to the current user session",
852 | 							responses: {
853 | 								"200": {
854 | 									description:
855 | 										"Authorization URL generated successfully for linking an OAuth2 account",
856 | 									content: {
857 | 										"application/json": {
858 | 											schema: {
859 | 												type: "object",
860 | 												properties: {
861 | 													url: {
862 | 														type: "string",
863 | 														format: "uri",
864 | 														description:
865 | 															"The authorization URL to redirect the user to for linking the OAuth2 account",
866 | 													},
867 | 													redirect: {
868 | 														type: "boolean",
869 | 														description:
870 | 															"Indicates that the client should redirect to the provided URL",
871 | 														enum: [true],
872 | 													},
873 | 												},
874 | 												required: ["url", "redirect"],
875 | 											},
876 | 										},
877 | 									},
878 | 								},
879 | 							},
880 | 						},
881 | 					},
882 | 				},
883 | 				async (c) => {
884 | 					const session = c.context.session;
885 | 					const provider = options.config.find(
886 | 						(p) => p.providerId === c.body.providerId,
887 | 					);
888 | 					if (!provider) {
889 | 						throw new APIError("NOT_FOUND", {
890 | 							message: BASE_ERROR_CODES.PROVIDER_NOT_FOUND,
891 | 						});
892 | 					}
893 | 					const {
894 | 						providerId,
895 | 						clientId,
896 | 						clientSecret,
897 | 						redirectURI,
898 | 						authorizationUrl,
899 | 						discoveryUrl,
900 | 						pkce,
901 | 						scopes,
902 | 						prompt,
903 | 						accessType,
904 | 						authorizationUrlParams,
905 | 					} = provider;
906 | 
907 | 					let finalAuthUrl = authorizationUrl;
908 | 					if (!finalAuthUrl) {
909 | 						if (!discoveryUrl) {
910 | 							throw new APIError("BAD_REQUEST", {
911 | 								message: ERROR_CODES.INVALID_OAUTH_CONFIGURATION,
912 | 							});
913 | 						}
914 | 						const discovery = await betterFetch<{
915 | 							authorization_endpoint: string;
916 | 							token_endpoint: string;
917 | 						}>(discoveryUrl, {
918 | 							method: "GET",
919 | 							headers: provider.discoveryHeaders,
920 | 							onError(context) {
921 | 								c.context.logger.error(context.error.message, context.error, {
922 | 									discoveryUrl,
923 | 								});
924 | 							},
925 | 						});
926 | 						if (discovery.data) {
927 | 							finalAuthUrl = discovery.data.authorization_endpoint;
928 | 						}
929 | 					}
930 | 
931 | 					if (!finalAuthUrl) {
932 | 						throw new APIError("BAD_REQUEST", {
933 | 							message: ERROR_CODES.INVALID_OAUTH_CONFIGURATION,
934 | 						});
935 | 					}
936 | 
937 | 					const state = await generateState(c, {
938 | 						userId: session.user.id,
939 | 						email: session.user.email,
940 | 					});
941 | 
942 | 					const additionalParams =
943 | 						typeof authorizationUrlParams === "function"
944 | 							? authorizationUrlParams(c)
945 | 							: authorizationUrlParams;
946 | 
947 | 					const url = await createAuthorizationURL({
948 | 						id: providerId,
949 | 						options: {
950 | 							clientId,
951 | 							clientSecret,
952 | 							redirectURI:
953 | 								redirectURI ||
954 | 								`${c.context.baseURL}/oauth2/callback/${providerId}`,
955 | 						},
956 | 						authorizationEndpoint: finalAuthUrl,
957 | 						state: state.state,
958 | 						codeVerifier: pkce ? state.codeVerifier : undefined,
959 | 						scopes: c.body.scopes || scopes || [],
960 | 						redirectURI:
961 | 							redirectURI ||
962 | 							`${c.context.baseURL}/oauth2/callback/${providerId}`,
963 | 						prompt,
964 | 						accessType,
965 | 						additionalParams,
966 | 					});
967 | 
968 | 					return c.json({
969 | 						url: url.toString(),
970 | 						redirect: true,
971 | 					});
972 | 				},
973 | 			),
974 | 		},
975 | 		$ERROR_CODES: ERROR_CODES,
976 | 	} satisfies BetterAuthPlugin;
977 | };
978 | 
```
Page 49/67FirstPrevNextLast